LLM 时代 Agent 重试机制放大双写问题——重复事件可触发欺诈调查、支付冻结与重复退款,黑五夜一单可达数百万欧元。
双重写入问题早在 LLM 出现之前就存在了。如今,一条重复的事件可能引发欺诈调查、支付冻结、退款或金融执行。
黑色星期五晚上 23:57,一个全球电商平台批准了一笔 240 万欧元的商家提现。
账本提交了交易。应用发布了 PayoutApproved。然后请求超时了。
没有服务一定宕机。数据库是健康的。消息代理也可能是健康的。网络在几毫秒后恢复。所有仪表盘仍然是绿色的。
然而系统失去的东西比可用性更危险。
它失去了确定性。

代理收到事件了吗?
如果生产者什么都不做,风险系统可能永远不会分析这笔提现。如果生产者重试——而第一次发布实际上成功了——事件可能被投递两次。如果下游执行不是幂等的,相同的业务操作可能被执行两次。
几毫秒的不确定性可以变成一个价值数百万欧元的问题:
操作失败了,还是我们只是未能观察到它成功了?
这不是一个根本上的 Kafka 问题、NATS 问题或 AI 问题。这是一个分布式系统问题。
现代 AI 智能体让它代价更高,因为事件消费者的能力变得更强了。一条重复的事件不再只意味着重复的投影或重复的日志行。它可能唤醒一个负责评估欺诈、建议支付冻结、发起调查、请求退款或调用连接外部金融系统的工具的智能体。
投递语义并没有变差。事件背后的东西变得更强大。
架构需要回答五个问题:
事件在何处成为权威来源?
失败后如何恢复发布?
消费者如何容忍重投递?
业务需要时如何保证顺序?
如何证明一个不可逆的外部效果只发生了一次?
批准一笔提现的服务通常执行两个在概念上相关但存在于两个独立事务系统中的操作:数据库和代理。

没有跨越两个系统的自动原子事务,这产生了三种不同的失败状态:
这就是双重写入问题。代理给你提供了解耦、缓冲、持久化、扇出、重放和故障隔离。它不能给你的是事件与导致该事件存在的数据库状态之间的原子性。这个区别是后续所有内容的基础。
正确的问题不是"哪个代理最好?"——而是"这个工作负载实际上需要哪些保证?"
Apache Kafka——在长保留期、基于分区的顺序、高吞吐量、重放、流处理和成熟的连接器生态方面表现强劲。自 Kafka 4.0 起,ZooKeeper 模式已被移除,KRaft 成为必选——少了一个运维依赖,但分区、保留期、再平衡和灾难恢复仍然需要精心的工程设计。
Redpanda——广泛的 Kafka 协议兼容性,包括幂等生产者和事务,但运维配置不同。架构边界不变:Redpanda 事务不能使对外部 Postgres 数据库——或银行转账——的写入变为原子操作。
RabbitMQ——仲裁队列、发布者确认、持久化消息、单活跃消费者、流和路由交换,使其非常适合命令、任务分发和工作流编排。
NATS JetStream——结合了低延迟的 NATS 消息传递与持久化、持久消费者、保留期和基于确认的投递。在分布式拓扑、请求-应答工作负载和边缘环境中很有吸引力。
它们中没有一个能独自消除双重写入问题。
双重写入问题比 LLM 早了几十年。重试、重复消息、丢失确认和事件乱序是已有的分布式系统故障模式。改变的是消费者现在能用重复事件做什么。

如果旧的投影消费者处理了两次重复,且投影是幂等的,影响可能微乎其微。如果同样的重复现在经过一条智能体路径,它可能触发第二次欺诈调查、第二次账户冻结或第二次退款指令。
这并不意味着 AI 智能体天生不安全。它意味着围绕它们的架构必须假设消息会到达多次、智能体会犯错、工具会超时、模型会变化、外部系统会返回模糊的结果。
AI 智能体绝不应该仅仅因为生成了一个看似合理的答案就转移资金。更安全的边界是将推理与执行分离:

AI 建议。策略决定。系统执行。
智能体分析、分类、解释和建议。确定性服务评估限额、权限、职责分离和合规性。只有在那之后,幂等执行器才执行外部效果。
企业系统很少从零开始就干净。银行、保险公司和电信运营商通常依赖早于事件驱动架构主流化之前数年构建的 ERP 和结算引擎。将它们重写为发出领域事件可能风险太大。
变更数据捕获(通常指 Debezium)观察事务数据库日志并捕获已提交的变更,而不触碰遗留应用。

