简介:Agent 的“结束”从来不是一个布尔值。本文从一次生产环境的幂等性两难切入,把循环的停止点拆解为模型流、工具批次、Turn、Driver 空闲与长期目标五层,并给出终止原因的细分枚举、状态机示意与工程落地建议,帮助你把重试、审计与恢复逻辑建立在正确的语义边界上。
一、引子:一个 504 引出的两难
把 Agent 接入工单系统,让它自主领取任务、执行并回报结果,是当前最常见的落地形态之一。设想这样一个场景:工单要求升级某个 SDK,Agent 改完代码、跑绿测试,随后调用接口创建 Pull Request。接口等待三十秒后返回 504。
此时页面上看不到 PR,但服务端可能已经创建成功,只是响应没能送回来。让 Agent 重试,明天可能多出一个内容完全相同的 PR;就此收工,又可能漏掉真正的失败。这是典型的“非幂等写操作 + 不确定超时”组合:一次超时把系统推入“结果未知”状态,停止决策直接变成了业务决策。
再往前一步:即使 PR 建好了,CI 还在排队。模型这一轮宣称“已经完成”,到底算不算完成?这类问题一旦进入生产环境,答案就必须由运行时的结构给出,而不是靠提示词碰运气。
二、while (hasToolCalls) 为什么不够用
朴素实现里,Agent 循环往往长这样:只要模型还在发起工具调用就继续跑,工具不调了就退出。这个结构在演示环境够用,进生产后会接连失守:
换句话说,“结束”不是一个单一时刻,而是要在几个不同的位置分别确认。与其追问循环有多少行,不如追问两件事:到底谁说了算,什么时候可以接着跑。
三、把“停”拆成五层
参考成熟 Agent 运行时的通用分层,一次运行中的停顿点大致有五层,它们是嵌套关系,外层结束不蕴含内层语义。
第一层:模型流结束。模型输出流关闭,只说明本次请求收到了结束信号——可能是正常结束,也可能撞上输出上限、触发错误,或被调用方取消。它对应常见的 finish_reason 字段,含义是“这次补全结束了”,而不是“任务结束了”。
第二层:工具批次结束。模型在上一轮发起的一批工具调用全部返回。若在此时退出循环,等于替模型做了“结果不重要”的假设,切断了它基于结果继续推理的机会。
第三层:Turn 结束。一个 Turn 真正结束要同时满足三个条件:工具结果已交回模型、本轮没有待处理的回复、输入队列里没有新消息。用户中途塞进来的 steering,或工具通过回调补进来的上下文,都会把这一轮重新拉起来。
第四层:Driver Activity 结束。当前 Agent 暂时没有下一条可执行的 Turn,回到 idle。这一层只回答“这个 Agent 实例现在忙不忙”,不关心任何业务目标是否达成。
第五层:长期 Goal 结束。持久化的目标状态变为 complete、blocked 或 paused,才算目标级终止。Agent 此刻 idle,不代表目标已完成——调度器下一次巡检时,仍可能通过 follow-up 机制把它叫回来继续推进。
| 层级 | 结束意味着什么 | 不意味着什么 |
|---|---|---|
| 模型流 | 本次补全收到结束信号 | 工具已执行、任务完成 |
| 工具批次 | 上一批工具调用全部返回 | 模型已确认结果、Turn 结束 |
| Turn | 结果已回灌、无待处理消息 | Agent 空闲、目标完成 |
| Driver | 暂无下一条 Turn,回到 idle | 目标已完成 |
| Goal | 持久状态进入终态 | ——(这是最外层的终止) |
四、终止原因:一个不能偷懒的枚举
如果只记录“成功/失败”两种结束,审计与重试都会失真。至少应区分以下几类:
TurnEndReason:COMPLETED # 模型给出最终答复,无待续事项BLOCKED # 依赖外部条件,暂时无法推进MAX_TOKENS # 撞上上下文或输出上限,本轮被截断ABORTED # 运行时收到取消指令,可执行清理ERROR # 本轮执行中发生错误INTERRUPTED # 进程中断后由恢复层补记,无清理机会
最值得展开的是 aborted 与 interrupted 的区别:
混淆两者的代价非常具体。做副作用审计时:aborted 的 Turn,你知道取消前执行到哪一步;interrupted 的 Turn,只能靠持久化日志反推。决定是否重试时:aborted 通常意味着“有人不想要这个结果”,盲目重试违背用户意图;interrupted 则往往值得在对账后安全续跑。blocked 与 max-tokens 也各有含义:blocked 表示依赖外部条件暂时无法推进,适合挂起等待;max-tokens 表示本轮被截断,续跑前需要考虑压缩上下文或分段输出。
五、谁按下停止键:四种决策主体
把五层结束点再抽象一步,会发现停止的决策权分散在四类主体手里:
设计运行时时,一个有用的检验标准是:任何一处终止,都能回答“是这四类主体中的哪一个、基于什么信号做出的决定”。答不出来,说明状态记录有缺口。
六、工程落地:状态机示意与五条建议
核心循环的示意如下(伪代码):
loop:resp = call_model(messages) # 第一层:模型流if resp.finish_reason == "length":return MAX_TOKENSif not resp.tool_calls:return COMPLETED # 模型不再发起工具results = run_all(resp.tool_calls) # 第二层:等本批工具全部返回append(messages, results) # 结果回灌给模型if has_pending_input(): # 第三层:steering/工具注入的新消息continue # Turn 被重新拉起continue # 否则模型需再看一眼结果# 第四层:循环退出且无新 Turn → Driver 回到 idle# 第五层:Goal 由持久状态决定,调度器巡检时可再次唤醒
配套五条实践建议:
七、结语
Agent 循环的难点从来不是“怎么跑起来”,而是“由谁、在哪个语义层级、以什么理由停下来”。模型流、工具批次、Turn、Driver、Goal 五层各管一段,彼此之间不能互相替代;终止原因的细分,则直接决定了审计与重试的正确性。当你的 Agent 开始在生产环境自主执行写操作时,把这些边界写清楚,比堆更多的提示词更能决定系统的可靠性。