跳到正文

TECH NOTE

6132 字 31 分钟 系列 18 / 22

01 Subagents:Deep Agents 的子智能体(内置 task 工具 + 一整套配置面)

ON THIS PAGE / 目录31 节

一句话讲透:主管像项目经理,子智能体像外包团队——你把一整包脏活丢出去,只收回一页结论文档;中间那几十次搜索、读文件、试错重来,全留在外包团队的办公室里,不进你的会议纪要。

它解决什么问题#

主管 Agent 的上下文窗口是最贵的资源。一次 web 搜索回来 3000 token,读一个文件 8000 token,跑十次窗口就满了——而这些中间结果 99% 是一次性的,主管真正需要的只是最后那句结论。

子智能体干的就是上下文隔离(context quarantine):脏活在一个独立上下文窗口里干完,只把最后一条 AIMessage 交回主管。

该用(✅):多步骤任务,中间产物会把主管上下文撑爆;需要专门指令或专门工具的领域(合同审查、SQL 查询、代码审查);不同任务需要不同模型能力(长文档用大窗口模型,数值分析换另一个);你希望主管只当”协调者”,专注高层调度。

不该用(❌):简单的一步任务——派活本身就是一次额外的 LLM 调用;你需要保留中间上下文(子智能体不回传中间过程);收益盖不过开销,调用次数、延迟、排错复杂度都会涨。

📌 注意:本篇只讲同步子智能体(主管阻塞等它跑完)。长任务、并行工作流、需要中途干预/取消的场景看 03 异步子智能体。调用次数的账见 03 性能对比。

整体机制#

graph TB
Main["主管 Supervisor<br/>(主 Agent)"] -->|"task 工具<br/>subagent_type=xxx"| Sub["子智能体 Subagent<br/>独立上下文窗口"]
Sub --> R["研究 Research"]
Sub --> C["编码 Code"]
Sub --> G["通用 General-purpose"]
R --> Res["最终结果 Final Result<br/>只回一条摘要"]
C --> Res
G --> Res
Res -->|"作为 ToolMessage 回传"| Main

这张图在说什么:

① 只有一个入口:主管永远通过内置的 task(description, subagent_type) 工具派活。subagent_type 填的就是子智能体的 name。
② 上下文在两个箭头处断开:向下的箭头只携带那段 description,向上的箭头只携带一条最终消息。子智能体中间调了什么工具、失败了几次,主管一概看不见。
③ 子智能体之间互不通信:三个平级的隔离房间,唯一的共享总线是主管;要串联就得分两轮派活。

┌────────────────────────────────────────────────┐
│ 主管 Supervisor(主 Agent) │
│ context = [用户问题] [task 调用] [子智能体摘要] │
└───────────────────┬────────────────────────────┘
│ task(description, subagent_type)
▼
┌────────────────────────────────────────┐
│ 子智能体 Subagent(隔离的上下文窗口) │
│ search() → 12 条结果 ┐ │
│ read_file() → 3 个大文件 ├─ 全是脏活 │
│ 重试 8 轮 → 全是噪音 ┘ │
└──────────────────┬─────────────────────┘
│ 只回最后一条 AIMessage
▼ 主管 context 只多了这一小段摘要

和上层 04 / 14 篇什么关系#

  • 04 Subagents 子智能体 讲模式原理:为什么是”主管 + 子智能体即工具”,上下文隔离为什么成立。
  • 14 手写 Tool 包装 vs Deep Agents 内置 subagents 用源码和抓包证明 subagents= 展开后就是一个 task 工具加一张注册表。
  • 本篇不重复以上原理,只回答工程问题:每个字段怎么填、默认值是什么、什么时候继承什么时候覆盖、fork 怎么配、怎么流式、怎么排错。

配置面(SubAgent 字段全表)#

subagents= 收一个列表,元素可以是 SubAgent 字典,也可以是 CompiledSubAgent。字典式 SubAgent 的全部字段如下。

