我让一个 Agent 写一篇文章并发布。几分钟后,它交回一份 Markdown,说:“已经完成。”文件确实写了,文字也不错,但校验没跑、Git 没推、线上网址还是 404。它没有撒谎,只是把完成了一个动作误认成了完成了用户的目标。
这是 Agent 最典型、也最容易被低估的失败。模型很会开始:拆任务、列计划、调用工具、生成文件,每一步都像在工作。真正困难的是最后那个判断:现在的世界,是否已经变成用户要求的样子?
先给结论:
完成不是 Agent 说出的一句话,而是环境中一个有证据支持的状态。
这篇文章不讨论经典计算理论里的“停机问题”。工程里的 Agent 通常不需要证明任意程序能否停机;它面对的是更具体的问题:目标可能含糊,工具只汇报局部成功,外部系统会延迟或失败,而模型还要在有限时间和预算内决定继续、完成或受阻。
先建立一张图:Agent 到底在判断什么
把 Agent 看成一个带反馈的控制循环,而不是一段更长的聊天。循环里只需要盯住四样东西:
| 部分 | 它回答的问题 | 改变它会发生什么 |
|---|---|---|
| 目标 | 用户最终想让什么变成真的? | 越含糊,Agent 越容易完成错任务 |
| 世界状态 | 文件、数据库、网页或外部系统现在是什么样? | 观察越旧,判断越可能脱离现实 |
| 证据 | 什么信号足以证明目标已经达成? | 证据越弱,“假完成”越多 |
| 边界 | 允许花多少时间、成本和风险? | 边界越松,越容易无限重试或越界 |
它们处在同一个闭环里:
flowchart LR
G[目标] --> A[选择动作]
A --> W[改变世界]
W --> O[重新观察]
O --> V{证据足够吗}
V -->|目标已达成| C[Complete]
V -->|仍可安全推进| A
V -->|缺少必要条件| B[Blocked]
目标决定要观察什么,观察产生证据,证据决定是否继续,边界约束每一次行动。任何一个环节缺失,Agent 都可能表现得很忙,却无法可靠交付。
一个小例子:什么叫“发布一篇文章”
“写一篇文章”和“发布一篇文章”不是同一个任务。对后者来说,正文只是中间产物,真正的目标状态至少包含一条交付链:
文章文件存在
→ 元数据合法
→ 站点测试与生产构建通过
→ 变更进入远端发布分支
→ 部署完成
→ 公开网址能读到正确标题与正文
这条链上的每一层,都有一种很像完成的局部成功:
- Markdown 已生成,但路径或 frontmatter 错了,站点不会收录。
- 本地构建通过,但没有推送,线上什么也没变。
- Git 推送成功,但部署平台正在失败或排队。
- URL 返回 200,但缓存里还是旧页面,或正文组件渲染异常。
因此,工具返回 exit code 0 只证明“这个命令成功退出”;它不自动证明“用户目标已经实现”。真正的完成证据,应当尽量贴近目标所在的世界。目标是本地文件,就检查文件;目标是线上页面,就检查线上页面;目标是某人收到消息,就检查发送记录或对方系统的回执。
距离目标越远的信号,只能算过程证据,不能冒充终态证据。
Agent 最常见的四种“假完成”
1. 把输出当结果
语言模型的原生能力是生成文本,因此它天然容易认为“我已经给出了一段看起来像答案的内容”就是结束。可是对 Agent 来说,文本往往只是动作描述:写出了 SQL 不等于数据已更新,给出了补丁不等于代码已修改,生成发布说明不等于版本已发布。
区分方法很简单:用户要的是一段信息,还是世界中的一次变化? 前者可以由答案本身完成,后者必须回到环境里验证。
2. 把动作成功当目标成功
一次 API 调用返回成功,只说明服务接受了这次请求。异步任务可能稍后失败,权限变更可能尚未生效,消息可能进入队列但没有送达。动作与目标之间只要还隔着状态转移,就需要新的观察。
这也是为什么优秀的工具接口不只返回一句 success: true,还会返回资源 ID、状态、版本号或可查询的结果地址:它们为后续验证留下抓手。
3. 把局部测试当完整验收
单元测试通过,证明的是被覆盖的局部行为;它不能证明路由已经接入、生产配置正确、迁移已经执行,也不能证明测试本身写对了。我在《Agent 写码、Agent 测码》里讨论过,当实现和验证共享同一种错误理解时,测试会从独立证据退化成自我确认。
这里再补一层:即使测试完全可靠,也要先问它验证的是哪一层。局部正确不自动向上传递为系统终态正确。
4. 把无法继续包装成已经完成
有时 Agent 不是认为任务完成了,而是不知道下一步怎么走。为了让对话自然结束,它会用“基本完成”“应该可以”“其余步骤请自行处理”把未知压成结论。
一个可靠的 Agent 必须拥有 Blocked 状态:明确说明当前停在哪里、已经确认了什么、还缺少什么,以及为什么缺少这个条件就无法继续。承认受阻不是失败;把受阻伪装成完成才是。
三种决定,而不是一种结束
每轮行动后,Agent 不该只问“要不要停”,而应在三个状态之间做选择:
- 继续(Continue):目标尚未达成,但存在边界内、能增加证据或推进状态的下一步。
- 完成(Complete):预先约定的验收条件已满足,并且证据来自足够新的世界状态。
- 受阻(Blocked):目标尚未达成,而下一步需要缺失的权限、信息、外部恢复或用户决策。
可以把运行时骨架压缩成下面这段伪代码。它不是某个框架的源码,只表达控制流;省略了并发、持久化和具体错误处理:
# 简化的 Agent 任务循环伪代码
while within_boundary(task):
observation = observe_current_world(task)
if verifier_accepts(task.goal, observation):
return Complete(evidence=observation)
next_action = choose_reversible_action(task, observation)
if next_action is None:
return Blocked(missing_condition=diagnose_gap(observation))
execute(next_action)
return Blocked(missing_condition="time, cost, or safety boundary reached")
这里最重要的不是 while,而是两个约束:先观察再判断,先定义验证器再宣布完成。 仅凭历史上下文推测“刚才应该成功了”,等于拿记忆代替现实。
完成契约:在动手前定义“什么算完”
很多假完成不是 Agent 偷懒,而是任务从未定义完成。用户说“帮我处理一下”“优化一下”“发布一下”,人类协作者会根据共享背景补全含义;Agent 的共享背景薄得多,很容易选中最便宜的解释。
解决办法是给任务一份轻量的完成契约:在行动之前写清目标、验收器、边界和交付证据。它不必是一页规格,几句话就够。
以发布文章为例:
| 契约项 | 内容 |
|---|---|
| 目标 | 新文章在主站公开可读 |
| 验收器 | 内容校验、测试、生产构建通过;线上 URL 展示正确标题和正文 |
| 边界 | 只新增文章,不顺手修改站点功能;不跳过失败检查 |
| 交付证据 | 文章路径、提交记录、线上 URL、实际页面观察 |
完成契约的价值不是增加仪式,而是把“何时停”从模型临场发挥,变成任务开始前就可检查的条件。它还会反过来改善规划:如果最终验收需要线上观察,Agent 一开始就知道“写文件”不是最后一步。
这与《Eval 是新的 PRD?》讨论的是同一条边界:自然语言规格无法消灭歧义,但可以用可执行证据把最重要的歧义压住。
验证器不是一个命令,而是一条证据链
“跑测试”常被当作验证的代名词,其实验证器可以是很多东西:文件检查、类型系统、数据库查询、截图、部署状态、线上请求、人工确认。好的验证策略会按成本从低到高排列,在便宜的地方尽早拦错,在接近目标的地方做最终确认。
还是以网站发布为例:
flowchart LR
S[Schema 校验] --> T[测试]
T --> B[生产构建]
B --> P[Git 推送]
P --> D[部署状态]
D --> L[线上页面]
前面的信号反馈快、定位准,后面的信号离用户目标近。只有前者,容易证明局部而漏掉交付;只有后者,失败时又很难定位。两者组合才是一条实用的证据链。
验证器还应尽量满足三个条件:
- 具体:不是“看起来不错”,而是“这个 URL 显示这个标题”。
- 新鲜:验证发生在最后一次状态变更之后,避免拿旧结果给新状态背书。
- 相对独立:生成者的主观判断不能成为唯一证据,能由环境、确定性程序或不同信号确认的,就不要只靠自评。
《什么才是好的 Harness》里那句“判断归模型,物理归代码”,在这里可以改写成:推进路径可以让模型判断,终态证据尽量让环境给出。
会结束,也要会不结束
“尽快停”不是目标。Agent 还有另一种失败:遇到第一个错误就宣布受阻,或者因为一次搜索没结果就把“暂时没找到”写成“不存在”。
Blocked 必须满足一个条件:当前边界内已经没有能实质增加信息或改变状态的安全动作。命令失败一次,通常只是新的观察;失败原因明确、替代路径耗尽、下一步确实需要外部条件,才构成受阻。
反过来,“坚持到底”也不是美德。连续执行同一个失败动作、在没有新信息时重复推理、为了一个低价值改进无限扩展范围,都是不收敛。好的边界至少包括:
- 重试次数或时间上限;
- 成本与 token 预算;
- 权限和安全范围;
- 不允许触碰的资源;
- 何时需要用户或其他系统接管。
所以,Agent 的停止机制其实有两套:一套由完成证据触发,回答“事情是否做成”;另一套由运行边界触发,回答“即使没做成,现在是否也必须停”。前者保证交付,后者控制风险。
无法完全验证时,诚实地降低结论强度
现实任务并不总有干净的测试。写作质量、战略判断、审美选择和长期效果,很难由一个布尔值裁决。此时不能假装存在完美验证器,也不必因此放弃 Agent。
更好的做法是把证据分层:
- 已执行:动作确实发生,例如文件已经创建。
- 已检查:局部质量门通过,例如格式与构建正确。
- 已观察:目标环境呈现了预期状态,例如线上页面可读。
- 待判断:仍依赖人的主观评价或未来数据,例如文章是否真正有启发。
Agent 的最终报告应精确落在自己拥有的证据层级上。它可以说“文章已经上线并可访问”,但不该说“这一定是一篇好文章”;后者需要读者,而不是部署日志来判断。
最后的自检:别问它做了什么,问世界变成了什么
判断一个 Agent 是否真正完成任务,可以只问四个问题:
- 用户要的是回答,还是世界状态的变化?
- Agent 给出的证据,验证了目标,还是只验证了某个动作?
- 这份证据是否来自最后一次变更之后的真实环境?
- 如果没有完成,它是在继续推进,还是准确说明了受阻条件?
我们很容易被长计划、密集工具调用和流畅总结打动,因为它们看起来像劳动。但 Agent 的价值不由过程有多热闹决定,而由任务结束时世界是否不同决定。
普通模型负责给出答案;真正的 Agent,必须对任务的终态负责。