返回
查看原链接原链接
Zhihu

Claude Code 源码剖析:Agent 工程化实践笔记

Claude Code 源码剖析:架构设计、Agent工作模式、System Prompt、记忆系统与上下文窗口管理

Claude Code 的 51 万行核心源码因 npm 打包配置错误而泄露,这为开发者提供了一部 Harness Engineering(线束工程)的教科书式范本——它证明在 AI Agent 领域,80% 的工程投入并非用于让模型“更聪明”,而是用于死磕“可靠性”。

一、背景与核心概念

1.1 事件背景

  • 源码泄露事件:Claude Code 因 npm 打包时配置失误,意外将 .map 文件一并上传,导致 51 万行核心代码公开泄露。
  • 重要澄清:泄露的是 Claude Code 客户端的源码,并非 Claude Opus 大模型的源码。

1.2 Harness Engineering(线束工程)

  • 定义:AI 圈新造的词,本质是“重新包装常识”——与其祈求大模型变聪明、不产生幻觉,不如用系统约束它的行为,给它划好赛道。
  • 行业风向转变:从「拼模型智商」转向「拼系统求稳」。
  • 与 Claude Code 的关系:Claude Code 源码是 Harness Engineering 的最佳实践样本,其中 80% 的代码并非黑科技,而是在解决可靠性问题。

二、Claude Code 是什么

2.1 基本定义

Claude Code 是 Anthropic 官方推出的编程 Agent 工具,是一个能直接在终端里干活的 AI 程序员。它不仅能聊天,还能读代码、改文件、跑命令、管理 Git。

2.2 Agent 与 ChatBot、Copilot 的区别

类型核心特征工作方式
ChatBot一次性的问答问一句答一句
Copilot写代码时给补全建议本质上是一次性预测
Agent感知-决策-行动的自主循环给定目标后自主决定读哪个文件、跑什么命令、改哪行代码,循环几十轮直到完成

2.3 Agent 核心循环

  • Agent 不是按预定义流程图运行,而是每次看到当前对话上下文后自主判断下一步动作。
  • 典型决策选项:读文件?执行命令?回复用户?
  • 循环持续进行,直到任务完成。

三、四层架构设计

Claude Code 采用四层分层架构来组织其庞大子系统(调 API、40 多种工具、权限管理、上下文压缩、记忆维护、多 Agent 协作)。

3.1 引擎层(大脑)

  • 职责:思考与调度,不包含任何业务逻辑。
  • 三个核心功能
  • 协调:把用户输入、系统指令、历史对话拼在一起,发给大模型。
  • 分发:大模型说“要用某个工具”时,找到并执行对应工具。
  • 决策:根据大模型返回决定继续循环还是结束对话。
  • 设计优势:新增能力只需新增工具,引擎层无需修改。

3.2 工具层(能力)

  • 内容:40 多个工具,每个工具就是 Agent 的一个能力(执行 Shell 命令、读写文件、搜索代码、生成子 Agent 等)。
  • 统一规范:强制定义三个安全属性,由类型系统强制要求,漏了任何一项代码就编译不过:
  • 只读还是可修改?
  • 是否具有破坏性,需要额外确认?
  • 能否与其他工具同时执行?
  • 设计哲学:每一把刀都有刀鞘,从出厂就配好了安全机制。

3.3 服务层(基础设施)

  • 内容:所有层共享的“水电煤”:
  • 调大模型 API(主循环、子 Agent 均走此层)。
  • 上下文压缩(五步压缩策略)。
  • MCP 协议(与外部工具服务器通信的标准接口)。

3.4 安全与治理层(安全网)

  • 特殊性:不像其他三层各管一块,而是像安全网罩在所有层之上。
  • 三个模块
  • 权限系统:决定哪些操作需用户确认、哪些可自动执行。
  • Hook 系统:允许在工具执行前后插入自定义行为(如“每次 git push 前自动跑 lint”)。
  • Bash 安全模块:对 Shell 命令做语法级别分析,检测命令注入、路径逃逸等危险模式,而非简单正则匹配关键词。

四、Agent 工作模式

4.1 ReAct 模式及其问题

是什么:2022 年提出的 Agent 范式,核心是把 Agent 的每一步拆成三个阶段:

  1. Thought(思考):输出一段思考文本,如“我需要先读取 config.ts 来了解数据库配置”。
  2. Action(行动):选择工具调用。
  3. Observation(观察):拿到工具结果。