字段类型必填性默认行为说人话坑
namestr必填无子智能体的唯一 ID,主管调 task(subagent_type=...) 用的就是它会写进 AIMessage 元数据和 LangSmith 的 lc_agent_name,改名等于改过滤条件;叫 general-purpose 时是”替换默认那个”而非新增
descriptionstr必填无写给主管看的”什么时候该派我”说明书主管只靠它做路由,写成 "helps with stuff" = 永远不被调用
system_promptstrmode="isolated" 时必填不继承主 Agent写给子智能体自己看的”怎么干活、输出什么格式”不填就是空提示词,输出格式全靠运气;mode="fork" 时退化成可选追加段
mode"isolated""fork"可选"isolated"(只看到派活描述)决定子智能体开局看到什么:一段任务描述,还是父级整段对话
toolslist可选继承主 Agent 的全部工具子智能体的工具白名单一旦写了就整体覆盖而非追加;想保留文件工具得自己塞 FilesystemMiddleware
modelstrBaseChatModel可选继承主 Agent 的模型给某个子智能体单独换便宜模型或长窗口模型
middlewarelist可选不继承,只叠加自己的给子智能体加日志、限流、文件工具白名单按 .name 合并进默认栈:同名替换默认项,异名插在核心中间件之后;名字写错会静默失效
interrupt_ondict可选继承主 Agent,子智能体值可覆盖子智能体的工具级人工审批没有 checkpointer 时形同虚设;值可以是 True / False / InterruptOnConfig
skillslist[str]可选不继承(唯一例外是 GP 子智能体)给这个子智能体单独挂技能目录双向完全隔离:父看不到子的技能,子也看不到父的;只有传了才创建 SkillsMiddleware
response_formatPydantic / Strategy / dict可选无(父级收到最后一条文本)让子智能体回结构化 JSON需 >=0.5.3;父级拿到的是字符串化 JSON,还得自己 json.loads
permissionslist[FilesystemPermission]可选继承父级限制子智能体只能读写哪些路径设置就整体替换父级规则;按声明顺序匹配,第一条命中即生效

继承 vs 覆盖:最容易踩的四个字段#

全篇最值钱的就是这张表——“我以为它叠加,其实它替换”。

字段不写时写了时
tools继承主 Agent 全部工具整体覆盖,不是追加
interrupt_on继承主 Agent子智能体值覆盖默认
skills不继承(只有 GP 子智能体继承)用子智能体自己的技能目录
permissions继承父级整体替换父级规则,不是合并
middleware不继承按 .name 合并进默认栈
model / response_format分别继承模型 / 无覆盖
# 主 Agent 的工具是 [search, read_file, write_file]
# 下面这个子智能体只会拿到 [search],文件工具全没了
searcher = {
"name": "searcher",
"description": "只做网络检索并返回摘要",
"system_prompt": "用 search 检索,返回不超过 200 字的摘要。",
"tools": [search], # 整体覆盖,不是"在继承基础上再加一个 search"
}
# 想保留文件工具,就得自己把 FilesystemMiddleware 带进来
from deepagents import FilesystemMiddleware
searcher_with_fs = {
**searcher,
"middleware": [FilesystemMiddleware(tools=["read_file", "write_file"])],
}

两种定义方式#

字典式 SubAgent#

就是上面那张表的字段全集,90% 的场景用它就够了。声明式、可序列化,字段语义由 Deep Agents 负责翻译成中间件栈。

CompiledSubAgent#

要更复杂的编排(自己的图、自己的循环、非 create_agent 的结构)时,把编译好的 LangGraph 图塞进去:

from deepagents import CompiledSubAgent, create_deep_agent
from langchain.agents import create_agent
# ① 先用 create_agent 或 LangGraph 图 API 编译一个 agent
custom_graph = create_agent(
model="openai:gpt-5.5", tools=[analyze_contract], system_prompt="你是数据分析专家……"
)
# ② 再包成 CompiledSubAgent;runnable 必须已经 .compile()
custom_subagent = CompiledSubAgent(
name="data-analyzer",
description="分析结构化数据并给出结论",
runnable=custom_graph,
)
agent = create_deep_agent(model="openai:gpt-5.5", subagents=[custom_subagent])

