当单一Agent的上下文窗口、工具权限与推理职责被无限扩张时,系统并不会变得更"智能",反而会更快滑向不可调试、不可恢复的泥潭。本文的出发点是一个朴素但常被忽视的判断:多智能体系统的核心挑战不在于单个Agent的提示词技巧,而在于Agent之间的架构边界。我们关注的不是"如何让一个Agent回答得更好",而是"当三个、十个、三十个Agent协同完成一项复杂任务时,系统如何保持可维护、可恢复、可观测"。
从单Agent到多Agent的迁移并非简单的数量叠加。当任务本身可以被分解为多个相对独立的子问题时,单一Agent被迫在同一个上下文中同时处理需求解析、代码生成、安全审查与测试验证,这不仅导致上下文污染与注意力稀释,还使工具权限边界变得模糊------一个本应只读代码的审查步骤,可能因为共享同一工具命名空间而意外获得写权限。多Agent协作的本质驱动力因此可以归结为三点:任务分解 (让每个Agent只面对足够小且明确的问题)、工具边界 (按职责最小化授予工具权限)与上下文隔离(避免无关信息干扰推理过程)。这三点共同指向同一个架构结论:必须有一个独立于具体Agent能力的编排层,来定义"谁在什么时候做什么、能碰什么、不能碰什么"。
本文系统阐述的正是这个编排层的设计方法。我们提出以编排器---执行器---共享状态三元结构作为多智能体系统的逻辑骨架。编排器负责解析任务依赖、决定执行顺序与处理分支逻辑;执行器是实际承担推理与工具调用的Agent,它们对全局状态无直接写权限;共享状态则作为系统唯一的持久化事实源,记录任务进度、中间产物与失败信息。这一职责切分不是教条,而是生产环境下的必然选择:没有清晰的共享状态,任何故障恢复都无从谈起;没有编排器与执行器的隔离,局部失败会以不可控的方式蔓延为全局失败。
在编排模式上,本文对比了三种主流方案。DAG工作流 将任务依赖静态建模为有向无环图,适合流程稳定、步骤明确的场景,其优势在于执行路径可预测、易于审计,但面对动态变化的任务结构时显得僵硬。动态规划器 让一个核心Agent根据当前状态实时决定下一步动作,灵活但引入了额外的决策不确定性与循环风险。事件驱动Agent通信则让Agent通过事件总线异步交互,解耦程度最高,但一致性保障与调试难度也随之上升。三者并非互斥,实际系统中常见的做法是以DAG为骨架、在局部节点嵌入动态决策、在跨系统边界处引入事件通知。本文给出了选型判断依据,帮助读者根据任务特征而非技术偏好做出选择。
共享状态的设计是本文的另一重点。我们基于Redis与Postgres两种基础设施,分别讨论了状态快照、分布式锁与乐观并发控制的实现方式。状态快照 解决的是"任务执行到一半系统崩溃后从哪里恢复"的问题;锁与乐观并发控制 解决的是"多个Agent同时尝试修改同一状态时如何避免写冲突"的问题。文中特别强调了一个常被忽视的细节:Agent执行的副作用(如写入外部代码仓库、发送通知)必须与状态更新解耦,否则任何重试都可能造成重复副作用。这正是幂等执行成为强制要求的原因------每个执行器操作必须设计为可安全重复调用,或在调用前通过唯一执行ID进行去重。
故障隔离与重试策略部分,我们直面多Agent系统中最棘手的部分失败问题。一个包含十个步骤的任务,第九步失败时,系统是否要回滚前八步?哪些步骤需要回滚,哪些可以保留?本文给出的答案是:以Agent为故障边界,以状态快照为回滚依据,以幂等键为安全重试前提。每个Agent执行单元拥有独立超时、独立重试次数与独立错误分类(可重试错误、不可重试错误、部分成功)。编排器根据错误分类决定是重试、跳过还是触发补偿操作。这种设计将"部分失败"从灾难性事件降级为可管理的常规分支。
作为贯穿全文的实证案例,我们拆解了一个多Agent代码评审系统:需求解析Agent将PR描述转化为结构化评审清单,代码分析Agent对变更文件做静态审查,安全Agent检查依赖漏洞与敏感信息泄露,最终由汇总Agent生成评审报告。该系统完整展示了编排器如何协调三个执行器、共享状态如何记录每个文件的评审进度、以及当安全Agent超时失败时系统如何在不丢失已产出结果的情况下完成降级输出。文中给出的实现基于Java与Spring Boot,包含可运行的核心代码,读者可以直接将其作为多Agent系统的最小可行架构参考。
本文不讨论某个具体大模型API的调用细节,也不陷入提示词工程的技巧罗列。我们的焦点始终放在架构层面:当系统由多个Agent组成时,哪些决策必须提前做出,哪些边界必须显式定义,哪些失败必须预先设计应对方案。读完本文,读者应当能够回答三个问题:我的任务适合用哪种编排模式?我的共享状态应该存什么、怎么存?当某个Agent失败时,系统如何有尊严地恢复而不是无声地崩溃?
这正是生产级多智能体系统与演示级Agent脚本之间的分水岭。
引言:为什么多智能体架构是必然选择
在构建第一个基于大语言模型的自动化系统时,几乎所有团队都会走上同一条路:把一切能力塞进一个 Agent。这个 Agent 拥有二十个工具、一篇长达五千字的系统提示词、对所有数据库表的读写权限,以及一个被塞满历史对话的上下文窗口。在演示环境中,它表现得令人惊叹;在测试数据上,它通过了所有验收用例。然而当它被部署到生产环境的第三周,问题开始以不同形式涌现:响应延迟从三秒膨胀到四十秒,工具调用偶尔互相冲突,一次失败的数据库写入让整个对话链陷入不可恢复的状态,而调试日志显示------Agent 在某个步骤中"自信地"调用了错误的工具,且没有任何机制能够阻止它。
这不是提示词写得不够好,也不是模型能力不足。这是一个架构问题。
单 Agent 的三大结构性缺陷
单一 Agent 架构的困境并非来自模型本身的局限性,而是源于我们在工程层面赋予它的三种互相矛盾的职责。当这些职责被压缩进同一个推理循环时,系统会以可预测的方式走向失稳。
第一,上下文膨胀导致注意力稀释。 一个承担复杂任务的 Agent 需要同时理解用户意图、维护任务进度、记忆工具返回结果、遵循安全策略,并在每一步选择下一个动作。这些信息全部堆积在同一个上下文窗口中。随着任务步骤增加,早期关键信息被逐渐挤出注意力核心区域,Agent 开始重复调用已经执行过的工具、遗忘既定约束、或在无关信息上过度推理。上下文窗口不是无限的,即使技术上将窗口扩展到百万 token,模型对窗口中部信息的有效利用效率也会显著下降。更致命的是,上下文膨胀直接推高每次推理的成本与延迟,形成"越复杂越慢、越慢越复杂"的恶性循环。
第二,工具边界模糊引发调用冲突。 当所有工具都暴露给同一个 Agent 时,模型必须在每一次推理中从庞大的工具集合里做选择。工具之间的语义重叠------例如"查询数据库"与"查询缓存"两个工具------会让模型产生选择歧义;工具之间的副作用冲突------例如一个工具写入主库、另一个工具触发消息推送------则可能在错误的时机被错误地串联执行。更隐蔽的问题是权限污染:一个需要执行高风险操作的 Agent 同时也拥有读取低风险信息的工具,这使得最小权限原则在单 Agent 架构中几乎无法落地。一旦 Agent 被提示注入攻击诱导,攻击面就是所有可用工具的并集。
第三,不可维护性与不可恢复性。 单 Agent 的行为逻辑本质上是一个隐式的、由模型动态生成的控制流。它没有明确的执行边界,没有可独立测试的单元,也没有可重放的执行轨迹。当系统在某个步骤失败时,开发者面对的问题不是"哪个模块出错了",而是"模型为什么在那个时刻做出了那个决定"。重试整个对话链代价高昂,部分重试又缺乏明确的边界定义。随着任务复杂度增长,提示词补丁越打越多,系统行为越来越难以预测,最终演变为无人敢改动的"提示词泥潭"。
多智能体架构的三个核心驱动力
多智能体架构的本质,是将一个不可控的单一推理循环,拆解为多个边界清晰、职责单一、可独立治理的协作单元。这种拆解由三个核心驱动力推动,它们分别对应上述三大缺陷。
驱动力一:任务分解。 复杂任务天然具有层次结构。一个代码评审任务可以被分解为"读取变更集""静态分析""架构一致性检查""安全扫描""汇总报告"五个子任务,每个子任务有自己的输入、输出和完成标准。多智能体架构允许我们将每个子任务分配给专门的 Agent,每个 Agent 只需要理解自己领域的上下文和决策规则。这不仅缩小了每个 Agent 的上下文规模,更重要的是,它让任务流程从模型的隐式推理变为系统的显式编排。编排层可以定义子任务的依赖关系、执行顺序和分支条件,使整个流程可观测、可测试、可优化。
驱动力二:工具边界。 在多智能体架构中,工具不再是全局共享的资源池,而是按 Agent 职责进行划分。负责数据库查询的 Agent 只有读权限,负责写入的 Agent 只有写权限,负责消息推送的 Agent 不接触任何业务数据。这种边界划分使得最小权限原则得以真正落地:每个 Agent 的攻击面被限制在其职责所需的工具子集内。同时,工具冲突问题被结构性消除------不存在两个语义重叠的工具被同一个推理循环同时考虑的情况,因为它们在架构层面就归属于不同的 Agent。
驱动力三:上下文隔离。 每个 Agent 拥有独立的上下文窗口,只包含执行其子任务所需的最小信息集。上游 Agent 的输出经过结构化处理后作为下游 Agent 的输入,而不是简单地将整个对话历史传递下去。这种隔离带来了三重收益:每个 Agent 的推理质量因上下文精炼而提升;推理成本因上下文缩减而显著下降;故障影响范围因上下文隔离而被限制在单个 Agent 内部------一个 Agent 的上下文污染不会级联到整个系统的推理链中。
从"必然选择"到"正确实现"的距离
承认多智能体架构的必然性只是起点。接下来的问题是:如何正确地实现它? 多智能体系统的失败案例中,很少有团队是因为"选择了单体架构"而失败的,更多的是因为在向多智能体迁移时,把单体的问题换了一种形式重新引入。
最常见的错误是将多个 Agent 简单地串联起来,却让它们共享同一个全局状态存储和同一个消息通道。这种设计在表面上是多智能体,实质上仍然是单体------Agent 之间的边界只存在于提示词层面,而不存在于架构层面。当状态被任意读写、消息被无序广播、故障无法定位到具体 Agent 时,系统面临的不是三个单 Agent 的问题,而是三个单 Agent 的问题互相纠缠后产生的组合爆炸。
本文的后续章节将围绕三个架构支柱展开:任务编排 (如何定义 Agent 之间的协作流程)、共享状态 (如何在多 Agent 之间安全地传递和持久化信息)、故障隔离(如何让单个 Agent 的失败不会拖垮整个系统)。我们将对比 DAG 工作流、动态规划器与事件驱动通信三种编排模式的适用场景与设计取舍;讨论基于 Redis 和 PostgreSQL 的状态快照、锁机制与乐观并发控制;设计 Agent 级超时、幂等执行与部分失败回滚的实践方案;最后通过一个多 Agent 代码评审系统的完整拆解,展示这些设计原则如何在真实代码中落地。
读完本文,你将获得的不是另一套"多智能体提示词模板",而是一组可迁移的架构设计能力:能够判断什么任务需要拆解、如何拆解、拆解后如何编排、状态如何流转、故障如何恢复。 这些能力,才是多智能体系统从原型走向生产环境的分水岭。
问题与挑战:多Agent协作中的核心痛点
上一章我们论证了从单Agent走向多Agent协作的必然性。然而,"多个Agent一起工作"这件事本身并不会自动带来架构上的胜利。恰恰相反,多Agent系统的复杂性增长曲线远比功能增长曲线陡峭。如果不在一开始就正视并解决几个核心架构问题,团队最终得到的很可能不是一个更强大的系统,而是一堆互相干扰、状态漂移、难以调试的"数字员工"在系统里各自为政。
本章将深入剖析多Agent协作中最常见的四类架构痛点:状态一致性难保障、部分失败难恢复、Agent间通信耦合度高、编排逻辑与业务逻辑混杂。理解这些痛点的本质,是后续章节设计编排层、状态层与故障隔离机制的前提。
2.1 状态一致性难保障:谁拥有"真相"?
单Agent系统中,状态管理相对简单------对话历史、工具调用结果、中间推理步骤都可以放在一个执行上下文中。但在多Agent系统中,状态被天然地分散到了多个执行单元中。
考虑一个典型的场景:一个"需求分析Agent"将解析后的用户故事写入了共享工作区,一个"架构设计Agent"基于这份用户故事生成了技术方案,而一个"代码生成Agent"正在根据技术方案编写代码。此时如果需求分析Agent因为某种原因重新运行并更新了用户故事,架构设计Agent和代码生成Agent的工作就建立在了一个已经过期的状态之上。
这种状态漂移问题的根源在于:多Agent系统缺乏一个被所有参与者共同认可的、具有明确版本语义的"真相源"(Source of Truth)。每个Agent都倾向于维护自己的局部状态副本,而这些副本之间没有同步机制。当Agent数量增加、执行时间变长时,状态不一致的概率呈指数级上升。
在实践中,我们观察到三种典型的状态不一致模式:
| 模式 | 描述 | 典型后果 |
|---|---|---|
| 读-改-写竞态 | 两个Agent同时读取同一状态,基于旧值做出决策后先后写回 | 后写回的Agent覆盖了先写回Agent的更新 |
| 幽灵依赖 | Agent A依赖Agent B的输出,但B在执行过程中被重试或替换,A持有的引用已失效 | A基于不存在或已变更的中间结果继续执行 |
| 跨步骤状态漂移 | 工作流中某个步骤的状态被外部系统修改,而编排器未感知 | 后续步骤基于过期快照执行,产生级联错误 |
解决状态一致性问题的核心思路不是"让所有Agent共享内存"------这在分布式环境中既不现实也不安全。正确的方向是引入显式的状态存储层,并为之设计清晰的读写协议和版本控制机制。这一点我们将在第四章详细展开。
2.2 部分失败难恢复:一个Agent挂了,整个流程怎么办?
在单Agent系统中,失败通常意味着整个任务失败,处理方式相对简单:记录错误、通知用户、可能重试一次。但在多Agent系统中,失败是局部的、异步的、并且往往发生在工作流的中途。
设想一个包含五个步骤的多Agent工作流:数据采集Agent成功执行,数据清洗Agent成功执行,特征工程Agent在执行到80%时因为外部API限流而超时失败,模型训练Agent和报告生成Agent还在等待上游结果。此时系统面临的核心问题不是"要不要重试",而是:
- 已经完成的前两步工作是否需要全部重做? 如果重做,浪费了计算资源和时间;如果不重做,如何保证重试后的特征工程Agent拿到的输入与之前一致?
- 正在等待的两个Agent应该被取消、挂起还是继续等待? 如果取消,它们的部分初始化工作如何清理?如果继续等待,超时时间如何设定?
- 特征工程Agent自身已经写入的中间状态如何处理? 这些状态可能是不完整的、自相矛盾的,如果不清理,后续即使重试成功也可能读到脏数据。
这些问题的本质是:多Agent系统缺乏一个统一的故障域模型。哪些失败是独立的、可以局部重试的?哪些失败是关联的、需要级联回滚的?哪些操作是幂等的、可以安全重复执行的?如果没有在架构层面明确回答这些问题,开发团队就只能依靠大量的try-catch和手工补偿逻辑来"打补丁",最终系统会变得无法维护。
一个特别值得警惕的反模式是**"全有或全无"的重试策略**------即任何一个Agent失败就从头开始整个工作流。这种策略在演示中看似"安全",但在生产环境中会带来严重的资源浪费和用户体验问题。更合理的做法是基于依赖关系构建细粒度的恢复单元,让系统只重做真正受影响的部分。
2.3 Agent间通信耦合度高:网状依赖的泥潭
当系统中有三个Agent时,让它们直接互相调用似乎是最高效的方案:Agent A调用Agent B获取数据,Agent B调用Agent C进行验证,Agent C又回头询问Agent A某个参数。这种点对点通信在Agent数量少时运转良好,但随着系统演进,通信拓扑会从简单的链式结构退化成一张密不透风的网状结构。
网状通信带来的问题远不止"看起来混乱"这么简单:
第一,认知负担急剧增加。 每个Agent都需要知道它应该和谁通信、通信的协议是什么、对方失败时应该如何处理。这意味着修改任何一个Agent的行为都可能影响到所有与它直接或间接通信的Agent。
第二,循环依赖的风险。 当Agent之间的调用关系不受约束时,很容易形成A→B→C→A的调用环。这种循环在静态分析中可能不明显,但在运行时会导致死锁、无限递归或状态震荡。
第三,可观测性急剧下降。 当一次请求需要经过六七个Agent的层层转发才能完成时,追踪一个具体决策的"责任链"变得极其困难。出了问题之后,团队往往需要花费大量时间在日志中拼凑调用路径。
解决通信耦合问题的关键在于引入一个中介层------编排器或事件总线------来打破Agent之间的直接依赖。Agent不再需要知道"谁需要我的输出",只需要将结果发布到约定的位置;也不再需要知道"我应该调用谁来获取输入",只需要声明自己依赖什么。这种间接通信模式将通信拓扑从O(n²)的网状结构降为O(n)的星型结构,大幅降低了系统复杂度。
2.4 编排逻辑与业务逻辑混杂:最隐蔽的架构腐蚀
最后一个痛点是最隐蔽的,因为它不会直接导致系统崩溃,却会缓慢而坚定地侵蚀系统的可维护性。这就是编排逻辑与业务逻辑的混杂。
所谓编排逻辑,指的是决定"什么任务在什么条件下由哪个Agent执行、失败后如何处理、结果如何传递"的控制流代码。所谓业务逻辑,指的是Agent内部执行具体任务时所使用的提示词、工具调用策略、数据转换规则等。在早期的多Agent实现中,这两种逻辑往往被写在同一个代码文件里,甚至写在同一个函数里。
以下是一个典型的反模式示例(使用Python伪代码展示问题,而非推荐做法):
python
# 反模式:编排逻辑与业务逻辑混杂
def run_code_review_pipeline(code: str) -> dict:
# 编排逻辑:决定先做静态分析
static_result = call_llm(
system_prompt="你是一个静态代码分析专家,检查代码中的语法错误和代码风格问题...", # 业务逻辑
user_message=code,
timeout=30, # 编排逻辑
retry_count=3 # 编排逻辑
)
# 编排逻辑:根据静态分析结果决定是否继续
if static_result.severity > 0.7:
# 业务逻辑:安全审查的提示词
security_result = call_llm(
system_prompt="你是一个安全审查专家,重点关注SQL注入、XSS和权限绕过...",
user_message=code
)
# 编排逻辑:合并结果
return {"static": static_result, "security": security_result}
else:
# 编排逻辑:跳过安全审查,直接进入性能分析
performance_result = call_llm(
system_prompt="你是一个性能分析专家,关注算法复杂度和资源使用...",
user_message=code
)
return {"static": static_result, "performance": performance_result}
这段代码的问题在于:如果你想修改"什么条件下跳过安全审查"这个编排策略,你不得不同时阅读和理解三个Agent的提示词。反过来,如果你想优化安全审查Agent的提示词,你也不得不小心翼翼地确保不会破坏编排逻辑中的条件判断。当这样的代码在系统中积累到几十个分支时,任何修改都变成了高风险操作。
更糟糕的是,这种混杂使得独立测试变得几乎不可能。你无法单独测试编排逻辑而不触发真实的LLM调用,也无法单独测试某个Agent的提示词效果而不运行整个流程。开发和调试的效率因此大幅下降。
2.5 架构设计需要解决的具体目标
基于以上四个核心痛点的分析,我们可以提炼出多Agent系统架构设计需要明确解决的具体目标。这些目标不是抽象的口号,而是后续章节中每一个设计决策的评判标准:
目标一:建立单一真相源。 系统必须有一个明确的状态存储层,所有Agent的读写操作都通过这个存储层进行,且存储层需要支持版本控制和冲突检测。这是解决状态一致性问题的根本手段。
目标二:实现细粒度故障恢复。 系统必须能够识别工作流中哪些步骤是幂等的、哪些步骤需要补偿操作、哪些步骤的失败需要级联处理。故障恢复的粒度应该是"步骤级"而非"工作流级"。
目标三:解耦Agent间通信。 系统必须通过编排器或事件机制来中介Agent之间的信息传递,Agent之间不应存在直接的、硬编码的调用关系。这既降低了耦合度,也提升了系统的可演化性。
目标四:分离编排与执行。 编排逻辑应该是一等公民,拥有自己的代码结构、配置方式和测试策略,与Agent内部的业务逻辑(提示词、工具选择、输出格式)严格分离。
目标五:保持可观测性。 架构设计必须考虑到追踪、日志和审计的需求。每一次Agent调用、每一次状态变更、每一次故障恢复都应该能够被追溯。
在接下来的章节中,我们将首先讨论多Agent系统的核心架构模式------编排器-执行器-共享状态的三层结构,然后深入状态存储的具体设计,最后通过一个完整的代码示例来展示这些原则如何落地。理解本章所剖析的痛点,是理解后续设计决策必要性的关键前提。
核心概念:编排器、执行器与共享状态
上一章我们确认了一个判断:多Agent系统的复杂性不在于Agent数量,而在于它们之间的协作结构 。本章将这一判断落实为三个可操作的架构角色------编排器(Orchestrator) 、执行器(Executor) 与共享状态(Shared State)。这三个角色是多智能体系统从"能跑"走向"能维护"的基石。理解它们各自的职责边界,以及它们之间如何通过状态契约协作,是后续所有故障隔离、并发控制与恢复策略的前提。
3.1 为什么需要明确的三层职责
一个常见的反模式是:每个Agent都拥有完整的任务上下文、都可以调用任何工具、都可以直接与其他Agent通信。这种"全连接"结构在Agent数量超过三个时就会迅速退化------状态散落在各个Agent的本地记忆中,任务进度无法被外部观测,故障时无法判断"谁该负责重试"。
编排器、执行器、共享状态的分离,本质上是把"做什么""谁来做""做到哪了"三个问题拆开:
| 角色 | 核心职责 | 不应做的事 |
|---|---|---|
| 编排器 | 任务分解、依赖管理、调度决策、失败处理策略 | 不直接操作领域工具,不持有执行细节 |
| 执行器 | 完成单一领域任务,返回结构化结果 | 不决定"下一步做什么",不直接与其他执行器通信 |
| 共享状态 | 跨Agent上下文传递、进度持久化、并发控制 | 不包含业务逻辑,不主动触发动作 |
这一划分借鉴了工作流引擎(如Temporal、Airflow)与微服务架构中长期验证过的模式,但针对LLM Agent的非确定性特征做了适配。下面逐一展开。
3.2 编排器:任务分解与调度的唯一入口
编排器是多智能体系统的"指挥中枢"。它接收一个高级目标,将其分解为若干可执行的子任务,并决定这些子任务的执行顺序与依赖关系。在实现层面,编排器通常表现为一个确定性的状态机或工作流定义,而不是另一个LLM Agent。
为什么编排器不应是LLM Agent? 因为调度决策需要可预测、可审计、可重放。如果用LLM来做调度,每次推理都可能产生不同的分解方案,这会给故障恢复带来灾难性的不确定性。更稳健的做法是:编排逻辑用代码表达,任务内容用LLM生成。
java
// 编排器示例:以确定性DAG定义任务依赖
public class CodeReviewOrchestrator {
private final TaskGraph graph;
private final ExecutorRegistry registry;
private final SharedStateStore stateStore;
public void run(String taskId) {
// 从共享状态加载或初始化任务图
TaskGraph graph = stateStore.loadTaskGraph(taskId)
.orElseGet(() -> buildInitialGraph(taskId));
while (graph.hasNext()) {
List<TaskNode> readyNodes = graph.getReadyNodes();
for (TaskNode node : readyNodes) {
Executor executor = registry.get(node.getExecutorType());
// 编排器只负责调度,不关心执行器内部如何工作
ExecutionResult result = executor.execute(node.getInput());
stateStore.saveNodeResult(taskId, node.getId(), result);
graph.markCompleted(node.getId());
}
}
}
}
编排器的关键设计决策包括:
- 静态DAG vs 动态规划:静态DAG在任务开始前确定全部依赖关系,适合流程稳定的场景(如代码评审流水线);动态规划器在运行中根据中间结果决定后续任务,适合探索性场景(如多步推理、信息检索)。两者可以混合使用:外层静态DAG保证主流程可控,内层动态规划处理局部不确定性。
- 调度策略:串行、并行、条件分支、重试队列。编排器需要明确每个子任务的超时时间、重试上限和失败降级路径。
- 可观测性:编排器必须将每次调度决策写入共享状态,形成完整的执行审计日志。没有审计日志的编排器,在生产环境中等于没有编排器。
3.3 执行器:单一领域任务的完成者
执行器是真正"干活"的角色。每个执行器绑定一个明确的领域边界------例如"代码静态分析执行器""测试执行器""文档生成执行器"------并且只使用该领域内的工具集。
执行器之间的隔离是多智能体系统可维护性的关键。 如果一个执行器可以调用另一个执行器的工具,那么工具边界就形同虚设,上下文污染和权限扩散将不可避免。执行器之间的协作必须通过共享状态间接完成,而不是直接调用。
执行器的接口契约应该尽可能简单且结构化:
java
public interface Executor {
String getType();
ExecutionResult execute(ExecutionInput input);
}
public record ExecutionInput(
String taskId,
String nodeId,
Map<String, Object> parameters,
Map<String, Object> contextSnapshot // 来自共享状态的上下文快照
) {}
public record ExecutionResult(
Status status, // SUCCESS, FAILED, TIMEOUT, PARTIAL
Map<String, Object> output,
List<StateMutation> stateMutations, // 对共享状态的变更声明
String errorMessage
) {}
注意 ExecutionResult 中包含 stateMutations,而不是直接写入共享状态。这是状态变更声明式提交的关键设计:执行器声明"我想修改什么",由编排器或状态层决定是否接受这些变更。这样做的好处是:
- 冲突检测:多个并行执行器对同一状态键的冲突修改可以在提交时被检测到。
- 回滚能力:如果后续任务失败,可以基于变更声明构造补偿操作。
- 审计完整性:每次状态变更都有明确的来源和时序。
执行器内部可以是LLM Agent,也可以是纯确定性代码。不要为所有执行器都套上LLM------如果一个任务可以用确定性算法完成(如代码格式检查、测试覆盖率计算),用LLM反而是浪费且不可靠的。
3.4 共享状态:跨Agent上下文传递的契约
共享状态是多智能体系统中唯一被允许的跨Agent通信通道。它解决的核心问题是:当编排器将任务分解后,执行器A的产出如何安全、可靠地传递给执行器B,同时保持系统的可恢复性。
共享状态不是简单的全局变量。 它需要具备以下特性:
- 持久化:状态写入后不因进程重启而丢失。
- 版本化:每次写入都有版本号,支持乐观并发控制。
- 可快照:能够在任意时间点保存完整状态快照,用于恢复和调试。
- 可观测:状态变更可以被订阅,但订阅者不能修改状态。
在技术选型上,Redis和PostgreSQL是两个主流方向,各有适用场景:
| 特性 | Redis | PostgreSQL |
|---|---|---|
| 读写延迟 | 亚毫秒级 | 毫秒级 |
| 事务能力 | 有限(Lua脚本) | 完整ACID |
| 状态快照 | RDB/AOF | 时间点恢复 |
| 乐观并发控制 | WATCH/MULTI | 版本号/行级锁 |
| 适用场景 | 高频小状态、短期任务 | 低频大状态、长期审计 |
对于大多数生产级多智能体系统,PostgreSQL是更稳妥的默认选择 ,因为它的ACID事务和SQL查询能力让状态审计、部分回滚和并发控制变得简单可靠。Redis适合作为性能优化层,缓存热状态,但持久化事实源应放在PostgreSQL。
3.5 三个关键术语:幂等执行、状态快照、乐观并发控制
这三个术语是共享状态层能够支撑故障恢复的基石,必须精确定义。
幂等执行(Idempotent Execution) 指同一个任务被执行多次,产生的最终效果与执行一次完全相同。在多智能体系统中,幂等性意味着:如果执行器A已经成功完成了任务并写入了状态,但由于网络超时导致编排器认为它失败了,编排器可以安全地重新调度执行器A,而不会导致重复的副作用。
实现幂等的关键在于任务标识的确定性。每个任务节点在创建时就被赋予一个稳定且唯一的ID,执行器在写入状态时以该ID为键。重试时,如果发现该ID的状态已经存在且为成功,则直接返回已有结果。
java
// 幂等执行示例:以任务节点ID为幂等键
public ExecutionResult executeIdempotently(String nodeId, Executor executor, ExecutionInput input) {
// 先检查共享状态中是否已有该节点的成功结果
Optional<ExecutionResult> existing = stateStore.findResultByNodeId(nodeId);
if (existing.isPresent() && existing.get().status() == Status.SUCCESS) {
return existing.get(); // 幂等返回,不重复执行
}
ExecutionResult result = executor.execute(input);
stateStore.saveResultWithIdempotencyKey(nodeId, result);
return result;
}
状态快照(State Snapshot) 是共享状态在某一时间点的完整副本。快照的价值在于恢复点:当系统在任务执行中途发生不可恢复的错误时,可以回滚到最近一次快照,而不是从头开始。快照的粒度需要权衡:太频繁的快照会带来存储和性能开销,太稀疏的快照则会导致恢复时丢失过多进度。
实践中常用的策略是检查点(Checkpoint):在任务图的关键节点(如阶段边界、外部API调用前后)自动创建快照。检查点的触发条件由编排器定义,而非执行器自行决定。
乐观并发控制(Optimistic Concurrency Control, OCC) 允许多个执行器并行读取和修改共享状态,但在提交修改时检查版本冲突。如果执行器A基于版本1的状态进行计算,准备写入时发现状态已经被执行器B更新到版本2,则A的写入被拒绝,A需要基于最新状态重新计算或放弃。
sql
-- PostgreSQL中的乐观并发控制示例
UPDATE task_state
SET status = 'COMPLETED',
result = :result,
version = version + 1
WHERE task_id = :taskId
AND version = :expectedVersion; -- 如果版本不匹配,影响行数为0
OCC特别适合多智能体系统的原因在于:LLM执行器的执行时间高度不确定,从几秒到几分钟都有可能。悲观锁(在执行期间锁定状态)会导致长时间阻塞,而OCC让执行器自由计算,只在提交时做冲突检测,大大提高了并行度。
3.6 三者协作的运行时视图
下面用一张时序图展示编排器、执行器与共享状态在典型任务中的协作流程:
执行器B(测试运行) 执行器A(静态分析) 共享状态 编排器 执行器B(测试运行) 执行器A(静态分析) 共享状态 编排器 #mermaid-svg-ghACFZofnXL3FP93{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ghACFZofnXL3FP93 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ghACFZofnXL3FP93 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ghACFZofnXL3FP93 .error-icon{fill:#552222;}#mermaid-svg-ghACFZofnXL3FP93 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ghACFZofnXL3FP93 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ghACFZofnXL3FP93 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ghACFZofnXL3FP93 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ghACFZofnXL3FP93 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ghACFZofnXL3FP93 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ghACFZofnXL3FP93 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ghACFZofnXL3FP93 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ghACFZofnXL3FP93 .marker.cross{stroke:#333333;}#mermaid-svg-ghACFZofnXL3FP93 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ghACFZofnXL3FP93 p{margin:0;}#mermaid-svg-ghACFZofnXL3FP93 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ghACFZofnXL3FP93 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ghACFZofnXL3FP93 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ghACFZofnXL3FP93 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ghACFZofnXL3FP93 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ghACFZofnXL3FP93 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ghACFZofnXL3FP93 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ghACFZofnXL3FP93 .sequenceNumber{fill:white;}#mermaid-svg-ghACFZofnXL3FP93 #sequencenumber{fill:#333;}#mermaid-svg-ghACFZofnXL3FP93 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ghACFZofnXL3FP93 .messageText{fill:#333;stroke:none;}#mermaid-svg-ghACFZofnXL3FP93 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ghACFZofnXL3FP93 .labelText,#mermaid-svg-ghACFZofnXL3FP93 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ghACFZofnXL3FP93 .loopText,#mermaid-svg-ghACFZofnXL3FP93 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ghACFZofnXL3FP93 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ghACFZofnXL3FP93 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ghACFZofnXL3FP93 .noteText,#mermaid-svg-ghACFZofnXL3FP93 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ghACFZofnXL3FP93 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ghACFZofnXL3FP93 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ghACFZofnXL3FP93 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ghACFZofnXL3FP93 .actorPopupMenu{position:absolute;}#mermaid-svg-ghACFZofnXL3FP93 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ghACFZofnXL3FP93 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ghACFZofnXL3FP93 .actor-man circle,#mermaid-svg-ghACFZofnXL3FP93 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ghACFZofnXL3FP93 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 加载任务图,检查检查点返回任务图与状态快照调度节点A(携带上下文快照)声明状态变更(版本1)提交成功返回结构化结果更新节点A状态(幂等键)调度节点B(携带A的产出)声明状态变更(版本2)提交成功返回结构化结果创建检查点快照快照完成
注意两个关键点:执行器之间没有直接通信 ,所有数据传递都通过共享状态中转;编排器是唯一触发状态快照的角色,这保证了恢复点的一致性。
3.7 职责边界的常见破坏方式
理解职责边界的最好方式,是识别那些看似合理实则危险的越界行为:
- 执行器直接调用另一个执行器:这绕过了共享状态,导致状态不可追踪。正确做法是执行器将需要协作的信息写入共享状态,由编排器决定调度哪个执行器。
- 执行器自行决定重试:重试策略是编排器的职责。执行器自行重试可能导致重复副作用,且编排器无法感知重试次数和状态。
- 编排器直接操作领域工具:编排器一旦开始调用具体工具,它就变成了一个"超级执行器",职责边界模糊,测试和替换都变得困难。
- 共享状态包含业务逻辑:例如在状态存储中写触发器来自动调度任务。这会让状态层变成隐形的编排器,系统行为难以推理。
3.8 小结
编排器、执行器与共享状态的三层分离,是多智能体系统从实验走向生产的第一道架构防线 。它强制团队回答三个问题:谁在决定做什么?谁在做?做到哪了? 当这三个问题的答案分别归属于明确的组件时,故障定位、状态恢复和系统扩展才有了可操作的起点。
下一章我们将进入编排模式的具体对比------DAG工作流、动态规划器与事件驱动Agent通信,分析在不同任务特征下如何选择编排策略。
架构设计:三种编排模式对比与选型
上一章我们明确了编排器、执行器与共享状态三个核心角色的职责边界。现在面临的关键问题是:编排器如何组织多个执行器的协作流程? 这个决策直接影响系统的可维护性、故障恢复能力与扩展成本。实践中,多智能体系统的编排模式主要收敛为三种:DAG工作流编排 、动态规划器编排 与事件驱动Agent通信。三者并非互斥,而是对应不同的问题特征与系统成熟度阶段。
3.1 模式一:DAG工作流编排------确定性任务的基石
DAG(有向无环图)工作流是最直观的编排方式。开发者显式定义任务节点与依赖关系,编排器按拓扑顺序调度执行器,前序节点成功后触发后续节点。
#mermaid-svg-ALBqZ18ghvAHwPy5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ALBqZ18ghvAHwPy5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ALBqZ18ghvAHwPy5 .error-icon{fill:#552222;}#mermaid-svg-ALBqZ18ghvAHwPy5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ALBqZ18ghvAHwPy5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ALBqZ18ghvAHwPy5 .marker.cross{stroke:#333333;}#mermaid-svg-ALBqZ18ghvAHwPy5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ALBqZ18ghvAHwPy5 p{margin:0;}#mermaid-svg-ALBqZ18ghvAHwPy5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ALBqZ18ghvAHwPy5 .cluster-label text{fill:#333;}#mermaid-svg-ALBqZ18ghvAHwPy5 .cluster-label span{color:#333;}#mermaid-svg-ALBqZ18ghvAHwPy5 .cluster-label span p{background-color:transparent;}#mermaid-svg-ALBqZ18ghvAHwPy5 .label text,#mermaid-svg-ALBqZ18ghvAHwPy5 span{fill:#333;color:#333;}#mermaid-svg-ALBqZ18ghvAHwPy5 .node rect,#mermaid-svg-ALBqZ18ghvAHwPy5 .node circle,#mermaid-svg-ALBqZ18ghvAHwPy5 .node ellipse,#mermaid-svg-ALBqZ18ghvAHwPy5 .node polygon,#mermaid-svg-ALBqZ18ghvAHwPy5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ALBqZ18ghvAHwPy5 .rough-node .label text,#mermaid-svg-ALBqZ18ghvAHwPy5 .node .label text,#mermaid-svg-ALBqZ18ghvAHwPy5 .image-shape .label,#mermaid-svg-ALBqZ18ghvAHwPy5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-ALBqZ18ghvAHwPy5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ALBqZ18ghvAHwPy5 .rough-node .label,#mermaid-svg-ALBqZ18ghvAHwPy5 .node .label,#mermaid-svg-ALBqZ18ghvAHwPy5 .image-shape .label,#mermaid-svg-ALBqZ18ghvAHwPy5 .icon-shape .label{text-align:center;}#mermaid-svg-ALBqZ18ghvAHwPy5 .node.clickable{cursor:pointer;}#mermaid-svg-ALBqZ18ghvAHwPy5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ALBqZ18ghvAHwPy5 .arrowheadPath{fill:#333333;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ALBqZ18ghvAHwPy5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ALBqZ18ghvAHwPy5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ALBqZ18ghvAHwPy5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ALBqZ18ghvAHwPy5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ALBqZ18ghvAHwPy5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ALBqZ18ghvAHwPy5 .cluster text{fill:#333;}#mermaid-svg-ALBqZ18ghvAHwPy5 .cluster span{color:#333;}#mermaid-svg-ALBqZ18ghvAHwPy5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ALBqZ18ghvAHwPy5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ALBqZ18ghvAHwPy5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-ALBqZ18ghvAHwPy5 .icon-shape,#mermaid-svg-ALBqZ18ghvAHwPy5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ALBqZ18ghvAHwPy5 .icon-shape p,#mermaid-svg-ALBqZ18ghvAHwPy5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ALBqZ18ghvAHwPy5 .icon-shape .label rect,#mermaid-svg-ALBqZ18ghvAHwPy5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ALBqZ18ghvAHwPy5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ALBqZ18ghvAHwPy5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ALBqZ18ghvAHwPy5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 任务解析
代码扫描
架构分析
安全审查
报告聚合
人工复核
这种模式的核心假设是任务结构在运行前已知且稳定。每个节点的输入输出契约清晰,执行器无状态或仅依赖显式传入的上下文。在代码评审系统中,如果流程固定为"解析→扫描→审查→聚合",DAG是最佳选择。
优点:
- 可预测性极强:执行路径在运行前即可静态分析,便于预估耗时与资源消耗
- 故障定位简单:失败节点明确,重试策略可精确到单个节点
- 可观测性好:每个节点的输入输出可独立记录与回放
缺点:
- 刚性过强:无法根据中间结果动态调整后续路径
- 节点耦合度高:新增一个分析维度需要修改DAG定义
- 不适合开放式任务:当Agent需要根据发现的问题自主决定下一步时,DAG成为束缚
适用场景 :CI/CD流水线中的静态检查、数据ETL、合规审计等流程边界清晰、步骤相对固定的任务。
3.2 模式二:动态规划器编排------面对不确定性的自适应选择
当任务无法预先定义完整流程时,动态规划器模式让一个"规划Agent"根据当前状态决定下一步行动。规划器维护一个任务队列 与上下文摘要,每完成一个子任务就重新评估剩余工作。
共享状态 执行器C(文档审查) 执行器B(测试生成) 执行器A(代码分析) 规划器 共享状态 执行器C(文档审查) 执行器B(测试生成) 执行器A(代码分析) 规划器 #mermaid-svg-lkMI7YzbskMDj8Ja{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lkMI7YzbskMDj8Ja .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lkMI7YzbskMDj8Ja .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lkMI7YzbskMDj8Ja .error-icon{fill:#552222;}#mermaid-svg-lkMI7YzbskMDj8Ja .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lkMI7YzbskMDj8Ja .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lkMI7YzbskMDj8Ja .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lkMI7YzbskMDj8Ja .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lkMI7YzbskMDj8Ja .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lkMI7YzbskMDj8Ja .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lkMI7YzbskMDj8Ja .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lkMI7YzbskMDj8Ja .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lkMI7YzbskMDj8Ja .marker.cross{stroke:#333333;}#mermaid-svg-lkMI7YzbskMDj8Ja svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lkMI7YzbskMDj8Ja p{margin:0;}#mermaid-svg-lkMI7YzbskMDj8Ja .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lkMI7YzbskMDj8Ja text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-lkMI7YzbskMDj8Ja .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lkMI7YzbskMDj8Ja .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-lkMI7YzbskMDj8Ja .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-lkMI7YzbskMDj8Ja .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-lkMI7YzbskMDj8Ja #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-lkMI7YzbskMDj8Ja .sequenceNumber{fill:white;}#mermaid-svg-lkMI7YzbskMDj8Ja #sequencenumber{fill:#333;}#mermaid-svg-lkMI7YzbskMDj8Ja #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-lkMI7YzbskMDj8Ja .messageText{fill:#333;stroke:none;}#mermaid-svg-lkMI7YzbskMDj8Ja .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lkMI7YzbskMDj8Ja .labelText,#mermaid-svg-lkMI7YzbskMDj8Ja .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-lkMI7YzbskMDj8Ja .loopText,#mermaid-svg-lkMI7YzbskMDj8Ja .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-lkMI7YzbskMDj8Ja .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lkMI7YzbskMDj8Ja .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-lkMI7YzbskMDj8Ja .noteText,#mermaid-svg-lkMI7YzbskMDj8Ja .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-lkMI7YzbskMDj8Ja .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lkMI7YzbskMDj8Ja .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lkMI7YzbskMDj8Ja .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lkMI7YzbskMDj8Ja .actorPopupMenu{position:absolute;}#mermaid-svg-lkMI7YzbskMDj8Ja .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-lkMI7YzbskMDj8Ja .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lkMI7YzbskMDj8Ja .actor-man circle,#mermaid-svg-lkMI7YzbskMDj8Ja line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-lkMI7YzbskMDj8Ja :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读取项目元数据生成初始任务计划分配"核心模块复杂度分析"写入分析结果返回完成信号读取最新状态发现复杂度超标,追加测试任务分配"高复杂度模块测试生成"写入测试用例返回完成信号评估是否需要文档审查条件性分配"API文档一致性检查"返回审查结论写入最终报告
这种模式的核心价值在于将"下一步做什么"从静态结构转变为运行时决策。规划器本质上是一个轻量级Agent,其推理能力决定了编排质量。
优点:
- 灵活性强:能根据中间结果动态调整策略,处理长尾场景
- 任务粒度可控:规划器可以决定拆分粒度,避免过度工程化
- 渐进式探索:适合需求模糊、需要多轮迭代的任务
缺点:
- 不可预测性:执行路径不确定,难以预估总耗时与资源消耗
- 规划器成为单点:规划质量高度依赖规划Agent的能力,且规划器故障会导致全局停滞
- 调试困难:同一个输入可能产生不同的执行路径,问题复现成本高
适用场景 :代码库迁移、安全漏洞响应、复杂问题诊断等需要根据发现动态调整策略的任务。
3.3 模式三:事件驱动Agent通信------松耦合的协作网络
事件驱动模式彻底放弃了中心化编排。每个Agent独立运行,通过事件总线订阅感兴趣的消息类型,自主决定何时触发行动。这种模式最接近人类团队的协作方式------没有明确的"项目经理",每个人根据信息流自主响应。
#mermaid-svg-aUvTwViOCBXhYFoc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aUvTwViOCBXhYFoc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aUvTwViOCBXhYFoc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aUvTwViOCBXhYFoc .error-icon{fill:#552222;}#mermaid-svg-aUvTwViOCBXhYFoc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aUvTwViOCBXhYFoc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aUvTwViOCBXhYFoc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aUvTwViOCBXhYFoc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aUvTwViOCBXhYFoc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aUvTwViOCBXhYFoc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aUvTwViOCBXhYFoc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aUvTwViOCBXhYFoc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aUvTwViOCBXhYFoc .marker.cross{stroke:#333333;}#mermaid-svg-aUvTwViOCBXhYFoc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aUvTwViOCBXhYFoc p{margin:0;}#mermaid-svg-aUvTwViOCBXhYFoc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aUvTwViOCBXhYFoc .cluster-label text{fill:#333;}#mermaid-svg-aUvTwViOCBXhYFoc .cluster-label span{color:#333;}#mermaid-svg-aUvTwViOCBXhYFoc .cluster-label span p{background-color:transparent;}#mermaid-svg-aUvTwViOCBXhYFoc .label text,#mermaid-svg-aUvTwViOCBXhYFoc span{fill:#333;color:#333;}#mermaid-svg-aUvTwViOCBXhYFoc .node rect,#mermaid-svg-aUvTwViOCBXhYFoc .node circle,#mermaid-svg-aUvTwViOCBXhYFoc .node ellipse,#mermaid-svg-aUvTwViOCBXhYFoc .node polygon,#mermaid-svg-aUvTwViOCBXhYFoc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aUvTwViOCBXhYFoc .rough-node .label text,#mermaid-svg-aUvTwViOCBXhYFoc .node .label text,#mermaid-svg-aUvTwViOCBXhYFoc .image-shape .label,#mermaid-svg-aUvTwViOCBXhYFoc .icon-shape .label{text-anchor:middle;}#mermaid-svg-aUvTwViOCBXhYFoc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aUvTwViOCBXhYFoc .rough-node .label,#mermaid-svg-aUvTwViOCBXhYFoc .node .label,#mermaid-svg-aUvTwViOCBXhYFoc .image-shape .label,#mermaid-svg-aUvTwViOCBXhYFoc .icon-shape .label{text-align:center;}#mermaid-svg-aUvTwViOCBXhYFoc .node.clickable{cursor:pointer;}#mermaid-svg-aUvTwViOCBXhYFoc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aUvTwViOCBXhYFoc .arrowheadPath{fill:#333333;}#mermaid-svg-aUvTwViOCBXhYFoc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aUvTwViOCBXhYFoc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aUvTwViOCBXhYFoc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aUvTwViOCBXhYFoc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aUvTwViOCBXhYFoc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aUvTwViOCBXhYFoc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aUvTwViOCBXhYFoc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aUvTwViOCBXhYFoc .cluster text{fill:#333;}#mermaid-svg-aUvTwViOCBXhYFoc .cluster span{color:#333;}#mermaid-svg-aUvTwViOCBXhYFoc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aUvTwViOCBXhYFoc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aUvTwViOCBXhYFoc rect.text{fill:none;stroke-width:0;}#mermaid-svg-aUvTwViOCBXhYFoc .icon-shape,#mermaid-svg-aUvTwViOCBXhYFoc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aUvTwViOCBXhYFoc .icon-shape p,#mermaid-svg-aUvTwViOCBXhYFoc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aUvTwViOCBXhYFoc .icon-shape .label rect,#mermaid-svg-aUvTwViOCBXhYFoc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aUvTwViOCBXhYFoc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aUvTwViOCBXhYFoc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aUvTwViOCBXhYFoc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发布:高危漏洞发现
订阅:漏洞事件
订阅:漏洞事件
发布:修复PR已创建
订阅:修复事件
发布:影响范围报告
订阅:影响报告
发布:测试通过
订阅:测试结果
事件总线
安全扫描Agent
修复建议Agent
影响评估Agent
回归测试Agent
发布决策Agent
优点:
- 极低的Agent耦合度:新增Agent只需订阅相关事件,无需修改现有Agent
- 天然支持异构系统:Agent可以用不同语言、不同模型实现
- 弹性扩展:事件消费者可以水平扩展,应对突发负载
缺点:
- 全局行为难以推理:事件触发链可能形成非预期的循环或竞争条件
- 一致性挑战:多个Agent同时响应同一事件时,可能产生冲突操作
- 调试极其困难:需要完整的事件日志才能还原系统行为,且事件时序问题难以复现
适用场景 :监控告警响应、实时安全防护、多团队协作的大型系统等需要高度解耦且事件流自然存在的场景。
3.4 选型决策框架:任务确定性与Agent耦合度的二维矩阵
三种模式没有绝对的优劣,选择取决于两个核心维度:任务确定性 (流程在运行前可知的程度)与Agent耦合度(Agent之间需要多紧密的协作)。
| 任务确定性 \ Agent耦合度 | 低耦合 | 高耦合 |
|---|---|---|
| 高确定性 | DAG工作流(首选) | DAG工作流 + 共享状态同步 |
| 中确定性 | 动态规划器 | 动态规划器 + 事件通知 |
| 低确定性 | 事件驱动 | 事件驱动 + 规划器辅助 |
具体决策路径:
-
如果流程可以预先定义且步骤稳定,优先选择DAG工作流。即使Agent数量增多,DAG的显式依赖关系仍然是维护成本最低的方案。
-
如果流程需要根据中间结果调整,但整体目标明确,选择动态规划器。关键在于规划器的提示词设计------它需要明确知道"何时停止规划,开始执行"。
-
如果系统本身就是事件驱动的,或者Agent之间需要长期独立运行 ,选择事件驱动模式。但必须配套事件溯源 与幂等处理机制,否则故障排查会变成灾难。
-
混合模式是常态:生产级系统往往在宏观层面使用DAG定义主流程,在微观层面让单个执行器内部使用动态规划或事件响应。例如代码评审系统的主流程是DAG,但"安全审查"节点内部可能根据代码特征动态决定检查深度。
3.5 整体组件关系
无论选择哪种编排模式,多智能体系统的组件关系都遵循相似的分层结构:
#mermaid-svg-kGL08HdLs7noKn7e{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kGL08HdLs7noKn7e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kGL08HdLs7noKn7e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kGL08HdLs7noKn7e .error-icon{fill:#552222;}#mermaid-svg-kGL08HdLs7noKn7e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kGL08HdLs7noKn7e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kGL08HdLs7noKn7e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kGL08HdLs7noKn7e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kGL08HdLs7noKn7e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kGL08HdLs7noKn7e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kGL08HdLs7noKn7e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kGL08HdLs7noKn7e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kGL08HdLs7noKn7e .marker.cross{stroke:#333333;}#mermaid-svg-kGL08HdLs7noKn7e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kGL08HdLs7noKn7e p{margin:0;}#mermaid-svg-kGL08HdLs7noKn7e .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kGL08HdLs7noKn7e .cluster-label text{fill:#333;}#mermaid-svg-kGL08HdLs7noKn7e .cluster-label span{color:#333;}#mermaid-svg-kGL08HdLs7noKn7e .cluster-label span p{background-color:transparent;}#mermaid-svg-kGL08HdLs7noKn7e .label text,#mermaid-svg-kGL08HdLs7noKn7e span{fill:#333;color:#333;}#mermaid-svg-kGL08HdLs7noKn7e .node rect,#mermaid-svg-kGL08HdLs7noKn7e .node circle,#mermaid-svg-kGL08HdLs7noKn7e .node ellipse,#mermaid-svg-kGL08HdLs7noKn7e .node polygon,#mermaid-svg-kGL08HdLs7noKn7e .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kGL08HdLs7noKn7e .rough-node .label text,#mermaid-svg-kGL08HdLs7noKn7e .node .label text,#mermaid-svg-kGL08HdLs7noKn7e .image-shape .label,#mermaid-svg-kGL08HdLs7noKn7e .icon-shape .label{text-anchor:middle;}#mermaid-svg-kGL08HdLs7noKn7e .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kGL08HdLs7noKn7e .rough-node .label,#mermaid-svg-kGL08HdLs7noKn7e .node .label,#mermaid-svg-kGL08HdLs7noKn7e .image-shape .label,#mermaid-svg-kGL08HdLs7noKn7e .icon-shape .label{text-align:center;}#mermaid-svg-kGL08HdLs7noKn7e .node.clickable{cursor:pointer;}#mermaid-svg-kGL08HdLs7noKn7e .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kGL08HdLs7noKn7e .arrowheadPath{fill:#333333;}#mermaid-svg-kGL08HdLs7noKn7e .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kGL08HdLs7noKn7e .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kGL08HdLs7noKn7e .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kGL08HdLs7noKn7e .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kGL08HdLs7noKn7e .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kGL08HdLs7noKn7e .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kGL08HdLs7noKn7e .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kGL08HdLs7noKn7e .cluster text{fill:#333;}#mermaid-svg-kGL08HdLs7noKn7e .cluster span{color:#333;}#mermaid-svg-kGL08HdLs7noKn7e div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kGL08HdLs7noKn7e .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kGL08HdLs7noKn7e rect.text{fill:none;stroke-width:0;}#mermaid-svg-kGL08HdLs7noKn7e .icon-shape,#mermaid-svg-kGL08HdLs7noKn7e .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kGL08HdLs7noKn7e .icon-shape p,#mermaid-svg-kGL08HdLs7noKn7e .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kGL08HdLs7noKn7e .icon-shape .label rect,#mermaid-svg-kGL08HdLs7noKn7e .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kGL08HdLs7noKn7e .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kGL08HdLs7noKn7e .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kGL08HdLs7noKn7e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 基础设施
状态层
执行层
编排层
接入层
API网关
管理控制台
编排器
DAG引擎
动态规划器
事件总线
执行器A
执行器B
执行器C
执行器D
共享状态存储
缓存
事件日志
消息队列
持久化数据库
可观测性平台
关键设计原则:
- 编排层与执行层分离:编排器不直接操作业务数据,执行器不感知全局流程
- 状态层是所有Agent的唯一数据源:任何Agent不得绕过共享状态直接通信
- 事件日志是不可变基础设施:所有事件必须持久化,支持事后回放与审计
选型不是一次性的架构决策,而是随着系统演进不断调整的过程。从DAG起步,在需要灵活性的节点引入动态规划,当系统规模达到一定阈值后逐步解耦为事件驱动,这是一条被反复验证的演进路径。下一章我们将深入共享状态层的设计细节------这是三种模式共同依赖的根基。
共享状态存储设计:快照、锁与乐观并发控制
在上一章中,我们确立了编排器---执行器---共享状态的三层职责边界。共享状态层不是简单的键值存储,而是整个多智能体系统在故障发生时能否保持一致性与可恢复性的关键基础设施。本章深入讨论共享状态存储的四个核心设计命题:状态快照的保存与恢复机制 、分布式锁的使用边界 、版本号与CAS实现的乐观并发控制 ,以及状态分区策略。
为什么共享状态需要专门设计
单个Agent的执行通常是"无状态"或"轻状态"的------输入提示词,输出结果,状态仅存在于调用方的内存中。但多智能体协作引入了一个本质性挑战:多个执行器在时间与空间上解耦,却需要对同一份逻辑状态达成一致认知。例如,一个代码评审系统中,Agent A负责静态分析,Agent B负责依赖漏洞扫描,Agent C负责架构合规检查。三者并行执行,最终需要一个聚合器汇总结果。如果聚合器重启,它必须能够从持久化存储中恢复"哪些子任务已完成、哪些仍在进行、哪些已失败"的精确状态,否则会导致重复执行或结果丢失。
共享状态存储的设计目标因此明确为三点:
- 可恢复性:编排器或执行器崩溃后,系统能从持久化状态中恢复,不丢失已完成的工作。
- 一致性:并发执行的多个Agent对共享状态的读写不会产生竞态条件或脏读。
- 可扩展性:状态存储本身不能成为系统吞吐量的瓶颈,需要支持分区与水平扩展。
状态快照的保存与恢复机制
状态快照是多智能体系统故障恢复的基石。它的核心思想是:在任务执行的关键节点,将共享状态的完整或增量表示持久化到存储中,以便在故障发生后从最近的快照点恢复。
快照的内容
一个设计良好的状态快照应包含以下四类信息:
| 快照内容 | 说明 | 示例 |
|---|---|---|
| 任务元数据 | 任务ID、类型、创建时间、优先级 | task_id: "cr-2024-001" |
| 执行进度 | 各子任务的当前状态机位置 | subtask_3: "COMPLETED" |
| 中间结果 | 已完成子任务的输出引用 | result_ref: "s3://bucket/results/3.json" |
| 依赖关系 | 子任务之间的DAG边及当前满足情况 | subtask_5.depends_on: [3, 4] |
快照不应存储Agent的完整提示词或大体积的中间产物。这些数据应存放在对象存储(如S3、OSS)中,快照只保存引用。这样可以将快照体积控制在KB级别,使高频快照成为可能。
基于Redis的快照实现
Redis适合存储热状态 ------需要频繁读写、低延迟访问的执行状态。对于快照,我们使用Redis的DUMP与RESTORE命令,或者更简单地将快照序列化为JSON存入String类型。
java
// 快照服务:将任务执行状态保存到Redis
public class RedisSnapshotService {
private final StringRedisTemplate redis;
private final ObjectMapper objectMapper;
public RedisSnapshotService(StringRedisTemplate redis, ObjectMapper objectMapper) {
this.redis = redis;
this.objectMapper = objectMapper;
}
/**
* 保存任务快照
* @param taskId 任务ID
* @param snapshot 快照对象
* @param ttlSeconds 快照过期时间,防止Redis内存无限增长
*/
public void saveSnapshot(String taskId, TaskSnapshot snapshot, long ttlSeconds) {
try {
String key = "snapshot:" + taskId;
String json = objectMapper.writeValueAsString(snapshot);
redis.opsForValue().set(key, json, Duration.ofSeconds(ttlSeconds));
} catch (JsonProcessingException e) {
throw new SnapshotException("Failed to serialize snapshot for task " + taskId, e);
}
}
/**
* 恢复任务快照
* @param taskId 任务ID
* @return 快照对象,不存在时返回null
*/
public TaskSnapshot restoreSnapshot(String taskId) {
String key = "snapshot:" + taskId;
String json = redis.opsForValue().get(key);
if (json == null) {
return null;
}
try {
return objectMapper.readValue(json, TaskSnapshot.class);
} catch (JsonProcessingException e) {
throw new SnapshotException("Failed to deserialize snapshot for task " + taskId, e);
}
}
}
基于Postgres的快照实现
Postgres适合存储冷状态------需要长期保留、支持复杂查询的历史状态与审计记录。快照表的设计应支持按任务ID查询最新快照,以及按时间范围查询历史快照。
sql
CREATE TABLE task_snapshots (
id BIGSERIAL PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
snapshot_version BIGINT NOT NULL,
state JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (task_id, snapshot_version)
);
CREATE INDEX idx_task_snapshots_task_id ON task_snapshots(task_id, created_at DESC);
使用Postgres的JSONB类型存储快照内容,既保留了结构化查询能力,又避免了为每种快照结构单独建表的维护成本。snapshot_version字段与乐观并发控制机制配合使用,确保快照写入的单调递增性。
快照频率与恢复策略
快照频率需要在恢复精度 与性能开销 之间权衡。推荐采用事件驱动快照而非固定时间间隔快照:
- 子任务状态变更时 :每次子任务从
RUNNING变为COMPLETED或FAILED时触发快照。 - 阶段边界处:DAG中的关键路径节点完成时触发快照。
- 优雅关闭时:编排器收到SIGTERM信号时,先保存最终快照再退出。
恢复策略遵循"从最近快照恢复,幂等重放未完成步骤 "的原则。恢复后的编排器读取快照,将状态机推进到快照点,然后重新调度所有状态为RUNNING或PENDING的子任务。这要求每个子任务的执行必须是幂等的------我们将在下一章详细讨论幂等执行的设计。
分布式锁的使用边界
分布式锁在多智能体系统中是一个容易被滥用的工具。许多工程师在遇到并发问题时,第一反应是"加锁",但锁的引入会带来死锁风险、性能瓶颈与运维复杂度 。我们需要明确:锁只应保护短临界区,绝不应跨越Agent执行边界。
锁的适用场景
| 场景 | 是否适合用锁 | 原因 |
|---|---|---|
| 快照版本号递增 | 是 | 临界区极短,仅一次内存操作 |
| 任务状态从PENDING到RUNNING的转换 | 是 | 防止两个执行器同时领取同一任务 |
| 整个子任务的执行过程 | 否 | Agent执行可能持续数分钟,持锁期间阻塞所有其他操作 |
| 聚合多个子任务的结果 | 否 | 应使用乐观并发控制,见下节 |
基于Redis的分布式锁实现
Redis的SETNX命令是实现分布式锁的经典方式,但需要正确处理锁过期 与锁释放的原子性。以下是一个健壮的实现:
java
public class RedisDistributedLock {
private final StringRedisTemplate redis;
private static final String LOCK_PREFIX = "lock:";
private static final long DEFAULT_TTL_MS = 10_000; // 10秒
public RedisDistributedLock(StringRedisTemplate redis) {
this.redis = redis;
}
/**
* 尝试获取锁
* @param lockKey 锁的标识
* @param ttlMs 锁的过期时间(毫秒)
* @return 锁令牌,获取失败返回null
*/
public String tryLock(String lockKey, long ttlMs) {
String token = UUID.randomUUID().toString();
String key = LOCK_PREFIX + lockKey;
Boolean success = redis.opsForValue().setIfAbsent(
key, token, Duration.ofMillis(ttlMs)
);
return Boolean.TRUE.equals(success) ? token : null;
}
/**
* 释放锁,使用Lua脚本确保原子性
* @param lockKey 锁的标识
* @param token 获取锁时返回的令牌
*/
public void unlock(String lockKey, String token) {
String key = LOCK_PREFIX + lockKey;
String script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
redis.execute((RedisCallback<Object>) connection -> {
connection.eval(
script.getBytes(StandardCharsets.UTF_8),
ReturnType.INTEGER,
1,
key.getBytes(StandardCharsets.UTF_8),
token.getBytes(StandardCharsets.UTF_8)
);
return null;
});
}
}
关键设计点:
- 锁令牌(token):每个锁获取者持有唯一令牌,释放时验证令牌,防止释放他人持有的锁。
- TTL兜底:锁必须设置过期时间,防止持有者崩溃后锁永久无法释放。
- Lua脚本原子释放 :
GET+DEL必须原子执行,否则在GET和DEL之间锁可能过期并被其他线程获取,导致误删。
锁的替代方案:原子操作与状态机
在许多场景中,锁可以被Redis的原子操作 或状态机约束 替代。例如,任务状态从PENDING到RUNNING的转换,可以通过Redis的WATCH+MULTI事务或Lua脚本实现,无需显式加锁:
lua
-- 原子地将任务从PENDING转为RUNNING,返回是否成功
local key = KEYS[1]
local current = redis.call('HGET', key, 'status')
if current == 'PENDING' then
redis.call('HSET', key, 'status', 'RUNNING')
redis.call('HSET', key, 'claimed_by', ARGV[1])
redis.call('HSET', key, 'claimed_at', ARGV[2])
return 1
else
return 0
end
这种方式将状态转换逻辑封装在存储层,避免了应用层的锁竞争。
版本号与CAS实现的乐观并发控制
乐观并发控制(Optimistic Concurrency Control, OCC)是多智能体系统中处理并发写入的首选策略。其核心思想是:假设并发冲突是罕见的,先执行操作,提交时验证数据是否被其他写入者修改,若已修改则重试或放弃 。在共享状态存储中,OCC通过版本号与**Compare-And-Swap(CAS)**操作实现。
版本号的设计
每个共享状态记录(如任务、子任务、聚合结果)都应携带一个单调递增的version字段。写入者读取记录时获取当前版本号,写入时在WHERE子句中校验版本号是否未变:
sql
-- 乐观并发控制更新:仅当版本号匹配时才更新
UPDATE task_states
SET status = 'COMPLETED',
result_ref = :resultRef,
version = version + 1,
updated_at = NOW()
WHERE task_id = :taskId
AND version = :expectedVersion;
如果更新影响的行数为0,说明版本号已变(其他写入者修改了记录),调用方需要重新读取最新状态并决定如何处理。
基于Redis的CAS实现
Redis的WATCH命令提供了原生的CAS支持。以下是一个完整的乐观并发控制示例:
java
public class OptimisticStateUpdater {
private final StringRedisTemplate redis;
public OptimisticStateUpdater(StringRedisTemplate redis) {
this.redis = redis;
}
/**
* 使用CAS更新任务状态
* @param taskId 任务ID
* @param expectedVersion 期望的版本号
* @param newStatus 新状态
* @return 更新成功返回true,版本冲突返回false
*/
public boolean updateTaskStatusWithCas(
String taskId, long expectedVersion, String newStatus) {
String key = "task:" + taskId;
// 使用executeWithStrictCAS确保WATCH期间没有其他写入
List<Object> results = redis.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) {
operations.watch(key);
String currentVersion = (String) operations.opsForHash()
.get(key, "version");
if (currentVersion == null ||
Long.parseLong(currentVersion) != expectedVersion) {
operations.unwatch();
return Collections.singletonList("VERSION_CONFLICT");
}
operations.multi();
operations.opsForHash().put(key, "status", newStatus);
operations.opsForHash().put(key, "version",
String.valueOf(expectedVersion + 1));
return operations.exec();
}
});
return !results.isEmpty() && !"VERSION_CONFLICT".equals(results.get(0));
}
}
冲突解决策略
当CAS检测到版本冲突时,调用方需要决定如何处理。常见的策略有三种:
- 重试:重新读取最新状态,基于最新状态重新计算并尝试提交。适用于冲突后重新计算的成本较低的场景。
- 合并 :将当前写入与最新状态合并后提交。适用于不同写入者修改不同字段的场景(如一个Agent更新
result_ref,另一个更新progress_percent)。 - 放弃并报告:放弃本次写入,将冲突报告给编排器,由编排器决定是否需要人工介入或重新规划。
对于多智能体系统,重试策略通常是默认选择,因为Agent的执行通常是确定性的,重新计算不会产生副作用(前提是幂等设计)。
状态分区策略
随着多智能体系统规模的增长,单一Redis实例或单一Postgres表会成为性能瓶颈。状态分区(Sharding)是将状态数据分散到多个存储节点上的技术,其核心挑战在于选择合适的分区键 与处理跨分区操作。
分区键的选择
分区键的选择取决于数据的访问模式。在多智能体系统中,任务ID是最自然的分区键,因为:
- 同一个任务的所有子任务状态、快照、锁都围绕任务ID组织。
- 跨任务的查询(如"所有失败的任务")通常用于监控与审计,可以容忍较高的延迟。
- 任务之间天然独立,不存在跨任务的强一致性需求。
#mermaid-svg-6sF0AAmyZIS0ZjLL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6sF0AAmyZIS0ZjLL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6sF0AAmyZIS0ZjLL .error-icon{fill:#552222;}#mermaid-svg-6sF0AAmyZIS0ZjLL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6sF0AAmyZIS0ZjLL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6sF0AAmyZIS0ZjLL .marker.cross{stroke:#333333;}#mermaid-svg-6sF0AAmyZIS0ZjLL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6sF0AAmyZIS0ZjLL p{margin:0;}#mermaid-svg-6sF0AAmyZIS0ZjLL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6sF0AAmyZIS0ZjLL .cluster-label text{fill:#333;}#mermaid-svg-6sF0AAmyZIS0ZjLL .cluster-label span{color:#333;}#mermaid-svg-6sF0AAmyZIS0ZjLL .cluster-label span p{background-color:transparent;}#mermaid-svg-6sF0AAmyZIS0ZjLL .label text,#mermaid-svg-6sF0AAmyZIS0ZjLL span{fill:#333;color:#333;}#mermaid-svg-6sF0AAmyZIS0ZjLL .node rect,#mermaid-svg-6sF0AAmyZIS0ZjLL .node circle,#mermaid-svg-6sF0AAmyZIS0ZjLL .node ellipse,#mermaid-svg-6sF0AAmyZIS0ZjLL .node polygon,#mermaid-svg-6sF0AAmyZIS0ZjLL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6sF0AAmyZIS0ZjLL .rough-node .label text,#mermaid-svg-6sF0AAmyZIS0ZjLL .node .label text,#mermaid-svg-6sF0AAmyZIS0ZjLL .image-shape .label,#mermaid-svg-6sF0AAmyZIS0ZjLL .icon-shape .label{text-anchor:middle;}#mermaid-svg-6sF0AAmyZIS0ZjLL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6sF0AAmyZIS0ZjLL .rough-node .label,#mermaid-svg-6sF0AAmyZIS0ZjLL .node .label,#mermaid-svg-6sF0AAmyZIS0ZjLL .image-shape .label,#mermaid-svg-6sF0AAmyZIS0ZjLL .icon-shape .label{text-align:center;}#mermaid-svg-6sF0AAmyZIS0ZjLL .node.clickable{cursor:pointer;}#mermaid-svg-6sF0AAmyZIS0ZjLL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6sF0AAmyZIS0ZjLL .arrowheadPath{fill:#333333;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6sF0AAmyZIS0ZjLL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6sF0AAmyZIS0ZjLL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6sF0AAmyZIS0ZjLL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6sF0AAmyZIS0ZjLL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6sF0AAmyZIS0ZjLL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6sF0AAmyZIS0ZjLL .cluster text{fill:#333;}#mermaid-svg-6sF0AAmyZIS0ZjLL .cluster span{color:#333;}#mermaid-svg-6sF0AAmyZIS0ZjLL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6sF0AAmyZIS0ZjLL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6sF0AAmyZIS0ZjLL rect.text{fill:none;stroke-width:0;}#mermaid-svg-6sF0AAmyZIS0ZjLL .icon-shape,#mermaid-svg-6sF0AAmyZIS0ZjLL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6sF0AAmyZIS0ZjLL .icon-shape p,#mermaid-svg-6sF0AAmyZIS0ZjLL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6sF0AAmyZIS0ZjLL .icon-shape .label rect,#mermaid-svg-6sF0AAmyZIS0ZjLL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6sF0AAmyZIS0ZjLL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6sF0AAmyZIS0ZjLL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6sF0AAmyZIS0ZjLL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 分区2
分区1
分区0
hash(task_id) % 3
hash(task_id) % 3
hash(task_id) % 3
task:0
subtasks: 0-99
snapshots: 0-99
task:1
subtasks: 100-199
snapshots: 100-199
task:2
subtasks: 200-299
snapshots: 200-299
编排器/执行器
Redis分区实现
Redis Cluster提供了内置的分区能力,使用**哈希槽(hash slot)机制将键分布到16384个槽中。对于多智能体系统,我们使用哈希标签(hash tag)**确保同一任务的所有键落在同一槽中:
java
public class PartitionedStateStore {
private final RedisClusterClient clusterClient;
/**
* 生成带有哈希标签的键,确保同一任务的所有键在同一分区
* Redis哈希标签:键中第一个{...}内的内容用于计算哈希槽
*/
public String buildKey(String taskId, String keyType) {
return "{" + taskId + "}:" + keyType;
}
// 使用示例:
// buildKey("task-123", "status") -> "{task-123}:status"
// buildKey("task-123", "snapshot") -> "{task-123}:snapshot"
// buildKey("task-123", "lock") -> "{task-123}:lock"
}
哈希标签{task-123}确保{task-123}:status、{task-123}:snapshot、{task-123}:lock都映射到相同的哈希槽,从而支持在同一个任务内使用Redis的多键操作(如MGET、Lua脚本)。
Postgres分区实现
Postgres 10+支持声明式分区 。对于任务状态表,可以使用task_id的哈希分区:
sql
CREATE TABLE task_states (
task_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
result_ref TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (task_id, version)
) PARTITION BY HASH (task_id);
-- 创建4个分区
CREATE TABLE task_states_p0 PARTITION OF task_states
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE task_states_p1 PARTITION OF task_states
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE task_states_p2 PARTITION OF task_states
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE task_states_p3 PARTITION OF task_states
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
跨分区操作的权衡
状态分区带来了水平扩展能力,但也引入了跨分区操作的限制:
故障隔离与重试策略:Agent级超时、幂等与回滚
在上一章中,我们把共享状态层打造成了多智能体系统的一致性基石。但一致性基础设施本身并不能阻止故障发生------它只能保证故障发生后系统仍可观测、可恢复。真正决定系统韧性上限的,是编排层如何在故障发生的第一时间做出正确反应:是快速失败、自动重试,还是安全降级?
本章聚焦多智能体系统中故障隔离的三个核心机制:Agent级超时控制、幂等执行保证与部分失败回滚。我们将通过一个基于状态机驱动的任务执行引擎,展示这三者如何协同工作,使系统在单个Agent失败时不会引发级联故障。
为什么"隔离"比"恢复"更优先
在多智能体系统中,一个常见误区是把所有希望寄托于重试机制。但重试本身是有代价的:它消耗时间、占用资源,更危险的是------如果原始操作已经部分生效,盲目重试可能造成数据重复写入或状态污染。
因此,正确的顺序是:先隔离,再恢复。隔离意味着:
- 超时边界:每个Agent调用都有明确的执行时限,超时即中断,不让慢调用拖垮整个任务链。
- 幂等边界:每个Agent的操作可安全重复执行,重试不会产生副作用。
- 回滚边界:当部分Agent成功、部分失败时,系统能撤销已成功的部分,回到一致状态。
这三层边界共同构成一个"故障容器":故障被限制在单个任务步骤内,不会向外扩散。
状态机驱动的任务状态流转
在实现具体机制之前,我们需要一个明确的任务状态模型。上一章提到的任务记录(TaskRecord)在数据库中是静态快照,而在编排器的内存中,它应该是一个状态机。
#mermaid-svg-EhCc6zKHviw2CcqB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EhCc6zKHviw2CcqB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EhCc6zKHviw2CcqB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EhCc6zKHviw2CcqB .error-icon{fill:#552222;}#mermaid-svg-EhCc6zKHviw2CcqB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EhCc6zKHviw2CcqB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EhCc6zKHviw2CcqB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EhCc6zKHviw2CcqB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EhCc6zKHviw2CcqB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EhCc6zKHviw2CcqB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EhCc6zKHviw2CcqB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EhCc6zKHviw2CcqB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EhCc6zKHviw2CcqB .marker.cross{stroke:#333333;}#mermaid-svg-EhCc6zKHviw2CcqB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EhCc6zKHviw2CcqB p{margin:0;}#mermaid-svg-EhCc6zKHviw2CcqB defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-EhCc6zKHviw2CcqB g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-EhCc6zKHviw2CcqB g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-EhCc6zKHviw2CcqB g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-EhCc6zKHviw2CcqB g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-EhCc6zKHviw2CcqB g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-EhCc6zKHviw2CcqB .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-EhCc6zKHviw2CcqB .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-EhCc6zKHviw2CcqB .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-EhCc6zKHviw2CcqB .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-EhCc6zKHviw2CcqB .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-EhCc6zKHviw2CcqB .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-EhCc6zKHviw2CcqB .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-EhCc6zKHviw2CcqB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EhCc6zKHviw2CcqB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EhCc6zKHviw2CcqB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EhCc6zKHviw2CcqB .edgeLabel .label text{fill:#333;}#mermaid-svg-EhCc6zKHviw2CcqB .label div .edgeLabel{color:#333;}#mermaid-svg-EhCc6zKHviw2CcqB .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-EhCc6zKHviw2CcqB .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-EhCc6zKHviw2CcqB .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-EhCc6zKHviw2CcqB .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-EhCc6zKHviw2CcqB .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-EhCc6zKHviw2CcqB .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EhCc6zKHviw2CcqB .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EhCc6zKHviw2CcqB #statediagram-barbEnd{fill:#333333;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EhCc6zKHviw2CcqB .cluster-label,#mermaid-svg-EhCc6zKHviw2CcqB .nodeLabel{color:#131300;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-EhCc6zKHviw2CcqB .note-edge{stroke-dasharray:5;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-note text{fill:black;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram-note .nodeLabel{color:black;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagram .edgeLabel{color:red;}#mermaid-svg-EhCc6zKHviw2CcqB #dependencyStart,#mermaid-svg-EhCc6zKHviw2CcqB #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-EhCc6zKHviw2CcqB .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EhCc6zKHviw2CcqB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 任务创建
编排器拾取
Agent超时/异常
重试间隔到期
部分失败需回滚
回滚完成
回滚后可重试
全部步骤成功
超过最大重试次数
PENDING
RUNNING
RETRY_WAIT
COMPENSATING
FAILED
SUCCEEDED
这个状态机的关键设计是引入了 RETRY_WAIT 和 COMPENSATING 两个中间态。它们不是"失败",而是过渡态------系统正在积极处理故障,而不是被动等待人工介入。
Agent级超时:给每次调用装上"断路器"
在多智能体系统中,Agent调用通常是远程的、非确定性的。一个Agent可能因为模型推理慢、外部API阻塞或内部死循环而长时间无响应。如果没有超时控制,编排器线程会被耗尽,整个系统吞吐量急剧下降。
实现方案 :在编排器的执行器层,为每个Agent调用包裹一层超时控制。以Java为例,使用 CompletableFuture 配合 ScheduledExecutorService 实现非阻塞超时:
java
import java.time.Duration;
import java.util.concurrent.*;
import java.util.function.Supplier;
public class AgentTimeoutExecutor {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
/**
* 在指定超时时间内执行Agent调用,超时则抛出AgentTimeoutException。
*
* @param agentCall 实际Agent调用逻辑
* @param timeout 超时时间
* @param agentName 用于日志与监控标识
* @return Agent执行结果
*/
public <T> T executeWithTimeout(
Supplier<T> agentCall,
Duration timeout,
String agentName) throws AgentTimeoutException, ExecutionException {
CompletableFuture<T> future = CompletableFuture.supplyAsync(agentCall);
// 超时任务:在指定时间后尝试取消future
scheduler.schedule(() -> {
if (!future.isDone()) {
future.completeExceptionally(
new AgentTimeoutException(
"Agent [" + agentName + "] exceeded timeout of " + timeout
)
);
}
}, timeout.toMillis(), TimeUnit.MILLISECONDS);
try {
return future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ExecutionException("Thread interrupted while waiting for agent", e);
} catch (ExecutionException e) {
if (e.getCause() instanceof AgentTimeoutException) {
throw (AgentTimeoutException) e.getCause();
}
throw e;
}
}
public static class AgentTimeoutException extends Exception {
public AgentTimeoutException(String message) {
super(message);
}
}
}
关键设计决策:
-
超时不是取消 :
future.completeExceptionally()只是让等待方提前返回,底层Agent调用线程可能仍在运行。这是刻意为之------强制中断线程(Thread.stop())是不安全的,可能导致资源泄漏。真正的取消需要Agent侧配合检查中断标志。 -
超时时间分层 :不同Agent应有不同的超时预算。例如,代码分析Agent可能允许60秒,而文档生成Agent可能只需15秒。超时预算应该作为任务定义的一部分,而不是全局常量。
-
超时后的状态流转 :当捕获
AgentTimeoutException后,编排器应将任务状态从RUNNING迁移到RETRY_WAIT,而不是直接标记为FAILED。超时可能是暂时性的(如模型服务过载),立即重试往往能成功。
幂等执行:让重试成为安全操作
重试机制的前提是------被重试的操作是幂等的。在多智能体系统中,幂等性不能依赖Agent自身的实现(Agent的输出本质上是非确定性的),而必须在编排层和状态层强制保证。
实现策略 :为每个任务步骤分配幂等键(Idempotency Key),并在共享状态层记录每个步骤的执行结果。当重试发生时,编排器先检查该步骤是否已经成功执行过。
java
import java.util.Optional;
import java.util.UUID;
public class IdempotentStepExecutor {
private final SharedStateStore stateStore;
/**
* 幂等地执行一个任务步骤。
*
* @param stepId 步骤唯一标识
* @param taskId 所属任务ID
* @param execution 实际执行逻辑
* @return 步骤执行结果(可能是历史结果)
*/
public StepResult executeIdempotent(
String stepId,
String taskId,
StepExecution execution) {
// 1. 检查是否已有成功记录
Optional<StepResult> existingResult =
stateStore.findStepResult(taskId, stepId);
if (existingResult.isPresent()) {
// 步骤已成功执行过,直接返回历史结果
return existingResult.get();
}
// 2. 生成幂等键,写入"执行中"标记
String idempotencyKey = UUID.randomUUID().toString();
boolean acquired = stateStore.tryAcquireStepLock(
taskId, stepId, idempotencyKey
);
if (!acquired) {
// 其他执行器正在执行此步骤,等待其结果
return stateStore.waitForStepResult(taskId, stepId);
}
// 3. 执行步骤
try {
StepResult result = execution.run();
// 4. 记录结果(原子写入)
stateStore.saveStepResult(taskId, stepId, result);
return result;
} finally {
// 5. 释放锁
stateStore.releaseStepLock(taskId, stepId, idempotencyKey);
}
}
public interface StepExecution {
StepResult run();
}
}
幂等性的三个层次:
| 层次 | 实现位置 | 保证内容 |
|---|---|---|
| 调用幂等 | 编排器 | 同一逻辑步骤不会被重复调度执行 |
| 写入幂等 | 状态层 | 步骤结果的写入是原子且去重的 |
| 副作用幂等 | Agent侧 | Agent对外部系统的操作可安全重复 |
实践建议:不要试图让Agent自身保证幂等性。Agent的输出是不可控的,幂等约束必须由编排器和状态层强制执行。Agent只需要声明其操作是否具有副作用,编排器据此决定重试策略。
部分失败回滚:补偿事务模式
当一个任务包含多个步骤,且步骤B失败时,步骤A已经成功执行并可能产生了副作用。此时系统面临选择:放弃整个任务 ,还是回滚步骤A?
在分布式系统中,这对应补偿事务(Compensating Transaction)模式。每个可回滚的步骤都需要注册一个补偿操作。
java
import java.util.ArrayList;
import java.util.List;
public class CompensatingTaskExecutor {
private final List<CompensableStep> executedSteps = new ArrayList<>();
/**
* 执行一个可补偿的任务步骤,并在失败时自动回滚已执行的步骤。
*/
public TaskResult executeWithCompensation(
List<CompensableStep> steps) {
int currentIndex = 0;
try {
for (; currentIndex < steps.size(); currentIndex++) {
CompensableStep step = steps.get(currentIndex);
StepResult result = step.execute();
executedSteps.add(step);
// 检查步骤结果是否触发回滚条件
if (result.requiresCompensation()) {
throw new StepFailedException(
"Step " + step.getStepId() + " reported failure"
);
}
}
return TaskResult.success();
} catch (Exception e) {
// 逆序执行补偿操作
rollbackExecutedSteps();
return TaskResult.failed(e, currentIndex);
}
}
private void rollbackExecutedSteps() {
// 从最后一个成功的步骤开始,逆序补偿
for (int i = executedSteps.size() - 1; i >= 0; i--) {
CompensableStep step = executedSteps.get(i);
try {
step.compensate();
} catch (Exception compensationError) {
// 补偿失败需要记录,但不能中断其他补偿操作
logCompensationFailure(step, compensationError);
}
}
executedSteps.clear();
}
public interface CompensableStep {
String getStepId();
StepResult execute() throws Exception;
void compensate() throws Exception;
}
}
回滚策略的关键决策:
-
补偿操作必须幂等:补偿本身也可能失败或被重复调用。补偿操作应该设计为"设置回目标状态",而不是"撤销操作"------前者天然幂等,后者依赖操作历史。
-
补偿失败需要告警,但不阻断:如果多个步骤需要补偿,其中一个补偿失败不应阻止其他补偿执行。所有补偿完成后,系统应记录哪些补偿失败,并触发人工介入告警。
-
区分"可回滚"与"不可回滚"步骤 :某些操作天然不可回滚(如发送通知邮件)。对于这类步骤,应该将其安排在任务链的最后,或者为其设计替代的补偿策略(如发送更正通知)。
完整示例:带重试与回滚的Agent任务执行器
下面是一个完整的任务执行器示例,整合了超时、幂等和回滚三个机制:
java
public class ResilientAgentTaskExecutor {
private final AgentTimeoutExecutor timeoutExecutor;
private final IdempotentStepExecutor idempotentExecutor;
private final CompensatingTaskExecutor compensatingExecutor;
private final TaskStateMachine stateMachine;
private final int maxRetries;
private final Duration retryDelay;
public ResilientAgentTaskExecutor(
AgentTimeoutExecutor timeoutExecutor,
IdempotentStepExecutor idempotentExecutor,
CompensatingTaskExecutor compensatingExecutor,
TaskStateMachine stateMachine,
int maxRetries,
Duration retryDelay) {
this.timeoutExecutor = timeoutExecutor;
this.idempotentExecutor = idempotentExecutor;
this.compensatingExecutor = compensatingExecutor;
this.stateMachine = stateMachine;
this.maxRetries = maxRetries;
this.retryDelay = retryDelay;
}
/**
* 执行一个具有完整故障隔离能力的Agent任务。
*/
public TaskResult executeTask(TaskDefinition task) {
stateMachine.transition(task.getTaskId(), TaskState.RUNNING);
int attempt = 0;
while (attempt < maxRetries) {
try {
TaskResult result = executeTaskOnce(task);
if (result.isSuccess()) {
stateMachine.transition(task.getTaskId(), TaskState.SUCCEEDED);
return result;
}
if (result.requiresCompensation()) {
// 部分失败,需要回滚
stateMachine.transition(task.getTaskId(), TaskState.COMPENSATING);
compensatingExecutor.rollback(task.getExecutedSteps());
if (task.isRetryableAfterCompensation()) {
stateMachine.transition(task.getTaskId(), TaskState.RETRY_WAIT);
sleep(retryDelay);
attempt++;
continue;
} else {
stateMachine.transition(task.getTaskId(), TaskState.FAILED);
return result;
}
}
} catch (AgentTimeoutExecutor.AgentTimeoutException e) {
// 超时:进入重试等待
stateMachine.transition(task.getTaskId(), TaskState.RETRY_WAIT);
sleep(retryDelay);
attempt++;
}
}
stateMachine.transition(task.getTaskId(), TaskState.FAILED);
return TaskResult.failed("Max retries exceeded");
}
private TaskResult executeTaskOnce(TaskDefinition task)
throws AgentTimeoutExecutor.AgentTimeoutException {
List<CompensableStep> steps = new ArrayList<>();
for (StepDefinition stepDef : task.getSteps()) {
CompensableStep step = new CompensableStep() {
@Override
public String getStepId() {
return stepDef.getStepId();
}
@Override
public StepResult execute() throws Exception {
// 幂等执行 + 超时控制
return idempotentExecutor.executeIdempotent(
stepDef.getStepId(),
task.getTaskId(),
() -> timeoutExecutor.executeWithTimeout(
() -> stepDef.getAgent().run(stepDef.getInput()),
stepDef.getTimeout(),
stepDef.getAgentName()
)
);
}
@Override
public void compensate() throws Exception {
stepDef.getCompensationAction().run();
}
};
steps.add(step);
}
return compensatingExecutor.executeWithCompensation(steps);
}
}
监控与可观测性
故障隔离机制的有效性最终取决于可观测性。建议在编排器中埋入以下指标:
| 指标名称 | 类型 | 含义 |
|---|---|---|
agent_timeout_total |
Counter | Agent超时总次数 |
agent_retry_total |
Counter | 重试总次数 |
compensation_total |
Counter | 回滚操作总次数 |
compensation_failed_total |
Counter | 回滚失败总次数 |
task_state_duration_seconds |
Histogram | 任务在各状态停留时长 |
这些指标能帮助团队识别系统性故障模式:如果某个Agent的超时率持续攀升,说明其底层服务可能过载;如果补偿失败率异常,说明补偿逻辑本身存在缺陷。
小结
故障隔离不是单一机制,而是超时、幂等、回滚三者协同的工程实践。超时划定了故障的时间边界,幂等保证了重试的安全性,回滚提供了从部分失败中恢复的路径。三者之上,状态机提供了统一的任务生命周期视图,使系统在故障发生时始终处于可解释、可预测的状态。
核心原则 :多智能体系统的可靠性不取决于单个Agent的可靠性,而取决于编排层能否在Agent不可靠的前提下,通过隔离与恢复机制维持整体的一致性。假设每个Agent都会失败,然后设计系统使其在失败时仍然正确。
下一章我们将通过一个完整的多智能体代码评审系统,把前面讨论的编排模式、状态存储和故障隔离机制整合到一个可运行的架构示例中。
代码实践:多Agent代码评审系统架构拆解
前面几章我们从理论层面讨论了编排模式、共享状态设计与故障隔离策略。但架构设计最终要落到代码上才有说服力。本章以一个多Agent代码评审系统为完整示例,展示编排器如何将代码解析、风格检查、安全扫描、逻辑评审四类子任务分发给不同执行器,并通过共享状态汇总结果。示例代码使用 Java 编写,核心依赖仅为 Redis 客户端(Jedis)和 Jackson,便于读者在本地复现。
8.1 系统整体架构
这个代码评审系统的核心诉求是:一次提交触发多个维度的审查,每个维度的执行时长、失败概率和资源消耗各不相同,但最终必须汇总为一份统一的评审报告。 如果放在单一Agent中完成,提示词会膨胀到难以维护,任何一个维度的失败都会拖垮整个审查流程。拆分为多Agent后,每个执行器只关注自己的工具边界和上下文,编排器负责调度与汇总。
#mermaid-svg-lWD73gtz0Rtfh7KA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lWD73gtz0Rtfh7KA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lWD73gtz0Rtfh7KA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lWD73gtz0Rtfh7KA .error-icon{fill:#552222;}#mermaid-svg-lWD73gtz0Rtfh7KA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lWD73gtz0Rtfh7KA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lWD73gtz0Rtfh7KA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lWD73gtz0Rtfh7KA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lWD73gtz0Rtfh7KA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lWD73gtz0Rtfh7KA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lWD73gtz0Rtfh7KA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lWD73gtz0Rtfh7KA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lWD73gtz0Rtfh7KA .marker.cross{stroke:#333333;}#mermaid-svg-lWD73gtz0Rtfh7KA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lWD73gtz0Rtfh7KA p{margin:0;}#mermaid-svg-lWD73gtz0Rtfh7KA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lWD73gtz0Rtfh7KA .cluster-label text{fill:#333;}#mermaid-svg-lWD73gtz0Rtfh7KA .cluster-label span{color:#333;}#mermaid-svg-lWD73gtz0Rtfh7KA .cluster-label span p{background-color:transparent;}#mermaid-svg-lWD73gtz0Rtfh7KA .label text,#mermaid-svg-lWD73gtz0Rtfh7KA span{fill:#333;color:#333;}#mermaid-svg-lWD73gtz0Rtfh7KA .node rect,#mermaid-svg-lWD73gtz0Rtfh7KA .node circle,#mermaid-svg-lWD73gtz0Rtfh7KA .node ellipse,#mermaid-svg-lWD73gtz0Rtfh7KA .node polygon,#mermaid-svg-lWD73gtz0Rtfh7KA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lWD73gtz0Rtfh7KA .rough-node .label text,#mermaid-svg-lWD73gtz0Rtfh7KA .node .label text,#mermaid-svg-lWD73gtz0Rtfh7KA .image-shape .label,#mermaid-svg-lWD73gtz0Rtfh7KA .icon-shape .label{text-anchor:middle;}#mermaid-svg-lWD73gtz0Rtfh7KA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lWD73gtz0Rtfh7KA .rough-node .label,#mermaid-svg-lWD73gtz0Rtfh7KA .node .label,#mermaid-svg-lWD73gtz0Rtfh7KA .image-shape .label,#mermaid-svg-lWD73gtz0Rtfh7KA .icon-shape .label{text-align:center;}#mermaid-svg-lWD73gtz0Rtfh7KA .node.clickable{cursor:pointer;}#mermaid-svg-lWD73gtz0Rtfh7KA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lWD73gtz0Rtfh7KA .arrowheadPath{fill:#333333;}#mermaid-svg-lWD73gtz0Rtfh7KA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lWD73gtz0Rtfh7KA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lWD73gtz0Rtfh7KA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lWD73gtz0Rtfh7KA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lWD73gtz0Rtfh7KA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lWD73gtz0Rtfh7KA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lWD73gtz0Rtfh7KA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lWD73gtz0Rtfh7KA .cluster text{fill:#333;}#mermaid-svg-lWD73gtz0Rtfh7KA .cluster span{color:#333;}#mermaid-svg-lWD73gtz0Rtfh7KA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lWD73gtz0Rtfh7KA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lWD73gtz0Rtfh7KA rect.text{fill:none;stroke-width:0;}#mermaid-svg-lWD73gtz0Rtfh7KA .icon-shape,#mermaid-svg-lWD73gtz0Rtfh7KA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lWD73gtz0Rtfh7KA .icon-shape p,#mermaid-svg-lWD73gtz0Rtfh7KA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lWD73gtz0Rtfh7KA .icon-shape .label rect,#mermaid-svg-lWD73gtz0Rtfh7KA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lWD73gtz0Rtfh7KA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lWD73gtz0Rtfh7KA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lWD73gtz0Rtfh7KA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 共享状态层
执行器 Executors
编排器 Orchestrator
接收评审请求
创建共享状态
分发子任务
等待结果
汇总报告
ParserAgent
代码解析
StyleAgent
风格检查
SecurityAgent
安全扫描
LogicAgent
逻辑评审
Redis
ReviewState
8.2 共享状态模型
共享状态是整个系统的枢纽。我们使用 Redis Hash 存储一次评审任务的全部状态,key 格式为 review:{reviewId}。状态中记录每个子任务的状态机:PENDING → RUNNING → SUCCESS / FAILED / TIMEOUT,以及最终汇总报告的生成状态。
java
public class ReviewState {
private String reviewId;
private String commitSha;
private String repoPath;
private Map<String, SubTaskState> subTasks; // key: parser/style/security/logic
private String finalReport;
private ReviewStatus overallStatus; // PENDING, PARTIAL, COMPLETED, FAILED
public enum SubTaskState {
PENDING, RUNNING, SUCCESS, FAILED, TIMEOUT
}
public enum ReviewStatus {
PENDING, PARTIAL, COMPLETED, FAILED
}
}
选择 Redis 而非进程内内存的原因是:执行器可能运行在不同的线程池甚至不同的机器上 ,进程内状态无法跨执行器共享。Redis Hash 的字段级原子操作(HSET、HGET)天然适合这种"多写一读"的场景。每个执行器只更新自己负责的字段,编排器通过轮询或订阅来感知整体进度。
8.3 编排器实现
编排器的核心职责是:创建共享状态、并发分发子任务、在部分失败时决定降级策略、最终汇总报告。 它本身不执行任何审查逻辑,只做调度与决策。这种"瘦编排器"设计让调度逻辑和业务逻辑彻底解耦。
java
public class ReviewOrchestrator {
private final ExecutorService executorPool;
private final Jedis redis;
private final ObjectMapper mapper;
public ReviewOrchestrator(ExecutorService executorPool, Jedis redis) {
this.executorPool = executorPool;
this.redis = redis;
this.mapper = new ObjectMapper();
}
/**
* 发起一次代码评审。四个子任务并发执行,编排器等待全部结束后汇总。
* 即使部分子任务失败,也会生成 PARTIAL 状态的报告。
*/
public ReviewState orchestrate(String reviewId, String repoPath, String commitSha)
throws InterruptedException {
// 1. 初始化共享状态
ReviewState state = new ReviewState();
state.setReviewId(reviewId);
state.setRepoPath(repoPath);
state.setCommitSha(commitSha);
state.setOverallStatus(ReviewStatus.PENDING);
state.setSubTasks(new ConcurrentHashMap<>());
for (String task : List.of("parser", "style", "security", "logic")) {
state.getSubTasks().put(task, ReviewState.SubTaskState.PENDING);
}
persistState(state);
// 2. 并发分发子任务,每个执行器持有独立的超时与重试配置
CountDownLatch latch = new CountDownLatch(4);
List<Future<?>> futures = new ArrayList<>();
futures.add(executorPool.submit(
new ParserAgent(reviewId, repoPath, redis, latch)));
futures.add(executorPool.submit(
new StyleAgent(reviewId, repoPath, redis, latch)));
futures.add(executorPool.submit(
new SecurityAgent(reviewId, repoPath, redis, latch)));
futures.add(executorPool.submit(
new LogicAgent(reviewId, repoPath, redis, latch)));
// 3. 等待所有子任务结束(latch 确保所有执行器都已写入最终状态)
latch.await(30, TimeUnit.SECONDS);
// 4. 取消仍未完成的 Future(超时兜底)
for (Future<?> f : futures) {
if (!f.isDone()) {
f.cancel(true);
}
}
// 5. 汇总结果
ReviewState finalState = loadState(reviewId);
ReviewReport report = aggregateReport(finalState);
finalState.setFinalReport(mapper.writeValueAsString(report));
finalState.setOverallStatus(determineOverallStatus(finalState));
persistState(finalState);
return finalState;
}
private ReviewStatus determineOverallStatus(ReviewState state) {
long successCount = state.getSubTasks().values().stream()
.filter(s -> s == ReviewState.SubTaskState.SUCCESS)
.count();
if (successCount == 4) return ReviewStatus.COMPLETED;
if (successCount > 0) return ReviewStatus.PARTIAL;
return ReviewStatus.FAILED;
}
}
关键设计点在于 CountDownLatch 的使用:编排器不依赖 Future.get() 的阻塞超时来判断任务是否结束,而是让每个执行器在完成或失败后都调用 latch.countDown() 。这样即使用线程池中的某个任务被卡住,latch.await(30, TimeUnit.SECONDS) 也能保证编排器不会无限等待。超时后,编排器主动取消未完成的任务,并将对应状态标记为 TIMEOUT。
8.4 执行器基类:统一状态更新与超时控制
四个执行器的结构高度相似,差异仅在于审查逻辑。因此我们抽象出一个基类,封装状态更新、超时控制、异常捕获 等横切关注点。每个具体执行器只需实现 doReview() 方法。
java
public abstract class BaseReviewAgent implements Runnable {
protected final String reviewId;
protected final String repoPath;
protected final Jedis redis;
protected final CountDownLatch latch;
protected final ObjectMapper mapper = new ObjectMapper();
protected BaseReviewAgent(String reviewId, String repoPath,
Jedis redis, CountDownLatch latch) {
this.reviewId = reviewId;
this.repoPath = repoPath;
this.redis = redis;
this.latch = latch;
}
@Override
public void run() {
String taskName = getTaskName();
try {
updateSubTaskState(taskName, ReviewState.SubTaskState.RUNNING);
String result = doReview();
updateSubTaskResult(taskName, result);
updateSubTaskState(taskName, ReviewState.SubTaskState.SUCCESS);
} catch (Exception e) {
updateSubTaskState(taskName, ReviewState.SubTaskState.FAILED);
// 记录异常信息到共享状态,供编排器汇总时参考
updateSubTaskError(taskName, e.getMessage());
} finally {
latch.countDown();
}
}
protected abstract String getTaskName();
protected abstract String doReview() throws Exception;
protected void updateSubTaskState(String task, ReviewState.SubTaskState state) {
redis.hset("review:" + reviewId + ":subtasks", task, state.name());
}
protected void updateSubTaskResult(String task, String result) {
redis.hset("review:" + reviewId + ":results", task, result);
}
protected void updateSubTaskError(String task, String error) {
redis.hset("review:" + reviewId + ":errors", task, error);
}
}
这个基类体现了上一章讨论的故障隔离原则 :任何执行器的异常都不会向上传播到编排器,而是被捕获后写入共享状态。编排器看到的是"某个子任务失败了",而不是"整个评审流程崩溃了"。此外,finally 块中的 latch.countDown() 保证了无论成功、失败还是超时,编排器的等待条件都能被满足。
8.5 具体执行器示例:安全扫描Agent
以安全扫描Agent为例展示具体实现。它模拟了对代码中危险模式(如 SQL 注入、硬编码密钥)的检测。真实场景中可以替换为调用 Semgrep、CodeQL 等外部工具。
java
public class SecurityAgent extends BaseReviewAgent {
private static final List<String> DANGEROUS_PATTERNS = List.of(
"SELECT.*WHERE.*\\+.*userInput",
"password\\s*=\\s*\"[^\"]*\"",
"api[_-]?key\\s*=\\s*\"[^\"]*\""
);
public SecurityAgent(String reviewId, String repoPath,
Jedis redis, CountDownLatch latch) {
super(reviewId, repoPath, redis, latch);
}
@Override
protected String getTaskName() {
return "security";
}
@Override
protected String doReview() throws Exception {
// 模拟扫描过程
Thread.sleep(800);
List<String> findings = scanFiles(repoPath);
return mapper.writeValueAsString(findings);
}
private List<String> scanFiles(String repoPath) {
List<String> findings = new ArrayList<>();
try (Stream<Path> paths = Files.walk(Path.of(repoPath))) {
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(file -> {
try {
String content = Files.readString(file);
for (String pattern : DANGEROUS_PATTERNS) {
if (content.matches("(?s).*" + pattern + ".*")) {
findings.add(file.getFileName() + ": " + pattern);
}
}
} catch (IOException ignored) { }
});
} catch (IOException e) {
// 文件读取失败时抛出,由基类捕获并标记为 FAILED
throw new RuntimeException("Failed to scan repository", e);
}
return findings;
}
}
其他三个执行器(ParserAgent、StyleAgent、LogicAgent)结构相同,只是 doReview() 中的审查逻辑不同。ParserAgent 负责解析 AST 并检查语法错误,StyleAgent 检查命名规范和代码格式,LogicAgent 分析潜在的逻辑缺陷(如空指针解引用、死循环)。限于篇幅不逐一列出,完整代码可在配套仓库中获取。
8.6 汇总报告与部分失败降级
编排器的 aggregateReport 方法从共享状态中读取各子任务的结果与错误信息,生成统一报告。关键点在于:即使部分子任务失败,报告仍然生成,只是明确标注哪些维度不可用。 这比"全有或全无"的策略更符合生产环境的实际需求------开发者宁可拿到一份缺少安全扫描结果的报告,也不愿意整个评审流程因为安全扫描器故障而完全不可用。
java
public class ReviewReport {
private String reviewId;
private String commitSha;
private Map<String, String> results; // task -> result JSON
private Map<String, String> errors; // task -> error message
private List<String> failedTasks;
private String generatedAt;
// getters and setters omitted
}
汇总逻辑的核心代码如下:
java
private ReviewReport aggregateReport(ReviewState state) {
ReviewReport report = new ReviewReport();
report.setReviewId(state.getReviewId());
report.setCommitSha(state.getCommitSha());
report.setGeneratedAt(Instant.now().toString());
Map<String, String> results = redis.hgetAll("review:" + state.getReviewId() + ":results");
Map<String, String> errors = redis.hgetAll("review:" + state.getReviewId() + ":errors");
report.setResults(results);
report.setErrors(errors);
report.setFailedTasks(errors.keySet().stream().toList());
return report;
}
8.7 运行示例与验证
以下是一个简单的 main 方法,演示如何启动整个评审流程:
java
public class Main {
public static void main(String[] args) throws Exception {
Jedis redis = new Jedis("localhost", 6379);
ExecutorService pool = Executors.newFixedThreadPool(4);
ReviewOrchestrator orchestrator = new ReviewOrchestrator(pool, redis);
String reviewId = UUID.randomUUID().toString();
ReviewState result = orchestrator.orchestrate(
reviewId, "/path/to/repo", "a1b2c3d4e5f6");
System.out.println("Overall status: " + result.getOverallStatus());
System.out.println("Final report: " + result.getFinalReport());
pool.shutdown();
redis.close();
}
}
运行后,Redis 中会留下完整的审计轨迹:review:{id}:subtasks 记录每个子任务的状态机变化,review:{id}:results 存储成功结果,review:{id}:errors 存储失败原因。这种设计让故障排查变得简单------不需要翻看日志,直接查询共享状态就能知道哪个Agent在哪个环节出了问题。
8.8 为什么这个架构能上生产
这个示例虽然精简,但体现了生产级多Agent系统的几个关键架构决策:
| 架构决策 | 实现方式 | 生产价值 |
|---|---|---|
| 职责切分 | 编排器只做调度,执行器只做审查 | 修改审查逻辑不影响调度流程 |
| 状态外置 | Redis Hash 存储子任务状态 | 执行器可跨线程/跨机器运行 |
| 故障隔离 | 基类捕获所有异常,不向上传播 | 单个Agent失败不拖垮整个流程 |
| 超时兜底 | CountDownLatch + Future.cancel | 避免编排器被卡住的Agent永久阻塞 |
| 部分降级 | PARTIAL 状态 + 失败任务标注 | 核心功能可用性优先于完整性 |
这套架构的本质是把"不可靠的执行"与"可靠的编排"分离开来。 执行器可能因为外部工具故障、代码库异常、网络超时等原因失败,但编排器通过共享状态始终掌握全局信息,并能在部分失败时做出合理的降级决策。这正是多Agent系统从"演示级"走向"生产级"的关键一步。
下一章我们将讨论多Agent系统的可观测性设计:如何在不侵入业务逻辑的前提下,采集编排链路的关键指标与分布式追踪信息。
最佳实践与经验总结
在前面的章节中,我们从编排模式、共享状态设计、故障隔离策略一路走到代码评审系统的完整实现。当这些设计决策落到真实的生产环境时,很多看似"理论上成立"的方案会暴露出维护成本高、故障扩散快、调试困难等问题。本章将这些经验沉淀为五条可操作的设计法则,每一条都对应一个具体的架构决策点,并附带反例说明"如果不这样做会怎样"。
法则一:保持 Agent 单一职责
多智能体系统的第一个陷阱,往往出现在"Agent 应该做多少事"这个看似简单的问题上。一个常见的错误是:为了让系统"看起来更智能",开发者会让一个 Agent 同时承担任务规划、工具调用、结果汇总、异常处理等多种职责。这在 demo 阶段运行良好,但当任务复杂度上升时,这个"全能 Agent"会迅速成为系统的单点瓶颈。
单一职责的边界应该按"能力域"而非"任务步骤"来划分。 例如,在代码评审系统中,我们划分了 CodeParsingAgent、StyleCheckAgent、SecurityScanAgent 和 LogicReviewAgent。这四个 Agent 的职责边界是清晰的能力域:解析关注语法结构,风格检查关注编码规范,安全扫描关注漏洞模式,逻辑评审关注业务正确性。它们之间不存在职责重叠,任何一个 Agent 的替换或升级都不会影响其他 Agent 的执行逻辑。
反过来说,如果我们将"解析 + 风格检查"合并到一个 Agent 中,那么这个 Agent 就需要同时加载语法树解析工具和风格规则库。当风格规则需要频繁更新时(这在真实项目中非常常见),每次更新都意味着要重新测试整个 Agent 的解析功能------这显然是不合理的。
实践建议: 当一个 Agent 的提示词或系统指令超过约 500 字,或者它需要调用超过 5 个不同的工具时,就应该考虑拆分。拆分的判断标准不是"这个 Agent 做了什么",而是"这个 Agent 可以独立演化吗"。
法则二:编排逻辑与业务逻辑分离
这是五条法则中最容易被忽视的一条,也是决定系统可维护性的关键分水岭。在多智能体系统中,编排逻辑 回答的是"先做什么、后做什么、失败后怎么办"的问题;业务逻辑回答的是"这件事具体怎么做"的问题。两者如果混在一起,任何一方的变更都会牵动另一方。
在代码评审系统的实现中,CodeReviewOrchestrator 承担了全部编排职责:它决定四个子任务的执行顺序(风格检查与安全扫描并行、代码解析先行)、管理共享状态的初始化与更新、处理超时与重试。而四个执行器 Agent 完全不感知编排的存在------它们只接收输入、执行自己的业务逻辑、返回结果。这种设计带来一个直接的好处:如果未来需要将"并行执行"改为"串行执行",或者引入新的依赖关系(比如逻辑评审必须等待安全扫描完成),只需要修改 Orchestrator 中的任务图定义,四个执行器 Agent 的代码一行都不用动。
反例警示: 如果让 SecurityScanAgent 在自己的执行逻辑中直接触发 LogicReviewAgent 的启动,那么这两个 Agent 之间就产生了隐式的编排耦合。当安全扫描的耗时变化时,逻辑评审的启动时机也会受到影响,而这一切对外部观察者来说是不可见的。调试时你会发现自己需要在多个 Agent 的代码之间跳转,才能拼凑出完整的执行流程。
实践建议: 将编排逻辑集中在一个明确的组件中(无论是 DAG 引擎、工作流定义文件还是 Orchestrator 类),并确保执行器 Agent 之间不直接通信。所有 Agent 间的数据传递都应通过编排器或共享状态完成。
法则三:状态写入最小化
共享状态是多智能体协作的粘合剂,但也是系统复杂度的放大器。每增加一个状态字段,就增加了一类潜在的不一致问题。因此,状态写入最小化 的核心思想是:只持久化那些"必须跨 Agent 共享"或"必须支持故障恢复"的数据,其他数据应尽量保持在单个 Agent 的局部作用域内。
在代码评审系统中,共享状态 ReviewState 只包含三类数据:任务元数据(taskId、status、timestamps)、各 Agent 的执行结果(parsedCode、styleIssues、securityIssues、logicIssues)、以及最终汇总报告。注意,代码解析产生的中间数据结构(如 AST 节点列表)并没有写入共享状态------它作为 CodeParsingAgent 的返回对象直接传递给了编排器,由编排器在内存中传递给下游 Agent。这是一个刻意的设计选择:中间数据不进共享状态,只有结果数据才需要持久化。
这样做有三个理由。第一,中间数据往往体积较大,写入 Redis 或 Postgres 会显著增加 I/O 开销。第二,中间数据的生命周期很短,不需要跨故障恢复------如果解析 Agent 失败了,重新执行即可,没有必要从状态存储中恢复一个半成品的 AST。第三,中间数据的结构经常变化,如果将其纳入共享状态,任何结构调整都会导致状态 schema 的迁移。
实践建议: 在定义共享状态 schema 时,对每一个字段问三个问题:"这个字段会被多个 Agent 读取吗?""这个字段在 Agent 失败后需要恢复吗?""这个字段对最终结果或审计追踪是必需的吗?" 只有至少一个问题的答案是"是"时,才将其纳入共享状态。
法则四:优先使用事件驱动解耦
当多智能体系统的 Agent 数量从三四个增长到十几个甚至更多时,基于编排器直接调用的同步模式会遇到瓶颈:编排器需要感知所有 Agent 的存在,任何一个 Agent 的接口变化都可能导致编排器修改。此时,事件驱动架构提供了一种更松散的耦合方式。
事件驱动的核心思想是:Agent 之间不直接调用,而是通过事件总线(如 Kafka、RabbitMQ 或 Redis Streams)进行异步通信。一个 Agent 完成自己的工作后,发布一个领域事件;其他关心该事件的 Agent 订阅并做出响应。编排器退化为事件流的"引导者"而非"指挥者"------它只负责发布初始任务事件,后续的协作由 Agent 之间的订阅关系自动完成。
LogicAgent SecurityAgent ParsingAgent EventBus Orchestrator LogicAgent SecurityAgent ParsingAgent EventBus Orchestrator #mermaid-svg-wnp5itd4kb8ipNjo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wnp5itd4kb8ipNjo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wnp5itd4kb8ipNjo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wnp5itd4kb8ipNjo .error-icon{fill:#552222;}#mermaid-svg-wnp5itd4kb8ipNjo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wnp5itd4kb8ipNjo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wnp5itd4kb8ipNjo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wnp5itd4kb8ipNjo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wnp5itd4kb8ipNjo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wnp5itd4kb8ipNjo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wnp5itd4kb8ipNjo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wnp5itd4kb8ipNjo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wnp5itd4kb8ipNjo .marker.cross{stroke:#333333;}#mermaid-svg-wnp5itd4kb8ipNjo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wnp5itd4kb8ipNjo p{margin:0;}#mermaid-svg-wnp5itd4kb8ipNjo .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wnp5itd4kb8ipNjo text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-wnp5itd4kb8ipNjo .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wnp5itd4kb8ipNjo .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-wnp5itd4kb8ipNjo .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-wnp5itd4kb8ipNjo .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-wnp5itd4kb8ipNjo #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-wnp5itd4kb8ipNjo .sequenceNumber{fill:white;}#mermaid-svg-wnp5itd4kb8ipNjo #sequencenumber{fill:#333;}#mermaid-svg-wnp5itd4kb8ipNjo #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-wnp5itd4kb8ipNjo .messageText{fill:#333;stroke:none;}#mermaid-svg-wnp5itd4kb8ipNjo .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wnp5itd4kb8ipNjo .labelText,#mermaid-svg-wnp5itd4kb8ipNjo .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-wnp5itd4kb8ipNjo .loopText,#mermaid-svg-wnp5itd4kb8ipNjo .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-wnp5itd4kb8ipNjo .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wnp5itd4kb8ipNjo .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-wnp5itd4kb8ipNjo .noteText,#mermaid-svg-wnp5itd4kb8ipNjo .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-wnp5itd4kb8ipNjo .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wnp5itd4kb8ipNjo .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wnp5itd4kb8ipNjo .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wnp5itd4kb8ipNjo .actorPopupMenu{position:absolute;}#mermaid-svg-wnp5itd4kb8ipNjo .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-wnp5itd4kb8ipNjo .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wnp5itd4kb8ipNjo .actor-man circle,#mermaid-svg-wnp5itd4kb8ipNjo line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-wnp5itd4kb8ipNjo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} publish(TaskCreated)TaskCreatedpublish(CodeParsed)CodeParsedCodeParsedpublish(SecurityScanCompleted)publish(LogicReviewCompleted)SecurityScanCompleted + LogicReviewCompletedpublish(ReviewReportGenerated)
这种模式的显著优势在于可扩展性 :新增一个 Agent 只需要订阅它关心的事件并发布自己产生的事件,不需要修改现有 Agent 或编排器的任何代码。在代码评审系统的场景中,如果未来需要增加一个"依赖漏洞检查 Agent",它只需要订阅 CodeParsed 事件即可。
但事件驱动也有代价:调试难度上升、错误追踪链路变长、最终一致性带来的时序问题。因此,我们的建议是"优先使用"而非"必须使用"。判断标准是:当 Agent 数量超过约 6 个,或者 Agent 之间存在明显的异步处理需求(如长时间运行的任务),或者需要支持动态加入/移除 Agent 时,事件驱动的收益会超过其引入的复杂度。在此之前,同步编排器的简单性和可调试性更有价值。
法则五:为每个 Agent 设置独立超时与资源配额
多智能体系统中的故障隔离,最直接的落地手段就是独立超时与资源配额。如果所有 Agent 共享一个全局超时设置,就会出现"木桶效应":一个慢 Agent 拖垮整个任务,或者一个 Agent 的无限循环耗尽所有可用资源。
在代码评审系统的实现中,每个执行器 Agent 都有独立的超时配置:
java
// 各 Agent 独立超时配置(毫秒)
Map<String, Integer> agentTimeouts = Map.of(
"code_parsing", 10_000, // 代码解析:10 秒
"style_check", 15_000, // 风格检查:15 秒
"security_scan", 30_000, // 安全扫描:30 秒(规则库较大)
"logic_review", 45_000 // 逻辑评审:45 秒(需要深度推理)
);
这些超时值不是拍脑袋定的,而是基于各 Agent 的工作负载特征:代码解析是纯计算任务,耗时可控;安全扫描需要加载规则库并执行模式匹配,耗时中等;逻辑评审可能需要多次 LLM 调用,耗时最长。为每个 Agent 设置独立的超时,意味着一个 Agent 的超时不会导致其他 Agent 被误杀。
资源配额 则更进一步,它限制的是 Agent 对底层资源的消耗。在基于 LLM 的多智能体系统中,资源配额通常体现为最大 token 消耗量、最大工具调用次数、最大并发请求数 。例如,LogicReviewAgent 的配置可能包含 maxTokens: 8000 和 maxToolCalls: 10,确保它不会在一次评审中消耗过多的 LLM 配额。在分布式执行环境中,资源配额还可以是 CPU 核心数、内存上限或 GPU 时间片。
实践建议: 将超时和资源配额作为 Agent 配置的一部分,与 Agent 的代码放在一起管理。不要将这些值硬编码在编排器的调用逻辑中,而是通过配置文件或配置中心注入。这样,当某个 Agent 的执行环境发生变化时(比如从本地模型切换到远程 API),只需要调整该 Agent 的配置,不会影响其他组件。
总结:五条法则的内在联系
这五条法则不是孤立的,它们之间存在清晰的逻辑关系:
| 法则 | 解决的核心问题 | 违反后的典型症状 |
|---|---|---|
| Agent 单一职责 | 能力边界模糊 | 单点瓶颈、更新困难 |
| 编排与业务分离 | 控制流与数据流耦合 | 修改一处牵动全局 |
| 状态写入最小化 | 状态复杂度膨胀 | 数据不一致、I/O 瓶颈 |
| 事件驱动解耦 | Agent 间耦合过紧 | 新增 Agent 需改多处 |
| 独立超时与配额 | 故障扩散 | 一个 Agent 拖垮全系统 |
从架构演进的视角看,这五条法则形成了一条清晰的设计路径:先确保每个 Agent 做对一件事(单一职责),再确保它们之间如何协作是清晰可改的(编排分离),然后控制协作中产生的共享数据量(状态最小化),当协作规模扩大时引入事件驱动(解耦),最后为每个协作单元设置独立的安全边界(超时与配额)。
多智能体系统的架构设计,本质上是在智能性 与可控性之间寻找平衡。过于集中的设计会损失智能体的自主性,过于分散的设计则会失去系统的可观测性。这五条法则提供的不是一份教条式的清单,而是一个在复杂度增长过程中反复校准的参照系。当你下一次面对一个行为异常的多智能体系统时,不妨回到这五条法则,看看是哪一条被忽视了------答案往往就在其中。
常见坑与排错指南
多智能体系统从"跑通 demo"到"生产可维护"之间,横亘着一系列高频且隐蔽的工程陷阱。这些坑往往不在单 Agent 内部,而在 Agent 之间的交互边界、共享状态的并发访问以及事件链路的可靠性上。本章将这些高频问题归纳为四类,逐一给出诊断方法与可落地的解决方案。
坑一:状态竞争导致的结果不一致
症状 :两个或多个 Agent 同时读写同一份共享状态,最终结果取决于执行顺序,偶发出现"覆盖丢失"或"读到中间态"。例如,代码评审系统中,一个 Agent 正在更新评审状态为 APPROVED,另一个 Agent 同时基于旧的 PENDING 状态触发了一轮新的代码分析,导致最终状态回退。
诊断方法:
- 在状态写入处记录版本号(
version字段)与操作者 ID,回放日志检查是否存在交错写入。 - 使用 Redis 的
MONITOR命令或 Postgres 的pg_stat_activity观察并发写入时序。 - 复现时故意在两个 Agent 之间注入 50~200ms 延迟,观察结果是否变化。
根因:共享状态没有并发控制原语,或控制粒度过粗,Agent 之间形成了事实上的"读-改-写"竞态。
解决方案:
- 乐观并发控制(OCC) :为每个状态记录增加
version字段,更新时使用UPDATE ... WHERE version = ?条件。若影响行数为 0,说明发生了并发冲突,触发重试或让上层编排器裁决。
java
// 基于 Postgres 的乐观锁更新示例
public boolean updateReviewStatus(UUID reviewId, String expectedStatus,
String newStatus, int expectedVersion) {
String sql = """
UPDATE review_state
SET status = ?, version = version + 1, updated_at = now()
WHERE review_id = ? AND version = ? AND status = ?
""";
int rows = jdbcTemplate.update(sql, newStatus, reviewId, expectedVersion, expectedStatus);
if (rows == 0) {
// 版本冲突或状态已变更,交由编排器决定重试或放弃
logger.warn("Optimistic lock conflict for review {}", reviewId);
return false;
}
return true;
}
-
单写者原则:对于关键状态字段,明确指定唯一拥有写权限的 Agent(或编排器),其他 Agent 只能通过消息队列提交"意图",由写者串行处理。这从根本上消除了竞态。
-
状态快照 + 对比合并:对于允许最终一致的场景,Agent 在写入前读取快照,计算差异后合并。合并函数必须满足交换律与结合律(如 CRDT 中的 LWW-Register 或 OR-Set)。
实战要点:不要用分布式锁来"解决"状态竞争。锁只能保证互斥,无法解决"基于旧值做决策"的逻辑错误,且引入锁超时、死锁等新问题。先问"这个状态是否真的需要并发写",再问"能否用版本号表达冲突"。
坑二:Agent 死循环与重复执行
症状:某个 Agent 在任务未真正完成时反复触发自身或相互触发,形成循环;或者同一个任务被执行多次,产生重复副作用(如重复发送通知、重复创建分支)。
诊断方法:
- 在事件头中加入
correlation_id与causation_id,构建事件溯源链。当发现链中出现重复的(agent_id, causation_id)对时,即可判定循环。 - 统计每个 Agent 的"单位任务完成率"与"平均重试次数"。若某 Agent 的重试率异常高且每次重试都产生新事件,则很可能处于循环。
根因:
- 触发条件设计为"状态不满足就重试",但重试本身不改变状态,形成"等待-重试-等待"死循环。
- Agent 缺少幂等键,网络超时后客户端重发,导致同一任务被执行多次。
- 事件总线采用"至少一次"投递语义,但消费者没有去重机制。
解决方案:
- 幂等执行 :为每个任务分配全局唯一的
task_id,Agent 执行前先检查task_id是否已存在于执行记录表中。若存在,直接返回已有结果,不重复执行副作用操作。
java
// 幂等执行骨架
public AgentResult execute(Task task) {
String taskId = task.getTaskId();
// 尝试插入执行记录,利用唯一约束保证只有一个实例能插入成功
boolean inserted = executionLogDao.tryInsert(taskId, agentId, Status.RUNNING);
if (!inserted) {
// 已有执行记录,说明是重复投递,直接返回已有结果
return executionLogDao.getResult(taskId);
}
try {
AgentResult result = doExecute(task);
executionLogDao.markCompleted(taskId, result);
return result;
} catch (Exception e) {
executionLogDao.markFailed(taskId, e.getMessage());
throw e;
}
}
-
循环检测与熔断 :在编排器中维护每个
correlation_id的 Agent 访问计数。当同一 Agent 在同一链路中被触发超过阈值(如 3 次),强制终止该链路并输出诊断信息,避免无限循环消耗资源。 -
触发条件与执行解耦:将"检查条件是否满足"与"执行动作"分离。条件不满足时,不直接重试,而是注册一个"条件变更监听"。当条件确实发生变化时,再由事件驱动触发执行。这样避免了无意义的轮询循环。
实战要点:死循环往往不是单个 Agent 的 bug,而是多个 Agent 的触发条件形成了"循环依赖图"。在系统设计阶段,用有向图画出 Agent 之间的触发关系,检查是否存在环。若存在环,必须在环上至少设置一个"终止条件"或"人工介入点"。
坑三:事件丢失与乱序
症状:某些任务"神秘消失"------没有报错,但下游 Agent 从未收到触发事件;或者事件到达顺序与发送顺序不一致,导致状态被错误覆盖。
诊断方法:
- 在事件生产端与消费端分别记录序号(
sequence_number),对比两端序号集合,找出缺失的事件。 - 在事件头中加入时间戳与上游序号,消费端检测到序号跳跃时立即告警。
- 使用消息队列的"死信队列"与"消息轨迹"功能,追踪事件从生产到消费的完整路径。
根因:
- 事件总线配置为"至多一次"投递(如某些内存队列),进程崩溃时事件丢失。
- 消费者采用异步提交 offset,处理失败但 offset 已提交,导致事件被跳过。
- 多个事件由不同分区并行投递,而消费者没有按业务键做分区有序处理。
解决方案:
-
关键事件持久化:将 Agent 间通信的事件写入持久化存储(Postgres 表或 Kafka topic),而不是仅依赖内存队列。生产端使用"先写事件日志,再执行业务"的事务性 outbox 模式。
-
按业务键分区有序 :对于需要严格有序的事件(如同一
review_id的状态变更事件),使用消息队列的 key 分区机制,保证同一业务键的事件进入同一分区,由单线程消费者顺序处理。
java
// Kafka 生产者:按 reviewId 分区,保证同一评审的状态事件有序
public void publishReviewEvent(ReviewEvent event) {
String key = event.getReviewId().toString();
kafkaTemplate.send("review-events", key, event);
}
// 消费者:单线程消费同一分区内的事件,天然有序
@KafkaListener(topics = "review-events", concurrency = "1")
public void onReviewEvent(ReviewEvent event) {
// 按到达顺序处理,无需额外排序
processor.process(event);
}
- 消费端幂等 + 顺序校验:即使投递层保证了有序,消费端仍需校验事件序号。若发现跳跃,先将后续事件暂存,等待缺失事件到达或触发补偿查询。
实战要点 :不要在应用层用"重发"来弥补事件丢失。重发会引入重复事件,而重复事件又会触发幂等问题。正确做法是:持久化事件 + 至少一次投递 + 消费端幂等。三者配合才能在"不丢"与"不重"之间取得平衡。
坑四:锁粒度过大引发的性能瓶颈
症状:系统在高并发下吞吐量骤降,Agent 执行时间变长,但 CPU 与内存使用率并不高。排查发现大量 Agent 在等待同一把锁,锁等待时间占据了执行时间的主要部分。
诊断方法:
- 在锁获取处记录等待时间(
lock_wait_ms),统计各锁的竞争率。若某把锁的等待时间占比超过 30%,说明锁粒度过大。 - 使用 Postgres 的
pg_locks视图或 Redis 的CLIENT LIST观察锁竞争情况。 - 对比"串行执行耗时"与"并发执行耗时",若两者接近,说明锁已经将并发串行化。
根因:
- 使用一把全局锁保护所有共享状态,即使不同 Agent 操作的是完全独立的数据。
- 锁的持有时间过长,在锁内执行了远程调用、文件 IO 或复杂计算。
- 读写锁使用不当,读多写少的场景仍使用互斥锁。
解决方案:
- 锁粒度拆分 :将全局锁拆分为按业务键(如
review_id)的细粒度锁。不同评审任务之间互不阻塞。
java
// 基于 Guava Striped 的细粒度锁
private final Striped<Lock> stripedLocks = Striped.lazyWeakLock(128);
public void updateReviewState(UUID reviewId, Consumer<ReviewState> updater) {
Lock lock = stripedLocks.get(reviewId);
lock.lock();
try {
ReviewState state = reviewStateDao.load(reviewId);
updater.accept(state);
reviewStateDao.save(state);
} finally {
lock.unlock();
}
}
-
缩短锁持有时间:锁内只做内存中的状态变更,将持久化、通知、日志等耗时操作移到锁外。如果必须持久化,使用"先写意图日志,锁外异步提交"的方式。
-
读写分离 :对于读多写少的场景,使用
ReadWriteLock或数据库的 MVCC 机制。读操作不加锁,写操作使用版本号做乐观并发控制。 -
无锁化设计 :对于高频更新的计数器、状态标志等,使用原子操作(如 Redis 的
INCR、Java 的AtomicInteger)替代显式锁。
实战要点 :锁粒度的优化不是"越细越好"。过细的锁会增加管理开销与死锁风险。正确做法是:先测量锁竞争率,再针对性拆分。如果竞争率低于 5%,保持粗粒度锁反而更简单可靠。
排查工具与流程总结
多智能体系统的排错与单体应用有本质区别:问题往往不在某个 Agent 的代码里,而在 Agent 之间的交互时序、状态变更链路与事件投递可靠性上。建议为系统内置以下可观测性能力:
| 可观测性维度 | 关键指标 | 工具/手段 |
|---|---|---|
| 事件链路 | 事件序号、投递延迟、丢失率 | 事件头埋点 + 链路追踪 |
| 状态变更 | 版本号、写入者、变更历史 | 状态表审计字段 + 事件溯源 |
| 锁竞争 | 锁等待时间、竞争率、持锁时长 | 锁埋点 + 指标采集 |
| Agent 执行 | 任务耗时、重试率、循环检测 | 执行日志 + 熔断告警 |
排查流程建议:
- 先定位"哪个 Agent 出了问题":通过链路追踪确定异常发生在哪个 Agent 的输入/输出边界。
- 再判断"是数据问题还是时序问题":检查该 Agent 读取的状态版本与事件序号,判断是读到了旧数据还是事件乱序。
- 最后分析"是并发冲突还是逻辑错误":通过锁等待指标与版本冲突日志区分是资源竞争还是业务规则缺陷。
多智能体系统的排错能力,本质上是架构设计的一部分。如果在系统设计阶段没有为事件、状态与锁埋入足够的观测点,那么当问题发生时,你将面对的是一个无法解释的"黑盒"。可观测性不是事后补的监控面板,而是贯穿架构始终的设计约束。
性能优化:并行执行与状态访问优化
多智能体系统进入生产环境后,性能瓶颈很少出现在单个 Agent 的推理速度上,而是集中暴露在编排层的调度效率 与状态层的访问模式中。当十几个 Agent 并发运行时,数据库连接池被频繁的小事务占满、事件队列因下游处理不及时而无限增长、共享状态上的锁竞争让本应并行的任务被迫串行化------这些问题的共同特征是在低负载下几乎不可见,一旦任务量越过临界点,系统吞吐量会断崖式下跌。
本章讨论的性能优化不涉及模型推理加速或提示词压缩,而是聚焦于架构层面的四个关键杠杆:并行执行 、状态缓存 、批量提交 与背压机制。它们分别对应多智能体系统中最常见的四类性能退化模式,且彼此之间存在耦合关系------例如,提高并行度会加剧状态锁竞争,而引入状态缓存又可能牺牲一致性。因此,本章在给出每个优化手段的同时,也会明确其适用边界与代价。
并行执行:从串行链到有向无环图
单 Agent 场景中,任务执行天然是串行的:用户输入、Agent 处理、工具调用、结果返回,每一步都依赖前一步的输出。进入多 Agent 协作后,情况发生了根本变化------一个任务分解出的多个子任务之间,往往只存在部分依赖。如果编排器仍然按顺序逐个调度 Agent,大量时间被浪费在等待与当前子任务无关的 Agent 完成上。
以代码评审系统为例:一个 PR 可能同时涉及安全审查、性能审查和风格审查。这三类审查由不同的 Agent 负责,彼此之间没有数据依赖,完全可以并行执行。假设每个审查 Agent 需要 30 秒完成,串行执行需要 90 秒,并行执行只需要 30 秒加极小的调度开销。当系统每天处理数百个 PR 时,这个差距直接决定了用户体验和资源利用率。
实现并行执行的关键在于将任务建模为 DAG(有向无环图),而非线性序列。编排器在收到任务后,首先进行依赖分析,识别哪些子任务可以同时启动,哪些必须等待前驱节点完成。以下是一个基于 Java 的简化实现思路:
java
public class DagScheduler {
private final Map<String, AgentTask> tasks = new HashMap<>();
private final Map<String, Set<String>> dependencies = new HashMap<>();
private final ExecutorService executor = Executors.newFixedThreadPool(8);
public void addTask(String id, AgentTask task, Set<String> dependsOn) {
tasks.put(id, task);
dependencies.put(id, dependsOn == null ? Set.of() : dependsOn);
}
public Map<String, TaskResult> execute() throws InterruptedException {
Map<String, TaskResult> results = new ConcurrentHashMap<>();
Map<String, Integer> pendingDeps = new HashMap<>();
Map<String, List<String>> dependents = new HashMap<>();
// 初始化依赖计数和反向依赖表
for (String id : tasks.keySet()) {
pendingDeps.put(id, dependencies.get(id).size());
for (String dep : dependencies.get(id)) {
dependents.computeIfAbsent(dep, k -> new ArrayList<>()).add(id);
}
}
CountDownLatch latch = new CountDownLatch(tasks.size());
for (String id : tasks.keySet()) {
if (pendingDeps.get(id) == 0) {
submitTask(id, results, pendingDeps, dependents, latch);
}
}
latch.await();
executor.shutdown();
return results;
}
private void submitTask(String id, Map<String, TaskResult> results,
Map<String, Integer> pendingDeps,
Map<String, List<String>> dependents,
CountDownLatch latch) {
executor.submit(() -> {
try {
TaskResult result = tasks.get(id).execute();
results.put(id, result);
// 依赖此任务的后继节点依赖计数减一
for (String dependent : dependents.getOrDefault(id, List.of())) {
int remaining = pendingDeps.merge(dependent, -1, Integer::sum);
if (remaining == 0) {
submitTask(dependent, results, pendingDeps, dependents, latch);
}
}
} finally {
latch.countDown();
}
});
}
}
这个调度器的核心逻辑是依赖驱动的任务释放:每个任务完成时,检查哪些后继任务的所有前驱都已就绪,就绪则立即提交执行。线程池大小需要根据下游资源(数据库连接数、外部 API 限流阈值)来设定,而非简单地等于 CPU 核数。
值得注意的是,并行度不是越高越好。当并行 Agent 数量超过状态存储的连接池容量或外部服务的速率限制时,任务会在连接获取阶段排队,反而增加整体延迟。实践中,并行度上限通常由最稀缺的资源决定,需要结合压测数据确定。
状态缓存:减少不必要的数据库往返
多智能体协作的一个典型特征是高频、小粒度的状态读写。Agent 在推理过程中可能需要反复查询共享上下文------例如,前一个 Agent 的产出、任务的当前阶段标记、某个工具调用的历史结果。如果每次查询都直接访问 Redis 或 Postgres,网络往返和序列化开销会迅速累积,成为系统吞吐量的隐形杀手。
以 Redis 为例,单次 GET 的本地延迟约为 0.1--0.5ms,看起来微不足道。但当一个任务涉及 50 次状态查询、系统并发处理 100 个任务时,每秒将产生 5000 次 Redis 请求。在高负载下,Redis 的单线程事件循环可能成为瓶颈,而客户端连接池的争用也会引入额外的等待时间。
解决方案是在 Agent 执行上下文内引入进程内状态缓存。编排器在任务启动时批量加载所需的共享状态到本地内存,Agent 在推理过程中优先读取缓存,只有在需要读取其他 Agent 的最新写入时才穿透到远程存储。以下是一个带有版本控制的缓存实现:
java
public class StateCache {
private final Map<String, CachedEntry> cache = new ConcurrentHashMap<>();
private final StateStore remoteStore;
public StateCache(StateStore remoteStore) {
this.remoteStore = remoteStore;
}
public String get(String key) {
CachedEntry entry = cache.get(key);
if (entry != null && !entry.isExpired()) {
return entry.value();
}
// 缓存未命中或已过期,从远程加载
String value = remoteStore.get(key);
if (value != null) {
cache.put(key, new CachedEntry(value, System.currentTimeMillis()));
}
return value;
}
public void put(String key, String value) {
remoteStore.put(key, value);
cache.put(key, new CachedEntry(value, System.currentTimeMillis()));
}
public void invalidate(String key) {
cache.remove(key);
}
private record CachedEntry(String value, long loadedAt) {
boolean isExpired() {
return System.currentTimeMillis() - loadedAt > 5_000; // 5秒TTL
}
}
}
缓存策略的选择需要在新鲜度 与性能 之间做权衡。对于多智能体系统,一个实用的原则是:单任务生命周期内不变的配置类状态可以长期缓存;跨 Agent 共享的中间结果使用短 TTL 或显式失效机制。例如,Agent A 写入的中间产物,Agent B 需要读取时,可以通过任务 ID 关联的版本号来判断缓存是否仍然有效。
更激进的方案是快照隔离:每个任务在启动时获取一份共享状态的不可变快照,整个任务生命周期内都基于这份快照进行推理,任务结束时的写入通过版本校验来检测冲突。这种方式将读操作完全本地化,代价是可能基于过时数据做出决策,需要业务层容忍一定程度的最终一致性。
批量提交:降低锁竞争与写入放大
多智能体系统的状态写入模式与典型 Web 应用有本质区别。一个 Agent 执行过程中可能产生数十次小写入------更新进度标记、记录中间结果、追加日志条目。如果每次写入都作为一个独立事务提交,不仅产生大量网络往返,还会在数据库层面造成严重的锁竞争。
考虑 Postgres 场景:Agent A 更新 task_status 表中的一行,Agent B 同时更新同一行(例如,多个 Agent 都在推进同一个父任务的进度)。每次 UPDATE 都会获取行级锁,事务提交后释放。如果两个 Agent 以 10ms 的间隔交替更新,锁等待时间可能超过实际工作时间。更糟糕的是,频繁的小事务会导致 WAL 写入放大,拖慢整个数据库实例。
批量提交 的核心思想是将 Agent 生命周期内的多次状态变更累积在本地缓冲区,在关键节点或任务结束时一次性提交。这显著减少了事务数量和锁持有时间,同时降低了网络开销。以下是一个批量写入器的示例:
java
public class BatchStateWriter {
private final Map<String, String> pendingWrites = new HashMap<>();
private final StateStore store;
private final int batchSize;
private final long flushIntervalMs;
public BatchStateWriter(StateStore store, int batchSize, long flushIntervalMs) {
this.store = store;
this.batchSize = batchSize;
this.flushIntervalMs = flushIntervalMs;
}
public void bufferWrite(String key, String value) {
pendingWrites.put(key, value);
if (pendingWrites.size() >= batchSize) {
flush();
}
}
public synchronized void flush() {
if (pendingWrites.isEmpty()) return;
Map<String, String> batch = new HashMap<>(pendingWrites);
pendingWrites.clear();
store.batchPut(batch); // 单次批量写入
}
public void startAutoFlush() {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(this::flush, flushIntervalMs, flushIntervalMs, TimeUnit.MILLISECONDS);
}
}
批量提交的关键设计决策是触发时机 。批量大小过大,任务失败时丢失的中间状态就越多;批量大小过小,优化效果不明显。实践中,通常结合两种触发条件:达到阈值(如 50 条变更)或超过时间间隔(如 200ms)。此外,任务结束(无论成功还是失败)时必须强制 flush,确保状态不丢失。
对于需要跨 Agent 实时可见的状态,批量提交会引入可见性延迟。解决方案是区分状态类型:进度类状态(如"步骤 3/7 完成")可以容忍秒级延迟,适合批量写入;而控制类状态(如"任务已取消")必须立即写入并通知相关 Agent。这种分类处理避免了批量优化对系统正确性的破坏。
背压机制:防止事件队列过载
事件驱动是多智能体系统常见的通信模式。Agent 之间通过事件总线交换消息:任务完成事件、状态变更事件、错误通知等。这种模式的优势是解耦------发送方不需要知道接收方是谁;但劣势也很明显:如果消费者处理速度长期低于生产者,事件队列将无限增长,最终导致内存耗尽或延迟不可控。
背压机制的本质是让生产速度适应消费速度。在多智能体系统中,这意味着当事件队列长度超过阈值时,编排器应该减缓新任务的提交速度,甚至暂停接受新任务,直到下游 Agent 消化掉积压。
以下是一个带有背压控制的事件队列实现:
java
public class BackpressureEventQueue {
private final BlockingQueue<AgentEvent> queue;
private final int highWatermark;
private final int lowWatermark;
private final AtomicBoolean accepting = new AtomicBoolean(true);
public BackpressureEventQueue(int capacity, int highWatermark, int lowWatermark) {
this.queue = new ArrayBlockingQueue<>(capacity);
this.highWatermark = highWatermark;
this.lowWatermark = lowWatermark;
}
public boolean offer(AgentEvent event) {
if (!accepting.get()) {
return false; // 拒绝新事件,由调用方决定重试或丢弃
}
boolean accepted = queue.offer(event);
if (queue.size() >= highWatermark) {
accepting.set(false); // 触发背压
}
return accepted;
}
public AgentEvent poll(long timeout, TimeUnit unit) throws InterruptedException {
AgentEvent event = queue.poll(timeout, unit);
if (queue.size() <= lowWatermark) {
accepting.set(true); // 解除背压
}
return event;
}
public int size() {
return queue.size();
}
}
这里采用了高低水位线策略:队列长度达到高水位时停止接收新事件,降到低水位时恢复接收。两个水位线之间的差值(滞后区间)防止系统在"接受---拒绝"之间频繁震荡。高水位的设定需要结合最大可容忍延迟:如果事件从产生到被消费的最大延迟不能超过 2 秒,而消费者平均处理速度为 100 事件/秒,那么高水位不应超过 200。
背压的传播路径同样重要。当 Agent 层的事件队列触发背压时,这个信号必须向上传递给编排器,进而影响任务调度策略。一种常见的做法是:编排器在提交新任务前检查下游事件队列的可用容量,如果容量不足则延迟调度或拒绝新任务。对于无法拒绝的外部请求(如用户提交的 PR),则需要引入优先级队列,高优先级任务优先进入有限的处理能力。
性能优化的整体视角
上述四个优化手段并非孤立存在,它们之间存在复杂的相互作用。提高并行度会增加状态访问频率,从而强化状态缓存和批量提交的必要性;批量提交引入的可见性延迟可能影响依赖状态实时性的 Agent,需要通过事件通知或版本校验来弥补;背压机制限制的正是并行执行的上限,防止系统在过载时雪崩。
在实践中,性能调优应该遵循测量---定位---优化---验证的循环。首先通过链路追踪和指标监控确定瓶颈在哪个环节:是数据库连接池等待、事件队列积压,还是 Agent 本身的推理耗时?然后针对性地应用本章讨论的优化手段,每次只改变一个变量,通过压测对比优化前后的吞吐量和 P99 延迟。多智能体系统的性能问题往往是多个因素叠加的结果,单点优化可能只是把瓶颈从一个环节转移到另一个环节,只有系统性地审视编排层与状态层的交互模式,才能获得稳定可持续的性能提升。
一个值得记住的经验法则是:在多智能体系统中,最快的状态访问是不访问,最快的事件传递是不传递。通过合理的任务切分减少 Agent 间的数据依赖,通过本地缓存消除冗余的远程读写,通过批量提交压缩事务频率,这三者共同构成了性能优化的第一性原理。背压机制则是最后的保险丝,确保系统在极端负载下依然保持可控行为,而非崩溃或不可预测地降级。
总结与展望
回顾本文从单 Agent 到多 Agent 协作的演进路径,可以清晰地看到一条主线:多智能体系统的架构复杂度并不在于 Agent 本身的能力,而在于如何让多个 Agent 在共享环境中可靠地协作。编排层、状态层与故障恢复层构成了这一复杂度的三个核心支点,它们各自的边界清晰程度,直接决定了系统能否从"演示级"走向"生产级"。
编排层的设计要点回顾
编排层是多智能体系统的"指挥中枢",其核心职责是将复杂任务分解为可执行的子任务,并在正确的时机将正确的子任务分配给正确的 Agent。本文讨论的三种编排模式------DAG 工作流、动态规划器与事件驱动通信------并非互斥选项,而是适用于不同任务特征的互补方案。
DAG 工作流在任务拓扑结构可预先确定时表现最佳。它的优势在于可预测性:执行路径在运行时之前即可审计,失败影响范围可以通过图结构静态分析。但 DAG 的刚性也构成了它的上限------当任务需要根据中间结果动态调整后续步骤时,静态图要么需要预定义所有可能分支(导致图规模爆炸),要么被迫引入"万能节点"来掩盖灵活性不足的问题。
动态规划器则走向了另一个极端:它赋予系统最大的灵活性,让 LLM 在每一步根据当前状态决定下一步行动。这种模式的代价是可观测性下降------执行路径不再可预测,审计变得困难,且规划器自身的推理错误会直接转化为错误的编排决策。本文提出的折中方案------"规划器生成 DAG,执行器按图执行"------在实践中被证明是平衡灵活性与可控性的有效策略:规划器在任务开始时一次性生成完整执行计划,执行阶段严格按图推进,只有在检测到计划失效时才触发重新规划。
事件驱动 Agent 通信则适用于长周期、异步协作场景。它的核心价值在于解耦 :Agent 之间不直接调用,而是通过事件总线交换消息。这种解耦带来了水平扩展能力和故障隔离的自然优势,但同时也引入了事件顺序性、重复投递和最终一致性等分布式系统的经典难题。本文强调的关键设计决策是:事件驱动不应替代编排,而应作为编排的传输层。让事件通道负责"传递什么",让编排逻辑负责"何时传递、传递给谁",这一职责切分避免了事件驱动系统常见的"逻辑散落在每个消费者中"的退化模式。
状态层的核心原则
如果说编排层决定了多智能体系统"做什么",状态层则决定了系统"知道什么"。本文反复强调的一个核心观点是:共享状态不是数据库的简单封装,而是一个需要精心设计访问语义的架构组件。
状态快照与增量更新的选择,本质上是在读效率 与写效率 之间的权衡。对于 Agent 数量在个位数、任务步骤在几十步以内的系统,全量快照的简单性带来的收益远超其性能损耗。但当 Agent 数量增长到两位数、任务步骤达到数百步时,增量更新配合版本号成为必要选择。本文给出的实践建议是:从全量快照开始,在性能数据驱动下渐进式引入增量更新,而不是一开始就构建复杂的增量同步机制。
乐观并发控制(OCC)在多智能体场景中的适用性值得特别强调。与传统的悲观锁相比,OCC 避免了长时间持有锁导致的 Agent 等待,这与 Agent 执行时间不可预测的特征高度契合。但 OCC 的有效性依赖于一个前提:并发冲突率必须保持在较低水平。这意味着状态模型的设计应当尽量减少多个 Agent 同时修改同一状态片段的概率------例如,将任务状态按子任务 ID 进行分区,使每个 Agent 主要写入自己负责的分区,只在协调点进行跨分区读取。
故障恢复层的实践检验
故障恢复层的设计目标可以用一句话概括:让系统在部分失败时继续前进,而不是整体回退。本文讨论的 Agent 级超时、幂等执行与部分失败回滚,共同构成了实现这一目标的三层防线。
Agent 级超时是第一道防线,它防止单个 Agent 的异常行为拖垮整个任务。但超时本身只是一个信号,超时之后的处理策略才是设计的关键。本文推荐的"超时-标记-补偿 "模式------超时后将任务标记为 TIMEOUT 状态,触发补偿逻辑,同时保留 Agent 的中间输出以供诊断------在实践中被证明比简单的"超时即失败"更具鲁棒性。
幂等执行是第二道防线,它使得重试机制在分布式环境中变得安全。多智能体系统中的幂等设计有一个特殊挑战:LLM 调用天然不具备确定性 。同一个 Agent 在相同输入下可能产生不同的输出,这使得传统的幂等定义(相同输入产生相同输出)难以直接适用。本文提出的解决方案是将幂等性要求从 Agent 输出层面转移到状态变更层面------即 Agent 的执行结果写入共享状态时,写入操作本身必须是幂等的(通过版本号或唯一键实现),而 Agent 输出的非确定性通过状态层的冲突检测来管理。
部分失败回滚是第三道防线,也是实现成本最高的一层。本文强调的一个关键洞察是:回滚的目标不是恢复"之前的状态",而是达到"一致的状态" 。在多 Agent 协作中,完全回滚到任务开始前的状态往往既不现实也不必要。更实际的做法是定义补偿边界------即哪些状态变更可以被安全地撤销,哪些需要向前修复。这一边界应当在任务设计阶段就明确,而不是在失败发生后临时决定。
展望:多智能体系统的三个演进方向
站在当前技术发展的节点上,多智能体系统的架构演进可以识别出三个清晰的方向,它们分别对应编排层、状态层与故障恢复层的下一次能力跃迁。
自适应编排:从静态计划到运行时学习。 当前的动态规划器虽然能够在任务执行过程中调整计划,但这种调整本质上仍基于预定义的规则或 LLM 的即时推理。未来的自适应编排将引入执行反馈的学习闭环 :系统不仅记录"任务是否成功",还记录"哪类编排决策在哪些条件下导致了成功或失败"。这些历史数据可以被用于在运行时微调规划策略------例如,当系统反复观察到某个 Agent 在特定类型的子任务上频繁超时时,规划器可以自动调整该 Agent 的调用方式(如增加超时时间、更换模型、或分解为更小的子任务)。这种自适应能力的关键技术挑战在于:如何在引入学习机制的同时保持编排决策的可解释性。一个可能的折中方案是让学习系统输出"建议的编排参数调整",而非直接修改编排逻辑,从而在自动化和可控性之间保持平衡。
跨 Agent 记忆共享:从独立上下文到集体经验。 当前多智能体系统中的"记忆"大多是 Agent 私有的------每个 Agent 维护自己的上下文窗口,通过共享状态读写来交换信息。这种模式的问题在于:一个 Agent 在执行中学到的经验无法被其他 Agent 直接复用 。跨 Agent 记忆共享的目标是构建一个集体经验层 ,使 Agent 能够查询"在类似任务中,其他 Agent 是如何处理的"。技术实现上,这需要解决两个核心问题:记忆的表示格式 (如何将 Agent 的执行经验编码为可检索的结构化知识)和记忆的检索相关性 (如何在一个 Agent 面对当前任务时,从集体记忆中检索出真正有用的历史经验)。向量数据库与语义检索是当前的候选方案,但更关键的挑战在于记忆的质量过滤------不是所有执行记录都值得被记住,劣质经验的传播可能比没有记忆更有害。
自愈能力:从人工介入到自动恢复。 当前的多智能体系统在遇到异常时,大多采取"记录-告警-等待人工处理"的模式。自愈能力的演进方向是让系统能够自主识别异常模式并执行恢复动作 。这包括:自动检测 Agent 的持续性能退化(如连续多次超时或输出质量下降)并触发模型切换或降级策略;识别状态层的数据不一致并自动触发修复流程;在检测到任务陷入循环或死锁时主动打破僵局。自愈能力的设计需要遵循一个重要的安全原则:自愈动作的影响范围应当受限且可撤销。一个实用的渐进路径是:先实现"建议式自愈"(系统检测异常并给出恢复建议,由人工确认后执行),再逐步过渡到"受控自愈"(系统在预定义的场景和边界内自动执行恢复),最终才考虑"完全自愈"。
结语
多智能体系统的架构设计,本质上是在能力边界 与可靠性边界之间寻找平衡。单 Agent 的简单性在多任务、多工具、多上下文的复杂场景下必然被打破,但多 Agent 协作引入的编排复杂度、状态一致性挑战与故障恢复难题,如果没有经过深思熟虑的架构设计来应对,反而会让系统变得更加脆弱。
本文提出的"编排器---执行器---共享状态"三层职责切分模型,以及围绕这一模型展开的编排模式选择、状态存储设计、并发控制策略与故障恢复机制,构成了一个可落地的实践框架。这个框架的核心价值不在于提供"唯一正确"的答案,而在于帮助架构师在面对具体场景时,知道在哪些维度上需要做出权衡,以及每种权衡的代价是什么。
多智能体系统仍处于快速演进的阶段。今天被广泛接受的设计模式,可能在半年后就会被更优的方案取代。但那些来自分布式系统领域的底层原则------职责分离、故障隔离、状态一致性、可观测性------不会因为 Agent 的智能化而失效。恰恰相反,Agent 的自主性和不可预测性使得这些原则变得更加重要。智能体越"智能",架构的约束就越需要"可靠"。这是多智能体系统走向生产环境的根本前提,也是每一位在这一领域探索的工程师需要持续思考的命题。
开源项目
以下开源项目覆盖了多智能体编排、工作流引擎与状态存储三个层面。选择它们的原因在于:代码可读、设计文档完善,且在生产环境中有实际验证。
多智能体编排框架
LangGraph(https://github.com/langchain-ai/langgraph)
LangGraph 将多 Agent 协作建模为有向图,节点是 Agent 或工具调用,边是条件转移。它最值得学习的设计是 checkpoint 机制 :每个节点执行后自动持久化状态,这使得从任意节点恢复执行成为可能。本文第 3 章讨论的状态快照与第 4 章讨论的 Agent 级超时,在 LangGraph 中都有对应的实现。建议重点阅读其 Checkpointer 接口与 interrupt 功能的源码。
AutoGen(https://github.com/microsoft/autogen)
AutoGen 的核心抽象是 ConversableAgent,所有 Agent 通过消息传递进行协作。其 GroupChatManager 实现了动态规划器的角色------根据对话上下文决定下一个发言的 Agent。本文第 2 章对比 DAG 与动态规划器时,AutoGen 的动态调度策略是重要参考。建议关注其 max_round 与终止条件的处理,这直接关系到多 Agent 对话的收敛性。
CrewAI(https://github.com/crewAIInc/crewAI)
CrewAI 以"角色---任务---流程"三层模型简化了多 Agent 系统的构建。其 Crew 类封装了顺序执行与层级执行两种模式。对于希望快速验证多 Agent 协作模式的团队,CrewAI 是上手成本最低的选择之一。但其状态管理能力相对有限,生产化时需要结合外部存储。
工作流引擎
Temporal(https://github.com/temporalio/temporal)
Temporal 是一个分布式工作流引擎,其核心价值在于将业务逻辑与持久化、重试、超时、补偿等横切关注点解耦 。工作流代码以近乎普通函数的方式编写,但每一步执行都被持久化,失败后可从中断点恢复。本文第 4 章讨论的幂等执行、部分失败回滚与 Saga 模式,在 Temporal 中都有成熟的 API 支持。多 Agent 系统的编排层如果需要一个可靠的"骨架",Temporal 是目前最值得评估的方案之一。建议阅读其 Activity 的重试策略与 Saga 示例。
Prefect(https://github.com/PrefectHQ/prefect)
Prefect 是面向数据工程与 ML 工作流的编排引擎,其 Task 与 Flow 抽象支持声明式依赖、缓存与重试。相比 Temporal,Prefect 的部署更轻量,适合中小规模的多 Agent 任务流。其 State 对象的设计------包含 Pending、Running、Success、Failed 等状态及状态转换------与本文第 3 章讨论的任务状态建模高度一致。
分布式状态与消息基础设施
Redis(https://github.com/redis/redis)
Redis 在多 Agent 系统中承担两种角色:共享状态缓存 与分布式锁提供者 。其 SETNX 与 Redlock 算法是乐观并发控制的常用实现手段。本文第 3 章讨论的基于 Redis 的状态快照与锁方案,建议结合 Redis 官方文档中的 Distributed Locks 章节阅读。需要注意 Redlock 在极端网络分区下的争议,生产环境应结合具体场景评估。
NATS JetStream(https://github.com/nats-io/nats-server)
NATS JetStream 提供了持久化消息流与消费者组,适合作为多 Agent 之间事件驱动通信的基础设施。与 Kafka 相比,NATS 的部署复杂度更低,且原生支持请求-响应模式,适合 Agent 之间的同步调用。本文第 2 章讨论的事件驱动 Agent 通信,可以基于 JetStream 的 WorkQueue 模式实现。