跳到主要内容

LLM 流水线中,幂等性是必选项

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个批量推理任务在六分钟后完成。网络在返回响应时发生了抖动。你的重试逻辑开始介入。两分钟后,任务再次完成——而你的账单也翻了一倍。这只是将传统的幂等性思维应用于 LLM 流水线而不根据随机系统进行调整时,所发生的最温和的情况。

大多数生产团队都是通过惨痛的教训才发现这个问题的:本意是为了从瞬时错误中恢复的重试,却触发了第二次付款、发送了重复的电子邮件,或者在数据库中写入了相互矛盾的记录。解决方案不是更好的重试逻辑,而是一个全新的心智模型——当你的核心组件是概率性的时,幂等性究竟意味着什么。

为什么经典定义失效了

在确定性系统中,幂等性很简单:多次运行相同的操作产生的结果与运行一次相同。重试是安全的,因为输出是固定的。

LLM 在 Token 级别违反了这一点。基于温度(Temperature)的采样、top-p 过滤以及供应商端的批处理效应,意味着相同的 Prompt 输入在每次调用时都会产生不同的输出。即使在 temperature=0 时,不同硬件区域和供应商更新带来的细微浮点非确定性也会导致结果发生偏移。经典的定义——输入相同,输出相同——在这里根本不适用。

这很重要,因为大多数重试基础设施是为确定性世界构建的。指数退避(Exponential backoff)、幂等性密钥(Idempotency keys)、响应缓存——所有这些都假设“重试相同的请求”是一个连贯的概念。在 LLM 系统中,你需要精确定义“相同”的含义。

有两种根本不同的重试意图,将它们混为一谈是生产环境 Bug 诞生的根源。

双重重试陷阱

为了瞬时可靠性的重试(Retry for transient reliability)意味着:操作成功了(或本该成功),但响应在传输过程中丢失了。网络超时、短暂的 503 错误、或者超过截止时间的缓慢响应——底层工作已经完成,你想要的是你应该得到的结果,而不是再次触发操作。

为了采样不同答案的重试(Retry to sample a different answer)意味着:LLM 返回的输出未能通过验证——格式错误、违反了约束、触发了护栏(Guardrail)——而你希望下一次采样会更好。

这需要相反的处理方式。对于瞬时重试,你需要严格的幂等性:从缓存中返回完全相同的结果,不进行重新计算。对于采样重试,你需要相反的操作——生成一个真正的新结果,这意味着你的重试必须具有不同的有效输入(通常是将错误反馈附加到 Prompt 中)。

工程师常犯的错误是将采样重试视为瞬时重试。一个 Agent 发送了一个工具调用,工具超时了,Agent 假设原始调用失败并进行重试。但第一次调用其实已经完成了——只是没有及时回传。现在你有了两次工具执行,两次副作用,以及一个混乱的状态机。

下游的后果随工具的功能而缩放。重复发送一条 Slack 消息很烦人;重复扣款则是一起客户服务事故;而在数据库中写入两条相互矛盾的记录则是一场调试噩梦,可能在有人注意到不一致之前持续数周之久。

幂等性密钥模式及其局限

标准的修复方法是幂等性密钥(Idempotency key):为每个变动操作包含一个稳定的唯一标识符。在服务器端,将第一次执行的结果针对该密钥进行存储。重试时,直接返回缓存的结果而不重新执行。Stripe 推广了这一做法;HTTP 标准已将其正式定为 Idempotency-Key 请求头。

对于确定性 API,这效果很好。对于 LLM 流水线,存在三个复杂点:

密钥必须作用于逻辑操作,而非 API 调用。 一个 Agent 编排步骤可能涉及五次 LLM 调用和三次工具调用。幂等性密钥应该覆盖不可重复的工作单元——通常是带有外部副作用的操作——而不是单个推理请求。

采样重试会破坏密钥语义。 如果你将解析失败的结果缓存到幂等性密钥下,后续的重试将返回相同的错误输出。此时密钥必须失效,或者重试必须使用不同的密钥。最清晰的模式是:在密钥中包含重试计数或上一次错误的哈希值,这样每次采样重试都被视为独立操作,但在网络失败时仍然可以恢复。

供应商级别的缓存不等于幂等性。 前缀缓存(Prefix caching)、Prompt 缓存和语义缓存都能降低成本和延迟,但它们并非设计为幂等性机制。它们可能会返回缓存的响应,但它们不提供幂等性密钥所具备的交付保证。混淆两者会产生重试是安全的虚假信心。

Agent 动作的语义去重

当你尝试去重的东西不是原始 API 调用,而是一个 Agent 动作——“发送一封电子邮件”、“创建一个工单”、“提交一个表单”时——仅对原始请求进行内容哈希通常是不够的。Agent 每次生成的工具调用参数可能略有不同(不同的措辞、等价的 ID),但表达的是相同的意图。

语义去重(Semantic deduplication)在执行前增加了相似度检查:

  1. 将提议的工具调用(或其规范化表示)进行嵌入(Embedding),并与幂等性密钥一起存储。
  2. 当新的工具调用到达时,将其与来自同一 Agent 会话的近期存储调用进行对比。
  3. 如果余弦相似度(Cosine similarity)超过阈值(对于近乎相同的通常为 0.9,对于意图等价的通常为 0.85),则视为重复。

生产部署报告显示,使用这种方法在真实 Agent 流量上可以达到 20-45% 的缓存命中率——而且这种收益在同一子目标通过不同推理路径实现的 Agent 循环中会产生叠加效应。阈值校准至关重要:太低会导致合法的不同操作被拦截;太高则会错过由于措辞不同的重复动作。