这三步循环直到任务完成。当时流行是因为 GPT-3.5 时代模型推理能力有限,需要显式引导。

三大问题

  • Token 浪费:每轮输出 Thought 文本占用上下文。编程任务循环 50 轮时,累计浪费可达数万 Token。
  • 应用层代码复杂:需解析输出、区分 Thought/Action、提取 Action、拼接 Observation,格式不标准就崩溃。
  • 为弱模型设计:Claude Opus 级别模型推理能力已足够强,可在内部完成推理。

4.2 Tool-Use Loop

核心思路:一个 while(true) 循环,没有 Thought 步骤。模型在内部完成推理(通过 Extended Thinking),直接返回两种结果之一:

  • tool_use:“我要用某个工具”→ 应用层执行工具,把结果拼入消息列表,继续循环。
  • end_turn:“我说完了”→ 跳出循环,把结果返回用户。

核心源码逻辑(TypeScript 伪代码):

async function queryLoop(params, consumedCommandUuids) {
  let state = { messages, toolUseContext, turnCount: 1, ... }
  while (true) {
    // 步骤1:压缩上下文(五步从轻到重)
    // 步骤2:调用大模型API,流式接收
    for await (const event of streamAPI(params)) {
      yield event  // 流式输出每个 token
    }
    // 步骤3:分析模型返回
    if (response.stopReason === 'end_turn') break  // 完成,跳出循环
    // 步骤4:执行工具调用(并发/串行编排)
    const toolResults = await executeToolCalls(toolUseMessages)
    // 步骤5:更新 state,继续循环
    state = { ...state, messages: updatedMessages, turnCount: turnCount + 1 }
    continue
  }
}

核心设计哲学:信任模型的推理能力,保持应用层框架尽可能简单。

4.3 ReAct vs Tool-Use Loop 对比

维度ReActTool-Use Loop
推理方式显式 Thought 文本模型内部 Extended Thinking
工具调用解析文本提取 ActionAPI 原生 tool_use
终止判断检测“Final Answer”API 原生 end_turn
Token 开销每轮输出 Thought无额外开销
编排复杂度高(需解析 Thought/Action)低(只需 if/else)
适合场景弱模型 + 简单工具强模型 + 复杂工具集

4.4 Plan Mode(先规划再执行)

是什么:Claude Code 的两阶段工作流,在同一个 Tool-Use Loop 中通过 EnterPlanModeExitPlanMode 两个工具实现。

三步流程

  1. 进入规划模式
  • 模型自主判断“这是复杂任务”时调用 EnterPlanMode。
  • 简单任务(修 typo、加 console.log)明确不进入。
  • 用户也可通过 Shift+Tab 手动切换。
  1. 只读探索 + 设计方案
  • 权限降为只读,只能用 Read、Grep、Glob 探索代码库。
  • 不能写文件、改代码、跑命令。
  • 计划写入 .claude/plans/ 目录。
  • 每 5 轮对话系统会偷偷插入“小纸条”,提醒模型“还在 Plan Mode,别手痒改代码”。
  1. 用户审批后实施
  • 模型调用 ExitPlanMode,需用户确认。
  • 批准后权限恢复,模型按计划自由执行读写操作。

设计亮点

  • 对模型来说,Plan Mode 不是特殊“模式切换”,只是调用了两个工具。
  • 引擎层不需要做任何特殊处理,query() 仍是简单的 while(true) 循环。
  • 体现“工具即能力”的设计理念。

五、System Prompt 的构造

总述:System Prompt 是 Claude Code 的灵魂,定义身份、行为规范、可用工具、安全约束。它不是静态文本,而是动态组装,由十几个 Section 拼接而成,并做了精巧的缓存优化。

5.1 角色定义与安全红线

角色定位

你是一个交互式代理(interactive agent),帮助用户完成软件工程任务。
请使用下面的指令和可用的工具来协助用户。

重要:你绝对不能为用户生成或猜测 URL,除非你确信这些 URL
是为了帮助用户完成编程任务。你可以使用用户在消息或本地文件中
提供的 URL。
  • 定位为 interactive agent 而非 assistantchatbot,暗示模型应主动行动。
  • 立刻划安全红线:不能乱编 URL,防止用户执行恶意链接。

