
结果已经有了,为什么 Agent 还会漏交?
一次恢复实验里,Agent 已经返回了 done。如果验收只看最后一句话,任务似乎结束了。但检查下一次模型调用实际收到的输入,少了一份本该存在的工具结果。
那份结果并非没有生成。它在暂停前就已经产生,还可以随运行状态一起保存。问题发生在恢复时:系统把“本地已经有”误当成“下一步已经收到”,于是没有再交出去。
我们在 OpenAI Agents SDK 的固定代码上运行了十个相关回归:修复实现全部通过;仅换回上一版本的目标函数,十个全部失败。这组对照说明,研究 Agent 的完成状态,不能只盯住它有没有产出,还要问产出到了谁那里。
两个工具调用,一次人工等待
Agents SDK 负责把模型、工具调用和运行状态接起来。模型的一次响应可以提出多个工具调用,其中有的必须先由人批准。这种让人介入执行的机制通常称为 HITL。
上游修复 #4947涉及一种混合情形:模型同时调用一个需要批准的工具,以及一个根本没有注册的工具。运行器先为不存在的工具生成错误结果,然后因另一个调用等待批准而暂停。恢复使用服务端管理的会话延续时,旧逻辑可能漏掉那份本地错误。
错误结果也是结果。它告诉模型某次调用没有执行,仍需要与原调用配对。仅仅保留“工具不存在”这条本地记录,不足以完成这项交接。
这也解释了为什么问题容易逃过一般的完成检查:等待批准的工具后来可以正常执行,模型也可以继续返回文本。缺失的是另一条调用的结果,表面进度不一定停在那里。
我们把观察点放在下一次输入
本轮固定修复提交 3f9397f035a891ce3623723eb0c2181847006fc9,运行上游新增的十个测试。SDK 的运行器、审批处理、状态序列化与恢复逻辑实际执行;模型用上游提供的 ScriptedModel 替身,按预设响应行动,并记录收到的输入。Python 为 3.12.10,没有调用真实 OpenAI 服务。
对照不是另装一个发行版。我们保留同一份 SDK 和同一套测试,在另一个进程里,仅把决定“哪些结果尚未送出”的函数换成前一提交 83c737fd0b8d9a53bd39fa2a0856070417bb0bd3 的实现。这称为单函数消融:让其他条件保持一致,观察这一项变化是否足以重新引出失败。
| 本地运行的原回归 | 数量 | 修复实现 | 旧函数消融 |
|---|---|---|---|
| 两种会话延续 × 内存或 JSON 状态 × 流式或非流式 | 8 | 全部通过 | 全部缺少不存在工具的结果 |
| 两次分阶段批准 | 1 | 三份结果各出现一次 | 缺少不存在工具的结果 |
| 没有待执行工具分组的中断状态 | 1 | 保留未送出结果 | 错把该结果列为已送出 |
前八组覆盖 conversation_id 和 auto_previous_response_id,不额外宣称跑过直接指定 previous_response_id 的完整 Runner 路径。最后一组在会话追踪器层直接使用该字段,观察层次不同。
更有区分力的是失败位置:前八组已经通过“最终文本等于 done”的断言,随后才在调用 ID 列表中发现 call-missing 消失。即使最终文案完全符合预期,交付仍可能不完整。

图 1:受控实验中的结果配对差异。来源:本轮 runs/sdk.json,模型输入由 ScriptedModel 记录;不代表真实服务端消费确认。
为什么保存状态还不够
恢复并不意味着把所有本地对象重新发送。使用服务端会话时,有些项目已经由服务端持有,重复发送也会出问题。运行器必须决定:哪些已有归属,哪些仍需补交。
旧函数依赖中断时剩余的待执行工具分组。不存在的工具已经在本地得到错误答复,不会留下普通待执行项。若恢复逻辑只从这份分组表寻找“尚未交付”,它就看不到这份已经产生的错误。状态序列化也不会替它补回这层分类。
修复改用最近一次模型响应中的调用 ID 补全候选集合,同时仍先排除服务端已有的项目。我们本地的消融结果支持这一原因:恢复旧函数后,缺失恰好回到相同调用,而其余测试和运行路径保持不变。
这个案例没有要求另造一套完整账本。现有响应记录已经保存了修复所需的信息。应先确认已有事实能否回答问题,再决定是否新增存储。
“只出现一次”到底在哪里成立
两次分阶段批准的测试尤其容易被过度解读。它实际验证的是:先批准第一个工具,运行再次暂停,此时不应发出新的模型请求;再批准第二个工具,最终送入脚本模型的三份结果各出现一次。
这是一项明确而有用的恢复性质,却还不是网络系统的一般“恰好一次交付”。本轮没有制造请求已经到达、确认却丢失的情形,没有验证服务端持久化或真实模型消费,更没有证明工具对外部世界只产生一次效果。
这次修复启发的对照实验,也留下了一个更具体的后续问题:若服务端已经接受请求,但确认在途中丢失,恢复逻辑应依据什么决定补交还是避免重复?下一轮可以在确认返回处注入故障,分别观察请求构造、服务端记录和重试行为;这需要选择合适的 SDK 或传输层测试入口,不能由本轮 ScriptedModel 结果推定答案。这是待验证的问题,不是本轮新发现的 SDK 缺陷。
Paperclip 的审计边界文档讨论的是另一个层次:数据库提交无法回滚已经发生的外部效果,不确定确认需要持久化意图与幂等处理。它与本例值得对照,但不能拿本例的测试替它作证。
因此,至少要分清四件事:结果已经生成、已经保存、已经交给下一主体、已经被下一主体消费。某个实现可能用现有记录推导这些事实,未必各建一个字段;但不能因为缺少区分,就把它们视为同一件事。
给恢复路径多加一项验收
检查自己的 Agent 时,可以从一个很小的反例开始:同一轮里,一个工具等待批准,另一个在本地产生成功或错误结果;保存状态,恢复,再检查下一次实际输入中的调用与结果是否一一配对。接着分两次批准,观察中间有没有多发请求、最后有没有重复结果。
对 CodeFlowMu/FCoP,这已进入开发评审输入。首先要核对现有任务记录、调用 ID 和交付记录能否表达上述差别;本轮没有证明产品已有同类缺陷,也没有安排新的交付账本实现。
最终文案仍是有用的结果,但它不能代替交接证据。恢复成功的含义,应包含该交出去的结果确实进入了下一步。