0. 为什么要从“删功能”开始

第一次认真研究 Claude Code,很容易掉进功能列表里。

它会读代码、改文件、跑测试、搜索网页,也能加载 Skills、连接 MCP、创建子 Agent、管理任务和进入 Plan Mode。把这些名词全列出来并不难,难的是回答另一个问题:这些东西为什么会同时出现在一个 Code Agent 里?

如果直接看最终形态,几十种工具和大量配置搅在一起,很难分清哪些是骨架,哪些是后来为了解决工程问题加上的零件。

所以本文换一种办法。我们先想象一个几乎什么都不会的 Agent,然后不断给它安排更难的任务。每次它遇到一个绕不过去的问题,我们再加一层能力:

只有模型
  ↓ 不能接触真实环境
加入 ReAct 循环和工具
  ↓ 工具越来越多,上下文放不下
加入渐进式披露
  ↓ 不知道项目规则,也缺少权限边界
加入文件系统和权限系统
  ↓ 通用 Agent 不懂专业流程
加入 Skills
  ↓ 内建能力连接不到公司系统
加入 MCP
  ↓ 有些动作不能靠模型自觉执行
加入 Hooks
  ↓ 单个上下文承担不了大型任务
加入子 Agent
  ↓ 复杂修改需要先审方案再动手
加入 Plan Mode
  ↓ 长对话装不下,跨会话又记不住
加入上下文压缩和记忆

沿着这条路线往下走,我们会逐渐拼出 Claude Code 的工作原理图,而不只是罗列一份功能说明书。

先说明边界:Claude Code 的版本变化很快,工具数量、事件名称和内部阈值都可能调整。本文讨论的是公开文档、公开代码分析和实际使用中能够互相印证的机制。涉及具体数字时,应把它理解为特定版本的切面,而不是长期不变的协议。

1. 从 ReAct 出发:模型怎样开始“做事”

先把 Claude Code 的文件工具、终端和 Git 能力全部拿掉,只留下一个大模型。

此时用户问:

2026 年 8 月 5 日,深圳和广州的气温相差多少?

模型可以写出一段很像答案的文字,却没有可靠办法知道当天的气温。原因很简单:大模型本质上接收 token,再预测后续 token。它能分析输入,却不会自己打开天气网站,也不会凭空执行一个接口。

要让模型从“会说”变成“会做”,外面必须有一段程序替它接触环境。

Agent 其实是两个东西

为了不把概念说玄,可以先把 Agent 拆成两部分:

LLM:根据当前上下文决定下一步
程序:准备上下文、执行动作、保存结果、控制循环

后面这段程序通常被称为 Harness。模型并不直接操作电脑。它只是在某一轮响应里表达:“我要调用这个工具,参数是这些。”Harness 识别出工具调用,真正执行,然后把结果作为一条新消息送回模型。

于是,一个最小循环出现了:

用户问题
模型判断下一步
调用工具 ──→ 程序执行 ──→ 返回结果
   ↑                         │
   └──────── 再次判断 ───────┘

这就是 ReAct 的基本精神。

这里的 ReAct 不是前端框架 React。它来自 Reason 与 Act,也就是推理和行动。论文 ReAct: Synergizing Reasoning and Acting in Language Models 提出的关键做法,是让推理轨迹与环境行动交替出现。推理负责维护计划,行动负责取得模型原本不知道的信息。

Thought、Action、Observation 到底是什么

教科书常把 ReAct 写成三段:

Thought → Action → Observation → Thought → ... → Final

以天气问题为例,可以把过程简化为:

Thought: 先查深圳当天气温。
Action: query_weather(city="深圳", date="2026-08-05")
Observation: 深圳 26~31℃,多云。

Thought: 还缺广州数据。
Action: query_weather(city="广州", date="2026-08-05")
Observation: 广州 25~30℃,多云。

Thought: 最低温相差 1℃,最高温也相差 1℃。
Final: 在这个示例中,深圳的最低温和最高温都比广州高 1℃。

这里的气温只用于演示流程。真正重要的是:第二次行动不是预先写死的,而是模型看到第一次 Observation 后做出的决定。如果第一次接口返回“城市不存在”,下一步就可能变成修正参数;如果已经拿到两地数据,下一步则不该继续查询。

模型怎么告诉程序“我要用工具”

在支持工具调用的模型 API 中,工具一般通过结构化定义提供。一个简化的天气工具可能长这样:

{
  "name": "query_weather",
  "description": "查询指定城市和日期的天气",
  "input_schema": {
    "type": "object",
    "properties": {
      "city": { "type": "string" },
      "date": { "type": "string" }
    },
    "required": ["city", "date"]
  }
}

