跳到主要内容

并行智能体系统中的隐性数据损坏问题

· 阅读需 14 分钟
Tian Pan
Software Engineer

当一个多智能体系统开始出现奇怪的行为——给出不一致的答案、丢失任务追踪、做出与早期推理相矛盾的决策——本能反应是责怪模型。调整提示词,换一个更强的模型,添加更多上下文。

真正的原因往往更平凡,也更危险:并发写入导致的共享状态损坏。两个智能体读取同一块内存,都计算出更新,其中一个默默地覆盖了另一个。结果状态在技术上是有效的——没有异常抛出,没有模式违规——但在语义上是错误的。之后读取它的每一个智能体都在对错误信息进行正确的推理。

这种故障模式在单个操作层面是不可见的,在测试环境中难以复现,仅通过查看输出几乎无法与模型错误区分开来。O'Reilly 2025年关于多智能体内存工程的研究发现,36.9%的多智能体系统故障源于智能体间的不对齐——智能体在对共享信息的不一致视图上运行。这不是一个理论上的顾虑。

为什么这看起来像模型故障

共享内存损坏的阴险之处在于它在起源地的几步之后才显现出来。一个协调器派生出五个并行研究智能体。智能体A和B都读取共享任务队列(计数:3),都处理任务,都将结果写回。智能体B的写入最后落地,默默地覆盖了智能体A的写入。任务计数仍然显示为3,但智能体A的工作消失了。

现在,合成智能体D读取"3个任务完成",并接收来自4个智能体(而非5个)的输出。它对接收到的数据进行完美推理——但那些数据是错误的。最终合成看起来像幻觉或推理错误。如果你再次以串行方式运行相同的工作流,它运行良好。这个bug只在并发负载下出现,这意味着它完全逃过了大多数开发环境测试。

时间窗口使这个问题更加严重。在运行20到50个并发智能体的生产系统中,在测试环境中需要微秒级精度才能复现的竞态条件会例行发生。你无法按需触发它们。你只能提前对它们进行检测。

三种故障模式

并行智能体系统中的共享内存竞争以三种不同的模式表现出来:

丢失更新。 智能体A读取balance = 100,智能体B读取balance = 100,智能体A写入95,智能体B写入150。最终状态:150。智能体A的工作消失了。这是经典的读-改-写竞态条件。用数据库术语来说,这是导致丢失更新的不可重复读。

脏读。 智能体A执行多步骤状态变更——任务开始"处理",数据转换,状态更新为"完成"。智能体C在变更中途读取,看到部分更新的状态:任务处于"处理"状态,但下游计数尚未更新。智能体C对这种部分状态进行推理并做出决策,一旦智能体A的变更完成,这些决策就会变得不一致。

级联污染。 单个竞态条件造成的损坏状态向下游扩散,因为其他智能体将其纳入自己的推理。Galileo AI的模拟发现,单个损坏的状态值在引入后四小时内毒化了87%的下游决策。这种毒性之所以传播,是因为每个后续智能体都将损坏的数据视为基本事实。

所有三种故障模式都共享同一个特征:单个操作看起来有效;只有当你检查跨时间的多个操作之间的关系时,不一致性才会出现。

将数据库隔离级别应用于智能体内存

分布式系统领域对这些确切问题有数十年的艰难获得的解决方案。数据库隔离级别——未提交读、已提交读、可重复读、可序列化——不是特定于数据库的概念。它们描述了任何共享状态系统都可以实现或近似实现的一致性保证。

未提交读意味着智能体可以读取并发智能体正在修改过程中的状态。对于偶尔的陈旧读取可以接受的超低延迟系统很有用。对于部分状态在语义上无效的任何情况都很危险。

已提交读意味着智能体只看到已提交的更改。防止脏读,但允许这样一种情况:在单个智能体执行过程中,如果另一个智能体在两次读取之间提交,同一次读取可能返回不同的结果。这是大多数多智能体框架中的默认一致性模型——比大多数工程师假设的要弱。

可重复读保证在单个智能体的逻辑事务中,同一次读取始终返回相同的值。智能体在其推理持续期间获得共享状态的一致快照。对该快照的并发更新被推迟到智能体完成之后。这适用于对共享数据进行多步骤决策的智能体。

