“完成!”不是返回码:为什么智能体完成需要结构化信号
Agent 以“全部搞定——如果需要任何修改请告诉我!”结束它的回合,而你的编排器必须决定是将工单标记为已解决、启动下一次交接,还是重试。这句话不是一个返回码。它只是一个训练出来的、为了在聊天结束时听起来很贴心的礼貌语,而它下游的每一行自动化代码都继承了这种模糊性。那些将此视为解析问题的团队会编写捕获 \b(done|complete|finished)\b 的正则并收工。而那些在生产环境中运行 Agent 的团队最终会明白,完成是一个事件,而不是一种情绪。
失败模式通常是双峰且枯燥的。要么是 Agent 在未完成时宣布完成——过早终止——而编排器愉快地在一个半成品产物上推进工作流。要么是 Agent 确实完成了,但表述方式与检测器不匹配(“我已经落地了更改,尽管边界情况的测试仍然不稳定”),编排器于是发起重试,导致重复工作、产生重复的副作用,有时甚至会推翻成功的第一次尝试。这两种模式都会静默地退化。在有人阅读 Trace 并注意到 Agent 说了“我想这些就是全部了”而计费系 统将其视为一次提交(commit)之前,任何仪表盘都不会显示异常。
解决方法不是更智能的解析。而是给 Agent 一个结构化的终止方式——一个具有枚举状态、原因代码和你的流水线可以路由的句柄(handle)的“完成工具(done-tool)”——并将编排器改为等待该事件,而不是监听聊天流。
“完成”的表面积
基于散文(prose)的完成检测会随着模型的词汇量而扩展,而词汇量实际上是无边界的。以下是从业者仅从一个季度的生产日志中就不得不处理的部分短语清单:“all set(全部就绪)”、“should be good to go(应该没问题了)”、“that should do it(这样应该就行了)”、“I've wrapped that up(我已经收尾了)”、“done, pending your review(完成,等待你的审核)”、“I'll leave it here for now(我暂时先到这里)”、“let me know if you need more(如果需要更多请告诉我)”、“that's everything(就是这些了)”、“nothing else to add(没别的要补充了)”、“finished — with a small caveat(完成了——有一点小瑕疵)”。前五个是明确的成功。后五个都很模糊:pending your review 是交接,for now 是暂停,with a small caveat 是部分完成。你的正则对它们的捕获是一视同仁的。
问题不在于单个短语难以分类——你可以在数据集上微调一个分类器,准确率能达到 90%。问题在于,表面积(surface area)每次在模型迭代时都会扩大,因为解码时的行为会随 Checkpoint 漂移,以前罕见的短语(“我接下来会……”)可能一夜之间变得常见 。每次漂移都是你完成检测器的一次静默回归。你发布了一个提示词更新,Agent 换了个新的告别语,三周后有人发现重试率上升了 15%。
结构化完成不具备这种特性。一个名为 task_complete 且 status: "success" 的工具调用只有唯一一个入口。Agent 要么调用它,要么不调用。没有表述漂移,也不会像自然语言那样在工具参数中模棱两可,而且解析过程是模式检查(schema check)而非语言模型。
关于多 Agent 系统为何失败的研究支持了这一点:在产生 MASFT 分类法的 150 多个 Trace 调查中,过早终止(Agent 在完成子任务前宣布“完成”)约占失败案例的 6%,该论文专门将其归因于终止条件从未明确过的星形拓扑(star-topology)系统。这种拓扑结构掩盖了 Bug。而结构化完成则让 Bug 无处遁形。
散文式完成失败的三种表现形式
第一种是假阳性(False Positive)——编排器认为 Agent 已完成,但实际上并没有。这种代价最高,因为下游状态会推进。一个常见的模式是:Agent 捕获了一个异常,写了一段包含“completed”单词的总结(“completed initial attempt; write failed(完成了初步尝试;写入失败)”),然后正则抓取到了它。工作流继续推进。产出物缺失。Bug 会在两个步骤后显现,当消费者读取一个空行并报错时,但 Trace 指向的是消费者,而不是那个从未写入成功的 Agent。
第二种是假阴性(False Negative)——Agent 实际上已经完成了,但编 排器发起了重试。这就是重复副作用的情况。Agent 工作流中的重试很少是幂等的,因为 Agent 在第一遍推理时填充了上下文,并在第二次运行时改变了计划。重试可能会重新发送一封邮件、重新打开一个 PR,或者重新运行一个已经应用过的 SQL 迁移。你最终会在 Jira 中得到两个声称解决同一问题的工单,而它们都不是权威记录。
第三种是静默退出(Silent Quit)——Agent 的回合结束了,但没有任何完成语。最后的助手消息是一段推理过程、一个部分计划,或者一个 Agent 不知道如何恢复的工具错误。编排器没有匹配到“完成”的正则,也没有匹配到“失败”的正则,通常只是记录下“Agent 回合结束”然后等待。在生产环境中,这成为了长尾事故中最常见的一类:数小时未被发现的停机时间,流水线名义上是健康的,但没有任何进展。一个运营内部 Agent 的团队在一次公开回顾中报告了长达六个小时的静默失败,直到人工察觉——Agent 遇到了频率限制,总结了错误并结束了回合,而编排器没有任何信号来进行重试。
这三种形式都有相同的根源:Agent 与编排器之间的协议是自然语言约定,而不是类型化接口。Agent 可以随心所欲地表达“完成”,而编排器被迫从有损的总结中逆向推导意图。
结构化完成合约
一个可用的合约包含三个字段,它们都很重要。去掉其中任何一个,合约就会退化为散文:
status—— 一个枚举值。典型值:success、partial、failed、needs_input、aborted。不是自由文本。编排器的状态机针对每个状态值都有且只有一个转换,歧义属于 schema 违规,而非主观判断。reason_code—— 作用域限于该状态的短机器可读代码。例如failed: validation_failed、failed: tool_error、needs_input: ambiguous_requirement、partial: budget_exhausted。编排器使用它来路由:tool_error进行退避重试,validation_failed升级到人工审核队列,ambiguous_requirement在 UI 中弹出澄清提示,budget_exhausted设置检查点并调度恢复。resumable_handle—— 一个不透明的令牌,通常是指向检查点状态的指针(如果任务已终止则为null)。重试路径不会重新发送整个对话;它从该句柄恢复。如果没有它,部分完成将强制完整重启,重试的成本等同于新运行的成本。
一个完整的信号看起来像 task_complete({status: "partial", reason_code: "budget_exhausted", resumable_handle: "ckpt_a7f2", summary: "已查看 12 个文件中的 8 个;剩余 4 个已加入队列。"})。Summary 仍然是散文 —— Agent 仍然是一个语言模型,人类也会阅读这个产物 —— 但编排器据以行动的字段都是类型化的。
框架提供商一直向这种形态靠拢。OpenAI 的 Agents SDK 在 Agent 生成匹配声明的 output_type 且不再进行工具调用时终止其运行循环,这是一种结构化检查而非字符串匹配。微软的 Agent Framework 将交接实现为专用的工具调用,而非散文提示;编排器对工具做出反应,而不是对段落做出反应。LangGraph 将终止建模为向显式结束节点的图转换。在每种情况下,做法都是相同的:将“Agent 说它完成了”替换为“Agent 触 发了一个类型化事件”。
迁移对话型 Agent
目前生产环境中的大多数 Agent 最初都是以对话形式进行原型设计的。系统提示词告诉它们要帮助用户,当它们停止生成 token 时轮次结束,下游的某些脚本会读取最后一条消息。转向结构化完成在很大程度上是机械性的,但这种迁移有两个棘手的难点。
第一是 Agent 必须实际调用该工具。仅仅在工具集中添加一个 task_complete 工具是不够的 —— 经过 RLHF 训练的模型默认会使用训练好的告别语(“我已经完成了 X,如果还需要其他帮助请告诉我”),除非系统提示词强制要求使用该工具,并通过示例强化这种行为。迁移第一周常见的失败是:一半的运行能正确调用工具,而另一半则以散文结尾,因为模型回到了对话习惯。解决方法是建立一个解析失败路径,将“轮次结束但未调用完成工具”视为一种故障模式 —— 通常是带有更强系统提示词引导的重试 —— 而不是假装该轮次已成功。
第二是编排器必须停止从助手流中读取意图。一旦合约被类型化,任何通过窥探消息内容来决定下一步该做什么的逻辑都是一种退步。诱惑在于保留旧的正则表达式“作为兜底” —— 不要这样做。兜底方案会掩盖真实信号中的故障,你永远不会发现 Agent 已经停止调用工具,直到仪表盘骗了你整整一个星期。如果缺失 task_complete,编排器应将其视为事故,而不是将其粉饰太平。
在可观测性方面,结构化完成转化为真正的事件流。你得到的不再是“第 47 轮以一条包含‘完成’字样的 180 字符消息结束”,而是“task_complete 带有 status=partial,reason_code=budget_exhausted,resumable_handle=ckpt_a7f2,trace_id=...”。关联 ID 自然流动。SLO 变得可以定义:p95(time_to_task_complete)、rate(status=failed)、rate(missing_completion_event)。你无法从散文转录中构建出这些指标。
成本与收益
结构化完成的成本是真实存在的,但很小:一个工具 schema、几个枚举值、一次系统提示词更改,以及一小段用于根据状态分发的编排器代码。第一周的运行会暴露出一类以前处于静默状态的潜在故障 —— 没有完成事件就结束的轮次、路由不清晰的原因代码、团队尚未建模的部分完成情况。那不是退步;那是你之前存在的 Bug 表面第一次变得可见。
收益是 Agent 与其周围一切之间更清晰的边界。具有结构化完成的 Agent 是一个具有类型化接口的黑盒,这意味着你可以更换模型、调整提示词和升级框架,而无需编排器重新学习 Agent 的习惯表达。可观测性不再是转录考古学。重试变得受限且幂等,因为可恢复句柄取代了对话回放。静默失败 —— 生产事故中比任何其他类别都持久的故障模式 —— 默认变得响亮,因为缺失完成事件属于 schema 违规,而你的流水线知道如何对 schema 违规进行告警。
“完成”是一种意图。你的流水线需要一个事件。停止混淆两者的最廉价方法,就是不再让 Agent 用语言表达完成,而是开始让它发出系统专为读取而构建的信号。
- https://openai.github.io/openai-agents-python/running_agents/
- https://learn.microsoft.com/en-us/agent-framework/workflows/checkpoints
- https://arxiv.org/pdf/2503.13657
- https://orq.ai/blog/why-do-multi-agent-llm-systems-fail
- https://galileo.ai/blog/agent-failure-modes-guide
- https://temporal.io/blog/of-course-you-can-build-dynamic-ai-agents-with-temporal
- https://dev.to/bobrenze/ai-agent-silent-failures-what-6-hours-of-undetected-downtime-taught-me-about-monitoring-3ja8
- https://medium.com/@deolesopan/data-contracts-for-agents-keep-tools-and-schemas-stable-as-systems-evolve-8af6f3e024ba
- https://learn.microsoft.com/en-us/agent-framework/user-guide/workflows/orchestrations/handoff
- https://www.chanl.ai/blog/memory-silent-failure-mode
