跳到主要内容

幂等性危机:LLM 智能体作为事件流消费者

· 阅读需 12 分钟
Tian Pan
Software Engineer

每个事件流系统最终都会将同一条消息投递两次。网络抖动、Broker 重启、偏移量提交失败——至少一次投递不是 Bug,而是契约。传统消费者能够优雅地处理这种情况,因为它们是确定性的:处理同一事件两次,得到相同的结果,写入相同的记录。第二次写入是一个空操作(no-op)。

LLM 不是确定性处理器。相同的提示词加上相同的输入,每次运行都会产生不同的输出。即使设置了 temperature=0,浮点运算、批次组合效应以及硬件调度的差异也会引入方差。针对"确定性" LLM 设置的研究发现,在自然发生的多次运行中,准确率差异高达 15%,最优与最差性能之间的差距甚至达到 70%。至少一次投递加上非确定性处理器,并不会给你带来至多一次的行为,只会带来不可预测的行为——这是一场蓄势待发的生产环境危机。

如果你的 AI 智能体从 Kafka、SQS 或任何至少一次投递的队列中消费消息,你已经在这个风险中运行了。重复消息在开发环境中可能永远不会出现,但它会在最糟糕的时刻找上你的生产系统:Broker 故障切换期间、智能体刚刚批准了一笔贷款之后,或者多步骤客户入职工作流进行到一半时。

为什么传统幂等性对 LLM 失效

经典幂等性建立在一个简单的保证之上:f(x) = f(x)。处理相同的输入两次,得到相同的输出两次。对于确定性业务逻辑来说,这轻而易举。如果你的消费者函数依据固定规则更新数据库行,你可以安全地重放它。数据库写入要么完成,要么已经完成。

LLM 推断从两个方面打破了这一保证。

第一,输出本身是非确定性的。一个 LLM 在判断"这笔交易是否应该被标记为欺诈?"时,第一次可能回答"是",第二次可能回答"否"。事件完全相同,环境状态完全相同,但答案却不同。仅仅检查某个事件 ID 是否已被处理过的去重策略在这里毫无帮助,因为问题不在于事件是否被处理过,而在于处理时做出了什么决策。

第二,LLM 的副作用通常难以撤销。一个确定性消费者重复处理某个事件,可能会插入一条重复的数据库记录,你可以通过唯一约束来检测并忽略它。而由 LLM 驱动的消费者可能会发送电子邮件、触发下游 API 调用、更新客户的信用等级,或者发出后续事件。这些操作天然不具备幂等性。你通常依赖的约束——重放安全性——需要提前知道 LLM 会做什么,而这是你无法预知的。

架构层面的影响是深远的:如果没有额外的机制,你不能把 LLM 推断当作至少一次投递管道中的普通函数来对待。

去重窗口:停止重复处理,开始缓存

第一个需要添加的模式是去重窗口——一个持久化的记录,保存哪些事件已经被处理过以及做出了什么决策。

实现方式很直接,但需要严格的纪律。在调用 LLM 之前,你的消费者会在状态存储(数据库表、Redis hash 或 Kafka Streams 状态存储)中查找传入事件的 ID。如果该事件已经见过,就检索并重放存储的决策,而不是重新运行 LLM。如果是新事件,则运行 LLM,存储结果,然后执行副作用。

存储记录至少需要三个字段:事件 ID(或复合幂等性键)、存储的输出,以及时间戳。时间戳至关重要,因为你不能永久存储事件结果——磁盘压力是真实存在的。基于 Kafka 系统的行业实践倾向于使用 32 天的去重窗口,这几乎覆盖了所有实际的 Broker 重试窗口,同时保持存储可管理。

复合幂等性键通常比原始事件 ID 更好用。事件 ID 告诉你是否见过这条确切的消息;而像 customerId:orderId:actionType 这样的复合键则告诉你是否执行过这个逻辑操作,无论相同的消息是否以不同的封装重新发送。对于业务操作来说,逻辑去重通常才是你真正想要的。

原子写入是大多数实现出错的地方。你必须在与下游状态变更相同的事务中存储 LLM 决策并将事件标记为已处理。如果你存储了决策,然后在更新下游状态之前崩溃,你就造成了一个不一致的系统。重试时,你会重放存储的决策而不是重新调用 LLM——但下游状态从未更新。解决方案是使用数据库事务或事务性发件箱(transactional outbox)模式,将幂等性记录和业务状态变更原子性地写入。

将决策日志与读模型分离

一旦有了去重机制,你需要仔细思考实际存储的内容,以及下游消费者如何使用它。

最佳实践是将决策日志与读模型分离。决策日志是每次 LLM 推断的仅追加记录:事件 ID、时间戳、模型版本、原始输出,以及如果你的管道产生置信度分数的话也包含在内。它永不改变。读模型是下游系统查询的当前业务状态的反规范化投影。

当重复事件到达且你检索到缓存决策时,你将其幂等性地重放到读模型中。读模型的更新逻辑是确定性的——它获取存储的决策,并使用固定规则将其应用到当前状态。LLM 的非确定性现在被隔离在初始推断阶段,而该阶段恰好只发生一次。

这种分离还免费为你提供了审计和重放能力。如果模型被回滚,你可以用新的模型行为重新处理决策日志,重新计算读模型,并在推广变更之前比较输出结果。如果某个决策存在争议,你拥有模型究竟决定了什么以及何时决定的永久记录。

