Claude Code 上下文工程架构:源码级深度解析

35 阅读14分钟

Claude Code 上下文工程架构:源码级深度解析

基于 @anthropic-ai/claude-code@2.1.88 还原源码,结合四层上下文压缩机制与项目实际架构,从源码视角拆解 AI 如何连续聊几百轮也不混乱。


一、为什么 AI 聊久了会"失忆"?

现象与真相

  • 现象:聊着聊着,AI 突然忘了刚才改的哪行代码,开始胡乱回答。
  • 误区:是不是大模型太笨了,理解不了。
  • 真相:是它的上下文窗口被撑爆了。所有发给模型的 messages、system prompt、tool results 都占用 token,当总量接近模型上下文窗口上限(如 200k),模型就无法正常工作。

Claude Code 的应对策略

这不是单点功能,而是一个 分 4 层顶级架构的防御系统,层层往下兜底:

层级名称成本核心机制
L1微压缩 (Microcompact)零成本(纯规则)删除过时工具输出
L2会话记忆 (Session Memory)低成本(fork agent)提炼结构化事实到 MEMORY.md
L3完整压缩 (Full Compact)高成本(调用模型)模型降维打击 + 思维链剥离
L4自动触发 (Auto Trigger)调度系统节拍器 + 熔断器

img.png


二、上下文三层架构:构建发给 AI 的 Prompt

在讨论"压缩"之前,先看上下文是怎么构建的。系统通过三个核心函数构建上下文,在 QueryEngine.ts 中通过 Promise.all() 并行获取:

2.1 SystemPrompt(系统提示词)

  • 文件: prompts.tsgetSystemPrompt()
  • 内容:30+ 工具的 schema 定义、MCP 服务器指令、输出风格配置、技能工具命令、语气和风格指令

2.2 UserContext(用户上下文)

  • 文件: context.tsgetUserContext()
  • 内容:CLAUDE.md 文件内容(项目级 AI 指令)、当前日期
  • 优化:通过 memoize() 缓存,对话期间不变

2.3 SystemContext(系统上下文)

  • 文件: context.tsgetSystemContext()
  • 内容:Git 状态(分支、提交、工作区状态)、缓存破坏器

注入机制

api.ts 中:

// SystemContext 追加到系统提示词末尾
appendSystemPrompt(systemPrompt, systemContext)

// UserContext 作为第一条用户消息前置(标记为 isMeta)
prependUserContext(messages, userContext)

三、Token 是核心资源:5 种计算方式

tokens.ts 中,系统提供了 5 种不同的 token 计算方式,服务于不同场景:

函数用途包含缓存 token
getTokenCountFromUsage()单次 API 调用完整上下文
finalContextTokensFromLastResponse()跨压缩边界的 task_budget 计算
messageTokenCountFromLastAPIResponse()Claude 单次响应生成量否(仅 output)
tokenCountWithEstimation()测量上下文大小的权威函数视情况
roughTokenCountEstimationForMessages()无 API 数据时的 fallback估算

权威函数:tokenCountWithEstimation()

这是自动压缩阈值检查的权威函数。关键设计:

  1. 使用最后一次 API 响应的 实际 token 计数(来自 API usage)
  2. 对之后的消息使用 估算(避免重复计算)
  3. 正确处理并行工具调用的 interleaved 消息
消息流: [..., assistant(id=A), user(result), assistant(id=A), user(result)]
                          ↑ 这里只估算1个  ↑ 但实际有2个
解决方案:回退到第一个相同 message.id 的节点

上下文窗口配置

utils/context.ts 中:

const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000    // 所有模型统一 200k
const COMPACT_MAX_OUTPUT_TOKENS    = 20_000     // 压缩摘要最大输出
const MAX_OUTPUT_TOKENS_DEFAULT    = 32_000     // 默认最大输出
const CAPPED_DEFAULT_MAX_TOKENS    = 8_000      //  capped 默认值

四、L1 微压缩:零成本的规则清理

核心逻辑

纯规则驱动,不调用大模型(零花费)。保留最近 N 个白名单工具结果,删除过时的大体积时效性输出(如 Read、Bash、Grep 等工具的历史结果)。

