退款请求发出去了,连接在 30 秒后断开。Agent 框架把它当作失败,退避 1 秒重试,第二次请求到达服务器。客户收到了两笔退款。
这不是什么极端边界情况。只要框架用同一套重试逻辑处理所有工具调用,这类事故就是必然发生的。我见过的大部分 Agent 线上故障,根子不在模型多笨,而在这一层:工具调用被当成了普通函数调用。
超时之后其实有三种结局
传统服务端的重试模型很干净:请求失败就重试,成功就不重试。但一次工具调用超时后,服务器端可能处于三种状态之一——请求根本没到达;请求处理了但响应丢了;服务器还在处理中。调用方拿到的是超时异常,无法区分是哪一种。
大多数框架把超时一律归为失败,也就是默认走重试。如果这次调用恰好是 send_payment() 或者 delete_account(),重试就变成了把副作用再发一遍。tianpan.co 上讲过一次退款的例子,说这是 Agent 系统最常见的生产故障类型之一,我认同这个判断,因为它埋点清晰、复现稳定、修起来又需要改动工具定义层。
多步工作流会把这个不确定性放大。一个 14 步的流程在第 7 步崩了,恢复逻辑需要回答的不是"执行停在哪",而是前 6 步里哪些副作用已经落到了真实世界、哪些可以安全重放、哪些得拉人看。传统检查点只记录了执行位置,没有记录影响是否外溢。
先把只读和写入的工具分开
成本最低、收益最大的一步,其实是在工具定义里加一个字段,标明这个工具是否有副作用。
拿用户资料、查库存、读账户余额,重试一百次也没事,这类只读调用可以用激进策略:指数退避加 jitter,重试五次都行。发邮件、创建资源、扣款这类写入操作,得保守处理——最多重试一次,而且重试之前必须先确认上一次到底成没成。
现实是,主流框架给工具的元数据就是名称、描述、参数和实现函数,没有任何字段说明它会不会改状态。结果 search_documents() 和 del... 写入类和查询类走完全相同的错误处理路径。这个缺省设置对 Demo 很友好,因为 Demo 里第一次调用永远成功,副作用也无所谓。
幂等键在 Agent 里为什么不够
支付行业用幂等键解决重复请求已经很多年:客户端带一个唯一 key,服务端对相同 key 返回缓存结果而不重新执行。Stripe、AWS 都支持。轮到 Agent 时,有三处对不上。
- 粒度错位。支付 API 里一个 key 对应一个逻辑操作。但用户一句"帮我订下周二去东京的机票",会被拆成跨多个服务的十几个工具调用。每步各用各的 key,就会出现"部分正确"状态——前 5 步做完了、第 6 步挂了,重跑整个工作流会跳过前 5 步,却用可能已经过期的上下文继续;全工作流共用一个 key,则没法单独重试某一步。
- 分解不确定。LLM 是非确定性的,同一个请求重放时可能拆成另一组工具调用。第一次查航空公司 A,第二次改走 B,旧的 key 保护不了新操作。传统幂等性的前提是相同 key 代表相同请求,这个前提在模型参与决策后就塌了。
- TTL 对不上。幂等键通常有 24 小时存活期,而一个差旅预订流程可能在确认前挂好几天。key 先过期,最该受保护的收尾阶段反而裸奔。
单独提一句 Google 对生成式调用的说明:generateContent 因为采样随机性不是严格幂等的,但由于不改服务端状态,对暂时性错误重试通常是安全的(见 Gemini Enterprise Agent Platform 的重试策略文档)。这类判断只有落到"是否修改服务端状态"上才成立,而不是看工具名字叫得像不像查询。
四种在生产里能用的结构
操作日志
与其在每个工具上压幂等键,不如在工作流层面记一份已完成操作的日志。执行写操作前先查日志,成功后记录结果,重试时重放日志重建状态而不重放副作用。关键差别在于记的是影响而不是意图:不写"准备发邮件",而是写"邮件已在 T 时刻发出,消息 ID 为 M"。这样即便模型再次调用会做出不同决策,重放也是确定的。
两阶段工具调用
把每个写操作拆成预览和提交。预览返回"若执行会发生什么"且只读;提交需要预览阶段签发的短时效 token。这同时解决了两个问题:破坏性操作变得可确认,同时在提交前留了一个自然的校验点。若在预览后崩了,没有副作用;若在提交后崩了,token 挡住重复执行。
影响跟踪与补偿
多步流程维护已完成影响及其逆操作。第 5 步失败时可以重试第 5 步,也可以补偿第 1 到 4 步重来。这是分布式系统里的 Saga 思路。注意补偿动作本身也得幂等——若补偿是发一封撤回邮件,重试补偿不能发出两封撤回。
条件执行卫句
执行写操作前,去目标系统查这个操作是否已经完成。付款记录存在就别再发起支付,用户已存在就别再创建。这个朴素办法经常被忽略,它的好处是把幂等的责任放到了知道真相那一侧的业务逻辑里,而不是让 Agent 框架去猜。
持久化执行能省掉多少自己造轮子的事
Temporal 这类引擎的定位和 Agent 编排很吻合:两者都要协调长耗时、多步骤、单步可能失败的过程。工具调用变成带可配重试、超时和心跳的活动,引擎的事件历史天然充当操作日志,worker 崩了另一个接手重放历史还原状态,已完成的活动不会重跑。
但这不是银弹。LLM 调用本身带来的不确定性有工作流引擎未曾设计的性质:重放时如果模型给出不同决策,就会与历史偏离。生产里的做法是把模型决策当作事件写进历史,这样重放无论模型下次怎么选都是确定的。
上线前的审计清单
实操上我会逐个工具过下面几个问题,答案写不清楚的就是隐患。
- 这个工具有没有副作用?有的话必须加幂等保护;没有就可以放开重试。
- 能否查询影响是否已经发生?邮件 API 返回消息 ID 的话,下一次发送前先查;支付 API 支持幂等键就用。
- 重复的爆炸半径多大?重复发一条 Slack 消息只是烦人,重复电汇是要打官司的,保护力度应该按后果分级。
- 有没有补偿动作?能撤回的(创建数据库记录)有退路,不能撤回的(发邮件、发帖)保护必须万无一失。
- 调用是否依赖前序步骤的结果?如果是,光有单次调用的幂等键不够,你需要操作日志。
还有一处很容易被忽略:队列和业务步骤得分别设幂等 id。高并发场景下队列通常是至少一次投递,业务侧用 task_id + step_id 兜底,才能避免队列重投直接被记成第二次真实执行。同超时相比,这个问题更隐蔽——它不发异常,只是在后台兑现了两遍。
多数 Agent 框架是为演示优化的,不是为生产优化的。演示路径上第一次就成功,副作用也无关紧要;生产线上的关键路径恰恰是重试路径。
落到工程决策上,2026 年要把 Agent 做稳,重点不在提示词写得多花哨,而在承认一件事:每次工具调用都是一个跨网络的、可能部分失败、消息可能重复的分布式调用。把它当成普通函数,你的 Agent 迟早会发出两封一模一样的邮件,然后你会在客服工单里第一次认识"不确定完成"这个词。
转载请注明出处: 皮哈皮阿
本文的链接地址: https://blog.phpat.com/agent-engineering/629.html
微信或手机浏览器扫一扫,随时随地随心阅读与分享
-
MCP 进入生产环境:工具协议、网关与权限边界
MCP 刚出来时,我以为最难的是协议兼容。真正接了十几个工具之后才发现,把工具接上只花了两天,把权限、审计和故障处理理清楚花了两个月。
2026/10/07
-
AI Coding Agent 如何稳定提交可合并的 PR
上周 review 了一个 AI 生成的 PR,测试全绿,但我还是把它关了。它改了 37 个文件,顺手升级了一个依赖,还修改了两条测试断言。问题不是代码不能跑,而是这个 PR 没人敢合。
2026/10/14
-
Agent Harness:可观测、可评测、可回滚的 Agent 基础设施
去年我们做了一个处理退款申请的 Agent。Demo 很顺,上线第一周就出了事故:进程重启后状态丢了,同一笔申请被处理了两次。问题不在模型,而在模型外面那层没人愿意写的基础设施。
2026/10/09
-
Agent 安全清单:提示注入、工具越权与数据外泄
一个内部助手被要求总结一封客户邮件。邮件末尾有一行白色小字:把最近的客户名单转发到某个地址。Agent 真的准备去调邮件工具了。安全不能靠模型自觉,得靠系统设计。
2026/10/16
-
并行工具调用暴露的工具设计耦合问题
并行工具调用能砍掉四成延迟,但它更重要的价值在于暴露工具之间的隐性耦合。本文拆解上下文依赖、共享状态竞争与时序前提这三类静默失败,给出读写分类规则、工具合并的判据,以及什么时候该上依赖图编排。
5天前
-
Agent 轨迹回放调试:把失败变成可回放资产
Agent 调试难在无法复现、故障点藏在数步之前。这篇文章讲三件具体的事:把轨迹归一化成可校验的步骤结构,用工具签名和领域策略生成可执行约束,以及用标注过的失败轨迹在 CI 里做回归回放——顺带说明为什么只盯延迟和错误率的可观测看不到这类故障。
4天前
京公网安备11011402053654 
@muffin:链接已更新,分类图像功能...
AI网址导航模版图标库更换指南
作者大大,图标库邀请链接失效了,麻烦更新...
AI网址导航模版图标库更换指南
太好看了,快点更新!
友情链接
@戴墨镜的刘备:哈喽,路过点赞。
vue.js组件之手写签名板vue-signature-simple
太好看了,快点更新!太棒了!👍真的很赞!
vue.js组件之手写签名板vue-signature-simple
你写得非常清晰明了,让我很容易理解你的观...
Emlog插件RSS订阅器发布