8583 字
43 分钟
LangChain / LangGraph 源码深潜:Multi-Agent、Subagents 与 Handoffs 机制解剖

LangChain / LangGraph源码学习路线:第 17 篇 Multi-Agent、Subagents 与 Handoffs源码解剖#

核心问题: 多 Agent 系统如何在 LangChain / LangGraph 中表达控制权转移、状态传递和子 Agent 边界,而不是退化成一团 prompt 拼接?

源码主线: supervisor agentagent-as-tool / subgraphCommand(goto=...)state updatehandoff target agent

前置文章: 第 9 篇 Plan-and-Execute、第 16 篇 Agentic RAG

依赖基线: langgraph==1.2.7langchain==1.3.11langchain-core==1.4.8

源码基线: https://github.com/langchain-ai/langchain/tree/langchain==1.3.11、https://github.com/langchain-ai/langchain/tree/langchain-core==1.4.8、https://github.com/langchain-ai/langgraph/tree/1.2.7

阅读边界: 本文覆盖 supervisor、agent-as-tool、subgraph、Command handoff 和状态交接;不展开组织管理理论、复杂博弈策略和远程多进程 Agent 平台。


0. 本篇在源码学习主线中的位置#

前面的文章已经解释了底层 Runnable、Tool、StateGraph、Checkpoint 和 Streaming。本篇把这些能力放到一个更接近生产 Agent 的问题里:Multi-Agent / Handoffs。

Agentic RAG / Tool Runtime
Multi-Agent / Handoffs
Functional API

本篇只解决:

  • 解释 supervisor 与 subagent 的控制关系。
  • 解释 agent-as-tool 和 handoff 的区别。
  • 解释状态如何在多 Agent 间收敛而不是互相污染。

本篇不展开:

  • 不讨论远程 Agent 网络协议。
  • 不把每个工具都强行包装成 Agent。

1. 本篇问题、学习目标与能力边界#

核心问题#

多 Agent 系统如何在 LangChain / LangGraph 中表达控制权转移、状态传递和子 Agent 边界,而不是退化成一团 prompt 拼接?

学习目标#

完成本篇后,读者必须能够:

  1. 画出Multi-Agent / Handoffs的构建期对象关系。
  2. 解释一次运行时调用如何进入主链。
  3. 说明关键分支、异常和停止条件。
  4. 区分公共 API、扩展接口和内部实现。
  5. 根据旅行规划助手场景做工程选型。

能力边界#

能力本篇是否覆盖说明
构建期对象组装解释公开参数如何变成运行时对象
运行时主链解释 invoke/stream/tool/task 等主路径
分支与异常解释失败、降级、重试、终止边界
扩展协作解释与相邻框架组件如何组合
底层供应商实现不展开模型、数据库或平台内部实现

2. 核心概念与最小心智模型#

一句话定义#

Multi-Agent 是把复杂任务拆成多个具有独立职责的 Agent 或子图,并通过 supervisor、tool call 或 handoff 控制协作边界的架构形态。

最小心智模型#

Agentic RAG / Tool Runtime
Multi-Agent / Handoffs
Functional API
工程化 Agent 能力

核心术语#

术语源码对象语义不要误解为
Supervisorrouter / controller node决定下一个 Agent 或工具更聪明的大模型而已
Subagentagent graph / runnable封装一个专门能力域普通函数
Agent-as-toolBaseTool wrapper把 Agent 当工具调用并返回结果控制权永久转移
HandoffCommand(goto=...)把控制权转给另一个节点/Agent普通工具返回值

与相邻抽象的边界#

对象负责什么不负责什么与本篇对象的关系
Runnable统一 invoke / stream / batch 协议不决定业务策略提供可组合执行底座
StateGraph编排状态、节点和边不实现所有外部系统承载复杂 Agent 工作流
RunnableConfig传递 config、metadata、callbacks不保存业务状态让观测和配置沿调用链传播

3. 完整执行链路#

本篇的主线不是“多个 prompt 轮流调用”,而是控制权如何在多个 Agent 或子图之间移动。LangGraph 中最值得观察的对象是 state、message、Command 和 graph runtime:谁更新 state,谁决定下一跳,谁把 worker 的结果带回 supervisor。

从用户请求到 worker handoff 的对象流转#

用户请求
HumanMessage(content="帮我规划大阪 4 天,交通和餐厅都要可靠")
父图 state["messages"]
supervisor node
路由决策:"travel_planner" / "research_agent" / "finalize" / END
Command(goto="research_agent", update={"active_agent": "research_agent", "handoff_reason": "需要检索营业时间"})
LangGraph runtime 合并 update,移动到目标节点
worker agent / subgraph 接收 state 子集
worker 追加 AIMessage、ToolMessage 或结构化结果
Command(goto="supervisor", update={"messages": [...], "research_notes": [...]}, graph=Command.PARENT)
父图 supervisor 继续路由
最终 AIMessage / END

