Agent 上下文越来越长?一个 task 工具的秘密

0 阅读8分钟

这是从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..."

看起来没问题?但问题是:

  1. 这些内容全堆在父上下文里
  2. 后续对话越来越慢
  3. 有用信息被噪声淹没
  4. 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.

关键点:

  1. 子 Agent 从空白上下文启动messages = [{ role: 'user', content: prompt }]
  2. 子 Agent 自己的工具循环:读文件、执行命令,都在自己上下文里
  3. 只返回摘要:中间过程丢弃,只带回最终文本

数据结构

// 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

组件s03s04
工具数量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 的核心,不是多一个模型,而是多一个干净上下文。 把局部任务的噪声隔离出去,主对话才能保持清晰。


下一步

想继续深入?

  1. 阅读源码src/planning/subagent.ts 只有 80 行
  2. 动手改造:尝试实现 fork 模式(继承父上下文)
  3. 阅读 s05:看看技能如何按需加载

项目地址:github.com/OPBR/build-… 感兴趣的话谢谢大家点个 star 😘😘😘

🚀 写在最后

本文是 《从 0 到 1 构建 Claude Code》 系列专栏的第四篇。我们将持续深度拆解 Agentic Programming 的核心机制。公众号合集

如果你对 LLM 原生开发、TypeScript 架构设计 感兴趣,欢迎关注我的公众号,我们一起在 AI 时代完成技术进化。公众号内第一时间更新内容

前端的AI野心公众号
长按二维码关注:前端的AI野心

相关阅读