安全约束(值得 Agent 开发者借鉴的写法):

重要:允许协助已授权的安全测试、防御性安全研究、CTF 挑战赛
和教育场景。拒绝涉及破坏性技术、DoS 攻击、大规模目标扫描、
供应链攻击或用于恶意目的的检测规避请求。
  • “先肯定再约束”写法:先说可以做什么,再划定不能做什么。
  • 比纯禁止清单更有效,模型能拿到判断依据,而非一堆模糊红线。

5.2 行为准则

先读再改

一般来说,不要对你没有阅读过的代码提出修改建议。如果用户
要求你查看或修改某个文件,先读一遍它。在提出修改建议之前,
先理解现有代码。
  • 解决 Agent 的常见问题:根据描述直接生成代码,不先看现有代码,导致风格不一致或重复实现。

少即是多

不要在用户要求之外添加功能、重构代码或进行"改进"。修一个 bug
不需要顺手清理周围的代码。一个简单功能不需要额外的可配置性。

不要为一次性操作创建辅助函数、工具类或抽象层。
三行相似的代码比一个过早的抽象更好。
  • 明确禁止 Agent 修 bug 时顺手重构整个文件。

先诊断再换方案

如果某个方案失败了,先诊断原因再决定是否换方案——读报错信息、
检查你的假设、尝试有针对性的修复。不要盲目重试完全相同的操作,
但也不要因为一次失败就放弃一个可行的方案。
  • 解决“摆烂式重试”或“草率放弃”两个极端。

5.3 操作安全:可逆性与影响范围

核心原则:用可逆性(reversibility)和影响范围(blast radius)两个维度判断风险。

仔细考虑操作的可逆性和影响范围。一般来说,你可以自由执行本地的、
可逆的操作,比如编辑文件或运行测试。但对于难以撤销、影响共享系统
或有风险的操作,请先和用户确认后再执行。

需要用户确认的高风险操作示例

  • 破坏性操作:删除文件/分支、删表、rm -rf
  • 难以逆转的操作:force-push、git reset --hard、修改已发布的 commit
  • 对他人可见的操作:推送代码、创建/关闭 PR、发送消息
  • 上传到第三方工具:内容可能被缓存或索引,即使删除也无法撤回

防权限蔓延的补充

用户批准了某个操作(比如 git push)一次,并不意味着他在所有
场景下都批准这个操作。授权仅对指定的范围有效,不能超出范围。
  • 授权是一次性的、有范围的。此原则在 Agent 权限设计中至关重要。

5.4 工具使用指南:专用工具优先

当有专用工具可用时,不要用 Bash 来执行命令。使用专用工具可以让
用户更好地理解和审查你的工作。这一点至关重要:

读取文件用 Read 工具,而不是 cat、head、tail 或 sed
编辑文件用 Edit 工具,而不是 sed 或 awk
创建文件用 Write 工具,而不是 echo 重定向
搜索文件用 Glob 工具,而不是 find 或 ls
搜索内容用 Grep 工具,而不是 grep 或 rg

设计动机

  • 可审查性:调用 Read 时 UI 展示“正在读取 src/index.ts”;执行 cat 时用户只看到一条命令和一大坨输出。
  • 安全性:专用工具有权限检查(如 Read 检查文件路径是否在允许范围内),Bash 命令没有此保护。

5.5 Git 安全协议

Git 安全协议:
绝不修改 git config
绝不执行破坏性 git 命令(push --force、reset --hard、
  checkout .、clean -f),除非用户明确要求
绝不跳过 hooks(--no-verify),除非用户明确要求
绝不 force push 到 main/master 分支,如果用户要求则发出警告

关键:始终创建新的 commit(NEW commit),而不是用 --amend 修改。
当 pre-commit hook 失败时,commit 实际上并没有发生——所以
--amend 会修改上一个(不相关的)commit,可能导致代码丢失。
正确做法是:修复问题后创建一个新的 commit。

特别值得注意的细节--amend 警告。当 pre-commit hook 失败时 commit 没发生,此时用 --amend 会修改上一个不相关的 commit,导致代码丢失。这类微妙 bug 难以发现,Claude Code 直接在 Prompt 中防住。

5.6 输出风格约束