这里的关键不是“调用了另一个函数”,而是控制权移动。普通工具调用通常是父 Agent 调用工具并等待字符串结果;handoff 则表示下一个运行节点变成另一个 Agent 或子图,它拥有一段独立执行窗口,并通过 state update 把结果交回。

对象形态也在变化:用户请求是 HumanMessage,supervisor 的决策可以是字符串路由、结构化输出或 Command,worker 的产物可能是消息列表、业务字段或返回父图的 Command。如果把这些都压成一个大 prompt,就会丢失“谁做了什么、为什么转交、转交后谁负责”的边界。

伪代码:supervisor 路由与 handoff command#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

from typing import Literal
from langgraph.types import Command
WorkerName = Literal["travel_planner", "research_agent", "booking_guard", "finalize"]
def supervisor_node(state: MultiAgentState) -> Command[WorkerName]:
decision = supervisor_chain.invoke({
"messages": state["messages"],
"active_agent": state.get("active_agent"),
"open_tasks": state.get("open_tasks", []),
"handoff_history": state.get("handoff_history", []),
})
if decision.next_agent == "finalize":
return Command(
goto="finalize",
update={"handoff_reason": decision.reason},
)
if decision.next_agent not in state["allowed_agents"]:
return Command(
goto="booking_guard",
update={
"handoff_error": f"agent not allowed: {decision.next_agent}",
"handoff_reason": decision.reason,
},
)
return Command(
goto=decision.next_agent,
update={
"active_agent": decision.next_agent,
"handoff_reason": decision.reason,
"handoff_history": [
*state.get("handoff_history", []),
{"from": "supervisor", "to": decision.next_agent, "reason": decision.reason},
],
},
)

这段伪代码的输入是父图 state,输出不是普通 dict,而是 CommandCommand.goto 表达下一跳,Command.update 表达进入下一跳之前要合并到 state 的字段。对象形态从模型决策结果变成运行时路由协议。

源码观察点是 gotoupdate 的分离。goto 只决定控制权去哪里,update 才决定哪些事实写入 state。这样 supervisor 可以记录 handoff 原因、当前负责人、历史轨迹,而不需要 worker 反向猜测为什么自己被调用。

框架这样设计,是为了把“状态变更”和“执行拓扑变更”放在一个返回对象里。节点不再只能返回 state patch,也可以同时告诉 runtime 下一步去哪;这正是 handoff 比普通函数返回更适合多 Agent 协作的地方。

伪代码:worker 接收任务并返回父图#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def research_agent_node(state: MultiAgentState) -> Command[Literal["supervisor"]]:
task = extract_research_task(
messages=state["messages"],
reason=state["handoff_reason"],
)
worker_messages = [
SystemMessage(content="你是旅行事实核查 Agent,只返回有来源的结论。"),
*select_messages_for_worker(state["messages"]),
HumanMessage(content=task),
]
result = research_agent.invoke({"messages": worker_messages})
notes = extract_research_notes(result["messages"])
return Command(
goto="supervisor",
graph=Command.PARENT,
update={
"messages": [AIMessage(content=summarize_for_supervisor(notes), name="research_agent")],
"research_notes": notes,
"active_agent": "supervisor",
},
)

输入是父图传来的共享 state,但 worker 不应该消费全部字段。select_messages_for_worker 是上下文边界:只把完成核查任务需要的消息交给 worker。输出是返回父图的 Command,并且 update 中只带回 summary、notes 和必要消息。

源码观察点是 graph=Command.PARENT。当 worker 是子图时,返回父图不是“跳到同名节点”那么简单,而是告诉 runtime 这次 goto 应该在父图命名空间解析。否则子图内部节点名和父图节点名可能混淆。

框架这样设计,是为了让 worker 有独立执行空间,同时不污染父图状态。worker 可以在内部调用工具、检索、重试,但交回父图时只暴露经过整理的结果。这样 supervisor/worker 的边界才清楚:supervisor 管控制权,worker 管专门任务。

agent-as-tool 与 handoff 的差异链路#

def supervisor_with_agent_tool(state: MultiAgentState) -> dict:
tool_result = research_agent_tool.invoke({
"question": latest_user_question(state["messages"]),
"scope": "opening_hours_and_transport",
})
return {
"messages": [ToolMessage(content=tool_result, tool_call_id="manual_call")],
"research_notes": parse_notes(tool_result),
}

agent-as-tool 的输入输出更像普通工具:父节点仍然持有控制权,worker 被压缩成一次工具调用,返回值通常是字符串或结构化结果。handoff 则把控制权交出去,让目标 Agent 成为下一段运行的主体。