模型读取工具说明后,可以返回普通文本,也可以返回一个结构化的 tool_use 块。Harness 看到 tool_use,就进入执行分支;没有工具调用时,则把模型文本视为阶段性说明或最终回答。

因此,程序每轮实际处理的是两种结果:

模型输出工具调用 → 执行工具 → 将 tool_result 放回消息历史
模型输出最终文本 → 结束这一轮任务

工具可以一次都不用,也可能连续使用很多次。循环本身不会自动保证结束,Harness 通常还要设置最大轮数、超时、费用或用户中断等停止条件。

ReAct 和 Workflow 不是非此即彼

Workflow 的路径在任务开始前已经比较明确。例如:先读取 CSV,再清洗数据,然后生成报告,最后发送邮件。模型可以完成其中某一步,但没有权力随意改变流程。

ReAct 则把更多决策留在运行时。搜索代码时到底读哪些文件、测试失败后回到哪里,这些通常无法提前画成完整流程图。

Coding Agent 更偏向 ReAct,是因为代码任务的路径高度依赖中间结果。不过它并没有抛弃 Workflow。后面会看到,Hooks、Skills、Plan Mode 和权限规则都在给自由循环插入确定性结构。

现在我们有了一个能调用天气接口的 Agent。但 Claude Code 要做的是编码,ReAct 方框里的 Tool 到底应该放什么?

2. 基础工具箱:让模型能够完成一条编码链路

给 Agent 一个任务:

找出登录接口偶发 500 的原因,修复并验证。

这句话看起来只有一行,实际至少包含五类工作:定位文件、理解逻辑、修改代码、运行验证、检查变更。缺任何一类,Agent 都只能做到一半。

先看一条可能的执行轨迹

Glob:寻找 auth、login 相关文件
Grep:搜索错误信息或接口路径
Read:阅读处理函数、类型定义和测试
Edit:修改异常分支
Bash:运行目标测试
Read:根据失败栈继续定位
Edit:补充边界处理
Bash:再次测试并查看 git diff

这条轨迹已经覆盖日常开发的大部分动作。不同版本的 Claude Code 会增减工具,但核心分工相当稳定。

阅读和理解

  • Read 读取文件或指定区间;
  • Glob 按路径模式寻找文件;
  • Grep 按文本或正则定位实现;
  • Bash 可以查看 Git 历史、目录结构和命令输出。

搜索工具的价值不只是速度。它们还能控制进入上下文的信息量。面对几千个文件,正确做法通常是先用 Glob 缩小目录,再用 Grep 找到行号,最后 Read 相关片段,而不是把整个仓库喂给模型。

修改

  • Edit 对已经读过的内容做精确替换;
  • Write 创建新文件或重写完整内容;
  • Bash 在确有需要时运行代码生成器、格式化器或 Git 命令。

这里有一个不太显眼但非常重要的差别:专用编辑工具知道“哪个文件从什么内容变成什么内容”,而一段任意 Shell 命令对 Harness 来说更像黑盒。前者更容易校验权限、展示 diff 和追踪影响范围。

验证

Agent 修改完文件,不代表任务完成。它还需要通过环境拿到反证机会:

  • 单元测试;
  • 构建;
  • 类型检查;
  • Lint;
  • 页面预览;
  • Git diff 和状态检查。

大模型很擅长写出“问题已经修复”的句子,但这句话没有验证价值。测试输出才是 Observation。

Bash 为什么万能,却不适合包办一切

理论上,通过 Bash 可以 cat 文件、sed 修改、find 搜索、curl 请求接口,几乎所有事情都能完成。既然这样,为什么还要设计 Read、Edit、Grep 等专用工具?

因为程序需要理解一次行动会产生什么后果。

Read(file_path="src/auth.ts")

它的意图很明确:只读一个文件。权限系统可以直接判定风险,结果也可以按文件内容缓存。

而下面这条命令的影响要复杂得多:

bash -lc "some-script.sh"

脚本内部可能读文件、写文件、访问网络,甚至继续启动别的进程。越通用的工具,能力越强,程序越难精确判断。

所以更合理的原则是:有结构化专用工具时优先使用;专用工具覆盖不了时,再让 Bash 兜底。

工具也可以不是“操作电脑”

Claude Code 中,很多系统能力也通过工具形态交给模型:

  • 主动向用户澄清问题;
  • 创建任务或更新任务状态;
  • 启动子 Agent;
  • 加载 Skill;
  • 进入或退出 Plan Mode;
  • 创建隔离 worktree。

这样做的好处是,主循环不用为每种能力写一套完全不同的调度逻辑。对于模型来说,它们都是“在当前状态下可以选择的下一步行动”。

工具箱到这里已经相当完整。但新的麻烦来了:如果每个工具都带一份几百上千字符的 JSON Schema,几十个工具一起放进请求,模型每一轮都要重新面对一大摞说明书。

