Skip to content

15|继续工作:没做完怎么办

上一篇的停止条件覆盖了正常完成和异常停止。但有一种情况它没覆盖:模型没出错,也没回答完,只是被截断了。

报告写了一半,输出长度到了上限,模型停下。这时候不是错误(上一篇讲过,输出 token 耗尽是 incomplete 状态不是异常),也不是正常完成(任务没做完)。本篇讲继续工作:被截断时怎么让模型把剩下的写完,又不陷入无限继续。

先分清三种 token

推理模型的 token 不是单一概念,预算至少分三块。混在一起算会漏掉最大的一项。

AIOps 概念图:三种 token

一次请求的用量分输入 token、输出 token、推理 token。输入 token 是请求体本身(系统指令、历史、工具定义)。输出 token 是模型可见的回答。推理 token 是模型内部思考消耗,藏在 response.usage.output_tokens_details.reasoning_tokens 里,和输出 token 一样按输出价计费,但不出现在可见回答里。

python
used_input = response.usage.input_tokens
used_output = response.usage.output_tokens
used_reasoning = (
    response.usage.output_tokens_details.reasoning_tokens
    if response.usage.output_tokens_details else 0
)
total_used = used_input + used_output + used_reasoning

reasoning.effort 越高推理 token 越大,账单和延迟同步上升。漏算推理 token 是成本预估偏低的常见原因——表面看输出只有几百 token,实际加上推理可能翻几倍。

这三块共享一个上界:context window。窗口是输入+输出+推理的总容量上限,不是三者各有各的额度。推理占多了,留给输入历史和输出回答的空间就少。

怎么判断被截断了

模型停下来的原因不全是"写完了"。Responses API 里,被截断的停止体现在响应状态上:

python
def is_truncated(response) -> bool:
    return (
        response.status == "incomplete"
        and response.incomplete_details
        and response.incomplete_details.reason == "max_output_tokens"
    )

response.status == "incomplete" 加上 reason == "max_output_tokens",明确表示被输出上限截断了。这是触发继续工作的信号。

要和"主动完成"区分开:response.status == "completed" 是模型自己觉得写完了,强行继续可能画蛇添足。两种处理完全不同,先查 status 再决定动不动。

继续不是追加一句 user 消息

这是继续工作最容易写错的地方。

AIOps 概念图:沿原响应继续

朴素的做法是追加一句"请继续"作为 user 消息:

python
# 错误做法
state.messages.append({"role": "user", "content": "请继续"})
response = client.responses.create(model=settings.model, input=state.messages)

这对推理模型有问题。模型在上一轮产生了内部推理状态,朴素追加会丢失这条推理链——模型从零重新思考,可能重复已输出内容,甚至换个方向重新写。

正确做法是用 previous_response_id 续接,服务端托管了上一轮的状态,包括推理链:

python
CONTINUE_PROMPT = "上文因长度限制中断,请继续完成剩余内容。保持相同的格式和结构。"

def inject_continue(response, client, settings):
    follow_up = client.responses.create(
        model=settings.model,
        instructions=settings.instructions,   # 续接时必须重发
        input=CONTINUE_PROMPT,
        previous_response_id=response.id,     # 服务端回放上一轮,含推理状态
        max_output_tokens=settings.max_output_tokens,
    )
    return follow_up

无状态场景(store=false 或要自己控上下文)改用手工回放——把上一轮的 response.output items 放进 input,再追加继续提示。OpenAI 提供 encrypted reasoning items,在无状态下把推理状态加密回传,避免推理链断裂。

核心是让模型看到"自己之前写到哪了、想了什么",而不是从空白开始。继续提示也要明确两点:为什么继续("上文因长度限制中断")、怎么继续("保持相同格式"),防止模型换格式或重写全文。

防止无限循环

继续机制最大的风险是无限循环:模型每次继续写一点点,系统每次都触发继续,永远停不下来。

python
class ContinueTracker:
    def __init__(self, max_continues=3):
        self.max_continues = max_continues
        self.continue_count = 0
        self.last_output_length = 0
        self.stagnant_count = 0

    def should_continue(self, current_output):
        self.continue_count += 1
        if self.continue_count > self.max_continues:
            return False   # 次数上限

        if len(current_output) - self.last_output_length < 50:
            self.stagnant_count += 1
            if self.stagnant_count >= 2:
                return False   # 增量停滞
        else:
            self.stagnant_count = 0

        self.last_output_length = len(current_output)
        return True

三种情况停止继续:次数上限(连续继续超过 max_continues 次)、增量停滞(连续两次继续增量不足 50 字符,说明模型无话可写)、预算耗尽。

和错误恢复的区别

继续工作和上一篇的错误恢复看起来都是"让模型再试一次",但本质不同。

错误恢复处理的是异常(BadRequestError 等),目标是消除错误让请求成功。继续工作处理的是响应状态(incomplete),目标是让模型输出更多内容。错误恢复是故障修复,继续工作是正向推进。两层机制可以共存:先错误恢复,恢复成功后如果被截断,再触发继续工作。

继续机制的成本通常比一次性给足输出空间更高——每次继续是一次新 API 调用,固定开销付多次。最优策略是在预算允许范围内给 max_output_tokens 足够空间,尽量减少触发继续。生成固定格式短报告时关掉继续机制,生成长篇排查报告时开启并设 max_continues=2

继续之后

到这里循环与控制设计的核心都讲到了:两层循环、错误处理、停止条件、继续工作。但还有两个执行细节要补:调用前的消息预处理,和调用后的工具并发执行。

下一篇讲消息预处理管线。循环每轮调 API 前,消息要先经过从轻到重的处理——这和第 10 篇的压缩策略呼应,但角度不同,是调用前的固定管线。