工程上可以这样判断:如果只是让研究 Agent 帮忙查一段资料,agent-as-tool 足够;如果研究 Agent 需要多轮工具调用、向用户追问、进入子图、或决定何时返回 supervisor,就应该使用 handoff 或 subgraph。

正常结束条件#

一次多 Agent handoff 正常结束,至少要满足:每次路由的目标在允许集合中,worker 返回的 state patch 符合父图 schema,消息交接没有丢失关键上下文,子图返回父图时明确使用父图边界,最终由 supervisor 或 finalize 节点收束到 END。

4. 源码地图、关键文件与阅读顺序#

核心目录#

langgraph/
├── graph/
├── pregel/
├── types.py
└── func/
langchain_core/
├── runnables/
├── tools/
├── retrievers.py
└── messages/

关键文件#

优先级文件核心对象阅读目的
1libs/langgraph/langgraph/types.pyCommand理解 goto/update/resume 类型
2libs/langgraph/langgraph/graph/state.pyStateGraph理解节点和子图如何编排
3libs/langgraph/langgraph/pregel/main.pyPregel理解运行时调度
4libs/core/langchain_core/tools/base.pyBaseTool理解 agent-as-tool 协议

推荐阅读顺序#

1. 先读官方概念文档,确认公共契约。
2. 再读公开 API 或装饰器入口。
3. 顺着构建期对象进入 runtime。
4. 追踪一次 invoke / stream / tool call / task call。
5. 单独检查异常、重试、恢复和扩展点。
6. 最后回到旅行规划助手做工程判断。

不建议的阅读顺序#

不建议直接从最底层 private helper 开始读。源码学习的第一目标是建立调用链,而不是收集函数名。先找公开入口,再沿参数和返回值追下去,才不会把内部实现误当成稳定 API。


5. 对象模型、继承关系与协议边界#

核心对象关系#

Public API / Decorator / Tool Protocol
Runtime wrapper
State / Config / Context
Storage / Model / Tool / Graph Runtime

对象职责#

对象生命周期输入输出核心职责
公开入口构建期或调用期用户参数runtime object提供稳定 API
runtime context单次调用config/state下游上下文传递配置、状态和观测信息
协议对象单步执行上游对象下游可消费结果维持模块边界
扩展点构建期注册、运行时触发request/response修改或观察后的结果插入业务控制逻辑

协议边界#

公共 API
负责:给业务代码稳定入口。
不负责:暴露所有内部调度细节。
运行时协议
负责:让状态、配置、工具、模型和持久化协作。
不负责:替业务判断所有策略。
扩展接口
负责:允许业务插入可维护的定制逻辑。
不负责:保证错误扩展仍然安全。

稳定接口与内部实现#

类型对象文章中的使用原则
公共 API官方文档列出的函数、类和装饰器可以用于工程示例
扩展接口middleware、store、retriever、task、Command 等协议说明契约和约束
内部实现private helper、runner、loop 细节只用于解释,不建议业务依赖

6. 源码阅读策略与证据标准#

本篇阅读策略#

先找公开入口
确认输入输出类型
沿调用方追到核心实现
记录状态与对象变化
检查分支、异常和结束条件
回到设计目的

证据等级#

标记含义写作要求
源码事实当前正式版源码可以证明附源码链接
官方契约官方文档或 API Reference 明确说明附官方链接
简化伪代码压缩真实控制流标注不是源码逐字复制
作者推断根据调用链得出的理解明确使用“从调用关系可以推断”
工程建议面向项目实践的建议说明适用条件

本篇证据清单#

结论证据类型文件或文档定位
公共入口存在稳定契约官方契约官方 docs / reference第 14 章链接
核心协议对象存在源码定义源码事实官方源码仓库第 4 章关键文件
运行时分支需要按协议处理作者推断调用链和源码结构第 9 章
工程选型依赖场景边界工程建议旅行规划助手场景第 11 章

7. 构建期源码解剖#

本章回答:多 Agent 系统在构建期如何把 supervisor、worker、subgraph、agent-as-tool 和 handoff policy 装配成可运行对象?构建期最重要的是定义边界,而不是把所有 Agent 塞进一个 prompt。

构建期职责#

输入归一化动作构建结果
state schema声明共享字段、私有字段、消息 reducer父图和子图的状态协议
supervisor node绑定路由模型或策略函数决定下一跳的控制节点
worker agent / subgraph封装专门能力和内部工具可被 handoff 或 tool 调用的执行单元
handoff policy限制目标集合、原因、返回路径可审计的控制权转移规则

supervisor graph 构建#

真实源码签名#

