TECH NOTE
1868 字 9 分钟 系列 14 / 2214 附录:手写 `@tool` 包装子智能体 vs Deep Agents 的 `subagents=`——同一个原理,两种实现
ON THIS PAGE / 目录6 节
一句话结论:底层原理是同一个——都是
04篇里的 Subagents 模式(主管 + 子智能体即工具 + 上下文隔离)。区别只在”包装”这件事谁来做:手写版你自己写create_agent+@tool(04篇的 tool per agent),Deep Agents 内置的是一套 单一派发工具(single dispatch tool)+ Agent 注册表 + 中间件栈——也就是04篇「决策二:工具模式」里那条约定优于配置的路。
实测日期:2026-08-30 | 环境:
deepagents0.7.5 +langchain1.3.15 | 模型:DeepSeek(deepseek-v4-flash-vision-exp→ 实际deepseek-flash)| 协议:Responses API实验脚本与抓包文件:
demo/deepagents_demo.py、demo/captured_deepagents.jsonl;运行方式见复现实验说明;手写版对照见13 附录(demo/demo.py)
一、先看结论表
| 维度 | 手写版(create_agent + @tool,04 篇 tool per agent) | Deep Agents(create_deep_agent(subagents=[...])) |
|---|---|---|
| 暴露给模型的工具 | 每个子智能体一个工具(本例 schedule_event) | 只有一个 task 工具,参数 description + subagent_type |
| 模型怎么知道有哪些子智能体 | 工具名 + 工具描述(tool description) | task 的工具描述里自动拼上一份 Agent 目录(- name: description) |
| 子智能体怎么定义 | 自己 create_agent(model, tools, system_prompt) | 传 dict(name/description/system_prompt/tools/model/…)或 CompiledSubAgent |
| 子智能体的工具 | 你显式指定 | 默认继承主 Agent 的全部工具(除非显式给 tools 覆盖) |
| 上下文隔离 | 你自己决定传什么(我们传了 request 一句话) | 内置 mode:"isolated"(只传 description),可切 mode:"fork"(带父对话历史 + 复用 prompt cache) |
| 结果怎么回到主管 | return result["messages"][-1].content | 内置:反向找最后一条非空 AIMessage → 包成 ToolMessage,并合并 state |
| 结构化输出 | 自己解析 | response_format= → 父级直接收到 JSON |
| 人工介入 / 权限 | 自己接 interrupt | 每子智能体可配 interrupt_on、permissions |
| 可观测性 | 自己打点 | 自动打 ls_agent_type="subagent";工具里能读 lc_agent_name 判断是谁在调 |
| 白送的东西 | 无 | 默认注入 general-purpose 子智能体 + 文件系统工具(ls/read/write/edit/delete/glob/grep)+ todos/规划 + 摘要压缩 + skills + memory + 子智能体工具隔离 |
| 首轮上下文成本 | 小(本例 in=347) | 大(本例 in=2622,工具 schema + 系统提示词都更重) |
| 适合 | 想精细控制、轻量、只用一种模式 | 想要一整套现成的 agent harness(规划/文件/压缩/技能/子智能体) |
二、原理相同的证据:源码里就是一个”单一派发工具”
deepagents/middleware/subagents.py(0.7.5)把 subagents= 变成了这些东西:
① 工具 schema:就是 task(description, subagent_type)
class TaskToolSchema(BaseModel): """Input schema for the `task` tool."""
description: str = Field(description=( "A detailed description of the task for the subagent to perform autonomously. " "Include all necessary context and specify the expected output format.")) subagent_type: str = Field(description=( "The type of subagent to use. Must be one of the available agent types listed in the tool description."))② Agent 目录直接拼进工具描述
subagent_description_str = "\n".join(f"- {s['name']}: {s['description']}" for s in compiled_subagents)description = TASK_TOOL_DESCRIPTION.format(available_agents=subagent_description_str)③ 隔离模式:只把任务描述塞进子智能体(和我们手写版传 request 一句话是同一个动作)
subagent_state["messages"] = [HumanMessage(content=description)]④ 结果回传:反向找最后一条非空 AIMessage,包成 ToolMessage
for msg in reversed(result["messages"]): if isinstance(msg, AIMessage): text = msg.text.rstrip() if msg.text else "" if text: content = text breakreturn Command(update={**state_update, "messages": [ToolMessage(content, tool_call_id=tool_call_id)]})⑤ 附带的可观测性:_subagent_tracing_context() 会给子智能体运行打上 ls_agent_type="subagent" 元数据。
换句话说:Deep Agents 并没有发明新机制,它是把
04篇「单一派发工具 + 注册表」那一节产品化了,再套上 Deep Agents 自己的中间件栈。
三、抓包实证:线报文里长什么样
同一次运行,暴露给模型的工具清单(Responses API 的 tools 数组,节选):
• ls params=['path']• read_file params=['file_path','offset','limit']• write_file params=['file_path','content']• edit_file params=['file_path','old_string','new_string','replace_all']• delete params=['file_path']• glob params=['pattern','path']• grep params=['pattern','path','glob','output_mode','max_count']• task params=['description','subagent_type'] ← 唯一的派发入口• save_note params=['title','body'] ← 我们自己的业务工具task 工具的描述(真实报文,节选):
Launch an ephemeral subagent to handle a complex, multi-step task in an isolated context window.
Available agent types and the tools they have access to:- general-purpose: General-purpose agent for researching complex questions, ...- research-agent: 用于需要多步调研、比较、查证的问题
Specify subagent_type to select the agent. Usage notes:- Launch multiple agents concurrently when their tasks are independent, using a single message with multiple tool calls.- Each invocation is stateless: the agent sees only the prompt you give it and returns a single final report. ...三个关键信息:① 一个工具管所有子智能体(靠 subagent_type 分发);② general-purpose 是自动注入的(我们只传了 research-agent);③ 描述里直接告诉模型”可以一次消息里并发派多个”、“每次调用是无状态的、只看到你给的那段 prompt”——这跟 04 篇讲的 specs / inputs 两个阀门一一对应。
这次运行的 4 次模型调用(形状和手写版完全一致)
| # | 谁在调模型 | 上下文 | 产出 | in / out |
|---|---|---|---|---|
| 1 | 主管 | system(技术选型助手) + 用户原话(+ 8 个工具的 schema) | task(description=…, subagent_type="research-agent") + 一句”我来交给子代理” | 2622 / 466 |
| 2 | 子智能体 | 只有它自己的 system + 主管给的任务描述(输入项数 = 2) | ls(path="/") | 2536 / 841 |
| 3 | 子智能体 | 上者 + ls 结果 | 结构化调研结论(回给主管) | 3392 / 1473 |
| 4 | 主管 | system + 用户原话 + task 调用 + 工具结果 | 面向用户的最终回答 | 4561 / 933 |
对照 13 篇手写版的 ①主管派活 → ②子智能体调工具 → ③子智能体作答 → ④主管收尾——结构完全同构。
两个额外观察:
- 子智能体默认继承主 Agent 的工具:
research-agent没写tools,于是它手里直接有ls(文件系统工具),第 2 次调用就去ls("/")找资料了。想限制它,就得显式传tools=(文档:显式指定会整体覆盖继承来的工具集)。 - 派活次数由模型决定:同一份代码的前一次运行里,主管连续调了 5 次
task(12 条消息,边调研边补细节);抓包这次只调了 1 次。所以”Deep Agents 一次请求 = 几次模型调用”没有固定答案——上限取决于模型何时认为够了。
四、什么时候用哪个
| 你的情况 | 建议 |
|---|---|
| 只需要”一个主管 + 几个专家”、想完全掌控提示词与工具边界、不想引入额外中间件 | 手写 @tool 包装(04 篇 tool per agent),代码量也就十几行 |
| 子智能体数量多、团队各自维护、希望加一个新专家”不改协调者代码” | task 单一派发工具(手写或者直接用 Deep Agents) |
| 需要规划(todos)、虚拟文件系统、超长上下文压缩、技能按需加载、权限与人工介入 | Deep Agents(这些是白送的;手写得自己拼) |
| 想让子智能体”接着父级已开工的活干” | Deep Agents mode:"fork"(手写版要自己把父级 messages 传进去) |
| 只是想让子智能体独立跑一段、要结构化结果 | 两者都行;Deep Agents 用 response_format= 更省事 |
代价提醒:Deep Agents 的首轮上下文明显更贵(本例 in=2622 vs 手写版 in=347,因为多了 7 个文件系统工具的 schema 与更重的系统提示词),而且它默认注入 general-purpose、默认让子智能体继承全部工具——这两条默认值在”工具很危险”的场景里要主动改掉(general_purpose_subagent=GeneralPurposeSubagentProfile(enabled=False) 关掉默认子智能体;给子智能体显式 tools= 收窄权限)。
五、自己复现
# 抓包跑 Deep Agents 版(脚本见 demo/deepagents_demo.py)CAPTURE_OUT=$PWD/captured_deepagents.jsonl \ /tmp/mitmenv/bin/mitmdump -q -s capture.py -p 8888 --set confdir=/tmp/subagents-demo/.mitmproxy
export CLOUD_MODEL_BASE_URL=https://api.deepseek.comexport CLOUD_MODEL_API_KEY=<你的 key>export CLOUD_MODEL_NAME=deepseek-v4-flash-vision-expexport CLOUD_MODEL_PROTOCOL=RESPONSESHTTPS_PROXY=http://127.0.0.1:8888 SSL_CERT_FILE=/tmp/subagents-demo/.mitmproxy/mitmproxy-ca-cert.pem \ python deepagents_demo.py
python summarize.py captured_deepagents.jsonl说明:两次抓包(
13篇的手写版、本篇的 Deep Agents 版)用的是同一个 mitmdump addon,抓包文件只存请求/响应正文,不含请求头与密钥。