深入分析AI Agent调用工具返回成功状态码但实际操作未完成的问题,提出在wrapper层验证副作用再将结果传给模型的解决方案。
一个 Agent 被要求发送一封付款确认邮件。它调用了 send_email 工具。工具返回 {"status": "queued", "message_id": "msg_abc123"}。Agent 写入用户:Done. Confirmation email sent. 两小时后,一封支持工单来了。客户从未收到邮件。提供商的队列在区域性故障切换期间丢失了消息,从 Agent 到收件箱之间的每一层都没有人对"这封邮件是否到达了客户"这一主张持有所有权。每一层都收到了一个 token,转发了它,然后宣布自己这部分完成了。
事故复盘时的本能是责怪模型。它以完全自信的语气报告了虚假信息,所以一定是模型产生了幻觉。并没有。模型读取到了一个成功信号,并忠实地转发了它。这个信号在模型看到它之前就是一个谎言,而且它之所以是谎言,原因与语言模型毫无关系。
一次工具调用的 200 意味着请求已被接受,而不是效果已经发生。把完成所有权放进包装层:在"success"这个词到达模型之前,验证副作用。模型无法从状态码推断出完成,而且它会报告出该 token 所暗示的准确置信度。
这与"验证是一个循环,而不是一个断言"以及"在可以的地方用确定性,在必须的地方用判断"属于同一套第一性原理路线。验证即循环认为,在一个生成系统中,你必须检查 Agent 的工作,而不是假设它正确。本文讲的是一个更窄、更隐蔽的检查缺失之处:不是模型的推理,而是它下面的工具边界——在那里状态码被误认为是事实。这也是我在 RPC vs NATS 中写过的一个古老的分布式系统问题的直接后裔:谁拥有完成权。Agent 技术栈重新发现了这个问题,而且在大多数情况下得到了错误的答案。
沿着一个典型技术栈向上追踪 token,你会发现三个独立的地方,一个诚实但不完整的信号被提升为了虚假信号。
工具 API 层。工具返回 200,是因为请求被接受了,而不是因为效果被观察到了。邮件提供商在入队时返回 200。异步任务端点返回 202 和一个 job id。最终一致性写入在写在一个节点上 durable 后就返回 200,但此时还没有传播到任何后续读取能找到它的地方。在所有这些情况中,200 是关于"收到"的事实陈述,对结果什么都没说。
Agent 框架层。框架看到一个 2xx,将工具调用标记为成功,并在对话中追加一个成功条目。这一层造成了真正的损害,因为它把两个完全不同状态——请求被接受与效果被观察到——压缩成了一个词。框架有状态码,却丢弃了它所有的含义。
模型层。模型在工具结果中读到 success,并生成 Done. Email sent,置信度很高。置信度不是缺陷。这是给定它收到的 token 时的正确响应。让一个人转发一条标记着 SUCCESS 的消息,他也会将其作为成功来转发。模型是这条在它之前三步就已经撒谎的链上唯一诚实的环节。
客户,在这一切的末端,得到了一份与可观察现实相矛盾的自信报告。而事故回顾指向了唯一根据其输入行为正确的组件。
潜在的问题比 Agent 更古老,比 HTTP 更古老。任何时候,当一个操作跨越边界,就必须有人来回答"它真的发生了吗"。
RPC 的经典创伤是调用者在调用中途收到网络错误。操作运行了还是没有?网络错误不是"没有运行"。它是一个未知。操作可能在服务器上已经提交了,而它的确认在返回途中丢失了。没有幂等 key 来安全重试,也没有写后读检查来确认效果,调用者确实无法判断,而且它尝试的任何恢复都是在猜测。
在每一个可靠系统中,走出困境的方法是指定一个所有者。要么调用者轮询直到亲眼看到效果,要么接收者提交后只在那时才报告回来。正如我在 RPC vs NATS 一文中论述的,发后即忘 messaging 把所有权推给了调用者,请求-响应把它推给了接收者。两种放置都没有错。永远错的是对"哪一层持有它"含糊不清,因为这意味着答案是"没有人",而没有人正是产生幽灵确认邮件的元凶。
AI 工具调用继承了这个问题,而且在匆忙将模型连接到真实行动的过程中,大部分都跳过了决定谁拥有完成权的那个环节。包装层信任 API 的 200。API 信任它的调度器。调度器信任 worker。Worker 在入队时记录成功。Token 一直传送到模型并最终到达用户,而没有任何一层承诺过观察到了这个效果。
修复从拒绝让状态码代替结果开始。每个工具包装层都必须声明它运行在三种模式中的哪一种,而且这个声明存在于包装层中,而不是在模型的头脑里。
这是包装层级别的纪律,有一个具体原因:模型无法从 200 派生模式。两个工具可能返回字节完全相同成功 payload,而一个有 durable 地应用了效果,另一个只是将其排队了。这种区别只存在于包装层拥有的知识中,而状态码不携带这种知识。如果包装层不编码模式,那么到了模型看到结果时信息已经丢失,再多的 prompt 也无法恢复它。这是技术边界的缩影:确定性层知道一些模型无法推断的东西,所以确定性层必须陈述它。
对于任何处于 accepted 模式的东西,success 不是工具返回的值。它是验证者确认的一种状态。包装层返回一个句柄,而验证步骤站在工具和模型的下一轮之间。
包装层返回 {"status": "accepted", "polling_token": "..."}。它不返回 success。
一个验证器步骤轮询 token 直到它达到一个终态——已送达或失败——读取真实效果而不是重新读取接受它的队列。
只有终态才会暴露给模型。
如果轮询耗尽其预算,包装层暴露 uncertain——操作可能已完成也可能未完成——而且明确不是 success。
那个验证器是介于工具和模型下一轮之间的中间件,与"验证是一个循环"的循环形状相同:Agent 行动,系统验证,评分结果,并根据结果路由。而且验证必须亲自观察效果,而不是效果的代理。重新读取那个已经说过 accepted 的队列会愉快地永远确认 accepted。那是一把错误的尺子:一个看起来像验证但衡量了错误东西的检查,这比没有检查更糟糕,因为它制造了虚假的自信。一个名副其实的写后读,读的是收件人的邮箱状态、插入的行、写下的文件,而不是"收到了一条生成它们的请求"的确认。
Agent action
|
v
Tool wrapper
|
|-- committed --> Effect observed --------------> Model context: "success"
|
|-- accepted --> Verifier (poll the real effect)
|-- terminal: delivered ---> Model context: "success"
|-- terminal: failed ------> Model context: "failed"
|-- budget exhausted ------> Model context: "uncertain"
\-- not yet terminal ------> keep polling (loop back)
那张图中的每条路径都结束在一个诚实的 token 上。模型报告 success 的唯一方式是它下面的某一层实际观察到了效果落地。
这是普通的可靠性卫生,一次一个工具地应用。对于你的 Agent 可以调用的每个工具,你应该能够对全部五个问题回答"是":
返回値是否区分了排队与应用?
如果工具是异步的,是否有轮询 token 或回调来确认终态?
包装层是否在 success 到达模型之前强制验证?
验证超时时,包装层是否暴露 uncertain,而不是默认 success 或 failure?
是否在一个地方拥有重试预算,这样重试风暴不会加剧歧义?
最后一个问题是这里与成本以及正确性的连接点。在三层堆叠的未经协调的重试正是膨胀 AI 账单的重试风暴,而且它们同样是完成所有权失败:三层各自希望操作发生了,但没有一个拥有答案。在一个地方放一个重试预算同时修复了同一个 bug 的两面。
任何不符合这五条中任意一条的工具都带有完成所有权债务,而且那种债务是以用户可见的错误来支付的——Agent 断言了没有发生的事情。
对于任何构建过分布式系统的人来说,整件事都有一个熟悉的形状。两阶段提交说:我持有所有权,在效果 durable 之前不会报告 done。最终一致性说:我转发我这部分,并信任下一层来收敛。两者都是合法的设计。麻烦的是大多数 Agent 工具栈构建得像最终一致性系统,但报告得像两阶段提交:它们为一个仍然只在传播中、或者仍然只在队列中、或者已经悄悄丢弃了的效果,发出一个干脆的、立即的成功。
修复不是强迫每个工具都做两阶段提交。大量效果确实是最终一致的,这没问题。修复是报告中的诚实。如果系统是最终一致的,到达模型的 token 必须说明这一点——accepted 然后是一个验证的终态,绝不是一个过早的成功。如果你已经运送过幂等 key 和写后读检查,你已经有了这件事的每一块。轮询 token 是一个换了标签的请求 UUID,而验证器是你已经知道怎么写的写后读检查。
跳过这一步的账单不是以堆栈跟踪的形式到来的。它是以支持工单的形式到来的,几个小时后,来自一个 Agent 以完全自信告诉已经完成某事的客户。在完成所有权进入包装层、在你仍然可以区分 accepted 与 observed 的地方提前读取完成权,模型就不再撒谎了,因为第一次它下面的每一层都没有对模型撒谎。
本文属于生成系统第一性原理路线。它的正确性同族:Validation Is a Loop, Not an Assertion、A Wrong Ruler Is Worse Than No Ruler、Determinism Where You Can, Judgement Where You Must。分布式系统祖先:RPC vs NATS, Who Owns Completion。成本伴侣:Your AI Bill Is a Distributed Systems Problem。
Originally published at harrisonsec.com.