builder.add_conditional_edges("supervisor", route)

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class MultiAgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
active_agent: str | None
allowed_agents: list[str]
handoff_reason: str | None
handoff_history: list[dict]
research_notes: list[dict]
plan_draft: str | None
final_answer: str | None
builder = StateGraph(MultiAgentState)
builder.add_node("supervisor", supervisor_node)
builder.add_node("travel_planner", travel_planner_node)
builder.add_node("research_agent", research_agent_node)
builder.add_node("booking_guard", booking_guard_node)
builder.add_node("finalize", finalize_node)
builder.add_edge(START, "supervisor")
builder.add_edge("finalize", END)
multi_agent_graph = builder.compile()

输入是状态 schema 和节点函数,输出是编译后的父图。messages 使用 reducer 合并,active_agenthandoff_reasonresearch_notes 等字段则是业务状态。对象形态从一组 Python 节点变成 runtime 可调度的图。

源码观察点是 state schema。多 Agent 系统最容易出问题的地方是共享状态过宽:所有 worker 都能看见和修改所有字段。构建期应该先区分共享消息、路由字段、worker 结果字段和最终输出字段。

框架这样设计,是为了让图结构承担协作边界。supervisor 不需要知道 worker 内部怎么检索,worker 也不需要知道整个产品流程;双方通过 state schema 和 Command 协议对接。

handoff Command 准备#

真实源码签名#

Command(goto="research_agent", update={...})

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def make_handoff(
*,
source: str,
target: str,
reason: str,
state: MultiAgentState,
) -> Command:
if target not in state["allowed_agents"]:
return Command(
goto="booking_guard",
update={"handoff_error": f"disallowed target: {target}"},
)
return Command(
goto=target,
update={
"active_agent": target,
"handoff_reason": reason,
"handoff_history": [
*state.get("handoff_history", []),
{"from": source, "to": target, "reason": reason},
],
},
)

输入是来源、目标、原因和当前 state,输出是 Command。它不是普通状态更新,因为它同时改变控制流。goto 是运行时调度信号,update 是状态事实,两者缺一不可。

源码观察点是目标白名单。让模型直接输出任意节点名是危险的,尤其是有高风险 booking、支付、外部 API 的系统。构建期应该明确可路由目标,并在 handoff helper 中集中校验。

框架这样设计,是为了让 handoff 可审计。每次交接都能记录 from/to/reason,后续 trace、评测和人工复盘才能解释为什么任务被交给某个 worker。

agent-as-tool 包装#

真实源码签名#

tool = subagent.as_tool(...)

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

research_tool = research_agent.as_tool(
name="research_travel_facts",
description=(
"Use for bounded fact lookup. Input must include a focused question "
"and a scope. Returns summarized notes with sources."
),
)
def planner_node(state: MultiAgentState) -> dict:
result = research_tool.invoke({
"question": "大阪城到道顿堀晚间交通方式",
"scope": "transport",
})
return {"research_notes": parse_research_tool_result(result)}

输入是 subagent,输出是工具对象。运行时父 Agent 仍然掌握控制权,subagent 被限制在一次工具调用内。对象形态从“可运行 Agent”变成 BaseTool 协议。

源码观察点是描述和输入 schema。agent-as-tool 不是 handoff,它不应该让子 Agent 无限执行或自行改写父图状态。适合用在边界清晰、返回值可压缩的专门能力上。

subgraph 构建与父图边界#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

class ResearchState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
task: str
notes: list[dict]
research_builder = StateGraph(ResearchState)
research_builder.add_node("search", search_node)
research_builder.add_node("verify", verify_node)
research_builder.add_node("return_to_parent", return_to_parent_node)
research_builder.add_edge(START, "search")
research_builder.add_edge("search", "verify")
research_builder.add_edge("verify", "return_to_parent")
research_subgraph = research_builder.compile()
parent_builder.add_node("research_agent", research_subgraph)

输入是子图 schema 和节点,输出是可挂到父图的 compiled subgraph。子图可以有自己的 state,但返回父图时必须映射成父图理解的字段,例如 research_notes 和 summary message。

源码观察点是父子 state 的映射。不要假设子图所有字段都能自动进入父图;生产系统最好显式写一个 return node,把 worker 内部轨迹压缩成父图需要的结果。

框架支持 subgraph,是为了隔离复杂 worker。研究 Agent 可以有搜索、验证、去重、引用检查多个内部节点,但父图只需要知道“研究完成了吗、有什么 notes、下一步交给谁”。

构建期产物#

产物保存的信息运行时用途
父图 StateGraphsupervisor、worker 节点、END 路径承载控制权流转
Command helpergoto、update、目标校验、原因记录表达 handoff
agent-as-toolname、description、args schema让父 Agent 短调用子能力
subgraphworker 内部状态和节点隔离复杂专门任务

8. 运行时主链源码解剖#

本章回答:一次多 Agent 调用进入运行时后,supervisor 如何路由,worker 如何接收 state,handoff command 如何被 runtime 消费,结果如何回到父图。

运行时入口#

