何时需要多智能体
多智能体本质是一笔交易:用通信与协调的成本,换取专业性、并行与职责隔离。先算清这笔账,再决定要不要拆。
干净的专业上下文:每个成员只看自己领域的工具与规则,不被别人的长尾知识污染。真并行:独立子任务同时执行。职责隔离:不同成员可用不同模型、不同权限、独立测试。
协调开销:路由、交接、汇报都是额外 LLM 调用,token 与延迟经常翻倍。错误传播:上游理解错,下游全错。调试复杂度:trace 从一条线变一棵树,归因难度陡增。
第五章末尾给出过四个升级信号,这里展开成可执行的判定:对照你的单智能体,逐条检查——
- 工具域冲突:系统提示里塞着两三套互相干扰的工具说明(销售话术与法务条款),模型混用频发。
- 上下文污染:子任务的中间结果(长检索、大文件)把主对话窗口挤爆,裁剪又丢关键信息。
- 并行需求:任务天然可拆(调研五个独立主题),串行执行慢到不可接受。
- 职责差异:不同环节需要不同强度的模型(贵的做裁决,便宜的做执行)或不同权限(只读与可写分离)。
Anthropic 在其多智能体研究系统的复盘中给出关键经验:多智能体在读多写少的开放任务(调研、信息综合)上收益最大——子任务可并行、结论可验证;而在精确定量的封闭任务上经常不划算。同时他们明确提示:多智能体的 token 消耗可达单智能体对话的数倍,没有并行收益就别拆。
任何"多智能体提升了效果"的结论都必须有单智能体对照。工程顺序:把单智能体(第五章)优化到瓶颈明确可归因(工具太多?上下文太挤?串行太慢?),再按信号拆分。跳过基线直接上多智能体,是这类项目失败的第一大原因。
拓扑结构:五种编排模式
谁可以调用谁、信息怎么流动,决定了系统的性格。五种模式覆盖了绝大多数场景,从最可控到最自由。
| 模式 | 结构 | 适用 | 主要风险 |
|---|---|---|---|
| Supervisor | 中心路由,成员向中心汇报 | 任务可分解、需要统一裁决(调研报告、内容生产) | 中心瓶颈;路由错误波及全局 |
| 层级 | Supervisor 套 Supervisor | 成员很多(十个以上),按域分组 | 层间信息损耗;配置复杂 |
| 网状 | 成员任意互调 | 探索性协作、研究型系统 | 循环调用、死锁、成本失控 |
| 流水线 | 固定顺序传递 | 阶段固定的生产流程 | 这其实是工作流,别硬叫多智能体 |
| Handoff | 控制权整体转移 | 对话型场景:客服转接、前台转专家 | 权限随交接传递需要设计 |
从 Supervisor 起步:可控、可调试、模式成熟(下一节完整实现)。对话型产品加 Handoff。网状只在你有明确的探索收益与严格预算时考虑。层级是成员规模撑大了之后的演化方向,不是起点。
通信机制:黑板与交接
拆出多个智能体之后,第一个要回答的问题不是"谁更聪明",而是"它们怎么说话"。信息在成员之间每流转一次都有损耗,通信设计直接决定最终质量。
工程上有两种基本机制,绝大多数系统是它们的混合:黑板模式(共享状态)让所有成员读写同一份状态,彼此解耦,谁都不需要知道别人的存在;交接模式(消息传递)让成员把结构化消息直接交给指定对象,信息精准但耦合更高。LangGraph 的 StateGraph 天然是黑板:状态由 reducer 统一管理写入、人人可读;结构化交接则用 Pydantic 模型约束,两种机制可以叠加。
黑板模式的写入纪律
黑板的核心问题不是"能不能写",而是"谁写了什么"。用 operator.add 做追加式 reducer 时,每个成员的输出都会永久留在黑板上,一旦有人把原始工具轨迹整段写进去,状态就开始滚雪球。状态 schema 要把"追加的对话流"与"覆盖的交付物"分成两类键:
import operator
from typing import Annotated, TypedDict
class TeamState(TypedDict):
# 黑板:所有成员的汇报自动追加,谁都不覆盖谁
messages: Annotated[list, operator.add]
# 结构化交付物:后写覆盖先写,由收尾节点维护
report: str
纪律只有一条:成员汇报结论,不汇报过程。工具调用与中间推理发生在成员自己的子图会话里(第二章的 ReAct 循环),只有最终产出以一条干净的消息追加到黑板。过程与产出分离,是黑板模式能长期运行的前提。
结构化交接
当控制权要从一个成员转移到另一个成员时,口头式的"你继续吧"是事故之源。用 Pydantic 把交接内容固化成一张"交接单",缺什么字段在编译期就看得见:
from pydantic import BaseModel, Field
class Handoff(BaseModel):
"""控制权转移时必须携带的交接单"""
to: str = Field(description="目标成员,只能是当前在线名单中的一个")
task: str = Field(description="一句话说清要对方完成什么")
context: str = Field(
description="对方需要但无法自行获取的背景:用户是谁、已经决定了什么"
)
deliverable: str = Field(
description="期望产出形式:结论摘要 / 结构化数据 / 待选项"
)
| 信息 | 该不该传 | 理由 |
|---|---|---|
| 任务目标与完成标准 | 必传 | 没有它,对方只能靠猜 |
| 用户身份与偏好 | 必传(摘要) | 影响语气与格式,但只传结论不传原始对话 |
| 关键已定决策 | 必传 | 防止对方推翻已经裁决过的问题 |
| 完整对话历史 | 谨慎 | 大多数交接只需要摘要;全量历史是上下文炸弹 |
| 工具原始返回 | 不传 | 传结论与引用,原始数据留在成员自己的会话里 |
| 成员的中间推理 | 不传 | 对下游没有行动价值,只占 token |
判断标准:对方执行任务时会不会回过头来问你。会,说明 context 缺了;从不问、且处理的信息从未超过一屏,说明还可以再精简。交接单里每多一段无关背景,对方跑偏的概率就多一分。
典型事故:三个成员都把完整 ReAct 轨迹(包括每次工具调用与原始返回)追加进 messages,第四轮时主管的输入已接近 10 万 token,路由质量随上下文膨胀一路下滑,账单随之起飞。写入前问一句:这条信息对"下一个读黑板的人"有行动价值吗?
Supervisor 编排实战
完整实现生产中最常用的模式:调研员、主笔、审稿人三个成员加一个主管,用 LangGraph 1.x 组装"调研、写作、审稿、定稿"的闭环。这是五种拓扑里可控性与表达力的最佳平衡点。
第一步:定义成员
每个成员用 create_agent 创建,只带自己领域的工具与规则。注意系统提示的写法:能力边界、输出格式、禁区,一个都不能少:
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.prebuilt import create_agent
llm = init_chat_model("openai:gpt-4o-mini")
@tool
def web_search(query: str) -> str:
"""搜索公开网络信息,返回带来源链接的摘要。输入应是聚焦的搜索词。"""
... # 接 Tavily / SerpAPI 等搜索后端
researcher = create_agent(
llm,
tools=[web_search],
system_prompt=(
"你是调研专员,为技术报告收集事实。\n"
"1. 每条事实必须附来源 URL,找不到就明说,不许编造。\n"
"2. 汇报格式:按主题分组的事实清单,每条一行。\n"
"3. 只调研:不写结论段,不评价事实。"
),
)
writer = create_agent(
llm,
tools=[],
system_prompt=(
"你是主笔,把调研清单写成正式报告。\n"
"1. 只使用调研记录中出现的事实,缺料处标注[待补充]。\n"
"2. 关键论断后标注来源编号,如 [1]。\n"
"3. 不做任何新的检索。"
),
)
critic = create_agent(
llm,
tools=[],
system_prompt=(
"你是审稿人,只挑错不重写。输出固定两节:\n"
"## 通过项\n## 问题清单(每条:位置 / 问题 / 建议)\n"
"没有问题时,问题清单一节写'无'。"
),
)
create_agent 返回的是完整的 LangGraph 图(第二章)。它作为节点挂进主管图后,工具调用循环发生在子图内部,主管看到的只是成员追加到黑板的最终汇报。这就是职责隔离在 trace 上的样子:主管的 trace 树里,每个成员节点可以单独展开、单独重放。
第二步:团队状态与路由决策
黑板沿用第三章的 TeamState;路由决策用结构化输出收窄成枚举加理由,杜绝"派给不存在的成员"这类非法路由:
import operator
from typing import Annotated, Literal, TypedDict
from pydantic import BaseModel, Field
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
class TeamState(TypedDict):
messages: Annotated[list, operator.add] # 黑板(第三章)
report: str # 交付物,由收尾节点填充
class Routing(BaseModel):
"""主管的路由决策"""
next: Literal["researcher", "writer", "critic", "FINISH"]
reason: str = Field(description="选择该成员的一句话理由,供人工审查")
SUPERVISOR_PROMPT = """你是团队主管,负责在成员间路由任务。
成员能力:
- researcher:联网调研,产出带来源的事实清单
- writer:把事实清单写成报告草稿
- critic:审稿,输出通过项与问题清单
路由纪律:
1. researcher 尚未产出事实清单之前,不允许派 writer。
2. writer 产出草稿后,必须派 critic。
3. critic 列出实质问题(问题清单不是"无")时回到 writer,最多修改 2 轮。
4. critic 通过,或修改轮数用尽,选 FINISH。"""
第三步:主管节点
主管不带工具、不做检索,只做一件事:读黑板,决定下一个成员,并把理由写回黑板。用 Command(goto=...) 动态路由(第二章同款机制),FINISH 时跳到 END:
router = llm.with_structured_output(Routing)
def supervisor(
state: TeamState,
) -> Command[Literal["researcher", "writer", "critic", "__end__"]]:
decision = router.invoke([
{"role": "system", "content": SUPERVISOR_PROMPT},
*state["messages"],
])
if decision.next == "FINISH":
return Command(goto=END, update={"messages": ["主管:任务完成,定稿。"]})
# 把路由理由写回黑板:下一个成员知道为什么轮到自己,事后可回放
return Command(
goto=decision.next,
update={"messages": [f"主管派活 -> {decision.next}:{decision.reason}"]},
)
第四步:装配与运行
成员节点干完活统一回流到主管(add_edge(name, "supervisor")),形成星形循环。运行时用 stream_mode="updates" 能看到每一跳谁在动:
builder = StateGraph(TeamState)
builder.add_node("supervisor", supervisor)
members = {"researcher": researcher, "writer": writer, "critic": critic}
for name, agent in members.items():
builder.add_node(name, agent)
builder.add_edge(name, "supervisor") # 成员干完活必回主管
builder.add_edge(START, "supervisor")
graph = builder.compile()
inputs = {"messages": [("user",
"写一份 LangGraph 1.x 学习路线报告,面向有 LangChain 基础的工程师")]}
config = {"recursion_limit": 60, "thread_id": "team-001"} # 一轮=主管+成员两跳
for chunk in graph.stream(inputs, config, stream_mode="updates"):
print(chunk) # 每跳打印一次:谁在执行、黑板新增了什么
# 最终报告:黑板上 writer 的最后一条定稿消息
# 需要结构化交付时,加一个收尾节点从 messages 抽取并写入 report
| SUPERVISOR_PROMPT 要点 | 做法 | 反例 |
|---|---|---|
| 能力清单 | 一句话说清每个成员会什么、不会什么 | "都是聪明助手,看着办" |
| 顺序约束 | 显式写明前置条件:没有调研不许写作 | 指望模型自行领悟流程 |
| 终止条件 | FINISH 的判据 + 修改轮数上限 | "满意了就结束" |
| 路由留痕 | reason 写入黑板,出错可回放归因 | 静默路由,错了无从查起 |
supervisor 循环里每一跳都计入递归深度:一轮"主管 + 成员"就是两跳,默认上限 25 只够 12 轮循环,调研类任务很容易触顶抛 GraphRecursionError。显式设置并做成配置项;同时把"最多修改 2 轮"写进主管提示做双保险,程序性上限与提示性上限各拦一道。
路由是 LLM 决策,就有错判概率:跳过调研直接派写作、审稿未通过就 FINISH。三个兜底:结构化输出的 Literal 枚举保证路由目标永远合法(不会派出不存在的成员);顺序约束写进提示后,在测试集上专跑"跳步"用例验证;关键流程再加一道确定性校验节点,用代码检查前置条件,而不是全靠模型自觉。
Handoff 与 Swarm
对话型产品不需要常驻主管:更自然的模式是控制权随对话流动。账务专员聊到故障排查,直接把整个会话交给技术支持。这就是 Handoff,也是 Swarm 类框架的全部核心。
OpenAI 在 2024 年开源的 Swarm 框架(官方定位是教学实验)把多智能体收敛成三个原语:成员 = 指令 + 工具,交接 = 一次工具调用,当前持有控制权的成员决定一切。没有主管,没有路由中心,转接本身就是普通工具。这个模型简洁得惊人,也非常适合客服这类线性对话。LangGraph 里可以用"返回 Command 的工具"完整复刻它。
把交接做成工具
关键机制是 Command(goto=..., graph=Command.PARENT):成员子图里的工具不返回字符串,而是返回一条转移指令,让控制权跳出当前子图、在父图里直接跳到目标成员。工具参数里带上第三章的交接单:
from typing import Annotated
from langchain_core.messages import ToolMessage
from langchain.tools import tool
from langgraph.prebuilt import InjectedToolCallId
from langgraph.types import Command
def make_handoff(target: str, description: str):
"""生成一个转接工具:谁装上它,谁就能把控制权交给 target 成员"""
@tool(description=description)
def handoff(
task: str, # 交接单核心字段
tool_call_id: Annotated[str, InjectedToolCallId], # 框架自动注入
) -> Command:
return Command(
goto=target, # 父图中的目标成员节点
graph=Command.PARENT, # 跳出当前子图,在父图里路由
update={
"messages": [
ToolMessage(f"已转接给 {target}。", tool_call_id=tool_call_id),
{"role": "user", "content": task}, # 对方接手的任务说明
]
},
)
return handoff
装配 Swarm
两个成员互相装上对方的转接工具,图里甚至不需要条件边——转移完全由工具调用驱动:
from langgraph.graph import StateGraph, START, MessagesState
billing = create_agent(
llm,
tools=[make_handoff("tech", "当问题转向产品故障、报错、配置排查时,转接技术支持")],
system_prompt="你是账务专员,只处理账单、退款、发票问题。超出职责立即转接。",
)
tech = create_agent(
llm,
tools=[make_handoff("billing", "当问题转向账单金额、退款进度时,转接账务专员")],
system_prompt="你是技术支持,只处理故障排查与配置问题。超出职责立即转接。",
)
builder = StateGraph(MessagesState) # 共享对话状态:会话历史全程连续
builder.add_node("billing", billing)
builder.add_node("tech", tech)
builder.add_edge(START, "billing") # 前台默认路由:先到账务
swarm = builder.compile()
# 控制权随对话流动:账单问题 -> 转接 -> 故障排查
swarm.invoke({"messages": [("user", "这个月账单多扣了钱,而且 App 一直闪退")]})
| Swarm 原语 | LangGraph 对应 |
|---|---|
| Agent(指令 + 工具) | create_agent(system_prompt=, tools=) |
| handoff 函数 | 返回 Command(goto=, graph=Command.PARENT) 的工具 |
| active_agent 变量 | 图中"当前执行节点",由转移指令驱动 |
| client 把消息发给 active_agent | 消息追加进共享 MessagesState,谁持有控制权谁响应 |
Swarm 没有主管提示,路由知识全部压缩在 handoff 工具的 description 里。写法用"当……时,转接……"的条件句式,并把边界说死("只处理 X,其余一律转接")。描述含糊,转接就会迟到或乱转;改一句描述就能改路由行为,也是它好调试的原因。
Handoff 转移的不只是控制权,还有整个对话历史与既有工具权限。账务专员能看到的退款记录,转接后技术支持同样看得到。三道防线:交接处用代码校验目标成员的权限等级;敏感字段在交接前脱敏;高危工具不挂在成员身上,收敛到带 interrupt 的审批节点(第五章)。
并行与汇聚
多智能体最实在的收益是并行:五个独立子题同时调研,总耗时只有串行的零头。但并行引入两个新问题:怎么派活(fan-out),怎么收拢(fan-in)。
LangGraph 的派活工具是 Send(第二章动态并行同款):一个节点按运行时数据返回多条 Send,每条 Send 生成一个独立分支,各自带着自己的输入跑同一个成员;分支结果通过 reducer(operator.add)自然汇聚。这就是 Anthropic 复盘里点名的 orchestrator-worker 模式:编排者拆任务,工作者并行执行,最后归并:
import operator
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
class ResearchState(TypedDict):
topic: str
subtopics: list[str] # 计划节点拆出的子题
findings: Annotated[list[str], operator.add] # 各分支自动汇聚
report: str
def fan_out(state: ResearchState) -> list[Send]:
"""每个子题派一个调研分支,输入互不干扰"""
return [
Send("researcher", {"topic": state["topic"], "sub": sub})
for sub in state["subtopics"]
]
def researcher_branch(state: dict) -> dict:
# 分支私有输入:只有 topic + sub,看不到其他分支(天然隔离)
result = researcher.invoke({"messages": [
("user", f"只调研子题「{state['sub']}」,产出 5 条带来源的事实")
]})
summary = result["messages"][-1].content # 分支只上抛结论
return {"findings": [f"## {state['sub']}\n{summary}"]}
def aggregate(state: ResearchState) -> dict:
"""收拢:分支结论已按子题分段,交给主笔"""
merged = "\n\n".join(state["findings"]) # reducer 已把各分支结果追加在一起
draft = writer.invoke(f"把以下分组事实综合成报告:\n{merged}").content
return {"report": draft}
builder = StateGraph(ResearchState)
builder.add_node("plan", plan_node) # 拆子题的 LLM 节点
builder.add_node("researcher", researcher_branch)
builder.add_node("aggregate", aggregate)
builder.add_conditional_edges("plan", fan_out, ["researcher"])
builder.add_edge("researcher", "aggregate")
builder.add_edge(START, "plan")
builder.add_edge("aggregate", END)
graph = builder.compile()
graph.invoke(
{"topic": "向量数据库选型", "subtopics": ["主流产品对比", "价格模型", "运维成本"]},
config={"max_concurrency": 3}, # 限制同时在跑的分支数
)
汇聚策略:收拢不是 join 一下就完
分支产出怎么合并,取决于它们的关系是互补、重叠还是竞争:
| 策略 | 做法 | 适用 |
|---|---|---|
| 直接追加 | operator.add 收进列表 | 分支产出天然独立(各自的事实清单) |
| 归并去重 | 聚合节点按 key 合并、剔除重复项 | 多分支结果有重叠(同义事实、同一来源) |
| 裁决投票 | 一个裁判 LLM 挑最优或投票合并 | 分支产出互相竞争(多个方案、多个答案) |
| 摘要压缩 | 每个分支先摘要,再拼接总稿 | 分支产出很长,全量拼接会撑爆主笔上下文 |
| 结构化合并 | 各分支输出同一 Pydantic 模型,按键合并 | 结果要进下游结构化流程(第七节) |
拆分要"互斥且尽量穷尽":互相重叠的子题导致重复检索与互相矛盾的结论;遗漏的子题让报告有洞。拆分本身就是一次 LLM 规划任务,值得单独的提示与几个示例,并在测试集上单独评估 plan 节点的输出,它是并行系统的单点质量瓶颈。
一、token 峰值:N 个分支同时跑,瞬时消耗是串行的 N 倍,rate limit 与预算都要按峰值规划;二、失败面变大:一个分支超时或抛错,整图默认跟着失败,要么在分支内 try/except 降级为"该子题暂缺",要么在聚合处容忍缺失;三、不确定性放大:分支完成顺序不可依赖,任何"依赖上一个分支结果"的隐式假设都是错的。max_concurrency 从 3 开始调,别一上来就拉满。
上下文隔离与共享记忆
成员越多,上下文越贵也越乱。隔离让每个成员只看自己需要的;共享记忆让该跨成员存在的(决策、偏好、教训)有统一的安放之处。两者是一体两面。
第一章把上下文污染列为升级信号,这里的解法分三层:隔离(成员的中间过程留在成员内部)、压缩(上抛结论而非过程)、外部化(跨成员的长期信息放进 Store,按需检索)。
| 手段 | 做法 | 效果 |
|---|---|---|
| 子图私有状态 | 成员用 create_agent 子图,内部 MessagesState 不进父图 | 工具轨迹、中间推理对队友天然不可见 |
| 摘要上抛 | 成员结束时输出一条结论消息 | 黑板只按结论量级增长 |
| 结构化接口 | 成员结论强制走 Pydantic 模型 | 父图拿到可校验的数据,不是自由文本 |
| Store 外部化 | 跨成员信息写 store,用命名空间隔离 | 上下文按需检索,不再全量携带 |
结构化上抛:成员的输出接口
把"成员向父图汇报什么"当成 API 设计问题:定义返回模型,而不是放任自由文本。在 TeamState 里加 task: str 与 research: ResearchReport 两个键即可:
from pydantic import BaseModel, Field
class ResearchReport(BaseModel):
"""调研成员对父图的唯一输出接口"""
facts: list[str] = Field(description="事实清单,每条附来源 URL")
gaps: list[str] = Field(description="没查到的关键问题")
confidence: float = Field(description="整体置信度 0-1")
reporter = llm.with_structured_output(ResearchReport)
def researcher_node(state: TeamState) -> dict:
# 1. 私有会话:完整工具循环只存在于这个局部变量里
scratch = researcher.invoke(
{"messages": [("user", f"调研:{state['task']}")]}
)
# 2. 压缩:把长会话压成结构化结论
report = reporter.invoke([
{"role": "system", "content": "把调研记录压成结构化报告,逐条保留来源。"},
*scratch["messages"],
])
# 3. 上抛:父图只拿到结论,不拿到过程
return {
"messages": [f"调研完成:{len(report.facts)} 条事实,置信度 {report.confidence}"],
"research": report,
}
共享记忆:Store 与命名空间
跨成员、跨会话要长期存在的信息(团队决策、用户偏好、踩坑教训)不放黑板,放 Store(第五、二章同款机制)。关键是命名空间纪律:
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
# 任意成员写入:团队级决策,命名空间 (类型, 范围)
store.put(("decision", "team"), "style-guide", {
"text": "报告采用中文书面语,代码示例统一 Python 3.12",
"author": "critic", # 谁写入的,出问题可归因
})
# 任意成员读取(跨线程持久,生产环境换 PostgresStore)
item = store.get(("decision", "team"), "style-guide")
items = store.search(("preference", "user-42")) # 列出命名空间内条目
隔离后主管只看得到结论,看不到过程;过程出错时,你只能看到"置信度偏低"这类间接信号。调试期先全透明:把成员中间消息写进一个 debug 键,或用 stream_mode="messages" 直接旁听,定位稳定后再收紧接口。生产系统常见做法是 debug 键按采样率开启,兼顾成本与可观测性。
共享记忆最常见的腐烂方式是命名空间随手起:("team", "alpha")、("memory", "user")、("notes", "misc") 混用,半年后没人知道哪条该信。定死结构(类型, 范围, 主体),如 ("decision", "team") 与 ("preference", "user", "42");写入带时间戳与来源成员;定期跑维护任务归档过期条目。记忆不治理,多智能体系统会慢慢被自己的历史淹死。
评估与调试
单智能体的 trace 是一条线,多智能体的 trace 是一棵树。评估和调试方法都要跟着升级:除了端到端答案质量,还要单独看协调质量——路由对不对、成员有没有白干活、钱烧在谁身上。
三层指标
评估体系分三层,缺一不可:
| 层 | 指标 | 怎么测 |
|---|---|---|
| 结果层 | 端到端答案质量 | LLM 裁判 + 人工抽检(第三、五章同款方法) |
| 协调层 | 路由正确率、循环深度、交接损耗 | 回放 trace 对照人工标注的关键跳 |
| 资源层 | 单次运行 token、各成员成本占比 | 成本归因脚本,按 trace 聚合 |
协调层是多智能体特有的盲区:一次运行端到端分数不错,但路由来回震荡(writer、critic 反复互踢)、或某个成员的产出从未被下游引用,都是白烧钱的结构病,只看最终分数看不出来。
路由正确率:离线回放
给测试集标注"期望的成员序列",然后回放比对。这是衡量主管提示词质量最直接的指标:
def route_sequence(graph, inputs, config) -> list[str]:
"""跑一遍图,按顺序记录成员跳(过滤掉 supervisor 本身)"""
seq = []
for update in graph.stream(inputs, config, stream_mode="updates"):
seq.extend(update.keys()) # 每个 update 的键 = 本跳执行的节点
return [n for n in seq if n != "supervisor"]
def eval_routing(graph, cases) -> float:
"""cases: [{"inputs": ..., "expect": ["researcher", "writer", "critic", "FINISH"]}]"""
hit = total = 0
for case in cases:
seq = route_sequence(graph, case["inputs"], {"recursion_limit": 60})
for expect, actual in zip(case["expect"], seq):
total += 1
hit += expect == actual
return hit / total # 低于 0.9:先修主管提示,再谈别的优化
成本归因:钱烧在谁身上
把 trace 导出(LangSmith 或 OTel)后按节点聚合 token 用量,一眼看出该优化谁:
from collections import defaultdict
def attribute_tokens(spans) -> dict[str, int]:
"""spans:一次运行的 trace 导出,每条含节点名与 token 用量"""
per_node = defaultdict(int)
for span in spans:
if span["type"] == "llm": # 只统计 LLM 调用
per_node[span["node"]] += span["total_tokens"]
return dict(per_node)
# {'supervisor': 42000, 'researcher': 310000, 'writer': 68000, 'critic': 51000}
# researcher 一家烧掉六成:先优化它的检索策略,而不是全团队换模型
调试四步
- 先看树形结构,再看内容。trace 视图里先确认执行顺序符合设计,再钻进出问题的成员。
- 单独重放成员。成员是子图,可以独立 invoke,拿当时的黑板内容复现它的行为。
- 时间旅行定位分叉点。用 checkpointer 的状态历史找到"从哪一跳开始走歪"(第二章)。
- 旁听消息流。stream_mode="messages" 能实时看到每个成员实际收到了什么——大多数"成员变笨"其实是黑板上多了脏东西。
第四节的 reason 字段不只是给人看的:把每跳 reason 收集起来喂给 LLM 裁判做离线评审,比人工翻 trace 快一个量级。调试时先扫 reason 序列,多数问题一眼可见("调研还没做完就派了 writer")。
分数没掉不代表每个成员都有贡献:可能 writer 一直无视 critic 的意见,critic 是挂名的。做成员消融测试——逐个移除成员跑评估集,指标不掉的成员就是摆设,删掉省钱。团队规模应该由消融证据决定,而不是设计文档。
生产化:预算、并发与降级
多智能体在生产环境的头号问题往往不是"效果不够好",而是成本不可控、延迟不可控、故障面变大。三道闸门:预算熔断、并发上限、降级阶梯。
预算熔断
Anthropic 复盘给出的量级:多智能体系统的 token 消耗约为单智能体对话的 15 倍。没有全局预算的团队等于开了张空白支票。预算检查放在主管路由处,因为它是所有任务的必经之路:
TOKEN_BUDGET = 400_000 # 每次运行的全局预算,按 trace 成本归因校准
def supervisor_with_budget(
state: TeamState,
) -> Command[Literal["researcher", "writer", "critic", "__end__"]]:
# tokens_used:中间件在每个 LLM 调用后累加 usage_metadata(TeamState 加 int 字段)
if state["tokens_used"] > TOKEN_BUDGET:
# 预算耗尽:不再派新任务,让主笔用手头材料收尾,而不是报错
return Command(
goto="writer",
update={"messages": ["主管:预算耗尽,基于已有材料直接收尾,不要再索取新料。"]},
)
decision = router.invoke([
{"role": "system", "content": SUPERVISOR_PROMPT},
*state["messages"],
])
if decision.next == "FINISH":
return Command(goto=END, update={"messages": ["主管:任务完成,定稿。"]})
return Command(
goto=decision.next,
update={"messages": [f"主管派活 -> {decision.next}:{decision.reason}"]},
)
并发与限流
图内用 max_concurrency 限制同时分支数(第六节),但真正的瓶颈通常在图外:N 个成员各自做搜索,消耗的是同一个 API 配额。给每个工具客户端加限流器,或把搜索封装成带全局配额的内部服务;延迟上,并行分支的总延迟等于最慢分支,给分支设超时熔断比无限期等待好得多。
降级阶梯
故障不是会不会来,而是什么时候来。把响应策略预先定成阶梯,每层都有明确的触发条件:
| 层级 | 触发条件 | 行为 |
|---|---|---|
| L0 满配 | 一切正常 | 全员上岗,裁决与执行各用各的模型 |
| L1 省钱 | 成本接近日预算 | 执行成员换便宜模型,主管与审稿保留强模型 |
| L2 减员 | 延迟超 SLO 或个别成员故障 | 砍并行度、跳过 critic,主线保通 |
| L3 单体 | 多智能体链路整体异常 | 回退第五章的单智能体兜底(必须常备) |
| L4 静态 | 模型服务全不可用 | 语义缓存、模板回复、转人工队列 |
L3 的单智能体兜底不是"理论上还在":做成可一键切换的部署单元,每月真实演练一次。没演练过的降级路径等于没有——真出事那天你多半会发现它的工具早已改名、提示词早已过期。演练顺便校准兜底的质量预期,让值班的人知道降级后产品是什么体验。
单次预算熔断只是止损,常态管控靠告警:按 trace 聚合的日均成本、P95 单次成本、成员成本占比突变(researcher 突然占九成,多半是检索在空转)。多智能体的成本异常通常早于质量异常出现,把成本曲线挂进值班大盘,比等用户投诉靠谱得多。
常见陷阱与 FAQ
高频问题与高频事故,逐条给出可操作的答案。
从主管加两个成员起步:一个成员没有分工收益,五个以上先问为什么单智能体撑不住。每个成员都要过消融测试(第八节)——移除后指标明显下降才配留在图里。团队规模是演化出来的,不是规划出来的。
分层配比是主流:主管与审稿这类裁决角色用强模型,调研与执行用便宜模型。但先用同一个模型把流程跑通,再按 trace 成本归因逐个替换——凭感觉配模型基本都会配错,钱常常烧在你以为便宜的那层。
三个常见原因:没有真并行(成员串行排队,只多了路由开销);主管每跳都重读全量黑板(黑板膨胀,见第三章);成员之间来回踢皮球(循环深度超标)。对照 trace 逐项检查:并行分支是否真的同时跑、黑板是否按结论量级增长、循环深度是否超出设计。
不必然。它买的是并行与隔离,不是准确性;准确性来自检索质量与验证环节(第三章的评估指标、critic 的逐条核对)。开放式调研类任务收益最大(Anthropic 复盘的原话是读多写少的任务),封闭精确任务往往得不偿失。先跑对照实验,再下结论。
概念同构:角色、任务、交接、编排。LangGraph 的优势在底层原语可控(状态、检查点、中断、时间旅行)与生产化设施完整;高层框架上手快,但遇到边角问题还是要下探到底层。本手册讲的是通用概念,学扎实了哪个框架都能用。
三道防线:程序性的 recursion_limit 硬熔断;主管提示里写明修改轮数上限;trace 里监控循环深度并告警。成员可互相转接的网状拓扑最容易循环,没有强预算约束就不要上(第一节与第九节都拦过这个坑)。
实战练习与资源
最后一组练习把六章串起来:从单智能体基线出发,一步步搭出可评估、可熔断、可降级的多智能体系统。
延伸资源
一线复盘:15 倍 token、编排者-执行者模式、并行与提示工程教训,本章多处观点的出处 anthropic.com LangGraph:Multi-Agent Concepts
Supervisor、Swarm、层级模式的官方概念文档与最小示例 langchain-ai.github.io OpenAI Swarm
交接原语的最小教学实现,读懂它等于读懂 Handoff github.com Microsoft AutoGen
对话式多智能体的另一派实现,对比着看能理解设计取舍 github.com