输出效率
直奔重点。先尝试最简单的方案。要极度简洁。
工具调用之间的文字不超过 25 个词。最终回复不超过 100 个词。

先给出答案或行动,而不是推理过程。跳过填充词、开场白和
不必要的过渡句。不要复述用户说过的话——直接做就行。
  • 25 词限制非常苛刻,旨在避免 Agent“话痨”——没人想看每次读文件前的废话开场白。

5.7 环境信息注入

每次对话开始时注入当前环境信息:

环境信息
主工作目录:/Users/you/my-project
是否为 Git 仓库:是
操作系统平台:darwin (macOS)
Shell 类型:zsh
当前模型:Claude Opus 4.6 (1M context)
知识截止日期:2025 年 5 月
  • 没有这些信息,模型可能在 macOS 上执行 apt-get install,或在 zsh 环境里用 bash 语法。

5.8 分割线与三级缓存

动态边界:System Prompt 中间插入 SYSTEMPROMPTDYNAMICBOUNDARY 标记:

┌─────────────────────────────────────────────────┐
│  [角色定义] 你是一个交互式代理,帮助用户完成...    │  ← 所有用户完全一样
│  [安全红线] 重要:允许协助已授权的安全测试...       │  ← 所有用户完全一样
│  [行为准则] 一般来说,不要对你没有阅读过的代码...   │  ← 所有用户完全一样
│  [操作安全] 仔细考虑操作的可逆性...               │  ← 所有用户完全一样
│  [工具使用] 当有专用工具可用时...                  │  ← 所有用户完全一样
│  [Git 安全] 绝不修改 git config...               │  ← 所有用户完全一样
│  [输出风格] 直奔重点,要极度简洁...               │  ← 所有用户完全一样
├────── SYSTEMPROMPTDYNAMICBOUNDARY ────────┤
│  [环境信息] 主工作目录: /Users/you/my-project    │  ← 每个用户不一样
│  [CLAUDE.md] 本项目使用 TypeScript + Jest...      │  ← 每个项目不一样
│  [记忆指令] 你有一个持久记忆系统...               │  ← 每次对话可能不一样
│  [MCP 指令] 你已连接 GitHub MCP server...         │  ← 每个用户不一样
└─────────────────────────────────────────────────┘

为什么这么设计:Claude API 有 Prompt Cache 机制——两次请求的 Prompt 前缀完全相同,API 复用上次计算结果,费用降低 90%

三级缓存体系

  1. 全局缓存:分割线之上,跨组织跨用户共享(全球用户用同一份)。
  2. 组织缓存:同一组织内跨会话共享。
  3. 会话缓存:同一个 Section 在一次会话内只计算一次。

5.9 System Prompt 设计的三个最值得抄作业的设计

  1. 先给范围再画红线:先说可以做什么,再说不能做什么。模型拿到判断标准,而非模糊禁令。
  2. 用两个维度把风险分出层次:不看“看起来危不危险”,而看“能否撤回”和“影响谁”。比“危险/安全”二分法精细得多。
  3. 静态内容和动态内容用分割线隔开:看似简单的排版调整,背后是实打实的成本优化——让所有用户共享缓存,每次 API 调用省 90% 费用。

六、记忆系统

问题背景:每次启动 Claude Code 都是全新会话,模型不记得上次对话,但用户偏好、项目背景、行为反馈需跨会话保持。

为什么不使用向量数据库

  • Agent 需要记住的大部分不是“相似的文档片段”,而是“用户说过‘不要 mock 数据库’”这种结构化行为指令
  • 向量相似度检索“不要 mock 数据库”效果差——可能匹配一堆含“数据库”关键词的无关内容,重要行为反馈被淹没。

6.1 记忆四类型分类

export const MEMORY_TYPES = [
  'user',      // 用户画像:角色、偏好、知识水平
  'feedback',  // 行为反馈:该做什么、不该做什么
  'project',   // 项目动态:在做什么、截止日期、协作信息
  'reference', // 外部指针:哪里能找到什么信息
] as const

设计动机:不搞通用的 any 类型,因无约束记忆会迅速膨胀成垃圾堆。限定四类型逼 Agent 做分类决策。

