1. 引言
在构建复杂多智能体系统时,静态定义所有Agent往往会陷入"预先枚举所有可能对话路径"的陷阱。面对开放域、长周期、跨领域的任务,我们更需要一种动态派生架构:主Agent根据任务上下文实时派生专用的Sub-Agent,并在任务完成后将其回收或重用。这种架构让系统具备按需扩展、自组织和自适应能力,是大模型应用迈向"自主规划"的关键一步。
本文将深入解剖主Agent动态派生Sub-Agent的完整链路,包括何时触发派生、如何定义Sub-Agent的角色与工具、管理生命周期、传递上下文与结果回收,并结合LangGraph/React风格给出可落地的设计范式与代码骨架。
2. 动态派生架构的设计动机
在讲具体实现之前,先回答"为什么需要动态派生":
- 任务边界不可预知:用户可能突然切换需求,从"写SQL"切换到"解释财务指标",甚至要求"让两个专家辩论方案",静态Agent树难以覆盖。
- 减少上下文窗口压力:将长文档流水线拆分为多个短暂的Sub-Agent,每个只携带所需的最小上下文,降低遗忘风险。
- 专业化与隔离:不同子任务需要不同的工具集(如SQL执行、绘图、法规检索),动态派生允许按需注入精准工具,避免全局工具混乱。
- 并行与容错:主Agent可并行启动多个Sub-Agent分别探索不同假设,并由主Agent仲裁结果,提升吞吐与鲁棒性。
3. 核心架构总览
我们设计一个三层心智模型:主Agent作为"项目经理",Sub-Agent作为"专家雇员",通过消息总线与共享状态协调。关键组件如下:
#mermaid-svg-jp9JEiNZZiNk3d4X{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-jp9JEiNZZiNk3d4X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jp9JEiNZZiNk3d4X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jp9JEiNZZiNk3d4X .error-icon{fill:#552222;}#mermaid-svg-jp9JEiNZZiNk3d4X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jp9JEiNZZiNk3d4X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jp9JEiNZZiNk3d4X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jp9JEiNZZiNk3d4X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jp9JEiNZZiNk3d4X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jp9JEiNZZiNk3d4X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jp9JEiNZZiNk3d4X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jp9JEiNZZiNk3d4X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jp9JEiNZZiNk3d4X .marker.cross{stroke:#333333;}#mermaid-svg-jp9JEiNZZiNk3d4X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jp9JEiNZZiNk3d4X p{margin:0;}#mermaid-svg-jp9JEiNZZiNk3d4X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jp9JEiNZZiNk3d4X .cluster-label text{fill:#333;}#mermaid-svg-jp9JEiNZZiNk3d4X .cluster-label span{color:#333;}#mermaid-svg-jp9JEiNZZiNk3d4X .cluster-label span p{background-color:transparent;}#mermaid-svg-jp9JEiNZZiNk3d4X .label text,#mermaid-svg-jp9JEiNZZiNk3d4X span{fill:#333;color:#333;}#mermaid-svg-jp9JEiNZZiNk3d4X .node rect,#mermaid-svg-jp9JEiNZZiNk3d4X .node circle,#mermaid-svg-jp9JEiNZZiNk3d4X .node ellipse,#mermaid-svg-jp9JEiNZZiNk3d4X .node polygon,#mermaid-svg-jp9JEiNZZiNk3d4X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jp9JEiNZZiNk3d4X .rough-node .label text,#mermaid-svg-jp9JEiNZZiNk3d4X .node .label text,#mermaid-svg-jp9JEiNZZiNk3d4X .image-shape .label,#mermaid-svg-jp9JEiNZZiNk3d4X .icon-shape .label{text-anchor:middle;}#mermaid-svg-jp9JEiNZZiNk3d4X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jp9JEiNZZiNk3d4X .rough-node .label,#mermaid-svg-jp9JEiNZZiNk3d4X .node .label,#mermaid-svg-jp9JEiNZZiNk3d4X .image-shape .label,#mermaid-svg-jp9JEiNZZiNk3d4X .icon-shape .label{text-align:center;}#mermaid-svg-jp9JEiNZZiNk3d4X .node.clickable{cursor:pointer;}#mermaid-svg-jp9JEiNZZiNk3d4X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jp9JEiNZZiNk3d4X .arrowheadPath{fill:#333333;}#mermaid-svg-jp9JEiNZZiNk3d4X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jp9JEiNZZiNk3d4X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jp9JEiNZZiNk3d4X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jp9JEiNZZiNk3d4X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jp9JEiNZZiNk3d4X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jp9JEiNZZiNk3d4X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jp9JEiNZZiNk3d4X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jp9JEiNZZiNk3d4X .cluster text{fill:#333;}#mermaid-svg-jp9JEiNZZiNk3d4X .cluster span{color:#333;}#mermaid-svg-jp9JEiNZZiNk3d4X 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-jp9JEiNZZiNk3d4X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jp9JEiNZZiNk3d4X rect.text{fill:none;stroke-width:0;}#mermaid-svg-jp9JEiNZZiNk3d4X .icon-shape,#mermaid-svg-jp9JEiNZZiNk3d4X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jp9JEiNZZiNk3d4X .icon-shape p,#mermaid-svg-jp9JEiNZZiNk3d4X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jp9JEiNZZiNk3d4X .icon-shape .label rect,#mermaid-svg-jp9JEiNZZiNk3d4X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jp9JEiNZZiNk3d4X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jp9JEiNZZiNk3d4X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jp9JEiNZZiNk3d4X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户请求
主Agent (Dispatcher)
任务分解与规划
动态创建 SubAgent A
携带角色描述 + 工具定义
动态创建 SubAgent B
携带角色描述 + 工具定义
...
并行/串行执行
主Agent汇总结果
最终响应
主Agent本身是一个带记忆的决策体,拥有"规划-执行-反思"循环。它的核心能力是解析任务 → 生成Sub-Agent描述 → 委托执行 → 合成答案。Sub-Agent则是一次性构造的轻量对话实体,带有独立系统提示、工具列表和状态快照。
4. 动态派生的完整流程
4.1 触发时机与决策点
主Agent通常通过Tool Calling 触发派生。例如,我们为主Agent定义一个特殊工具 dispatch_agent:
工具名称:dispatch_agent
描述:创建并运行一个子Agent来完成指定子任务。返回子Agent的执行结果。
参数:
- role: str,子Agent扮演的角色描述
- task: str,需要完成的子任务详细描述
- tools: List[str],需要分配给子Agent的工具名称列表
- context: str(可选),传递给子Agent的上下文摘要
当主Agent判断"当前任务需要其他专家介入"时,会调用该工具,从而进入派生流程。
4.1.1 核心运转逻辑:主Agent 的循环 + 状态机本质
抛开 Sub-Agent 的创建细节,回到主Agent 本身:主Agent 本质上是一个"推理-决策-执行"的循环体,内部通过有限状态机驱动。 理解这一点是掌握整个动态派生架构的关键。
核心循环(ReAct / Plan-Act-Observe)
每一轮循环中,主Agent 执行三个固定步骤:
- 感知(Observe) --- 读取当前状态:用户原始请求、历史对话、已回收的 Sub-Agent 结果、工具调用返回值。
- 推理(Think) --- 判断当前处于哪个阶段:是否需要继续拆分任务?某个 Sub-Agent 的结果是否足够?是否需要用工具获取更多信息?是否可以直接合成答案?
- 行动(Act) --- 执行一种动作:
- 调用
dispatch_agent→ 派生一个或多个 Sub-Agent; - 调用普通工具(搜索、计算、查库)→ 自己解决问题;
- 调用
final_response→ 合成最终答案,结束循环。
- 调用
状态机视角:五个核心状态
用状态机来建模,主Agent 在一个请求的生命周期内会在以下状态之间转移:
#mermaid-svg-oSH08NmjPDEreZF1{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-oSH08NmjPDEreZF1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oSH08NmjPDEreZF1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oSH08NmjPDEreZF1 .error-icon{fill:#552222;}#mermaid-svg-oSH08NmjPDEreZF1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oSH08NmjPDEreZF1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oSH08NmjPDEreZF1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oSH08NmjPDEreZF1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oSH08NmjPDEreZF1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oSH08NmjPDEreZF1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oSH08NmjPDEreZF1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oSH08NmjPDEreZF1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oSH08NmjPDEreZF1 .marker.cross{stroke:#333333;}#mermaid-svg-oSH08NmjPDEreZF1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oSH08NmjPDEreZF1 p{margin:0;}#mermaid-svg-oSH08NmjPDEreZF1 defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-oSH08NmjPDEreZF1 g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-oSH08NmjPDEreZF1 g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-oSH08NmjPDEreZF1 g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-oSH08NmjPDEreZF1 g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-oSH08NmjPDEreZF1 g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-oSH08NmjPDEreZF1 .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-oSH08NmjPDEreZF1 .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-oSH08NmjPDEreZF1 .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-oSH08NmjPDEreZF1 .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-oSH08NmjPDEreZF1 .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-oSH08NmjPDEreZF1 .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-oSH08NmjPDEreZF1 .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-oSH08NmjPDEreZF1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oSH08NmjPDEreZF1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oSH08NmjPDEreZF1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oSH08NmjPDEreZF1 .edgeLabel .label text{fill:#333;}#mermaid-svg-oSH08NmjPDEreZF1 .label div .edgeLabel{color:#333;}#mermaid-svg-oSH08NmjPDEreZF1 .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-oSH08NmjPDEreZF1 .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-oSH08NmjPDEreZF1 .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-oSH08NmjPDEreZF1 .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-oSH08NmjPDEreZF1 .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-oSH08NmjPDEreZF1 .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oSH08NmjPDEreZF1 .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oSH08NmjPDEreZF1 #statediagram-barbEnd{fill:#333333;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oSH08NmjPDEreZF1 .cluster-label,#mermaid-svg-oSH08NmjPDEreZF1 .nodeLabel{color:#131300;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-oSH08NmjPDEreZF1 .note-edge{stroke-dasharray:5;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-note text{fill:black;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram-note .nodeLabel{color:black;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagram .edgeLabel{color:red;}#mermaid-svg-oSH08NmjPDEreZF1 #dependencyStart,#mermaid-svg-oSH08NmjPDEreZF1 #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-oSH08NmjPDEreZF1 .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oSH08NmjPDEreZF1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 接收用户请求
任务需要拆分/委托
任务简单,自己处理
Sub-Agent 已发出
全部 Sub-Agent 完成/超时
收回部分结果后仍需进一步拆解
工具调用返回, 需继续推理
信息足够,开始合成
输出最终答案
PLANNING
DISPATCHING
EXECUTING
WAITING
AGGREGATING
| 状态 | 说明 |
|---|---|
PLANNING |
分析任务,决定下一步:自己执行、派生 Sub-Agent、还是直接汇总 |
DISPATCHING |
构造 dispatch_agent 调用,生成 Sub-Agent 描述与工具列表 |
WAITING |
阻断/异步等待 Sub-Agent 返回结果(可并行等待多个) |
EXECUTING |
主Agent 自己调用工具并观察结果 |
AGGREGATING |
汇总所有结果,合成最终回答 |
状态机与动态派生的关系
动态派生正是在状态机中 PLANNING → DISPATCHING → WAITING → AGGREGATING 这条路径上的核心动作。主Agent 通过循环多次进出这些状态,就能实现:
- 递归派生 :从
WAITING回到PLANNING,发现 Sub-Agent 的结果仍然不够,继续派生新的 Sub-Agent; - 动态调整 :在
AGGREGATING时如果信息仍不充分,可以回到PLANNING补充查询; - 自愈与降级 :某个 Sub-Agent 超时或失败,状态机可以走
WAITING → PLANNING路径重新决策(重试、换模型、转人工)。
为什么强调"循环 + 状态机"?
很多工程师初看动态派生时,容易被"创建 Sub-Agent"这个动作吸引注意力,而忽略了主Agent 自身的持续决策过程。实际上:
- 循环 保证了主Agent 不会在一次工具调用后就草率结束,而是持续追问"信息够不够?还需要什么?";
- 状态机 保证了决策逻辑的确定性,避免 LLM 在每个阶段随意发散。配合 Tool Calling 的函数签名约束,状态转移是可控、可观测、可审计的。
在工程实现上,LangGraph 天然支持这种"循环 + 状态机"模式------每个节点即一个状态处理函数,状态图定义的边即转移路径,而 Agent 的推理循环正是在这些节点中反复执行直到到达终止节点。
一句话总结:主Agent 不干杂活
如果把多智能体系统比作一家公司,主Agent 就是那个从来不写代码、不做报表的项目经理 。它的全部工作可以浓缩成一句话:"看谁拉错了,看谁还没开始。"
更精确地说,主Agent 在状态机中只轮转两件事:
-
盯状态 ------遍历
sub_tasks列表中每个子任务的status:pending(还没开始)→ 催促分派或自己补位failed/timeout(拉错了/超时了)→ 触发重试/降级/转人工done(正常完成)→ 收入completed字典,等着汇总
-
拍板决策------根据整体盘面做唯一决策:
- 全部完成 → 走
AGGREGATING,合成答案交差 - 部分失败但可接受 → 照样进汇总,标记风险
- 关键子任务全挂 → 回到
PLANNING重新拆解或直接降级回复
- 全部完成 → 走
而真正"干活"的动作------查数据库、调 API、写 SQL、画图表------全部扔给 Sub-Agent 完成。主Agent 连一行工具调用的代码都不用写,它的"工具"只有一个 dispatch_agent,而这个工具的本质就是把活外包出去,然后等着收结果。
这种设计的好处是显而易见的:主Agent 的 Prompt 极短(不用塞一堆工具描述)、决策链路极清晰(只有调度和汇总两条路径),天然规避了"全局工具泛滥导致 LLM 幻觉"的问题。用状态机的语言说:主Agent 的 EXECUTING 状态几乎是摆设,它真正的执行永远是 DISPATCHING → WAITING。
面试金句:"主Agent 就像一个只盯 Kanban 的 PM------自己不动手,但知道谁卡住了、谁掉链子了、什么时候该拍板收尾。这种'纯调度化'的设计,恰恰是多智能体系统能稳定扩展的核心原因。"
在下面 4.1.2 节中,我们会看到这个理念如何落地为具体的 LangGraph 状态机代码------每一个节点都在做"盯 + 决策",而把真实的工具执行留给 Sub-Agent。
4.1.2 搭建全局状态机:基于 LangGraph 的工程实现
上一节从概念层面描述了主Agent 的五个核心状态。本节将其落地为可运行的代码 ,用 LangGraph 的 StateGraph 搭建一个完整的全局状态机。
为什么用 LangGraph 搭建状态机?
LangGraph 天然适合这个场景,原因有三:
- 状态图即状态机:节点 = 状态处理函数,边 = 转移路径,条件边 = 转移判断,与状态机模型一一对应。
- 内置循环支持 :图可以包含环(
PLANNING → EXECUTING → PLANNING),天然支持 Agent 的持续推理循环。 - 可观测与可中断 :支持
checkpointer(状态快照)、interrupt(人工介入点),满足生产环境的调试与审批需求。
Step 1:定义全局状态结构
首先用 TypedDict 定义主Agent 在整个生命周期中需要维护的状态字段:
python
from typing import TypedDict, List, Optional, Annotated
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
from dataclasses import dataclass, field
from enum import Enum
# --- 状态枚举 ---
class AgentState(str, Enum):
PLANNING = "planning"
DISPATCHING = "dispatching"
WAITING = "waiting"
EXECUTING = "executing"
AGGREGATING = "aggregating"
# --- 子任务的数据结构 ---
@dataclass
class SubTask:
task_id: str
role: str
task: str
tools: List[str]
context: str = ""
status: str = "pending" # pending / running / done / timeout / failed
result: str = ""
# --- 全局 Agent 状态 ---
class MasterAgentState(TypedDict):
# 消息历史(自动合并新消息)
messages: Annotated[List[BaseMessage], add_messages]
# 当前状态机阶段
agent_state: str
# 任务规划阶段产出的子任务列表
sub_tasks: List[dict]
# 已回收的 Sub-Agent 结果(task_id -> result)
completed: dict
# 汇总后的最终答案
final_answer: str
# 迭代计数器,防止无限循环
iteration_count: int
max_iterations: int
关键设计决策:
agent_state是状态机的核心驱动字段,LangGraph 的条件边根据它决定下一步走哪个节点。sub_tasks存储主Agent 在PLANNING阶段分解出的所有子任务,每个子任务在DISPATCHING阶段被逐一或并行发出。iteration_count是安全阀:防止 LLM 陷入无限循环(例如反复回到PLANNING却不推进),达到max_iterations时强制走降级路径。
Step 2:实现各状态节点
以下是五个核心节点的实现骨架。每个节点接收 MasterAgentState,处理后返回状态更新:
python
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_core.language_models import BaseChatModel
# --- 节点的共享依赖(通过闭包或类注入) ---
# llm: BaseChatModel # 大模型实例
# dispatch_tool: callable # dispatch_agent 工具的实现函数
# max_iterations: int = 10 # 最大迭代次数
def planning_node(state: MasterAgentState) -> dict:
"""
PLANNING 节点:分析当前消息,决定下一步动作。
产出可能是:直接回答、调用工具、或拆分出一组 sub_tasks。
"""
# 构造规划专用的 System Prompt
planning_prompt = """你是一个任务规划器。根据用户请求和当前进度,决定下一步:
1. 如果任务简单、不需要拆分,设置 action="execute",并给出需要调用的工具名。
2. 如果需要多个专家协同,设置 action="dispatch",并列出每个子任务的 role / task / tools。
3. 如果信息已经足够,设置 action="finalize",直接给出最终答案。
请用 JSON 格式输出你的决策:
{
"action": "execute" | "dispatch" | "finalize",
"reasoning": "你的判断理由",
"sub_tasks": [ // 仅在 action=dispatch 时填写
{"role": "...", "task": "...", "tools": ["..."]}
],
"direct_answer": "..." // 仅在 action=finalize 时填写
}"""
messages = [SystemMessage(content=planning_prompt)] + state["messages"]
response = llm.invoke(messages)
# 解析 LLM 返回的 JSON 决策
decision = parse_json(response.content)
# 关键:PLANNING 不直接执行动作,只更新 sub_tasks 和状态标记
updates = {
"agent_state": AgentState.DISPATCHING.value
if decision["action"] == "dispatch"
else AgentState.EXECUTING.value
if decision["action"] == "execute"
else AgentState.AGGREGATING.value,
"iteration_count": state["iteration_count"] + 1,
"messages": [response],
}
if decision["action"] == "dispatch":
# 为每个子任务生成唯一 task_id
import uuid
updates["sub_tasks"] = [
{**st, "task_id": str(uuid.uuid4())[:8]}
for st in decision["sub_tasks"]
]
if decision["action"] == "finalize":
updates["final_answer"] = decision["direct_answer"]
return updates
def dispatching_node(state: MasterAgentState) -> dict:
"""
DISPATCHING 节点:遍历 sub_tasks,逐一或并行调用 dispatch_agent。
注意:实际生产中这里应用异步并发,简化示例用同步循环。
"""
results = {}
for sub_task in state["sub_tasks"]:
if sub_task["status"] != "pending":
continue
try:
result = dispatch_agent(
role=sub_task["role"],
task=sub_task["task"],
tools=sub_task["tools"],
context=sub_task.get("context", ""),
)
sub_task["status"] = "done"
sub_task["result"] = result
results[sub_task["task_id"]] = result
except TimeoutError:
sub_task["status"] = "timeout"
results[sub_task["task_id"]] = "[超时] Sub-Agent 未在规定时间内返回"
except Exception as e:
sub_task["status"] = "failed"
results[sub_task["task_id"]] = f"[失败] {str(e)}"
return {
"agent_state": AgentState.WAITING.value,
"sub_tasks": state["sub_tasks"],
"completed": {**state.get("completed", {}), **results},
}
def waiting_node(state: MasterAgentState) -> dict:
"""
WAITING 节点:检查所有 sub_tasks 的状态,决定下一步。
- 全部完成 → 进入 AGGREGATING
- 部分失败但可接受 → 进入 AGGREGATING
- 需要重试或补充 → 回到 PLANNING
"""
all_done = all(
st["status"] in ("done", "timeout", "failed")
for st in state["sub_tasks"]
)
if not all_done:
# 通常不会发生(DISPATCHING 已同步等待),但异步模型下这是关键检查点
return {"agent_state": AgentState.WAITING.value}
# 判断是否有致命失败需要重试
critical_failures = [
st for st in state["sub_tasks"]
if st["status"] == "failed"
]
if critical_failures and state["iteration_count"] < state["max_iterations"]:
# 将失败信息注入消息,回到 PLANNING 让主Agent 重新决策
fail_summary = "以下子任务执行失败:\n" + "\n".join(
f"- [{st['role']}] {st['task']}: {st['result']}"
for st in critical_failures
)
return {
"agent_state": AgentState.PLANNING.value,
"messages": [HumanMessage(content=f"{fail_summary}\n请决定:重试、降级、还是忽略?")],
}
return {"agent_state": AgentState.AGGREGATING.value}
def executing_node(state: MasterAgentState) -> dict:
"""
EXECUTING 节点:主Agent 自己调用工具(非派生场景)。
简化起见,此处委托给 LLM 的 Tool Calling 机制。
"""
# 在实际实现中,这里会进入 ReAct 循环
# 简化:让 LLM 判断是否需要继续
response = llm.invoke(state["messages"])
return {
"messages": [response],
"agent_state": AgentState.AGGREGATING.value,
}
def aggregating_node(state: MasterAgentState) -> dict:
"""
AGGREGATING 节点:汇总所有 Sub-Agent 结果,合成最终答案。
"""
results_text = "\n\n".join(
f"### [{st['role']}] 结果\n{st['result']}"
for st in state["sub_tasks"]
)
aggregation_prompt = f"""请基于以下专家分析结果,为用户生成一份完整、结构化的最终回答。
专家分析结果:
{results_text}
用户原始问题:
{state['messages'][0].content}
要求:整合所有专家的发现,指出共识与分歧,给出可执行的建议。"""
final_response = llm.invoke([
SystemMessage(content="你是一名首席分析师,负责整合多专家的结论。"),
HumanMessage(content=aggregation_prompt),
])
return {
"agent_state": "completed",
"final_answer": final_response.content,
"messages": [final_response],
}
Step 3:组装状态图------定义节点与转移边
有了节点实现后,用 LangGraph 的 StateGraph 将它们串联起来:
python
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
def build_master_agent_graph() -> StateGraph:
"""构建主Agent 的全局状态机图。"""
workflow = StateGraph(MasterAgentState)
# 注册五个核心节点
workflow.add_node("planning", planning_node)
workflow.add_node("dispatching", dispatching_node)
workflow.add_node("waiting", waiting_node)
workflow.add_node("executing", executing_node)
workflow.add_node("aggregating", aggregating_node)
# 设置入口节点
workflow.set_entry_point("planning")
# --- 定义条件边(状态转移的核心) ---
# 每次节点执行完后,根据 agent_state 字段决定下一步
def route_from_planning(state: MasterAgentState) -> str:
"""PLANNING 之后的路由逻辑。"""
if state["iteration_count"] >= state["max_iterations"]:
# 强制终止:超过最大迭代次数,直接汇总
return "aggregating"
state_val = state["agent_state"]
if state_val == AgentState.DISPATCHING.value:
return "dispatching"
elif state_val == AgentState.EXECUTING.value:
return "executing"
elif state_val == AgentState.AGGREGATING.value:
return "aggregating"
return END
def route_from_dispatching(state: MasterAgentState) -> str:
return "waiting" # 发出子任务后必然进入等待
def route_from_waiting(state: MasterAgentState) -> str:
state_val = state["agent_state"]
if state_val == AgentState.AGGREGATING.value:
return "aggregating"
elif state_val == AgentState.PLANNING.value:
return "planning" # 需要重试/补充
return "waiting" # 仍在等待
def route_from_executing(state: MasterAgentState) -> str:
state_val = state["agent_state"]
if state_val == AgentState.AGGREGATING.value:
return "aggregating"
return "planning" # 工具调用后回到规划
# 添加条件边
workflow.add_conditional_edges("planning", route_from_planning, {
"dispatching": "dispatching",
"executing": "executing",
"aggregating": "aggregating",
END: END,
})
workflow.add_conditional_edges("dispatching", route_from_dispatching, {
"waiting": "waiting",
})
workflow.add_conditional_edges("waiting", route_from_waiting, {
"aggregating": "aggregating",
"planning": "planning",
"waiting": "waiting",
})
workflow.add_conditional_edges("executing", route_from_executing, {
"aggregating": "aggregating",
"planning": "planning",
})
# AGGREGATING 是终点
workflow.add_edge("aggregating", END)
# 编译图(可选:注入检查点实现状态持久化与回放)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
return app
完整工作流示意:
#mermaid-svg-vMoqlsitO5jaz4OC{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-vMoqlsitO5jaz4OC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vMoqlsitO5jaz4OC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vMoqlsitO5jaz4OC .error-icon{fill:#552222;}#mermaid-svg-vMoqlsitO5jaz4OC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vMoqlsitO5jaz4OC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vMoqlsitO5jaz4OC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vMoqlsitO5jaz4OC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vMoqlsitO5jaz4OC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vMoqlsitO5jaz4OC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vMoqlsitO5jaz4OC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vMoqlsitO5jaz4OC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vMoqlsitO5jaz4OC .marker.cross{stroke:#333333;}#mermaid-svg-vMoqlsitO5jaz4OC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vMoqlsitO5jaz4OC p{margin:0;}#mermaid-svg-vMoqlsitO5jaz4OC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vMoqlsitO5jaz4OC .cluster-label text{fill:#333;}#mermaid-svg-vMoqlsitO5jaz4OC .cluster-label span{color:#333;}#mermaid-svg-vMoqlsitO5jaz4OC .cluster-label span p{background-color:transparent;}#mermaid-svg-vMoqlsitO5jaz4OC .label text,#mermaid-svg-vMoqlsitO5jaz4OC span{fill:#333;color:#333;}#mermaid-svg-vMoqlsitO5jaz4OC .node rect,#mermaid-svg-vMoqlsitO5jaz4OC .node circle,#mermaid-svg-vMoqlsitO5jaz4OC .node ellipse,#mermaid-svg-vMoqlsitO5jaz4OC .node polygon,#mermaid-svg-vMoqlsitO5jaz4OC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vMoqlsitO5jaz4OC .rough-node .label text,#mermaid-svg-vMoqlsitO5jaz4OC .node .label text,#mermaid-svg-vMoqlsitO5jaz4OC .image-shape .label,#mermaid-svg-vMoqlsitO5jaz4OC .icon-shape .label{text-anchor:middle;}#mermaid-svg-vMoqlsitO5jaz4OC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vMoqlsitO5jaz4OC .rough-node .label,#mermaid-svg-vMoqlsitO5jaz4OC .node .label,#mermaid-svg-vMoqlsitO5jaz4OC .image-shape .label,#mermaid-svg-vMoqlsitO5jaz4OC .icon-shape .label{text-align:center;}#mermaid-svg-vMoqlsitO5jaz4OC .node.clickable{cursor:pointer;}#mermaid-svg-vMoqlsitO5jaz4OC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vMoqlsitO5jaz4OC .arrowheadPath{fill:#333333;}#mermaid-svg-vMoqlsitO5jaz4OC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vMoqlsitO5jaz4OC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vMoqlsitO5jaz4OC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vMoqlsitO5jaz4OC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vMoqlsitO5jaz4OC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vMoqlsitO5jaz4OC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vMoqlsitO5jaz4OC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vMoqlsitO5jaz4OC .cluster text{fill:#333;}#mermaid-svg-vMoqlsitO5jaz4OC .cluster span{color:#333;}#mermaid-svg-vMoqlsitO5jaz4OC 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-vMoqlsitO5jaz4OC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vMoqlsitO5jaz4OC rect.text{fill:none;stroke-width:0;}#mermaid-svg-vMoqlsitO5jaz4OC .icon-shape,#mermaid-svg-vMoqlsitO5jaz4OC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vMoqlsitO5jaz4OC .icon-shape p,#mermaid-svg-vMoqlsitO5jaz4OC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vMoqlsitO5jaz4OC .icon-shape .label rect,#mermaid-svg-vMoqlsitO5jaz4OC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vMoqlsitO5jaz4OC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vMoqlsitO5jaz4OC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vMoqlsitO5jaz4OC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 任务需拆分
简单任务
信息足够
全部完成
需要重试
工具返回
信息足够
用户请求
🧠 PLANNING
分析任务/生成子任务列表
📤 DISPATCHING
并行调用 dispatch_agent
🔧 EXECUTING
主Agent 自己调工具
📊 AGGREGATING
汇总合成最终答案
⏳ WAITING
收集 Sub-Agent 结果
返回最终答案
Step 4:运行与使用
python
# 构建图实例
master_agent = build_master_agent_graph()
# 配置:线程 ID 用于状态持久化
config = {"configurable": {"thread_id": "session-001"}}
# 用户请求
result = master_agent.invoke(
{
"messages": [HumanMessage(content="请分析我司Q2财报,对比行业竞品,给出改进建议。")],
"agent_state": AgentState.PLANNING.value,
"sub_tasks": [],
"completed": {},
"final_answer": "",
"iteration_count": 0,
"max_iterations": 10,
},
config=config,
)
print(result["final_answer"])
落地要点总结
| 关注点 | 实践建议 |
|---|---|
| 状态持久化 | 使用 LangGraph 的 checkpointer(MemorySaver / SqliteSaver / PostgresSaver),确保服务重启后能从断点恢复,避免用户请求丢失。 |
| 人工介入 | 在关键状态转移前(如 DISPATCHING → WAITING)加 interrupt,允许人工审核 Sub-Agent 的角色描述和工具列表,防止 LLM 幻觉导致错误派生。 |
| 循环控制 | iteration_count + max_iterations 是硬约束;额外的软约束可通过 System Prompt 中的明确指令(如"连续 3 轮无进展就汇总退出")。 |
| 异步改造 | 示例中 dispatching_node 是同步循环,生产环境应改为 asyncio.gather + 信号量控制并发数,详见第 8.2 节「延迟与并发控制」。 |
| 可观测性 | 在每个节点的入口/出口记录 agent_state 和关键字段快照,接入 LangSmith 或自建日志系统,便于回溯「为什么这次走了重试路径」。 |
通过这套状态机架构,主Agent 不再是"黑盒 LLM 调用",而是一个可控、可观测、可恢复的确定性流程引擎------这正是动态派生能稳定跑在生产环境的关
4.1.3 生产级状态机升级:Redis 全局状态池 + 增量驱动决策
上一节的 LangGraph 状态机默认把 MasterAgentState 存在 Python 进程内存里(MemorySaver),这在生产环境有两个致命问题:进程重启状态全丢,多实例部署时状态不共享。本节引入 Redis 全局状态池 来解决这两个问题,并在此基础上实现主Agent 的增量驱动决策------主Agent 不再每次轮询全量状态,而是订阅状态变更事件,只在有变化时才做决策。这就是面试中常说的**"指挥家角色"**。
为什么需要全局状态池?
| 问题 | 内存版状态机(MemorySaver) | Redis 全局状态池 |
|---|---|---|
| 进程重启 | 全部请求丢失 | 状态持久化在 Redis,重启后可恢复 |
| 多实例部署 | 实例 A 派生的 Sub-Agent 结果,实例 B 看不到 | 所有实例共享同一 Redis 键空间 |
| Sub-Agent 异步回调 | 只能轮询或回调到同一进程 | Sub-Agent 完成后直接写 Redis,主Agent 订阅变更通知 |
| 可观测性 | 状态是黑盒,只能靠日志 | 任意时刻都能通过 Redis CLI 查看当前任务盘面 |
全局状态池的数据结构设计
Redis 里不存整坨序列化对象,而是拆成行级状态,每个字段独立读写,避免大 key 带来的序列化开销和并发冲突:
python
import redis
import json
from typing import Optional
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
class GlobalStatePool:
"""
全局状态池:基于 Redis 的行级状态存储。
每个请求一个 namespace(按 request_id 分区),字段独立 key。
"""
def __init__(self, request_id: str):
self.ns = f"agent:{request_id}" # 命名空间
# ─── 状态机阶段 ───
def set_phase(self, phase: str):
r.set(f"{self.ns}:phase", phase) # planning / dispatching / waiting / aggregating
def get_phase(self) -> str:
return r.get(f"{self.ns}:phase") or "planning"
# ─── 子任务管理(Hash 结构,每个子任务一个 field) ───
def add_sub_task(self, task_id: str, role: str, task: str, tools: list):
r.hset(f"{self.ns}:sub_tasks", task_id, json.dumps({
"role": role,
"task": task,
"tools": tools,
"status": "pending",
"result": "",
}))
def update_sub_task_status(self, task_id: str, status: str, result: str = ""):
"""Sub-Agent 完成后回调此方法,更新状态 + 发布变更事件。"""
current = json.loads(r.hget(f"{self.ns}:sub_tasks", task_id) or "{}")
current["status"] = status
current["result"] = result
r.hset(f"{self.ns}:sub_tasks", task_id, json.dumps(current))
# 👇 关键:发布变更事件,触发主Agent 增量决策
r.publish(f"{self.ns}:events", json.dumps({
"event": "sub_task_updated",
"task_id": task_id,
"status": status,
}))
def get_all_sub_tasks(self) -> dict:
raw = r.hgetall(f"{self.ns}:sub_tasks")
return {k: json.loads(v) for k, v in raw.items()}
def get_sub_task(self, task_id: str) -> Optional[dict]:
raw = r.hget(f"{self.ns}:sub_tasks", task_id)
return json.loads(raw) if raw else None
# ─── 关键约束 ───
def set_constraint(self, key: str, value: str):
r.hset(f"{self.ns}:constraints", key, value)
def get_constraints(self) -> dict:
return r.hgetall(f"{self.ns}:constraints") or {}
# ─── 生命周期 ───
def set_completed(self, final_answer: str):
r.set(f"{self.ns}:final_answer", final_answer)
r.set(f"{self.ns}:phase", "completed")
r.expire(f"{self.ns}:*", 3600) # 1 小时后自动清理
def get_final_answer(self) -> Optional[str]:
return r.get(f"{self.ns}:final_answer")
关键设计 :update_sub_task_status 里那行 r.publish 是整个增量驱动决策的发动机------当 Sub-Agent 完成(或超时、失败)时,它向 Redis Pub/Sub 频道发布一条变更事件,主Agent 的监听器收到后触发一次决策循环,而不是无脑轮询。
增量驱动决策:主Agent 的"指挥家"模式
有了全局状态池 + 事件发布机制,主Agent 的行为从"轮询式"升级为"事件驱动式":
python
import asyncio
import redis.asyncio as aioredis
class MasterAgentOrchestrator:
"""
增量驱动主Agent:订阅 Redis 事件,只在状态变化时决策。
------ 像交响乐指挥家,眼睛盯着乐谱(状态池),耳朵听着演奏(事件),
只在某个声部出错或完成时才挥动指挥棒。
"""
def
gentState` 存在 Python 进程内存里(`MemorySaver`),这在生产环境有两个致命问题:进程重启状态全丢,多实例下状态不同步。接下来我们设计一套 **Redis 全局状态池 + 增量驱动决策**的升级方案,让主Agent 从"任务执行者"彻底蜕变为"任务调度与监控者"。
**为什么需要全局状态池?**
| 问题 | 内存版状态机(MemorySaver) | Redis 全局状态池 |
|---|---|---|
| 进程重启 | 全部请求丢失 | 状态持久化在 Redis,重启后可恢复 |
| 多实例部署 | 状态不共享,负载均衡导致请求错乱 | 全局唯一状态视图,任意实例可接管 |
| Sub-Agent 回调 | 必须持有原线程引用,无法异步解耦 | Sub-Agent 完成后直接写 Redis,主Agent 通过 Pub/Sub 感知 |
| 断点续跑 | 不支持 | 任意时刻中断后可从 Redis 恢复继续 |
| 人工介入 | 需要额外 API 注入内存 | 直接修改 Redis 中的 `agent_state` 字段即可强制转移 |
**全局状态池的数据结构设计**
Redis 里不存整坨序列化对象,而是拆成**行级状态**,每个字段独立读写,避免大 key 带来的序列化开销和并发冲突:
```python
import redis
import json
from typing import Optional
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
class GlobalStatePool:
"""Redis 全局状态池:管理所有请求的分布式状态。"""
@staticmethod
def _request_key(request_id: str) -> str:
return f"agent:request:{request_id}"
# ─── 核心字段读写 ───
def get_agent_state(self, request_id: str) -> str:
return r.hget(self._request_key(request_id), "agent_state") or "planning"
def set_agent_state(self, request_id: str, state: str):
r.hset(self._request_key(request_id), "agent_state", state)
# 状态变更时发布事件,驱动增量决策
r.publish(f"agent:events:{request_id}", json.dumps({
"event": "state_changed",
"new_state": state,
}))
def add_sub_task(self, request_id: str, sub_task: dict):
"""向 sub_tasks 列表中追加一个新任务。"""
r.rpush(
f"{self._request_key(request_id)}:sub_tasks",
json.dumps(sub_task)
)
def get_sub_tasks(self, request_id: str) -> list:
raw = r.lrange(f"{self._request_key(request_id)}:sub_tasks", 0, -1)
return [json.loads(item) for item in raw]
def update_sub_task_status(
self, request_id: str, task_id: str, status: str, result: str = ""
):
"""更新单个子任务的状态(原地替换)。"""
tasks_key = f"{self._request_key(request_id)}:sub_tasks"
tasks = [json.loads(item) for item in r.lrange(tasks_key, 0, -1)]
for t in tasks:
if t["task_id"] == task_id:
t["status"] = status
t["result"] = result
break
# 原子替换整个列表
r.delete(tasks_key)
for t in tasks:
r.rpush(tasks_key, json.dumps(t))
# 🚨 关键:发布 Sub-Agent 完成事件,驱动增量决策
r.publish(f"agent:events:{request_id}", json.dumps({
"event": "sub_task_updated",
"task_id": task_id,
"status": status,
"result": result,
}))
关键设计 :update_sub_task_status 里那行 r.publish 是整个增量驱动决策的发动机------当 Sub-Agent 完成(或超时、失败)时,它向主Agent 发送一个"状态变更"事件,主Agent 无需轮询就能立即感知并做出下一步决策。
增量驱动决策:主Agent 的"指挥家"模式
有了全局状态池 + 事件发布机制,主Agent 的行为从"轮询式"升级为"事件驱动式":
python
import asyncio
import redis.asyncio as aioredis
class MasterAgentOrchestrator:
"""增量驱动的主Agent 编排器:只盯事件,不干杂活。"""
def __init__(self, redis_url: str = "redis://localhost:6379"):
self.redis = aioredis.from_url(redis_url)
self.state_pool = GlobalStatePool()
async def listen_and_react(self, request_id: str):
"""主循环:订阅 Redis 事件,增量做出决策。"""
pubsub = self.redis.pubsub()
await pubsub.subscribe(f"agent:events:{request_id}")
async for message in pubsub.listen():
if message["type"] != "message":
continue
event = json.loads(message["data"])
await self._decide_and_act(request_id, event)
async def _decide_and_act(self, request_id: str, event: dict):
"""根据最新事件 + 全局状态做单步决策。"""
current_state = self.state_pool.get_agent_state(request_id)
# ── 决策逻辑(5 条硬规则) ──
if event["event"] == "sub_task_updated":
sub_tasks = self.state_pool.get_sub_tasks(request_id)
all_done = all(
t["status"] in ("done", "timeout", "failed")
for t in sub_tasks
)
if all_done:
# 规则 1:全部完成 → 汇总
await self._transition_to_aggregating(request_id)
return
# 规则 2:有失败/超时 → 判断是否致命
critical_failures = [
t for t in sub_tasks
if t["status"] in ("failed", "timeout")
]
if critical_failures:
await self._handle_failures(request_id, critical_failures)
return
# 规则 3:部分完成 → 继续等待(什么都不做)
return
if event["event"] == "state_changed":
# 规则 4:外部人工介入修改了状态,响应之
new_state = event["new_state"]
print(f" 人工介入:状态 → {new_state}")
return
# 规则 5:未识别事件 → 采集日志,不做动作
print(f" 未处理事件:{event}")
async def _transition_to_aggregating(self, request_id: str):
"""切换状态到汇总阶段。"""
self.state_pool.set_agent_state(request_id, "aggregating")
# 触发汇总逻辑(实际项目中调用 LangGraph 的 aggregating_node)
print(f" 全部完成 → 进入 AGGREGATING")
async def _handle_failures(
self, request_id: str, failures: list
):
"""处理失败子任务。"""
# 获取当前重试计数
retry_count = int(
self.redis.hget(
self.state_pool._request_key(request_id), "retry_count"
) or 0
)
if retry_count < 3:
# 重试:把失败任务状态改回 pending
for f in failures:
self.state_pool.update_sub_task_status(
request_id, f["task_id"], "pending"
)
self.state_pool.set_agent_state(request_id, "dispatching")
self.redis.hincrby(
self.state_pool._request_key(request_id), "retry_count", 1
)
print(f" 有 {len(failures)} 个失败任务 → 回 DISPATCHING(重试 #{retry_count + 1})")
else:
# 超过重试次数 → 带着失败结果进入汇总
print(f" 已重试 {retry_count} 次 → 放弃重试,进 AGGREGATING")
await self._transition_to_aggregating(request_id)
决策逻辑清单------主Agent 的"指挥棒"规则
上面的 _decide_and_act 方法可以总结为 5 条硬规则,这也是面试中可以直接回答的"增量驱动决策表":
| 当前盘面 | 主Agent 动作 | 状态转移 |
|---|---|---|
| 全部 Sub-Agent 完成 | 汇总 + 标记 completed | → AGGREGATING |
| 有失败/超时,且未达重试上限 | 重置失败任务为 pending | → DISPATCHING |
| 有失败/超时,已达重试上限 | 放弃重试,带着部分结果汇总 | → AGGREGATING |
| 部分完成(仍有 running/pending) | 什么都不做,继续等待 | 保持 WAITING |
| 外部事件(人工修改状态) | 响应新状态,执行对应的状态节点 | 由新状态决定 |
核心思想 :主Agent 只订阅 Redis 状态变更事件,不在
WAITING阶段轮询------没消息就是一切正常,有消息才根据增量变化做单步决策。这就是"指挥家角色"的精髓:不做定时巡检,只等乐手举手示意。
Redis 状态池的额外收益
除了增量决策,这种架构还解锁了几个生产级能力:
- 跨请求复用 Sub-Agent :Redis 里存的是行级状态,下次用户问同类财报问题时,主Agent 检测到已有"竞品研究员"的结果(
status=done+result非空),可直接复用而不再重新派生,降低 LLM 调用成本。 - 断点续跑 :用户请求在
WAITING阶段因服务重启而中断,重启后主Agent 从 Redis 恢复所有sub_tasks状态,从断点继续等待------用户完全无感知。 - 人工介入零侵入 :运维人员只需在 Redis 中执行
HSET agent:request:{id} agent_state planning,主Agent 立刻通过 Pub/Sub 感知状态变更,回到PLANNING重新决策,无需重启服务或修改代码。 - 多实例无状态水平扩展:主Agent 本身不持有任何内存状态,任意实例订阅同一个 Redis channel 即可接管请求,天然适配 K8s 的自动扩缩容与滚动更新。
以上是生产级状态机的完整升级方案。下面继续介绍 Sub-Agent 的具体定义、初始化与执行通信机制。
init (self, request_id: str, llm: any, max_iterations: int = 10):
self.state = GlobalStatePool(request_id)
self.llm = llm
self.max_iterations = max_iterations
self.iteration = 0
self._decision_lock = asyncio.Lock() # 防止并发决策
async def listen_and_conduct(self):
"""
核心循环:订阅变更事件 → 增量决策 → 执行 → 等待下一个事件。
只有盘面发生变化时才启动决策推理,而不是空转轮询。
"""
pubsub = aioredis.from_url("redis://localhost:6379").pubsub()
await pubsub.subscribe(f"{self.state.ns}:events")
# 初始驱动:没有任何 Sub-Agent 时,直接进入 PLANNING
if not self.state.get_all_sub_tasks():
await self._decide_and_act()
async for message in pubsub.listen():
if message["type"] != "message":
continue
event = json.loads(message["data"])
# 只对"子任务完成"事件做增量决策
if event.get("event") == "sub_task_updated":
await self._decide_and_act()
# 终止条件:阶段变为 completed 就退出
if self.state.get_phase() == "completed":
await pubsub.unsubscribe()
break
async def _decide_and_act(self):
"""
增量决策的核心逻辑------"盘面扫描 + 单步决策"。
因为只在状态变化后调用,每次决策都是增量的、轻量的。
"""
async with self._decision_lock:
self.iteration += 1
if self.iteration > self.max_iterations:
# 安全阀:超限直接降级汇总
await self._force_aggregate()
return
sub_tasks = self.state.get_all_sub_tasks()
pending = [tid for tid, st in sub_tasks.items() if st["status"] == "pending"]
failed = [tid for tid, st in sub_tasks.items() if st["status"] in ("failed", "timeout")]
done = [tid for tid, st in sub_tasks.items() if st["status"] == "done"]
# ── 决策规则(确定性,不依赖 LLM) ──
# 规则 1:全部完成 → 汇总
if len(done) == len(sub_tasks) and not pending:
self.state.set_phase("aggregating")
await self._aggregate(done, sub_tasks)
return
# 规则 2:有关键失败 + 未超迭代上限 → 重试
if failed and self.iteration < self.max_iterations:
for tid in failed:
st = sub_tasks[tid]
# 重置为 pending,让下一轮 dispatching 重新执行
self.state.update_sub_task_status(tid, "pending", "")
self.state.set_phase("planning") # 回到 planning 触发重新 dispatching
await self._dispatch(pending + failed, sub_tasks)
return
# 规则 3:还有 pending → 继续分发
if pending:
self.state.set_phase("dispatching")
await self._dispatch(pending, sub_tasks)
return
# 规则 4:没有需要处理的任务(边缘情况)→ 汇总已有结果
self.state.set_phase("aggregating")
await self._aggregate(done, sub_tasks)
async def _dispatch(self, task_ids: list, sub_tasks: dict):
"""并行分发 Sub-Agent(这里只示范入口,实际调用项目中的 dispatch_agent)。"""
tasks = []
for tid in task_ids:
st = sub_tasks[tid]
# 实际:调用 dispatch_agent(role=st["role"], task=st["task"], tools=st["tools"])
tasks.append(asyncio.sleep(1)) # 占位
await asyncio.gather(*tasks)
async def _aggregate(self, done_ids: list, sub_tasks: dict):
"""汇总所有已完成 Sub-Agent 的结果。"""
reports = []
for tid in done_ids:
st = sub_tasks[tid]
reports.append(f"### [{st['role']}] 分析报告\n{st['result']}")
prompt = f"请整合以下专家报告:\n\n" + "\n\n---\n\n".join(reports)
# 实际:response = self.llm.invoke(prompt)
final_answer = "汇总后的分析报告..." # 占位
self.state.set_completed(final_answer)
async def _force_aggregate(self):
"""超限兜底:强制汇总已有结果。"""
sub_tasks = self.state.get_all_sub_tasks()
done = [tid for tid, st in sub_tasks.items() if st["status"] == "done"]
if done:
await self._aggregate(done, sub_tasks)
else:
self.state.set_completed("抱歉,分析超时,请稍后重试。")
**决策逻辑清单------主Agent 的"指挥棒"规则**
上面的 `_decide_and_act` 方法可以总结为 5 条硬规则,这也是面试中可以直接回答的"增量驱动决策表":
| 当前盘面 | 主Agent 动作 | 状态转移 |
|---|---|---|
| 全部 Sub-Agent 完成 | 汇总 + 标记 completed | `→ AGGREGATING → completed` |
| 有关键失败 + 未超迭代上限 | 重置失败子任务为 pending,重新分发 | `→ PLANNING → DISPATCHING` |
| 还有 pending 子任务 | 并行分发 | `→ DISPATCHING` |
| 超迭代上限 | 强制降级:有结果就汇总,没有就道歉 | `→ AGGREGATING → completed` |
| 没有变化(无事件触发) | **不决策,空转等待** | 维持当前状态 |
> **核心思想**:主Agent 只订阅 Redis 状态变更事件,不在 `WAITING` 阶段轮询------没消息就是一切正常,有消息才根据增量变化做单步决策。这就是"指挥家角色"的精髓:**只在声部出问题时才介入,其余时间让乐团自己演奏**。
**Redis 状态池的额外收益**
除了增量决策,这种架构还解锁了几个生产级能力:
- **跨请求复用 Sub-Agent**:Redis 里存的是行级状态,下次用户问同类财报问题时,主Agent 检测到已有"竞品研究员"的结果(`status=done` + `result` 非空),直接跳过派生,从状态池读缓存。
- **人工介入可控**:把 `phase` 设为 `awaiting_approval`,前端管理后台检测到此状态后展示子任务列表,人工点击"批准执行"后再把阶段切换为 `dispatching`。
- **断点续跑**:如果 `dispatching` 阶段进程挂了,重启后检查 Redis:`sub_tasks` 里 3 个 `pending`、2 个 `done`,直接继续跑,不重复已完成的任务。
- **可观测**:Grafana 实时监控 `agent:{request_id}:*` 键空间的行数与状态分布,异常一目了然。
**实践建议清单**
| 关注点 | 实现方式 |
|---|---|
| 状态行级拆分 | 不存整坨 JSON,用 Hash 的 field 存每个 sub_task,避免大 key 和锁竞争 |
| 事件发布 | Redis Pub/Sub 发布 `sub_task_updated` 事件,驱动增量决策;生产环境可升级为 Stream 支持 replay |
| 并发安全 | `_decision_lock`(asyncio.Lock)保证同一请求不会并发决策;Redis `WATCH` + `MULTI` 保证写入事务性 |
| 超时与清理 | 为每个 `agent:{id}:*` 设 TTL(如 1 小时),防止僵尸 key 堆积 |
| 分布式部署 | 所有 Agent 实例共享同一 Redis 集群,天然支持 K8s 水平扩缩 |
键。
### 4.2 Sub-Agent的定义与初始化
调用 `dispatch_agent` 后,系统从主Agent的工具注册表中取出对应工具实体,并与传入的角色描述、任务指令拼接成**系统的 System Prompt**。一个典型的子Agent系统提示模板如下:
```markdown
你是一名{role}。你的任务是:{task}
你可以使用以下工具来完成工作:
{tool_descriptions}
重要规则:
- 你只能使用提供的工具,不要自行想象接口。
- 完成后,请调用 `final_report` 工具提交最终结果,给出简洁、结构化的答案。
- 如果任务无法完成,请清晰说明原因。
Sub-Agent实例通常复用底层的LLM或Agent运行时,但为其分配独立的对话历史 和状态上下文。若框架支持,可以为每个Sub-Agent开启单独的协程或进程,实
4.1.4 常见错误与排查指南
在生产环境中运行基于 LangGraph 的状态机时,即使架构设计合理,仍会遇到一些让人头疼的运行时问题。本节梳理 4 个高发故障,给出可直接上手的排查步骤和修复方案。
问题一:状态死循环------PLANNING ↔ WAITING 无限跳转
现象 :主Agent 在 PLANNING 和 WAITING 之间反复横跳,iteration_count 迅速增长直至触及 max_iterations 后被强制终止,最终返回"分析超时"的降级回复。
根因 :waiting_node 检测到 Sub-Agent 失败后回到 PLANNING,但 planning_node 面对失败信息又派生了一组完全相同的子任务,导致死循环。
排查步骤:
- 在日志平台按
trace_id聚合本次请求的完整调用链路,观察agent_state的变化序列。如果看到PLANNING → DISPATCHING → WAITING → PLANNING → DISPATCHING → WAITING → ...且每次派生的sub_tasks列表完全一致,即可确认死循环。 - 检查
planning_node的输入消息中是否正确包含了失败上下文(waiting_node返回的HumanMessage)。若日志显示该HumanMessage为空或被截断,说明失败信息未正确注入。 - 检查
planning_node的 System Prompt 中是否包含"如果子任务已经失败过一次,不要重复派发相同的子任务"这类防重复指令。
日志关键字段:
trace_id:本次请求的全链路追踪 IDagent_state序列:PLANNING → DISPATCHING → WAITING → PLANNING → ...sub_tasks_hash:每轮派生的子任务列表摘要(用于对比是否重复)iteration_count/max_iterations
修复方案:
- 硬防重 :在
planning_node中维护一个failed_sub_task_ids集合。每次进入PLANNING时,若 LLM 返回的sub_tasks中某个子任务的role + task与已失败的完全一致,直接丢弃并提示 LLM 换一个方案。 - 限制重试次数 :在
MasterAgentState中为每个SubTask增加retry_count字段。waiting_node回到PLANNING前检查该值,若 ≥ 3 则直接标记为failed_perm并跳过。 - 降级出口 :在
route_from_planning条件边中增加一条兜底逻辑:if state["iteration_count"] >= state["max_iterations"] - 1: return "aggregating",确保最后一轮强制汇总并退出。
问题二:Sub-Agent 超时导致主Agent 整体卡死
现象 :用户请求发送后,主Agent 在 WAITING 状态停留超过 30 秒无响应,整个调用链路僵死,最终触发网关超时(502/504)。
根因 :dispatching_node 中使用了同步阻塞方式调用 dispatch_agent,而某个 Sub-Agent 调用的下游 API(如数据库查询、第三方接口)响应极慢或直接挂起,阻塞了整个节点。
排查步骤:
- 在主Agent 日志中搜索
dispatching_node的出口日志,确认是否有 Sub-Agent 的status长期为pending或running。 - 查看该 Sub-Agent 的
last_tool_call_time字段------若距今超过设定的task_timeout(如 45 秒),说明外部工具已僵死。 - 检查该 Sub-Agent 所调用的工具的下游健康状况:数据库慢查询、第三方 API 延迟、网络分区等。
日志关键字段:
sub_task_id+statuslast_tool_call_time:最后一次工具调用的时间戳elapsed_seconds:从 Sub-Agent 启动到当前已用时间tool_name+error_code(如Timeout、ConnectionError)
修复方案:
- 异步 + 超时 :将
dispatching_node中的同步调用改为asyncio.wait_for(run_sub_agent_async(...), timeout=task_timeout)。超时后捕获asyncio.TimeoutError,将对应 Sub-Agent 的status置为timeout,不会阻塞其他 Sub-Agent。 - 双层超时 :为 Sub-Agent 设置整体
task_timeout(如 45 秒),同时为每个单独工具调用设置tool_call_timeout(如 10 秒)。任一层超时即终止该 Sub-Agent。 - 熔断器:对频繁超时的工具启用断路器模式------连续失败 N 次后,后续调用直接返回降级结果(如缓存值或空列表),避免雪崩。
问题三:Redis 连接断裂导致全局状态丢失
现象 :服务运行一段时间后,主Agent 突然无法读取 MasterAgentState,agent_state 字段回退为初始值 PLANNING,导致请求从中间状态重新开始执行。更严重的场景:进程重启后,所有正在执行的请求状态全部丢失。
根因 :LangGraph 默认使用 MemorySaver(进程内存)做状态持久化,进程重启即丢失。即使配置了 Redis 作为 Checkpointer,若 Redis 连接池耗尽、网络闪断或 Redis 服务重启,仍会导致状态读写失败。
排查步骤:
- 检查 LangGraph 的
checkpointer配置------若代码中写的是MemorySaver(),说明根本没有持久化,进程重启必然丢状态。 - 若已配置 Redis Checkpointer,检查 Redis 连接池参数:
max_connections是否足够(建议 ≥ 20),socket_timeout是否设置(建议 5 秒),retry_on_timeout是否开启。 - 在 Redis 服务端查看连接数、内存使用、慢查询日志,确认是否存在资源瓶颈导致连接被拒绝。
- 在应用日志中搜索
Redis connection error、ConnectionResetError、Timeout reading from socket等关键词。
日志关键字段:
checkpointer_type:MemorySaver/RedisSaver/PostgresSaverredis_connection_errors:连接失败计数redis_latency_p99:Redis 读写延迟 P99 值state_snapshot_success:状态快照是否写入成功
修复方案:
- 升级 Checkpointer :生产环境必须 将
MemorySaver替换为RedisSaver或PostgresSaver。LangGraph 官方提供了langgraph.checkpoint.redis.aio.AsyncRedisSaver,支持异步读写和连接池管理。 - 连接池与重试 :配置 Redis 连接池的
max_connections=20,retry_on_timeout=True,health_check_interval=30。在应用层封装读写操作,加上指数退避重试(最多 3 次)。 - 双写兜底 :关键状态字段(如
agent_state、sub_tasks)在写 Redis 的同时异步写一份到本地 SQLite,作为 Redis 故障时的紧急降级读取源。 - 心跳检测:主Agent 启动时向 Redis 写入心跳 key,每 10 秒刷新 TTL。监控该 key 是否存在,若消失则触发告警。
问题四:状态机死锁------多个 Sub-Agent 互相等待对方结果
现象 :主Agent 的状态机卡在 WAITING,所有 Sub-Agent 的 status 均为 pending,但实际上它们已经完成了各自的任务,只是没有人将结果写回 sub_tasks 列表。
根因 :在异步模式下,dispatching_node 发出 Sub-Agent 后直接进入 WAITING,但 Sub-Agent 完成后的回调函数未能正确更新主Agent 的 sub_tasks 状态。常见于:回调函数中使用了错误的 thread_id、Redis Pub/Sub 消息丢失、或者回调中抛出异常但未被捕获。
排查步骤:
- 检查 Redis Pub/Sub 通道是否正常------在
update_sub_task_status函数中打印发布日志,在MasterAgentOrchestrator的订阅回调中打印接收日志。若发布但未接收,说明通道名称不匹配或消息丢失。 - 检查回调函数中的
thread_id是否与主Agent 当前config中的thread_id一致。若不一致,状态更新会写到另一个线程的状态快照中。 - 检查 Sub-Agent 完成后的返回值格式是否符合预期------若返回的不是标准 JSON 或缺少
task_id字段,回调函数可能静默失败。
日志关键字段:
pubsub_channel:发布/订阅的通道名称pubsub_message_published:消息发布时间戳pubsub_message_received:消息接收时间戳(若为空 = 消息丢失)callback_error:回调函数中的异常信息
修复方案:
- 回调幂等 + 重试 :回调函数在更新
sub_tasks前先检查当前状态是否为pending(防止重复更新)。更新失败时写入死信队列,由独立的修复 Worker 重试。 - 兜底轮询 :在
WAITING节点中增加一个定时轮询机制------每隔 5 秒主动扫描sub_tasks列表中每个子任务的实际执行状态(直接查 Redis 状态池),而不是完全依赖回调。这样即使 Pub/Sub 消息丢失,也能在 5 秒内恢复。 - 超时强制推进 :在
waiting_node中增加全局超时------若进入WAITING超过 60 秒仍未全部完成,直接检查所有子任务状态,将仍为pending的标记为timeout并强制进入AGGREGATING。 - 添加看门狗 :独立启动一个定时任务(如 APScheduler),每 30 秒扫描所有
WAITING状态的thread_id,发现长时间无进展的请求主动注入一条HumanMessage唤醒状态机。
现并行。
4.3 执行与中间通信
Sub-Agent的执行周期是:接收系统提示 → 进入ReAct/Function Call循环 → 调用工具 → 观察结果 → 继续,直到调用 final_report 或达到最大步数。
主Agent在调用 dispatch_agent 后会阻塞等待 ,或者在并行模式下注册回调。为了实现更复杂的 双向通信 ,我们可以引入消息通道:Sub-Agent可以在达到某个中间检查点时,通过 ask_human 工具向主Agent提澄清问题,主Agent再通过注入新消息继续推动执行。
4.4 结果回收与生命周期管理
Sub-Agent通过 final_report 工具产出的文本被视为最终交付物,由 dispatch_agent 工具返回给主Agent。随后,Sub-Agent实例被销毁或进入休眠缓存,其资源被回收(关闭网络连接、释放数据库会话等)。但为了可能的后续复用(特别是长期记忆),Sub-Agent的关键产出和中间推理摘要会写入主Agent的工作记忆中。
5. 代码实现骨架(LangGraph/LangChain风格)
以下是一个基于LangChain + LangGraph的简化骨架,展示主Agent如何使用自定义工具实现动态创建。
python
from typing import List, Optional
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.prebuilt import create_react_agent
import uuid
# 全局工具注册表(键为工具名,值为工具实例或创建函数)
GLOBAL_TOOLS = {
"search": search_tool_instance,
"calculator": calculator_tool,
"sql_executor": sql_executor,
}
# 子Agent工厂函数
def create_sub_agent(role: str, task: str, tool_names: List[str]) -> any:
tools = [GLOBAL_TOOLS[name] for name in tool_names if name in GLOBAL_TOOLS]
system_msg = f"""你是一名{role}。任务:{task}
你可以使用工具:{', '.join(tool_names)}"""
# 使用langgraph的预构建ReAct agent
sub_agent = create_react_agent(
model=llm, # 共用或指定不同模型
tools=tools,
state_modifier=system_msg
)
return sub_agent
@tool
def dispatch_agent(
role: str,
task: str,
tools: List[str],
context: Optional[str] = None
) -> str:
"""创建子Agent执行指定任务并返回结果。"""
print(f"[主Agent] 正在创建子Agent: {role}")
sub = create_sub_agent(role, task, tools)
# 构建输入消息(可携带上下文)
messages = [HumanMessage(content=task)]
if context:
messages.insert(0, HumanMessage(content=f"背景信息:{context}"))
# 调用子Agent并等待完成(同步/异步均可)
result = sub.invoke({"messages": messages})
# 提取最终结果(通常取最后一条AI消息)
final_output = result["messages"][-1].content
print(f"[主Agent] 子Agent完成,结果:{final_output[:100]}...")
return final_output
在实际系统中,create_sub_agent 还可以注册一个 final_report 工具让Sub-Agent显式提交,以区分中间步骤和最终输
5.1 实战:财报分析场景
下面我们把动态派生架构成一个 可运行的端到端示例 :模拟主Agent 接到「分析 Q2 财报并对比竞品,给出改进建议」请求后,派生出数据分析师与竞品研究员两个 Sub-Agent 协同工作。代码可直接复制运行(依赖 langchain-core、langgraph 和任一兼容 OpenAI 接口的 LLM)。
5.1.1 工具定义
先为 Sub-Agent 准备好两把"手术刀"------财务数据拉取工具和竞品信息搜索工具。生产环境这里对接真实 API/数据库,此处用模拟数据完整跑通链路。
python
from langchain_core.tools import tool
import json, time
# ========================
# 模拟数据(生产环境替换为真实 API/DB)
# ========================
MOCK_FINANCIAL_DATA = {
"Q2_2025": {
"revenue": 8.2, # 亿
"revenue_yoy_growth": 0.12, # 同比增长 12%
"net_profit": 1.15,
"net_profit_yoy_growth": -0.05,
"gross_margin": 0.42,
"operating_expenses": 2.3,
"r_and_d_ratio": 0.18,
"main_revenue_sources": {
"云计算": 3.1,
"广告": 2.5,
"会员订阅": 1.6,
"其他": 1.0,
},
},
"Q2_2024": {
"revenue": 7.32,
"net_profit": 1.21,
"gross_margin": 0.45,
"operating_expenses": 2.05,
"r_and_d_ratio": 0.15,
},
}
MOCK_COMPETITOR_DATA = {
"竞品A": {
"market_share": 0.28,
"revenue": 10.5,
"strength": "品牌效应强、渠道覆盖广",
"weakness": "云业务起步晚、研发投入占比仅 10%",
},
"竞品B": {
"market_share": 0.18,
"revenue": 5.8,
"strength": "技术创新快、用户粘性高",
"weakness": "营收规模小、尚未盈利",
},
"行业均值": {
"revenue_growth": 0.09,
"gross_margin": 0.38,
"r_and_d_ratio": 0.14,
},
}
# ========================
# Sub-Agent 工具
# ========================
@tool
def fetch_financial_data(query: str) -> str:
"""拉取公司财务数据。query 应包含时间范围(如 'Q2 2025 营收、净利润、毛利')。"""
# 解析 query 提取需要的字段(简化:直接按 Q2_2025 返回)
if "2025" in query or "最新" in query or "Q2" in query:
data = MOCK_FINANCIAL_DATA["Q2_2025"]
else:
data = MOCK_FINANCIAL_DATA["Q2_2024"]
# 模拟网络延迟
time.sleep(0.5)
return json.dumps(
{
"query": query,
"data": data,
"unit": "亿元(人民币)",
"note": "数据来源:模拟财务系统",
},
ensure_ascii=False,
indent=2,
)
@tool
def search_competitor_info(keyword: str) -> str:
"""搜索竞品公开信息。keyword 为竞品名称或行业关键词。"""
time.sleep(0.3)
results = {}
for name, info in MOCK_COMPETITOR_DATA.items():
if keyword.lower() in name.lower() or keyword == "行业":
results[name] = info
if not results:
return json.dumps(
{"message": f"未找到与 '{keyword}' 相关的竞品信息", "suggestion": "尝试搜索 '竞品A'、'竞品B' 或 '行业'"},
ensure_ascii=False,
)
return json.dumps(
{"keyword": keyword, "results": results, "source": "模拟竞品数据库"},
ensure_ascii=False,
indent=2,
)
5.1.2 主Agent 规划:拆解财报分析任务
主Agent 的规划逻辑复用了 4.1.2 节的状态机------唯一区别是 规划 Prompt 针对财报分析场景做了定制,让 LLM 能自动拆出合适子任务:
python
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_openai import ChatOpenAI
import uuid, json
def parse_json(text: str) -> dict:
"""从 LLM 输出中安全提取 JSON。"""
try:
return json.loads(text)
except json.JSONDecodeError:
# 容错:尝试截取最后一个 { 到 } 之间的内容
start = text.rfind("{")
end = text.rfind("}")
if start != -1 and end != -1:
return json.loads(text[start : end + 1])
raise
# ========================
# 主Agent 规划节点(财报分析定制版)
# ========================
FINANCIAL_PLANNING_PROMPT = """你是一名资深的商业分析总监。用户请求与财报分析相关。
**你的核心工作流程:**
1. 仔细阅读用户请求,确定需要分析的具体维度。
2. 将任务拆分为 2-3 个可由专家 Sub-Agent 并行完成的子任务。
3. 每个子任务必须指定:role(专家角色)、task(具体任务)、tools(可使用的工具名)。
4. 如果请求过于宽泛,宁可多拆一个维度也不要遗漏。
**可用工具(由 Sub-Agent 使用):**
- fetch_financial_data:拉取财务指标。
- search_competitor_info:搜索竞品分析信息。
**典型拆分示例(用户问财报分析+竞品对比):**
- 子任务 1:role="数据分析师",task="拉取 Q2 财报核心指标并分析趋势",tools=["fetch_financial_data"]
- 子任务 2:role="竞品研究员",task="搜索主要竞品的市场表现并与我方对比",tools=["search_competitor_info"]
请用 JSON 格式输出你的决策:
{
"action": "dispatch",
"reasoning": "简短的判断理由",
"sub_tasks": [
{
"role": "数据分析师",
"task": "提取 Q2 营收、净利润、毛利、各业务线收入,分析同比增长与环比变化",
"tools": ["fetch_financial_data"]
}
]
}
"""
def financial_planning_node(state: dict) -> dict:
"""财报分析专用规划节点:调用 LLM 生成 sub_task 列表。"""
llm = state.get("llm") or ChatOpenAI(model="gpt-4o", temperature=0)
messages = [
SystemMessage(content=FINANCIAL_PLANNING_PROMPT),
HumanMessage(content=state["messages"][-1].content),
]
response = llm.invoke(messages)
decision = parse_json(response.content)
if decision.get("action") != "dispatch":
# 不拆任务(简单查询),直接返回
return {
"agent_state": "aggregating",
"final_answer": decision.get("direct_answer", "无法完成分析。"),
"messages": [response],
}
# 为每个子任务生成唯一 ID
sub_tasks = []
for st in decision["sub_tasks"]:
sub_tasks.append(
{
"task_id": str(uuid.uuid4())[:8],
"role": st["role"],
"task": st["task"],
"tools": st.get("tools", []),
"context": "",
"status": "pending",
"result": "",
}
)
return {
"agent_state": "dispatching",
"sub_tasks": sub_tasks,
"messages": [response],
"planning_reasoning": decision.get("reasoning", ""),
}
5.1.3 Sub-Agent 创建与并行执行
这是动态派生的核心 :根据 role 和 tools 按需创建 Sub-Agent,并为其注入专属工具集:
python
from langgraph.prebuilt import create_react_agent
import asyncio
# 全局工具注册表
TOOL_REGISTRY = {
"fetch_financial_data": fetch_financial_data,
"search_competitor_info": search_competitor_info,
}
def create_sub_agent(role: str, task: str, tool_names: list, llm) -> any:
"""动态创建 Sub-Agent 实例。"""
tools = [TOOL_REGISTRY[name] for name in tool_names if name in TOOL_REGISTRY]
system_prompt = f"""你是一名{role}。
你的任务是:{task}
你可以使用以下工具来获取数据:
{chr(10).join(f'- {t.name}: {t.description}' for t in tools)}
重要规则:
1. 先调用工具获取数据,再基于数据给出专业分析。
2. 分析要具体,不要泛泛而谈------引用具体数字和趋势。
3. 完成后,请调用 `final_report` 工具提交结构化分析报告。
4. 如果数据缺失,明确说明"缺少 XX 数据,基于已有信息推断如下"。
"""
sub_agent = create_react_agent(
model=llm,
tools=tools,
state_modifier=system_prompt,
)
return sub_agent
async def run_sub_agent_async(sub_task: dict, llm, context: str = "") -> str:
"""异步运行单个 Sub-Agent。"""
role, task, tool_names = sub_task["role"], sub_task["task"], sub_task["tools"]
print(f" 🚀 派生 Sub-Agent [{role}],工具:{tool_names}")
sub = create_sub_agent(role, task, tool_names, llm)
messages = [HumanMessage(content=task)]
if context:
messages.insert(0, HumanMessage(content=f"上下文补充:{context}"))
try:
result = sub.invoke({"messages": messages})
final_msg = result["messages"][-1].content
print(f" ✅ [{role}] 完成,输出 {len(final_msg)} 字符")
return final_msg
except Exception as e:
print(f" ❌ [{role}] 异常:{e}")
return f"[执行失败] {str(e)}"
async def run_all_sub_agents_parallel(sub_tasks: list, llm) -> dict:
"""并行运行所有 Sub-Agent。"""
tasks = [run_sub_agent_async(st, llm) for st in sub_tasks]
results = await asyncio.gather(*tasks)
for st, result in zip(sub_tasks, results):
st["status"] = "done" if not result.startswith("[执行失败]") else "failed"
st["result"] = result
return {st["task_id"]: st["result"] for st in sub_tasks}
5.1.4 汇总节点:合成分析报告
所有 Sub-Agent 返回后,汇总节点对两份报告做交叉分析:
python
FINANCIAL_AGGREGATION_PROMPT = """你是一名首席战略官。请基于以下专家分析结果,为用户撰写一份结构化财报分析报告。
**报告结构要求:**
1. 核心财务指标概览(营收、利润、毛利、增速)
2. 业务线表现分析(各业务线收入占比与变化)
3. 竞品对标(我方 vs 竞品 A/B 的优劣势)
4. 存在问题与风险(数据下滑、成本上升等)
5. 改进建议(短期可落地 2-3 条 + 长期战略 1-2 条)
专家分析结果:
{expert_reports}
用户原始请求:
{user_request}
注意:引用具体数字,指出共识与分歧,结论要有可执行性。"""
def financial_aggregating_node(state: dict) -> dict:
"""财报分析专用汇总节点。"""
llm = state.get("llm") or ChatOpenAI(model="gpt-4o", temperature=0.3)
reports = "\n\n---\n\n".join(
f"### {st['role']} 分析报告\n{st['result']}"
for st in state["sub_tasks"]
if st["status"] == "done"
)
user_request = state["messages"][0].content if state["messages"] else "未提供"
prompt = FINANCIAL_AGGREGATION_PROMPT.format(
expert_reports=reports, user_request=user_request
)
final_response = llm.invoke([
SystemMessage(content="你是一名首席战略官,负责整合多份专家报告。"),
HumanMessage(content=prompt),
])
return {
"agent_state": "completed",
"final_answer": final_response.content,
"messages": [final_response],
}
5.1.5 完整运行入口
将以上组件串联成一个可直接运行的 Demo:
python
async def financial_analysis_demo():
"""财报分析 Demo:主Agent 动态派生两个 Sub-Agent 协同完成分析。"""
print("=" * 60)
print("📊 财报分析多智能体 Demo")
print("=" * 60)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# --- Step 1:规划 ---
print("\n🧠 [PLANNING] 主Agent 分析任务,生成子任务列表...")
initial_state = {
"messages": [
HumanMessage(
content="请分析我司 2025 年 Q2 财报,对比行业主要竞品(竞品A、竞品B),找出问题并给出改进建议。"
)
],
"llm": llm,
}
planning_result = financial_planning_node(initial_state)
sub_tasks = planning_result["sub_tasks"]
print(f" 规划结果:派生 {len(sub_tasks)} 个子任务")
for st in sub_tasks:
print(f" - [{st['role']}] {st['task'][:40]}...")
# --- Step 2:派生执行 ---
print(f"\n📤 [DISPATCHING] 并行启动 {len(sub_tasks)} 个 Sub-Agent...")
completed = await run_all_sub_agents_parallel(sub_tasks, llm)
# --- Step 3:汇总 ---
print("\n📊 [AGGREGATING] 汇总专家分析结果,生成最终报告...")
agg_state = {
"sub_tasks": sub_tasks,
"messages": initial_state["messages"],
"llm": llm,
}
final = financial_aggregating_node(agg_state)
print("\n" + "=" * 60)
print("📋 最终分析报告")
print("=" * 60)
print(final["final_answer"])
print("\n✅ Demo 结束。")
return final["final_answer"]
# ========================
# 运行(在 Jupyter / Python 脚本中直接调用)
# ========================
if __name__ == "__main__":
asyncio.run(financial_analysis_demo())
5.1.6 运行输出示例(LLM 生成的实际内容)
执行流程如下(省略模型推理内容,展示结构):
🖨️ 终端输出:
============================================================
📊 财报分析多智能体 Demo
============================================================
🧠 [PLANNING] 主Agent 分析任务,生成子任务列表...
规划结果:派生 2 个子任务
- [数据分析师] 提取 Q2 营收、净利润、毛利、各业务线收入,分析同比与环比变化
- [竞品研究员] 搜索竞品A、竞品B 和行业均值,对比市场份额、营收、研发投入等指标
📤 [DISPATCHING] 并行启动 2 个 Sub-Agent...
🚀 派生 Sub-Agent [数据分析师],工具:['fetch_financial_data']
🚀 派生 Sub-Agent [竞品研究员],工具:['search_competitor_info']
✅ [数据分析师] 完成,输出 847 字符
✅ [竞品研究员] 完成,输出 612 字符
📊 [AGGREGATING] 汇总专家分析结果,生成最终报告...
============================================================
📋 最终分析报告
============================================================
## Q2 2025 财报分析报告
### 一、核心财务指标概览
- 营收 8.2 亿元,同比增长 12%(行业均值 9%),表现优于大盘。
- 净利润 1.15 亿元,同比下降 5%------「增收不增利」问题突出。
- 毛利率 42%,较去年同期的 45% 下滑 3 个百分点。
### 二、业务线分析
- 云计算收入 3.1 亿(占比 38%),仍是第一大引擎。
- 广告收入 2.5 亿(占比 30%),增速放缓。
- 毛利率下滑主因:云业务基础设施成本上升,拖累整体毛利。
### 三、竞品对标
- 竞品A:市占率 28%,营收 10.5 亿,品牌强但研发投入仅 10%。
- 竞品B:市占率 18%,营收 5.8 亿,技术创新快但尚未盈利。
- 我方市占率约 22%,处于第二梯队------优势是研发投入充足(18%),短板是盈利质量下滑。
### 四、问题与风险
1. **净利润下滑**:运营费用同比增长 12%,挤压利润空间。
2. **毛利率下降**:云业务成本控制需优化。
3. **竞品A 规模压制**:营收差距仍有 28%。
### 五、改进建议
**短期(1-2 季度):**
1. 优化云业务资源利用率,目标将毛利率拉回 44%。
2. 广告业务引入 AI 定向投放,提升单用户价值。
**长期(1-2 年):**
1. 将研发优势转化为产品壁垒,在云原生工具链建立差异化。
2. 通过战略投资或合作补齐渠道短板,缩小与竞品A 的规模差距。
============================================================
✅ Demo 结束。
5.1.7 关键设计要点回顾
| 设计点 | 实践方式 |
|---|---|
| 主Agent 不干活 | 规划节点只生成 sub_tasks 列表,不调用任何数据工具;真正的数据拉取和竞品搜索全由 Sub-Agent 完成。 |
| 按需派生 | 根据任务描述动态决定 role 和 tools:财报分析派「数据分析师 + fetch_financial_data」,竞品对比派「竞品研究员 + search_competitor_info」。 |
| 工具隔离 | 每个 Sub-Agent 只拿到完成自身任务所需的最小工具集,避免全局工具污染。 |
| 并行执行 | asyncio.gather 并行跑两个 Sub-Agent,总耗时 ≈ max(单个 Sub-Agent 耗时),而非串行叠加。 |
| 可替换性 | 把模拟数据 MOCK_FINANCIAL_DATA 换成真实 API(如 Tushare、Wind、内部数仓),代码其余部分无需修改。 |
| 可复用性 | financial_planning_node 和 financial_aggregating_node 稍改 Prompt 即可复用于其他分析场景(如销售分析、用户增长分析)。 |
出。
6. 高级话题:上下文传递与记忆管理
动态派生的痛点之一是上下文断裂:主Agent和Sub-Agent各持一份上下文,容易产生信息孤岛。解决方案如下:
- 最小上下文注入 :主Agent在调用
dispatch_agent时,用压缩算法(如LLM摘要)生成一份简洁的背景摘要作为context字段传递,而非全量复制历史。 - 共享矢量记忆:所有Agent接入同一个向量数据库,Sub-Agent在执行中可按需检索长期记忆(如知识库片段、历史决策),而不是每次由主Agent显式传入。
- 双向结果回灌:Sub-Agent完成后,主Agent主动将结果写回全局记忆,并附上元数据(任务ID、角色、关键结论),供后续其他S
上下文传递的整体数据流
下面的时序图直观展示了主Agent 与 Sub-Agent 之间如何通过「最小上下文注入 + 共享矢量记忆 + 双向结果回灌」三阶段策略避免信息孤岛:
共享矢量数据库 Sub-Agent B Sub-Agent A 上下文压缩器 主Agent 用户 共享矢量数据库 Sub-Agent B Sub-Agent A 上下文压缩器 主Agent 用户 #mermaid-svg-3C9FLUxDX5OPI7wN{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-3C9FLUxDX5OPI7wN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3C9FLUxDX5OPI7wN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3C9FLUxDX5OPI7wN .error-icon{fill:#552222;}#mermaid-svg-3C9FLUxDX5OPI7wN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3C9FLUxDX5OPI7wN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3C9FLUxDX5OPI7wN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3C9FLUxDX5OPI7wN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3C9FLUxDX5OPI7wN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3C9FLUxDX5OPI7wN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3C9FLUxDX5OPI7wN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3C9FLUxDX5OPI7wN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3C9FLUxDX5OPI7wN .marker.cross{stroke:#333333;}#mermaid-svg-3C9FLUxDX5OPI7wN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3C9FLUxDX5OPI7wN p{margin:0;}#mermaid-svg-3C9FLUxDX5OPI7wN .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3C9FLUxDX5OPI7wN text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-3C9FLUxDX5OPI7wN .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3C9FLUxDX5OPI7wN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-3C9FLUxDX5OPI7wN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-3C9FLUxDX5OPI7wN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-3C9FLUxDX5OPI7wN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-3C9FLUxDX5OPI7wN .sequenceNumber{fill:white;}#mermaid-svg-3C9FLUxDX5OPI7wN #sequencenumber{fill:#333;}#mermaid-svg-3C9FLUxDX5OPI7wN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-3C9FLUxDX5OPI7wN .messageText{fill:#333;stroke:none;}#mermaid-svg-3C9FLUxDX5OPI7wN .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3C9FLUxDX5OPI7wN .labelText,#mermaid-svg-3C9FLUxDX5OPI7wN .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-3C9FLUxDX5OPI7wN .loopText,#mermaid-svg-3C9FLUxDX5OPI7wN .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-3C9FLUxDX5OPI7wN .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-3C9FLUxDX5OPI7wN .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-3C9FLUxDX5OPI7wN .noteText,#mermaid-svg-3C9FLUxDX5OPI7wN .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-3C9FLUxDX5OPI7wN .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3C9FLUxDX5OPI7wN .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3C9FLUxDX5OPI7wN .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3C9FLUxDX5OPI7wN .actorPopupMenu{position:absolute;}#mermaid-svg-3C9FLUxDX5OPI7wN .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-3C9FLUxDX5OPI7wN .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3C9FLUxDX5OPI7wN .actor-man circle,#mermaid-svg-3C9FLUxDX5OPI7wN line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-3C9FLUxDX5OPI7wN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发送复杂任务请求解析任务,生成 sub_tasks调用摘要压缩历史上下文返回精简背景摘要dispatch_agent(role, task, context=摘要)dispatch_agent(role, task, context=摘要)按需检索长期记忆/知识库片段返回相关上下文结合注入上下文与检索结果执行任务返回结果 + 关键结论按需检索长期记忆/知识库片段返回相关上下文结合注入上下文与检索结果执行任务返回结果 + 关键结论将两个 Sub-Agent 的结果写入全局记忆汇总所有结果,合成最终答案返回最终响应
ub-Agent或主Ag
下面的流程图从主Agent 视角展示了超时/异常检测的完整决策链,涵盖重试、降级与环境重构:
#mermaid-svg-DoephsPostUGgDxG{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-DoephsPostUGgDxG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DoephsPostUGgDxG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DoephsPostUGgDxG .error-icon{fill:#552222;}#mermaid-svg-DoephsPostUGgDxG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DoephsPostUGgDxG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DoephsPostUGgDxG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DoephsPostUGgDxG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DoephsPostUGgDxG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DoephsPostUGgDxG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DoephsPostUGgDxG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DoephsPostUGgDxG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DoephsPostUGgDxG .marker.cross{stroke:#333333;}#mermaid-svg-DoephsPostUGgDxG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DoephsPostUGgDxG p{margin:0;}#mermaid-svg-DoephsPostUGgDxG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DoephsPostUGgDxG .cluster-label text{fill:#333;}#mermaid-svg-DoephsPostUGgDxG .cluster-label span{color:#333;}#mermaid-svg-DoephsPostUGgDxG .cluster-label span p{background-color:transparent;}#mermaid-svg-DoephsPostUGgDxG .label text,#mermaid-svg-DoephsPostUGgDxG span{fill:#333;color:#333;}#mermaid-svg-DoephsPostUGgDxG .node rect,#mermaid-svg-DoephsPostUGgDxG .node circle,#mermaid-svg-DoephsPostUGgDxG .node ellipse,#mermaid-svg-DoephsPostUGgDxG .node polygon,#mermaid-svg-DoephsPostUGgDxG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DoephsPostUGgDxG .rough-node .label text,#mermaid-svg-DoephsPostUGgDxG .node .label text,#mermaid-svg-DoephsPostUGgDxG .image-shape .label,#mermaid-svg-DoephsPostUGgDxG .icon-shape .label{text-anchor:middle;}#mermaid-svg-DoephsPostUGgDxG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DoephsPostUGgDxG .rough-node .label,#mermaid-svg-DoephsPostUGgDxG .node .label,#mermaid-svg-DoephsPostUGgDxG .image-shape .label,#mermaid-svg-DoephsPostUGgDxG .icon-shape .label{text-align:center;}#mermaid-svg-DoephsPostUGgDxG .node.clickable{cursor:pointer;}#mermaid-svg-DoephsPostUGgDxG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DoephsPostUGgDxG .arrowheadPath{fill:#333333;}#mermaid-svg-DoephsPostUGgDxG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DoephsPostUGgDxG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DoephsPostUGgDxG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DoephsPostUGgDxG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DoephsPostUGgDxG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DoephsPostUGgDxG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DoephsPostUGgDxG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DoephsPostUGgDxG .cluster text{fill:#333;}#mermaid-svg-DoephsPostUGgDxG .cluster span{color:#333;}#mermaid-svg-DoephsPostUGgDxG 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-DoephsPostUGgDxG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DoephsPostUGgDxG rect.text{fill:none;stroke-width:0;}#mermaid-svg-DoephsPostUGgDxG .icon-shape,#mermaid-svg-DoephsPostUGgDxG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DoephsPostUGgDxG .icon-shape p,#mermaid-svg-DoephsPostUGgDxG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DoephsPostUGgDxG .icon-shape .label rect,#mermaid-svg-DoephsPostUGgDxG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DoephsPostUGgDxG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DoephsPostUGgDxG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DoephsPostUGgDxG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
超时
步数爆炸
工具调用僵死
否
是
有
无
主Agent 监控 Sub-Agent 运行
检测到异常?
继续等待 / 正常回收结果
异常类型判断
标记 status=timeout
记录 elapsed_seconds
标记 status=timeout
记录 agent_step_count
记录 last_tool_call_time
与 tool_name
是否超过 max_retries?
重试:重新 dispatch_agent
retry_count += 1
已有部分结果可用?
降级处理:基于部分结果
汇总输出,标记风险
降级回复:告知用户当前
无法完成,建议简化问题
进入 AGGREGATING 状态
ent查询。
7. 容错与监控
- 超时与重试:为每个Sub-Agent设置TTR(最大推理步数或时间),超时则强制终止并让主Agent根据已有部分结果决策重试或降级。
- 工具权限隔离 :避免将整个工具集传递给Sub-Agent,采用白名单模式(如示例中的
tools列表)。对于高风险工具(如删除数据库、发送邮件),可引入双重确认或限制在特定沙箱环境。 - 可观测性 :为每次派生生成
trace_id,贯穿主Agent和Sub-Agent,将调用链路记录到日志平台或LangSmith,便于回溯"哪个子Agent因为什么原因出错"
7.1 常见错误与排查指南
在多智能体动态派生架构中,即使状态机设计得当,生产环境仍可能出现几类高频故障。下面列出 4 类典型问题及应对策略------每类问题都标注了**排查用关键
| 问题类型 | 典型现象 | 关键日志字段 | 修复动作 |
|---|---|---|---|
| 无限循环 / 步数爆炸 | Sub-Agent 不停调用相同工具,status 永久 running,主Agent 在 WAITING 超时 |
trace_id、agent_step_count、tool_name、tool_input、tool_output、status |
设置 max_steps 硬限制;System Prompt 强化停止条件;增加进度感知机制 |
| 工具调用超时 / 僵死 | 单个 dispatch_agent 调用迟迟不返回,主Agent 整体卡死 |
last_tool_call_time、tool_name、error_code(Timeout / RateLimitError)、elapsed_seconds |
双层超时(task + tool);异步 + 熔断;备用工具链降级 |
| 上下文传递丢失 / 信息孤岛 | Sub-Agent 忽略关键约束,专家结论矛盾但主Agent 未发现 | original_user_request、context_passed、context_length_before/after、aggregation_prompt |
结构化上下文拆分(constraints / background / history);关键约束清单锁定;交叉验证 + 仲裁 |
| 主Agent 规划失误 / 派生不合理 | 派生无帮助的角色;任务过度拆分导致成本暴增 | planning_reasoning、sub_tasks_list、dispatch_count、final_answer vs 用户请求相关性 |
强化规划 Prompt(含反例与边界条件);派生审核(人工/规则自动拦截);限流与预算设定;AB 记忆反馈闭环 |
日志字段**,方便你快速接入监控系统。
问题一:子Agent 无限循环 / 步数爆炸
典型现象 :Sub-Agent 不停调用工具却不产出 final_report,sub_tasks 中的状态永久停留在 running,主Agent 在 WAITING 节点超时后被迫中断。
排查步骤:
- 检查该子 Agent 的
trace_id对应的完整调用日志(可在 LangSmith / 自建平台按trace_id聚合)。 - 统计 ReAct 循环步数:观察
tool_calls序列,判断 LLM 是否在重复执行相同工具且无信息增益(比如反复搜索同一关键词)。 - 检查 System Prompt 中是否缺少明确的停止条件------例如未写"当你已经有了足够信息,请立即调用
final_report"。 - 检查工具返回内容是否被 LLM 误判为"缺失信息",例如 API 返回空列表但 LLM 不认为这已经是答案。
日志关键字段:
trace_id:串联整个子 Agent 链路sub_task_id:对应SubTask.task_idagent_step_count:当前步数tool_name/tool_input/tool_output:每次工具调用的入参与结果status:running/done/timeout/failed
修复建议:
- 硬限制 :在
dispatch_agent或 Sub-Agent 运行时设置max_steps(如 15 步),超步后强制将status置为timeout并注入一条兜底消息:"已达到最大步数,必须立即给出目前最好的答案"。 - System Prompt 强化 :添加指令:"如果你连续 3 次调用同一工具且返回内容相似,应基于已有信息结束任务,调用
final_report"。 - 增加"进度感知":在每步推理后记录简要进度,若连续 2 步进度不变则主动询问主Agent 是否继续。
问题二:工具调用超时 / Sub-Agent 僵死
典型现象 :单个 dispatch_agent 调用迟迟不返回,主Agent 整体请求卡死。常见于 Sub-Agent 调用的下游 API 响应慢、工具内部网络波动、或 LLM API 速率限制。
排查步骤:
- 从主Agent 日志中找到
dispatching_node的出口日志,确认sub_tasks列表中哪个子任务status仍为pending或running。 - 检查该子任务的工具列表,确认最近一次
tool_call的时间戳------若与当前时间差距超过超时阈值,说明卡在外部工具。 - 检查该工具的隔离设置:是否使用异步超时?是否为该工具设置了
request_timeout? - 查看 LLM 供应商的速率限制日志,判断是否因 QPS 过高被限流导致模型推理挂起。
日志关键字段:
sub_task_id+statuslast_tool_call_time:最后一次工具调用的时间戳tool_name+error_code(如Timeout、RateLimitError)elapsed_seconds:从发出到当前已用时间
修复建议:
- 双层超时 :为 Sub-Agent 设置整体
task_timeout(如 45 秒),为每个单独工具调用设置tool_call_timeout(如 10 秒)。超时后抛出特定异常,由dispatching_node捕获并标记timeout。 - 异步 + 熔断 :将
dispatch_agent改为asyncio.wait_for方式调用,配合信号量控制并发;对常超时的工具启用断路器,连续失败 N 次后临时降级(返回缓存值或空结果)。 - 备用工具链:如果某工具因下游故障频繁超时,可在工具注册表中预设降级工具(如主查询用 SQL,降级用搜索引擎),由 Sub-Agent 自行切换。
问题三:上下文传递丢失 / 信息孤岛
典型现象:Sub-Agent 执行结果看似合理,但忽略了用户原始请求中的关键约束(如"只分析华东地区"),或是 Sub-Agent 之间结论矛盾但主Agent 未能发现。本质是主Agent 在派生时只传递了任务描述,没有附带足够的背景信息。
排查步骤:
- 在
planning_node日志中查看为每个 Sub-Agent 生成的context字段内容:是否仅有一句话摘要?是否丢失了用户原始输入的关键约束? - 检查
dispatch_agent实际发出的 System Prompt 全文,对比主Agent 看到的消息历史,确认遗漏了哪些信息。 - 如果使用了上下文压缩(摘要),查看压缩前后的原文与摘要对比,判断摘要模型是否丢弃了关键细节。
- 查看
AGGREGATING节点的 prompt,是否明确要求"发现并指出各专家结论中的矛盾"。
日志关键字段:
original_user_request:用户原始输入全文context_passed:实际传递给 Sub-Agent 的上下文文本context_length_before/context_length_after:压缩前后字符数sub_agent_result:Sub-Agent 返回的完整答案aggregation_prompt:汇总时使用的 prompt 全文
修复建议:
- 结构化上下文 :将传递给 Sub-Agent 的
context拆为constraints(必须遵守的约束)、background(背景知识)、relevant_history(相关历史片段),而不是一段自由文本。 System Prompt 中添加对应占位符。 - 关键信息锁定 :让主Agent 在
PLANNING阶段显式输出"关键约束清单",每个 Sub-Agent 的 prompt 中强制包含该清单。 - 交叉验证 :在
AGGREGATING节点中加入"矛盾检测"步骤,若两个专家结论相左,主动回到PLANNING追加一个仲裁 Sub-Agent。
问题四:主Agent 规划失误 / 派生角色不合理
典型现象:主Agent 派生出一个对当前任务毫无帮助的角色(例如用户问"如何优化 MySQL 查询",却派生了一个"前端性能优化"专家);或者把明显可独立完成的任务错误拆分成 10 个子任务,导致成本暴增。
排查步骤:
- 查看
PLANNING节点的 LLM 原始输出(JSON 决策),看reasoning字段是否自洽。 - 检查主Agent 的 System Prompt 中角色规划指令是否清晰:"只有当前问题确实需要另一个专业领域的知识时才派生"。
- 检查
sub_tasks列表中各子任务的role和task是否高度重叠(多个角色描述相似),说明 LLM 在"过度拆分"。 - 统计
dispatch_agent调用次数:若单个请求触发了 > 5 次派生,大概率规划不合理。
日志关键字段:
planning_reasoning:主Agent 给出派生理由sub_tasks_list(含role+task)dispatch_count:本次请求的总派生次数final_answervsuser_request的相关性评分(可离线评估)
修复建议:
- 强化规划 Prompt:添加反例与边界条件:"如果你可以独立完成该任务(如解决一般编程问题、解释概念),不要派生;派生仅用于需要调用特有工具、或确实需要不同领域深度分析时。"
- 派生审核(人工/自动) :在
DISPATCHING → WAITING之间插入一个审核节点(LangGraph 的interrupt),允许在开发阶段人工审查角色列表与任务描述,生产环境可改为基于规则(如规则:严禁派生"哲学家""诗人"等非功能性角色)自动拦截。 - 限流与预算:设定单请求最大派生数(如 5 个),超出则自动降级为"将任务列表合并后由主Agent 自行处理"。
- AB 记忆:记录每次规划决策与实际用户满意度,形成反馈闭环,定期用优质案例微调规划 prompt。
。
8. 工程落地注意事项
理论设计再优雅,到了生产环境仍会遇到一系列现实挑战。以下是动态派生架构落地时必须重点关注的工程问题。
8.1 成本控制
每次动态派生都意味着新的一轮 LLM 调用,成本容易被忽视:
- Token 预算管理:主Agent 的 System Prompt 已占用大量 token,派生时传递上下文若不加控制,单次调用轻松突破 10k token。建议为每次派生设定 token 上限,超出部分先压缩摘要再传递。
- 模型级联:简单任务派轻量模型(如 GPT-4o-mini),复杂推理才启用强模型,避免"杀鸡用牛刀"。
- 结果缓存:对于相似子任务(如"查询某 API 文档"),可将 Sub-Agent 的输入-输出对写入 Redis,命中缓存直接返回,跳过 LLM 推理。
- 并行 vs 串行的成本权衡:并行虽快,但同时持有多个 LLM 连接会成倍拉高 QPS;对非紧急任务可改为串行队列 + 异步通知,平摊瞬时负载。
8.2 延迟与并发控制
用户可感知的响应时间 = 主Agent 规划时间 + max(各 Sub-Agent 执行时间) + 汇总时间。优化方向:
- 流式输出:主Agent 拿到 Sub-Agent 的首个 token 就立刻开始汇总,而不是等全部完成。
- 并发上限:用信号量控制最大并发 Sub-Agent 数(如 5 个),超出的排队等待,防止突刺打垮 LLM API。
- 超时熔断:每个 Sub-Agent 设置绝对超时(如 30s),超时即返回部分结果并标记为"不完整",由主Agent 决定是否重试。
- 异步非阻塞设计:主Agent 应采用 async/await 或协程模型,避免一个慢 Sub-Agent 阻塞整个调用链。
8.3 状态一致性与幂等
Sub-Agent 可能执行有副作用的操作(写数据库、调支付接口),一旦重试就可能重复执行:
- 幂等设计 :为每次派生生成唯一
task_id,Sub-Agent 执行前检查是否已处理过,已处理则直接返回缓存结果。 - 事务边界隔离:严格限制 Sub-Agent 只能调用只读工具;写操作必须交回主Agent 统一执行,并由主Agent 做最终一致性保障。
- 补偿机制:对于必须由 Sub-Agent 执行的写操作(如发邮件),需同时注册补偿逻辑(撤销/回滚),形成 Saga 模式。
8.4 调试与可测试性
动态派生的不确定性让传统单测难以覆盖:
- 确定性回放:记录每次派生的完整快照(输入 prompt、工具调用序列、模型响应),存入本地日志或 LangSmith,支持离线回放分析。
- 模拟 Sub-Agent:测试主Agent 的调度逻辑时,用 mock 替换真实 Sub-Agent 调用,返回预定结果,验证规划-汇总链路是否正确。
- 分段可观测:不仅记录最终成功/失败,还要记录"主Agent 生成了什么角色描述""Sub-Agent 调用了哪些工具""每条工具调用的耗时",形成完整调用拓扑。
8.5 部署与资源调度
- 无状态 Sub-Agent:Sub-Agent 应设计为无状态函数,所需上下文全部由主Agent 注入,方便随时扩缩容与重启。
- 资源隔离:高风险 Sub-Agent(如执行用户提交的代码)必须跑在沙箱容器或 WebAssembly 环境,与主系统网络隔离。
- 灰度与回滚:动态派生的 prompt 模板与工具集应支持热更新与版本管理。新版本先灰度 10% 流量,观察成功率与延迟后再全量推送。
- 模型降级:当主力模型不可用(配额耗尽、区域故障)时,自动降级到备用模型或本地部署的开源模型,保证系统可用性。
8.6 安全与合规
- Prompt 注入防御:Sub-Agent 的任务描述来自用户输入时,必须做输入清洗,防止恶意指令诱导 Sub-Agent 绕过安全护栏。
- 工具权限最小化:每个 Sub-Agent 只被授予完成当前子任务所需的"最小工具集",而不是全局工具列表。
- 敏感数据脱敏:传递给 Sub-Agent 的上下文中,自动替换手机号、身份证号等敏感信息,Sub-Agent 不应接触原始明文。
- 审计追踪:每次派生与工具调用都有完整日志,满足合规审查要求(如 SOC2、GDPR)。
8. 总结
多智能体系统的动态派生架构,本质上是通过元认知 + 工具抽象 让主Agent获得"创建临时专家"的能力。主Agent通过专用工具(如 dispatch_agent)触发创建,传入角色、任务和工具列表,Sub-Agent在隔离环境中执行,最后将结果带回。这一设计让大模型应用摆脱固定流水线,具备动态组合、按需伸缩、并行探索的自治特性。
在实际落地时,需重点设计好:派生触发的决策粒度 、上下文压缩与传递策略 、工具安全隔离 和生命周期管理。掌握这些核心点,你就能构建出灵活而稳健的多智能体系统,从容应对开放式复杂任务。
扩展阅读建议:结合LangGraph的
Send机制与Map-Reduce模式的并行Agent;研究AutoGen、CrewAI等框架如何封装动态多Agent协作,从而获得更具体的工程灵感。