调用方式公开入口核心运行对象返回类型
父图调用graph.invoke(initial_state)StateGraph / Pregel runtime合并后的 state
supervisor 路由node 返回 Command 或路由标签graph scheduler下一跳 + state update
worker 子图subgraph node子图 runtime父图字段 patch 或 parent command
agent-as-tooltool.invoke(args)BaseTool wrappertool result / ToolMessage

supervisor 路由主链#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def supervisor_node(state: MultiAgentState, config: RunnableConfig) -> Command:
routing_input = {
"conversation": trim_messages(state["messages"]),
"active_agent": state.get("active_agent"),
"research_notes": summarize_notes(state.get("research_notes", [])),
"plan_draft": state.get("plan_draft"),
}
route = supervisor_router.invoke(routing_input, config=config)
if route.next == "research_agent":
return make_handoff(
source="supervisor",
target="research_agent",
reason=route.reason,
state=state,
)
if route.next == "travel_planner":
return make_handoff(
source="supervisor",
target="travel_planner",
reason=route.reason,
state=state,
)
return Command(goto="finalize", update={"handoff_reason": route.reason})

输入是共享 state 和 config,输出是 Commandrouting_input 是经过筛选的上下文,不是整个 state;supervisor 只需要对话摘要、已有研究结论和计划草稿来决定下一跳。

源码观察点是 supervisor/worker 边界。supervisor 不执行检索、不写详细行程、不直接调用 booking;它只判断下一段工作应该由谁负责。把执行细节塞进 supervisor,会让路由节点越来越胖,后续很难测试和替换。

框架这样设计,是为了让控制节点保持小而明确。节点返回 Command 后,runtime 负责合并 update 和调度 goto;业务代码不需要手动调用下一个节点函数。

state/message 交接#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def select_messages_for_worker(messages: list[BaseMessage], worker: str) -> list[BaseMessage]:
if worker == "research_agent":
return [
msg for msg in messages
if msg.type in {"human", "ai"} and not contains_private_booking_data(msg)
][-6:]
if worker == "booking_guard":
return [msg for msg in messages if mentions_booking_or_payment(msg)]
return messages[-10:]
def worker_input_from_parent(state: MultiAgentState, worker: str) -> dict:
return {
"messages": select_messages_for_worker(state["messages"], worker),
"task": state["handoff_reason"],
}

输入是父图 messages 和目标 worker,输出是 worker 可见的输入。对象形态仍然是 BaseMessage 列表,但经过裁剪和权限过滤。不要把父图全部 state 直接交给每个 worker。

源码观察点是 messages reducer。父图中 messages 往往采用追加合并,但 worker 输入可以是子集。共享 state 的合并规则和 worker 可见上下文是两个问题:前者是 runtime 协议,后者是业务安全策略。

框架这样设计,是为了支持既共享协作事实,又限制上下文扩散。多 Agent 系统里,信息隔离和任务专注同样重要;否则每个 worker 都会被无关历史干扰,甚至看到不该看的字段。

worker 返回父图#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def travel_planner_node(state: MultiAgentState) -> Command[Literal["supervisor"]]:
planner_input = {
"messages": select_messages_for_worker(state["messages"], "travel_planner"),
"research_notes": state.get("research_notes", []),
}
plan = planner_chain.invoke(planner_input)
return Command(
goto="supervisor",
graph=Command.PARENT,
update={
"messages": [AIMessage(content=plan.summary, name="travel_planner")],
"plan_draft": plan.markdown,
"active_agent": "supervisor",
},
)

输入是父图 state 的受控子集,输出是返回 supervisor 的 Commandplan_draft 是业务字段,messages 是对话审计字段,二者都要写入父图,但语义不同。

源码观察点是 Command.PARENT。当 worker 作为子图运行时,返回父图需要明确指定父图边界。没有这个边界,runtime 可能在子图内部解析 goto,导致“看起来节点名正确但跳不到预期位置”的问题。

框架这样设计,是为了允许 worker 有内部拓扑。worker 可以先检索、再规划、再自检,最后只把稳定结果返回父图;父图不必知道 worker 内部经历了多少步。

agent-as-tool 运行时#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def call_research_as_tool(state: MultiAgentState) -> dict:
result = research_tool.invoke({
"question": latest_user_question(state["messages"]),
"scope": "facts_only",
})
return {
"messages": [AIMessage(content="已完成事实核查。", name="supervisor")],
"research_notes": result["notes"],
}

输入是工具 args,输出是普通 state patch。控制权没有离开当前节点;父图不会把下一跳交给 research tool,它只是拿到一个结果继续执行。

源码观察点是 agent-as-tool 的返回值压缩。工具结果应该尽量结构化、短小、可校验;如果子 Agent 需要多轮决策并影响路由,就不要把它压成 tool。

运行时结束条件#