四类型详解

  • User(用户画像):记住用户是谁、擅长什么、知识水平。例如“写了十年 Go 的后端工程师,第一次接触 React”,Agent 用后端类比解释前端概念。让回答因人而异。
  • Feedback(行为反馈):记住“不要做什么”和“做得好继续保持”。关键在记录三个维度:
  • 规则本身:集成测试必须使用真实数据库,不能用 mock
  • Why(为什么):上季度 mock 测试全部通过但生产环境迁移失败
  • How to apply(怎么应用):在这个模块写测试时,始终连接真实数据库
  • Why 重要的原因:遇到边缘情况时,Agent 根据 Why 判断规则是否适用。
  • Project(项目动态):记“正在发生什么”。特殊要求:必须把相对日期转成绝对日期。“周四之前冻结合并”要存成“2026-03-05 之前冻结合并”——因“周四”过几天就没意义,绝对日期永远准确。
  • Reference(外部指针):记“去哪找什么信息”(Bug 在 Linear 哪个项目、Grafana 看板地址、Slack 频道)。Agent 不需要知道外部系统具体内容,只需知道去哪找。

6.2 不记什么:排除清单

明确不存的内容

不存内容原因
代码模式、项目架构、文件结构通过 grep、git、CLAUDE.md 可获取;存了导致记忆与代码状态不一致
Git 历史和最近改动git log/blame 才是权威来源
调试方案和修复方法修复已在代码里,commit 消息已记录上下文
CLAUDE.md 已写内容避免重复
临时任务状态和当前对话上下文会话级信息,不需跨会话保持

核心原则可以从当前代码推导出来的信息,一律不存。代码是“活的”随时在变,记忆是“死的”存下来就定格了。若记忆说“AuthService 在 src/auth.ts 第 42 行”但代码已重构,这条记忆就变成“权威的错误”,比没有记忆更糟。

6.3 怎么存:索引 + 独立文件

每条记忆为一个独立 .md 文件,开头有 YAML 格式元信息:

---
name: no-mock-database
description: 集成测试必须使用真实数据库,不能用 mock
type: feedback
---

集成测试必须使用真实数据库,不能用 mock。

Why: 上季度 mock 测试全部通过但生产环境迁移失败了。
How to apply: 在这个模块写测试时,始终连接真实数据库。
  • name:人类可读标识。
  • description:一句话摘要,专门用于检索时的相关性匹配。
  • type:标记四类型之一。

MEMORY.md 索引:不超过 200 行(25KB)的轻量目录:

[No Mock Database](feedback/no-mock-db.md) — tests must use real DB
[User Preferences](user/preferences.md) — prefers terse responses
[Auth Rewrite](project/auth-rewrite.md) — driven by compliance, not tech debt

截断逻辑源码

export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25000  // 25KB

export function truncateEntrypointContent(raw: string): EntrypointTruncation {
  // 同时检查行数和字节数上限
  const wasLineTruncated = lineCount > MAX_ENTRYPOINT_LINES
  const wasByteTruncated = byteCount > MAX_ENTRYPOINT_BYTES

  if (wasLineTruncated || wasByteTruncated) {
    // 截断并附加警告
  }
}
  • 行数和字节数双重检查:防有人写 199 行但每行 500 字——行数没超但字节数爆了。
  • 存储架构关键设计:MEMORY.md 索引始终加载到 System Prompt,独立记忆文件按需加载。这解决了经典矛盾:全塞进 Prompt 会占满上下文,完全不塞则 Agent 不知道有哪些记忆可用。

6.4 怎么召回:Sonnet 当秘书

三步召回流程

第一步:扫描所有记忆文件头信息

// 只读每个文件的前 30 行(frontmatter 区域),不读全文
const { content, mtimeMs } = await readFileInRange(filePath, 0, 30)
const { frontmatter } = parseFrontmatter(content)
  • 足够提取 name、description、type,不读记忆完整内容。
  • 即使有 200 个文件,扫描开销也很小。
  • 按修改时间倒序,最多取 200 个。

第二步:拼接成清单,发给 Sonnet 选择

