1. 从源码拆解 Claude Code 的上下文工程
作为一名长期从事AI系统开发的工程师,我最近有幸深入研究了Claude Code的源码实现。这套系统最令我惊叹的不是它的AI能力本身,而是它处理上下文的那套精妙工程体系。今天我就带大家从源码层面,看看这个业界领先的AI编程助手是如何管理上下文的。
1.1 上下文工程的核心挑战
在构建AI编程助手这类需要长时间运行、处理多源输入、维护跨会话状态的Agent系统时,我们面临的核心挑战远不止"怎么写prompt让模型表现更好"这么简单。实际工程中需要解决:
- 输入风暴:同一时刻可能有用户键盘输入、后台任务通知、权限请求同时涌入,如何高效排队处理?
- 窗口限制:对话超过20轮后,上下文窗口快满了,如何在不丢失关键信息的前提下进行压缩?
- 知识持久化:当前会话学到的知识,如何让下次会话还能记得?
- 错误兜底:当模型输出幻觉或格式错误时,工程系统如何优雅地处理?
这些问题的答案不在prompt设计里,而在系统架构和工程实现中。Claude Code的解决方案,为我们提供了一个优秀的参考范例。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分层记忆系统:六层优先级的指令加载
2.1 记忆层级架构
Claude Code的记忆系统是整个上下文工程最精巧的部分。与大多数AI应用简单塞入一个system prompt的做法不同,它采用了六层优先级的指令加载策略。
在src/utils/claudemd.ts文件中,我们可以清晰地看到这个层级结构(L1-L26注释):
code复制优先级顺序(从低到高):
1. Managed(全局策略)
2. User(个人偏好)
3. Project(项目级规则)
4. Local(本地目录规则)
5. AutoMem(自动记忆)
6. TeamMem(团队记忆)
关键设计原则是:越贴近当前工作上下文的指令,权重越大。这种设计让系统既能保持全局一致性,又能灵活适应不同场景的特殊需求。
2.2 实现细节解析
让我们深入getMemoryFiles()函数的实现(L790-L1075):
typescript复制// 1. 加载Managed(全局策略)
const managedClaudeMd = getMemoryPath('Managed')
result.push(...(await processMemoryFile(managedClaudeMd, 'Managed', ...)))
// 2. 加载User(个人偏好),允许引用外部文件
result.push(...(await processMemoryFile(userClaudeMd, 'User', processedPaths, true)))
// 3. 从根目录向下遍历到CWD,逐层加载Project和Local
const dirs: string[] = []
let currentDir = originalCwd
while (currentDir !== parse(currentDir).root) {
dirs.push(currentDir)
currentDir = dirname(currentDir)
}
for (const dir of dirs.reverse()) {
// 加载CLAUDE.md, .claude/CLAUDE.md, .claude/rules/*.md
// 以及CLAUDE.local.md
}
// 4. 加载AutoMem和TeamMem
if (isAutoMemoryEnabled()) { ... }
if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) { ... }
几个值得注意的工程细节:
-
目录遍历策略:采用从根目录到当前工作目录(CWD)的遍历顺序,通过
dirs.reverse()确保根目录规则先加载(低优先级),当前目录规则后加载(高优先级)。这种设计特别适合monorepo场景,可以在根目录放置通用规则,在子项目中放置特定规则。 -
模块化引用:支持
@include指令引用其他文件,递归深度限制为5层(MAX_INCLUDE_DEPTH)。这让我们可以把大型文档拆分为模块化结构,CLAUDE.md文件只需维护索引关系。 -
条件规则:
.claude/rules/目录下的文件可以带frontmatter指定paths模式,只在操作匹配路径的文件时才注入。例如,可以创建只针对*.py文件的Python风格指南,而不会影响TypeScript文件的上下文。
typescript复制function parseFrontmatterPaths(rawContent: string): {
content: string
paths?: string[]
} {
const { frontmatter, content } = parseFrontmatter(rawContent)
if (!frontmatter.paths) return { content }
// 解析glob模式,过滤匹配路径
const patterns = splitPathInFrontmatter(frontmatter.paths)
.map(pattern => pattern.endsWith('/**') ? pattern.slice(0, -3) : pattern)
.filter(p => p.length > 0)
return { content, paths: patterns }
}
这种分层、条件化、可组合的记忆系统,将上下文从静态文本变成了动态配置系统,大大提升了灵活性和适应性。
3. 动态上下文组装:智能编译system prompt
3.1 系统上下文构建
Claude Code的system prompt不是写死的字符串,而是在每次对话开始时动态组装的。src/context.ts文件展示了这个过程。
系统上下文分为两部分:systemContext和userContext。
systemContext主要捕获环境状态快照:
typescript复制export const getSystemContext = memoize(async () => {
// 并行获取Git状态:当前分支、主分支、最近5条提交、工作区状态
const gitStatus = await getGitStatus()
return {
...(gitStatus && { gitStatus }),
// 调试用的cache breaker
}
})
getGitStatus()通过Promise.all并行执行五个git命令(L61-L77),获取分支、默认分支、status、log和用户名信息。特别值得注意的是,status输出有2000字符截断限制(MAX_STATUS_CHARS),超过后会提示模型使用BashTool自行运行git status。
设计上,git状态被明确标注为"会话开始时的快照,不会在对话过程中更新"。这个决策很重要,它避免了因频繁刷新导致的缓存失效和上下文波动问题。
3.2 用户上下文合并
userContext负责合并各层级的指令:
typescript复制export const getUserContext = memoize(async () => {
// 合并CLAUDE.md层级:Managed → User → Project → Local → AutoMem → TeamMem
const claudeMd = getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
// 缓存供yoloClassifier使用(决定是否自动执行)
setCachedClaudeMdContent(claudeMd || null)
return {
...(claudeMd && { claudeMd }),
currentDate: `Today's date is ${getLocalISODate()}.`,
}
})
两个上下文都使用了memoize装饰器,确保整个会话期间只计算一次。这意味着CLAUDE.md的修改在当前会话内不会生效(除非显式清除缓存)。这是性能与一致性权衡的结果。
3.3 最终组装
在query循环中,这些上下文被组装进API调用:
typescript复制const fullSystemPrompt = asSystemPrompt(
appendSystemContext(systemPrompt, systemContext)
)
// userContext被前置到消息序列
messages = prependUserContext(messagesForQuery, userContext)
系统提示被分为静态前缀和动态后缀,中间用SYSTEM_PROMPT_DYNAMIC_BOUNDARY分隔。静态前缀可以跨用户缓存共享(prompt cache hit),而动态后缀是用户特定的。这种设计既提高了性能,又保持了灵活性。
4. 上下文压缩:多级递进策略
4.1 压缩策略概览
当对话历史增长到接近上下文窗口上限时,Claude Code不是简单截断,而是采用了一套精妙的多级压缩策略。在src/query.ts的query循环(L307开始)中,每一轮迭代都按顺序执行以下压缩步骤:
- Tool Result Budget:裁剪工具输出
- Snip Compact:轻量删除
- Microcompact:局部压缩
- Context Collapse:折叠(实验性)
- Auto Compact:全量压缩(阈值触发)
这种渐进式压缩策略确保了系统在不同负载下都能高效运行。
4.2 各级压缩实现
工具结果裁剪
typescript复制messagesForQuery = await applyToolResultBudget(
messagesForQuery,
toolUseContext.contentReplacementState,
...
)
每个工具的输出都有字节上限。超大的文件读取结果会被替换为引用指针。这从源头控制了上下文的增长速度。
历史片段删除
typescript复制if (feature('HISTORY_SNIP')) {
const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)
messagesForQuery = snipResult.messages
snipTokensFreed = snipResult.tokensFreed
}
Snip是最轻量的压缩——直接移除旧的、不重要的消息片段,不需要调用模型,几乎没有计算开销。
微压缩
typescript复制const microcompactResult = await deps.microcompact(
messagesForQuery, toolUseContext, querySource
)
messagesForQuery = microcompactResult.messages
Microcompact比Snip更智能,但仍不涉及完整summarization。它只处理可以安全压缩的局部冗余。
上下文折叠
typescript复制if (feature('CONTEXT_COLLAPSE') && contextCollapse) {
const collapseResult = await contextCollapse.applyCollapsesIfNeeded(
messagesForQuery, toolUseContext, querySource
)
messagesForQuery = collapseResult.messages
}
Context Collapse是一个实验性功能,它在消息序列上做"折叠"操作——概念上类似代码编辑器的折叠功能,把一段交互折叠成摘要,但保留展开的能力。
自动全量压缩
typescript复制const { compactionResult } = await deps.autocompact(
messagesForQuery, toolUseContext, { systemPrompt, userContext, ... },
querySource, tracking, snipTokensFreed
)
当token用量超过阈值(effectiveContextWindow - 13000),触发全量压缩。这会调用模型生成会话摘要,替换掉旧消息。
4.3 压缩阈值与熔断
src/services/compact/autoCompact.ts中的阈值计算:
typescript复制export const AUTOCOMPACT_BUFFER_TOKENS = 13_000
export function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model)
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
}
关键的安全机制是:连续失败3次后触发熔断器(MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3),停止重试。这防止了当上下文已经不可恢复地超限时,反复尝试压缩只会浪费API调用的情况。
代码注释中有一个真实案例:
BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures (up to 3,272) in a single session, wasting ~250K API calls/day globally.
4.4 压缩prompt设计
src/services/compact/prompt.ts定义了压缩指令:
typescript复制const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
`
这段前导指令非常强硬,反复强调不要用工具。注释解释了原因:
on Sonnet 4.6+ adaptive-thinking models the model sometimes attempts a tool call despite the weaker trailer instruction. With maxTurns: 1, a denied tool call means no text output → falls through to the streaming fallback (2.79% on 4.6 vs 0.01% on 4.5).
压缩摘要要求保留9个维度的信息:
- 主要请求意图
- 技术概念
- 文件和代码片段
- 错误和修复
- 问题解决过程
- 所有用户消息
- 待办任务
- 当前工作
- 下一步(特别强调用原文引用最近的对话内容)
还有一个巧妙的设计:使用<analysis>标签作为"草稿本",让模型先整理思路再输出摘要。formatCompactSummary()会在摘要进入上下文前把<analysis>部分剥掉——它只是提升生成质量的手段,不占用宝贵的上下文空间。
5. 持久记忆系统:跨会话知识保留
5.1 记忆类型体系
Claude Code不只是在会话中管理上下文,它还有一套跨会话的持久记忆系统。在src/memdir/memoryTypes.ts中,记忆被分为四类:
| 类型 | 含义 | 示例 |
|---|---|---|
| user | 关于用户的信息 | "用户是数据科学家,目前关注可观测性" |
| feedback | 用户给出的行为指导 | "不要在测试中mock数据库,因为上次生产出过问题" |
| project | 项目动态信息 | "下周四开始merge freeze,移动端要切release分支" |
| reference | 外部资源指针 | "pipeline bug跟踪在Linear的INGEST项目里" |
这个分类体系有一个明确的排除原则:
typescript复制export const WHAT_NOT_TO_SAVE_SECTION: readonly string[] = [
'## What NOT to save in memory',
'- Code patterns, conventions, architecture, file paths, or project structure — '
+ 'these can be derived by reading the current project state.',
'- Git history, recent changes, or who-changed-what — '
+ '`git log` / `git blame` are authoritative.',
'- Debugging solutions or fix recipes — '
+ 'the fix is in the code; the commit message has the context.',
...
]
原则很清晰:能从代码和git历史中推导出来的信息,不存储。只持久化那些"不可推导"的知识——用户身份、行为偏好、项目动态、外部引用。
5.2 自动提取机制
src/services/extractMemories/实现了后台记忆提取。它作为主对话的一个fork运行——共享相同的系统提示和消息前缀,但只有有限的工具集(读文件、Grep、Glob、只读Bash、编辑/写入工具仅限memory目录)。
提取prompt包含效率指导:
typescript复制`You have a limited turn budget. FileEditTool requires a prior FileReadTool
of the same file, so the efficient strategy is: turn 1 — issue all
FileReadTool calls in parallel for every file you might update; turn 2 —
issue all FileWriteTool/FileEditTool calls in parallel.`
采用两轮策略:第一轮并行读所有可能要更新的文件,第二轮并行写。最大化利用有限的turn资源。
5.3 记忆验证机制
记忆有个固有问题:存的时候是对的,读的时候可能已经过时了。Claude Code专门为此设计了验证机制(TRUSTING_RECALL_SECTION):
typescript复制"A memory that names a specific function, file, or flag is a claim
that it existed *when the memory was written*. It may have been
renamed, removed, or never merged. Before recommending it:
- If the memory names a file path: check the file exists.
- If the memory names a function or flag: grep for it."
简而言之:记忆不是事实,是历史快照。使用前必须先验证其当前有效性。
5.4 会话内存设计
除了跨会话的持久记忆,还有会话内的结构化笔记系统(src/services/SessionMemory/)。Session Memory使用固定模板维护当前会话状态:
typescript复制export const DEFAULT_SESSION_MEMORY_TEMPLATE = `
# Session Title
# Current State — 当前正在做什么
# Task specification — 用户要求构建什么
# Files and Functions — 重要文件列表
# Workflow — 通常运行哪些命令
# Errors & Corrections — 遇到的错误和修复方法
# Codebase and System Documentation
# Learnings — 什么有效什么无效
# Key results — 关键输出结果
# Worklog — 步骤级工作日志
`
每个section有token上限(2000),总量上限12000 token。当接近限制时会提示模型压缩旧内容。这种机制让Session Memory在长会话中保持可用,不会无限膨胀。
6. 消息队列与状态管理
6.1 三级优先级队列
所有输入——用户键盘输入、后台任务通知、权限请求——都进入同一个队列。src/utils/messageQueueManager.ts实现了三级优先级:
typescript复制const PRIORITY_ORDER: Record<QueuePriority, number> = {
now: 0, // 立即处理
next: 1, // 用户输入
later: 2, // 系统通知
}
两个入队函数区分了优先级:enqueue()默认next(用户输入),enqueuePendingNotification()默认later(系统消息)。这确保用户交互永远优先出队。
6.2 高效状态管理
状态管理绕过了React Context,采用模块级单例+useSyncExternalStore:
typescript复制const commandQueue: QueuedCommand[] = []
let snapshot: readonly QueuedCommand[] = Object.freeze([])
const queueChanged = createSignal()
function notifySubscribers(): void {
snapshot = Object.freeze([...commandQueue])
queueChanged.emit()
}
每次队列变化都创建新的冻结快照——引用变化触发重渲染,不可变性保证并发安全。这比React Context的逐层传播更可靠,尤其在终端UI这种对更新延迟敏感的场景。
6.3 智能批处理
出队时有智能批处理(src/utils/queueProcessor.ts):斜杠命令和Bash模式单独处理(需要完整执行链路和错误隔离),普通消息按mode批量消费。这种设计既保证了关键操作的完整性,又提高了普通消息的处理效率。
7. 附件系统:运行时上下文注入
7.1 多样化附件类型
Claude Code的附件系统(src/utils/attachments.ts,3998行)是上下文工程中最复杂的一环。它负责在每轮对话中动态注入与上下文相关的额外信息。
统计显示系统支持40+种附件类型,包括但不限于:
- 文件附件:用户@mention的文件内容
- 嵌套记忆:根据当前操作路径动态注入的条件规则
- 相关记忆:基于语义相关性从memdir中检索的持久记忆
- TODO/任务提醒:每隔10轮注入待办状态
- Plan模式提醒:每隔5轮注入计划状态
- 技能发现:根据用户意图自动发现并注入相关技能描述
- 诊断信息:LSP诊断结果、lint错误
- 工具增减delta:可用工具集变化的增量通知
- Agent列表delta:子Agent可用性变化的增量通知
- 日期变更:跨日对话时注入新日期
7.2 资源控制机制
相关记忆的注入有严格的资源控制:
typescript复制export const RELEVANT_MEMORIES_CONFIG = {
MAX_SESSION_BYTES: 60 * 1024, // 单会话累计注入上限60KB
MAX_PER_TURN: 5, // 每轮最多注入5个记忆
MAX_SIZE_PER: 4 * 1024 // 每个记忆最大4KB
} as const
整个会话最多注入60KB的记忆内容,超过后停止预取。这防止了长会话中反复注入记忆导致上下文爆炸的问题。
8. Query Loop:系统核心
8.1 主循环架构
src/query.ts的query()函数是整个系统的心脏。它的主循环(简化后)如下:
typescript复制while (true) {
1. Tool Result Budget → 裁剪工具输出
2. Snip Compact → 轻量删除
3. Microcompact → 局部压缩
4. Context Collapse → 折叠(实验性)
5. Auto Compact → 全量压缩(如果需要)
6. 组装system prompt + user context
7. 调用模型API,流式接收响应
8. 执行工具调用(支持流式并行执行)
9. 生成Tool Use Summary(异步)
10. 收集附件(记忆、诊断、提醒等)
11. 判断:还有工具调用未处理?→ continue
12. 判断:需要错误恢复?→ continue
13. 完成 → return
}
8.2 状态管理
每一轮迭代都维护了大量状态:
typescript复制type State = {
messages: Message[]
toolUseContext: ToolUseContext
autoCompactTracking: AutoCompactTrackingState | undefined
maxOutputTokensRecoveryCount: number
hasAttemptedReactiveCompact: boolean
turnCount: number
pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
// ...
}
8.3 错误恢复机制
错误恢复特别值得关注。当遇到不同错误时,系统会采取不同的恢复策略:
- 模型输出超长(触发
max_output_tokens):最多重试3次 - 上下文太大(触发
prompt_too_long):依次尝试:- Context Collapse drain
- Reactive Compact
- 回退模型
这些恢复路径之间有明确的优先级和互斥关系。代码注释中反复强调某个feature flag的withhold和recover必须"一致",否则会"吞掉消息"。
9. 可复用的设计模式
从Claude Code的源码中,我们可以提炼出几个通用的Agent上下文管理模式:
9.1 分层指令 + 就近覆盖
不要使用单一的大system prompt。将指令分层(全局→用户→项目→会话),后加载的覆盖先加载的。这让同一套系统可以适应不同团队、不同项目的需求,同时保持默认行为的一致性。
9.2 渐进压缩
不要等上下文满了才压缩。设置多级阈值(Snip → Microcompact → Collapse → Full Compact),从轻量到重量逐级触发。轻量操作频繁执行、几乎无成本;重量操作只在必要时触发、有完备的失败熔断。
9.3 不可推导原则
只持久化那些无法从当前代码和git历史中推导出来的信息。代码模式、架构、文件结构——这些可以随时重新读取。用户偏好、项目决策动机、外部资源位置——这些不存就丢了。
9.4 附件Delta
不要每轮都全量注入所有上下文。跟踪什么变了,只注入增量(deferred_tools_delta、agent_listing_delta等)。既节省token,又避免重复信息干扰模型注意力。
9.5 确定性包裹不确定性
模型输出是不确定的,但围绕它的一切——消息队列、状态管理、压缩调度、记忆持久化——都是确定性的工程系统。使用Object.freeze()防止意外修改,用引用变化触发更新,用熔断器控制重试,用memoize保证一致性。在不确定的模型输出外面套一层确定性的壳。
10. 总结与启示
Claude Code的上下文工程不是某个单一的技巧,而是一整套相互配合的系统设计:
- 分层记忆解决"什么知识该在什么时候出现"
- 动态组装解决"system prompt如何因时而变"
- 多级压缩解决"大上下文窗口怎么用到极致"
- 持久记忆解决"跨会话如何不失忆"
- 附件系统解决"运行时怎么按需补充上下文"
对构建Agent系统的工程师来说,这里真正值得学的不是某段代码的精巧,而是这种把上下文当一等公民来管理的架构意识。优秀的AI系统需要的不仅是更好的模型和prompt,更需要精心设计的工程体系来管理上下文生命周期。