正常结束:supervisor 路由到 finalize,finalize 写入 final_answer,然后进入 END。
worker 返回:子图或 worker 返回 Command(goto="supervisor", graph=Command.PARENT)。
短调用返回:agent-as-tool 返回工具结果,父节点继续持有控制权。
保护结束:达到 recursion_limit、handoff 次数上限或人工中断。
异常失败:未知节点、非法目标、坏 state patch 或 worker 不可恢复异常上抛并进入 trace。

9. 关键分支、异常与边界#

多 Agent 的异常通常不是单个函数失败,而是控制权、状态和消息边界出错。本章重点看未知目标、非法 handoff、worker 失败、父图返回和循环保护。

未知或未授权目标#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def validate_handoff_target(target: str, state: MultiAgentState) -> Command | None:
if target not in REGISTERED_NODES:
return Command(
goto="supervisor",
update={"handoff_error": f"unknown node: {target}"},
)
if target not in state["allowed_agents"]:
return Command(
goto="booking_guard",
update={"handoff_error": f"disallowed handoff: {target}"},
)
return None

输入是目标节点名和 state,输出要么是修复性 Command,要么是 None 表示可以继续。这里的对象形态仍然是路由协议,而不是异常字符串;这样 runtime 可以继续把错误交给 supervisor 或 guard 节点处理。

源码观察点是不要直接信任模型输出的节点名。supervisor 可以由 LLM 驱动,但图的节点集合是代码定义的稳定边界。模型输出必须经过白名单和注册表校验。

框架这样设计的工程意义是把“语义路由”和“拓扑安全”分开。模型可以判断应该找谁,代码必须决定是否允许它真的跳过去。

循环与递归保护#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def route_with_handoff_limit(command: Command, state: MultiAgentState) -> Command:
history = state.get("handoff_history", [])
if len(history) >= 8:
return Command(
goto="finalize",
update={
"handoff_error": "handoff limit reached",
"final_answer": "任务在多 Agent 协作中未能稳定收敛,需要人工检查。",
},
)
last_three = [(item["from"], item["to"]) for item in history[-3:]]
if len(set(last_three)) == 1:
return Command(
goto="supervisor",
update={"handoff_error": "repeated handoff loop detected"},
)
return command

输入是即将返回的 Command 和当前 state,输出是可能被替换的 Command。状态变化在于记录了保护性错误,而不是继续盲目路由。

源码观察点是 recursion_limit 只能提供框架级上限,不能解释业务为什么循环。业务层的 handoff_history 能告诉你是 supervisor 和 planner 互相踢皮球,还是 research_agent 一直返回证据不足。

框架保护和业务保护应该同时存在。前者防止无限执行耗尽资源,后者让失败可解释、可修复。

worker 失败与返回父图失败#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def safe_worker_node(state: MultiAgentState) -> Command:
try:
return research_agent_node(state)
except RecoverableToolError as exc:
return Command(
goto="supervisor",
graph=Command.PARENT,
update={
"messages": [AIMessage(content=f"研究 Agent 暂时失败:{exc}", name="research_agent")],
"handoff_error": str(exc),
"active_agent": "supervisor",
},
)
except Exception:
raise

输入是 worker state,输出是返回父图的 Command 或直接抛出不可恢复异常。可恢复错误被包装成 state update,不可恢复错误继续向上抛出,让 trace 记录真实失败。

源码观察点是异常不要全部吞掉。worker 内部工具失败可以返回 supervisor 重新规划;schema 错误、未知节点、权限错误等不应伪装成正常结果,否则父图会基于坏状态继续执行。

state patch 不符合 schema#

条件行为结果
worker 返回未知字段拒绝合并或在边界转换防止污染父图 state
messages 返回字符串而非 BaseMessage转换或抛错保持消息 reducer 正常工作
子图忘记 Command.PARENT路由留在子图或报错明确父子图命名空间
agent-as-tool 返回过大历史压缩为 notes / summary避免父图上下文膨胀
handoff reason 缺失写入默认原因或拒绝 handoff保持审计链完整

supervisor/worker 边界#

边界supervisor 应该做worker 应该做
控制权决定下一跳、终止或人工介入在授权范围内完成专门任务
state写入路由原因和全局进度写入本任务结果和 summary
messages维护用户可理解的协作轨迹只追加必要结果,不倾倒内部草稿
异常分类、重试、降级或结束暴露可恢复失败,不伪造成功

能力边界#

容易误判的能力实际提供者本篇对象的真实职责
自动让多 Agent 更聪明任务拆分、路由策略、评测提供控制权和状态交接机制
自动保证 worker 不越权权限策略、白名单、middleware提供可插入的路由边界
自动保证收敛终止条件、handoff 上限、人工审核提供可观测执行路径

10. 扩展机制与框架协作#