[feedback] feedback-no-mock.md (2026-03-28): 集成测试必须使用真实数据库
[user] user-preferences.md (2026-03-25): 用户是后端工程师,偏好简洁回复
[project] project-auth.md (2026-03-20): 认证模块重写由合规需求驱动
const result = await sideQuery({
  model: getDefaultSonnetModel(),
  system: '你是一个记忆选择器,从列表中选出最多 5 条与用户问题最相关的记忆...',
  messages: [{ role: 'user', content: `用户问题: ${query}\n\n可用的记忆:\n${manifest}` }],
  max_tokens: 256,  // 只需返回文件名列表,非常短
})
  • Sonnet 只返回文件名列表(如 ["feedback-no-mock.md", "project-auth.md"]),不是记忆内容本身

第三步:加载选中记忆完整内容注入上下文

  • 读取这几条记忆完整内容,以 <system-reminder> 注入当前对话。
  • 附带记忆陈旧度检测:超过 1 天的记忆自动附加警告。
export function memoryFreshnessText(mtimeMs: number): string {
  const d = memoryAgeDays(mtimeMs)
  if (d <= 1) return ''  // 今天或昨天的记忆不加警告
  return `这条记忆已经有 ${d} 天了。记忆是某个时间点的观察,
  不是实时状态——其中关于代码行为或 file:line 引用的断言可能已经过时。
  在当作事实引用之前,请先对照当前代码验证。`
}
  • 例:30 天前存了“AuthService 在 src/auth.ts 第 42 行使用了 JWT”,代码早已改。陈腐度警告提醒模型先验证再引用。

6.5 性能优化:并行预取

  • 执行时机:Sonnet 侧查询在用户提交消息后立刻开始,与主模型 API 调用并行执行
  • 时序:Sonnet 比 Opus 快得多(延迟常仅几百毫秒),主模型响应回来时记忆选择早已完成——几乎不增加额外延迟。
  • 工具过滤小优化:如果用户正在调用某 MCP 工具,Sonnet 选择器自动过滤该工具的使用文档类记忆(模型已在用,用法文档是噪声);但“该工具的已知 bug 和注意事项”类记忆仍会被选中(正在使用时最需要知道坑在哪里)。

6.6 记忆系统核心设计哲学

  1. 记该记的,不记能推导的:四类型封闭集合 + 排除清单,防止记忆膨胀成垃圾堆。
  2. 存索引,按需加载详情:MEMORY.md 索引常驻 System Prompt,具体内容独立文件按需加载——既知可用记忆又不撑爆上下文。
  3. 用小模型做秘书,大模型做决策:Sonnet 负责并行预取和选择,Opus 只管决策,加陈旧度检测,实现零延迟、低成本、高可靠。

七、上下文窗口管理

问题背景:大模型有上下文窗口限制,即使 200K Token 窗口,一次复杂的编程任务(读几十个文件、执行几十条命令)很容易塞满。

业界常见做法 vs Claude Code 做法

做法问题
简单截断(只保留最近 N 条)对编程 Agent 灾难性:20 轮前读过的关键配置可能被截掉
全量摘要很贵(摘要本身是 API 调用),且有信息损失

7.1 五步压缩策略总览

核心理念:压缩一定有信息损失,能不求不压,必须压时从最轻手段开始。如同医院分诊制度。

层级手段信息损失API 开销触发条件
第 1 层大结果存磁盘几乎为零工具结果超 50KB
第 2 层砍掉远古消息消息过时
第 3 层清理老工具输出中低缓存过期/数量超限
第 4 层读时投影压缩上下文达 90%
第 5 层全量摘要高(一次 API 调用)上下文达 ~93%

为什么分五步而非一步到位:每一步代价递增。第 1 层几乎没有信息损失(完整内容还在磁盘,只是不在上下文);第 5 层整段对话变成摘要。大部分场景前三层就够,不需要昂贵全量摘要。

7.2 第 1 步:大结果存磁盘

问题:读 10MB 日志文件,Read 工具返回全部内容吃几万 Token;同时读 3 个大文件可能占大半个上下文窗口。

做法:在工具结果进入消息列表前先做“体检”:

async function maybePersistLargeToolResult(toolResultBlock, toolName) {
  const size = contentSize(content)
  // 单个工具结果超过阈值(默认约 50KB)?
  if (size <= threshold) {
    return toolResultBlock  // 没超,原样通过
  }
  // 超了!把完整内容存到磁盘文件
  const result = await persistToolResult(content, toolUseId)
  // 用一个 2KB 的预览替换原内容
  const preview = buildLargeToolResultMessage(result)
  return { ...toolResultBlock, content: preview }
}
  • 单个工具结果超约 50KB 时,完整内容写盘,消息里只留 2KB 预览摘要。
  • 消息级总量控制:同一条消息所有工具结果总计不超 200KB;超了挑最大几个写盘,直到降到限额以内。
  • 这一层的精妙之处:完整内容没丢,还在磁盘上。模型后面需要某片段可再次调 Read 读特定行范围。