3. 工具的渐进式披露:不是工具越多越聪明

我们很容易产生一个直觉:给 Agent 接的工具越多,它能做的事情越多,因此能力越强。

前半句没错,后半句不一定。

工具定义本身会进入模型上下文。假设 50 个工具平均各占 500 token,仅工具说明就要消耗约 25,000 token;更麻烦的是,模型每次选工具时都要在大量相近名称之间判断。

一个只想搜索 TypeScript 文件的任务,没有必要同时理解如何操作 TAPD、如何读取 Figma、怎样查询数据库和怎样编辑 Notebook。

把工具分成“桌面上”和“仓库里”

可以把工具池想成一间工作室。

Read、Edit、Grep 这些每天都用的工具放在桌面上,伸手就能拿到。很少使用的工具放在仓库里,模型先知道仓库里大概有什么,真正需要时再去取。

程序层面可以把它们分成两类:

Always loaded:完整 schema 一开始就提供
Deferred:开始时只有名称或简短索引,发现后再加载完整定义

延迟工具没有完整参数结构时,模型不能直接可靠调用。它需要先通过 Tool Search 或相应的发现机制找到工具,获得引用或完整说明,下一轮才具备调用条件。

从一次 API 请求看发生了什么

假设用户说:“读取这个 Notebook 的第三个单元格并修改代码。”

第一轮请求里,模型看到了核心工具的完整 schema,同时只知道存在 Notebook 相关的延迟能力。

模型先发起工具搜索:

ToolSearch("edit notebook cell")

搜索结果返回与 Notebook 编辑工具关联的信息。这个“发现结果”进入对话历史后,后续请求才会展开该工具的可调用定义。模型随后生成真正的 Notebook 编辑调用。

所以路径是:

知道名字 → 搜索发现 → 加载定义 → 调用工具

这多了一次循环,却换来了更小、更干净的常驻上下文。

为什么不在发现后改写系统提示词

这里还牵涉 Prompt Cache。

模型服务会缓存长请求中稳定的前缀。系统提示词和常驻工具如果一直不变,后续轮次更容易复用缓存。如果每发现一个工具,就把系统提示词前部重新拼接一遍,前缀发生变化,后面的缓存也可能失效。

更好的办法是维持前缀稳定,把新发现的工具信息放进不断增长的对话正文。这样既能让模型看到它,又不会频繁改动最昂贵的固定部分。

这也是为什么 tool_reference 和“完整工具 schema”不是一回事。前者更像对某项能力的索引,后者才是实际调用所需的参数说明。把引用留在对话历史里,程序和模型服务可以在正确位置展开工具,而不是反复改写整个前缀。

渐进式披露不只属于工具

后面会反复看到同一种设计:

  • 文件太多:先 Glob/Grep,再 Read;
  • Skills 太多:先看名称和描述,再读 SKILL.md
  • MCP 能力太多:先发现 Server 和工具,再加载 schema;
  • 记忆太多:先加载索引,再按主题读取正文。

“需要时才展开”是本文真正的主线。它既节省 token,也减少模型一次要做的选择。

工具问题暂时解决了。接下来轮到环境问题:同一个 Agent 进入两个项目,为什么能采用不同的命令、规则和权限?

4. 文件系统:代码仓库,也是 Agent 的外部上下文

假设 Claude Code 打开两个项目。

项目 A 使用 pnpm,要求提交前运行 pnpm test;项目 B 使用 Go,禁止直接编辑生成文件。用户不可能每次启动都重新讲一遍这些规则,Claude Code 也不能把所有项目的习惯永久塞进同一个系统提示词。

文件系统恰好提供了一个简单的答案:把与项目有关的信息放在项目附近,需要时读取。

第一类信息:我现在在哪里

Claude Code 启动时会感知一组基础环境:

  • 当前工作目录(CWD);
  • 被允许访问的额外目录;
  • 当前操作系统和 Shell;
  • 是否处于 Git 仓库;
  • 当前分支、远程仓库和工作区状态。

CWD 不只是命令行里的一段路径,它定义了 Agent 的默认工作边界。用户在仓库根目录启动 Claude Code,意味着绝大多数搜索和修改都应该聚焦这里,而不是从整个磁盘开始扫描。

Git 状态同样重要。如果工作区已经有未提交修改,Agent 必须先区分哪些是用户原有变更,哪些是自己刚产生的结果。否则一次看似普通的重构,就可能覆盖用户正在进行的工作。

第二类信息:在这里应该怎样工作

项目可以用 CLAUDE.md.claude/rules/ 和设置文件保存规则。例如:

# 项目约定

- 使用 pnpm,不要生成 package-lock.json
- 修改 API 必须更新契约测试
- generated/ 下的文件由脚本产生,禁止手动编辑
- 提交前运行 pnpm lint && pnpm test