字段只有四个:name、description、runnable,以及 fork 模式下的 mode。三条硬性要求:

  • runnable 必须是已经 .compile() 过的图,不能是未编译的 builder。
  • 图的 state schema 必须含 messages 键——子智能体就是靠它把结果传回主管的,缺了这条回传链路直接断。
  • 它是原样使用的:不继承 create_deep_agent(state_schema=...),要自定义 state 字段得自己用兼容 schema 编译。

结果回传规则和字典式一致:state 里 structured_response 非 None 就序列化成 JSON 当 ToolMessage 内容;否则取最后一条非空 AIMessage 的文本。

怎么选:能用字典就用字典——字段有默认值、自动接中间件栈、后续升级不容易被内部实现变更打到。只有字典表达不了的图结构才上 CompiledSubAgent。

默认的 general-purpose 子智能体#

只要你没有提供一个叫 general-purpose 的同步子智能体,Deep Agents 就自动注入一个。它天生具备:自己的默认 system prompt(叠加 profile 覆盖)、和主 Agent 一样的全部工具、一样的模型(除非你覆盖),以及配置了 skills 时继承主 Agent 的 skills。

替换、改名、关掉#

替换——在 subagents 里放一个 name="general-purpose" 的项,默认那个就不注入了:

agent = create_deep_agent(
model="openai:gpt-5.5",
tools=[internet_search],
subagents=[
{
"name": "general-purpose", # 同名 = 替换默认项
"description": "通用研究员,负责多步检索与综合",
"system_prompt": "你是通用助手,检索后只回结论。",
"tools": [internet_search],
"model": "openai:gpt-5.5", # 给委派任务单独指定模型
},
],
)

改名 / 改提示词——不要动 subagents,改当前生效的 harness profile。0.7.5 里 profile 用 register_harness_profile 按 provider:model 或 provider 注册:

from deepagents import (
GeneralPurposeSubagentProfile,
HarnessProfile,
register_harness_profile,
)
register_harness_profile(
"openai:gpt-5.5",
HarnessProfile(
general_purpose_subagent=GeneralPurposeSubagentProfile(
description="通用研究助手,适合需要多轮检索的复杂问题",
)
),
)

关掉——两件事一起做,缺一不可:① 在生效的 harness profile 上设 GeneralPurposeSubagentProfile(enabled=False);② 调 create_deep_agent 时 subagents= 不传任何同步子智能体。因为 Deep Agents 只有在至少存在一个同步子智能体时才挂 SubAgentMiddleware 和 task 工具。两个条件都满足,Agent 才真正没有任何委派能力。(异步子智能体不受影响,它走自己的中间件和工具。)

💡 提示:别想着用 excluded_middleware 绕。SubAgentMiddleware 是必需脚手架,把它列进去会直接抛 ValueError。general_purpose_subagent.enabled = False 才是官方路径。

什么时候该用它#

纯粹为了隔离上下文、不需要专门行为时,它就是最优解。主管不用自己做 10 次 web 搜索,一句 task(subagent_type="general-purpose", description="调研量子计算趋势") 就够了:子智能体在内部做完所有检索,只回一段摘要。

skills 继承规则#

  • general-purpose 子智能体:自动继承主 Agent 的 skills
  • 自定义子智能体:默认不继承,要用 skills= 显式给

只有配了 skills 的子智能体才会拿到 SkillsMiddleware 实例,技能状态双向完全隔离。

research_subagent = {
"name": "researcher",
"description": "带专门技能的研究助手",
"system_prompt": "你是研究员。",
"skills": ["/skills/researcher/"], # 只吃自己的技能目录
}
agent = create_deep_agent(
model="openai:gpt-5.5",
skills=["/skills/main/"], # 主 Agent 和 GP 子智能体吃这份
subagents=[research_subagent], # researcher 只吃自己那份
)

分叉子智能体(mode: "fork")#