挑战:缓存如何保护?

切token,在哪切,怎么切? 移除字幕并重新生成图片 (6).png

  • 之前发过去的 Prompt 前缀,其实是被缓存在服务器的。下次你再聊,如果前缀没有变,服务器直接复用,不仅便宜,效率还特别高。
  • 切下去,绝对不能够把服务端的缓存搞失效了。
  • 问题:怎么切才既能删除垃圾,又能保住缓存?

双轨决策机制

模式触发条件操作方式
Cached Microcompact用户连续对话,缓存存活调用专门API 精细编辑,只删旧结果,原来的prompt前缀不变
Time-based Microcompact缓存过期或超时本地替换,长输出 → 简短占位符

线程隔离 (QuerySource)

主线程 (Main Thread) 与 Forked Agent (子Agent) 隔离。防止子 Agent 误删主线程不存在的工具记录,导致上下文逻辑乱套。

img_1.png


五、L2 会话记忆:提炼事实,而非简单摘要

核心思路

绝对不要给历史记录做简单的摘要(会丢失细节)。而是将杂乱的聊天记录提炼成 结构化的事实

机制:The Memory Elevator

后台悄悄派一个 Forked Agent,将对话提炼为结构化文件:

# MEMORY.md (自动生成的位置: .claude/memory.md)

如:
- project
  - TypeScript、Vue、React框架
- progress
  - DB Migration 80%
- preference
  - camelCase 命名规范等

img_2.png

源码位置

  • 文件: sessionMemory.ts(495 行)
  • 触发方式
    • 阈值触发(上下文达到一定大小时自动触发)
    • 手动触发(/summary 命令)
  • 执行方式:隔离的 forked agent 执行提取,不影响主对话

优势

用极小的结构化文件覆盖长期上下文,同时保留最近几轮完整原始对话。

问题

  • 到底要保留多少会话内容?

3个阈值拨盘 (Threshold Dials)

参数默认值作用
最小 Tokens 量10,000太小的对话不压缩
最小消息条数5 条至少需要足够的对话轮次
最大 Token 上限40,000超过此值强制压缩
  • 它会从最后一条已经总结过的消息开始,一直往上处理,去拓展,直到同时满足上述的某个条件
  • 这个拓展过程中,也非常容易引发生产安全事故

六、结构边界锁:防止 API 报错与碎片化

在压缩过程中,系统设置了多重"结构边界锁"来防止 API 层面的错误:

Tool 绑定锁

如:保留 Tool Result(工具执行结果),必须绑定保留对应的 Tool Use(工具调用指令等),否则 API 会报错400。消息结构必须完整配对。

Message ID 绑定

流失碎片化的坑:强制绑定 <thinking>(思考过程) 和 tool_use,系统在这个边界处理上,强行讲一个ID随便绑定在一起,防止在做上下文截断的时候,一刀把模型思考过程切成两半,锁定api的结构边界。

img_3.png

问题:如果上述情况都无法满足,记忆压缩怎么也压不住,或者怎么也压不进去怎么办?

七、L3 完整压缩:模型降维打击

核心流程

compact.ts(1705 行)中实现完整的压缩流程:

1. 记录压缩前 token 数: tokenCountWithEstimation(messages)
2. 执行 PreCompact hooks
3. 调用 AI 生成摘要: streamCompactSummary()
   ├── 输入: 庞大对话日志(9个维度)
   │   源码位置: prompt.ts — BASE_COMPACT_PROMPT (第61-143行)
   │   | # | 维度 | 内容 |
   │   |---|------|------|
   │   | 1 | Primary Request and Intent | 用户的所有显式请求和意图 |
   │   | 2 | Key Technical Concepts | 讨论的技术概念、技术栈、框架 |
   │   | 3 | Files and Code Sections | 查看/修改/创建的文件及代码片段,含变更说明 |
   │   | 4 | Errors and fixes | 遇到的错误及修复方式,特别是用户反馈的修正 |
   │   | 5 | Problem Solving | 已解决的问题和正在进行的排查 |
   │   | 6 | All user messages | 所有非工具结果的用户消息(用于理解意图变化) |
   │   | 7 | Pending Tasks | 被明确要求处理的待办任务 |
   │   | 8 | Current Work | 摘要请求前正在进行的工作的精确描述(含文件名和代码) |
   │   | 9 | Optional Next Step | 下一步行动,需与用户最近的明确请求直接对齐,含原文引用 |
   ├── 强制模型进行 <analysis>(深度分析)
   └── 输出: <summary>(最终摘要)
