Claude Code 源码分析第 39 课 · 第 01
第 39 课

系统提示词

Claude Code 如何拼装系统提示词:静态前缀、动态尾部、CLAUDE.md 注入、缓存边界与不同模式下的覆盖逻辑。

01 概览

在模型处理第一条消息之前,Claude Code 会先拼装一份往往长达数千 token 的系统提示。它不是写死在二进制里的单段文本,而是按会话实时组装出来的:有些部分是全局稳定前缀,有些部分取决于当前目录、工具集、CLAUDE.md、MCP 连接和运行模式。

覆盖源文件
constants/prompts.tsutils/systemPrompt.tsconstants/systemPromptSections.tsutils/claudemd.tsmemdir/memdir.ts

真正重要的不是“提示里写了什么”,而是“这些内容按什么优先级进入、哪些可以缓存、哪些必须每轮重算”。这节课要看清的,就是这条装配链:优先级解析、内容工厂、动态分段、CLAUDE.md 注入与缓存边界。

第 1 层

优先级解析器

systemPrompt.ts — 覆盖 → 协调器 → 代理 → 自定义 → 默认

第 2 层

内容工厂

prompts.ts — 每个会话组装的静态+动态部分

第 3 层

部门登记处

systemPromptSections.ts — 记忆化 vs. 易失性段缓存

第 4 层

克劳德.md 加载器

claudemd.ts - 多范围文件发现,@include,frontmatter

第 5 层

记忆系统

memdir/memdir.ts — MEMORY.md + 自动内存注入

第 6 层

缓存边界

SYSTEM_PROMPT_DYNAMIC_BOUNDARY — 全局范围前缀分割

02 优先级解析器 — 哪个提示符会运行?

buildEffectiveSystemPrompt() in utils/systemPrompt.ts 是第一道门。 它实现了严格的优先级瀑布。 一旦找到更高优先级的源,较低层将被完全跳过。

flowchart TD A["buildEffectiveSystemPrompt()"] --> B{overrideSystemPrompt?} B -->|"Yes (loop mode)"| OV["Return [overrideSystemPrompt]\nAll other sources ignored"] B -->|"No"| C{COORDINATOR_MODE\nenv + feature flag?} C -->|"Yes"| CO["Return [getCoordinatorSystemPrompt()\n+ appendSystemPrompt]"] C -->|"No"| D{mainThreadAgentDefinition\nset?} D -->|"Yes + PROACTIVE active"| PA["Return [defaultSystemPrompt...\n+ Custom Agent Instructions\n+ appendSystemPrompt]"] D -->|"Yes, normal"| AG["Return [agentSystemPrompt\n+ appendSystemPrompt]"] D -->|"No"| E{customSystemPrompt\nset via --system-prompt?} E -->|"Yes"| CS["Return [customSystemPrompt\n+ appendSystemPrompt]"] E -->|"No"| DF["Return [defaultSystemPrompt...\n+ appendSystemPrompt]"] style OV fill:#2a1a1a,stroke:#c47a50,color:#b8b0a4 style DF fill:#1a3a1a,stroke:#6e9468,color:#b8b0a4 style CO fill:#1a2a3a,stroke:#7d9ab8,color:#b8b0a4 style PA fill:#2a1a3a,stroke:#8e82ad,color:#b8b0a4
关键见解
appendSystemPrompt 是唯一总是附加的东西——它被添加到每个分支中,除非 overrideSystemPrompt 是活跃的。 就是这样 --append-system-prompt 无论您处于哪种模式,都可以正常工作。

在主动/KAIROS 模式下,代理的指令是 appended 到默认提示而不是替换它。 这反映了“队友”模式:主动默认已经是精简的自主代理身份,并且域代理添加在顶部。

// utils/systemPrompt.ts — proactive mode branch
if (agentSystemPrompt && (feature('PROACTIVE') || feature('KAIROS')) && isProactiveActive()) {
  return asSystemPrompt([
    ...defaultSystemPrompt,
    `\n# Custom Agent Instructions\n${agentSystemPrompt}`,
    ...(appendSystemPrompt ? [appendSystemPrompt] : []),
  ])
}
03 内容工厂 — getSystemPrompt()