⚠️ 版本要求:fork 需要 deepagents>=0.7.13,且官方标注为 beta,API 和行为可能随版本变化。本机装的是 deepagents 0.7.5,实测 SubAgent TypedDict 里根本没有 mode 字段,包内也没有任何 fork 实现——fork 在本机不可用。 下面内容来自官方文档,落地前必须先升级版本。

默认 mode: "isolated":子智能体只看到你给的那段任务描述,对”事情是怎么走到这一步的”一无所知。fork 反过来,它继承父级的完整对话历史和一模一样的 system prompt。

什么时候需要 fork?当子智能体的任务是接着父级已经开的头往下做——父级已经诊断出问题,让子智能体去写修复;父级查完事故,让子智能体去起草复盘报告。fork 是定义子智能体时就定下来的模式,不是调用时临时选的。

配置#

在 SubAgent 上设 mode: "fork"。除了 skills,其余字段都可用(name、description、tools、model、middleware、interrupt_on、permissions、response_format):

from deepagents import create_deep_agent
def read_diff(path: str) -> str:
"""读取某个文件的 diff。"""
return f"diff for {path}"
comment_writer = {
"name": "comment-writer",
"description": "接着正在进行的 PR 评审,起草评审意见",
"mode": "fork",
"tools": [read_diff],
# 不写 system_prompt:fork 自带父级的,追加会破坏提示词缓存
}
agent = create_deep_agent("openai:gpt-5.5", tools=[read_diff], subagents=[comment_writer])

两个额外规则:skills 会被拒绝(fork 继承的是父级的对话,不是父级之外的技能);system_prompt 允许但不建议,它会被追加到父级继承来的 prompt 后面作为补充段,而这通常会打断提示词缓存(prompt cache)——除非确实有 fork 专属指令,否则留空。

CompiledSubAgent 也支持 mode: "fork",但它保持自己那份 system prompt,因为图已经编译好了。

它到底看到什么#

fork 拿到的不是一份新任务描述,而是父级自己的对话,只改一处:丢掉最后那条触发派活的 AI 消息,换成一段短前言,声明上面的消息是”接续”而不是新请求。

# 父级在派活前一刻拥有的消息列表
[
HumanMessage("看看 PR #482 的改动"),
AIMessage("", tool_calls=[{"name": "read_diff", "args": {"path": "src/auth/session.py"}}]),
ToolMessage("- session token 明文打日志\n- refresh 时没校验过期", tool_call_id="1"),
AIMessage("发现两个问题:token 明文打日志、refresh 没校验过期。"),
HumanMessage("找得好。给这两个问题起草评审意见。"),
AIMessage("", tool_calls=[{"name": "task", "args": {"subagent_type": "comment-writer", "description": "给上面发现的两个问题起草评审意见。"}}]),
]
# fork 实际看到的消息列表
[
HumanMessage("看看 PR #482 的改动"),
AIMessage("", tool_calls=[{"name": "read_diff", "args": {"path": "src/auth/session.py"}}]),
ToolMessage("- session token 明文打日志\n- refresh 时没校验过期", tool_call_id="1"),
AIMessage("发现两个问题:token 明文打日志、refresh 没校验过期。"),
HumanMessage("找得好。给这两个问题起草评审意见。"),
HumanMessage("你现在作为刚被调用的子智能体继续。给上面发现的两个问题起草评审意见。"),
]

差异只有两条,但很关键:

① 最后那条 AIMessage 被删掉了。那条消息本身就是”调用 task”这个动作,留着会让 fork 看到”我要派活给自己”这种自指结构。删掉之后,对话历史上最后一个发言者变成 HumanMessage,fork 自然就会接着往下答。 ② 换成一段 continuation 前言。原文里那段派活的 description 被改写成 HumanMessage,措辞是”你现在作为刚被调用的子智能体继续”——把语义从”这是一个新请求”提升成”这是刚才那件事的延续”。

fork 跑完后,它的回答作为一条普通工具结果回到父级,父级从原来断掉的地方无缝接上。

顺带的收益:提示词缓存。前缀和父级完全一致,fork 可以直接复用父级已经建立的缓存,不用冷启动。但如果 fork 用了一堆父级没用过的工具,导致工具定义段不同,缓存还是会 miss。

