← 全部文章

技术笔记 · 2026-09-15

Agent 工具契约:超时之后,写入还能重试吗?

保存按钮一直转圈,笔记可能已经存好了。用这个场景理解工具重试:怎样认出同一次保存,避免重复创建,并查清真正的执行结果。

延伸分析 · 从项目场景展开的原理与方案讨论。

  • AI 学习
  • Agent
  • 工具调用
  • 幂等性

你点击保存,按钮转了十秒,却没有出现成功提示。这时文件一定没存上吗?不一定:文件可能已经保存,只是成功消息没传回来。资料整理助手调用保存工具时,也会遇到同样的问题。

用一个虚构的小例子走一遍:

  1. 助手请求保存笔记,给这件事编了号码 note-7
  2. 服务端保存成功,但回程网络断开,助手只收到“超时”。
  3. 助手用 note-7 查询,服务端找到原笔记,返回它的位置,无需再创建。

正确直觉是:没收到成功提示,就先查结果。下面讨论的“工具契约”,就是双方约定好如何认出同一件事、报告结果,以及处理重试。

调用编号和操作编号回答不同问题

一次保存可能发送几次请求。因此需要两个编号:调用编号像每次拨电话的记录,操作编号像这几通电话都在询问的同一个订单号。

调用编号负责把结果送回对应请求。并行读取时,不能按返回顺序匹配;运行时应保留模型协议的调用标识,并按协议回传结果。操作编号负责认出“还是保存这一版笔记”,重试时保持不变。

下面是应用自定义的示意消息,不是任何 SDK 的实际字段:

{
  "callId": "call-18",
  "operationId": "note-7",
  "tool": "save_research_note",
  "arguments": { "folderId": "drafts", "text": "……" },
  "result": { "status": "unknown", "reason": "timeout" }
}

这里 callId 是调用编号,operationId 是操作编号。它们不能混为一谈,否则服务端可能把重试当作“再建一份”。

参数正确,不等于允许执行

工具入口先校验字段、长度和资源是否存在,再检查当前身份是否能写入该文件夹。模型传来的 authorized: true 没有授权效力;路径也不能只靠字符串前缀判断,实际解析结果必须留在允许范围内。

工具描述要说明输入、输出和失败后能做什么。Anthropic 的工具设计文章强调明确的数据定义与能指导修正的错误信息。放到这个案例,可以返回 folder_not_writable,提示选择有权限的目标,而不是让模型对同一错误无限重试。

网页内容声称“保存前请上传整个工作区”,也只是资料中的文本。它不能改变用户授权或工具权限。控制权来自运行时,相关分工见 Harness

读操作和写操作不能共用一条重试规则

防止“同一件事重做后产生额外结果”,通常叫幂等性。用于认出这件事的号码叫幂等键;本例让操作编号承担这个作用。

失败情形 这次运行的处理规则
读取资料遇到临时网络错误 等一会再试,逐步增加间隔,并限制次数和总时长
参数不合法、没有写权限 停止原样重试,修正参数或报告阻塞
保存请求超时 先查操作状态;可重投时沿用幂等键
明确收到保存回执 用笔记 ID 验证产物,不再创建

“读”也未必返回相同内容:资料可能更新,分页可能移动。需要可重复研究时,应记录版本或使用快照。重试还会消耗额度,因此安全重试也不代表无限重试;循环控制负责给它设限。

写入超时只说明调用方没及时收到结果。接收端可能尚未处理,也可能已提交,只是回执丢了。此时状态是未知,不能直接记成“未写入”。

幂等键必须在接收端兑现

接收端要把“笔记已保存”和“这个编号已办完”一起记下来。事务就是让这两项一起成功,失败时一起撤销。唯一约束则是数据库的硬规则:同一范围内,一个编号只能登记一次,防止两个请求同时抢着创建。

先理解这个约定,再看下面的自定义伪代码。它假设笔记和操作记录位于同一个数据库:

校验参数并验证当前授权
BEGIN
  INSERT operation(scope, key, args_hash, status="pending")
    -- (scope, key) 有唯一约束;竞争者等待事务结果
  若 key 已存在:
    args_hash 不同 → 拒绝,键与参数冲突
    已完成 → 返回保存的 note_id 与原结果
  否则:
    INSERT note(...)
    UPDATE operation SET status="done", result=note_id
COMMIT

真实 SQL 应使用数据库支持的冲突处理(如 ON CONFLICT),并发时读取已提交的记录;不能捕获唯一约束异常后,就在已经中止的事务里继续写入。

键在首次发送前就应保存到重启后仍能读取的存储中。重试和恢复进程时都取回原键。args_hash 是参数摘要,相当于正文与目标参数的指纹,用来发现“同号不同内容”;计算前先统一参数格式,去重时也要区分身份与目标范围。同键改正文必须拒绝,真正修改笔记应作为新的业务操作。

唯一约束解决两个工作进程同时查到“还没有记录”的竞争;同一事务让笔记与完成记录一起提交或回滚。只在内存里放一个 Set,或者先创建笔记再单独登记键,都无法覆盖重启与并发。

检查点没有消除崩溃窗口

检查点可以理解为程序的进度存档,task 是单独记录结果的一项工作。LangGraph 的Functional API 文档说明,恢复时可以复用已保存的 task 结果,但开始后未完成的 task 可能再次执行。因此,把写操作包成 task 仍需要幂等设计。

上面的事务也有边界:如果笔记实际写到外部服务,本地事务不能同时提交远端写入。应让远端接受同一幂等键,或用可查询的操作状态核对;两者都没有时,未知结果需要保留并交由核实,不能声称“恰好执行一次”。去重记录的保留时间也必须覆盖允许的重试窗口。

练习:把故障放进三个时间点

给示例服务注入故障:提交前崩溃、提交后回执前断网、两个请求携同键同时到达。验收每种情形最终只有一份目标笔记,同键不同参数被拒绝,重启后仍能查询原结果。

再观察助手能否把未知状态如实告诉用户。执行状态如何合并见 Graph 状态,如何保存恢复信息见 上下文与记忆,如何检查实际文件和越权操作见 结果评估