4. 创建压缩边界标记: createCompactBoundaryMessage()
5. 构建压缩后消息: buildPostCompactMessages()
6. 执行 PostCompact hooks

神来之笔:思维链剥离

系统会直接把 <analysis> 剥离掉,不进入最终上下文。只保留 <summary>

就像考试,草稿纸(分析过程)不交卷,只交答题卡(摘要)。既保证质量,又省下 Token。

缓存友好设计

cacheSafeParams: CacheSafeParams  // 保持 prompt cache 命中
  • 使用 cacheSafeParams 确保压缩操作不会破坏服务端已缓存的 prompt 前缀。
  • 其实系统在 Prompt 里,强行对大模型提了个要求:你在给我做最终的摘要之前,必须要在这个 <analysis> 标签里面,自己把前因后果详细地盘一遍,分析清楚。等待模型输出结果,系统在做最后的拼接上下文的时候,会直接一刀把这一段 <analysis> 剥离掉,绝对不让它进入最终的上下文,只保留底下那个绿色的最终摘要。

img_4.png

八、战后重建系统:压缩后重新注入核心模块

问题

  • 压缩相当于无差别降维打击,但可能误伤关键业务上下文。

过载重试机制

  • 如果压缩请求本身太长触发 prompt too long,系统会按 API 轮次分组丢弃旧记录,最多重试 3 次:

解决

压缩后第一件事:模型必须立刻清空当前缓存,重新注入核心模块

注入清单包括:

  • 文件内容(最多 5 个关键文件,5w token预算)
  • 项目的Plan
  • Skill 内容
  • 外挂的MCP工具说明
  • Agent 列表
问题

如果不做这一套会怎么样?

if (summary.startsWith(PROMPT_TOO_LONG_ERROR_MESSAGE)) {
  messagesToSummarize = truncateOldestMessages(messagesToSummarize)
  retryCacheSafeParams = rebuildCacheSafeParams(messagesToSummarize)
  continue  // 重试
}

img_5.png

九、L4 自动触发:系统的节拍器与熔断器

触发公式

每次模型调用后检查:

当前 Token 用量 > (上下文窗口上限 - 13000 Token 缓冲池)

autoCompact.ts(351 行)中:

export function getEffectiveContextWindowSize(model: string): number {
  const reservedTokensForSummary = Math.min(
    getMaxOutputTokensForModel(model),
    MAX_OUTPUT_TOKENS_FOR_SUMMARY  // 20,000
  )
  let contextWindow = getContextWindowForModel(model, getSdkBetas())

  // 支持环境变量覆盖
  const autoCompactWindow = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW
  if (autoCompactWindow) {
    contextWindow = Math.min(contextWindow, parseInt(autoCompactWindow, 10))
  }

  return contextWindow - reservedTokensForSummary
}

img_7.png

四级阈值系统

阈值位置作用
警告阈值~90% 上下文窗口提示用户上下文即将满了
错误阈值~95% 上下文窗口接近危险线
自动压缩阈值剩余 < 13k token触发自动压缩
阻止限制极限值达到后阻止新操作

系统死锁防御

明确排除 session_memorycompact 这两个 Fork 出的子 Agent 触发自动压缩:

防止子 Agent 嵌套触发自身压缩,陷入无限级联死锁。

熔断机制 (Breaker)

如果连续 3 次压缩失败,直接 break 停止重试。

真实事故:2026曾发生过一天浪费 25 万次 API 调用的事故,后续全靠这个熔断器止损。

image.png


十、外围防线:工具系统的上下文减负

指针替换模式 (Pointer Swap)

巨型 JSON (Tool Result)  →  存入外部数据库
上下文中只投递           →  使用数据指针 + 简短摘要
模型想查细节             →  拿着指针去外部取