本章回答:多 Agent 系统应该在哪里扩展,而不是改内部 scheduler?核心原则是 supervisor 管控制权,worker 管专门能力,handoff policy 管边界,observability 记录每次交接。

扩展点总览#

扩展点扩展方式执行时机可修改内容约束
supervisor router结构化输出模型 / 规则函数每轮路由前next_agent、reason、confidence必须校验目标集合
handoff policyhelper / middlewareCommand 返回前允许目标、上限、人工审批不信任模型自由节点名
worker context builder消息裁剪 / state 映射进入 worker 前worker 可见消息和字段避免跨域污染
return mapper子图 return nodeworker 返回父图前summary、notes、messages不泄露内部草稿
observabilitytags / metadata / trace每次节点调用from/to/reason/error避免敏感数据

自定义 handoff policy#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def should_handoff(state: MultiAgentState, decision: RouteDecision) -> Command:
if decision.confidence < 0.55:
return Command(
goto="supervisor",
update={"handoff_error": "low routing confidence"},
)
if decision.next_agent == "booking_guard" and not state.get("user_confirmed_booking"):
return Command(
goto="supervisor",
update={"handoff_error": "booking handoff requires user confirmation"},
)
return make_handoff(
source="supervisor",
target=decision.next_agent,
reason=decision.reason,
state=state,
)

输入是当前 state 和路由决策,输出是 Command。policy 不执行 worker 任务,它只决定是否允许控制权转移。对象形态保持在 handoff 协议层。

源码观察点是高风险 worker 的前置条件。比如 booking、支付、邮件发送不能只靠模型一句“需要预订”就进入 worker,必须有用户确认、权限和审计字段。

与 Memory 协作#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def load_worker_memory(user_id: str, worker: str, store: BaseStore) -> dict:
global_preferences = store.search(("user", user_id, "preferences"))
worker_memory = store.search(("user", user_id, "agents", worker))
return {
"global_preferences": redact_sensitive(global_preferences),
"worker_memory": worker_memory,
}

输入是 user id、worker 名称和 store,输出是该 worker 可见的记忆。全局偏好可以共享,worker 专属记忆按命名空间读取;不要让 research_agent 随便读取 booking_guard 的审批历史。

源码观察点是 store namespace。多 Agent memory 的关键不是“有没有记忆”,而是记忆属于谁、谁能读、什么时候写回。命名空间可以把全局用户偏好和 worker 私有经验分开。

框架提供 store/checkpointer 协议,但不替业务决定隐私边界。工程上应在 context builder 中显式选择哪些 memory 进入 worker 输入。

与 Observability 协作#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def tag_handoff_config(config: RunnableConfig, command: Command, state: MultiAgentState) -> RunnableConfig:
return {
**config,
"tags": [*config.get("tags", []), "multi-agent", "handoff"],
"metadata": {
**config.get("metadata", {}),
"from_agent": state.get("active_agent") or "supervisor",
"to_agent": command.goto,
"handoff_reason": state.get("handoff_reason"),
"handoff_count": len(state.get("handoff_history", [])),
},
}

输入是 config、command 和 state,输出是增强后的 config。观测字段不需要进入模型 prompt,它们用于 trace、评测和调试。

源码观察点是 from/to/reason。没有这些字段,多 Agent 失败时只能看到一堆模型调用,不知道控制权为什么移动。良好的 handoff trace 应该能重放“谁把任务交给谁,为什么,结果是什么”。

与 Agentic RAG 协作#

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def research_agent_node(state: MultiAgentState) -> Command:
rag_result = rag_graph.invoke({
"question": state["handoff_reason"],
"documents": [],
"answer": None,
})
return Command(
goto="supervisor",
graph=Command.PARENT,
update={
"research_notes": extract_notes(rag_result),
"messages": [AIMessage(content=rag_result["answer"], name="research_agent")],
},
)

输入是 handoff reason,内部调用第 16 篇的 RAG graph,输出是 research notes 和 summary message。这里 RAG 是 worker 的内部能力,不直接接管父图控制权。

这个协作方式把两篇文章串起来:第 16 篇解决“一个 Agent 如何可靠检索和 grounding”,第 17 篇解决“哪个 Agent 何时接手这项能力、结果如何交回”。两层边界清楚,系统才容易测试和扩展。

选择 agent-as-tool、handoff 还是 subgraph#

条件agent-as-toolhandoffsubgraph
一次短任务、返回结果即可
需要目标 Agent 多轮执行
需要隔离 worker 内部状态部分
需要返回父图并继续总控
需要父 Agent 始终持有控制权

11. 工程决策与适用场景#

适用场景#

场景是否推荐原因
旅行规划助手生产化需要状态、工具、检索、观测和恢复闭环
一次性脚本问答直接调用模型更简单
高风险工具调用可以加入 guardrail、interrupt、trace
大规模知识问答可以把 retrieval、rerank、grounding 拆清楚
简单静态 FAQ复杂运行时收益不高