陷阱:一行变更不等于自动成为领域事件。STATUS = 'A' 可能意味着已批准、待审核或一个在遗留系统之外没有意义的内部转换。如果下游服务必须理解物理表和隐晦的状态码,数据库模式已悄然成为了一份公共契约——而且是脆弱的。
一个转换层保护组织其他部分免受遗留系统物理表示的影响。CDC 在迁移之外也是战略性的:它可以填充搜索索引、数据仓库、读模型——而且最重要的是,它可以捕获一个 Outbox 表,传输应用刻意设计的事件而非技术噪声。
一个新的支付服务在同一时刻知道两个事实:提现改变了状态,描述该变更的事件必须最终被发布。它不执行两次独立写入,而是在一次本地事务中记录两个事实。
BEGIN;
UPDATE payouts
SET status = 'APPROVED', version = 17
WHERE id = 'pay_7f9q';
INSERT INTO outbox (
event_id, aggregate_id, aggregate_version,
event_type, payload, occurred_at
) VALUES (
'evt_01K...', 'pay_7f9q', 17,
'PayoutApproved', '{...}', CURRENT_TIMESTAMP
);
COMMIT;


这里的重复不是 Broker 损坏的证据——它是安全从不确定性中恢复的正确结果。
不要在"传递恰好发生一次"这个假设上构建正确性。要让消费者做到:重复传递不会重复业务效果。
工作的不变量:
记录原子一次。传递至少一次。效果应用一次。
Kafka 和 Redpanda 在定义的事务边界内提供精确一次语义。NATS 在配置的窗口内提供发布去重。这些都不能自动让 POST https://external-bank/pay 变成精确一次——Broker 控制不了银行。
Outbox 解决了一种形式的重复,不是全部。如果客户端在丢失响应后重试 POST /payouts,服务器可能在 Outbox 参与之前就创建了两个 payout。
每个事件都带有一个不可变的 eventId。消费者在应用效果之前记录该 ID:
BEGIN;
INSERT INTO inbox(event_id, received_at)
VALUES ('evt_01K...', CURRENT_TIMESTAMP)
ON CONFLICT DO NOTHING;
IF event_was_inserted THEN
UPDATE payout_projection ...;
END IF;
COMMIT;
如果 evt_01K... 再次到达,Inbox 约束会阻止重复的本地效果。仍然有一个边界:外部世界。
如果最后一跳——实际的银行转账——缺乏唯一性保证,内部去重毫无意义。

超时意味着 UNKNOWN,不是 FAILED。如果没有目的地的唯一性、与 Provider 的幂等性合约或协调循环,"精确一次"就不是保证——而是乐观。
"消息是有序的"是错误的说法。真正的问题是:相对于什么的顺序?

处理 v17 → v19 → v18 即使最终每个消息都到达,也可能产生无效状态。事件信封应携带 aggregateVersion,以便消费者检测过时事件、重复和序列间隙。
NATS subject 是路由地址;Kafka partition 是显式的存储和顺序边界——它们不等价。NATS 中的确定性路由需要一个不透明实体键一致地映射到分片:
finance.payouts.eu.s042.pk_7f9q.approved

并行 worker 保持到达顺序,不是完成顺序——如果外部副作用依赖顺序,仅靠存储顺序无法保护不变量。
必须能在断网时存活的分支、区域网关或边缘部署需要一个本地数据库和本地 Outbox,在连接恢复后同步一次。

NATS leaf 节点不会自动将本地数据库转变为无冲突的离线存储。架构仍需定义:每个实体哪个系统是权威的、如何解决冲突、如何检测间隙、本地磁盘满时怎么办、以及协调如何完成。
本地可用性加上最终全局收敛——而非独立于网络的全局一致性。

