数据截至 (上游 commit 25aa2735dabb)
子 agent:三种形态、隔离的上下文与远程任务
30 秒导读: 主 agent 的上下文窗口是最贵的资源。子 agent 就是「把一大坨探索赶进别人的上下文里干完,只把一句结论拿回来」。Deep Agents 把这件事做成三种 spec:声明式
SubAgent、直接给编译好 runnable 的CompiledSubAgent、跑在远端 LangGraph 部署上的AsyncSubAgent。本章讲清三者怎么分流、声明式那种为什么会被父层重新组栈、task工具内部到底做了哪几步、以及父子之间的 state 是怎么被裁剪的。
引用约定: 本章正文里的
path:line默认相对克隆里的 Python 包目录libs/deepagents/deepagents/——例如graph.py:647的完整路径是libs/deepagents/deepagents/graph.py:647。两个例外一律相对克隆根: 以libs/或examples/开头的引用(它们落在包目录之外),以及末尾 §11 代码地图的整张表。
前置章节:装配流水线 讲了 create_deep_agent 的整体组栈顺序,文件系统与权限 讲了 permissions 的语义。本章只讲「委派」这一条线。
1. 为什么要子 agent(先讲动机,不看代码)
一句话:上下文窗口是内存,子 agent 是把临时变量丢进函数作用域里算完就释放。
主线程里的每一条 tool 结果都是永久成本——它会一直留在 messages 里,后面每一轮都要重新发给模型。可一次「把整个仓库翻一遍找出所有用到某 API 的地方」会产生几十条 grep 结果,而你真正想要的只有最后那句「有 3 处,分别在 A/B/C」。
于是有了这条划分:
| 什么样的活该委派 | 为什么 |
|---|---|
| 探索型(搜索、翻文件、试错) | 中间过程量大,结论极短 |
| 可并行(研究三个人物、准备两份议程) | 彼此不依赖,可以同时开三个上下文 |
| 只要结果、不要过程 | 中间步骤对主线程没有复用价值 |
| 需要另一套工具/权限/模型 | 换一套装备比在主 agent 上开关工具更干净 |
反过来,官方工具描述也明说了什么时候不该委派:任务很小、需要看到中间推理、拆开反而增加延迟——这些"何时用/何时不用"的指引现在直接写在 TASK_TOOL_DESCRIPTION 里(middleware/subagents.py:285-296;旧版独立的 TASK_SYSTEM_PROMPT 已并入工具描述)。
关键约束(理解全章的钥匙): 子 agent 是一次性、单向的——父层只发一段 description 过去,子 agent 只回一条最终消息,中途不能对话。这条约束被反复写进工具描述里(middleware/subagents.py:292:"Each invocation is stateless")。正因为不能对话,父层才必须在 description 里把话说全。
用起来长这样:
# 示意,非源码
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-5",
subagents=[
{
"name": "researcher", # 父层用这个名字调 task()
"description": "深挖一个主题并给出摘要", # 模型靠这句决定要不要委派
"system_prompt": "你是研究员,返回结构化摘要。",
"tools": [web_search], # 不写就继承父层工具
}
],
)
主 agent 拿到的不是"researcher 这个 agent",而是一个叫 task 的普通工具;调用时填 subagent_type="researcher" 就行(middleware/subagents.py:272-283,TaskToolSchema)。
2. 三种形态与分流判据
这节讲:同样是 subagents=[...] 这一个参数,里面可以塞三种完全不同的东西,Deep Agents 靠字段存在性把它们分开。
2.1 三种形态对照
| 形态 | 你提供什么 | 谁来编译 | 识别字段 | 定义位置 |
|---|---|---|---|---|
SubAgent(声明式) | prompt / tools / model / middleware / skills / permissions / interrupt_on / response_format | 父层重新组栈后,由 create_sub_agent 编译 | 既无 graph_id 也无 runnable | middleware/subagents.py:36(SubAgent) |
CompiledSubAgent | 一个已编译好的 Runnable | 你自己,父层不碰 | 有 runnable | middleware/subagents.py:167(CompiledSubAgent) |
AsyncSubAgent(远程) | graph_id,可选 url / headers | 远端 LangGraph 部署 | 有 graph_id | middleware/async_subagents.py:34(AsyncSubAgent) |
三者的差别不只是"怎么造",更是运行语义不同:
| 形态 | 阻塞? | 结果怎么回来 | 归哪个中间件管 |
|---|---|---|---|
SubAgent | 阻塞,一次 invoke 打完 | 直接变成 ToolMessage | SubAgentMiddleware |
CompiledSubAgent | 阻塞 | 同上 | SubAgentMiddleware |
AsyncSubAgent | 不阻塞,立刻返回 task_id | 后续靠 check_async_task 拉 | AsyncSubAgentMiddleware |