7.3 第 2 步:砍掉远古消息(Snip)

问题:长对话上百轮,开头那几轮(探索性提问、试探性回答)到后面几乎完全没用,但仍占上下文。

做法:直接从对话开头移除一批老消息,插入边界标记告诉模型“这之前内容已清理”。

  • 不做摘要、不总结——“前面聊了什么”直接砍掉。
  • 零 API 开销:不需要额外调用大模型生成摘要。
  • 关键细节:Snip 会把“释放了多少 Token”(snipTokensFreed)传给第 5 层 Auto-Compact。因 Auto-Compact 按“当前上下文占多少 Token”决定是否触发——若 Snip 已释放足够空间,Auto-Compact 就不需触发,避免两层重复压缩。

7.4 第 3 步:裁剪老的工具输出(Micro-Compact)

问题:经过前两层,剩下“不太老但也不太新”的消息,不能直接砍掉(可能还有用),但里面大量工具输出已过时(30 分钟前读的文件可能已经被改过)。

核心思想时间衰减——越老的工具结果越不重要,可裁剪。但不是所有工具结果都能裁剪

const COMPACTABLE_TOOLS = new Set([
  FILE_READ_TOOL_NAME,   // 读文件 → 可以重新读
  ...SHELL_TOOL_NAMES,   // 执行命令 → 可以重新执行
  GREP_TOOL_NAME,        // 搜索 → 可以重新搜
  GLOB_TOOL_NAME,        // 查找文件 → 可以重新查
  WEB_SEARCH_TOOL_NAME,  // 搜索网页 → 可以重新搜
  FILE_EDIT_TOOL_NAME,   // 编辑文件 → 结果可裁剪
  FILE_WRITE_TOOL_NAME,  // 写文件 → 结果可裁剪
])

规律:可被裁剪的都是“可重新获取”的工具。永远不会被裁剪的:AgentTool(子 Agent 输出)、TaskTool(任务状态)——子 Agent 推理过程不可重复,砍掉就真的丢了。

裁剪逻辑:保留最近 N 个,清理其余:

// 收集所有可裁剪工具的结果 ID
const compactableIds = collectCompactableToolIds(messages)
// 保留最近 5 个,其余全部清理
const keepRecent = Math.max(1, config.keepRecent)  // 至少保留 1 个
const keepSet = new Set(compactableIds.slice(-keepRecent))
const clearSet = compactableIds.filter(id => !keepSet.has(id))

裁剪替换标记:被裁剪结果替换为 [Old tool result content cleared],模型看到此标记知道“这里有内容但被清理”。如果后来需要这些信息,可自己决定重新读文件或重跑命令。

为什么叫“时间衰减”:触发条件与时间有关——距上次 API 调用超过一定时间(默认约 60 分钟),说明 API 端 Prompt Cache 大概率已过期。既然缓存已没了,清理旧工具结果也不会浪费之前缓存投入。

7.5 第 4 步:读时投影(Context Collapse)

问题:前三层后上下文仍太大,下一步得做全量摘要。但全量摘要代价高,且丢失细节。需要“中间态”——比全量摘要轻,比 Micro-Compact 重。

核心概念——读时投影(Read-Time Projection)

  • 前三层是“写时压缩”:直接修改消息列表,替换或删除内容。
  • Context Collapse 不修改原始消息,只在调用 API 那一刻动态计算“压缩视图”给模型看。
// 注意:这是一个"读时投影"——不修改 REPL 的完整历史,
// 只在发送给 API 时计算压缩视图
if (feature('CONTEXT_COLLAPSE') && contextCollapse) {
  const collapseResult = await contextCollapse.applyCollapsesIfNeeded(
    messagesForQuery, toolUseContext, querySource
  )
  messagesForQuery = collapseResult.messages
}

两级触发阈值

  • 90% 上下文窗口:主动开始分段压缩旧消息(预留缓冲区)。
  • 95% 上下文窗口:紧急压缩更多内容(留足 API 响应空间)。