渐进式工具扩展 (Progressive Tool Expansion)

  • 一开始只给核心工具(Read, Edit, Bash...)
  • 需要时才通过 MCP 或远程发现加载新工具
  • 目的:治好大模型的"选择困难症",减少错误重试带来的上下文污染

MCP 相关文件:services/mcp/

  • 连接外部 MCP 服务器
  • 动态加载工具定义
  • OAuth 认证支持
  • 工具搜索(ToolSearch)

img_8.png

十一、核心循环:Query Loop 的 Token 管理

整个系统的核心循环在 query.ts(1729 行)中:

async function* queryLoop(params, consumedCommandUuids): AsyncGenerator<...> {
  let state: State = {
    messages: params.messages,
    maxOutputTokensOverride: params.maxOutputTokensOverride,
    autoCompactTracking: undefined,
    maxOutputTokensRecoveryCount: 0,
    hasAttemptedReactiveCompact: false,
    turnCount: 1,
    ...
  }

  const budgetTracker = feature('TOKEN_BUDGET') ? createBudgetTracker() : null
  let taskBudgetRemaining: number | undefined = undefined

  while (true) {
    // 1. 检查上下文窗口
    const tokenUsage = tokenCountWithEstimation(state.messages)

    // 2. 检查是否需要压缩
    const warningState = calculateTokenWarningState(tokenUsage, model)
    if (warningState.isAboveAutoCompactThreshold) {
      const compactionResult = await compactConversation(...)

      // 更新任务预算(跨压缩边界)
      if (params.taskBudget) {
        taskBudgetRemaining = Math.max(0,
          (taskBudgetRemaining ?? params.taskBudget.total) - preCompactContext
        )
      }

      state.messages = buildPostCompactMessages(compactionResult)
    }

    // 3. 执行 API 调用
    yield* executeAPIQuery(...)

    // 4. 处理工具调用

    // 5. 检查预算
    if (feature('TOKEN_BUDGET') && budgetTracker) {
      const decision = checkTokenBudget(budgetTracker, agentId, budget, globalTurnTokens)
      if (decision.action === 'stop') {
        return { reason: 'budget_exhausted', ... }
      }
    }
  }
}

十二、Token 预算系统

支持用户精确控制 token 消耗。在 utils/tokenBudget.ts 中:

三种语法格式

// 1. 简写(开头): "+500k"
// 2. 简写(结尾): " +500k."
// 3. 详细: "use 2M tokens" / "spend 500k tokens"

运行时检查

query/tokenBudget.ts 中:

  • 完成阈值:90% — 达到后停止
  • 收益递减阈值:500 token — 连续 3 次增量都 < 500 token 时停止

十三、特性开关系统

大量使用 feature()getFeatureValue_CACHED_MAY_BE_STALE() 进行特性控制:

feature('TOKEN_BUDGET')              // Token 预算功能
feature('REACTIVE_COMPACT')          // 响应式压缩
feature('CONTEXT_COLLAPSE')          // 上下文折叠
feature('HISTORY_SNIP')              // 历史裁剪
feature('COORDINATOR_MODE')          // 协调器模式
feature('KAIROS')                    // 助手模式
feature('EXPERIMENTAL_SKILL_SEARCH') // 实验性技能搜索
feature('PROMPT_CACHE_BREAK_DETECTION') // Prompt 缓存破坏检测

设计目的:特性灰度发布、A/B 测试、内部调试、实验性功能保护。


十四、四层压缩机制全景对比

层级操作触发条件调用模型产出
L1 微压缩删除旧输出连续对话缓存校验 / 时间超限否(零成本)白名单工具旧输出被清理
L2 会话记忆提炼/替换Token 逼近危险线否(低成本 fork)MEMORY.md 事实覆盖
L3 完整压缩总结 + 内嵌 CoTL2 失效或压缩后仍超标是(高成本)<analysis> 剥离的高质量全局总结
L4 自动触发调度/熔断剩余容量 < 13000 Token调度系统系统级防死锁与死循环拦截

十五、核心洞见:被动容器 vs 主动战场

传统的刻板认知

上下文窗口是一个静态的、被动的"容器",只要没满就随便塞,各种无效内容都往里塞。