fork 还是 isolated#

维度isolated(默认)fork
上下文只有你传进去的任务描述父级完整对话历史 + 同款 system prompt
system prompt / skills都在子智能体上自己设skills 不可设;system prompt 只会追加到父级的(破坏缓存,通常留空)
能不能再派活能用 task 工具不能用 task,必须自己把活干完
最适合不太依赖前情提要的独立工作接续父级已经开始的调查

动态子智能体(只指路)#

原文还讲了一种走解释器运行时的玩法:挂上解释器中间件后,Agent 可以从代码里派子智能体——循环、分支、批量并行,程序化地汇总结果,这叫动态子智能体(dynamic subagents)。

📌 注意:这条路依赖解释器运行时(interpreter runtime),官方标注 beta,且本机 0.7.5 包内没有解释器运行时,本篇不展开。细节见 02 动态子智能体。

结构化输出#

给子智能体设 response_format,父级收到的就不再是自由文本,而是可解析的 JSON。子智能体跑完时,它的结构化响应会被 JSON 序列化,作为 ToolMessage 的内容交给父级。

📌 注意:子智能体的结构化输出需要 deepagents>=0.5.3(本机 0.7.5 满足)。

schema 接受 create_agent 支持的一切:Pydantic 模型、ToolStrategy(...)、ProviderStrategy(...),或者裸 schema 类型。

from pydantic import BaseModel, Field
from deepagents import create_deep_agent
class ResearchFindings(BaseModel):
"""研究任务的结构化结论。"""
summary: str = Field(description="结论摘要")
confidence: float = Field(description="置信度,0 到 1")
sources: list[str] = Field(description="来源 URL 列表")
research_subagent = {
"name": "researcher",
"description": "研究话题并返回结构化结论",
"system_prompt": "彻底研究给定话题,返回你的结论。",
"tools": [web_search], # 你自己的检索工具
"response_format": ResearchFindings, # 父级拿到的是 JSON 字符串
}
agent = create_deep_agent(model="openai:gpt-5.5", subagents=[research_subagent])
# 父级 ToolMessage 内容是序列化 JSON:
# '{"summary": "...", "confidence": 0.87, "sources": ["https://..."]}'

两个实操要点:父级拿到的是字符串不是 dict,用它之前记得 json.loads;结构化输出会顶掉默认的”取最后一条消息文本”逻辑,两者不叠加。

流式与可观测性#

子智能体进度流#

用 stream_events 能拿到分类型的投影——messages、subagents、tool_calls 各自独立迭代。最简单的用法是迭代 stream.subagents,跟踪每个委派任务从启动、运行到完成的全程:每个 handle 上有 .name、.messages、.tool_calls、.output、.status。

stream = agent.stream_events(
{"messages": [{"role": "user", "content": "调研一个量子计算的最新进展。"}]},
version="v3",
)
coordinator_messages: list[str] = []
for name, item in stream.interleave("messages", "subagents"):
if name == "messages":
print("[coordinator]", item.text) # 主管自己的输出
coordinator_messages.append(item.text)
else:
print(f"[{item.name}] started") # 子智能体开始干活
for message in item.messages:
print(f"[{item.name}]", message.text)
print(f"[{item.name}] status: {item.status}")

要看见子智能体真的被调用,通常还得在主管的 system prompt 里下命令,比如”你是项目协调者,自己不掌握研究知识;每个用户请求都必须调用 task() 并把 subagent_type 设为 research-agent,绝不自己回答研究问题”。

原文这里给每个 provider 各写了一遍完整示例,逻辑完全相同,换 provider 只改 model 这一行:

Providermodel= 写法
Googlegoogle_genai:gemini-3.6-flash
OpenAIopenai:gpt-5.5
Anthropicanthropic:claude-sonnet-5
OpenRouteropenrouter:z-ai/glm-5.2
Fireworksfireworks:accounts/fireworks/models/glm-5p2
Basetenbaseten:zai-org/GLM-5.2
Ollamaollama:north-mini-code-1.0