最精妙的设计:与第 5 层的配合。Context Collapse 运行在 Auto-Compact 之前。若 Context Collapse 已把上下文压到阈值以下,Auto-Compact 完全不需要触发——模型保留更多细节上下文,而非被粗糙全量摘要替代。

7.6 第 5 步:全量摘要(Auto-Compact)

触发阈值计算

function getAutoCompactThreshold(model: string): number {
  const effectiveContextWindow = getEffectiveContextWindowSize(model)
  // 有效窗口 - 13K 缓冲区 = 触发阈值
  return effectiveContextWindow - 13000
}

以 200K Token 模型为例:有效窗口约 180K(预留 20K 给输出),减去 13K 缓冲区,上下文达 167K Token 时触发。

三步流程

  1. 生成摘要:调用大模型把整段对话总结成结构化的摘要,要求按多维度总结:
  • 用户的主要请求和意图
  • 关键技术概念
  • 涉及的文件和代码片段
  • 遇到的错误和修复方案
  • 问题解决过程
  • 用户的所有消息(不能遗漏任何一条
  • 待完成的任务
  • 当前工作状态
  • 建议的下一步

摘要如此细的原因:压缩后模型靠摘要“恢复记忆”,若漏掉关键信息(如待完成任务),模型会忘记。

  1. 替换旧消息:把压缩边界之前所有消息删掉,替换为摘要。插入边界标记消息,记录压缩前 Token 数,方便追踪。
  1. Post-Compact Restoration(压缩后恢复)
   export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5
   export const POST_COMPACT_TOKEN_BUDGET = 50000
   export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5000
   export const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25000
  • 从文件状态缓存找出最近访问的文件,按最后访问时间排序,挑最多 5 个、共不超过 50K Token 的文件重新注入。
  • 恢复活跃的 Skill(不超 25K Token)。
  • 有进行中的 Plan 时也恢复 Plan 文件。
  • 为什么做恢复:压缩后模型“失忆”了,不记得刚才读过的文件内容。若不恢复,模型第一反应是“让我重新读一下文件”,浪费一轮工具调用。主动恢复最近文件内容可让模型无缝继续。
  • 兜底熔断器:全量摘要连续失败 3 次(如 API 超时)则自动放弃,不无限重试,防止失败压缩拖垮 Agent。

7.7 五层压缩策略的相互协调

  • 各层非孤立运作,而是相互配合:
  • 第 2 层 Snip 告诉第 5 层“已释放多少 Token”,避免重复压缩。
  • 第 4 层 Context Collapse 在第 5 层之前运行,够用则第 5 层不触发。
  • 每一层都在为下一层“减负”。

7.8 核心设计哲学总结

能轻则轻,逐步加码。大部分场景下前三层足够,它们完全不需要额外 API 调用,只是“搬运”和“裁剪”数据。极端情况下才触发昂贵的全量摘要。

八、总结与启发

8.1 Claude Code 源码泄露的意义

  • 提供大量优秀 Agent 技术落地方案。
  • 极大缩短国内外 AI Agent 信息差,可能全面利好国产 Agent 爆发式发展。

8.2 核心启发

Claude Code 每一件事单拿出来都不是黑科技,但全串在一起就是一套能把野马驯成耕牛的缰绳系统。做 Agent 别老盯着模型发呆——模型是发动机,但一辆车能不能安全上路,靠的是刹车、方向盘、安全带。这些“不起眼”的东西,才是真正决定成败的。

九、关键行动清单

  1. 架构设计:采用分层架构,引擎层专注“思考”不混入业务逻辑;工具层用类型系统强制安全属性。
  2. Agent 循环:信任强模型内部推理能力,用 Tool-Use Loop 而非 ReAct 保持应用层简单。
  3. System Prompt:用“先给范围再画红线”的安全约束写法;用“可逆性 × 影响范围”判断风险;用分割线划分静态/动态内容以利用 Prompt Cache。
  4. 记忆系统:限定记忆类型防止膨胀;记忆存为独立文件 + 索引常驻 System Prompt;用小模型做记忆选择器;带陈旧度警告。
  5. 上下文管理:采用从轻到重的多步压缩策略;大工具结果写盘而非截断;读时投影避免修改原始历史;压缩后主动恢复最近文件内容。