LLM 工程手册LLM ENGINEERING

CHAPTER 06系统层 · MULTI-AGENT

Multi-Agent 多智能体

从单打独斗到团队协作:编排拓扑、通信与交接、并行汇聚、上下文隔离、预算与降级。这是手册最后一章,也是最需要克制的一章——先用好一个智能体,再谈一支团队。

开始阅读 难度高级 先修前五章 3小时
06.1

何时需要多智能体

多智能体本质是一笔交易:用通信与协调的成本,换取专业性、并行与职责隔离。先算清这笔账,再决定要不要拆。

你能换到什么

干净的专业上下文:每个成员只看自己领域的工具与规则,不被别人的长尾知识污染。真并行:独立子任务同时执行。职责隔离:不同成员可用不同模型、不同权限、独立测试。

你要付出什么

协调开销:路由、交接、汇报都是额外 LLM 调用,token 与延迟经常翻倍。错误传播:上游理解错,下游全错。调试复杂度:trace 从一条线变一棵树,归因难度陡增。

第五章末尾给出过四个升级信号,这里展开成可执行的判定:对照你的单智能体,逐条检查——

  1. 工具域冲突:系统提示里塞着两三套互相干扰的工具说明(销售话术与法务条款),模型混用频发。
  2. 上下文污染:子任务的中间结果(长检索、大文件)把主对话窗口挤爆,裁剪又丢关键信息。
  3. 并行需求:任务天然可拆(调研五个独立主题),串行执行慢到不可接受。
  4. 职责差异:不同环节需要不同强度的模型(贵的做裁决,便宜的做执行)或不同权限(只读与可写分离)。
来自一线的经验法则

Anthropic 在其多智能体研究系统的复盘中给出关键经验:多智能体在读多写少的开放任务(调研、信息综合)上收益最大——子任务可并行、结论可验证;而在精确定量的封闭任务上经常不划算。同时他们明确提示:多智能体的 token 消耗可达单智能体对话的数倍,没有并行收益就别拆。

先建单智能体基线

任何"多智能体提升了效果"的结论都必须有单智能体对照。工程顺序:把单智能体(第五章)优化到瓶颈明确可归因(工具太多?上下文太挤?串行太慢?),再按信号拆分。跳过基线直接上多智能体,是这类项目失败的第一大原因。

06.2

拓扑结构:五种编排模式

谁可以调用谁、信息怎么流动,决定了系统的性格。五种模式覆盖了绝大多数场景,从最可控到最自由。

Supervisor 中心路由 成员互不直连 层级 Hierarchical 团队套团队 适合大系统 网状 Network 任意互调 最灵活也最难控 流水线 Pipeline 固定顺序传递 其实是工作流 交接 Handoff 控制权接力 对话转接场景 控制力从左到右递减,灵活性递增;生产系统绝大多数落在 Supervisor 与 Handoff 两格
五种编排拓扑。橙色节点表示控制权所在:Supervisor 常驻中心,Handoff 随对话流动。
模式结构适用主要风险
Supervisor中心路由,成员向中心汇报任务可分解、需要统一裁决(调研报告、内容生产)中心瓶颈;路由错误波及全局
层级Supervisor 套 Supervisor成员很多(十个以上),按域分组层间信息损耗;配置复杂
网状成员任意互调探索性协作、研究型系统循环调用、死锁、成本失控
流水线固定顺序传递阶段固定的生产流程这其实是工作流,别硬叫多智能体
Handoff控制权整体转移对话型场景:客服转接、前台转专家权限随交接传递需要设计
选型建议

从 Supervisor 起步:可控、可调试、模式成熟(下一节完整实现)。对话型产品加 Handoff。网状只在你有明确的探索收益与严格预算时考虑。层级是成员规模撑大了之后的演化方向,不是起点。

06.3

通信机制:黑板与交接

拆出多个智能体之后,第一个要回答的问题不是"谁更聪明",而是"它们怎么说话"。信息在成员之间每流转一次都有损耗,通信设计直接决定最终质量。

工程上有两种基本机制,绝大多数系统是它们的混合:黑板模式(共享状态)让所有成员读写同一份状态,彼此解耦,谁都不需要知道别人的存在;交接模式(消息传递)让成员把结构化消息直接交给指定对象,信息精准但耦合更高。LangGraph 的 StateGraph 天然是黑板:状态由 reducer 统一管理写入、人人可读;结构化交接则用 Pydantic 模型约束,两种机制可以叠加。