getSystemPrompt() in constants/prompts.ts 是主要的内容工厂。 它构建了 defaultSystemPrompt 馈入优先级解析器的数组。 该函数有一个快速逃生口:if CLAUDE_CODE_SIMPLE=1 它返回一个三行存根并立即退出。

对于正常会话,它并行收集四个运行时状态,然后组装两半 - 一个 静态半 和一个 动态半 — 由控制提示缓存范围的边界标记分隔。

// constants/prompts.ts — abbreviated assembly
export async function getSystemPrompt(tools, model, additionalDirs, mcpClients) {
  const [skillToolCommands, outputStyleConfig, envInfo] = await Promise.all([
    getSkillToolCommands(cwd),
    getOutputStyleConfig(),
    computeSimpleEnvInfo(model, additionalDirs),
  ])

  return [
    // ── Static (globally cacheable) ──
    getSimpleIntroSection(outputStyleConfig),
    getSimpleSystemSection(),
    getSimpleDoingTasksSection(),      // code-style, YAGNI, security rules
    getActionsSection(),               // reversibility / blast-radius guidance
    getUsingYourToolsSection(enabledTools),
    getSimpleToneAndStyleSection(),
    getOutputEfficiencySection(),
    // ── Boundary marker ──
    SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
    // ── Dynamic (session-specific, registry-managed) ──
    ...resolvedDynamicSections,        // session_guidance, memory, env_info_simple, ...
  ].filter(s => s !== null)
}

静态部分——它们包含什么

部分功能 它灌输了什么 Scope
getSimpleIntroSection() 身份(“软件工程任务的交互式代理”)、URL 生成防护、CYBER_RISK_INSTRUCTION static
getSimpleSystemSection() Markdown渲染、权限模式工具审批、系统提醒标签、提示注入警告、hooks引导、自动压缩通知 static
getSimpleDoingTasksSection() 任务范围(“软件工程任务”)、YAGNI/无镀金规则、无推测性抽象、安全卫生、代码风格项目符号(仅限 ant 的额外功能) static
getActionsSection() 可逆性和爆炸半径指导——何时暂停和确认、危险行为分类(破坏性、难以逆转、他人可见) static
getUsingYourToolsSection() 优先使用专用工具而不是 Bash、REPL 模式路径、并行工具调用、任务跟踪工具 static
getSimpleToneAndStyleSection() 没有表情符号、文件:行引用、GitHub 问题格式、工具调用前没有冒号 static
getOutputEfficiencySection() 简洁/散文质量规则——ant build 获得丰富的散文指南,external 获得简短的“开门见山” static
仅 ANT 分支
几个部分——评论写作规范、虚假声明缓解、自信平衡、数字长度锚点——都受到限制 process.env.USER_TYPE === 'ant'。 这些是通过 Bun 死代码消除从外部二进制文件编译出来的。 每一个 // @[MODEL LAUNCH] 注释标记了在每个新模型发布时需要更新的说明。
04 动态部分——注册表

界碑之后的一切都通过 部门登记处 定义于 constants/systemPromptSections.ts。 每个部分要么被记忆(每个会话计算一次,缓存直到 /clear or /compact)或易失性(每轮重新计算)。

// systemPromptSections.ts
export function systemPromptSection(name, compute): SystemPromptSection {
  return { name, compute, cacheBreak: false }   // memoized
}

export function DANGEROUS_uncachedSystemPromptSection(name, compute, reason) {
  return { name, compute, cacheBreak: true }    // recomputes every turn
}

resolveSystemPromptSections() 迭代注册表,返回非cacheBreak部分的缓存值,并调用 compute() 对于易失性或未缓存的。 缓存条目由该部分的键控 name 字符串并存储在 bootstrap/state.ts 所以 React 渲染周期不会丢失它们。

