先记住这个答案
安全并行执行依赖已知子任务的核心是“无写冲突”和“上下文可合并”。写冲突指两个并行节点更新同一状态字段,例如同时写 summary,后写覆盖先写。可合并指每个节点结果应追加到不同键或用 reducer 聚合,如将 review_list 用 add reducer 追加。工程上以状态分键、只读共享输入,用图执行器并行触发,并为每个节点设计独立写集,即可保证安全。
- 并行需检查写集是否相交
- 状态键分区是常见的无锁方案
- 下游合并需显式 reducer 策略
写隔离与可合并输出的判定机制
已知依赖只保证拓扑序,不保证并发安全。两个无依赖子任务若都写同一个状态字段,比如分别产生中间结论却共用 analysis 键,后完成者覆盖先结果,导致信息丢失。因此规划后需枚举每个子任务的写集,检查两两交集是否为空。
即使写集无交集,还需定义上下文合并方式。LangGraph 等执行器要求每个节点返回状态更新字典,并行节点更新不同键即视为可合并;若同键需要保留多条,则需配置 reducer 如 add 将值追加为列表。只有满足这两个条件,才能安全并行。
多源调研中的并行分析节点
设 Agent 要对比三份报告并输出结论。解析节点输出 doc_text 后,三个分析节点 A、B、C 分别读该文本,写 analysis_A、analysis_B、analysis_C 字段,互不重叠。三者无依赖可并行,汇总节点等三者全部完成后再合并这些键成 final_report。
若某个分析节点失败,只重写自己的键,已完成其他键保留;汇总触发条件设为三个键均已非空,因此不会用到半成数据。实现时将每个分析节点设为独立分支,用 fan-out 并行执行,fan-in 用逻辑判断等待全部成功,从而在依赖图内做到无锁并行。
失效条件与串行化代价
当两个子任务写相同字段时,并行不安全。例如“更新库存”和“记录操作日志”都写 last_update 字段,并发会互相覆盖。即使引入锁,也会阻塞而丢失并行收益,且失败时恢复困难。更合理做法是合并节点或拆分字段。
还有隐含读写依赖:一个节点读取另一节点正要写的新值,但依赖图未声明。这需靠静态分析状态访问发现,一旦发现就把读取节点延后到写入完成后串行。代价是增加执行时间,但避免了脏读。状态分区虽增加 schema 复杂度,却是保并行性的长期手段。
容易答错的地方
- 依赖图无环即可并行
- 错误认为只要有向图无环,所有无直接边的节点都可同时运行。实际还要看写集是否冲突,例如两个节点都修改
user.balance则必须串行。安全并行需要额外检查状态写入重叠。 - 用全局锁解决写冲突
- 锁会让并发退化为串行,且 agent 常有长时间工具调用,锁易造成死锁。正确做法是让每节点只拥有自己的状态分区,天然免冲突;不能分区时才用串行或合并 reducer。
面试官还会怎么问?
如果必须保留两并行节点对同字段的独立结果怎么办?
把字段改成列表,用 reducer 对每条结果执行 add 追加,相当于写操作变成“追加”操作,不再覆盖。设计上应让每节点返回独立条目,而不是整体覆写字段。
执行中才发现两个并行节点实际要操作同一非状态资源(如发送邮件)怎么办?
无法通过状态写入隔离解决,需强制串行或引入资源级锁。在 agent 中可在计划里为这类节点增加同一把“互斥锁”,但锁等待可能延迟任务,最好规划期就识别副作用资源。
用了 LangGraph 的 StateGraph,如何表示并行节点间的写隔离?
为每个并行节点定义独立的状态键,且不要在 StateGraph 的 annotated 字段里对它们配 reducer。节点返回 {key: value},LangGraph 会并用不同 key 更新,互不干扰;汇总节点直接读取这些键的值进行合并即可。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。