可序列化是最强的保证:行为与智能体按某种顺序依次执行相同。并发执行在实现层面发生,但可观察到的结果与某种串行顺序匹配。这适用于只能发生一次的操作——认领任务、更新共享计数器、分配资源。

关键洞察是不同的内存区域需要同时具有不同的隔离级别。每个智能体应恰好认领一个项的共享任务队列需要可序列化的一致性。多个智能体只追加结果的共享发现库只需要已提交读。私有智能体草稿本根本不需要协调。将所有共享内存视为单一一致性域既过度设计(将所有内容锁定为可序列化会扼杀吞吐量),也欠缺设计(在所有地方应用最弱级别会在关键资源上创建竞态条件)。

为什么最后写入者胜利是错误的

最简单的冲突解决策略——当两个智能体写入冲突的值时,保留最近时间戳的那个——对于分布式系统,进而对于分布式智能体协调,从根本上是错误的。

时钟偏差是问题所在。即使使用NTP同步,机器时钟也会漂移数百毫秒。服务器1上的智能体A(时钟快100毫秒)在"系统时间10:00:05.000"写入。服务器2上的智能体B在"系统时间10:00:04.950"写入。智能体A胜出,因为它的时间戳更晚——即使智能体B根据实际现实时间先写入。最后写入者胜利不会选择最近的写入;它选择来自时钟最超前的机器的写入。

现代框架已经认识到这一点。LangGraph明确地用确定性归约函数替换了最后写入者胜利:"在并行分支收敛时,编排器根据预定义的状态转换规则确定性地合并片段,确保一致且可重现的状态演化。"合并两个并发写入的结果由合并函数定义,而不是由哪个写入先到达。

实际有效的冲突解决策略

基于归约的合并是大多数智能体用例最实用的方法。不是覆盖,而是定义一个合并并发更新的函数:

  • 对于任务列表:追加语义——两个智能体的添加都被保留
  • 对于计数器:加法语义——总计反映所有智能体的增量
  • 对于标志:根据语义使用交换性的OR或AND

LangGraph使这一点明确。开发者为每个状态字段指定一个归约器。如果两个并行智能体都向列表追加发现,两组发现都会保留。合并是确定性的、可重现的,且与时序无关。

带版本控制的乐观并发处理合并语义不存在的情况——正确行为是"恰好有一个智能体应该成功"的情况。每个状态值携带一个版本号。智能体读取当前值和版本,在本地修改值,然后仅在版本未更改时写回:UPDATE state SET value = new_value, version = version + 1 WHERE id = ? AND version = old_version。如果另一个智能体先写入,版本检查失败,当前智能体重试。不持有锁,不可能发生死锁。权衡是:这在冲突率低(5%以下)时效果良好。当许多智能体竞争同一资源时,大多数重试失败,吞吐量崩溃。对于任务队列等高竞争资源,具有原子出队操作的队列抽象更为合适。

用于因果追踪的向量时钟为系统提供了第三种方法,你需要知道的不仅仅是"最新值是什么",而是"这次写入是否发生在那次之后,还是与那次并发"。每个智能体维护一个逻辑时间戳向量。当智能体交换消息时,它们通过逐元素取最大值来合并向量。如果一个事件的向量严格小于另一个事件的向量,则这两个事件具有因果关系。这使得冲突解决逻辑能够推理顺序,而无需依赖同步时钟。

**CRDT(无冲突复制数据类型)**通过将状态限制为所有操作都可交换的数据结构,完全消除了冲突问题。只增计数器、仅追加日志和观察-删除集合是例子。多个智能体可以并发更新,所有副本都会收敛到相同的状态,无论更新顺序如何。限制是CRDT只适用于具有自然可交换操作的数据。你无法将"将此任务分配给恰好一个智能体"表达为CRDT。

现有框架的对与错

LangGraph是最接近正确解决方案的。状态作为不可变的类型化字典流经图。并行节点在"超步"中执行——所有节点接收相同的状态快照,并发运行,并在下一步开始之前通过归约器合并输出。检查点在每个超步后保存一致状态,支持恢复和重放。该模型是合理的:不可变性防止隐藏的变更,归约器强制对并发更新进行明确推理,超步提供自然的同步点。差距在于归约器只能合并值;它们不能强制执行跨字段不变量或保证原子任务分配。