栏目名称 Content 缓存行为
session_guidance 询问用户问题指导、交互式 shell 提示、代理工具指导、技能调用语法、验证代理合约 memoized
memory CLAUDE.md 层次结构 + MEMORY.md 自动记忆(参见§5) memoized
ant_model_override 从远程配置注入的内部 ant 构建后缀 memoized
env_info_simple CWD、git 状态、平台、shell、操作系统版本、型号名称、知识截止、型号系列参考 memoized
language 设置中的语言首选项→“始终使用 X 进行响应” memoized
output_style 从设置加载的命名输出样式(名称+提示字符串) memoized
mcp_instructions Per-server instructions 来自连接的 MCP 服务器的块 volatile — MCP 服务器在轮次之间连接/断开
scratchpad 每会话暂存器目录路径和使用规则 memoized
frc 函数结果清除通知(CACHED_MICROCOMPACT 功能) memoized
token_budget “继续努力,直到接近目标”指令 memoized
为什么 mcp_instructions 是 DANGEROUS_uncached
MCP 服务器可以在会话中连接和断开连接。 如果指令部分被记忆,那么在第一轮之后连接的服务器将永远不会将其指令放入上下文中。 使其易失性可确保其始终保持最新状态,但代价是可能会破坏服务器连接/断开事件上的提示缓存。
05 缓存边界——如何保持较低的代币成本

Claude 的 API 支持即时缓存:相同的前缀按正常输入令牌成本的一小部分进行计费。 要利用这一点,系统提示符必须分为 稳定前缀 (所有用户都相同)和 挥发性尾部 (每个会话的内容)。

// constants/prompts.ts
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'

// WARNING: Do not remove or reorder this marker without updating cache logic in:
// - src/utils/api.ts (splitSysPromptPrefix)
// - src/services/api/claude.ts (buildSystemPromptBlocks)

Everything before 标记可以使用 scope: 'global' 在 API 调用中——可在所有组织中缓存。 一切 after 包含特定于会话的内容(您的 CWD、git 状态、CLAUDE.md 文件、模型 ID),并且不得全局缓存。

block-beta columns 1 block:static["Static Prefix (scope: global)"] A["Intro · System · Doing Tasks · Actions · Tools · Tone · Output Efficiency"] end BOUNDARY["━━━ __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ ━━━"] block:dynamic["Dynamic Tail (scope: session)"] B["Session Guidance · Memory/CLAUDE.md · Env Info · Language · Output Style · MCP Instructions · Scratchpad · FRC · Token Budget"] end style BOUNDARY fill:#c47a50,color:#fff,font-weight:bold style static fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style dynamic fill:#1a2a1a,stroke:#6e9468,color:#b8b0a4
工程约束
放置的任何运行时条件表达式 before 边界将每个条件的不同缓存前缀哈希的数量乘以 2。 工程师明确地将特定于会话的标志(非交互模式、fork-subagent 模式)移至 getSessionSpecificGuidanceSection ——它生活在边界之后——正是因为这个原因。 对于同一错误类别,评论引用“PR #24490、#24171”。
06 CLAUDE.md 注入——用户指令如何输入

The memory 部分是加载用户放置在其文件系统中的每个 CLAUDE.md 文件的位置。 utils/claudemd.ts 跨四个范围发现文件并对其进行排名,按优先级顺序加载它们(优先级较低的首先,优先级较高的最后 - 因此模型会更多地关注它最后看到的文件)。

// utils/claudemd.ts — load order comment
// 1. Managed memory (/etc/claude-code/CLAUDE.md)   — Global for all users on machine
// 2. User memory   (~/.claude/CLAUDE.md)            — Private global for all projects
// 3. Project memory (CLAUDE.md / .claude/CLAUDE.md  — Checked into codebase
//                   .claude/rules/*.md)
// 4. Local memory  (CLAUDE.local.md)                — Private project-specific
发现算法
步行找到项目和本地文件 upward 从当前目录到文件系统根目录,检查每个目录 CLAUDE.md, .claude/CLAUDE.md,以及每一个 .md 文件输入 .claude/rules/。 更接近 CWD 的文件出现在阵列中的较晚位置,从而赋予它们更高的有效优先级。

@include 指令

CLAUDE.md 文件可以引用其他文件 @path 指令放置在叶文本中(不在代码块内)。 支持的表格:

# CLAUDE.md
@shared-rules.md                    # relative path (same dir)
@./scripts/lint-conventions.md      # explicit relative
@~/company/global-standards.md      # home-relative
@/absolute/path/to/rules.md         # absolute

包含的文件插入为 之前的单独条目 包含文件。 通过处理路径集可以防止循环引用。 不存在的目标会被默默地跳过。 加载程序限制包含大量文本文件扩展名的允许列表 - 拒绝二进制文件(图像、PDF)以防止意外的上下文膨胀。

Frontmatter 路径过滤

