打开一个 Agent 集群(fleet)的仪表板,你会看到一个干净的数字:完成率,94%。在它下方是一系列运行记录,每条都标记着两种状态之一 —— 正在运行(running)或未在运行(not running)。那 6% “未在运行”的记录看起来完全一样。其中一些完美地完成了任务。一些在离完成还差两步时达到了步骤限制。一些捕获到了工具错误并放弃了。一些正确地判定任务是不可能的。还有一些则干脆断了思路,停止输出 token。
你的监控无法区分这些情况。它只知道流程不再运行了。它不知道 为什么 ,而“为什么”正是你在决定是否要呼叫(page)值班人员时唯一关心的事情。
这是大多数 Agent 部署中悄然存在的架构鸿沟。我们痴迷于对 Agent 的 路径 进行埋点 —— 每一次工具调用、每一个 token、每一次检索 —— 然后却让 Agent 的 退出 坠入深渊,变成一个单一的、无类型的事件。一次运行结束了。框架记录了它的结束。值班人员得到一个数字,它将健康的完成与礼貌的放弃混为一谈,而区分它们的唯一方法就是在事后打开追踪(trace)记录,逐一阅读。
一个可能以六种具有显著差异的方式结束的过程,需要说明它是以哪种方式结束的。这句话听起来显而易见,但几乎没有 Agent 框架强制执行这一点。
完成率掩盖了分布情况
首先看看“完成率”到底测量的是什么。它是达到终端文本输出的运行次数与所有启动运行次数的比率。这个指标基本上是不加思索地从请求/响应模式的世界中借用来的,在那个世界里,服务器要么返回 200,要么不返回,而 500 则是明确的错误。
Agent 打破了这种模式。一次 Agent 运行并没有单一的失败模式和成功的模式。它是一个光谱,而光谱的两端都是合理的结局:
它 完成 了任务并生成了正确的结果。
它在任务中途 耗尽了预算 —— 达到了最大轮次上限或实际时长超时。
它 卡在了一个工具上 ,该工具报错、超时或返回了垃圾数据,且 Agent 无法绕过它。
它 判定任务不可行 并主动停止,这通常是 正确 的决定。
它 升级给人类 处理,交接工作而不是盲目猜测。
它 遗弃 了运行 —— 停止取得进展、陷入循环、偏离主题,并在上述情况都没有明确发生的情况下陷入沉寂。
94% 的完成率告诉你,有 6% 的情况落在了后面五种。它不会告诉你这 6% 主要是干净的不可行判定(一个健康的系统拒绝了它不该做的工作),还是主要是遗弃(一个系统在默默地失败)。这两种解读需要完全相反的应对措施。一种意味着可以发布;另一种则意味着停止推广。原本应该驱动这一决策的指标,在结构上却无法区分它们。
这是多 Agent 失败文献中不断出现的同一个陷阱。加州大学伯克利分校的 Sky Computing Lab 通过手动标注大约 150 条追踪记录并扩展到 1600 多条,建立了一个 Agent 失败分类法 —— MAST。核心发现并不是 LLM 很笨,而是大多数失败都是 结构性 的:缺失终止标准、没有角色约束、没有人防范的崩溃。在他们的 14 种失败模式中,有几种 —— 步骤重复、提前终止、忽略其他 Agent —— 正是完成率使之变得不可见的东西。你无法管理一个你的核心指标看不见的失败类别。
“停止原因”已经存在 —— 只是在错误的层级
令人沮丧的地方在于:模型提供商已经解决了这个问题的某个版本。每个聊天补全(chat-completions)响应都带有一个 finish_reason,而且它是一个封闭的枚举(enum) —— stop、length、tool_calls、content_filter。Anthropic 的 API 也有自己的:end_turn、max_tokens、tool_use、refusal 等。Claude Agent SDK 的 ResultMessage 更进一步,携带了一个 subtype,用于区分 success、error_max_turns 和 error_during_execution。
因此,单次 LLM 调用 清楚地知道它为什么停止。问题在于,这个信号存在于你实际操作的对象下方一两层。在 40 轮 Agent 运行的第 14 轮出现 finish_reason: length,只告诉你单次模型调用达到了 token 上限。它没有告诉你 Agent —— 即包装了数十次此类调用、工具调用、重试和分支的编排循环 —— 是成功了、放弃了还是升级了。
终端原因必须被提升到循环边界。Agent 流程,也就是你编写 SLO 所针对的单元,必须是发出类型化退出信号的单元。而几乎没有编排层这样做。框架将控制权返回给调用者,调用者看到循环结束,模型层丰富的枚举早已被扁平化为“运行结束”。
这种扁平化就是 Bug 所在。修复方案不是建立新的基础设施,而是拒绝丢弃你已经拥有的信号。
一种类型化的终端原因协议 这种约束既微小又具体。每次 Agent 运行都必须通过从一个封闭的枚举集中发出确切的一个值来结束。不是自由文本状态。也不是可能被捕获也可能不被捕获的异常。这是一个类型化的原因,监控系统无需解析散文即可进行聚合。一个可行的基准:
completed — Agent 完成了目标并自述成功。
budget_exhausted — 在完成前达到了最大轮数、Token 预算或墙钟时间限制。
blocked_on_tool — 必要的工具失败(错误、超时、格式错误),且 Agent 无法继续。
judged_infeasible — Agent 判定任务无法或不应完成,并有意识地停止。
escalated_to_human — Agent 在不确定的情况下选择转交人工,而非继续运行。
abandoned — 运行结束但未明确断言上述任何情况。这是兜底项,且应该是最响亮的警报。
两条设计规则使该协议真正发挥作用,而不仅仅是装饰性的。
首先,abandoned 是默认值,且它是一个警报。 如果一次运行结束而编排层没有收到明确且有效的理由,那么原因就是 abandoned。你永远不要让一次运行在沉默中“成功”退出。沉默就是失败。这反转了通常的默认设置——大多数系统除非抛出异常,否则假设运行成功——而这种反转正是全部意义所在。一个循环、漂移并停止输出 Token 的 Agent 不会抛出任何异常。在旧的默认设置下,它被视为成功。而在这一规则下,它被视为“抛弃(abandonment)”,事实也确实如此。
其次,原因是由循环断言的,而不是由调用者推断的。 这种事后重建原因的做法很有诱惑力——“没有输出且轮数等于最大值,因此预算耗尽”。不要这样做。推断是脆弱的,它掩盖了你正试图消除的歧义。强制执行步骤限制的编排循环最清楚限制何时被触发;它应该在退出时明确说明。捕获超时的工具包装器知道工具是阻塞者;它应该传播 blocked_on_tool。原因应该在知识产生的地方产生。
有一个细节值得注意。completed 和 judged_infeasible 是由模型自述 的,而模型有动机进行美化——声称已完成而实际上是放弃了,因为“我完成了”听起来比“我做不到”要好。这是现实存在的,也是为什么该协议需要一个评估切片(见下文)来根据追踪记录(trace)检查原因。但自述仍然比不报告要好得多。一个可以审计的美化原因总好过一个无法审计的空值。
该协议能释放什么潜力 一旦每次运行都带有类型化的原因,三件以前不可能的事情就变得可行了。
你的 SLO 改变了形式。 你不再监控单一的完成率阈值,而是监控退出原因的分布 。Google 的 SRE 实践将 SLI 框架定义为“成功事件除以总事件”——但有了类型化的原因,你可以提出一个更敏锐的问题:有多少比例的运行以“健康”的终止状态结束?其中 completed、judged_infeasible 和 escalated_to_human 都算作健康状态。一个正确拒绝不可能任务的 Agent 是在履行职责。一个在不确定时转交人工的 Agent 也是在履行职责。将这些情况与 abandoned 混为一谈始终是一种分类错误。类型化协议让你不再犯这种错。
告警获得了先行指标。 完成率变动缓慢且滞后——当它明显下降时,糟糕的运行已经交付了。原因分布的形态 会先发生变化。budget_exhausted 占比上升意味着任务变得更难了,或者提示词的更改降低了 Agent 的效率——这在完成率下降之前就清晰可见。blocked_on_tool 占比上升直接指向依赖项,而不是 Agent 本身。abandoned 占比上升则是最高级别的火警:这意味着运行正以一种无人设计应对的方式失败。你可以对每一项独立告警,且每项告警都指明了对应的修复路径。
调试获得了分流过滤器。 标准的可观测性建议——记录完整追踪——产生的追踪记录在事故压力下往往过于庞大而难以阅读。类型化的终端原因是进入这堆乱草堆的索引。运维人员首先过滤出 abandoned 的运行,因为这些是未诊断的失败;并忽略 judged_infeasible 的运行,因为这些符合预期。原因并不能取代追踪。它告诉你哪些追踪值得打开。
该协议本身也需要一个护栏:一个用于检查 Agent 是否报告了“正确”终止原因的评估切片(eval slice) ——确保一个实际上放弃了的运行不会被标记为 completed,确保“不可行”的判定是合理的。如果没有这项检查,一个面临表现压力的模型会悄悄地将投降重新贴标为成功,而你只是在更高的一层重建了原来的盲点。
让退出成为一等公民事件 值得深思的不对称性:这两年来,我们非常擅长对 Agent 的“行为”进行监测——追踪(tracing)、跨度(spans)、Token 计费、可回放轨迹,一整个观测市场在 2025 年突破了 10 亿美元。但我们几乎没有在 Agent 的“结束方式”上投入任何精力。退出只得到了 1 个 bit:运行中,或者没有。
那个 bit 正是运维(on-call)的痛点所在。它是健康机群与悄悄投降机群之间的区别,而现在大多数团队如果不手动阅读追踪记录,根本无法分辨自己属于哪一种。类型化的终端原因协议不是科研问题,也不是新平台。它只是一个封闭的枚举、一个将沉默视为失败的默认设置,以及一个让 Agent 保持诚实的评估切片。
一个退出未类型化的 Agent 是一个故障模式保持不可见的系统,直到有人逐一读取追踪信息(通常是在客户首先发现问题之后)。对于大多数 Agent 团队来说,最廉价的可靠性升级就是让 Agent 用一个枚举词说出它是如何结束的——并将该词的缺失视为看板上最响亮的信号。
会员专享
余下内容仅对会员开放。 会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
— 完整文章,包含未公开存档的部分 — 可落地的工作框架,附带权衡与决策依据 — 新文章抢先看,先于公开发布 登录以继续阅读→ 随时取消 · 一次订阅,畅读全部