System Prompt 三层组装与 CLAUDE.md 四级覆盖——上下文管理的最后一公里
System Prompt 三层组装与 CLAUDE.md 四级覆盖——上下文管理的最后一公里
《Claude Code 架构解密》精读笔记 · 第12篇
覆盖章节:第7章后半(7.5-7.8, p.184-198)
主题:系统 Prompt 组装管线、CLAUDE.md 覆盖链、上下文管理设计模式
导语:Prompt 不是一段文本,而是一条管线
上一篇文章,我们拆解了 Claude Code 面对上下文窗口极限的四层压缩策略——从零成本的 API 原生压缩到 LLM 驱动的 Full Compact。但压缩只是"对话历史"这端的问题,上下文管理还有另一端:每次 API 调用时注入的系统 Prompt。
你可能会想:系统 Prompt 不就是一段固定的文本吗?有什么好讲的?
在 Claude Code 的架构中,系统 Prompt 远不止"一段文本"——它是一条三层组装管线,精确映射 API 缓存粒度;它的第三层是一个四级覆盖链,从企业到个人层层叠加;当多层指令冲突时,仲裁者不是代码,而是 LLM 本身。
这不是"Prompt Engineering",这是 Prompt Architecture。
一、架构定位:三层管线在上下文管理中的位置
┌─────────────────────────────────────────────────────────┐
│ 上下文管理全景 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ 7.2-7.4 │ │ 7.5 │ │ 7.6 │ │
│ │ 四层压缩策略 │ │ System Prompt│ │ CLAUDE.md │ │
│ │ (对话历史端) │ │ 三层组装管线 │ │ 四级覆盖链 │ │
│ │ │ │ (行为指令端) │ │ (用户定制) │ │
│ └──────────────┘ └──────────────┘ └───────────┘ │
│ ↓ ↓ ↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 7.7 设计模式提炼 │ │
│ │ 后台Agent记忆 | 内容引用分离 | 缓存感知分层 | │ │
│ │ 覆盖链配置 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
三层管线解决的核心问题:如何在保持缓存命中率的同时,注入会话级动态内容和用户级定制指令?
二、核心点拆解
2.1 三层组装管线——缓存优化驱动的分层架构
buildEffectiveSystemPrompt() 是组装管线的入口,接收默认 Prompt 数组和用户指令层,输出品牌类型 SystemPrompt。
三层划分不是随意的,而是精确映射 Anthropic API 的缓存粒度:
| 层级 | 变更频率 | 缓存策略 | 对应 API 行为 |
|---|---|---|---|
| 静态层 | 几乎不变(版本更新才变) | cacheScope: 'global',跨组织缓存 |
Prompt Cache 前缀命中 |
| 动态层 | 会话内稳定 | 会话级缓存,少数每轮重算 | 同会话内缓存命中 |
| 用户指令层 | 按用户/项目不同 | 无缓存(完全个性化) | 每次全量传输 |
第 1 层:静态内容——不变的行为准则
静态层是整个 Prompt 的"基岩",由 7 个有序章节组成:
| 序号 | 内容概要 | 架构意图 |
|---|---|---|
| 身份与安全 | 身份定义 + 网络安全指令 | 划定身份边界和安全底线 |
| 系统环境 | 工具执行、输出格式、自动压缩说明 | 告知模型自身运行环境的约束 |
| 任务执行 | 编码风格、用户协助原则 | 控制代码质量和交互行为 |
| 风险操作 | 破坏性操作确认要求 | 安全架构的 Prompt 层 |
| 工具选择 | 优先使用专用工具而非 Bash | 避免"Bash 万能化"退化 |
| 格式规范 | 输出风格 | UX 一致性 |
| 输出效率 | 简洁性要求 | 降低 Token 消耗 |
每个章节标注 cacheScope: 'global',意味着对所有用户完全相同。当 Anthropic 的 Prompt Cache 命中时,这部分的 Token 计算成本为零。
Prompt 层安全 vs 代码层安全——一个值得深入的设计决策:
- 代码层安全(权限系统、沙箱、命令黑名单)是硬性约束——即使模型"想"执行
rm -rf /,代码也会拦截 - Prompt 层安全是软性引导——让模型"不想"执行危险操作,减少权限弹窗的频率
两者构成纵深防御。Prompt 中的破坏性操作列举与 BashTool 中的硬性黑名单形成互补——"软 + 硬"双层设计降低了权限弹窗频率,改善了用户体验。
静态分隔标记:第 1 层和第 2 层之间用 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 分隔。这个标记被 API 调用层的 splitSysPromptPrefix 函数使用,将 Prompt 分割为 cacheable prefix 和 non-cacheable suffix,直接映射到 cache_control 字段。
第 2 层:动态章节——注册表与缓存策略
动态章节层是三层管线中最精巧的设计。它采用注册表模式——每个章节通过工厂函数注册,运行时按名称解析:
type SystemPromptSection = {
name: string
cacheBreak: boolean // 是否每轮重算
}
两个工厂函数的区别仅在于 cacheBreak 字段:
| 工厂函数 | cacheBreak | 适用场景 |
|---|---|---|
systemPromptSection(name, compute) |
false | 会话内稳定的内容,如技能列表、MCP 指令 |
DANGEROUS_uncachedSystemPromptSection(name, compute, reason) |
true | 每轮都可能变化的内容,如当前时间 |
DANGEROUS_ 前缀的治理智慧——这不是危言耸听,而是一个刻意的架构治理模式:
- 承认成本:DANGEROUS_ 前缀让代码审查者立即意识到这个章节会破坏缓存
- 文档化理由:
_reason参数虽然运行时不使用,但强制在代码中留下"为什么需要每轮重算"的理由
这是 Claude Code 中反复出现的设计哲学:通过 API 设计使"错误的事情难以做到"。如果一个章节不需要每轮重算,没有开发者会主动选用带 DANGEROUS_ 前缀的函数——从而保护了 Prompt Cache 的命中率。
并行解析流程:所有动态章节通过 Promise.all 并行计算,前提是各章节之间没有数据依赖——环境信息、技能列表、MCP 指令都是独立获取的。“约束产生效率”:正是因为章节间无依赖的设计约束,才使得并行解析成为可能。
缓存失效的四个触发点:
| 触发场景 | 原因 |
|---|---|
| /clear 命令 | 用户主动重置会话 |
| /compact 命令 | 压缩后 Prompt 需要反映新状态 |
| 进入/退出 Worktree | CWD 变化导致环境信息过时 |
| 会话恢复 | 恢复时需重建完整状态 |
一个细节:清理缓存时会同步调用 clearBetaHeaderLatches()——确保功能开关在新上下文中重新评估,避免 Prompt 内容和 API 请求头之间的不一致。这种"关联失效"体现了系统一致性的严谨追求。
动态章节清单:
| 章节 | 缓存策略 | 内容示例 |
|---|---|---|
| 会话指导 | 会话内缓存 | 按会话类型注入的特定行为指导 |
| 记忆 | 会话内缓存 | memdir 加载的自动记忆文件 |
| 环境信息 | 会话内缓存 | CWD、Git 状态、平台、Shell、模型 |
| 语言 | 会话内缓存 | 用户语言偏好 |
| 输出样式 | 会话内缓存 | 输出风格配置 |
| MCP 指令 | 会话内缓存 | MCP 服务器注入的行为指令 |
| 暂存区 | 会话内缓存 | 暂存目录使用说明 |
| Token 预算 | 会话内缓存 | Token 预算说明(FeatureGate 控制) |
第 3 层:用户指令层(CLAUDE.md)
第三层是整个管线中最贴近用户的部分,核心载体是 CLAUDE.md 文件——一种 Markdown 格式的配置文件,用户通过自然语言为 Agent 设定行为准则。详见下文 2.2 节。
2.2 CLAUDE.md 四级覆盖链——从企业到个人的配置层叠
为什么需要多级配置?
一个场景:一家企业有 100 名开发者在使用 Claude Code。企业安全团队希望所有人的 Agent 都遵守"不得将代码推送到公共仓库"的规则;每个开发者又有自己的编码风格偏好;某个项目组有特定的代码规范;而某个开发者正在本地实验新的 lint 规则,不希望影响团队。
四级加载层级(优先级低→高):
/etc/claude-code/CLAUDE.md ← Managed(企业级,管理员设定)
~/.claude/CLAUDE.md ← User(个人全局偏好)
<repo>/CLAUDE.md ← Project(项目级规范)
<repo>/.claude/CLAUDE.md ← Project(隐藏目录,等效)
<repo>/.claude/rules/*.md ← Project Rules(条件规则)
<repo>/CLAUDE.local.md ← Local(本地私有,不入 Git)
这是经典的**覆盖链(Override Chain)**设计模式,与 CSS 的层叠规则异曲同工。
| 层级 | 存储位置 | 版本控制 | 典型内容 |
|---|---|---|---|
| Managed | /etc/claude-code/ | 由 MDM 工具分发 | 企业安全策略、合规要求 |
| User | ~/.claude/ | 不入版本控制 | 个人编码风格、语言偏好 |
| Project | 项目根目录 | 入 Git,团队共享 | 项目代码规范、架构约定 |
| Local | 项目根目录 | .gitignore 排除 | 本地实验性配置 |
条件规则——上下文感知的指令注入
在 .claude/rules/ 目录下,每个 Markdown 文件可以通过 YAML Frontmatter 声明自己适用的文件模式:
paths:
- "src/components/**/*.tsx"
- "src/hooks/**/*.ts"
React 组件必须使用函数组件,不使用 class 组件。
所有 hooks 必须以 use 前缀命名。
状态管理优先使用 useReducer 而非 useState(复杂场景)。
条件规则的必要性:前端规范和后端代码完全不同。如果所有规则无条件注入 Prompt,会产生两个问题:
- 窗口浪费:不相关的规则占用了宝贵的上下文窗口空间
- 遵循度下降:模型面对大量不相关指令时,对关键指令的遵循度会降低
条件规则让 Prompt 只包含与当前任务相关的指令——这与 7.2 节"按需保留"压缩思想一脉相承。
@include 引用机制与安全防护
记忆文件支持 @path 语法引用其他文件,实现配置的模块化组织:
# 项目级 CLAUDE.md
@./coding-standards.md
@./api-conventions.md
@~/personal-preferences.md
引用机制引入了潜在安全风险——循环引用和路径遍历。Claude Code 为此设计了三层防护:
- 循环引用防护:
processedPathsSet 追踪已处理路径,同一文件不会被处理两次 - 深度限制:
MAX_INCLUDE_DEPTH = 5,防止深层嵌套导致的栈溢出 - 符号链接处理:
safeResolvePath先解析符号链接到真实路径,同时在 processedPaths 中记录原始路径和解析后路径——防止通过符号链接绕过循环检测
内容处理管线
从磁盘文件到最终注入 Prompt 的内容,经过一条完整的处理管线:
读取文件 → 检查扩展名白名单(100+种) → 解析 YAML Frontmatter →
剥离 HTML 注释(marked Lexer) → 提取 @include 路径 →
对 AutoMem/TeamMem 应用截断(40000字符) → 递归处理 @include 引用
两个值得关注的细节:
- 剥离 HTML 注释:使用 marked Lexer 而非简单的正则替换,因为 HTML 注释可能出现在代码块内(应保留)或正文中(应剥离)
- 截断信任边界:
MAX_MEMORY_CHARACTER_COUNT = 40000只应用于自动记忆(AutoMem)和团队记忆(TeamMem),不限制用户手动编写的 CLAUDE.md。用户手写内容假定经过深思熟虑,而自动生成内容可能无限膨胀
覆盖语义与冲突仲裁——全部注入,模型仲裁
如果 Managed 层说"禁止使用 eval()“,而 Project 层说"在测试代码中允许使用 eval()”,最终谁生效?
Claude Code 的回答是:全部注入,由模型仲裁。
四级配置文件的内容全部被注入 Prompt,以标注来源层级的方式并列呈现。模型会看到两条矛盾的指令,并根据上下文(当前正在编辑的是测试文件还是生产代码)做出判断。
为什么不在代码层做硬性优先级覆盖?两个原因:
- 自然语言规则难以机器比较——"禁止使用 eval()"和"在测试中允许 eval()"不是简单的键值对,无法用程序判断它们是否冲突
- LLM 的上下文理解能力恰好适合处理模糊的优先级仲裁——它可以根据当前文件类型、操作意图综合判断
当然,"软仲裁"也有风险——Claude Code 的应对是在注入时标注层级来源,通过描述性标注影响模型对冲突指令的优先级判断。这是 Prompt 工程的一个精妙应用:通过描述性标注影响模型对冲突指令的优先级判断。
2.3 六级优先级仲裁器
三层管线组装完毕后,还需要面对 SDK 嵌入方、Agent 定义、用户参数等多方指令冲突。buildEffectiveSystemPrompt 中实现了一个六级优先级仲裁器:
| 级别 | 典型场景 | 设计意图 |
|---|---|---|
| Override | SDK 嵌入/自动化 | 完全控制模型行为,忽略所有默认规则 |
| Coordinator | 多 Agent 协调模式 | 协调器需要与执行者不同的行为模式 |
| Agent (proactive) | 自主 Agent 模式 | 保留默认安全规则,在其上叠加 Agent 指令 |
| Agent (normal) | 普通 Agent 任务 | Agent 完全定义自己的行为 |
| Custom | CLI --system-prompt | 开发者实验用 |
| Default + Append | 正常交互 | 标准行为 + 用户追加指令 |
关键安全决策:Proactive 模式下,Agent Prompt 是追加而非替换默认 Prompt。为什么?默认 Prompt 包含安全准则(如破坏性操作确认要求),如果替换了,Agent 可能在没有用户确认的情况下执行破坏性操作。
延迟加载与条件编译:通过 feature() + require() 实现条件加载——feature() 在构建时由 Bun bundler 评估,如果条件为 false 则整个 require 被消除(dead code elimination)。这避免了加载未启用功能的代码,减小 bundle 体积,同时解决了某些模块间的循环依赖问题。
2.4 上下文窗口约束——Prompt 的物理极限
系统 Prompt 不是可以无限增长的——它受上下文窗口大小的约束。context.ts 负责管理这一物理极限。
窗口大小的决策链:
环境变量覆盖 (仅内部用户)
→ 模型名[1m]后缀检测 → 1M tokens
→ getModelCapability() 能力查询
→ Beta Header 检查
→ 实验分组检查
→ 默认值:200,000 tokens
输出 Token 的精巧优化:
export const CAPPED_DEFAULT_MAX_TOKENS = 8_000 // 默认上限
export const ESCALATED_MAX_TOKENS = 64_000 // 达到上限后重试的值
根据实际数据,p99 的输出约 4,911 tokens,而默认 max_tokens 如果设为 32K/64K 会过度预留 8-16 倍容量。Claude Code 的策略是"先小后大"——默认 8K,只有当模型实际触达上限时才提升到 64K 重试。99%+ 的请求因此节省了大量预留空间,为对话历史腾出更多上下文窗口。
三、设计模式提炼
纵观第 7 章的压缩策略和系统 Prompt 工程,可以提炼出四个核心设计模式:
模式一:后台 Agent 驱动的会话记忆
问题:长对话压缩中 LLM 摘要不可避免地丢失结构化任务状态。
核心思想:Fork 一个专用子 Agent,在后台周期性提取结构化的会话记忆。
主对话流 (REPL)
↓ postSamplingHook
↓ extractSessionMemory()
↓ 双阈值判断 (tokens > 阈值 AND 工具调用 > 阈值)
↓ createSubagentContext()
↓ setupSessionMemoryFile()
↓ runForkedAgent() — 只允许 FileEditTool,只能编辑 summary.md
↓ 完成
设计要点:
- 双阈值触发(token 10000 + 工具调用 3 次),避免闲聊时浪费资源
- 工具权限隔离:后台 Agent 只被授权 FileEditTool + 只能编辑指定文件
sequential()并发控制 + Compact 前等待提取完成(15 秒超时)- 9 章节结构化模板,总限制 12000 tokens
与 Compact 的协同:Session Memory 优先于传统 Compact 被触发。当 Session Memory 存在时,Compact 操作会使用会话记忆内容作为摘要(而非重新调用 LLM 生成),实现**"零 LLM 成本"的 Compact**。
模式二:内容引用分离
问题:大段代码/日志内联存储导致历史文件膨胀到数百 MB。
核心思想:基于大小阈值的内联-引用分离——小内容直接存储,大内容用 SHA256 哈希引用,指向独立的缓存文件。
// 写入时分流
if (content.length <= 1024) {
stored = { id, type: 'text', content } // 小内容内联
} else {
const hash = sha256(content).slice(0, 16) // 16字符截断
await storePastedText(hash, content) // 存到 paste-cache/
stored = { id, type: 'text', contentHash: hash } // 只存引用
}
设计要点:
- 阈值 1024 字节:太小导致碎片化,太大导致历史膨胀——1KB 是经验平衡点
- SHA256 取前 16 字符(64bit),冲突概率在单用户场景下可忽略
- 倒序替换:展开引用时从后往前处理,避免前面的替换改变后面引用的字符偏移量
- 懒加载语义:历史文件记录引用而非内容,只在实际需要还原时才从 paste-cache 读取
模式三:缓存感知 Prompt 分层
问题:Prompt 包含不同变更频率的内容,混在一起无法利用 API 缓存。
核心思想:按变更频率分层,精确映射缓存策略。
关键技术:三层管线 + DANGEROUS_API 治理 + cache_control 映射 + SYSTEM_PROMPT_DYNAMIC_BOUNDARY 分隔标记。
模式四:覆盖链配置
问题:多级配置需要灵活覆盖,但自然语言规则难以机器比较。
核心思想:多级文件按优先级叠加,模型仲裁冲突。
关键技术:四级层级 + 条件规则(glob 模式)+ @include 引用 + LLM 冲突仲裁。
模式速查表
| 模式 | 问题 | 核心思想 | 关键技术 |
|---|---|---|---|
| 后台 Agent 记忆 | 长对话压缩丢失结构 | Fork 子 Agent 后台提取 | 双阈值触发、工具权限隔离、sequential 并发控制 |
| 内容引用分离 | 大内容内联导致膨胀 | 小内联、大哈希引用 | SHA256 截断、倒序替换、懒加载展开 |
| 缓存感知分层 | Prompt 含不同频率内容 | 按频率分层映射缓存 | 三层管线、DANGEROUS_API 治理、cache_control |
| 覆盖链配置 | 多级配置灵活覆盖 | 多级叠加+模型仲裁 | 四级层级、条件规则、@include 引用 |
四、横向对比
System Prompt 组装 vs 其他 Agent 框架
| 维度 | Claude Code | LangChain | OpenAI Assistants |
|---|---|---|---|
| Prompt 来源 | 三层管线(静态+动态+用户) | 代码级拼接 | 单一 instructions 字段 |
| 缓存感知 | 精确映射 API 缓存粒度 | 无 | API 级自动缓存 |
| 动态章节 | 注册表模式,并行计算 | 代码级条件渲染 | 无动态概念 |
| 用户定制 | 四级覆盖链 + 条件规则 | 无内置机制 | 无内置机制 |
| 冲突仲裁 | LLM 软仲裁 + 层级标注 | 代码级覆盖 | 最后写入者胜出 |
| 治理模式 | DANGEROUS_ API 命名 | 无 | 无 |
CLAUDE.md 四级覆盖 vs 其他配置体系
| 维度 | CLAUDE.md | ESLint 配置 | Git 配置 |
|---|---|---|---|
| 层级数 | 4 级(Managed/User/Project/Local) | 3 级(Home/Project/Override) | 3 级(System/Global/Local) |
| 条件规则 | glob 模式 + YAML Frontmatter | overrides 字段 | 无 |
| 冲突处理 | 全部注入 + LLM 仲裁 | 靠后优先覆盖 | 靠后优先覆盖 |
| 引用机制 | @include + 三层安全防护 | extends + plugins | includeIf(条件包含) |
| 企业管控 | Managed 层 + MDM 分发 | 共享配置包 | 系统级模板 |
核心差异:CLAUDE.md 的"全部注入 + LLM 仲裁"是独一无二的——其他配置体系都用代码级覆盖,而 Claude Code 把模糊冲突交给最擅长理解自然语言的 LLM。
五、实战启示
启示 1:缓存感知是 Prompt 架构的第一性原理
Claude Code 的三层管线不是"为了好看"而分层——每一层都精确映射 API 的缓存粒度。如果你在设计 Agent 系统的 Prompt 体系,第一件事不是写 Prompt 内容,而是搞清楚你的 API 提供商的缓存边界在哪里。
把不变的内容放前面(全局缓存),会话级变化的内容放中间(会话缓存),用户个性化内容放后面(无缓存)——这个"变更频率递增"的排列顺序就是缓存感知的核心。
启示 2:API 命名是最廉价的治理工具
DANGEROUS_ 前缀不需要任何运行时成本,却能在代码审查时立刻引发警觉。在你的系统中,如果某个操作有高成本或高风险,让它"看起来"危险比写文档更有效。
类似的模式:React 的 dangerouslySetInnerHTML、Rust 的 unsafe 块——都在用命名传递意图。
启示 3:覆盖链的关键不是"谁覆盖谁",而是"冲突怎么处理"
四级覆盖链的创新不在层级本身(CSS/Git/ESLint 都有多级配置),而在于冲突仲裁策略:
- 代码级覆盖(靠后优先)适合结构化配置,但对自然语言规则无能为力
- LLM 仲裁适合模糊冲突,但不是确定性的——需要层级标注辅助
实践建议:对于安全关键规则,在代码层做硬性执行(如权限系统的 deny 规则);对于风格偏好等软性规则,交给 LLM 仲裁——硬约束代码化,软约束 Prompt 化。
启示 4:输出 Token 的"先小后大"策略
默认 8K、触达后升级 64K——这不是一个简单的参数选择,而是对99% vs 1% 场景的精确建模。p99 输出 4,911 tokens 意味着 32K/64K 的默认值是在为 1% 的极端场景浪费 8-16 倍预留空间。
通用原则:为常见场景优化默认值,为极端场景准备升级路径。过度预留是上下文窗口的隐形杀手。
六、第7章全章总结:上下文管理的核心智慧
回望整个第7章,从四层压缩策略到三层 Prompt 管线到四级覆盖链,一条主线贯穿始终:
分层 + 渐进——不试图用一个万能方案解决所有问题,而是按照不同维度(成本、变更频率、信任层级)将问题分解,每层用最适合的策略处理。
- 压缩策略:零成本 → 零 LLM → LLM 驱动,渐进升级
- Prompt 管线:全局缓存 → 会话缓存 → 无缓存,变更频率递增
- 配置覆盖:企业 → 个人 → 项目 → 本地,信任层级递减
四者协同:压缩解决"对话太长",Session Memory 在压缩中保留结构化知识,Prompt 管线确保每次调用携带正确行为指令,CLAUDE.md 覆盖链让用户精细控制 Agent 行为——让一个有限窗口的 LLM 能够支撑无限长度的工作会话。
下期预告
第 13 篇,我们将进入第8章——安全纵深防御,这是 Claude Code 安全架构的集大成之作:六层防御模型总览、OS 级沙箱、命令注入防护、路径遍历防护。当 AI 拥有了执行能力,安全就不再是一个功能,而是一个体系。
第13篇预告:沙箱里的自由——Claude Code 六层纵深防御体系全解
本篇为《Claude Code 架构解密》精读笔记第12篇,覆盖第7章后半(7.5-7.8)。
上一篇:[第11篇] Context 的生死抉择——四层压缩、截断算法与 Session Memory
系列持续更新中,关注获取更多精读内容。
更多推荐

所有评论(0)