Agent 工具契约:超时之后,写入还能重试吗?
保存按钮一直转圈,笔记可能已经存好了。用这个场景理解工具重试:怎样认出同一次保存,避免重复创建,并查清真正的执行结果。
延伸分析 · 从项目场景展开的原理与方案讨论。
- AI 学习
- Agent
- 工具调用
- 幂等性
你点击保存,按钮转了十秒,却没有出现成功提示。这时文件一定没存上吗?不一定:文件可能已经保存,只是成功消息没传回来。资料整理助手调用保存工具时,也会遇到同样的问题。
用一个虚构的小例子走一遍:
- 助手请求保存笔记,给这件事编了号码
note-7。 - 服务端保存成功,但回程网络断开,助手只收到“超时”。
- 助手用
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 状态,如何保存恢复信息见 上下文与记忆,如何检查实际文件和越权操作见 结果评估。