模块三:Agent 核心循环 | 前置依赖:第 08 课 | 预计学习时间:60 分钟
学习目标
完成本课后,你将能够:
- 说明
context.ts中三个核心函数(getGitStatus、getUserContext、getSystemContext)的作用与缓存策略 - 描述
computeEnvInfo()如何组装环境信息并注入 System Prompt - 列举
context/目录下 9 个 React Context 的职责 - 画出上下文信息从收集到注入 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 专用) │
├─────────────────────────────────────────┤
│ 消息历史 │
│ ├── 历史对话 │
│ └── 工具结果 │
└─────────────────────────────────────────┘
上下文构建分两层:
- context.ts —— 收集原始数据(Git、CLAUDE.md、环境)
- 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')
})
设计要点:
- 5 个 git 命令并行执行 ——
Promise.all避免串行等待 --no-optional-locks—— 防止在大仓库中意外获取锁- 状态截断到 2000 字符 —— 防止超大 monorepo 的 status 撑爆上下文
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.md
→ filterInjectedMemoryFiles() // 过滤已注入的记忆文件
→ 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 中的 computeEnvInfo 和 computeSimpleEnvInfo 负责组装环境描述:
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 | 影响路径格式、命令选择 |
| Shell | process.env.SHELL | 影响 Bash 命令语法 |
| OS 版本 | os.type() + os.release() | 环境诊断 |
| 模型信息 | getMarketingNameForModel() | Claude 自我认知 |
| 知识截止 | getKnowledgeCutoff() | Claude 知道自己的知识边界 |
| Worktree | getCurrentWorktreeSession() | 指示在隔离环境中 |
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.ts | context/ 目录 | |
|---|---|---|
| 层级 | Agent 循环层 | UI 层 |
| 用途 | 收集数据给 Claude API | 管理 UI 状态 |
| 技术 | 普通函数 + memoize | React Context + Provider |
| 消费者 | query.ts → callModel | React 组件 |
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 而不是每次重新计算?
- 性能 —— Git 命令耗时几百毫秒,不应每轮重复
- 一致性 —— 会话内上下文不变,避免 Claude 看到不一致的信息
- 语义正确 —— 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 调用的完整数据流:
.claude/settings.json(用户设置)CLAUDE.md(项目规则)git status(Git 状态)os.platform()(操作系统信息)
标注每个数据经过哪些函数、存储在哪里、最终注入到 API 请求的什么位置。
练习 2:缓存策略分析
getUserContext 和 getSystemContext 都使用 memoize 缓存。思考:
- 如果用户在会话中途修改了 CLAUDE.md,Claude 会看到更新吗?
- 如果 Git 分支在会话中途切换了,Claude 会知道吗?
- 有没有场景下缓存会导致问题?
- 如果让你改进这个缓存策略,你会怎么做?
练习 3:React Context vs Agent Context
对比 context/ 目录下的 React Context 和 context.ts 中的上下文构建:
- 它们各自服务于什么消费者?
- 如果 Voice Context 需要影响 Claude 的行为(比如"用户正在语音输入,请等待"),信息需要经过什么路径?
- Notification Context 和 SystemMessage 有什么关系?
练习 4:设计新上下文
假设 Claude Code 需要感知"当前终端窗口大小"来优化输出格式。设计这个上下文注入:
- 数据从哪里获取?
- 应该放在
context.ts还是context/目录? - 应该通过 userContext 还是 systemContext 注入?
- 需要缓存吗?如果需要,缓存策略是什么?
本课小结
| 要点 | 内容 |
|---|---|
| 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 的能力边界。