TECH NOTE
6132 字 31 分钟 系列 18 / 2201 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 的全部字段如下。
| 字段 | 类型 | 必填性 | 默认行为 | 说人话 | 坑 |
|---|---|---|---|---|---|
name | str | 必填 | 无 | 子智能体的唯一 ID,主管调 task(subagent_type=...) 用的就是它 | 会写进 AIMessage 元数据和 LangSmith 的 lc_agent_name,改名等于改过滤条件;叫 general-purpose 时是”替换默认那个”而非新增 |
description | str | 必填 | 无 | 写给主管看的”什么时候该派我”说明书 | 主管只靠它做路由,写成 "helps with stuff" = 永远不被调用 |
system_prompt | str | mode="isolated" 时必填 | 不继承主 Agent | 写给子智能体自己看的”怎么干活、输出什么格式” | 不填就是空提示词,输出格式全靠运气;mode="fork" 时退化成可选追加段 |
mode | "isolated" | "fork" | 可选 | "isolated"(只看到派活描述) | 决定子智能体开局看到什么:一段任务描述,还是父级整段对话 |
tools | list | 可选 | 继承主 Agent 的全部工具 | 子智能体的工具白名单 | 一旦写了就整体覆盖而非追加;想保留文件工具得自己塞 FilesystemMiddleware |
model | str | BaseChatModel | 可选 | 继承主 Agent 的模型 | 给某个子智能体单独换便宜模型或长窗口模型 |
middleware | list | 可选 | 不继承,只叠加自己的 | 给子智能体加日志、限流、文件工具白名单 | 按 .name 合并进默认栈:同名替换默认项,异名插在核心中间件之后;名字写错会静默失效 |
interrupt_on | dict | 可选 | 继承主 Agent,子智能体值可覆盖 | 子智能体的工具级人工审批 | 没有 checkpointer 时形同虚设;值可以是 True / False / InterruptOnConfig |
skills | list[str] | 可选 | 不继承(唯一例外是 GP 子智能体) | 给这个子智能体单独挂技能目录 | 双向完全隔离:父看不到子的技能,子也看不到父的;只有传了才创建 SkillsMiddleware |
response_format | Pydantic / Strategy / dict | 可选 | 无(父级收到最后一条文本) | 让子智能体回结构化 JSON | 需 >=0.5.3;父级拿到的是字符串化 JSON,还得自己 json.loads |
permissions | list[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_agentfrom langchain.agents import create_agent
# ① 先用 create_agent 或 LangGraph 图 API 编译一个 agentcustom_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,实测SubAgentTypedDict 里根本没有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, Fieldfrom 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 这一行:
| Provider | model= 写法 |
|---|---|
google_genai:gemini-3.6-flash | |
| OpenAI | openai:gpt-5.5 |
| Anthropic | anthropic:claude-sonnet-5 |
| OpenRouter | openrouter:z-ai/glm-5.2 |
| Fireworks | fireworks:accounts/fireworks/models/glm-5p2 |
| Baseten | baseten:zai-org/GLM-5.2 |
| Ollama | ollama: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()
# 只捞某个子智能体的 runruns = client.list_runs( project_name="<your-project>", filter='has(metadata, \'{"lc_agent_name": "research-agent"}\')',)
# 捞"任意命名子智能体"的 run(只要带 lc_agent_name 键),以此排除主 Agentsubagent_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
@tooldef 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 用;另外它顶掉默认的”取最后一条文本”逻辑。
小结
- 一个工具、一张注册表:主管只通过
task(description, subagent_type)派活,subagent_type就是子智能体的name。原理细节见 04 和 14。 - 必填的只有
name/description/system_prompt(最后一个仅 isolated 模式需要),其余字段都有明确默认值。 - 继承与覆盖是四组反直觉规则:
tools整体覆盖、interrupt_on覆盖默认、skills不继承、permissions整体替换。这是全篇最值钱的一张表。 general-purpose是自动注入的,同名即替换,改装走 harness profile;关掉要 profile 与subagents=双管齐下,且不能用excluded_middleware(会抛ValueError)。- fork 是”接着干”而不是”重新干”:丢掉触发派活的那条 AI 消息、换成 continuation 前言,换来父级完整历史和提示词缓存复用——但需要
>=0.7.13,本机 0.7.5 用不了。 - 每个子智能体的名字都会写进
lc_agent_name,这是流式分组、LangSmith 过滤、工具内按智能体分支行为的统一抓手。
一句话选型
任务能拆、中间过程脏、结果要一句话——就用 isolated 子智能体;子智能体的活是”接着父级刚开的头往下写”——才值得上 fork;两者都够不上,别派活,自己干。