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 的每一步拆成三个阶段:
- Thought(思考):输出一段思考文本,如“我需要先读取 config.ts 来了解数据库配置”。
- Action(行动):选择工具调用。
- 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 对比
| 维度 | ReAct | Tool-Use Loop |
|---|---|---|
| 推理方式 | 显式 Thought 文本 | 模型内部 Extended Thinking |
| 工具调用 | 解析文本提取 Action | API 原生 tool_use |
| 终止判断 | 检测“Final Answer” | API 原生 end_turn |
| Token 开销 | 每轮输出 Thought | 无额外开销 |
| 编排复杂度 | 高(需解析 Thought/Action) | 低(只需 if/else) |
| 适合场景 | 弱模型 + 简单工具 | 强模型 + 复杂工具集 |
4.4 Plan Mode(先规划再执行)
是什么:Claude Code 的两阶段工作流,在同一个 Tool-Use Loop 中通过 EnterPlanMode 和 ExitPlanMode 两个工具实现。
三步流程:
- 进入规划模式:
- 模型自主判断“这是复杂任务”时调用 EnterPlanMode。
- 简单任务(修 typo、加 console.log)明确不进入。
- 用户也可通过 Shift+Tab 手动切换。
- 只读探索 + 设计方案:
- 权限降为只读,只能用 Read、Grep、Glob 探索代码库。
- 不能写文件、改代码、跑命令。
- 计划写入
.claude/plans/目录。 - 每 5 轮对话系统会偷偷插入“小纸条”,提醒模型“还在 Plan Mode,别手痒改代码”。
- 用户审批后实施:
- 模型调用 ExitPlanMode,需用户确认。
- 批准后权限恢复,模型按计划自由执行读写操作。
设计亮点:
- 对模型来说,Plan Mode 不是特殊“模式切换”,只是调用了两个工具。
- 引擎层不需要做任何特殊处理,query() 仍是简单的 while(true) 循环。
- 体现“工具即能力”的设计理念。
五、System Prompt 的构造
总述:System Prompt 是 Claude Code 的灵魂,定义身份、行为规范、可用工具、安全约束。它不是静态文本,而是动态组装,由十几个 Section 拼接而成,并做了精巧的缓存优化。
5.1 角色定义与安全红线
角色定位:
你是一个交互式代理(interactive agent),帮助用户完成软件工程任务。
请使用下面的指令和可用的工具来协助用户。
重要:你绝对不能为用户生成或猜测 URL,除非你确信这些 URL
是为了帮助用户完成编程任务。你可以使用用户在消息或本地文件中
提供的 URL。- 定位为
interactive agent而非assistant或chatbot,暗示模型应主动行动。 - 立刻划安全红线:不能乱编 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%。
三级缓存体系:
- 全局缓存:分割线之上,跨组织跨用户共享(全球用户用同一份)。
- 组织缓存:同一组织内跨会话共享。
- 会话缓存:同一个 Section 在一次会话内只计算一次。
5.9 System Prompt 设计的三个最值得抄作业的设计
- 先给范围再画红线:先说可以做什么,再说不能做什么。模型拿到判断标准,而非模糊禁令。
- 用两个维度把风险分出层次:不看“看起来危不危险”,而看“能否撤回”和“影响谁”。比“危险/安全”二分法精细得多。
- 静态内容和动态内容用分割线隔开:看似简单的排版调整,背后是实打实的成本优化——让所有用户共享缓存,每次 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 记忆系统核心设计哲学
- 记该记的,不记能推导的:四类型封闭集合 + 排除清单,防止记忆膨胀成垃圾堆。
- 存索引,按需加载详情:MEMORY.md 索引常驻 System Prompt,具体内容独立文件按需加载——既知可用记忆又不撑爆上下文。
- 用小模型做秘书,大模型做决策: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 时触发。
三步流程:
- 生成摘要:调用大模型把整段对话总结成结构化的摘要,要求按多维度总结:
- 用户的主要请求和意图
- 关键技术概念
- 涉及的文件和代码片段
- 遇到的错误和修复方案
- 问题解决过程
- 用户的所有消息(不能遗漏任何一条)
- 待完成的任务
- 当前工作状态
- 建议的下一步
摘要如此细的原因:压缩后模型靠摘要“恢复记忆”,若漏掉关键信息(如待完成任务),模型会忘记。
- 替换旧消息:把压缩边界之前所有消息删掉,替换为摘要。插入边界标记消息,记录压缩前 Token 数,方便追踪。
- 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 别老盯着模型发呆——模型是发动机,但一辆车能不能安全上路,靠的是刹车、方向盘、安全带。这些“不起眼”的东西,才是真正决定成败的。
九、关键行动清单
- 架构设计:采用分层架构,引擎层专注“思考”不混入业务逻辑;工具层用类型系统强制安全属性。
- Agent 循环:信任强模型内部推理能力,用 Tool-Use Loop 而非 ReAct 保持应用层简单。
- System Prompt:用“先给范围再画红线”的安全约束写法;用“可逆性 × 影响范围”判断风险;用分割线划分静态/动态内容以利用 Prompt Cache。
- 记忆系统:限定记忆类型防止膨胀;记忆存为独立文件 + 索引常驻 System Prompt;用小模型做记忆选择器;带陈旧度警告。
- 上下文管理:采用从轻到重的多步压缩策略;大工具结果写盘而非截断;读时投影避免修改原始历史;压缩后主动恢复最近文件内容。