TECH NOTE
5764 字 29 分钟 系列 4 / 2204 Subagents 子智能体:主管派活,专家干活
ON THIS PAGE / 目录18 节
一句话讲透:主管 Agent(supervisor)手里有一份”外包名单”,每个专家(子智能体)被包装成一个工具。主管决定派谁、给什么输入、怎么合并结果;专家干完把结果交回主管,由主管对用户说话。这是最通用、最好理解的一种多 Agent 打法。
它到底是什么
原文定义:
在 subagents 架构里,一个中心的主 Agent(常被称为 supervisor 主管)通过把子智能体当工具调用来协调它们。主 Agent 决定调哪个子智能体、给什么输入、怎么合并结果。默认情况下子智能体是无状态的——它们不记得过去的交互,所有对话记忆都由主 Agent 维护。这带来了上下文隔离:每次子智能体调用都跑在一个干净的上下文窗口里,避免主对话的上下文膨胀。
两句人话补充:
- 无状态不是缺陷,是设计:子智能体每次调用都是”新人报到”,只拿到本次任务描述。这样它不会被主对话里的杂事污染——这正是
03篇里 Subagents token 最省的原因。 - 但也可以”带着上下文”去:如果子智能体需要接着主 Agent 已做的工作干(比如接着改一个没修完的 bug),你可以把主 Agent 的历史消息传进去,见下文「子智能体输入」。
📎 想要内置实现:Deep Agents 自带 subagents 支持,见 Deep Agents / Subagents。
graph LR A[用户] --> B[主管 Agent] B --> C[子智能体 A] B --> D[子智能体 B] B --> E[子智能体 C] C --> B D --> B E --> B B --> F[回复用户]这张图在说什么:① 用户的每一句话都只进主管;② 主管按需要把一个或多个任务分发给子智能体(A/B/C 互不来往);③ 每个子智能体的结果必须回到主管(C→B、D→B、E→B 这三条回边是这个模式的灵魂);④ 最后由主管统一回复用户。
五个关键特征
| 特征 | 原文 | 说人话 |
|---|---|---|
| 集中式控制 | Centralized control:所有路由都经过主 Agent | 主管是唯一入口和唯一出口,好管控、好审计 |
| 不与用户直接交互 | No direct user interaction:子智能体把结果返回主 Agent,而不是给用户 | 用户感知不到”背后有几个专家”;但可以在子智能体内部用 interrupt 让用户参与 |
| 子智能体即工具 | Subagents via tools:子智能体是通过工具被调用的 | 这句话是全部实现的核心:包装成 @tool 就是一个子智能体 |
| 可并行执行 | Parallel execution:主 Agent 可以在同一个回合里调多个子智能体 | 一次问三个领域不需要串行等三次 |
| 无状态(默认) | (原文正文)由主 Agent 维护全部对话记忆 | 隔离强、成本稳定,代价是每次都重新推导 |
📌 主管 ≠ 路由(原文的 Note): 主管 Agent 是一个完整的 Agent——它维护对话上下文,并在多个回合里动态决定该调哪些子智能体。 Router 路由通常只是一次分类步骤,把请求分发出去,不维护持续的对话状态。 一句话:主管会聊天、会记事儿、会连续决策;路由只负责”看一眼、指个路”(详见
07篇)。
什么时候用它
原文给的判断标准:
- 你手上有多个互不相同的领域(比如日历、邮件、CRM、数据库);
- 子智能体不需要直接跟用户对话;
- 你想要集中式的工作流控制。
以及一句很重要的”反向提醒”:
💡 提示(原文):需要在子智能体里跟用户交互怎么办? 子智能体通常把结果返回主管而不是直接跟用户对话,但你可以在子智能体内部用 interrupt 暂停执行、收集用户输入。适用于子智能体需要澄清问题或要审批才能继续的场景——主管仍是指挥者,但子智能体可以在任务中途向用户要信息。
最小可用实现:三行核心思想
原文的”核心机制”就一段代码——把子智能体包成工具:
from langchain.tools import toolfrom langchain.agents import create_agent
# 1) 先建一个子智能体(它自己也是一个完整的 Agent)subagent = create_agent(model="google_genai:gemini-3.6-flash", tools=[...])
# 2) 把它包成一个工具:工具名 + 描述就是给主管看的"外包说明"@tool("research", description="Research a topic and return findings")def call_research_agent(query: str): result = subagent.invoke({"messages": [{"role": "user", "content": query}]}) return result["messages"][-1].content # 只把最终结论交回主管
# 3) 主管 Agent:工具清单里挂着这个子智能体main_agent = create_agent(model="google_genai:gemini-3.6-flash", tools=[call_research_agent])这段代码在说什么:① 子智能体本身就是 create_agent 造出来的普通 Agent;② @tool 装饰器把它变成主管可调用的工具,工具名和描述是主管决策的唯一依据(所以别起 helper、do_stuff 这种名字);③ 只把 messages[-1].content(最终回答)返回给主管——中间的工具调用、推理过程都被”关在”子智能体的上下文里。
📎 官方实战教程:Build a personal assistant with subagents(本笔记的精讲版见
09)
五个要拿主意的设计决策
原文把实现 Subagents 时的选择整理成一张表,下面逐项展开:
| 决策点 | 可选方案 | 一句话建议 |
|---|---|---|
| 同步 vs 异步 | 同步(阻塞)vs 异步(后台任务) | 主管需要结果才能继续 → 同步;用户不该干等 → 异步 |
| 工具模式 | 一智能体一工具 vs 单一派发工具 | 要精细控制 → 一智能体一工具;要”约定优于配置” → 单一 task 工具 |
| 子智能体规格 | 系统提示枚举 vs Enum 约束 vs 工具化发现 | 少于 10 个用前两种;多/动态用第三种 |
| 子智能体输入 | 只给任务描述 vs 给完整上下文 | 默认隔离;要”接着干”就给历史 |
| 子智能体输出 | 只回结果 vs 回完整会话历史 | 默认回精简结果;需要额外状态用 Command |
决策一:同步还是异步
| 方式 | 主管的行为 | 最适合 | 代价 |
|---|---|---|---|
| 同步 Sync | 等子智能体做完才继续 | 主管需要结果才能往下走 | 简单,但会把对话卡住 |
| 异步 Async | 子智能体在后台跑,主管继续聊 | 独立任务,用户不该干等 | 响应快,但实现更复杂 |
💡 提示(原文):别和 Python 的
async/await搞混。这里的”异步”指主管启动一个后台作业(通常跑在另一个进程或服务里)然后不阻塞地继续。
同步(默认行为)
默认情况下子智能体调用是同步的:主管等每个子智能体做完才继续。当下一个动作依赖子智能体结果时,用同步。
sequenceDiagram participant U as 用户 participant M as 主管 Agent participant R as 调研子智能体 U->>M: "东京天气怎么样?" M->>R: research("东京天气") Note over M: 阻塞等待结果… R-->>M: "当前 72°F,晴" M-->>U: "东京现在 72°F,晴天"这张图在说什么:① 主管把任务交给调研子智能体后什么都不做,就等;② 子智能体返回结论;③ 主管再基于结论回答用户。用户在这段时间里看不到任何东西。
什么时候用同步(原文):
- 主管需要子智能体的结果才能组织回答;
- 任务之间有顺序依赖(例如:取数据 → 分析 → 回答);
- 子智能体失败时应该阻塞主管的回答(比如涉及合规、金额)。
代价(原文):实现简单(调了就等);但所有子智能体做完之前用户看不到任何响应;长任务会把对话冻住。
异步(后台作业)
当子智能体的工作与主对话流程相互独立时用异步:主管甩出一个后台任务,自己继续陪用户聊。
sequenceDiagram participant U as 用户 participant M as 主管 Agent participant J as 作业系统 participant C as 合同审查子智能体 U->>M: "帮我审一下这份并购合同" M->>J: run_agent("legal_reviewer", task) J->>C: 启动 Agent J-->>M: job_id: "job_123" M-->>U: "已开始审查(job_123)" Note over C: 正在审 150+ 页… U->>M: "进度怎么样?" M->>J: check_status(job_id) J-->>M: "running" M-->>U: "还在审…" U->>M: "好了吗?" M->>J: check_status(job_id) J-->>M: "completed" M->>J: get_result(job_id) J-->>M: 合同分析结果 M-->>U: "审查完成:[结论]"这张图在说什么:① 主管不等待,立刻给用户一个作业号;② 用户随时可以问进度;③ 作业完成后主管取结果再汇报。用户全程不被阻塞。
什么时候用异步(原文):
- 子智能体的工作与主对话流程相互独立;
- 用户应该能在后台干活时继续聊天;
- 你想并行跑多个独立任务。
“三工具”模式(原文):
- Start job(启动):踢出后台任务,返回一个 job ID;
- Check status(查状态):返回当前状态(pending / running / completed / failed);
- Get result(取结果):取回已完成的结果。
作业完成后怎么通知用户(原文):一种做法是弹一条通知,用户点击后向 Agent 发一条 HumanMessage,例如”检查 job_123 并总结结果”。这样”推”就变成了”拉”,不需要 Agent 主动打断用户。
决策二:工具模式——一智能体一工具,还是一个大 task 工具
| 模式 | 最适合 | 代价 |
|---|---|---|
| 一智能体一工具 | 需要对每个子智能体的输入输出做精细控制 | 配置更多,但更可定制 |
| 单一派发工具 | 智能体很多、团队分布式开发、“约定优于配置” | 组合更简单,但每个子智能体可定制的空间更小 |
一智能体一工具
graph LR A[用户] --> B[主管 Agent] B --> C[子智能体 A] B --> D[子智能体 B] B --> E[子智能体 C] C --> B D --> B E --> B B --> F[回复用户]这张图在说什么:主管的工具清单里平铺着三个工具,每个工具对应一个子智能体,各自可以有自己独立的入参和返回值设计。
from langchain.tools import toolfrom langchain.agents import create_agent
# 每个子智能体 = 一个独立工具(入参/返回值都能单独设计)subagent = create_agent(model="...", tools=[...])
@tool("subagent_name", description="subagent_description")def call_subagent(query: str): result = subagent.invoke({"messages": [{"role": "user", "content": query}]}) return result["messages"][-1].content
main_agent = create_agent(model="...", tools=[call_subagent])主管在判断”这个任务符合某个子智能体的描述”时调用它,拿到结果后继续编排。想要细粒度控制上下文,见下文「上下文工程」。
单一派发工具(task 模式)
另一种做法:只暴露一个参数化工具,按名字去调起”临时子智能体”(ephemeral sub-agent)来干独立任务。
原文对机制的描述很精确:
- 任务描述(
description)会作为一条 human message 传给子智能体; - 子智能体的最后一条消息作为工具结果返回;
- 和”一智能体一工具”不同,这是基于约定的:一个
task工具搞定所有子智能体。
什么时候用它(原文):
- 想把 Agent 的开发分散到多个团队;
- 需要把复杂任务隔离到独立上下文窗口;
- 想在不改协调者代码的前提下可扩展地加新 Agent;
- 偏好”约定优于配置”。
原文也点明了代价:它用”上下文工程上的灵活性”换了”组合的简单性和强上下文隔离”。
graph LR A[用户] --> B[主管 Agent] B --> C{"task 工具<br/>agent_name, description"} C -->|research| D[调研 Agent] C -->|writer| E[写作 Agent] C -->|reviewer| F[审查 Agent] D --> C E --> C F --> C C --> B B --> G[回复用户]这张图在说什么:① 主管只认识一个 task 工具;② 靠 agent_name 参数做按名分发(相当于运行时查表);③ 结果先回到 task 工具,再回到主管——多了一层”派发器”。
关键特征(原文):
- 单一 task 工具:一个参数化工具,可按名字调起任何已注册的子智能体;
- 约定式调用:按名字选 Agent,任务作为 human message 传入,最后一条消息作为工具结果返回;
- 团队分布式:不同团队可以独立开发和部署各自的 Agent;
- Agent 发现:可以通过系统提示列出可用 Agent,也可以通过渐进式披露(用工具按需加载 Agent 信息)来发现。
💡 提示(原文,很有洞察):这种模式有个有趣的特性——子智能体的能力可能和主 Agent 完全一样。这时候调子智能体的首要理由其实是”上下文隔离”:让复杂的多步任务跑在独立的上下文窗口里,不去污染主对话历史;子智能体自主干完,只交回一份精简总结,主线程始终保持聚焦和轻量。
附:带任务派发器的 Agent 注册表(原文完整示例)
from langchain.tools import toolfrom langchain.agents import create_agent
# 不同团队开发的子智能体research_agent = create_agent(model="gpt-5.5", prompt="You are a research specialist...")writer_agent = create_agent(model="gpt-5.5", prompt="You are a writing specialist...")
# 可用子智能体的注册表("按名派活"的核心)SUBAGENTS = { "research": research_agent, "writer": writer_agent,}
@tooldef task(agent_name: str, description: str) -> str: """Launch an ephemeral subagent for a task.
Available agents: - research: Research and fact-finding - writer: Content creation and editing """ agent = SUBAGENTS[agent_name] result = agent.invoke({"messages": [{"role": "user", "content": description}]}) return result["messages"][-1].content
# 主协调 Agentmain_agent = create_agent( model="gpt-5.5", tools=[task], system_prompt=( "You coordinate specialized sub-agents. " "Available: research (fact-finding), writer (content creation). " "Use the task tool to delegate work." ),)⚠️ 原文如此:
task的 docstring 里硬编码了可用 Agent 列表,而系统提示里又重复了一遍——原文用这种方式演示”系统提示枚举”。真实项目里两处重复容易不同步,建议只留一处权威来源(或改用下面的 Enum / 工具化发现)。
决策三:上下文工程——三处”阀门”
原文把上下文工程分三类,每一类影响的对象不同:
| 类别 | 目的 | 影响谁 |
|---|---|---|
| 子智能体规格(specs) | 确保”该调用的时候”真能调起来 | 主管的路由决策 |
| 子智能体输入(inputs) | 确保子智能体拿到”刚好够用”的上下文 | 子智能体的表现 |
| 子智能体输出(outputs) | 确保主管能基于结果继续行动 | 主管的表现 |
(更完整的上下文工程方法见官方的上下文工程指南。)
子智能体规格:名字与描述就是提示词杠杆
原文说得很直白:子智能体的”名字”和”描述”是主 Agent 判断该调谁的主要依据——它们就是提示词杠杆,得仔细起。
- 名字(Name):主 Agent 怎么称呼这个子智能体。要清晰、动作导向(例如
research_agent、code_reviewer)。 - 描述(Description):主 Agent 对这个子智能体能力的认知。要具体说明它处理什么任务、什么时候该用它。
如果你用的是单一派发工具,还得额外告诉主 Agent”有哪些子智能体可用”。按数量和注册表是静态还是动态,有三种给法:
| 方式 | 最适合 | 代价 |
|---|---|---|
| 系统提示枚举 | 小而静态的清单(< 10 个) | 简单,但 Agent 变更时要改提示词 |
| Enum 约束 | 小而静态的清单(< 10 个) | 类型安全、显式,但变更时要改代码 |
| 工具化发现 | 大或动态的注册表 | 灵活可扩展,但增加复杂度 |
系统提示枚举
把可用的 Agent 直接列在主 Agent 的系统提示里——主 Agent 把它们当作指令的一部分读到。
什么时候用(原文):Agent 少且固定(< 10 个);注册表很少变;想要最简单的实现。
main_agent = create_agent( model="...", tools=[task], system_prompt=( "You coordinate specialized sub-agents. " "Available agents:\n" "- research: Research and fact-finding\n" "- writer: Content creation and editing\n" "- reviewer: Code and document review\n" "Use the task tool to delegate work." ),)在派发工具上加 Enum 约束
给 task 工具的 agent_name 参数加一个 Enum 约束:带来类型安全,并让可用 Agent 在工具 schema 里显式可见。
什么时候用(原文):Agent 少且固定(< 10 个);想要类型安全和明确的 Agent 名;比起”靠提示词引导”更偏好”靠 schema 校验”。
from enum import Enum
class AgentName(str, Enum): RESEARCH = "research" WRITER = "writer" REVIEWER = "reviewer"
@tooldef task(agent_name: AgentName, description: str) -> str: """Launch an ephemeral subagent for a task.""" # ...工具化发现(tool-based discovery)
额外提供一个专门的工具(比如 list_agents 或 search_agents),让主 Agent 按需发现有哪些 Agent。这实现了渐进式披露,也支持动态注册表。
什么时候用(原文):Agent 很多(> 10)或注册表在持续增长;注册表经常变或是动态的;想减小提示词体积、省 token;不同团队各自管理不同 Agent。
@tooldef list_agents(query: str = "") -> str: """List available subagents, optionally filtered by query.""" agents = search_agent_registry(query) return format_agent_list(agents)
@tooldef task(agent_name: str, description: str) -> str: """Launch an ephemeral subagent for a task.""" # ...
main_agent = create_agent( model="...", tools=[task, list_agents], system_prompt="Use list_agents to discover available subagents, then use task to invoke them.",)子智能体输入:给它什么上下文
默认只给任务描述。但有些上下文”没法写进静态提示词”——完整消息历史、之前的结果、任务元数据——这些可以从 Agent 的 state 里取出来,传进子智能体:
from langchain.agents import AgentStatefrom langchain.tools import tool, ToolRuntime
class CustomState(AgentState): example_state_key: str
@tool("subagent1_name", description="subagent1_description")def call_subagent1(query: str, runtime: ToolRuntime[None, CustomState]): # 按需把消息"加工"成适合子智能体的输入 subagent_input = some_logic(query, runtime.state["messages"]) result = subagent1.invoke({ "messages": subagent_input, # 也可以顺便传其它 state 字段; # 但这些字段必须在"主 Agent 和子智能体"两边的 state schema 里都定义。 "example_state_key": runtime.state["example_state_key"], }) return result["messages"][-1].content关键在 some_logic(原文):
- 只传任务描述(
query)→ 子智能体保持隔离; - 传父级消息历史(
runtime.state["messages"])→ 给子智能体铺垫已有工作,适合”接手父级已经开工的事”,比如继续一次 PR 审查、接着修一个 bug。
原文还提到 Deep Agents 把这两种做法命名为 mode: "isolated"(默认)和 mode: "fork",并帮你实现了 fork:被 fork 的子智能体会收到父级的完整消息历史(包括工具调用)和系统提示。
| 模式 | 子智能体收到什么 | 最适合 | 代价 |
|---|---|---|---|
| Isolated 隔离(默认) | 只有任务描述 | 需要很少前置上下文的聚焦工作 | 上下文隔离好,但父级做过的事它要重新推导一遍 |
| Forked 分叉 | 父级的对话历史(在 Deep Agents 里还包括系统提示) | 接着父级已经开工的任务继续做 | 有前置上下文,代价是提示词更大、隔离性更弱 |
子智能体输出:让它交回什么
原文给了两种策略,目标是让主管”能拿着结果继续做决策”:
- 提示词约束子智能体:明确要求它返回什么。常见故障是——子智能体做了一堆工具调用和推理,但最终消息里没把结果写进去。记得提醒它:主管只能看到你的最终输出。
- 用代码加工:在返回前调整或补充响应。例如除了最终文本,还用
Command把特定 state 字段一起带回。
from typing import Annotatedfrom langchain.tools import InjectedToolCallIdfrom langgraph.types import Command
@tool("subagent1_name", description="subagent1_description")def call_subagent1( query: str, tool_call_id: Annotated[str, InjectedToolCallId],) -> Command: result = subagent1.invoke({"messages": [{"role": "user", "content": query}]}) return Command(update={ # 把子智能体的额外状态带回来 "example_state_key": result["example_state_key"], "messages": [ ToolMessage( content=result["messages"][-1].content, tool_call_id=tool_call_id, ) ], })🧭 补充解读(非原文):这三处阀门可以用一句话记住——specs 决定”派不派得出去”,inputs 决定”专家手上有没有料”,outputs 决定”主管能不能接得上”。多 Agent 系统调不动,八成是这三处之一没设计好。
检查点与状态查看(Checkpointing and state inspection)
底层机制(原文):
- 默认情况下子智能体使用 inherited checkpointer(继承检查点)模式:每次调用都从全新状态开始,支持 interrupt,并且可以安全地并行运行。
- 如果你需要子智能体跨多次调用保留自己的持久对话历史,就用
checkpointer=True编译它(continuations 模式)。各模式的完整对比见子图持久化。
一个很实用的坑(原文):
因为子智能体是在工具函数内部被调用的,LangGraph 无法静态发现它们。这意味着用
get_state+subgraphs拿不到子智能体的状态。如果你需要读取嵌套图的状态(比如在 interrupt 期间),应该改成在自定义图的一个节点函数里调用子智能体。
🧭 补充解读(非原文):这条对做生产级可观测性的人非常关键——如果你打算把子智能体的执行放进 LangGraph 的 checkpoint / 人工审批链路里,“包成工具”这条路会让它隐身。要在图里可见、可暂停、可恢复,就得把子智能体作为子图节点接进主图(也就是
08篇 Custom workflow 的思路)。
📎 从 langgraph-supervisor 迁移:
langgraph-supervisor包已不再积极维护。官方提供了从create_supervisor迁移到本文 subagents 模式的指南(含 interrupt/resume 与外部 API 回调的流程):Migrate from langgraph-supervisor。
小结
- 一句话实现:子智能体 =
create_agent(...)包一层@tool;主管 = 工具清单里挂着这些工具的那个 Agent。 - 五个特征:集中式控制、不与用户直连(可用 interrupt 变通)、子智能体即工具、可并行、默认无状态。
- 同步 vs 异步:主管需要结果 → 同步(简单但卡对话);用户不该等 → 异步 + 三工具模式(start / status / get result),用”通知 → 用户点一下 → 再发消息”完成闭环。
- 两种工具模式:一智能体一工具(精细控制)vs 单一
task派发工具(团队分布、约定优于配置、上下文隔离是主要收益)。 - 三处上下文阀门:specs(名字/描述,三种枚举方式)、inputs(isolated vs forked)、outputs(提示词约束 +
Command带回状态)。 - 状态可见性有坑:包成工具的子智能体 LangGraph 静态发现不了,
get_state(subgraphs=...)读不到;要读嵌套状态就放进自定义图的节点里;langgraph-supervisor已弃用,请迁移。
一句话选型
- 选它:多个独立领域(日历/邮件/CRM/数据库)、不需要子智能体直接面对用户、想要集中式管控和强上下文隔离。
- 别选它:只有少量工具的简单场景(用单 Agent);用户需要直接跟专家来回对话(用
05Handoffs);只是想做一次分类分发(用07Router)。
上一篇:03 性能对比 | 下一篇:05 Handoffs 交接 | 实战:09 用 Subagents 搭个人助理