这是从0到1实现 claude code 的第4篇
接着上一节Agent 为什么会"跑偏"?一个 todo 工具的神奇效果,本次是s04,下载代码后测试可执行
pnpm s04
代码链接在文章末尾
你有没有发现:和 AI Agent 对话久了,它就越来越"笨"。
明明之前问过的问题,它会重新问一遍。明明已经分析过的文件,它又读一遍。你问它"刚才做了什么",它支支吾吾说不清楚。
为什么?
答案在 messages 数组里。每轮对话都在累积,中间过程全部堆在里面,有用的信息被淹没。
今天,我用一个 task 工具教你解决这个问题。读完这篇,你会明白:子 Agent 的核心不是"多一个模型",而是"多一个干净上下文"。
一个真实的痛点
用户问:
s03 >> 分析 src/core 目录下的所有文件,告诉我项目结构
Agent 为了回答这个问题,可能:
- 读
agent-loop.ts(3000 字符) - 读
tools.ts(5000 字符) - 读
types.ts(2000 字符) - 执行几次 bash 查目录结构
最终答案可能只有一句话:
"项目用 TypeScript 实现,核心是 agent-loop 的循环模式。"
但整个对话里,messages 数组已经塞满了 10KB+ 的中间过程。
下一个问题:
s03 >> 你刚才读了哪些文件?
模型可能回答:
"我读了 agent-loop.ts、tools.ts、types.ts..."
看起来没问题?但问题是:
- 这些内容全堆在父上下文里
- 后续对话越来越慢
- 有用信息被噪声淹没
- Token 成本直线上升
根本原因:上下文污染
用一个类比来理解:
没有子 Agent:你在一个房间里开会。每个人发言、每个讨论都留在这个房间里。房间越来越挤,噪音越来越大。
有子 Agent:你派一个人去隔壁房间开会。他回来只告诉你结论。你的房间保持安静。
父 messages = [用户问题, 工具调用, 工具结果, 工具调用, 工具结果, ...]
↑
噪声累积,有用信息被淹没
父 messages = [用户问题, task调用, 子Agent摘要, 最终回答]
↑
干净,只有关键信息
子 Agent 的核心价值:不是"多一个模型实例",而是"多一个干净上下文"。
s04 的设计思路
核心概念:上下文隔离
Parent agent Subagent
+------------------+ +------------------+
| messages=[...] | | messages=[] | <-- 空白上下文
| | dispatch | |
| tool: task | ---------->| while tool_use: |
| prompt="..." | | call tools |
| | summary | append results |
| result = "..." | <--------- | return last text |
+------------------+ +------------------+
|
Parent context stays clean.
Subagent context is discarded.
关键点:
- 子 Agent 从空白上下文启动:
messages = [{ role: 'user', content: prompt }] - 子 Agent 自己的工具循环:读文件、执行命令,都在自己上下文里
- 只返回摘要:中间过程丢弃,只带回最终文本
数据结构
// src/core/types.ts
interface SubagentContext {
messages: Message[] // 子 Agent 自己的上下文(从空白开始)
tools: ToolDefinition[] // 子 Agent 可用的工具(过滤后的)
handlers: Record<string, ToolHandler> // 工具执行函数
maxTurns: number // 最大轮数,防止无限跑
systemPrompt: string // 子 Agent 的系统提示词
}
这个结构定义了子 Agent 的"隔离边界"。
工具过滤:防止无限递归
一个关键设计:子 Agent 不能有 task 工具。
为什么?想象一下:
父 Agent 调用 task → 子 Agent 调用 task → 孙 Agent 调用 task → ...
无限递归,系统崩溃。
// 父 Agent 工具:base + task
const PARENT_TOOLS = [...BASE_TOOLS, TASK_TOOL]
const PARENT_HANDLERS = { ...BASE_HANDLERS, task: createTaskHandler() }
// 子 Agent 工具:只有 base(不含 task)
const CHILD_TOOLS = BASE_TOOLS // bash, read_file, write_file, edit_file
const CHILD_HANDLERS = BASE_HANDLERS // 没有 task
工具过滤是隔离的第一道防线。
runSubagent 函数:核心实现
// src/planning/subagent.ts
export async function runSubagent(prompt: string): Promise<string> {
// 1. 创建空白上下文
const subMessages: Message[] = [{ role: 'user', content: prompt }]
// 2. 子 Agent 配置
const context: SubagentContext = {
messages: subMessages,
tools: CHILD_TOOLS, // 不含 task
handlers: CHILD_HANDLERS,
maxTurns: 30, // 防止无限跑
systemPrompt: SUBAGENT_SYSTEM,
}
// 3. 循环执行,最多 30 轮
for (let turn = 0; turn < context.maxTurns; turn++) {
const response = await client.messages.create({
model: MODEL,
system: context.systemPrompt,
messages: context.messages,
tools: context.tools,
})
context.messages.push({ role: 'assistant', content: response.content })
if (response.stop_reason !== 'tool_use') {
break // 子 Agent 做完了
}
// 执行工具调用...
const results = await executeTools(response.content, context.handlers)
context.messages.push({ role: 'user', content: results })
}
// 4. 只返回最终文本摘要(中间过程丢弃)
return extractTextReply(context.messages) || '(no summary)'
}
四个步骤:
| 步骤 | 做什么 |
|---|---|
| 1 | 空白 messages 启动 |
| 2 | 配置隔离的工具集 |
| 3 | 循环执行,最多 30 轮 |
| 4 | 只返回摘要,丢弃中间过程 |
智能判断:什么时候用 task?
一个实际问题:模型怎么知道该走 task 还是直接用工具?
答案在系统提示词里:
const S04_SYSTEM = `You are a coding agent at ${WORKDIR}.
<task_tool_guidance>
Use the task tool when the request involves:
- Analyzing, exploring, or searching multiple files/directories
- Finding patterns or gathering information across the codebase
- Tasks where intermediate steps are noise but final summary matters
- Requests starting with "analyze", "find", "search", "list", "explore"
Do NOT use task tool for:
- Single file operations (read/edit one file)
- Simple bash commands
- Tasks that need current conversation context
</task_tool_guidance>
The task tool spawns a subagent with fresh messages. This keeps the parent context clean.`
用 <task_tool_guidance> 标签明确告诉模型判断条件:
用 task:
- 分析/搜索多个文件
- 查找代码库中的模式
- 中间过程是噪声,只要结论
不用 task:
- 单文件操作
- 简单 bash 命令
- 需要当前对话上下文的任务
task 工具定义
export const TASK_TOOL_DEFINITION: ToolDefinition = {
name: 'task',
description:
'Launch a subagent with isolated context for exploration tasks. Use this when: (1) analyzing/searching multiple files, (2) gathering information across codebase, (3) only final summary matters. Returns only the summary, keeping parent context clean.',
input_schema: {
type: 'object',
properties: {
prompt: { type: 'string', description: 'The specific task for the subagent' },
description: { type: 'string', description: 'Short label (e.g., "analyze core")' },
},
required: ['prompt'],
},
}
关键是 description:明确列出适用场景,而不是抽象的"派生子任务"。
动手试试
运行 s04:
pnpm run s04
你会看到:
╔════════════════════════════════════╗
║ s04 - Subagent ║
║ "Fresh context, clean parent" ║
╚════════════════════════════════════╝
Tools: bash, read_file, write_file, edit_file, task
s04 >> 分析 src/core 目录下的所有文件,告诉我它们各自的作用
> task (analyze src/core): 分析 src/core 目录下的所有文件...
项目使用 TypeScript 实现,核心是 agent-loop 的循环模式...
s04 >> 读取 package.json 的内容
> read_file
...直接读取,不走 task...
对比:
| 任务类型 | 模型行为 |
|---|---|
| "分析 src/core 所有文件" | 调用 task(多文件探索) |
| "读取 package.json" | 直接 read_file(单文件操作) |
验证上下文隔离
最关键的测试:
s04 >> 分析 src/core 目录下的所有文件
(等待完成后)
s04 >> 你刚才读了哪些文件?
预期结果:
模型回答:"我派生了一个子任务去分析 src/core,子 Agent 返回的摘要说..."
而不是:"我读了 agent-loop.ts、tools.ts、types.ts..."
如果模型能说出具体文件名,说明子 Agent 的中间过程污染了父上下文。这是错误的。
对比 s03 和 s04
| 组件 | s03 | s04 |
|---|---|---|
| 工具数量 | 5(base + todo) | 5(base + task) |
| 核心机制 | 计划状态外显 | 上下文隔离 |
| 解决问题 | Agent 跑偏 | 上下文污染 |
| 返回方式 | 渲染计划文本 | 只返回摘要 |
s03 解决"忘记做什么",s04 解决"做过的事堆在上下文里"。
两者互补,不是替代。
FAQ
Q:子 Agent 和多 Agent 系统有什么区别?
A:子 Agent 是一次性的。任务完成,上下文丢弃。
多 Agent 系统(s09-s11)是长期协作的 teammate,有自己的角色、状态、通信通道。
混淆这两者会让初学者迷失方向。先做一次性子任务隔离,再做长期角色协作。
Q:为什么不把父上下文全部传给子 Agent?
A:这叫 fork 模式,但不是第一步。
空白上下文是最简单的设计。只有在子任务确实需要"继承父对话背景"时,才考虑 fork。
Q:maxTurns 设成 30 合理吗?
A:是教学值。真实系统会根据任务类型动态调整。
30 轮足够完成大部分探索任务,也足够防止无限循环。
Q:如果子 Agent 失败了怎么办?
A:返回 (no summary) 或错误信息。父 Agent 看到后可以决定重试或换个策略。
小结
今天我们实现了:
- ✅ 理解了上下文污染的根本原因
- ✅ 设计了 SubagentContext 的数据结构
- ✅ 实现了工具过滤(防止递归派生)
- ✅ 理解了智能判断何时用 task
- ✅ 跑起来了一个有上下文隔离能力的 Agent
关键洞察:
子 Agent 的核心,不是多一个模型,而是多一个干净上下文。 把局部任务的噪声隔离出去,主对话才能保持清晰。
下一步
想继续深入?
- 阅读源码:
src/planning/subagent.ts只有 80 行 - 动手改造:尝试实现 fork 模式(继承父上下文)
- 阅读 s05:看看技能如何按需加载
项目地址:github.com/OPBR/build-… 感兴趣的话谢谢大家点个 star 😘😘😘
🚀 写在最后
本文是 《从 0 到 1 构建 Claude Code》 系列专栏的第四篇。我们将持续深度拆解 Agentic Programming 的核心机制。公众号合集
如果你对 LLM 原生开发、TypeScript 架构设计 感兴趣,欢迎关注我的公众号,我们一起在 AI 时代完成技术进化。公众号内第一时间更新内容
![]()
长按二维码关注:前端的AI野心
相关阅读: