ClaudeCode源码深度精讲part1–上下文
今天我们继续让codex老师带我们逐行学习claudecode的源码,我们先从上下文构建入手。
这个问题值得单独写一篇。作为一个懂的智能体搭建,想要在智能体领域有所建树的产品和研发,第一个面对的问题大概就是处理各种用户复杂环境下面的上下文
大部分agent在设计自己的上下文,默认指聊天记录,遇到压缩。Claude Code 里的上下文至少包括六块:
这六块内容每一块都有自己的装载方式、自己的优先级、自己的清理策略。Claude Code 的很多稳定性,都是这套拆法带来的。
## 一、Claude Code先解决“你现在在哪”
Claude Code 在启动阶段就会预热两类上下文:`getSystemContext()` 和 `getUserContext()`。相关代码在 [main.tsx](/..src/main.tsx) 和 [context.ts](/..src/context.ts)。
`system context` 解决的是“你现在在哪”。
这一层最典型的内容是 `gitStatus`。系统会把当前仓库状态、分支、工作区信息整理出来,放进模型的背景里。这个动作很像一个新人进组第一天,先被告知:现在在什么仓库,当前分支是什么,代码有没有未提交修改。
`user context` 解决的是“这个地方平时怎么干活”。
这一层里最关键的内容是 `claudeMd`,也就是 `CLAUDE.md` 体系,还有 `currentDate` 这样的基础信息。模型进入对话之前,先知道这个项目有哪些约定、有哪些额外规则、今天是什么时间。
这里的产品价值很直接。Claude Code 没有把一次任务理解成“用户发来一句话,我临时想办法回答”。它先给模型建立一个工作场景。模型先进入一个具体项目,再开始做事。
二、CLAUDE.md本质上是一套项目说明书系统
如果只看产品表面,会把 `CLAUDE.md` 理解成“给模型看的 README”。源码里的实现远比这个复杂。
核心逻辑在 [claudemd.ts](/..src/utils/claudemd.ts)。它会按顺序加载几类文件:
-
-
-
-
Project memory 这一层又拆成了三部分:
更重要的是,它会从当前工作目录一路往上找。离当前目录越近的规则,权重越高。这个设计很像公司制度、部门制度、项目制度、子目录制度逐层往下套。
如果你在一个 monorepo 里工作,根目录可以有一份总规则,某个业务目录可以有自己的 `CLAUDE.md`,某个子模块还可以在 `.claude/rules` 里放几条只对特定路径生效的规则。Claude Code 读到具体文件时,能拿到一套更贴近当前上下文的说明书。
这套机制对产品很重要。因为真实项目里的规则从来都不是平铺的。公司有公司的规范,团队有团队的规范,仓库有仓库的规范,目录有目录的规范。Claude Code 把这件事做成了代码。
## 三、Claude Code知道“这个目录有自己的脾气”
`CLAUDE.md` 体系里最有意思的一部分,目录局部规则。
这部分逻辑主要在 [attachments.ts](/..src/utils/attachments.ts) 的 `getNestedMemoryAttachmentsForFile(…)`,以及 [claudemd.ts](/..src/utils/claudemd.ts) 里的几组辅助函数。
模型刚读到某个文件时,系统要不要顺手把这个文件所在路径上的局部规则补给它。
Claude Code 的答案是要,而且补得很细。
它会先看当前涉及的是哪个文件,再去算这个文件从当前工作目录一路往下经过了哪些目录。然后它会把这条路径上可能相关的 `CLAUDE.md`、无条件规则、条件规则都捞一遍,再转成 attachment 注入模型。
## 四、用户眼前的现场信息,也会进入上下文
Claude Code 还有一大块上下文,来自用户当下的操作现场。
这部分主要也是在 [attachments.ts](/..src/utils/attachments.ts)。
因为很多编码场景里,用户真正想表达的重点,并不完全在自然语言输入里。用户可能只说一句“这里帮我看一下”,真正的重点其实在他刚刚选中的 30 行代码里。用户可能没有口头说明当前正在修改哪个模块,但 IDE 当前打开的文件已经把意图暴露出来了。用户可能没有重复强调构建报错,但诊断信息本身已经足够说明问题。
Claude Code 的做法很工程化。它把这些“现场信息”统一做成 attachment,然后再编译成模型能理解的消息。
这部分源码非常像一个成熟产品团队的思路。它不是等用户把所有上下文重新说一遍,而是主动从环境里拿那些已经存在的有效信号。
## 五、历史记忆分成两种,一种靠路径,一种靠相关性
Claude Code 的“记忆”并不是一个大仓库统一处理。它至少拆成了两种不同机制。
### 1. 一种记忆是跟路径走的
前面说过的 nested memory 就属于这一类。
它的触发条件很明确:你读到了某个文件,系统就去把这个文件路径附近的规则补出来。这类记忆更像“这个地方的制度”。
### 2. 另一种记忆是跟问题走的
这类就是 `relevant memories`。
核心代码在 [attachments.ts](/..src/utils/attachments.ts) 的 `startRelevantMemoryPrefetch(…)` 和 [findRelevantMemories.ts](/..src/memdir/findRelevantMemories.ts)。
系统会看你这一轮到底在问什么,然后去 memory 目录里挑出最有可能和当前任务相关的那几份记忆。
源码里会先扫描 memory 文件头,再调一次 side query,让模型在候选里只挑最多 5 个明确相关的文件。这个动作很克制。系统对“少而准”的偏好非常明显。
`startRelevantMemoryPrefetch(…)` 会先把这件事启动起来,让它在主流程执行的时候后台去找。等这一轮 tool 执行得差不多了,预取结果如果已经出来,再补进下一轮消息里。
如果某个 memory 文件模型已经通过 FileRead、Write、Edit 或前一轮 surfacing 见过了,这一轮就不重复送。
这套设计的产品意义很大。它说明 Claude Code 对“记忆”的理解不是“存得越多越好”,而是“当前这轮拿出来的那几份东西要真的有用”。
## 六、auto memory和CLAUDE.md在产品里扮演不同角色
这部分可以从 [memdir.ts](/..src/memdir/memdir.ts) 和 [paths.ts](/..src/memdir/paths.ts) 看出来。它有自己独立的入口 `MEMORY.md`,有自己的目录,有自己的写入方式,还有一整套关于 memory type、索引格式、截断规则的说明。
在某些模式下,系统会把 auto memory 的“使用规则”放进 system prompt,把真正的记忆内容留给后续动态召回。这个设计很有产品感。
内容负责回答“当前这一轮要不要把哪段记忆拿出来”。
这两层分开之后,系统就更容易扩展。你可以不断优化 memory 的召回逻辑,而不用每次都去改整套基础规则。
## 七、工具输出在Claude Code里属于高风险上下文
相关逻辑在 [toolResultStorage.ts](/..src/utils/toolResultStorage.ts)。
Claude Code 对 tool result 的态度很明确:它很有价值,但也很容易把上下文拖垮。
这些内容有信息量,但如果全量塞回模型,后果也很明显。后续轮次会被旧日志挤占,真正有用的信息反而变少。
第一步,大结果可以落盘,只给模型留一个 preview。
第二步,结果预算按 user message 分组来算,超过预算时再决定替换谁。
更细的一层在于,它会把这些替换决定记下来。这个状态放在 `ContentReplacementState` 里,后续 turn、resume、subagent fork 都会继续沿用。
这个设计的产品感觉很像一个有经验的顶级秘书帮总裁在做信息裁剪。完整日志你可以去档案室看,会议桌上只放摘要。下一次开会,继续沿用同一份摘要版本,避免来回改口径。
很多 Agent 产品到这一步还停留在“上下文超了就截断”。Claude Code 已经开始做“工具输出版本管理”了。
## 八、上下文压缩在Claude Code里是日常机制
Claude Code 对压缩的处理非常像日常保洁,不像临时抢救。相关逻辑分散在 [query.ts](/..src/query.ts)、[microCompact.ts](/..src/services/compact/microCompact.ts)、[autoCompact.ts](/..src/services/compact/autoCompact.ts) 和 [compact.ts](/..src/services/compact/compact.ts)。
`query.ts` 里每一轮真正发请求之前,都会先过一遍上下文治理链。顺序大致是:
-
先看 compact boundary 之后还有哪些消息
-
-
-
-
这条顺序很能说明问题。Claude Code 默认接受一件事:只要任务足够长,上下文就一定会变重。系统的工作不是等它炸掉,而是每一轮都提前清一遍。
第一层是小压缩,也就是 microcompact。
这一层主要清理旧的工具输出。比如前面某一轮 grep 打出来的大段结果、测试日志、构建日志、读文件返回的大块内容,当时有价值,往后几轮价值就会快速下降。`microCompact.ts` 做的事情,就是把这类已经过了时效的内容清掉一部分,让后面的轮次不要继续背着它们跑。
这一层不只是把历史变短。`compact.ts` 真正做的是把前面的长对话折叠成一个更短的版本,再重建一块还能继续工作的上下文面。压缩完成后,系统不会只留一个摘要就结束,它还会把最近读过的文件、当前 plan、plan mode 提示、技能信息、异步 agent 状态这些继续工作需要的东西补回来。
这部分特别像一个人在长期做项目时的工作习惯。聊过的细节可以压成笔记,已经看过的大段日志可以收起来,接下来还要盯住的文件、计划和任务状态要继续放在桌面上。Claude Code 的压缩逻辑,本质上就是在做这件事。
`autoCompact.ts` 把这套动作又往前推了一步。它不是等上下文彻底爆掉才压缩,而是先算 token 使用量,到阈值附近就主动触发。部分情况下它还会优先尝试 session memory compaction,也就是先调用已经提炼过的会话记忆,再决定当前轮保留多少近端消息。这说明 Claude Code 不是粗暴的写一个压缩提示词(龙虾就这么干的)粗暴的“把旧聊天总结一下”,它开始把长会话拆成“短期工作区”和“长期提炼记忆”两层。
这一段的产品价值很直接。长任务跑到后半段,真正稀缺的资源已经不是 token 数字本身,而是给到模型的上下文,还能不能维持任务重点。压缩机制的作用,除了能稳定把旧信息折起来,把当前还在发生的工作骨架留在前台。这样一来,对话可以继续变长,模型也不至于一路背着全部历史细节往前挪。
## 九、Claude Code会把消息再整理一遍,再送进模型
通常呢一般人会有一个默认,UI 里看到什么消息,模型就收到什么消息。Claude Code 不是这样。
[messages.ts](/..src/utils/messages.ts) 里的 `normalizeMessagesForAPI(…)` 会在真正发请求前,把消息重新整理一遍。它会做很多事:
-
-
-
-
合并同一个
message.id 的 assistant 碎片
-
-
-
Claude Code 里,给模型看的上下文是“整理后版本”,不是“聊天记录原件”。
这个动作很值钱。因为真实产品里,消息流经常会长歪。流式输出会碎,工具结果会插进来,恢复会打断结构,历史会变得不规整。Claude Code 没有把这些脏活留给模型自己消化,它先在底层做了一遍整理。
## 十、这套上下文系统带来的直接产品效果
只看上下文这一层,Claude Code 的产品思路已经很清楚了。
它想解决的是“让模型在每一轮里只看那批真正应该看的东西”。
项目规则、目录规则、现场信息、相关记忆、工具输出都被拆开处理之后,系统不容易在长任务里逐渐失焦。
用户当前选中了什么、打开了什么、刚改了什么、当前路径下有哪些规则,这些都能进入模型。模型看到的是一个更接近真实工作场景的上下文面。
每一块上下文都可以单独优化。记忆召回可以调整,附件可以扩展,规则装载可以继续细分,工具输出预算可以继续收紧。这个产品后续很好继续迭代。
## 十一、这部分源码给AI产品/研发的真正启发
Claude Code 这部分源码给我的感觉,它把上下文当成了产品主系统在认真对待。做 Agent 的团队如果只盯模型和工具,只做最简单的压缩策略,按比例压缩,后面很容易在长任务场景里遇到稳定性天花板。
这三种东西在产品里解决的问题不同。规则负责告诉模型这个地方怎么做事。记忆负责补充历史背景。现场信息负责反映这一轮到底发生了什么。混在一起做,系统很快会变得又重又乱。
只要产品进入真实开发环境,日志、读文件结果、构建输出都会成为主要噪音来源。Claude Code 在这部分的处理已经说明,工具越强,输出治理越重要。
很多产品上下文只做到“整个仓库”和“整段历史”。Claude Code 里目录级规则、路径级规则、文件现场信息都很重。这种局部上下文对编码质量的帮助,往往比单纯拉长窗口更直接。
## 结尾
今天是这个系列的第一篇,还是codex完成的,我们聚焦在上下文,看看Claude Code 的精妙思路。
可以用一句很朴素的话来概括:系统不断帮模型决定每一次最输入的是什么。
这句话听起来很简单,做起来其实非常重,是真正的智能体搭建的系统化治理思路
Claude Code 在源码里把这件事做成了多层规则、多种记忆、现场附件、输出治理和消息整理的一整套系统。
这一套系统先把地基打稳,后面的工具调用、子智能体和权限编排才有持续发挥的空间。