第 09 课:上下文构建

0 阅读8分钟

模块三:Agent 核心循环 | 前置依赖:第 08 课 | 预计学习时间:60 分钟


学习目标

完成本课后,你将能够:

  1. 说明 context.ts 中三个核心函数(getGitStatus、getUserContext、getSystemContext)的作用与缓存策略
  2. 描述 computeEnvInfo() 如何组装环境信息并注入 System Prompt
  3. 列举 context/ 目录下 9 个 React Context 的职责
  4. 画出上下文信息从收集到注入 API 调用的完整流转路径

9.1 为什么上下文构建重要?

Claude 在 Claude Code 中不是一个"裸"的 LLM —— 每次 API 调用都携带精心构建的上下文信息。这些上下文决定了 Claude 能做什么、知道什么、以及如何行为。

Claude 看到的完整输入:
┌─────────────────────────────────────────┐
│ System Prompt                           │
│   ├── 角色定义与行为规范                  │
│   ├── 工具使用说明                        │
│   ├── 环境信息(computeEnvInfo)          │ ← context.ts
│   ├── 记忆(CLAUDE.md 内容)              │ ← context.ts
│   ├── MCP 服务器指令                      │
│   └── 输出风格配置                        │
├─────────────────────────────────────────┤
│ User Context(前置到第一条消息)           │ ← context.ts
│   ├── CLAUDE.md 内容                     │
│   └── 当前日期                            │
├─────────────────────────────────────────┤
│ System Context(追加到 System Prompt)    │ ← context.ts
│   ├── Git 状态快照                        │
│   └── 缓存破坏注入(Ant 专用)            │
├─────────────────────────────────────────┤
│ 消息历史                                 │
│   ├── 历史对话                            │
│   └── 工具结果                            │
└─────────────────────────────────────────┘

上下文构建分两层:

  1. context.ts —— 收集原始数据(Git、CLAUDE.md、环境)
  2. constants/prompts.ts —— 组装 System Prompt(使用收集的数据)

9.2 context.ts — 三个核心函数

context.ts 只有 189 行,但它是上下文信息的入口。三个核心函数都用 memoize 缓存:

getGitStatus() — Git 状态快照

export const getGitStatus = memoize(async (): Promise<string | null> => {
  const isGit = await getIsGit()
  if (!isGit) return null

  const [branch, mainBranch, status, log, userName] = await Promise.all([
    getBranch(),                    // 当前分支名
    getDefaultBranch(),             // 主分支名
    execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short'], ...)
      .then(({ stdout }) => stdout.trim()),
    execFileNoThrow(gitExe(), ['--no-optional-locks', 'log', '--oneline', '-n', '5'], ...)
      .then(({ stdout }) => stdout.trim()),
    execFileNoThrow(gitExe(), ['config', 'user.name'], ...)
      .then(({ stdout }) => stdout.trim()),
  ])

  // 截断过长的 status
  const truncatedStatus = status.length > MAX_STATUS_CHARS  // 2000 字符
    ? status.substring(0, MAX_STATUS_CHARS) + '\n... (truncated...)'
    : status

  return [
    `This is the git status at the start of 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. 5 个 git 命令并行执行 —— Promise.all 避免串行等待
  2. --no-optional-locks —— 防止在大仓库中意外获取锁
  3. 状态截断到 2000 字符 —— 防止超大 monorepo 的 status 撑爆上下文
  4. memoize 缓存 —— 整个会话只执行一次,因为这是"会话开始时的快照"

注意注释特别说明:"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." 这告诉 Claude 不要把这个快照当作实时状态。

getUserContext() — 用户上下文

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()))

    // 缓存 CLAUDE.md 内容供 auto-mode 分类器使用
    setCachedClaudeMdContent(claudeMd || null)

    return {
      ...(claudeMd && { claudeMd }),
      currentDate: `Today's date is ${getLocalISODate()}.`,
    }
  },
)

