《Deep Agents 实战笔记》第 5 篇 | 基于 deepagents (Python) 官方文档 原文:https://docs.langchain.com/oss/python/deepagents/subagents
第 1 篇埋过一个伏笔:子代理是"用上下文隔离换任务质量",而任务描述(description)的写法直接决定委派质量,官方文档甚至专门写了排错案例。这一篇就来还债——把 task 工具拆开看清楚:主代理怎么派活、子代理怎么干活、description 写坏会发生什么,以及几种进阶玩法。
一、task 工具:一次"外包"的完整时序
先纠正一个常见误解:子代理不是一个常驻进程。每次调用 task 工具,Deep Agents 都会临时生成一个一次性子代理,它带着自己的系统提示词、工具集和全新的上下文窗口去干活,干完只把结果摘要交回来,然后"人走茶凉"。
一次委派的完整时序是这样的:
sequenceDiagram
participant M as 主代理
participant T as task 工具
participant SA as 子代理
participant FS as 文件系统
M->>T: task(subagent_type, description)
Note over T: SubAgentMiddleware 根据 name<br/>找到子代理定义并实例化
T->>SA: 启动(全新上下文,只有任务描述)
SA->>FS: 多轮工具调用<br/>搜索、读文件、写笔记
Note over SA: 中间过程全部留在<br/>子代理自己的上下文里
SA-->>T: 最终回复(结果摘要)
T-->>M: 摘要作为 ToolMessage 返回
Note over M: 主代理窗口依然干净
正在渲染图表…
这里面有个容易忽略的实现细节:同步子代理能力是由 SubAgentMiddleware(以及它挂载的 task 工具)提供的,而且只有当至少存在一个子代理时它才会被挂载。这也意味着:
- 你可以传一个自己命名为
general-purpose的子代理来替换默认版本; - 想彻底禁用委派能力,要在 harness profile 上设
general_purpose_subagent=GeneralPurposeSubagentProfile(enabled=False); - 不要试图把
SubAgentMiddleware塞进excluded_middleware——它是必需的基础组件,这么干会直接抛ValueError。
什么时候该派子代理、什么时候不该?官方给了一张很实用的判断清单:
| 该用 ✅ | 不该用 ❌ |
|---|---|
| 会弄乱主代理上下文的多步骤任务 | 简单的单步骤任务 |
| 需要自定义指令或工具的专门领域 | 需要保留中间上下文时 |
| 需要不同模型能力的任务 | 开销大于收益时 |
| 希望主代理专注于高层协调时 |
第四条最反直觉:子代理不是"人多力量大"的人海战术,它的本质是上下文分区。如果你的任务本来就不产生多少中间输出,派子代理纯粹是浪费。
二、通用型 vs 专用:两种子代理定义方式
Deep Agents 默认自带一个 general-purpose(通用型)子代理:用默认系统提示词、继承主代理的工具和技能、继承主代理的模型。它适合"只需要上下文隔离、不需要专门行为"的场景——比如主代理要做 10 次网络搜索,与其自己搜到上下文爆炸,不如打包丢给通用型子代理,取回一份摘要了事。
但真正的威力在专用子代理。什么时候用通用型?官方给了一个很干净的心法:如果你要的只是"别把主代理上下文弄脏",通用型就够了——与其让主代理执行 10 次网络搜索并用结果填满自己的上下文,不如委派一句 task(name="general-purpose", task="Research quantum computing trends"),子代理在内部完成所有搜索,只返回一份摘要。什么时候上专用型?当你发现自己在系统提示词里反复强调"研究要引用来源"、"报告不超过 500 字"这类专项纪律时,就该把它沉淀成一个专门的子代理了。
定义方式是基于字典的 SubAgent 规范,核心字段:
| 字段 | 说明 |
|---|---|
name | 唯一标识符,主代理调 task() 时用它点名 |
description | 职责描述,主代理据此决定何时委派 |
system_prompt | 子代理的指令(isolated 模式必填,不从主代理继承) |
tools | 默认继承主代理,一旦指定则完全覆盖 |
model | 可覆盖主代理的模型 |
skills | 子代理专属技能目录,不继承主代理 |
response_format | 结构化输出 schema,回传 JSON 而非自由文本 |
mode | isolated(默认)或 fork,控制上下文继承,见第五节 |
interrupt_on | 为特定工具配置人工审批(HITL),默认继承主代理 |
permissions | 文件系统权限规则,设置后完全替换父代理的权限 |
一个最典型的"研究员"定义:
research_subagent = {
"name": "research-agent",
"description": "Conducts in-depth research using web search and synthesizes findings",
"system_prompt": """You are a thorough researcher. Your job is to:
1. Break down the research question into searchable queries
2. Use internet_search to find relevant information
3. Synthesize findings into a comprehensive but concise summary
4. Cite sources when making claims
Output format:
- Summary (2-3 paragraphs)
- Key findings (bullet points)
- Sources (with URLs)
Keep your response under 500 words to maintain clean context.""",
"tools": [internet_search],
}
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
subagents=[research_subagent],
)
注意三个设计决策:
- tools 只给了搜索。一旦指定
tools,继承的工具会被完全覆盖——这是故意的,工具越少,子代理越专注,也越安全。官方的负面示例是给email-sender塞进send_email、web_search_tool、database_query、format_document一大堆不相干工具,专注度立刻崩掉。 - system_prompt 写了输出格式和字数上限。"Keep your response under 500 words" 这句很关键——子代理回传的东西会直接进主代理上下文,回传越克制,主代理越持久。
- 模型可以按需换。主代理用 Gemini,研究员子代理可以换
openai:gpt-5.5。官方的最佳实践是按任务选模型:审合同的用大上下文模型,做数值分析的用强推理模型。
三、description 好坏对比:一行字决定的翻车现场
现在说那个官方排错案例。description 是主代理决定"何时委派给谁"的唯一依据——注意,不是系统提示词,不是工具名,就是这一行 description。写坏了会发生什么?
官方给的正反对比非常直白:
# ❌ 差的:主代理完全无法判断什么时候该派它
AsyncSubAgent(
name="helper",
description="helps with stuff",
graph_id="helper",
)
# ✅ 好的:说清楚做什么、用什么手段、什么场景用
AsyncSubAgent(
name="researcher",
description="Conducts in-depth research using web search. Use for questions requiring multiple searches and synthesis.",
graph_id="researcher",
)
另一组(同步子代理文档里):
✅ "Analyzes financial data and generates investment insights with confidence scores"
❌ "Does finance stuff"
戏剧性在于后果的隐蔽。你跑一个 "Does finance stuff" 版本的 Agent,它不会报错。症状是间接的:
- 主代理收到"分析这份财报"的请求,看着 "Does finance stuff" 这行字,不知道这算不算 "finance stuff",于是决定自己上手——结果 20 次工具调用塞满了主代理上下文,子代理形同虚设;
- 或者反过来,你定义了三个子代理,description 都写得含糊,主代理随机点名,把合同审查派给了财务分析代理,输出驴唇不对马嘴;
- 你排查时盯着 trace 看,只看到"委派没发生"或"派错人了",没有任何异常堆栈。
这就是 Agent 工程里最磨人的一类 bug:**配置语法全对,行为全错,错误源头只有一行字符串。**我的经验是:description 要写成"动作 + 手段 + 适用场景"的三段式,像上面 researcher 那条——它是动作(conducts research)、手段(using web search)、场景(for questions requiring multiple searches)的完整组合。写完 description 做个自测:**假装你是只看得到这一行字的主代理,你能在三秒内决定"这事该不该派给它"吗?**不能,就重写。
顺便一提,调试这类问题 LangSmith 有个杀手锏:每个子代理产生的 run 都会在元数据里写入 lc_agent_name 键,你可以用元数据过滤器筛出某个子代理的所有 run,一眼看出"它到底被派过几次、干了什么":
from langsmith import Client
client = Client()
runs = client.list_runs(
project_name="<your-project>",
filter='has(metadata, \'{"lc_agent_name": "research-agent"}\')',
)
四、共享文件系统:子代理之间的"协作黑板"
isolated 模式下子代理看不到主代理的对话历史,那信息怎么传?除了任务描述和结果摘要这条"正门",还有一条侧门:文件系统。
Deep Agents 的文件工具背后是同一套可插拔 Backend(第 4 篇讲过),主代理和子代理默认共享这个虚拟文件系统。这就打开了一种中间层协作模式——把文件系统当黑板用:
- 主代理把任务背景、领域材料写进
/workspace/brief.md,任务描述里只需说"先读 brief.md"; - 多个子代理各自把产出写到不同路径,后来的子代理可以
read_file读前人的成果,不必通过主代理中转; - 中间产物落盘,谁需要谁去读,任何一方的上下文都不用背着别人的原始数据。
flowchart TB
M["主代理<br/>写任务简报 brief.md"]
subgraph BOARD["共享虚拟文件系统(协作黑板)"]
B1["brief.md<br/>任务简报与背景材料"]
B2["notes-research.md<br/>研究员的中间产出"]
B3["notes-data.md<br/>数据组的中间产出"]
end
M -->|"write brief.md"| B1
SA["子代理 A(研究)"] -->|"read"| B1
SA -->|"write"| B2
SB["子代理 B(分析)"] -->|"read"| B1
SB -->|"read"| B2
SB -->|"write"| B3
M -->|"read 摘要与结论"| B3
style M fill:#eef4ff,color:#1a3a8b,stroke:#4a72d9
style BOARD fill:#f4effb,color:#4a2170,stroke:#8b52c9
正在渲染图表…
不过有两点边界要拎清楚。第一,技能状态是双向完全隔离的:子代理配置了 skills 后运行自己独立的 SkillsMiddleware,它加载的技能对父代理不可见,反之亦然;只有通用型子代理会继承主代理的技能。第二,如果子代理的任务本质上是"延续主代理已经开始的工作",硬靠黑板传上下文就很绕——这正是 fork 模式的用武之地。
五、fork 模式:把父代理的上下文"分叉"给子代理
默认的 isolated 模式下,子代理只看得到任务描述,从零开始。而 mode: "fork" 的子代理会继承父代理的对话历史和状态——机制很精巧:fork 收到的就是父代理自己的消息列表,唯一改动是把末尾那次 task 调用替换成一句简短前言("Continuing as the subagent that was just invoked..."),标记"以下是延续而非新请求"。fork 干完后答案作为普通工具结果返回,父代理无缝续接。
comment_writer = {
"name": "comment-writer",
"description": "Continues an in-progress PR review and drafts review comments",
"mode": "fork",
"tools": [read_diff],
}
官方给的适用场景很形象:主代理已经审查了 PR、发现了两个安全问题(token 明文落日志、refresh 无过期校验),这时候派一个 fork 子代理去"针对已发现的问题起草评审意见"——isolated 子代理会傻乎乎地把 diff 重新审一遍,而 fork 子代理已经知道问题在哪,直接动笔。isolated 与 fork 的取舍:
| 维度 | isolated(默认) | fork |
|---|---|---|
| 上下文 | 只有任务描述 | 父代理的对话历史和状态 |
| 调用其他子代理 | 可以用 task | 不能,必须自己干活 |
| 最适合 | 几乎不需要先前上下文的聚焦型工作 | 延续父代理已经开始的调查 |
一个工程细节:fork 可以复用父代理的提示词缓存(匹配前缀),但前提是你别乱改系统提示词、工具定义这些共享前缀——改了缓存命中率就掉了,成本跟着涨。另外 fork 目前是 beta 功能,要求 deepagents>=0.7.13。
六、中间层子代理:流水线式分工
子代理不该只有"主代理 → 子代理"一层。官方的常见模式展示了三级流水线:每个环节都是专门化子代理,前一环的产出是后一环的输入。
flowchart LR
U["用户请求"] --> M["主代理<br/>只做协调"]
M -->|"task: 收集数据"| C["data-collector<br/>搜索 / API / 数据库"]
C -->|"原始数据"| M
M -->|"task: 分析"| A["data-analyzer<br/>统计分析"]
A -->|"关键洞察"| M
M -->|"task: 写报告"| W["report-writer<br/>格式化输出"]
W -->|"成稿"| M
M --> O["最终报告"]
style M fill:#eef4ff,color:#1a3a8b,stroke:#4a72d9
style C fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
style A fill:#fbf3e6,color:#6b4a12,stroke:#c99a3d
style W fill:#fdeef0,color:#701d2a,stroke:#c94a5e
正在渲染图表…
subagents = [
{
"name": "data-collector",
"description": "Gathers raw data from various sources",
"system_prompt": "Collect comprehensive data on the topic",
"tools": [web_search_tool, api_call, database_query],
},
{
"name": "data-analyzer",
"description": "Analyzes collected data for insights",
"system_prompt": "Analyze data and extract key insights",
"tools": [statistical_analysis],
},
{
"name": "report-writer",
"description": "Writes polished reports from analysis",
"system_prompt": "Create professional reports from insights",
"tools": [format_document],
},
]
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
system_prompt="You coordinate data analysis and reporting. Use subagents for specialized tasks.",
subagents=subagents,
)
这个模式的精髓是主代理的 system_prompt:**"You coordinate..."——明确告诉它自己是协调者,不是执行者。**配合每个环节的克制回传(官方建议让子代理只返回关键洞察、置信度、建议动作,不要原始数据和中间计算),整条流水线跑下来,主代理的上下文增量只有三段摘要。"克制回传"本身可以固化成系统提示词模板,官方给的数据分析员版本就很值得直接抄用:
data_analyst = {
"system_prompt": """Analyze the data and return:
1. Key insights (3-5 bullet points)
2. Overall confidence score
3. Recommended next actions
Do NOT include:
- Raw data
- Intermediate calculations
- Detailed tool outputs
Keep response under 300 words."""
}
配合 response_format 还能更进一步:给子代理传一个 Pydantic schema,父代理收到的就是可解析的 JSON 而非自由文本(要求 deepagents>=0.5.3):
from pydantic import BaseModel, Field
class ResearchFindings(BaseModel):
"""Structured findings from a research task."""
summary: str = Field(description="Summary of findings")
confidence: float = Field(description="Confidence score from 0 to 1")
sources: list[str] = Field(description="List of source URLs")
research_subagent = {
"name": "researcher",
"description": "Researches topics and returns structured findings",
"system_prompt": "Research the given topic thoroughly. Return your findings.",
"tools": [web_search],
"response_format": ResearchFindings,
}
七、编译型子代理:把整张 LangGraph 图塞进去
字典式定义最终都会被包装成标准的子代理循环。如果你需要完全控制执行流程——多节点图、条件分支、自定义状态——可以用 CompiledSubAgent 把一张已编译的 LangGraph 图直接注册为子代理:
from deepagents import CompiledSubAgent, create_deep_agent
from langchain.agents import create_agent
custom_graph = create_agent(
model=your_model,
tools=specialized_tools,
system_prompt="You are a specialized agent for data analysis...",
)
custom_subagent = CompiledSubAgent(
name="data-analyzer",
description="Specialized agent for complex data analysis tasks",
runnable=custom_graph,
)
唯一的技术约束:自定义图必须包含名为 messages 的 state key,否则父代理拿不到它的输出。CompiledSubAgent 同样支持 mode: "fork",但由于图在构建时就定型了,它会保留自己的系统提示词。
什么时候值得上编译型?我的判断标准是:当"子代理"需要的是确定性的流程控制而不是"一个带工具的 LLM 循环"时——比如多节点审批链、带重试分支的数据管道、需要自定义 state 字段的图。字典式定义做不到这些,硬塞进系统提示词里让模型"自觉遵守"只会得到概率性的服从;而图是代码,代码不会耍赖。反过来说,如果只是换套提示词加几个工具,字典式定义三行搞定,别为了用图而用图。
到这里,同步子代理的完整版图就拼齐了:通用型兜底、专用型分工、fork 继承上下文、黑板共享文件、流水线多级协作、编译型自由定制。但你可能已经发现一个别扭的地方——同步子代理是阻塞的:主代理派出去一个就得干等一个,三个子代理也只能排队跑。想让它们真正并发起来、派出去就不管?下一篇讲异步子代理和动态编排。
本篇踩坑点
- description 含糊不报错,只烂掉:
"Does finance stuff"式的描述会让主代理不派或乱派,用 LangSmith 的lc_agent_name元数据过滤器验证委派是否真的发生; - 指定
tools是覆盖不是追加:想保留主代理工具再添几个?做不到,列表一旦指定就完全替换继承的工具; - 自定义 isolated 子代理必须写
system_prompt:它不从主代理继承,漏了会直接报错;fork 模式则相反,system_prompt要尽量省略(追加会破坏提示词缓存); - 回传不克制等于白隔离:子代理系统提示词里要明确"返回摘要、不要原始数据、限制字数",否则主代理的上下文照样被撑爆;
- 别用
excluded_middleware禁用子代理:SubAgentMiddleware是必需组件,排除它会抛ValueError,正确姿势是 profile 里enabled=False。
下一篇预告:《06 | 子代理(下):异步与动态编排》——异步子代理让主代理"派出去就不管",后台跑任务、完成自动通知、中途改需求、随时叫停;动态子代理更进一步,让代理在解释器代码里用循环和并行批量派发,解锁扇出、锦标赛、对抗评审等编排模式。还会手把手写一个"3 个子代理对抗评审"的实战 demo。
参考资料:
- Subagents(官方文档,本文主要素材):https://docs.langchain.com/oss/python/deepagents/subagents
- Async Subagents(下一篇素材):https://docs.langchain.com/oss/python/deepagents/async-subagents
- Deep Agents Overview:https://docs.langchain.com/oss/python/deepagents/overview