第10章 上下文工程——有限窗口的无限智慧

第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()}.`,
  }
})

这种分离设计带来了几个好处:

  1. 独立缓存:系统上下文和用户上下文可以独立更新,避免不必要的重复计算
  2. 灵活配置:可以通过环境变量或命令行参数控制不同层级的上下文加载
  3. 性能优化:稳定的系统上下文在整个会话期间只计算一次

缓存策略:一次收集,全程使用

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 内容等只收集一次

  • 自动失效 :当系统提示注入变化时,手动清除缓存:

    typescript 复制代码
    export 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')
})

这个设计体现了几个关键原则:

  1. 明确边界MAX_STATUS_CHARS = 2000 提供了清晰的限制
  2. 友好提示:截断时告诉用户原因和替代方案
  3. 并行收集 :使用 Promise.all 并行获取多个 Git 信息,减少等待时间
  4. 快照语义:明确说明这是对话开始时的快照,不会实时更新

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()}.`,
  }
})

这段代码展示了几个设计亮点:

  1. 细粒度控制:通过环境变量和命令行参数提供多层控制
  2. 避免循环依赖 :使用 setCachedClaudeMdContent 打破潜在的循环依赖
  3. 内存文件过滤filterInjectedMemoryFiles 过滤掉内存注入的文件,避免重复
  4. 条件编译 :使用展开运算符 ...(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

这种设计体现了渐进式压缩的思想:

  1. Token 预警calculateTokenWarningState 提前检测上下文使用情况
  2. 自动压缩isAutoCompactEnabled 决定是否启用自动压缩
  3. 多种策略 :支持响应式压缩(reactiveCompact)和上下文折叠(contextCollapse
  4. 特性开关:使用 feature flags 控制不同压缩策略的启用

413 错误的优雅恢复

当上下文真正溢出时,API 会返回 413 错误。Claude Code 通过错误处理机制实现优雅恢复:

typescript 复制代码
import {
  PROMPT_TOO_LONG_ERROR_MESSAGE,
  isPromptTooLongMessage,
} from './services/api/errors.js'

虽然具体的恢复逻辑在其他文件中,但这种设计模式表明:

  1. 错误识别 :通过 isPromptTooLongMessage 识别上下文溢出错误
  2. 统一处理 :使用 PROMPT_TOO_LONG_ERROR_MESSAGE 提供用户友好的错误信息
  3. 自动重试:结合压缩机制,自动重试压缩后的请求

记忆提取:跨会话的知识积累

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++
  }
}

这个设计体现了几个关键特性:

  1. 项目隔离:只返回当前项目的历史记录
  2. 会话优先:当前会话的条目优先返回
  3. 大小限制MAX_HISTORY_ITEMS = 100 防止历史记录无限增长
  4. 延迟加载:使用异步生成器按需加载历史记录
  5. 内容分离:大型粘贴内容存储在外部,历史记录只保存引用

设计启示

1. 分层抽象是复杂性的关键

上下文工程展示了如何通过分层抽象来管理复杂性:

  • 系统层:稳定的全局信息
  • 用户层:动态的项目特定信息
  • 会话层:临时的对话历史

每一层都有独立的职责和生命周期,通过清晰的接口交互。

2. 缓存是最简单的性能优化

memoize 的使用证明了一个简单而强大的原则:如果计算成本高且结果可重用,就缓存它。在上下文工程中,这意味着:

  • Git 状态只收集一次
  • CLAUDE.md 内容只读取一次
  • 系统提示只在变化时重新计算

3. 边界条件需要显式处理

MAX_STATUS_CHARS = 2000MAX_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') 的使用表明:

  • 新功能可以通过特性开关逐步推出
  • 不同用户可以启用不同的功能组合
  • 实验性功能可以安全地集成到生产代码中

思考题

  1. 缓存失效策略 :当前代码在 setSystemPromptInjection 变化时清除缓存。如果 Git 状态在对话过程中发生了变化(例如用户执行了 git commit),应该如何处理?是否需要提供手动刷新 Git 状态的机制?

  2. 上下文优先级:当上下文窗口即将溢出时,应该如何决定保留哪些信息、丢弃哪些信息?是否应该为不同类型的上下文(系统指令、用户代码、历史消息)设置不同的优先级?

  3. 增量更新:当前的 Git 状态是"快照式"的,不会在对话过程中更新。如果需要支持增量更新(例如只显示新修改的文件),应该如何设计数据结构和更新机制?

  4. 跨项目上下文:当前的历史记录是项目隔离的。如果用户在多个相关项目间切换,是否应该支持跨项目的上下文共享?如何设计权限和隐私保护机制?

  5. 压缩算法选择:不同的压缩策略(响应式压缩、上下文折叠)适用于不同的场景。如何根据上下文的特征(代码密集型、对话密集型、工具调用密集型)自动选择最优的压缩策略?


"上下文窗口的约束不是限制,而是机会。它迫使我们思考什么是最重要的,什么是可以舍弃的。这种选择本身就是一种智慧。" ------ 编程思想

相关推荐
用户337922545681 小时前
基于 SAM3 + FastAPI 搭建智能图像标注工具实战
人工智能
ylscode1 小时前
谷歌Gemini Go正式登场:轻量级AI助手让低端手机也能玩转生成式智能
网络·人工智能·安全·chatgpt
moonsims1 小时前
基于Lattice Mesh的AI 的分布式共识与动态任务分配架构的无人机群“去中心化无声协同”技术和极低带宽下的韧性通信技术
人工智能·分布式·架构
七牛云行业应用1 小时前
GitHub Copilot 2026年6月新计费实战:AI Credits怎么算、怎么省
人工智能·github·copilot
薛定猫AI1 小时前
【技术干货】DeepSeek 桌面智能体应用全解析:开源 AI Agent 平台实战部署与 API 调用指南
人工智能·microsoft
华山令狐虫1 小时前
告别手写 SQL——DBAPI 企业版 v4.6.0 推出 AI 助手
数据库·人工智能·sql·dbapi
小小龙学IT1 小时前
Midscene.js:AI驱动的跨平台UI自动化革命
javascript·人工智能·ui
触底反弹1 小时前
从 Bun 到 DeepSeek:用 TypeScript 构建你的第一个 AI Agent
人工智能·http·typescript
贵慜_Derek1 小时前
《从零实现 Agent 系统》连载 23|Skill 体系与 Skill Creator:能力打包与迭代
人工智能·设计模式·架构