getUserContext 的输出结构:

{
  claudeMd: "# Project Rules\n...",  // CLAUDE.md 文件内容(可选)
  currentDate: "Today's date is 2026-04-08."
}

CLAUDE.md 的加载链:

getUserContext()
  → getMemoryFiles()           // 扫描文件系统找到所有 CLAUDE.mdfilterInjectedMemoryFiles() // 过滤已注入的记忆文件getClaudeMds()             // 读取并合并 CLAUDE.md 内容setCachedClaudeMdContent() // 缓存供分类器使用

三种禁用 CLAUDE.md 的方式:

方式效果
CLAUDE_CODE_DISABLE_CLAUDE_MDS=true完全禁用
--bare 模式(无 --add-dir跳过自动发现
--bare + --add-dir只加载显式指定的目录

getSystemContext() — 系统上下文

export const getSystemContext = memoize(
  async (): Promise<{ [k: string]: string }> => {
    const gitStatus =
      isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ||
      !shouldIncludeGitInstructions()
        ? null
        : await getGitStatus()

    const injection = feature('BREAK_CACHE_COMMAND')
      ? getSystemPromptInjection()
      : null

    return {
      ...(gitStatus && { gitStatus }),
      ...(feature('BREAK_CACHE_COMMAND') && injection
        ? { cacheBreaker: `[CACHE_BREAKER: ${injection}]` }
        : {}),
    }
  },
)

getSystemContext 的输出结构:

{
  gitStatus: "This is the git status...",  // Git 快照(可选)
  cacheBreaker: "[CACHE_BREAKER: ...]"     // 缓存破坏(Ant 专用,可选)
}

缓存破坏机制: BREAK_CACHE_COMMAND feature 允许 Ant 工程师在调试时注入自定义字符串到 System Prompt 中,强制 API 的 prompt cache 失效。


9.3 上下文如何注入到 API 调用

在 query.ts 中,上下文通过两条路径注入:

// 路径 1:System Context → 追加到 System Prompt
const fullSystemPrompt = asSystemPrompt(
  appendSystemContext(systemPrompt, systemContext),
)

// 路径 2:User Context → 前置到消息列表
for await (const message of deps.callModel({
  messages: prependUserContext(messagesForQuery, userContext),
  systemPrompt: fullSystemPrompt,
  // ...
}))

appendSystemContext — 追加系统上下文

System Prompt 原始内容
    
     appendSystemContext(systemPrompt, systemContext)
    
System Prompt + "\n\n" + gitStatus + "\n\n" + cacheBreaker

Git 状态被追加在 System Prompt 的末尾。

prependUserContext — 前置用户上下文

消息列表: [UserMessage1, AssistantMessage1, UserMessage2, ...]
    │
    ▼ prependUserContext(messages, userContext)
    │
消息列表: [UserMessage_with_claudeMd_and_date, UserMessage1, ...]

CLAUDE.md 内容和当前日期被注入到第一条用户消息之前。这利用了 Claude API 的 prompt caching —— System Prompt 部分被缓存,用户上下文在第一条消息中保持稳定。


9.4 computeEnvInfo — 环境信息组装

constants/prompts.ts 中的 computeEnvInfocomputeSimpleEnvInfo 负责组装环境描述:

computeEnvInfo(完整版)

export async function computeEnvInfo(
  modelId: string,
  additionalWorkingDirectories?: string[],
): Promise<string> {
  const [isGit, unameSR] = await Promise.all([getIsGit(), getUnameSR()])

  let modelDescription = ''
  // Undercover 模式下不显示模型信息
  if (process.env.USER_TYPE === 'ant' && isUndercover()) {
    // suppress
  } else {
    const marketingName = getMarketingNameForModel(modelId)
    modelDescription = marketingName
      ? `You are powered by the model named ${marketingName}. The exact model ID is ${modelId}.`
      : `You are powered by the model ${modelId}.`
  }

  return `Here is useful information about the environment you are running in:
<env>
Working directory: ${getCwd()}
Is directory a git repo: ${isGit ? 'Yes' : 'No'}
${additionalDirsInfo}Platform: ${env.platform}
${getShellInfoLine()}
OS Version: ${unameSR}
</env>
${modelDescription}${knowledgeCutoffMessage}`
}

computeSimpleEnvInfo(精简版)

精简版用于主循环的 System Prompt,使用 bullet point 格式:

export async function computeSimpleEnvInfo(
  modelId: string,
  additionalWorkingDirectories?: string[],
): Promise<string> {
  const envItems = [
    `Primary working directory: ${cwd}`,
    isWorktree ? `This is a git worktree...` : null,
    `Is a git repository: ${isGit}`,
    `Platform: ${env.platform}`,
    `Shell: ${shellName}`,
    `OS Version: ${unameSR}`,
    modelDescription,
    knowledgeCutoffMessage,
    // 最新 Claude 模型家族信息
    `The most recent Claude model family is Claude 4.5/4.6...`,
    `Claude Code is available as a CLI...`,
    `Fast mode...`,
  ].filter(item => item !== null)

  return [
    `# Environment`,
    `You have been invoked in the following environment: `,
    ...prependBullets(envItems),
  ].join(`\n`)
}

环境信息包含:

信息来源用途
工作目录getCwd()Claude 知道在哪操作文件
Git 仓库getIsGit()决定是否建议 git 操作
平台env.platform影响路径格式、命令选择
Shellprocess.env.SHELL影响 Bash 命令语法
OS 版本os.type() + os.release()环境诊断
模型信息getMarketingNameForModel()Claude 自我认知
知识截止getKnowledgeCutoff()Claude 知道自己的知识边界
WorktreegetCurrentWorktreeSession()指示在隔离环境中

Undercover 模式

if (process.env.USER_TYPE === 'ant' && isUndercover()) {
  // suppress — 不显示模型信息
}

Undercover 模式下,所有模型名称/ID 从 System Prompt 中移除。这是为了防止内部未发布的模型信息通过 Claude 的回复泄露到公开的 commit/PR 中。


9.5 context/ 目录 — React Context 集合

context/ 目录包含 9 个 React Context,为 UI 层提供全局状态:

context/
├── fpsMetrics.tsx          — FPS 性能指标
├── mailbox.tsx             — 消息邮箱(进程间通信)
├── modalContext.tsx         — 模态窗口状态
├── notifications.tsx        — 通知系统
├── overlayContext.tsx        — 覆盖层追踪(Escape 键协调)
├── promptOverlayContext.tsx  — 提示覆盖层
├── QueuedMessageContext.tsx  — 排队消息上下文
├── stats.tsx               — 统计数据存储
└── voice.tsx               — 语音输入状态

这些 React Context 与 context.ts 中的"上下文构建"是完全不同的概念

context.tscontext/ 目录
层级Agent 循环层UI 层
用途收集数据给 Claude API管理 UI 状态
技术普通函数 + memoizeReact Context + Provider
消费者query.ts → callModelReact 组件

notifications.tsx — 通知系统

通知系统是最复杂的 Context,支持优先级队列:

type Priority = 'low' | 'medium' | 'high' | 'immediate'

type BaseNotification = {
  key: string
  invalidates?: string[]    // 可以取消其他通知
  priority: Priority
  timeoutMs?: number        // 默认 8000ms
  fold?: (accumulator, incoming) => Notification  // 合并同 key 通知
}

type Notification = TextNotification | JSXNotification

通知优先级处理:

immediate → 立即打断当前通知并显示
high      → 排队最前面
medium    → 普通排队
low       → 排队最后面

fold 机制允许同 key 的通知合并,避免重复通知堆积。

voice.tsx — 语音状态

export type VoiceState = {
  voiceState: 'idle' | 'recording' | 'processing'
  voiceError: string | null
  voiceInterimTranscript: string  // 实时转写文本
  voiceAudioLevels: number[]      // 音频电平(用于波形显示)
  voiceWarmingUp: boolean
}

Voice Context 使用 Store 模式(与 AppState 类似):

const VoiceContext = createContext<Store<VoiceState> | null>(null)

export function useVoiceState(selector) {
  const store = useVoiceStore()
  return useSyncExternalStore(
    (callback) => store.subscribe(callback),
    () => selector(store.getState()),
  )
}

overlayContext.tsx — 覆盖层追踪

解决 Escape 键冲突问题:当有覆盖层(Select、MultiSelect 等)打开时,Escape 应该关闭覆盖层而不是取消请求。

export function useRegisterOverlay(id: string, enabled = true) {
  // 挂载时注册
  setAppState(prev => ({
    ...prev,
    activeOverlays: new Set([...prev.activeOverlays, id]),
  }))
  // 卸载时注销
  return () => {
    setAppState(prev => {
      const next = new Set(prev.activeOverlays)
      next.delete(id)
      return { ...prev, activeOverlays: next }
    })
  }
}

stats.tsx — 统计存储

提供三种度量操作:

export type StatsStore = {
  increment(name: string, value?: number): void  // 计数器
  set(name: string, value: number): void         // 量表
  observe(name: string, value: number): void     // 直方图(Reservoir Sampling)
  add(name: string, value: string): void         // 集合
  getAll(): Record<string, number>
}

observe 使用 Reservoir Sampling (Algorithm R) —— 在内存有限(1024 个样本)的情况下均匀采样,计算百分位数统计:

const RESERVOIR_SIZE = 1024
// ...
if (h.reservoir.length < RESERVOIR_SIZE) {
  h.reservoir.push(value)
} else {
  const j = Math.floor(Math.random() * h.count)
  if (j < RESERVOIR_SIZE) {
    h.reservoir[j] = value
  }
}

9.6 上下文构建的时序

所有上下文在会话开始时并行收集:

会话初始化
    │
    ├── getGitStatus()        ─┐
    ├── getUserContext()        ├── 并行执行,memoize 缓存
    ├── getSystemContext()     ─┘
    │
    ▼
首次 query() 调用
    │
    ├── systemPrompt = getSystemPrompt(tools, model, ...)
    │     └── 内部调用 computeSimpleEnvInfo()
    │
    ├── userContext = getUserContext()   // 从缓存读取
    │
    ├── systemContext = getSystemContext() // 从缓存读取
    │
    ▼
callModel({
  messages: prependUserContext(messages, userContext),
  systemPrompt: appendSystemContext(systemPrompt, systemContext),
})

为什么用 memoize 而不是每次重新计算?

  1. 性能 —— Git 命令耗时几百毫秒,不应每轮重复
  2. 一致性 —— 会话内上下文不变,避免 Claude 看到不一致的信息
  3. 语义正确 —— Git 状态是"会话开始时的快照",本就不应实时更新

但 memoize 也有缓存失效的机制:

export function setSystemPromptInjection(value: string | null): void {
  systemPromptInjection = value
  // 手动清除缓存
  getUserContext.cache.clear?.()
  getSystemContext.cache.clear?.()
}

当系统提示注入变化时(调试场景),手动清除缓存触发重新计算。


9.7 getSystemPrompt — 完整的 System Prompt 组装

虽然不完全属于 context.ts,但 System Prompt 的组装是上下文构建的终点:

export async function getSystemPrompt(
  tools: Tools,
  model: string,
  additionalWorkingDirectories?: string[],
  mcpClients?: MCPServerConnection[],
): Promise<string[]> {
  // 返回一个字符串数组,每个元素是一个"段落"
  return [
    // --- 静态内容(可缓存) ---
    getSimpleIntroSection(outputStyleConfig),     // 角色介绍
    getSimpleSystemSection(),                      // 系统行为规范
    getSimpleDoingTasksSection(),                   // 任务执行指南
    getActionsSection(),                            // 可用操作
    getUsingYourToolsSection(enabledTools),          // 工具使用说明
    getSimpleToneAndStyleSection(),                  // 语气与风格
    getOutputEfficiencySection(),                    // 输出效率要求
    // === 动态/静态边界 ===
    SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
    // --- 动态内容(每次可能不同) ---
    sessionGuidance,          // 会话特定指导
    memoryPrompt,             // 记忆内容
    envInfo,                  // 环境信息 ← computeSimpleEnvInfo
    languageSection,          // 语言设置
    outputStyleSection,       // 输出风格
    mcpInstructions,          // MCP 服务器指令
    scratchpadInstructions,   // 暂存区指令
    frcSection,               // Function Result Clearing
    summarizeToolResults,     // 工具结果摘要指令
  ].filter(s => s !== null)
}

注意 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记 —— 它将 System Prompt 分为"静态"和"动态"两部分。静态部分在不同会话之间完全相同,可以利用 API 的 prompt cache。动态部分每个会话不同(环境、记忆等),不能缓存。


课后练习

练习 1:上下文数据流图

画出从以下数据源到 Claude API 调用的完整数据流:

  1. .claude/settings.json(用户设置)
  2. CLAUDE.md(项目规则)
  3. git status(Git 状态)
  4. os.platform()(操作系统信息)

标注每个数据经过哪些函数、存储在哪里、最终注入到 API 请求的什么位置。

练习 2:缓存策略分析

getUserContextgetSystemContext 都使用 memoize 缓存。思考:

  1. 如果用户在会话中途修改了 CLAUDE.md,Claude 会看到更新吗?
  2. 如果 Git 分支在会话中途切换了,Claude 会知道吗?
  3. 有没有场景下缓存会导致问题?
  4. 如果让你改进这个缓存策略,你会怎么做?

练习 3:React Context vs Agent Context

对比 context/ 目录下的 React Context 和 context.ts 中的上下文构建:

  1. 它们各自服务于什么消费者?
  2. 如果 Voice Context 需要影响 Claude 的行为(比如"用户正在语音输入,请等待"),信息需要经过什么路径?
  3. Notification Context 和 SystemMessage 有什么关系?

练习 4:设计新上下文

假设 Claude Code 需要感知"当前终端窗口大小"来优化输出格式。设计这个上下文注入:

  1. 数据从哪里获取?
  2. 应该放在 context.ts 还是 context/ 目录?
  3. 应该通过 userContext 还是 systemContext 注入?
  4. 需要缓存吗?如果需要,缓存策略是什么?

本课小结

要点内容
context.ts 三函数getGitStatus(Git 快照)、getUserContext(CLAUDE.md + 日期)、getSystemContext(Git + 缓存破坏)
缓存策略全部 memoize,会话内只执行一次
computeEnvInfo组装 CWD、Git、Platform、Shell、OS、模型信息、知识截止
注入路径userContext → prependUserContext、systemContext → appendSystemContext
context/ 目录9 个 React Context:notifications、voice、mailbox、stats、overlay、modal 等
两种"上下文"context.ts(Agent 层,给 API)vs context/(UI 层,给 React 组件)
Undercover 模式Ant 内部功能,从 System Prompt 中去除所有模型信息
静态/动态边界System Prompt 分区以优化 prompt cache

下一课预告

第 10 课:Tool.ts — 工具接口设计 — 进入模块四"工具系统"。我们将完整解析 Tool<Input, Output, ProgressData> 泛型接口的 30+ 个方法与属性,理解 buildTool() 工厂函数的默认值策略,以及 ToolUseContext 这个巨大的上下文对象如何串联起工具执行的整个生命周期。工具是 Agent 的手和脚 —— 理解了工具接口,你就理解了 Claude Code 的能力边界。