黑板 · 共享状态 共享状态 messages 读写同一份状态 成员之间互相解耦 交接 · 消息传递 交接单 Handoff 任务 + 背景 + 产出 完成时同样用结构化汇报返回 控制权接力 信息靠消息携带 两种机制可以混用:LangGraph 中共享状态是默认通信层,结构化交接是精准补充
两种通信机制。黑板用双头箭头表示读写;交接模式下橙色节点持有控制权,交接单随控制权一起流动。

黑板模式的写入纪律

黑板的核心问题不是"能不能写",而是"谁写了什么"。用 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,路由质量随上下文膨胀一路下滑,账单随之起飞。写入前问一句:这条信息对"下一个读黑板的人"有行动价值吗?

06.4

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"
        "没有问题时,问题清单一节写'无'。"
    ),
)
每个成员就是一张 ReAct 子图

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
START supervisor 路由 + 留痕 END · FINISH 时定稿 researcher · 调研 产出带来源的事实清单 writer · 主笔 事实清单 → 报告草稿 critic · 审稿 通过项 + 问题清单 Command(goto=...) 实线去 · 虚线回 主管与成员的每一跳都计入 recursion_limit:一轮循环 = 2 跳,默认 25 只够 12 轮
Supervisor 星形循环。主管是唯一的控制点:所有成员经 Command(goto=...) 派出,干完活必回主管,FINISH 时落到 END。
SUPERVISOR_PROMPT 要点做法反例
能力清单一句话说清每个成员会什么、不会什么"都是聪明助手,看着办"
顺序约束显式写明前置条件:没有调研不许写作指望模型自行领悟流程
终止条件FINISH 的判据 + 修改轮数上限"满意了就结束"
路由留痕reason 写入黑板,出错可回放归因静默路由,错了无从查起
recursion_limit 是隐形刹车

supervisor 循环里每一跳都计入递归深度:一轮"主管 + 成员"就是两跳,默认上限 25 只够 12 轮循环,调研类任务很容易触顶抛 GraphRecursionError。显式设置并做成配置项;同时把"最多修改 2 轮"写进主管提示做双保险,程序性上限与提示性上限各拦一道。

主管也会犯错

路由是 LLM 决策,就有错判概率:跳过调研直接派写作、审稿未通过就 FINISH。三个兜底:结构化输出的 Literal 枚举保证路由目标永远合法(不会派出不存在的成员);顺序约束写进提示后,在测试集上专跑"跳步"用例验证;关键流程再加一道确定性校验节点,用代码检查前置条件,而不是全靠模型自觉。

06.5

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 的审批节点(第五章)。

06.6

并行与汇聚

多智能体最实在的收益是并行:五个独立子题同时调研,总耗时只有串行的零头。但并行引入两个新问题:怎么派活(fan-out),怎么收拢(fan-in)。

LangGraph 的派活工具是 Send(第二章动态并行同款):一个节点按运行时数据返回多条 Send,每条 Send 生成一个独立分支,各自带着自己的输入跑同一个成员;分支结果通过 reducer(operator.add)自然汇聚。这就是 Anthropic 复盘里点名的 orchestrator-worker 模式:编排者拆任务,工作者并行执行,最后归并:

plan 拆子题:互斥且穷尽 researcher 分支私有输入 researcher 分支私有输入 researcher 分支私有输入 aggregate 去重 · 归并 · 主笔 fan-out:Send × N fan-in:reducer 追加 分支互不可见,靠 reducer 汇聚;max_concurrency 限制同时在跑的分支数
orchestrator-worker 模式:plan 拆出互斥子题,Send 派出并行分支,reducer 自动汇聚进 aggregate。
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 开始调,别一上来就拉满。

06.7

上下文隔离与共享记忆

成员越多,上下文越贵也越乱。隔离让每个成员只看自己需要的;共享记忆让该跨成员存在的(决策、偏好、教训)有统一的安放之处。两者是一体两面。

第一章把上下文污染列为升级信号,这里的解法分三层:隔离(成员的中间过程留在成员内部)、压缩(上抛结论而非过程)、外部化(跨成员的长期信息放进 Store,按需检索)。