实际应用中的模式是一个漏斗:首先进行快速字符串匹配,然后对近似匹配进行嵌入相似度检查,最后仅对需要语义判断的边界情况进行基于 LLM 的验证。这使查找延迟保持在 50ms 以下——这是去重开销开始抵消其自身收益的阈值。

损坏的状态机究竟是什么样子的

多智能体系统是幂等性故障风险最高的领域,因为任何步骤的失败都可能产生级联效应。一个常见的模式是:

智能体 A 编排一个多步骤工作流:检索数据、分析数据、编写摘要、发送通知。每个步骤由专门的子智能体处理。智能体 A 等待完成信号。如果智能体 B 的完成信号延迟了——例如通知服务变慢——智能体 A 就会重试整个工作流。此时智能体 B 的通知正在发送两次。摘要也被编写了两次。如果写入操作没有受到幂等键(idempotency key)的保护,现在就会出现两条记录。

这种故障是隐蔽的,直到人工或下游系统发现重复数据。由于 LLM 在每次运行时输出略有不同,这意味着两条摘要记录在位级上并非完全一致——简单的去重查询无法捕捉到它们。这就是为什么在多智能体系统中,幂等性是工作流层面的关注点,而不仅仅是 API 层面的关注点。

Saga 模式直接解决了这个问题。每个智能体步骤都会发布一个完成事件并注册一个补偿操作——即如果后续步骤失败,则撤销该步骤的滚动回逻辑。在重试时,编排器仅重放未完成的步骤,而不是已经完成的步骤。每个步骤的处理器都是幂等的:它在执行前检查完成记录,如果已完成则跳过。

这是标准的分布式系统工程。对于 LLM 流水线来说,不同之处在于“完成记录”必须跟踪语义上的完成,而不仅仅是执行上的完成。一个运行完毕但返回了无法解析的响应的工具调用在语义上是不完整的——你希望重新执行它,而不是在重试时跳过它。完成记录需要一个状态字段来区分“已运行”和“已成功”。

从一开始就为幂等性而设计

在生产环境中经得起考验的模式通常具有以下几个特征。

将摄取与处理分离。 Webhook 处理器、队列消费者和 API 端点应立即确认收到请求,并将任务推送到内部队列。业务逻辑从该队列异步运行,去重在队列层面进行,而不是在处理器中进行。这将确认的可靠性与执行的可靠性解耦。

尽可能使工具调用保持纯净。 读取状态并返回结果的工具调用是“免费”的——你可以根据需要重试任意次数。写入状态的工具调用是昂贵的——必须使用幂等键进行保护。将智能体设计为倾向于读取而非写入,并将写入操作推迟到工作流末尾,自然会减少幂等性故障产生影响的范围。

执行前记录意图。 在执行任何不可逆的操作之前,将意图连同幂等键写入持久化日志。如果进程在执行后但在记录完成前崩溃,意图日志会告诉你发生了什么,以及在恢复时是进行补偿还是跳过。如果没有这个,恢复逻辑就会退化为“再试一次,听天由命”。

区分重试策略与采样策略。 在你的智能体循环配置中,这些应该是独立的旋钮。重试策略控制你使用相同的有效载荷重新尝试失败的网络调用的次数。采样策略控制你为了满足质量约束而生成新响应的次数。将它们混在一个简单的 "max_retries" 参数中是这里描述的大多数故障模式的根源。

成本倍增问题

幂等性故障一个被低估的后果是负载下的成本倍增。在流量激增或供应商发生事故期间,重试风暴(retry storms)很常见。如果你的重试逻辑不是幂等的,每次重试都会执行整个流水线——包括推理、工具调用和任何下游 API 费用。在大多数供应商那里,输出 Token 的定价都高于输入 Token。一个 50的批处理作业,如果在网络抖动下重试三次,成本就会飙升至50 的批处理作业,如果在网络抖动下重试三次,成本就会飙升至 200。

在规模化运营时,这种影响会复合。大规模运行智能体工作流的团队报告称,事故期间的重试风暴占到了每月意外推理支出的 30-60%。解决方法不是更激进的速率限制——而是确保重试返回缓存结果而不是触发新执行的幂等基础设施。

分层缓存架构——语义缓存优先,然后是前缀缓存,最后是完整推理——提供了一定的保护,但前提是缓存命中逻辑必须与幂等键系统集成。缓存命中应当满足幂等性保证:客户端接收到的逻辑结果应与原始请求成功时接收到的结果一致。如果缓存和幂等性是具有不同键的独立系统,重试可能会绕过缓存,仍然触发新的推理。

实际的切入点

对于开始将幂等性引入现有流水线的团队,优先级顺序如下:

  • 具有外部副作用的工具调用(支付、电子邮件、记录)——首先在工具层而非智能体层使用幂等键保护这些调用。
  • 智能体工作流步骤——在担心语义去重之前,先添加完成记录和“已完成则跳过”的逻辑。
  • 采样重试——确保这些重试使用不同的键或使缓存结果失效,并且始终将之前的错误附加到提示词中。
  • 监控——添加一个指标来统计各层的“幂等键命中次数”。重试风暴期间命中率的突然下降表明你的去重逻辑没有覆盖到某个代码路径。

心智模型的转变是最困难的部分。传统的幂等性是操作的属性。在 LLM 流水线中,幂等性是工作流的属性——当核心组件具有随机性时,它要求你明确定义什么是“相同的操作”。

References:Let's stay in touch and Follow me for more thoughts and updates