LangSmith 里的子智能体标记#

子智能体跑出来的每一个 run,都会在自己的 metadata 里带上 lc_agent_name,值就是子智能体的 name(例如 {'lc_agent_name': 'research-agent'})。主管的 run 也带这个名字。这就是”按子智能体过滤”的全部原理。

📎 原文附了一个公开的 LangSmith trace 示例,可以对照看主管 trace 和子智能体 run 的差异:https://smith.langchain.com/public/8690f61a-2e91-442f-b772-589ad1e807c7/r(fork 示例)

UI 里过滤:打开 tracing 项目 → 切到 Runs 视图 → Add filter 选 Metadata → Key 填 lc_agent_name,Value 填子智能体名(如 coordinator)。筛出来的就是这个子智能体产出的全部 run,可以存成命名视图复用。

SDK 里过滤:用 has 比较器匹配 metadata 键值对。

from langsmith import Client
client = Client()
# 只捞某个子智能体的 run
runs = client.list_runs(
project_name="<your-project>",
filter='has(metadata, \'{"lc_agent_name": "research-agent"}\')',
)
# 捞"任意命名子智能体"的 run(只要带 lc_agent_name 键),以此排除主 Agent
subagent_runs = client.list_runs(
project_name="<your-project>",
filter="has(metadata, 'lc_agent_name')",
)

同一个工具在多个智能体间共用#

工具里能读到”我是谁”,靠的就是同一个 lc_agent_name,只是这次从 runtime.config 的 metadata 里取:

from langchain.tools import ToolRuntime, tool
@tool
def shared_lookup(query: str, runtime: ToolRuntime) -> str:
"""查询信息。"""
agent_name = runtime.config.get("metadata", {}).get("lc_agent_name")
if agent_name == "fact-checker":
return strict_lookup(query)
return general_lookup(query)

另外两条运行期事实:runtime context 会自动往下透传——父级 invoke(context=...) 传的那份值,每个子智能体 run 和它内部的工具都能通过 runtime.context 读到(要按子智能体区分,就在 context 类型上开独立字段,或用 researcher:max_depth 这种命名空间 key)。所以 runtime.context 管”每个智能体不同的设置”,runtime.config 的 metadata 管”我是谁”,两者可以组合使用。

最佳实践#

description 要写清楚#

主管就是靠它做路由决策的。✅ 好:"分析财务数据并生成带置信度的投资洞察";❌ 差:"搞金融的"。

system prompt 要详细#

把工具用法、步骤和输出格式都写进去:

research_subagent = {
"name": "research-agent",
"description": "用 web 搜索做深度研究并综合结论",
"system_prompt": """你是严谨的研究员。你的任务是:
1. 把研究问题拆成可搜索的查询
2. 用 internet_search 找相关信息
3. 综合成全面但精简的摘要
4. 下结论时标注来源
输出格式:摘要(2-3 段)+ 关键发现(要点)+ 来源(带 URL)
回答控制在 500 字以内,保持上下文干净。""",
"tools": [internet_search],
}

工具集要最小#

只给真正需要的工具,既提升专注度也改善安全性:

# ✅ 好:聚焦的工具集
email_agent = {"name": "email-sender", "tools": [send_email, validate_email]}
# ❌ 差:工具太多太杂
email_agent_bad = {
"name": "email-sender",
"tools": [send_email, web_search, database_query, format_document],
}

按任务选模型#

不同模型擅长不同的事,一个 Agent 里可以混着用:

subagents = [
{
"name": "contract-reviewer",
"description": "审阅法律文档与合同",
"system_prompt": "你是资深法律审阅者……",
"tools": [read_document, analyze_contract],
"model": "google_genai:gemini-3.6-flash", # 大上下文,吃长文档
},
{
"name": "financial-analyst",
"description": "分析财务数据与市场趋势",
"system_prompt": "你是资深财务分析师……",
"tools": [get_stock_price, analyze_fundamentals],
"model": "openai:gpt-5.5", # 数值分析更强
},
]