代价是运营复杂度的提升。你每个事件要运行两次写入(决策日志 + 读模型),并在它们之间强制保持事务一致性。对于高吞吐量管道来说,这很重要。批量写入、使用连接池,并考虑你的状态存储(Postgres、DynamoDB、Cassandra)是否针对写入放大进行了合理配置。在每秒 10 万个事件的情况下,即使是微小的单事件延迟成本也会累积起来。

多步骤工作流的补偿事务

一旦有了去重和决策日志,单事件消费者就很简单了。多步骤智能体工作流则更难,因为工作流中途的崩溃或重复消息可能会使系统停留在部分应用的状态——既无法继续,也无法完全回滚。

Saga 模式直接解决了这个问题。Saga 不是跨多个服务的单个原子事务,而是一系列本地事务,每个事务都与一个补偿事务配对,后者可以撤销其效果。如果 5 步工作流的第 3 步失败,Saga 协调器会为第 1 步和第 2 步发出补偿事件。

对于 LLM 驱动的工作流,关键约束是补偿事务必须在正向工作流执行之前设计好。你无法在 LLM 决定要做什么之后再设计补偿事务,因为补偿逻辑需要是确定性的、预先编码的。LLM 负责做决策,你的基础设施负责编写撤销逻辑。

一个实际的例子:处理保险理赔的 LLM 智能体决定批准一项理赔并请求赔付。正向事件为:mark-claim-approved、schedule-payout、notify-customer。如果通知失败且 Saga 回滚,补偿事件为:cancel-payout、mark-claim-pending、log-rollback。这些补偿事件是你的工程师编写的简单基于规则的操作,它们不会重新调用 LLM。

这个模式需要明确的工作流状态。你需要在任意时间点知道哪些步骤已执行、哪些已被补偿。将其作为工作流记录存储在持久化存储中,并与每个步骤原子性地更新。Temporal 或 AWS Step Functions 等工具开箱即提供这种状态机记账——其"活动(activity)"模型与 Saga 步骤完美对应。

事件溯源作为幂等性基础

对于需要完整可审计性和重放能力的系统,事件溯源将幂等性作为结构性属性而非附加机制提供给你。

在事件溯源系统中,真相的来源不是当前状态,而是产生该状态的有序事件日志。当前状态是一个投影,通过重放日志中的事件得到。这意味着你可以随意多次重放相同的事件序列,并始终到达相同的当前状态——只要你的投影逻辑是确定性的。

对 LLM 智能体的挑战在于,LLM 的输出本身必须被记录为事件。"LLM 决定批准这笔贷款"是日志中的一个事件,而不仅仅是函数中的一个步骤。当你通过重放事件重建状态时,你重放的是存储的 LLM 决策,而不是重新运行 LLM。投影逻辑是确定性的,因为它消费的是已记录的决策,而不是实时模型。

这本质上是在基础设施层面实现的决策日志模式。事件溯源使其明确化,并通过架构而非开发者纪律来强制执行。

运营成本是真实的:事件日志会无限增长,需要快照策略来限制启动重放时间。但对于合规性、可审计性和模型回滚是真实需求的 AI 系统——欺诈检测、信贷决策、医疗分诊——事件溯源所提供的属性很难用其他方式复制。

从何处开始构建

上述模式相互叠加。如果你要在现有的事件驱动系统中添加 LLM 处理,务实的推出顺序如下:

  1. 首先是去重窗口。 在每次 LLM 调用之前添加幂等性键检查。将事件 ID(或复合键)和 LLM 输出与业务写入原子性地存储。这是最低限度的保护,一天即可实现。

  2. 其次是决策日志。 将 LLM 输出存储与业务状态存储分离。这需要再花一天时间,但能够支持模型回滚和你最终需要的审计能力。

  3. 为所有多步骤工作流建立补偿事务。 如果你的智能体产生一系列操作,在构建正向流程之前定义好补偿事件。这更难——需要思考每种故障模式——但在前期设计的成本远比事后改造低。

  4. 为高合规性领域引入事件溯源。 只有当你的正确性和可审计性要求确实需要时,才考虑完整的事件溯源。它增加了基础设施复杂度,而大多数团队在第一天并不需要这些。

跳过这些模式的吞吐量论据在规模上站不住脚。LLM 推断本身已经在数量级上主导了你的单事件延迟。用于幂等性追踪的数据库写入只给需要数百毫秒的操作增加了几毫秒。从风险收益比来看,尽早构建这些基础设施是强烈合算的选择。

改变一切的思维转变

传统的至少一次弹性建立在这样一个假设之上:重处理是安全的,因为输出是可重现的。LLM 打破了这一假设。系统设计的应对之道是让 LLM 推断恰好发生一次,方法是将其与投递保证分离:至少一次投递,恰好一次推断。这就是思维的重构。

恰好一次推断意味着 LLM 对每个逻辑事件只运行一次,其输出被持久化存储,所有后续处理都使用存储的输出。投递层可以根据需要积极重试,推断被隔离在去重门(deduplication gate)之后。越过该门的内容再也不会触碰模型。

在 2025-2026 年构建生产 AI 系统的团队正在以艰难的方式发现这一点。对 1,200 多个 LLM 部署的分析发现,几乎所有成熟系统都实现了某种带有重试逻辑和熔断器的消息队列——但大多数系统仍然将 LLM 推断视为普通服务调用,而非需要明确幂等性契约的非确定性状态转换。那些早早把这件事做对的团队,交付更可靠,排查故障更快,也更少向利益相关者解释为什么 AI 对同一问题给出了不同的答案。

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