这些内容最终仍会进入模型上下文,但区别在于:它们不必硬编码在 Claude Code 的全局 Prompt 中,而是根据当前目录动态加载。

规则还需要作用域。一个常见分层是:

Managed:组织管理员统一下发
User:当前用户在所有项目中的偏好
Project:随仓库共享的团队规则
Local:只在本机生效、不提交的项目设置

分层的价值不只是复用。组织级安全规则不能被项目随意覆盖,本地密钥和机器路径又不应该进入 Git。不同层级解决的是不同治理问题。

第三类信息:对话和中间状态

长任务会积累大量状态:已经读过哪些文件、哪些方案被否决、测试失败过几次、下一步还剩什么。

这些信息只放在模型脑子里并不稳妥。对话需要落盘,计划和调查结果也可以写成临时文件。这样做有几个实际好处:

  • 会话退出后可以恢复;
  • 上下文压缩后仍能重新读取关键资料;
  • 后台子 Agent 可以把结果写给主 Agent;
  • 用户能够直接检查 Agent 的计划和笔记。

“文件系统即上下文”并不意味着模型自动知道磁盘上的一切。文件只有在被程序注入或被工具读出后,才真正进入模型可见的消息历史。磁盘负责长期保存,工具负责把需要的部分搬进上下文。

Read before Edit:为什么编辑前通常要先读

专用编辑工具往往要求模型先读取文件。这不是形式主义。

想象一下:模型十分钟前读过 auth.ts,用户随后在编辑器里改了同一文件。如果 Agent 仍按旧内容做字符串替换,就可能覆盖用户的新修改。

因此,Harness 可以维护文件状态缓存,记录已读内容和修改时间。执行 Edit 时检查:

这个文件是否读过?
读取之后文件有没有变化?
准备替换的旧文本是否仍然存在?

任何一项不满足,都应要求重新读取,而不是凭旧上下文直接写入。缓存不仅为了少读一次文件,也是并发编辑时的安全校验。

权限边界为什么必须结构化

现在 Agent 已经能运行 Bash。理论上,一次命令就可以删除大量文件或把敏感内容发送到网络。

仅在 Prompt 里写“不要执行危险命令”不够。自然语言规则最终仍由概率模型解释;真正的边界必须由确定性程序执行。

一次工具调用在落地前,可以分成四层检查:

  1. Schema 校验:字段类型、必填参数是否正确;
  2. 工具业务校验:路径是否存在、替换文本是否唯一等;
  3. 工具特定检查:Shell 命令、目标目录或网络操作有什么风险;
  4. 通用权限决策:匹配 allow、ask、deny 规则。

最终结果可能是直接允许、请求用户批准,或明确拒绝。权限规则写在 JSON 配置中,程序可以无歧义地匹配;Prompt 中的安全说明则用来帮助模型提前选择更合理的动作。两者作用不同,不能互相替代。

文件系统解决了环境、规则、状态和权限配置。可是一个新的矛盾出现了:如果我们希望 Claude Code 同时精通代码审查、PDF、数据分析和发布流程,难道要把所有专业方法都写进 CLAUDE.md 吗?

5. Skills:不要让通用 Agent 背下所有说明书

模型支持很长的上下文,不代表它能同等精确地遵守里面每一条规则。

一份代码审查规范可能几千字,一份安全审计流程又是几千字,再加上文档排版、部署、数据分析等说明,全部放进系统 Prompt,常见结果是每一项都执行得不够稳定。

Skills 解决的是通用性和专业性之间的矛盾。

Skill 更像一本操作手册

一个典型 Skill 可能是:

pdf/
├── SKILL.md
├── references/
│   └── forms.md
├── scripts/
│   └── render_pdf.py
└── assets/
    └── cover-template.pdf

SKILL.md 说明何时使用、必须遵守什么流程、怎样验证结果。references 放深入资料,scripts 保存可以重复运行的代码,assets 保存模板。

这已经超出普通 Prompt 模板。它把“解决一类问题所需的文字、资料和程序”收在一起。

模型最开始看见什么

如果安装了几十个 Skills,Claude Code 不会一开始就把所有 SKILL.md 全文注入上下文。更合理的做法是先提供元数据:名称、简短描述、适用场景,以及可能的工具或运行模式要求。

这些元数据就是书脊。模型看到任务与某个 Skill 匹配后,才打开正文。

一个简化的 frontmatter 可能表达:

---
description: 按团队规范执行代码审查
allowed-tools: Read, Grep, Glob, Bash
context: fork
---

描述帮助模型决定“要不要用”,工具范围和上下文模式则帮助 Harness 决定“怎样运行”。

Inline 和 Fork 的差别