让子智能体只回精简结果#

明确要求”给摘要,不给原始数据”:

data_analyst = {
"system_prompt": """分析数据并返回:
1. 关键洞察(3-5 条)
2. 总体置信度
3. 建议的下一步
不要包含:原始数据、中间计算过程、详细的工具输出。
回答控制在 300 字以内。"""
}

常见坑#

主管不派活,什么都自己干。 三个解法依次试:① 把 description 写具体("用 web 搜索对特定话题做深度研究,需要多次搜索时使用" 好过 "helps with stuff");② 在主管的 system prompt 里明确下令——"重要:复杂任务请用 task() 工具委派给你的子智能体,这能保持你的上下文干净并提升效果";③ 确认子智能体确实注入了(见下一条)。

明明配了子智能体,task 工具却不存在。 检查是不是把 general_purpose_subagent 关掉了却没传任何同步子智能体——只有至少一个同步子智能体存在时 SubAgentMiddleware 才挂上。

以为 tools 是追加。 它是整体覆盖。写 "tools": [search] 之后文件工具全没了。同理 permissions 是整体替换父级规则。

上下文还是爆了。 子智能体回了一条又长又啰嗦的结论,等于没隔离。两个办法:① 硬性限制——"重要:只返回必要的摘要,不要包含原始数据、中间搜索结果或详细工具输出,控制在 500 字以内";② 让它把大数据落到文件系统——"数据量大时:先把原始数据存到 /data/raw_results.txt,再分析,最后只返回分析摘要"。

主管选错子智能体。 描述之间要有区分度,别造两个语义重叠的:

subagents = [
{
"name": "quick-researcher",
"description": "用于简单快速的研究问题,1-2 次搜索即可。需要基础事实或定义时使用。",
"system_prompt": "你是 quick-researcher 子智能体。",
},
{
"name": "deep-researcher",
"description": "用于复杂深度研究,需要多次搜索、综合与分析。写综合报告时使用。",
"system_prompt": "你是 deep-researcher 子智能体。",
},
]

fork 相关:skills 传了会被拒;system_prompt 传了会追加到父级 prompt 后面并破坏提示词缓存;fork 里不能用 task,它不能再往下派活。外加版本门槛 >=0.7.13。

结构化输出:父级 ToolMessage 里是 JSON 字符串,别直接当 dict 用;另外它顶掉默认的”取最后一条文本”逻辑。

小结#

  1. 一个工具、一张注册表:主管只通过 task(description, subagent_type) 派活,subagent_type 就是子智能体的 name。原理细节见 04 和 14。
  2. 必填的只有 name / description / system_prompt(最后一个仅 isolated 模式需要),其余字段都有明确默认值。
  3. 继承与覆盖是四组反直觉规则:tools 整体覆盖、interrupt_on 覆盖默认、skills 不继承、permissions 整体替换。这是全篇最值钱的一张表。
  4. general-purpose 是自动注入的,同名即替换,改装走 harness profile;关掉要 profile 与 subagents= 双管齐下,且不能用 excluded_middleware(会抛 ValueError)。
  5. fork 是”接着干”而不是”重新干”:丢掉触发派活的那条 AI 消息、换成 continuation 前言,换来父级完整历史和提示词缓存复用——但需要 >=0.7.13,本机 0.7.5 用不了。
  6. 每个子智能体的名字都会写进 lc_agent_name,这是流式分组、LangSmith 过滤、工具内按智能体分支行为的统一抓手。

一句话选型#

任务能拆、中间过程脏、结果要一句话——就用 isolated 子智能体;子智能体的活是”接着父级刚开的头往下写”——才值得上 fork;两者都够不上,别派活,自己干。

01 Subagents:Deep Agents 的子智能体(内置 task 工具 + 一整套配置面)
https://aicooleo.cn/blog/posts/deepagents-01-subagents/
作者
aicooleo
发布于
2026-08-31
许可协议
CC BY-NC-SA 4.0
多 Agent 精讲 · 第 18 / 22 篇 返回合集