Claude Code 工程哲学

上下文窗口是一个极端稀缺的战略资源,必须进行主动且有进攻性的调度与管理。

Token灵魂拷问

  • 不是问这条数据"能不能装下"
  • 每一条数据,始终追问"该不该装进去"

总结

从规则清理到模型降维,再到系统级熔断,这是一套教科书级别的上下文管理方案。

img_9.png

十六、数据流全景

用户输入
  
QueryEngine.submitMessage()
  
构建上下文: SystemPrompt + UserContext + SystemContext(并行 Promise.all)
  
query() 循环
  ├── L1: 微压缩(规则清理,零成本)
  ├── L2: 会话记忆(结构化事实提取)
  ├── L3: 完整压缩(模型降维 + 思维链剥离)
  ├── L4: 自动触发(节拍器 + 熔断器)
  ├── Token 计数(5 种方式精确测量)
  ├── 预算检查(用户指定 + 收益递减)
  ├── API 流式调用(看门狗 + 非流式降级)
  └── 工具执行(MCP 扩展 + 渐进式加载)
  
SDK / REPL 输出

十七、代码源索引

模块核心文件行数职责
入口main.tsx4684CLI 主入口、命令路由
初始化init.ts340系统初始化、配置
QueryEngineQueryEngine.ts1295对话生命周期管理
Query 循环query.ts1729核心查询循环、token 管理
Token 计数tokens.ts2615 种 token 计算
Token 预算tokenBudget.ts73用户预算解析
预算跟踪query/tokenBudget.ts93运行时预算检查
上下文构建context.ts189UserContext/SystemContext
上下文配置utils/context.ts221窗口大小、输出限制
系统提示prompts.ts914SystemPrompt 构建
自动压缩autoCompact.ts351压缩阈值计算
压缩实现compact.ts1705压缩完整流程
API 调用claude.ts3419流式 API 调用
会话内存sessionMemory.ts495跨会话记忆
上下文分析analyzeContext.ts1382上下文可视化
Token 估算tokenEstimation.ts495Token 估算服务

总计还原文件数:4756 个(含 1884 个 .ts/.tsx 源文件)


十八、关键设计模式

18.1 缓存策略

策略用途
lodash-es/memoize()缓存上下文函数(UserContext/SystemContext)
Anthropic Prompt Cache服务端缓存,通过 cacheSafeParams 保护
File State Cache文件状态缓存避免重复读取
Claude.md Cache项目指令缓存

18.2 异步并行

// 并行获取三个上下文
const [defaultSystemPrompt, userContext, systemContext] = await Promise.all([
  getSystemPrompt(...),
  getUserContext(),
  getSystemContext(),
])

18.3 Generator 流式处理

整个系统重度使用 async function*yield*

async function* query(params): AsyncGenerator<..., Terminal> {
  yield* queryLoop(params, consumedCommandUuids)
}

18.4 状态管理

// 不可变参数(从不重新赋值)
const { systemPrompt, userContext, ... } = params

// 可变跨迭代状态
let state: State = { messages: params.messages, ... }

// 循环中整体替换状态
state = { ...state, messages: newMessages, transition: 'auto_compact' }

十九、错误处理与恢复

max_output_tokens 错误恢复

const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3
if (error.apiError === 'max_output_tokens') {
  if (state.maxOutputTokensRecoveryCount < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) {
    state.maxOutputTokensOverride = ESCALATED_MAX_TOKENS // 64,000
    state.maxOutputTokensRecoveryCount++
    continue
  }
}

流式降级

流失败时自动降级到非流式请求。

Prompt Too Long 重试

压缩请求本身也可能 prompt too long,系统会截断最旧的消息并重试。


二十、性能优化总结

优化方向具体措施
启动性能延迟加载、早期配置、API 预连接、Profiler Checkpoint
运行时性能Token 估算、Prompt Cache、并行工具调用、流式处理
内存管理消息压缩、文件缓存、优雅关闭、Stream 资源释放
上下文管理L1-L4 四层压缩、指针替换、渐进式工具扩展

img_10.png

分析基于 @anthropic-ai/claude-code@2.1.88 还原源码,仅供技术研究学习