幂等性危机:LLM 智能体作为事件流消费者
每个事件流系统最终都会将同一条消息投递两次。网络抖动、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 万个事件的情况下,即使是微小的单事件延迟成本也会累积起来。
- https://developer.confluent.io/patterns/event-processing/idempotent-reader/
- https://developer.confluent.io/patterns/event-processing/idempotent-writer/
- https://www.morling.dev/blog/on-idempotency-keys/
- https://nejckorasa.github.io/posts/idempotent-kafka-procesing/
- https://www.conduktor.io/blog/building-idempotent-consumers
- https://atlan.com/know/event-driven-architecture-for-ai-agents/
- https://akka.io/blog/event-sourcing-the-backbone-of-agentic-ai/
- https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/
- https://arxiv.org/html/2408.04667v5
- https://unstract.com/blog/understanding-why-deterministic-output-from-llms-is-nearly-impossible/
- https://arxiv.org/html/2503.11951v3
- https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas
- https://docs.aws.amazon.com/prescriptive-guidance/latest/agentic-ai-patterns/prompt-chaining-saga-patterns.html
- https://domaincentric.net/blog/event-sourcing-projection-patterns-deduplication-strategies
- https://www.zenml.io/blog/what-1200-production-deployments-reveal-about-llmops-in-2025
- https://devops.com/agentic-systems-are-breaking-reliability-frameworks/
