前三篇搓出了一个能执行命令、会列计划、还能按需加载知识的 agent。一个人干活,从头干到尾。
大部分时候这没问题。但有些场景会让单个 agent 很为难。
比如用户说"帮我用 Java 搭一个 Spring Boot 项目,然后写个 README"。这里面涉及两个差异很大的领域:Java 架构设计和文档写作。一个 agent 要在同一个上下文里同时扮演 Java 架构师和技术写手,效果往往不太好——上下文越长,模型的注意力越分散,每个领域都只能给出及格水平的回答。
Claude Code 的做法是派 sub-agent。主 agent 负责理解用户意图、拆解任务,然后把具体的子任务委派给专门的 sub-agent 去执行。每个 sub-agent 有自己独立的上下文和专业知识,干完活把结果交回来。
这篇来实现这个机制。
Skill 和 Sub-Agent 的区别
上一篇的 skill 是"给 agent 一本参考书"。agent 读完之后,还是自己动手干活。知识注入到了主 agent 的上下文里,占用的是主 agent 的 token 预算。
Sub-agent 不一样。它是一个独立的 agent 实例,有自己的 system prompt、自己的消息历史、自己的工具集。主 agent 只需要告诉它"做什么",然后等结果就行。
打个比方:skill 像是程序员翻文档,sub-agent 像是把活儿派给另一个程序员。
SubAgentLoader:和 SkillLoader 几乎一样
sub-agent 的定义方式和 skill 一样——一个 markdown 文件,YAML front matter 写元信息,正文写 system prompt:
---
name: java-architect
description: "When specified for use"
---
You are a senior Java architect with deep expertise in Java 17+ LTS
and the enterprise Java ecosystem...
SubAgentLoader 的代码和上一篇的 SkillLoader 结构几乎一模一样:启动时扫描 sub_agents/ 目录,解析所有 .md 文件,提供 get_descriptions() 和 get_body() 两个接口。
区别在于 get_body() 的返回值。skill 返回的是知识内容,会注入到主 agent 的上下文里。sub-agent 返回的是 system prompt,会用来初始化一个新的 agent 实例。
run_subagent_task:核心工具
这是整篇文章最关键的部分。run_subagent_task 这个工具做的事情是:接收 sub-agent 名字和任务描述,启动一个独立的 agent 循环,跑完后把结果返回。
MAX_SUBAGENT_ROUNDS = 20
@tool
def run_subagent_task(sub_agent_name: str, task: str) -> str:
"""Delegate a subtask to a specialized subagent and return its summary."""
sub_agent_system_prompt = sub_agent_loader.get_body(sub_agent_name)
sub_llm = ChatOpenAI(
model="deepseek-chat",
base_url="https://api.deepseek.com/v1",
api_key=os.getenv("DEEPSEEK_API_KEY"),
).bind_tools(SUBAGENT_TOOLS)
messages: list[BaseMessage] = [
SystemMessage(content=sub_agent_system_prompt),
HumanMessage(content=task),
]
for round_num in range(MAX_SUBAGENT_ROUNDS):
response = sub_llm.invoke(messages)
messages.append(response)
if not response.tool_calls:
return response.content or "[subagent finished without output]"
for tool_call in response.tool_calls:
result = subagent_tools_dict[tool_call["name"]].invoke(tool_call["args"])
messages.append(ToolMessage(
content=result,
tool_call_id=tool_call["id"],
))
return "Error: subagent reached max rounds limit (20)"
逐段看。
首先,根据 sub-agent 名字拿到对应的 system prompt,创建一个新的 LLM 实例。注意这里是新建的,不是复用主 agent 的 llm。每个 sub-agent 有自己独立的模型实例。
然后,构建初始消息列表:system prompt + 任务描述。这就是 sub-agent 的全部上下文,干净利落,不带主 agent 的历史包袱。
接下来是一个 for 循环,最多跑 20 轮。每轮的逻辑和主 agent 一样:调用模型,如果有 tool_calls 就执行,没有就返回结果。
MAX_SUBAGENT_ROUNDS = 20 是安全阀。万一 sub-agent 陷入死循环(比如反复执行同一个命令),20 轮后强制终止。
防止无限递归
这里有一个容易忽略的细节。看工具列表的定义:
SUBAGENT_TOOLS = [run_bash, run_read_file, run_write_file, run_edit_file]
subagent_tools_dict = {t.name: t for t in SUBAGENT_TOOLS}
ALL_TOOLS = SUBAGENT_TOOLS + [run_subagent_task]
all_tools_dict = {t.name: t for t in ALL_TOOLS}
主 agent 的工具列表是 ALL_TOOLS,包含 run_subagent_task。sub-agent 的工具列表是 SUBAGENT_TOOLS,不包含 run_subagent_task。
为什么?如果 sub-agent 也能调用 run_subagent_task,它就可以再派一个 sub-agent,那个 sub-agent 又可以再派……无限递归,直到内存爆掉或者 API 费用爆掉。
所以 sub-agent 只有基础工具:跑命令、读文件、写文件、编辑文件。它能干活,但不能再分活儿。
独立上下文的好处
sub-agent 最大的价值不是"多了一个帮手",而是上下文隔离。
主 agent 的消息历史可能已经很长了——之前的对话、之前的工具调用结果、各种中间状态。在这么长的上下文里让模型去做一个专业任务,效果会打折扣。
sub-agent 的上下文是全新的。只有一个专业的 system prompt 和一个明确的任务描述。模型的全部注意力都集中在这个任务上。
而且 sub-agent 执行过程中产生的中间消息(工具调用、工具结果)不会污染主 agent 的上下文。主 agent 只拿到最终的总结结果,一段干净的文本。
实际运行的样子
假设 sub_agents/ 目录下有一个 java-architect.md,用户输入"帮我设计一个 Spring Boot 项目的目录结构":
[用户输入] 帮我设计一个 Spring Boot 项目的目录结构
[工具调用 - 第 1 轮]
- 调用工具: run_subagent_task
参数: {'sub_agent_name': 'java-architect', 'task': '设计一个 Spring Boot 项目的标准目录结构'}
[subagent 第 1 轮]
- 调用工具: run_bash({'command': 'mkdir -p src/main/java/com/example/{config,controller,service,repository,model}'})
结果: (no output)
[subagent 第 2 轮]
- 调用工具: run_write_file({'path': 'src/main/resources/application.yml', ...})
结果: Wrote 245 bytes to src/main/resources/application.yml
结果: 已按照 Spring Boot 最佳实践创建了标准目录结构...
[最终回复] Java 架构师已经帮你创建好了项目结构...
主 agent 决定把任务委派给 java-architect,sub-agent 独立执行了两轮工具调用,最后把总结返回给主 agent。主 agent 再把结果转述给用户。
和前三篇的对比
第一篇给了 agent 一双手。第二篇给了它一个计划本。第三篇给了它一个书架。这一篇给了它一个团队。
到这里,我们的 agent 已经具备了 Claude Code 的几个核心能力:工具调用、任务规划、知识加载、子任务委派。当然,真正的 Claude Code 在每个环节上都做了大量的工程优化——流式输出、并发控制、错误恢复、权限管理。但骨架是一样的。