工程决策表#

决策点推荐选择前提风险
是否抽象成独立模块有稳定职责时抽象边界清楚过早拆分增加复杂度
是否持久化需要恢复或复盘时持久化有 thread_id / user_id隐私和清理成本
是否加入 guardrail涉及外部工具或敏感数据时加入有明确策略误杀正常请求
是否流式观测用户等待时间长时加入前端能消费事件泄露内部状态

性能、可靠性与安全边界#

性能:额外抽象会增加序列化、检索、trace 和存储成本。
可靠性:可恢复机制要求状态 schema、幂等工具和错误分类清楚。
安全:metadata、state、tool args、retrieved docs 都可能包含敏感信息。
可观测性:至少记录 thread_id、node/tool、策略命中、错误和耗时。

12. 常见误区与源码纠正#

误区:把公共 API 当成全部源码机制#

错误原因:

公开 API 很短,看起来像全部逻辑都在这一层。

源码事实:

公开 API 通常只负责归一化和装配;真正的运行语义在 runtime、协议对象和扩展点之间流转。

工程影响:

只读公开 API 会误判能力边界,导致扩展时依赖错误位置。

误区:把所有中间结果都写进模型上下文#

错误原因:

Agent 运行时的 state、memory、documents、trace 看起来都像“上下文”。

源码事实:

state、store、Document、metadata、trace、prompt messages 是不同协议;只有经过明确注入的内容才进入模型上下文。

工程影响:

如果不区分这些协议,轻则上下文膨胀,重则泄露敏感数据或污染长期记忆。

误区:异常都应该在框架层吞掉#

错误原因:

Agent 产品希望“永远给用户一个答案”。

源码事实:

可恢复异常可以转成 retry、fallback、interrupt;不可恢复异常必须上抛并进入 trace。

工程影响:

吞掉异常会让错误状态继续进入后续节点,调试成本比显式失败高得多。


13. 最终心智模型与掌握检查#

构建期心智模型#

Public Config
Normalize
Build Runtime Object
Attach Protocol / Extension / Storage

运行时心智模型#

Input
Runtime Entry
Context / State / Config
Core Execute
State / Event / Output

分支与异常心智模型#

正常路径 → 返回协议对象并更新必要状态
分支路径 → 路由、降级、拒答、人工确认或恢复
可恢复异常 → retry / fallback / repair / resume
不可恢复异常 → trace + raise
保护上限 → 停止循环、预算或高风险动作

一句话总结#

Multi-Agent / Handoffs通过构建期协议装配形成可运行对象,运行时沿公开入口进入核心执行链,使用分支机制处理边界,并通过扩展点与 LangChain / LangGraph 的状态、工具、模型、存储和观测能力协作。

掌握检查#

  • 能说清公开入口与真实执行入口的区别。
  • 能画出构建期对象关系。
  • 能画出运行时对象流转。
  • 能解释至少一个核心函数的伪代码。
  • 能指出同步、异步、流式或批量路径的边界。
  • 能说明异常在哪里抛出、在哪里处理。
  • 能说明正常结束和保护性终止的区别。
  • 能区分公共 API、扩展接口和内部实现。
  • 能根据旅行规划助手场景判断是否应该使用该抽象。

14. 参考资料与下一篇衔接#

官方概念文档#

  1. LangChain Multi-agent
    https://docs.langchain.com/oss/python/langchain/multi-agent

  2. LangGraph Subgraphs
    https://docs.langchain.com/oss/python/langgraph/use-subgraphs

  3. Thinking in LangGraph
    https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph

官方 API Reference#

  1. Command
    https://reference.langchain.com/python/langgraph/types/

  2. StateGraph
    https://reference.langchain.com/python/langgraph/graphs/

官方源码#

  1. libs/langgraph/langgraph/types.py
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/types.py

  2. libs/langgraph/langgraph/graph/state.py
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/graph/state.py

  3. libs/core/langchain_core/tools/base.py
    https://github.com/langchain-ai/langchain/blob/langchain-core==1.4.8/libs/core/langchain_core/tools/base.py

下一篇衔接#

下一篇进入:

第 18 篇:Functional API、entrypoint 与 task 源码解剖

需要继续回答:

这项能力在旅行规划助手中应该放在哪一层?
它和前一篇能力如何协作?
哪些边界需要通过源码证据确认?
LangChain / LangGraph 源码深潜:Multi-Agent、Subagents 与 Handoffs 机制解剖
https://jupiter-ws.cn/posts/agent-frameworks/17_langchain_langgraph_multi_agent_handoffs_source_deep_dive/
作者
Jupiter
发布于
2026-03-21
许可协议
CC BY-NC-SA 4.0