在你的智能体能够自我重试之前,精确一次性处理(Exactly-Once)曾是一件难事
我们花了二十年的时间来教导服务如何安全地进行重试。这个方案已经非常成熟了:客户端生成一个唯一的幂等键 (idempotency key),将其附加到请求中,服务器在执行工作的同一个事务中记录该键及其结果。掉线、超时、500 错误 —— 客户端使用相同的键进行重试,服务器识别出该键,并返回记录的结果,而不是再次扣款。Stripe 多年前就推出了这种模式,它已成为任何涉及资金业务的 API 的基本要求。
整个设计都基于一个无人提及的假设:调用者会逐字节地重复其请求。 重试携带相同的键,是因为重试是同一段代码路径使用相同的变量重新执行。一旦打破这个假设,整个方案就会悄无声息地失效。
LLM 智能体在第一次重试时就打破了这一点。当工具调用超时时,智能体不会回放保存的 HTTP 请求 —— 它会根据现在的上下文窗口(其中包含了超时错误)重新进行推理,并发出一个 新的 工具调用。有时这个调用是完全相同的,但通常并非如此。模型会改写参数、重新排列 JSON 键、以不同的方式对数字取整,或者“体贴地”添加第一次遗漏的字段。如果你的幂等键是请求体的哈希值,那么恭喜你:重试会产生一个不同的键,绕过你的去重检查,并第二次执行副作用。你为一个已不存在的调用者构建了“精确一次” (exactly-once) 的保障。
调用者现在是非确定性的
经典的分布式系统可靠性划出了一条清晰的分界线:关于做什么的 决策 可以是混乱的,但 执行 必须是确定性的。智能体运行时将语言模型直接推入决策阶段,然后让其输出直接驱动执行,中间没有转换层。模型的工作是具有创造性和上下文敏感性的。这恰恰是你最不希望在生成去重键的组件中看到的属性。
行业报告显示,由于超时、验证失败以及模型单纯认为上次尝试无效,智能体的重试率大约在 15–30% 之间。因此,这并不是一个可以推迟处理的罕见边缘案例。在繁忙的智能体中,大约每四个工具调用中就有一个可能导致重复,而且由于它们是由随机过程生成的,这些重复项看起来并不像重复项。
还有一个更微妙的陷阱。智能体重试有两个截然不同的原因,它们需要相反的处理方式:
- 瞬时重试 (Transient retry):工具报错或超时,智能体正在重新尝试 同一个意图。你需要严格的幂等性 —— 返回缓存的结果,不要重新执行。
- 采样重试 (Sampling retry):智能体得到了一个有效的结果,但不喜欢它,想要一 个 全新的 尝试。你需要一次全新的执行。
朴素的内容哈希键无法区分这两者。两个恰好序列化后完全相同的“重新运行此查询”调用,会在模型实际想要运行两次时被合并为一个。与此同时,两个仅在备注字段措辞上有所不同的“向客户收费”调用,会在模型本意只执行一次时被执行两次。失败是双向的。
停止从请求中派生键
解决方法是停止将模型的输出作为幂等键的来源。键必须来自 工作流的位置,而不是来自模型在执行时碰巧发出的字节。
具体来说:工具调用是由它在智能体计划中的位置唯一标识的,而不是由它的参数标识的。如果你的运行时为每个智能体执行分配一个稳定的 workflow_id(或运行 ID),并为每个计划的操作分配一个稳定的步骤索引 (step index),那么幂等键就会变成类似 {workflow_id}:{step_id}:{tool_name} 的形式。重试运行 abc123 的第 4 步始终会产生相同的键,无论模型在第二次尝试时如何重新表述参数。从业者不断得出的准则是:你的幂等键必须根据工作流上下文确定,而不是根据执行时刻确定。
这颠覆了通常的建议。在 HTTP 领域,客户端生成一个 UUID 并重用它,负载内容可以随意。在智能体领域,负载是不受信任的部分,而 位置 则是锚点。相同的 (workflow_id, tool, scope) 意味着相同的效果,并返回记录的结果。具有相同参数的不同 workflow_id 意味着一个独立的、有意的效果 —— 即使工具和参数在字 节层面上完全相同。这是内容哈希永远无法带给你的属性,因为内容哈希混淆了“相同的请求”和“相同的意图”。
构建工具首选查询的分类账
派生自位置的键需要一个存放的地方。那就是 操作分类账 (action ledger) —— 工具在接触外部世界之前咨询的持久化表。一个可行的模式并不复杂:
idempotency_key作为主键(派生自工作流位置)status:PENDING、SUCCESS或FAILEDresult_data和error_data存为 JSON- 毫秒级时间戳以及
(agent_id, action_type, created_at)上的索引
唯一重要的规则是顺序:先检查分类账,然后执行副作用,最后记录结果 —— 必须按此顺序。 执行后再检查是团队交付失效幂等层最常见的方式;当你查看时,重复操作已经发生了。
并发是见真章的地方。两个具有相同键的工具调用可能会在几毫秒内同时到达 —— 比如一个触发并行操作的智能体,或者是一个正在与尚未真正终止的原始请求赛跑的重试。不要手动编写“先读后写”的逻辑,否则你会输掉竞态竞争。在唯一约束下插入一个 PENDING 行,让数据库来进行仲裁。胜出的插入操作负责执行;由于违反完整性约束而失败的插入操作则知道另一个尝试已经持有该键,并等待该结果,而不是重复工作。唯一性保证存在于存储引擎中,而不是在你的应用逻辑中,因 为你的应用逻辑正在与自身竞争。
在这里顺便对失败进行分类,因为智能体会重试那些你宁愿它不重试的失败。超时、连接中断、429 和 503 错误是瞬时的,可以安全重试。401、403 或 404 错误是永久性的 —— 重试只会消耗 token 并增加延迟。当你确实无法判断时,默认视为瞬时错误;针对幂等工具进行一次安全的重试成本很低,但悄无声息地丢弃一个合法的操作代价却很大。
当字节标识不再足够:语义去重
基于位置衍生的键可以干净地处理常见情况,但并非所有重复都源于对“同一个”计划步骤的重试。有时,智能体会从两条不同的推理路径推导出相同的意图 —— 它在其计划的不同时间点,两次决定发送同一封邮件或提交同一张工单。步骤 ID 不同,但现实世界的影响相同。位置键无法捕捉到这种情况,因为从设计上讲,它们将不同的位置视为相互独立的。
这就是团队在执行前增加语义层的原因。对提议的工具调用进行嵌入(Embedding),将其与同一会话中的近期调用进行对比。如果余弦相似度超过某个阈值 —— 从业者通常认为近乎相同的阈值为 0.9 左右,意图等价的阈值为 0.85 左右 —— 则将其视为可能的重复,并在执行前要求确认。这是一种软限制,而非硬性的键,你需要将其调整为倾向于误报(拦截真实操作并再次询问),而非漏报(允许重复操作通过)。将其作为兜底机制,而不是主要机制;嵌入阈值是一种启发式方法,而启发式方法不应该成为阻挡智能体 产生重复扣费的唯一防线。
持久化执行转移了问题,但并未消除问题
更重的方案是持久化执行(Durable execution) —— Temporal、Restate、DBOS、Inngest,以及现在内置于 LangGraph 和各大主流智能体 SDK 中的检查点层。这些框架会持久化已完成的步骤,并在崩溃后重放工作流,跳过任何已经完成的操作并注入存储的结果。这确实是一个很好的模式,它免费消除了一大类“工作节点在执行计划中途挂掉”导致的重复。
但请阅读这些系统都会包含的附属细则。如果工具调用成功,但工作节点在结果被记录检查点(Checkpoint)“之前”崩溃,重放时仍会重新发出该调用。持久化执行保证了你的工作流代码能够恢复;它并不保证外部世界只看到一次你的副作用。框架文档写得很清楚:任何写入外部状态的工具必须仍然携带一个与工作流状态绑定的幂等键,否则重放将会导致重复支付、重复工单或重复部署。持久化缩小了可能发生重复的时间窗口。它并没有关闭它。账本(Ledger)仍然是核心支撑。
实际该怎么做
“精确一次”(Exactly-once)从来都只是一个美好的谎言 —— 现实系统交付的是“至少一次交付”加上“幂等处理”,最终产生的“效果”是精确一次的。智能体并没 有改变这一逻辑。它们只是打破了让客户端处理变得简单的假设,因为现在的客户端是一个会重写自己请求的模型。
所以,请移动你的锚点。从智能体的工作流位置而非它输出的 token 中衍生幂等键。在每个产生副作用的工具前设置一个账本,并在操作前进行检查,在数据库中而不是在代码中仲裁并发键。区分瞬时重试与有意的重新采样,以免将两个预期的操作合并为一个。添加语义去重作为跨路径重复的兜底,并依靠持久化执行来缩小崩溃窗口 —— 但请保留账本,因为框架无法覆盖最后的一环。
这个令人不安的结论是:你信任了十年的可靠性原语包含了一个关于调用者的假设,而你的调用者已经变了。你的代码中凡是写着“重试看起来会是一样”的地方,现在都是一个潜在的重复执行 bug。在你的用户发现这些假设之前,先去找到它们。
- https://stripe.com/blog/idempotency
- https://www.padiso.co/blog/building-idempotent-tools-for-long-running-agents/
- https://zylos.ai/research/2026-04-24-durable-execution-agent-runtimes/
- https://www.buildmvpfast.com/blog/idempotent-ai-agent-retry-safe-patterns-production-workflow-2026
- https://github.com/rune0-dev/agent-ledger
- https://fast.io/resources/ai-agent-idempotent-operations/
- https://dev.to/mukundakatta/make-your-agents-api-calls-idempotent-before-you-need-to-2994
- https://www.daydreamsoft.com/blog/idempotency-and-exactly-once-processing-building-reliable-distributed-web-systems
- https://temporal.io/
- https://medium.com/@kaushalsinh73/7-patterns-that-make-agent-retries-idempotent-not-duplicative-dd48f022ce9b