CLAUDE.md 文件可以携带 YAML frontmatter 和 paths 钥匙。 只有这些 glob 模式下的文件才会收到指令。 这使您可以将特定于语言或特定于目录的规则放置在单个 CLAUDE.md 中,而不会污染每个项目。

---
paths:
  - src/components/**
  - "*.tsx"
---
Always use named exports in React components.

内存指令包装器

每个加载的内存文件都包含在由注入的标头中 claudemd.ts:

const MEMORY_INSTRUCTION_PROMPT =
  'Codebase and user instructions are shown below. Be sure to adhere to these ' +
  'instructions. IMPORTANT: These instructions OVERRIDE any default behavior and ' +
  'you MUST follow them exactly as written.'

这是您在自己的序言中看到的 <system-reminder> 语境。 各个文件内容由明确标记的块分隔,标识每个范围(用户内存、项目内存、本地内存),以便模型可以推断来源。

MEMORY.md 自动记忆

memdir/memdir.ts 处理位于顶部的单独的自动记忆系统。 启用后,每个会话 MEMORY.md 文件也被注入。 文件被截断为 200行 or 25,000 字节,以先触发者为准,如果被截断,则会附加警告。 这可以防止不断增长的内存文件使每个请求变得臃肿。

// memdir/memdir.ts
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000
07 环境信息——computeSimpleEnvInfo()

The env_info_simple 部分是由组装的 computeSimpleEnvInfo()。 它为模型提供了其运行时上下文的快照。

// Produced output (representative)
# Environment
You have been invoked in the following environment:
 - Primary working directory: /Users/alice/myproject
 - Is a git repository: true
 - Platform: darwin
 - Shell: zsh
 - OS Version: Darwin 25.3.0
 - You are powered by the model named Claude Sonnet 4.6. The exact model ID is claude-sonnet-4-6.
 - Assistant knowledge cutoff is August 2025.
 - The most recent Claude model family is Claude 4.5/4.6 ...
 - Claude Code is available as a CLI in the terminal, desktop app ...
 - Fast mode for Claude Code uses the same Claude Opus 4.6 model with faster output ...

Shell检测解析 process.env.SHELL 提取名称。 在 Windows 上,它附加了一个注释以首选 Unix shell 语法(/dev/null not NUL,路径中的正斜杠)。 知识截止日期根据模型规范名称进行硬编码 getKnowledgeCutoff() — 每个模型的发布都需要 // @[MODEL LAUNCH] update.

卧底模式
当 Anthropic 工程师运行内部构建时 isUndercover() 激活后,所有模型名称引用都会从环境部分中删除。 这可以防止内部模型 ID 泄露到公共提交、PR 或屏幕截图中。
08 MCP 服务器说明

当连接 MCP 服务器时, getMcpInstructions() 迭代 ConnectedMCPServer 对象并从任何暴露的对象组装一个块 instructions field.

// constants/prompts.ts
function getMcpInstructions(mcpClients) {
  const withInstructions = mcpClients
    .filter(c => c.type === 'connected')
    .filter(c => c.instructions)

  const blocks = withInstructions.map(c => `## ${c.name}\n${c.instructions}`).join('\n\n')

  return `# MCP Server Instructions\n\n...\n\n${blocks}`
}

这就是将“重要:在使用任何 chrome 浏览器工具之前,您必须首先使用 ToolSearch 加载它们”之类的文本注入到系统提示符中 - 它逐字来自 MCP 服务器的 instructions 字段,而不是来自 Claude Code 自己的代码。

还有一个实验 mcpInstructionsDelta 路径:启用该功能后,指令将作为 持续依恋 对象而不是每次都提示重新注入到系统中。 这可以避免当延迟连接的 MCP 服务器强制使用新的系统提示哈希时发生提示缓存崩溃。

09 子代理增强——enhanceSystemPromptWithEnvDetails()

子代理商推出 AgentTool 不经过 getSystemPrompt()。 他们以呼叫者通过的任何系统提示开始 - 通常 DEFAULT_AGENT_PROMPT. enhanceSystemPromptWithEnvDetails() 然后附加环境上下文和特定于代理的注释。

// The agent default prompt — what every subagent starts with
export const DEFAULT_AGENT_PROMPT = `You are an agent for Claude Code, Anthropic's official CLI for Claude. Given the user's message, you should use the tools available to complete the task. Complete the task fully—don't gold-plate, but don't leave it half-done. When you complete the task, respond with a concise report covering what was done and any key findings — the caller will relay this to the user, so it only needs the essentials.`

// Notes appended by enhanceSystemPromptWithEnvDetails()
`Notes:
- Agent threads always have their cwd reset between bash calls — use absolute file paths.
- In your final response, share file paths (always absolute, never relative) ...
- For clear communication the assistant MUST avoid using emojis.
- Do not use a colon before tool calls.`

computeEnvInfo() (完整版本,不是 computeSimpleEnvInfo) 在这里使用 - 它添加了 uname -sr 输出通过 getUnameSR() 并将环境块包装在 <env> XML 标签而不是主会话中使用的更简单的项目符号列表格式。

10 简单模式和其他逃生舱口

CLAUDE_CODE_SIMPLE=1

设置此环境变量会激活三行系统提示符 - 没有说明,没有 CLAUDE.md,没有工具指南,只有 CWD 和日期。 对于对原始模型功能进行基准测试非常有用,无需 Claude Code 的提示开销。

if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
  return [`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`]
}

主动/KAIROS 模式

When feature('PROACTIVE') or feature('KAIROS') 已启用且主动模式处于活动状态, getSystemPrompt() 返回简短的自主代理身份提示,而不是完整的交互式会话提示。 它包括内存、环境信息、MCP 指令和主动部分,但没有交互式编码任务指南。

return [
  `\nYou are an autonomous agent. Use the available tools to do useful work.\n\n${CYBER_RISK_INSTRUCTION}`,
  getSystemRemindersSection(),
  await loadMemoryPrompt(),
  envInfo,
  getLanguageSection(settings.language),
  getMcpInstructionsSection(mcpClients),
  getScratchpadInstructions(),
  getProactiveSection(),
].filter(s => s !== null)

要点

  • buildEffectiveSystemPrompt() 实现严格的优先级瀑布:覆盖 > 协调器 > 代理 > 自定义 > 默认,其中 appendSystemPrompt 总是追加。
  • 提示分为静态部分(全局缓存范围,对所有用户相同)和动态尾部分(特定于会话)。 界碑 __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ 是接缝。
  • 动态部分由命名注册表管理。 大多数内容都会在会议中被记住; 仅有的 mcp_instructions 是不稳定的,因为服务器在会话中连接。
  • CLAUDE.md 文件在四个范围内加载(托管 → 用户 → 项目 → 本地),靠近 CWD 的文件通过最后出现而具有更高的优先级。
  • The @include 指令让 CLAUDE.md 文件组成其他文件。 前题 paths 将指令门控到特定的文件模式。
  • 子代理绕过 getSystemPrompt() 完全地,从 DEFAULT_AGENT_PROMPT 然后用环境细节进行增强。
  • 存在多个逃生舱口: CLAUDE_CODE_SIMPLE=1 对于存根提示,主动模式用于自主代理身份,卧底模式用于删除所有模型名称引用。
  • Every // @[MODEL LAUNCH] 注释标记了在每个新的 Claude 模型版本中需要人工关注的代码。

知识检查

Q1. 如果设置了 overrideSystemPrompt,系统最终会采用哪条路径?
正确! overrideSystemPrompt 处在优先级瀑布的最顶层。一旦命中,它会短路整个解析流程,其他来源都不会再进入结果。
Q2. 为什么 mcp_instructions 被定义成不缓存的动态 section?
正确! MCP 连接状态是会变的。如果把它记忆化,后续新连上的服务器说明就永远进不了提示词;因此这里必须按轮次重算。
Q3.CLAUDE.md 的加载优先级里,哪个来源通常拥有最高有效优先级?
正确! 加载顺序是从全局到局部,越靠近当前工作目录、越后进入数组的内容,越能在模型上下文中形成更高的实际优先级。
Q4. __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ 的工程意义是什么?
正确! 边界前面的内容追求稳定、可跨会话共享缓存;边界后面的内容则依赖当前会话状态,必须按会话甚至按轮次变化。
Q5. 主动模式 / KAIROS 模式会如何改变系统提示?
正确! 主动模式的目标不是陪用户来回交互,而是让代理持续工作。因此它会切换到更短、更偏自治的身份提示,而不是完整的交互式会话模板。
0/5