AutoGen默认采用基于时间TTL过期的最终一致性。默认情况下对话是串行的;并发智能体对具有隔离的转录,没有新鲜的共享状态。对于严格独立的智能体,这是可以的。对于需要在积累过程中共享发现的智能体,这是不够的——缓存失效是基于时间而不是事件驱动的,这意味着智能体可能对同时在其他地方被更新的状态采取行动。

CrewAI使用SQLite存储任务状态,在crew内提供事务保证。基于角色的隔离减少了跨智能体污染。SQLite在高并发下成为瓶颈,因为它不支持并发写入。对于最多10个智能体的crew,这是可行的;超过这个数量,序列化开销就很显著了。

OpenAI Agents SDK根本不提供共享状态基础设施。智能体只共享通过上下文参数传递的内容。这适用于以映射-归约模式运行的独立智能体,但智能体之间任何有意义的协调都是开发者负责正确实现的。

重要的工程模式

在编写任何协调代码之前,按隔离需求对内存进行分类。 可序列化区域(任务队列、资源分配、任何具有"恰好一次"语义的内容)需要强一致性。仅追加区域(事件日志、发现库)可以使用更弱的一致性和基于合并的冲突解决。读密集型配置数据需要快照隔离。私有草稿本什么都不需要。混淆这些——无论是过度锁定还是锁定不足——是大多数生产故障的根本原因。

使每次状态写入都是幂等的。 在并发环境下重试是不可避免的,非幂等操作会复合错误。不要"将计数器增加1",而要写"如果版本匹配则将计数器设置为(值+1),从操作输入生成确定性ID"。同一幂等操作的多次执行产生相同的状态。

添加明确的同步点。 在LangGraph中,超步是同步点——下一步中没有智能体在当前步骤中的所有并行智能体完成之前开始。如果你在框架之外构建协调,同步点需要被明确设计。智能体D不应该在智能体A、B和C都提交了更新之前就开始对合并状态进行推理。隐式同步(等待任意时间、检查时间戳)是等待发生的竞态条件。

检测状态变更,而不仅仅是智能体输出。 记录每次状态读取和写入的分布式审计日志——智能体ID、时间戳、前后版本、值——是在生产故障发生时重建发生了什么的唯一方法。没有这个,事后分析需要靠猜测。有了它,你可以追踪产生损坏状态的确切读写序列。

在每次写入后验证状态不变量。 在提交状态更新之前,检查结果状态是否内部一致:任务计数等于待处理和已完成任务的总和,每个任务最多分配给一个智能体,没有智能体同时出现在空闲和工作队列中。这大约花费1-5%的运行时开销。它在损坏级联到下游智能体之前捕获损坏。

安全维度

共享内存竞争创造了一个容易被忽视的攻击面。在多个智能体写入共享状态的高并发窗口期间,对任何一个智能体具有写访问权限的攻击者可以注入看起来像合法智能体输出的条目。被污染的条目在会话间持续存在,并在未来的智能体检索"相关"内存时激活。

MemoryGraft攻击模式(在NeurIPS 2025上记录)直接利用了这一点:将成功的经验植入智能体的情节记忆中,未来的智能体将其检索并重放恶意行为,就好像它是从先前的成功中学到的一样。检测很困难,因为单个记忆条目在隔离状态下看起来是有效的;恶意效果只在特定的检索上下文中激活。

缓解措施包括按角色的写访问控制(只有某些智能体可以写某些内存区域)、验证内存条目自创建以来未被修改的加密承诺,以及减少旧条目影响的时间衰减——限制任何单个被污染记录的持久性。

前进的道路

关于多智能体共享状态的正确心智模型不是"我们有一些智能体更新的共享字典"。而是"我们有一个分布式数据库,智能体是客户端,我们需要明确地为每种数据类型推理一致性级别、冲突解决和隔离"。

做到这一点的框架——最显著的是LangGraph基于归约器的状态模型——在系统构建之前迫使开发者明确做出这些决定。归约器和超步屏障不是样板代码;它们是防止隐性损坏的一致性语义的明确声明。

对于在不提供此基础设施的框架上构建的团队,投资是直接的:对共享内存进行分类,为每个类别选择适当的隔离机制,实现幂等写入,添加同步点,并检测状态变更。工具是可用的。模式是众所周知的。不这样做的代价是工作流在生产中以看起来像模型错误但实际不是的方式失败。

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