手段做法效果
子图私有状态成员用 create_agent 子图,内部 MessagesState 不进父图工具轨迹、中间推理对队友天然不可见
摘要上抛成员结束时输出一条结论消息黑板只按结论量级增长
结构化接口成员结论强制走 Pydantic 模型父图拿到可校验的数据,不是自由文本
Store 外部化跨成员信息写 store,用命名空间隔离上下文按需检索,不再全量携带

结构化上抛:成员的输出接口

把"成员向父图汇报什么"当成 API 设计问题:定义返回模型,而不是放任自由文本。在 TeamState 里加 task: strresearch: 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");写入带时间戳与来源成员;定期跑维护任务归档过期条目。记忆不治理,多智能体系统会慢慢被自己的历史淹死。

06.8

评估与调试

单智能体的 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 一家烧掉六成:先优化它的检索策略,而不是全团队换模型

调试四步

  1. 先看树形结构,再看内容。trace 视图里先确认执行顺序符合设计,再钻进出问题的成员。
  2. 单独重放成员。成员是子图,可以独立 invoke,拿当时的黑板内容复现它的行为。
  3. 时间旅行定位分叉点。用 checkpointer 的状态历史找到"从哪一跳开始走歪"(第二章)。
  4. 旁听消息流。stream_mode="messages" 能实时看到每个成员实际收到了什么——大多数"成员变笨"其实是黑板上多了脏东西。
路由日志是一等公民

第四节的 reason 字段不只是给人看的:把每跳 reason 收集起来喂给 LLM 裁判做离线评审,比人工翻 trace 快一个量级。调试时先扫 reason 序列,多数问题一眼可见("调研还没做完就派了 writer")。

端到端分数会骗人

分数没掉不代表每个成员都有贡献:可能 writer 一直无视 critic 的意见,critic 是挂名的。做成员消融测试——逐个移除成员跑评估集,指标不掉的成员就是摆设,删掉省钱。团队规模应该由消融证据决定,而不是设计文档。

06.9

生产化:预算、并发与降级

多智能体在生产环境的头号问题往往不是"效果不够好",而是成本不可控、延迟不可控、故障面变大。三道闸门:预算熔断、并发上限、降级阶梯。

预算熔断

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 突然占九成,多半是检索在空转)。多智能体的成本异常通常早于质量异常出现,把成本曲线挂进值班大盘,比等用户投诉靠谱得多。

06.10

常见陷阱与 FAQ

高频问题与高频事故,逐条给出可操作的答案。

团队该有几个成员?

从主管加两个成员起步:一个成员没有分工收益,五个以上先问为什么单智能体撑不住。每个成员都要过消融测试(第八节)——移除后指标明显下降才配留在图里。团队规模是演化出来的,不是规划出来的。

成员们应该用同一个模型吗?

分层配比是主流:主管与审稿这类裁决角色用强模型,调研与执行用便宜模型。但先用同一个模型把流程跑通,再按 trace 成本归因逐个替换——凭感觉配模型基本都会配错,钱常常烧在你以为便宜的那层。

为什么我的多智能体比单智能体还慢?

三个常见原因:没有真并行(成员串行排队,只多了路由开销);主管每跳都重读全量黑板(黑板膨胀,见第三章);成员之间来回踢皮球(循环深度超标)。对照 trace 逐项检查:并行分支是否真的同时跑、黑板是否按结论量级增长、循环深度是否超出设计。

多智能体会让答案更准吗?

不必然。它买的是并行与隔离,不是准确性;准确性来自检索质量与验证环节(第三章的评估指标、critic 的逐条核对)。开放式调研类任务收益最大(Anthropic 复盘的原话是读多写少的任务),封闭精确任务往往得不偿失。先跑对照实验,再下结论。

该用 LangGraph 手搭,还是 CrewAI、AutoGen 这类框架?

概念同构:角色、任务、交接、编排。LangGraph 的优势在底层原语可控(状态、检查点、中断、时间旅行)与生产化设施完整;高层框架上手快,但遇到边角问题还是要下探到底层。本手册讲的是通用概念,学扎实了哪个框架都能用。

出现成员互相循环调用怎么办?

三道防线:程序性的 recursion_limit 硬熔断;主管提示里写明修改轮数上限;trace 里监控循环深度并告警。成员可互相转接的网状拓扑最容易循环,没有强预算约束就不要上(第一节与第九节都拦过这个坑)。

06.11

实战练习与资源

最后一组练习把六章串起来:从单智能体基线出发,一步步搭出可评估、可熔断、可降级的多智能体系统。

延伸资源