有些 Skill 适合直接把说明加入当前对话。比如一份短小的提交信息规范,主 Agent 读完后继续工作即可。

另一些 Skill 本身很长,执行过程中还会产生大量中间信息。把它放在隔离子 Agent 中更合适:子 Agent 读取完整说明、完成任务,只把精简结果返回主线。

Inline:方法进入当前上下文,用户可以继续干预
Fork:方法在独立上下文中执行,主线只接收结果

这两种模式分别优化交互连续性和上下文隔离。

Skill 真正复用的是什么

脚本复用计算过程,Skill 复用判断顺序。

例如,一份好的代码审查 Skill 不会只写“认真检查代码”,而会告诉 Agent:先读取项目规则,再看 diff;优先找正确性和安全问题;每条问题必须给出文件位置和触发条件;最后再检查测试覆盖。

团队过去踩过的坑,可以变成下一次任务默认执行的步骤。这就是 Skills 流行的根本原因:它让经验从聊天记录里脱离出来,变成可维护的文件。

但 Skills 只能教 Agent 怎样使用已有能力。如果我们需要访问 TAPD、Figma 或公司内部数据库,Claude Code 仍缺少真正的连接。

6. MCP:外部工具怎样进入同一个 Agent 循环

内置工具再多,也不可能知道每家公司的系统。

传统做法是为每个产品单独写插件。问题在于,不同 Agent、不同客户端要重复集成同一个服务。MCP(Model Context Protocol)试图统一这层接口:服务方按照协议暴露能力,Agent 客户端按照协议发现和调用。

一个 MCP Server 是一个工具箱

MCP Server 可以运行在本地进程,也可以是远程 HTTP 服务。它通常提供:

  • Tools:执行查询或动作;
  • Resources:读取文档、数据或其他内容;
  • Prompts:暴露可复用的任务模板。

例如,一个 GitHub MCP Server 可能同时提供搜索代码、读取 Issue、创建评论和查看 PR 等工具,而不是只有一个“GitHub”按钮。

Claude Code 连接 Server 后,先发现它提供的清单,再把外部工具转换成内部统一的工具表示。模型层面看到的仍然是名称、描述和 JSON Schema:

mcp__github__search_code
mcp__github__get_pull_request
mcp__tapd__search_workitems

这里的 mcp__tapd__search_workitems 是为了说明命名方式而写的示意名称。实际接入时,工具名取决于所使用的 TAPD MCP Server;如果没有现成 Server,也可以基于 TAPD API 封装对应工具。

对于 ReAct 循环来说,内置 Read 和远程 TAPD 查询没有本质差异:模型选择工具,Harness 执行,结果作为 Observation 返回。

stdio 与 HTTP 解决不同场景

本地工具常通过 stdio 通信。Claude Code 启动一个子进程,用标准输入输出交换 MCP 消息,适合本机脚本和开发工具。

远程服务更适合 HTTP,需要额外处理网络、OAuth、凭据刷新和服务可用性。协议统一了消息格式,却不会替使用者消除认证与安全问题。

MCP 工具也要遵守权限和渐进加载

外部工具通常比本地读取更敏感。查询生产数据库和创建 TAPD 需求或缺陷,不能默认拥有同样权限。

因此,MCP 工具仍要进入 Claude Code 的权限决策:哪些可以自动执行,哪些每次询问,哪些永远拒绝。

工具数量方面同样如此。一个 Server 可能提供几十项能力,如果完整 schema 全部常驻,刚解决的上下文问题又会回来。延迟加载和 Tool Search 对 MCP 尤其重要。

工具顺序为什么也会影响成本

在模型请求中,内建工具通常比动态变化的外部工具稳定。把稳定内容放在前缀,把容易增删的 MCP 定义放在后部,有利于维护 Prompt Cache。

这是一个很工程化的细节:两次请求即使 token 总数相同,只要前缀是否稳定不同,推理成本和延迟也可能不同。Claude Code 的上下文设计不只考虑“放多少”,还要考虑“哪些部分经常变化”。

现在 Agent 已经能连接几乎任意外部系统。不过,模型仍然掌握着下一步的选择权。有些事情我们不希望它选择,而希望它必然发生。

7. Hooks:把确定性要求从 Prompt 里拿出来

假设团队规定:每次 Agent 编辑 .go 文件后都要执行 gofmt

一种做法是在 CLAUDE.md 中写:“修改 Go 文件后记得格式化。”大多数时候模型会遵守,但“多数时候”不适合作为工程保证。上下文很长、任务中途失败或模型提前停止时,这一步仍可能遗漏。

Hook 的思路是:既然某件事必须在固定时间发生,就不要让模型负责记忆,把它挂到程序生命周期上。

Hook 插在哪里

Claude Code 的生命周期可以粗略画成:

SessionStart
UserPromptSubmit
┌──────── Agentic Loop ────────┐
│ 模型决定调用工具             │
│   ↓                          │
│ PreToolUse                   │
│   ↓                          │
│ PermissionRequest            │
│   ↓                          │
│ Tool execution               │
│   ↓                          │
│ PostToolUse / Failure        │
│   └──── 结果回到模型 ────────┘
└──────────────────────────────┘
Stop
SessionEnd

除此之外,还有子 Agent 启停、任务状态变化、文件变化、上下文压缩、配置更新等事件。具体事件会随版本扩展,官方 Hooks reference 是更可靠的实时清单。

Hook 收到什么,能够做什么

事件触发时,Claude Code 会把结构化 JSON 交给 Hook。以 PreToolUse 为例,输入会包含工具名和工具参数。

Hook 可以:

  • 记录审计日志;
  • 检查命令或文件路径;
  • 修改或补充上下文;
  • 阻止这次工具调用;
  • 在工具完成后运行测试;
  • 通过 HTTP 通知外部系统。

例如,阻止破坏性命令的逻辑不需要“说服”模型:

PreToolUse 收到 Bash 调用
脚本检查 command 字段
命中危险模式 → 返回 deny 和原因
工具不执行,原因作为反馈交给模型

模型可以根据失败反馈换一个安全方案,但不能绕过程序做出的拒绝。

Hooks 是 Workflow 留在 ReAct 里的骨架

ReAct 适合决定“下一步做什么”,Hook 适合保证“到了这个节点必须做什么”。

两者结合后,Agent 既能根据测试结果灵活调整,又能确保审计、格式化、权限检查等关键步骤不被遗忘。

这也是把逻辑写进 Prompt 和写进 Hook 的分界:

需要语义判断、允许灵活变化 → 交给模型
必须稳定触发、结果需要可预测 → 交给 Hook

8. 子 Agent:主对话为什么需要“分包”

单个 Agent 理论上可以连续工作很久,但上下文会逐渐混入大量局部信息。

例如调查一次性能问题,需要同时看数据库查询、前端请求、服务日志和基础设施指标。如果全部塞进主对话,主 Agent 最后可能拿着几万行日志,却忘了用户最初只关心某个接口为什么变慢。

子 Agent 的价值首先是隔离,其次才是并行。

一个子 Agent 也是完整循环

主 Agent 调用 Agent 工具后,创建的不是一次普通函数执行,而是另一个带有模型、工具和消息历史的 Agentic Loop。

主 Agent
  ├── 子 Agent A:只调查数据库
  ├── 子 Agent B:只分析前端请求
  └── 子 Agent C:只检查发布变更

每个子 Agent 可以拥有独立的系统说明、工具范围、模型和权限。它完成任务后,只把结论、证据和关键文件位置返回主 Agent,不需要把所有搜索轨迹倒回主对话。

空白、Fork 与 Resume

子 Agent 从哪里获得上下文,会影响成本和效果。

  • 空白上下文:只拿到主 Agent 明确交付的任务,干净但需要重新探索;
  • Fork:继承当前对话背景,启动快,但会复制不少无关历史;
  • Resume:从以前保存的子任务会话继续,适合中断后恢复。

选择时真正要问的是:新任务需要多少主线背景?上下文只要足够完成子任务即可,多出来的部分常常只是噪音。

同步、异步与文件系统信箱

有依赖的子任务必须同步。例如先让子 Agent 找出数据库表,再由主 Agent 根据结果设计迁移。

互不依赖的调查则可以异步并行。主 Agent 在等待日志分析时,仍能继续阅读代码。

后台 Agent 完成后,需要一种方式把结果送回来。对本地 Coding Agent 来说,文件系统就是很自然的信箱:结果写入任务文件或消息目录,主 Agent 收到完成通知后再读取。

这省去了额外消息队列,也延续了前面的原则:状态落盘,需要时再放回上下文。

Worktree 解决的是写入冲突

并行读取通常安全,并行编辑则不同。两个 Agent 同时修改同一工作区,可能互相覆盖或让测试结果失去归属。

Git worktree 可以为每个任务创建独立目录和分支:

main workspace
├── worktree-agent-a
└── worktree-agent-b

各自修改、测试,最后再由主线选择合并哪个方案。隔离环境比提醒两个 Agent“不要互相影响”更可靠。

Skill 和子 Agent 的关系

Skill 管理的是专业方法,子 Agent 管理的是独立执行上下文。二者经常组合:

  • 主 Agent 加载一份编排 Skill,再启动多个调查 Agent;
  • 一个大型 Skill 以 fork 模式交给子 Agent,避免主对话被长说明和中间结果占满。

这两种能力看起来不同,底层都在做同一件事:把复杂问题切成可管理的上下文块。