基本构建块:权威事务源、原子 Outbox、可靠发布、持久传输、不可变事件标识、幂等消费者、需要顺序处的顺序、确定性策略边界、幂等外部执行和协调。Broker 是其中的一个组件——不是架构本身。
{
"eventId": "evt_01K...",
"eventType": "PayoutApproved",
"aggregateId": "pay_7f9q",
"aggregateVersion": 17,
"schemaVersion": 3,
"occurredAt": "2026-08-07T10:57:01Z",
"correlationId": "corr_8x7b",
"causationId": "cmd_9c3a"
}
eventId 建立不可变标识。aggregateVersion 强制序列正确性。correlationId 和 causationId 端到端连接业务流。一旦 AI 智能体参与,这个元数据就变得至关重要。
"Payout blocked" 不是足够的审计跟踪。严肃的系统必须能够重建完整决策路径:
AI 输出是决策过程中的证据——它不应自动成为财务真相的来源。账本、策略服务和权限模型仍然是权威的。治理定义了最小化、哈希、保留和访问控制——目标是可重现性和问责制,不是监控。
显示绿色 CPU 和绿色运行时间的仪表板可能完全错过开篇场景中的故障——没有不可用,系统处于不确定状态。
追踪:CDC 延迟、发布延迟、消费者延迟、重投递率、重复检测计数、Inbox 冲突、序列间隙、死信增长、Outbox 积压、协调差异、Agent 失败、策略拒绝和外部超时率。
最危险的状态不是 service = DOWN。而是:
payment = UNKNOWN
这值得作为可协调的业务对象获得一等可见性——不是通用错误计数器里的一行。
事件系统广泛复制信息,这使得粗心的标识符代价高昂。不要在 topic/subject 名称中嵌入账号、卡号或身份证号——它们会泄露到日志、指标、追踪和仪表板。
❌ finance.account.447819002343.payment
✅ finance.payouts.eu.s042.pk_7f9q.approved
传输中加密和静态加密、细粒度访问控制、模式治理、受控回放、死信处理和数据驻留控制是不可妥协的。没有治理的持久性只会让错误存续更久。
架构应该遵循不变量。不变量不应该追随产品标识。
"这个 Broker 支持精确一次吗?"很少是有用的问题。有用的问题是:在哪里精确一次?
在一个 Kafka 事务内?在一个数据库内?跨 broker 和 Stripe?跨应用和银行?跨两个区域在分区期间?边界越宽,这个短语就越难捍卫。
只记录一次业务意图。
通知至少送达一次。
检测重复投递。
每个业务效果只应用一次。
对结果仍不确定的事物进行对账。
恰好一次不仅仅是传输层的属性。业务正确性是端到端的属性。
AI 并不废除分布式系统。智能体仍在网络上运行。工具仍会超时。数据库仍独立提交。消息仍可能重复。
AI 改变的是信息与行动之间的距离。当智能体系统获得操作权限时,分布式系统正确性变得更加重要,而非相反。智能体的可靠性主要不是一个提示工程问题——最重要的保证仍然存在于原子性、幂等性、排序、授权、隔离、可审计性和对账中。模型只是一个组件。
事务性发件箱模式通过在同一个本地事务中记录业务状态和发布意图,解决了异步系统中最难的边界之一。CDC 或中继将这个意图传输到 broker。Broker 提供持久化投递。收件箱模式和不可变事件标识符让消费者可以容忍重投。聚合版本保留业务排序。幂等性密钥保护命令和外部操作。策略服务将概率推理与确定性授权分离。对账解决了网络使结果不确定的情况。
AI 智能体并没有创造这些分布式系统问题。它们让忽视这些问题的代价更高。
一个重复事件现在可以传播得更远。重试可以激活更有能力的软件。不确定的结果可以触发影响客户、账户、基础设施或金钱的决策。
这就是为什么现代智能体架构不应该从询问哪个模型最聪明或哪个 broker 最快开始。它应该从业务不变量开始:
每笔资金流动必须有不可变标识符,来自授权的事务状态,在发布失败时存活,容忍重投,保留所需的业务顺序,通过确定性策略控制,以幂等方式执行,并保持可对账和可审计。
然后在一切看起来正常但确认永远不到来时,问出真正重要的问题:
你的系统能证明同一笔付款不会重复离开吗?
不是 Kafka 是否保持在线。不是数据库是否保持在线。不是你的 AI 智能体是否产生了正确的解释。
而是当网络停止告诉你发生了什么时,架构是否保持了现实。
Apache Kafka Documentation
Apache Kafka 4.0 — ZooKeeper Removal, KRaft-only Architecture
Debezium Outbox Event Router
Debezium Server + NATS JetStream Support
NATS JetStream Delivery & Acknowledgements
NATS JetStream Publication Deduplication
NATS Deterministic Subject Partitioning
NATS Leaf Nodes and JetStream Domains
RabbitMQ Queues, Quorum Queues, Streams
Redpanda Transactions & Kafka Compatibility