第10章 上下文工程------有限窗口的无限智慧
引言
想象你正在准备一次长途旅行,但你只有一个行李箱,容量有限。你需要精心选择每一件物品,既要确保必需品齐全,又要避免超重。这就是上下文工程的本质------在有限的 token 窗口内,为 AI 提供最相关、最有价值的信息。
Claude Code 面临着一个根本性的约束:上下文窗口。尽管现代 LLM 支持 200K tokens 的上下文,但这仍然是一个稀缺资源。每次对话都需要在这个有限的窗口中平衡系统指令、用户意图、项目代码、历史消息等多种信息。如何高效地收集、组织和压缩这些信息,成为了一个复杂而优雅的工程问题。
概念讲解
上下文窗口的稀缺性
上下文窗口是 LLM 的"短期记忆"。它决定了模型在一次推理中能够"看到"多少信息。200K tokens 听起来很多,但实际使用时会面临多重挑战:
- 系统指令:提示词模板、工具定义、权限规则等固定开销
- 项目代码:源文件、配置文件、文档等动态内容
- 对话历史:用户消息、AI 响应、工具调用记录
- 元数据:Git 状态、时间戳、成本追踪等辅助信息
这些内容会快速消耗 token 预算,导致上下文溢出。因此,上下文工程的核心是在信息密度和资源约束之间找到平衡。
上下文分层架构
Claude Code 采用分层架构来管理上下文:
typescript
// 系统上下文:相对稳定的全局信息
export const getSystemContext = memoize(async (): Promise<{[k: string]: string}> => {
const gitStatus = await getGitStatus()
const injection = feature('BREAK_CACHE_COMMAND') ? getSystemPromptInjection() : null
return {
...(gitStatus && { gitStatus }),
...(injection ? { cacheBreaker: `[CACHE_BREAKER: ${injection}]` } : {}),
}
})
// 用户上下文:动态的项目特定信息
export const getUserContext = memoize(async (): Promise<{[k: string]: string}> => {
const shouldDisableClaudeMd = isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
(isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
const claudeMd = shouldDisableClaudeMd
? null
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
setCachedClaudeMdContent(claudeMd || null)
return {
...(claudeMd && { claudeMd }),
currentDate: `Today's date is ${getLocalISODate()}.`,
}
})
这种分离设计带来了几个好处:
- 独立缓存:系统上下文和用户上下文可以独立更新,避免不必要的重复计算
- 灵活配置:可以通过环境变量或命令行参数控制不同层级的上下文加载
- 性能优化:稳定的系统上下文在整个会话期间只计算一次
缓存策略:一次收集,全程使用
Lodash 的 memoize 函数在这里发挥了关键作用:
typescript
import memoize from 'lodash-es/memoize.js'
export const getSystemContext = memoize(async () => { /* ... */ })
export const getUserContext = memoize(async () => { /* ... */ })
export const getGitStatus = memoize(async () => { /* ... */ })
memoize 的核心思想是记忆化:对于相同的输入,直接返回缓存的结果,避免重复计算。在上下文工程中,这意味着:
-
会话级缓存:在整个对话会话中,Git 状态、CLAUDE.md 内容等只收集一次
-
自动失效 :当系统提示注入变化时,手动清除缓存:
typescriptexport function setSystemPromptInjection(value: string | null): void { systemPromptInjection = value getUserContext.cache.clear?.() getSystemContext.cache.clear?.() } -
性能提升:避免重复的文件系统 I/O 和 Git 命令执行
这种缓存策略类似于"打包行李箱"时的清单------你只需要检查一次物品清单,而不是每次打开箱子都重新清点。
源码分析
Git 状态的智能截断
Git 状态是上下文的重要组成部分,但它的大小不可预测。一个大型项目的 git status 输出可能包含数千行修改的文件。Claude Code 采用了智能截断策略:
typescript
const MAX_STATUS_CHARS = 2000
export const getGitStatus = memoize(async (): Promise<string | null> => {
// ... 获取 Git 状态
const [branch, mainBranch, status, log, userName] = await Promise.all([
getBranch(),
getDefaultBranch(),
execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short'], {
preserveOutputOnError: false,
}).then(({ stdout }) => stdout.trim()),
execFileNoThrow(gitExe(), ['--no-optional-locks', 'log', '--oneline', '-n', '5'], {
preserveOutputOnError: false,
}).then(({ stdout }) => stdout.trim()),
execFileNoThrow(gitExe(), ['config', 'user.name'], {
preserveOutputOnError: false,
}).then(({ stdout }) => stdout.trim()),
])
// 检查状态是否超过字符限制
const truncatedStatus =
status.length > MAX_STATUS_CHARS
? status.substring(0, MAX_STATUS_CHARS) +
'\n... (truncated because it exceeds 2k characters. If you need more information, run "git status" using BashTool)'
: status
return [
`This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.`,
`Current branch: ${branch}`,
`Main branch (you will usually use this for PRs): ${mainBranch}`,
...(userName ? [`Git user: ${userName}`] : []),
`Status:\n${truncatedStatus || '(clean)'}`,
`Recent commits:\n${log}`,
].join('\n\n')
})
这个设计体现了几个关键原则:
- 明确边界 :
MAX_STATUS_CHARS = 2000提供了清晰的限制 - 友好提示:截断时告诉用户原因和替代方案
- 并行收集 :使用
Promise.all并行获取多个 Git 信息,减少等待时间 - 快照语义:明确说明这是对话开始时的快照,不会实时更新
CLAUDE.md 配置文件的加载机制
CLAUDE.md 是项目级别的配置文件,用于存储项目特定的指令、上下文和偏好。它的加载逻辑体现了灵活性:
typescript
export const getUserContext = memoize(async (): Promise<{[k: string]: string}> => {
// CLAUDE_CODE_DISABLE_CLAUDE_MDS: 硬禁用,总是跳过
// --bare: 跳过自动发现(cwd 遍历),但尊重显式的 --add-dir
// --bare 的意思是"跳过我没要求的",而不是"忽略我要求的"
const shouldDisableClaudeMd =
isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
(isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
// 等待异步 I/O(readFile/readdir 目录遍历),让事件循环自然地在第一次 fs.readFile 时让出
const claudeMd = shouldDisableClaudeMd
? null
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
// 为自动模式分类器缓存(yoloClassifier.ts 读取这个
// 而不是直接导入 claudemd.ts,这会创建一个
// 循环依赖:permissions/filesystem → permissions → yoloClassifier)
setCachedClaudeMdContent(claudeMd || null)
return {
...(claudeMd && { claudeMd }),
currentDate: `Today's date is ${getLocalISODate()}.`,
}
})
这段代码展示了几个设计亮点:
- 细粒度控制:通过环境变量和命令行参数提供多层控制
- 避免循环依赖 :使用
setCachedClaudeMdContent打破潜在的循环依赖 - 内存文件过滤 :
filterInjectedMemoryFiles过滤掉内存注入的文件,避免重复 - 条件编译 :使用展开运算符
...(claudeMd && { claudeMd })条件性地添加字段
自动压缩的触发机制
当上下文接近限制时,Claude Code 会自动触发压缩机制。从 query.ts 的导入可以看出,系统支持多种压缩策略:
typescript
import {
calculateTokenWarningState,
isAutoCompactEnabled,
type AutoCompactTrackingState,
} from './services/compact/autoCompact.js'
import { buildPostCompactMessages } from './services/compact/compact.js'
const reactiveCompact = feature('REACTIVE_COMPACT')
? (require('./services/compact/reactiveCompact.js') as typeof import('./services/compact/reactiveCompact.js'))
: null
const contextCollapse = feature('CONTEXT_COLLAPSE')
? (require('./services/contextCollapse/index.js') as typeof import('./services/contextCollapse/index.js'))
: null
这种设计体现了渐进式压缩的思想:
- Token 预警 :
calculateTokenWarningState提前检测上下文使用情况 - 自动压缩 :
isAutoCompactEnabled决定是否启用自动压缩 - 多种策略 :支持响应式压缩(
reactiveCompact)和上下文折叠(contextCollapse) - 特性开关:使用 feature flags 控制不同压缩策略的启用
413 错误的优雅恢复
当上下文真正溢出时,API 会返回 413 错误。Claude Code 通过错误处理机制实现优雅恢复:
typescript
import {
PROMPT_TOO_LONG_ERROR_MESSAGE,
isPromptTooLongMessage,
} from './services/api/errors.js'
虽然具体的恢复逻辑在其他文件中,但这种设计模式表明:
- 错误识别 :通过
isPromptTooLongMessage识别上下文溢出错误 - 统一处理 :使用
PROMPT_TOO_LONG_ERROR_MESSAGE提供用户友好的错误信息 - 自动重试:结合压缩机制,自动重试压缩后的请求
记忆提取:跨会话的知识积累
history.ts 实现了跨会话的历史记录和记忆提取:
typescript
const MAX_HISTORY_ITEMS = 100
const MAX_PASTED_CONTENT_LENGTH = 1024
export async function* getHistory(): AsyncGenerator<HistoryEntry> {
const currentProject = getProjectRoot()
const currentSession = getSessionId()
const otherSessionEntries: LogEntry[] = []
let yielded = 0
for await (const entry of makeLogEntryReader()) {
if (!entry || typeof entry.project !== 'string') continue
if (entry.project !== currentProject) continue
if (entry.sessionId === currentSession) {
yield await logEntryToHistoryEntry(entry)
yielded++
} else {
otherSessionEntries.push(entry)
}
if (yielded + otherSessionEntries.length >= MAX_HISTORY_ITEMS) break
}
for (const entry of otherSessionEntries) {
if (yielded >= MAX_HISTORY_ITEMS) return
yield await logEntryToHistoryEntry(entry)
yielded++
}
}
这个设计体现了几个关键特性:
- 项目隔离:只返回当前项目的历史记录
- 会话优先:当前会话的条目优先返回
- 大小限制 :
MAX_HISTORY_ITEMS = 100防止历史记录无限增长 - 延迟加载:使用异步生成器按需加载历史记录
- 内容分离:大型粘贴内容存储在外部,历史记录只保存引用
设计启示
1. 分层抽象是复杂性的关键
上下文工程展示了如何通过分层抽象来管理复杂性:
- 系统层:稳定的全局信息
- 用户层:动态的项目特定信息
- 会话层:临时的对话历史
每一层都有独立的职责和生命周期,通过清晰的接口交互。
2. 缓存是最简单的性能优化
memoize 的使用证明了一个简单而强大的原则:如果计算成本高且结果可重用,就缓存它。在上下文工程中,这意味着:
- Git 状态只收集一次
- CLAUDE.md 内容只读取一次
- 系统提示只在变化时重新计算
3. 边界条件需要显式处理
MAX_STATUS_CHARS = 2000 和 MAX_HISTORY_ITEMS = 100 体现了显式边界的重要性:
- 明确的限制让系统行为可预测
- 用户友好的错误信息提升体验
- 截断策略保证了系统的稳定性
4. 异步 I/O 的自然让出
CLAUDE.md 加载中的注释展示了异步编程的精髓:
typescript
// 等待异步 I/O(readFile/readdir 目录遍历),让事件循环自然地在第一次 fs.readFile 时让出
const claudeMd = shouldDisableClaudeMd
? null
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
这种设计确保了文件系统操作不会阻塞事件循环,保持了应用的响应性。
5. 特性开关支持渐进式演进
feature('REACTIVE_COMPACT') 和 feature('CONTEXT_COLLAPSE') 的使用表明:
- 新功能可以通过特性开关逐步推出
- 不同用户可以启用不同的功能组合
- 实验性功能可以安全地集成到生产代码中
思考题
-
缓存失效策略 :当前代码在
setSystemPromptInjection变化时清除缓存。如果 Git 状态在对话过程中发生了变化(例如用户执行了git commit),应该如何处理?是否需要提供手动刷新 Git 状态的机制? -
上下文优先级:当上下文窗口即将溢出时,应该如何决定保留哪些信息、丢弃哪些信息?是否应该为不同类型的上下文(系统指令、用户代码、历史消息)设置不同的优先级?
-
增量更新:当前的 Git 状态是"快照式"的,不会在对话过程中更新。如果需要支持增量更新(例如只显示新修改的文件),应该如何设计数据结构和更新机制?
-
跨项目上下文:当前的历史记录是项目隔离的。如果用户在多个相关项目间切换,是否应该支持跨项目的上下文共享?如何设计权限和隐私保护机制?
-
压缩算法选择:不同的压缩策略(响应式压缩、上下文折叠)适用于不同的场景。如何根据上下文的特征(代码密集型、对话密集型、工具调用密集型)自动选择最优的压缩策略?
"上下文窗口的约束不是限制,而是机会。它迫使我们思考什么是最重要的,什么是可以舍弃的。这种选择本身就是一种智慧。" ------ 编程思想