9. Plan Mode:重点不是“先想”,而是“先别动”

很多模型本来就会在改代码前列计划。那么 Plan Mode 为什么还要单独存在?

因为“模型写了一份计划”和“系统处于不可修改状态”是两回事。

用户说“先看看怎么改”,模型可能在探索过程中顺手修一个小问题;对于大型迁移,这种积极并不一定是好事。用户真正需要的是一条明确边界:方案确认前,只能调查,不能产生实质修改。

Plan Mode 改变的是工具权限

进入规划模式后,读取、搜索和分析能力仍然可用,编辑文件和有副作用的命令则受到限制。Agent 可以:

  1. 浏览代码结构;
  2. 查找现有模式和可复用实现;
  3. 让子 Agent 分别调查不同方案;
  4. 向用户询问关键选择;
  5. 写出实施步骤和验证计划。

它不能因为“已经想明白了”就自行开始改代码。离开 Plan Mode 需要一个明确的批准过程。

批准为什么比计划本身重要

计划的用途,是把高成本决策提前暴露给用户:

  • 会改哪些模块?
  • 是否增加数据库迁移?
  • 是否破坏旧接口?
  • 有没有更小的实现方案?
  • 哪一步会影响外部环境?

用户可以在这些问题还只是文字时纠正方向,而不是等几十个文件改完再返工。

所以 Plan Mode 的本质更接近事务的 prepare 阶段:先收集信息和形成方案,拿到确认后才 commit 到真实工作区。

10. 记忆的取舍:既怕装不下,也怕忘得太干净

到这里,一个复杂任务可能已经产生几十次工具调用。每次 Read、Grep、Bash 和 Web 查询都会向消息历史增加内容。

模型下一轮能看到什么,最终仍取决于这份历史。文件保存在磁盘上并不等于模型已经知道;只有文件内容被 Read 并作为工具结果加入上下文,模型才真正“看见”它。

这带来两个不同的问题:

当前对话太长:装不下
下次会话重新开始:记不住

Claude Code 分别用上下文管理和项目记忆处理它们。

先区分两类上下文

工具 schema、系统规则和 Skill 指令属于“昂贵上下文”。模型不仅要读到,还要精确遵守,位置和稳定性都很重要。

历史搜索结果和旧日志更像“可回忆上下文”。大多数时候只需保留结论或路径,需要细节时可以重新读取。

这一区分解释了为什么 Claude Code 不会对所有内容采用同样的压缩策略。

第一层:限制单轮工具结果

一次命令可能输出几十万字符。把完整日志直接塞进模型,既昂贵,也会淹没真正的错误行。

低成本做法是把超大结果保存到磁盘,在对话里留下路径、截断内容和基本说明。模型需要时可以 Grep 目标关键词,或分段读取,而不是一次吞下全部日志。

这一步没有摘要损失,只是把数据从“立即可见”改成“按需读取”。

第二层:清理陈旧工具输出

随着轮次增加,早期 Read 和 Bash 结果可能已经过时。文件后来被修改,旧内容继续留在上下文里反而容易混淆模型。

Micro-compact 一类机制会保留消息框架和最近结果,把较老的大块输出替换为占位信息。它不会随便清理用户要求或关键决策,而主要针对可以重新获取的工具结果。

代价很低,因为不需要再次调用模型做摘要。

第三层:对整段历史做摘要

工具结果清理后仍然过长,就必须压缩对话本身。

Auto-compact 会把较早历史整理成摘要,重点保留:

  • 用户目标和明确约束;
  • 已经完成的工作;
  • 失败过的方案及原因;
  • 关键文件和代码位置;
  • 当前未完成事项;
  • 验证结果。

摘要会损失细节,因此成本高于简单截断。这里需要的更接近一份交接记录,而非普通的聊天内容概括:压缩后的 Agent 必须能够接着干活。

第四层:超长请求后的兜底

本地 token 估算与服务端计算可能存在差异,自动压缩也可能没有及时触发。如果 API 仍然返回上下文过长错误,系统需要 Reactive Compact 再次收缩,而不是让整个任务直接失败。

第五层:用户主动 /compact

用户最清楚任务什么时候进入新阶段。

调查结束、准备实现之前主动压缩,往往比等系统被动触发更合理。用户还可以告诉摘要过程重点保留什么,例如“保留数据库兼容性讨论,旧的 UI 尝试可以丢掉”。

为什么不越早压缩越好

压缩会丢信息,而且还可能破坏 Prompt Cache 的稳定前缀。

有些早期细节当时看起来不重要,后面却可能成为排错线索。如果上下文容量仍充足、用户又在短时间内连续工作,过于积极地重写历史未必更便宜,也未必更聪明。

因此,上下文管理追求的是容量、信息完整性和缓存命中之间的平衡,单纯变短没有意义。

长期记忆解决另一个问题

Compact 只服务当前会话。关闭 Claude Code 后,下次怎样知道这个项目必须用 pnpm,或者某个测试在 CI 环境有特殊要求?

长期信息可以进入两类文件:

  • 用户或团队主动维护的 CLAUDE.md 与规则文件;
  • 从会话中提取、按主题整理的项目记忆。

记忆的过程可以理解为:

从一次会话提取有价值的信息
跨多次会话合并、去重、修正
维护简短索引
新会话启动时注入核心内容,细节按需读取

并非所有聊天都值得长期保存。一次临时路径、已经解决的报错、未经验证的猜测,如果永久进入记忆,反而会污染未来任务。记忆系统真正困难的地方,在于决定什么值得留下、什么时候应该更新。

11. 把十层结构重新拼起来

现在回头看一个完整请求:

给登录接口增加失败次数限制。先分析方案,确认后修改代码并运行测试。

Claude Code 的执行过程可能如下。

启动阶段

Harness 获取 CWD、Git 状态和系统环境,加载当前作用域下的规则与权限设置。核心工具完整可见,延迟工具和 Skills 只提供索引。

规划阶段

Plan Mode 限制写操作。主 Agent 用 Glob、Grep 和 Read 找到认证入口、用户表和现有测试。数据库调查如果相对独立,可以交给子 Agent。

如果识别到安全审查 Skill,系统再加载它的完整说明;如果需要查询外部身份服务,则通过 Tool Search 发现相关 MCP 工具。

批准阶段

主 Agent 汇总:需要新增哪些字段,失败计数在哪一层维护,锁定多久,旧接口是否兼容,准备运行哪些测试。用户在真正修改前确认方案。

执行阶段

离开 Plan Mode 后进入 ReAct 循环:

读模型与迁移约定
修改数据结构
观察类型检查错误
同步修改领域类型
运行目标测试
根据失败日志补充边界分支
再次测试并检查 diff

每个工具调用先经过参数与权限检查。编辑后可能触发格式化 Hook,高风险数据库命令可能请求用户批准。子 Agent 的调查结论通过任务结果或文件回到主线。

收尾阶段

Agent 不能只说“完成了”,还要列出实际变更、测试输出、没有验证的部分和潜在风险。会话结束前,有价值的项目知识可以更新到记忆,冗长工具输出则可以在后续压缩中清理。

把它画成一张粗略架构图:

                         ┌──────── Skills
                         ├──────── MCP Servers
                         ├──────── SubAgents
用户 ─→ 权限/计划边界 ─→ 主 Agentic Loop ─→ 最终结果
            │                  │
            │                  ├── LLM 决策
            │                  ├── Tool 执行
            │                  ├── Observation 回传
            │                  └── Hooks 插入确定性动作
            └──── 文件系统、会话历史、项目规则与记忆贯穿全程

12. 最后:Claude Code 强的并不只是模型

把功能一层层加回来之后,可以看到 Claude Code 的两个核心矛盾。

第一个矛盾是自由与控制。

代码任务无法提前写死所有步骤,所以需要 ReAct 和模型在运行时决定路径;但文件修改、权限和部署又不能完全依赖概率判断,所以需要结构化工具、Hooks、Plan Mode 和审批。

第二个矛盾是信息丰富与注意力有限。

Agent 希望拥有更多工具、规则和记忆,但模型每一轮真正需要的只是其中一小部分。Tool Search、文件搜索、Skills、MCP 发现和记忆索引,都在用渐进式披露解决这个问题。

可以用十句话收束全文:

它解决的实际问题
ReAct让模型根据环境反馈连续决定下一步
基础工具让文本决策变成文件、终端和网络动作
渐进式披露避免全部工具定义挤进常驻上下文
文件系统保存项目环境、规则、状态和协作结果
权限系统用确定性程序限制高风险行动
Skills按需加载一类任务的专业方法
MCP用统一协议接入外部工具和数据
Hooks在生命周期节点保证固定逻辑执行
子 Agent 与 Plan Mode隔离复杂任务,并建立行动前的审批边界
上下文与记忆让长任务装得下,让跨会话经验留得住

从这个角度看,Claude Code 不是一个更会写代码的聊天框。它是一套围绕大模型搭建的运行系统:模型负责在不确定环境中判断,程序负责把判断变成可执行、可观察、可约束的行动。

理解了这套结构,再看其他 Agent 产品也会容易很多。不要只问“它用了哪个模型”,还要继续问:它给模型看了什么?模型能调用什么?行动前谁做权限判断?失败结果怎样回来?长任务怎样保存状态?

这些模型之外的问题,往往才决定一个 Agent 是演示效果很好,还是能够真的长期工作。

参考资料