Appearance
第8章 System Prompt 分层设计
8.1 System Prompt 不是一段字符串
很多开发者第一次接触 Agent 开发时,system prompt 是这样写的:一个字符串常量,塞在代码里,想到什么加什么,越写越长,最后变成一坨没人敢动的文本。改一个词,三个功能崩了。加一条规则,和前面的指令冲突了。
这是 prompt 的泥球架构——和代码世界里的 Big Ball of Mud 如出一辙。
真实的生产级 Agent 系统不会这样做。如果你去读 Claude Code 的源码,会发现它的 system prompt 是一个精心设计的多层架构,由十几个模块在运行时组装而成。每个模块有明确的职责边界,静态内容和动态内容严格分离,用户自定义和系统默认互不干扰。
System prompt 是一个架构问题,不是一个文案问题。
本章的目标,就是把这个架构拆清楚。
8.2 分层模型:从人格到动态规则
一个设计良好的 system prompt 可以抽象为五个层次,从底层到顶层依次是:
第一层:Base Personality(基础人格层)
这是 Agent 最核心的身份定义。它回答一个问题:你是谁?包括名字、角色定位、基本行为准则、沟通风格。这一层极少变化,可能整个产品生命周期只改动几次。
你是 Claude,由 Anthropic 开发的 AI 助手。
你诚实、有帮助、无害。
当你不确定时,你会明确说明。看起来平平无奇,但这一层承担的是"锚定"功能。后续所有层次的指令都建立在这个基础之上。如果基础人格层定义了"你要诚实",后面的角色指令就不应该让 Agent 编造信息。
第二层:Role Instructions(角色指令层)
在基础人格之上,根据具体应用场景定义 Agent 的专业角色。同一个基础人格可以适配不同的角色指令。
Claude Code 的角色指令包括:你是一个编程助手,你在命令行环境中运行,你的任务是帮助用户完成编码工作。这些指令界定了 Agent 的能力范围和行为预期。
你是 Claude Code,一个运行在用户终端中的交互式编程代理。
你的工作环境信息:操作系统、shell、当前工作目录。
你应该完整地完成任务——不要过度设计,也不要半途而废。角色指令层的变更频率高于人格层,但仍然是相对稳定的。它通常随着产品版本迭代而调整。
第三层:Tool Definitions(工具定义层)
这一层描述 Agent 可以使用哪些工具、每个工具的参数格式和使用约束。工具定义层本身是半动态的——工具集合可能随着用户配置或权限变化。
关键原则:工具定义不仅包括"能做什么",还必须包括"什么时候该用"和"什么时候不该用"。比如 Claude Code 的文件编辑工具明确指出"你必须先用 Read 工具读过文件才能编辑",文件搜索工具则强调"不要用 Bash 跑 grep 命令,用专用的 Grep 工具"。
这些使用约束就是工具层的 prompt 架构设计,它们直接影响 Agent 的行为质量。
第四层:Context Injection(上下文注入层)
这是真正动态的部分。每次对话开始时,系统根据当前状态注入一系列上下文信息:当前日期、git 状态、项目结构、用户偏好、之前的对话摘要等。
Claude Code 在每次交互中会注入:当前工作目录、git 分支和最近提交、操作系统和 shell 环境信息。这些都不是写死在 prompt 模板里的,而是运行时实时采集后拼装进去的。
Working directory: /Users/dev/my-project
Current branch: feature/auth
Platform: darwin
Shell: zsh上下文注入层的设计难点在于取舍——不是所有可用信息都该注入。每多注入一段文本,都在消耗宝贵的 token 预算。
第五层:Dynamic Rules(动态规则层)
最顶层是根据特定条件触发的规则。比如用户通过 CLAUDE.md 文件定义的项目级指令,或者根据当前对话内容动态加载的专项规则。
这一层最灵活,也最容易出问题。动态规则可能和底层指令冲突,可能彼此矛盾,可能因为注入时机不对而被模型忽略。因此动态规则层需要格外关注优先级和冲突消解机制——这也是下一章的重点内容。
8.3 Claude Code 的 Prompt 模块化实践
让我们以 Claude Code 为具体案例,看看分层模型如何落地。
Claude Code 的 system prompt 并非一个完整的文本文件,而是由多个功能模块在运行时组装而成。通过分析其源码,可以识别出以下关键模块:
| 模块名称 | 职责 | 层次 |
|---|---|---|
| Identity | 身份声明、模型信息 | 基础人格 |
| Environment | OS、shell、cwd 等运行环境 | 上下文注入 |
| Tool Descriptions | 每个工具的描述和约束 | 工具定义 |
| Task Guidelines | 完成任务的通用方法论 | 角色指令 |
| Tone & Style | 沟通风格要求(简洁、不用 emoji) | 角色指令 |
| Output Efficiency | 减少不必要输出的规则 | 角色指令 |
| Git Protocols | Git 操作的详细行为规范 | 动态规则 |
| CLAUDE.md | 用户/项目级自定义指令 | 动态规则 |
每个模块是一个独立的文本片段,有自己的维护者和变更节奏。Identity 模块可能一年改一次,Environment 模块每次对话都重新生成,Git Protocols 模块随着最佳实践积累不断完善。
这种模块化设计带来的好处是显而易见的:
- 独立演进:改动 Git 规范不会意外影响工具描述。
- 条件加载:如果用户没有 git 仓库,Git Protocols 模块可以不加载。
- 可测试性:可以针对单个模块做单元测试,验证它是否正确引导了模型行为。
- 可复用性:Tone & Style 模块可以在不同产品之间共享。
8.4 关注点分离:静态、动态与用户自定义
prompt 架构的核心设计原则是关注点分离。具体来说,需要把内容按三个维度拆分:
静态指令(Static Instructions)
不随对话变化的部分。身份定义、角色说明、通用行为准则、输出格式要求——这些写一次,所有用户所有对话都一样。静态指令应该存储在代码仓库中,随产品版本一起发布。
动态上下文(Dynamic Context)
每次对话开始时实时生成的部分。当前时间、环境信息、会话状态、相关文件内容摘要。动态上下文由 harness 层的代码在运行时采集和注入。
用户自定义(User Customization)
由最终用户或项目维护者提供的指令。这是三者中最不可预测的部分——你无法控制用户会写什么。
为什么分离如此重要?因为每一类内容的生命周期、变更频率和质量保障方式完全不同:
- 静态指令需要 code review,需要 prompt 回归测试。
- 动态上下文需要运行时校验,需要容错处理。
- 用户自定义需要优先级机制和安全过滤。
把它们混在一起,就像把配置文件、环境变量和用户输入全部硬编码到同一个函数里——调试噩梦。
8.5 CLAUDE.md 模式:用户自定义的优雅解法
CLAUDE.md 是 Claude Code 引入的一个精巧设计,值得深入分析其工程思想。
核心思路很简单:在项目根目录放一个 CLAUDE.md 文件,其内容会被自动注入到 system prompt 中。但简单背后有一系列精心的设计决策:
层级覆盖机制。 CLAUDE.md 可以存在于多个位置:用户级(~/.claude/CLAUDE.md)、项目级(项目根目录)、目录级(子目录)。内层配置可以覆盖外层配置,就像 CSS 的层叠规则或 Git 的 .gitignore 层级。
声明式优先于命令式。 CLAUDE.md 的内容是声明式的——"这个项目使用 TypeScript"、"提交消息用中文"、"不要修改 vendor 目录"。它描述约束和偏好,而不是编写执行流程。
零侵入性。 不需要修改任何核心代码,不需要了解 prompt 的内部结构。用户只需要会写 Markdown 就能定制 Agent 行为。这大幅降低了自定义的门槛。
版本可控。 CLAUDE.md 可以提交到 Git 仓库,团队成员共享同一套项目级约束。新成员 clone 仓库后自动获得一致的 Agent 行为。
从架构角度看,CLAUDE.md 模式解决了一个经典的平台工程问题:如何在不暴露系统内部实现的前提下,给用户提供足够的定制能力? 答案是提供一个定义良好的注入点,配合明确的优先级规则。
这个模式具有高度的可迁移性。如果你在构建自己的 Agent 平台,完全可以借鉴这种设计:
python
def build_system_prompt(user_id, project_path, conversation):
layers = []
# 静态层
layers.append(load_static("identity.txt"))
layers.append(load_static("role_instructions.txt"))
layers.append(load_static("tool_definitions.txt"))
# 动态上下文层
layers.append(collect_environment(project_path))
# 用户自定义层
user_config = load_user_config(user_id) # ~/.agent/config.md
project_config = load_project_config(project_path) # .agent.md
layers.append(merge_configs(user_config, project_config))
# 组装
return "\n\n".join(layers)8.6 模板组装:运行时拼装的工程细节
理解了分层模型之后,下一个问题是:这些层如何在运行时组装成最终的 system prompt?
最朴素的方式是字符串拼接。但生产级系统需要考虑更多:
条件化加载。 不是所有模块在所有场景下都需要。如果当前任务不涉及 git 操作,加载 Git Protocols 模块就是在浪费 token。Claude Code 会根据当前工作目录是否是 git 仓库来决定是否注入 git 相关指令。
顺序敏感性。 大模型对 prompt 中信息的位置是敏感的。一般来说,越靠前的内容权重越高(primacy effect),最末尾的内容也有较高关注度(recency effect),中间部分最容易被忽略。因此关键指令应该放在 prompt 的开头或结尾。
分隔符设计。 模块之间需要清晰的视觉分隔,帮助模型理解结构边界。常见做法包括使用 Markdown 标题、XML 标签或自定义分隔线。Claude Code 使用 XML 风格的标签(如 <system-reminder>)来标注动态注入的内容块,让模型能够区分核心指令和运行时上下文。
token 预算管理。 这是模板组装中最务实的考量。system prompt 和对话历史共享同一个上下文窗口。prompt 越长,留给对话的空间越小。一个 200k 窗口的模型,如果 system prompt 占了 30k,对话就只剩 170k(还要预留输出空间)。
实践中的常见策略:
- 设定 system prompt 的 token 上限(比如不超过窗口的 15%)。
- 对动态内容做截断和摘要。比如 git log 只取最近 5 条,文件列表只展示前两层目录。
- 对低优先级模块实施"压缩模式"——当 token 紧张时,用精简版替代完整版。
- 监控各模块的 token 占比,识别"膨胀"的模块。
8.7 把 Prompt 当代码管理
如果 system prompt 是架构,那它就应该享受和代码同等的工程待遇。
版本控制。 所有 prompt 文本必须入版本管理。不是放在数据库里由产品经理在后台随便改,而是放在代码仓库里,走 PR 流程,有 review、有 changelog。
每一次 prompt 变更都应该能回答三个问题:
- 改了什么?(diff 可见)
- 为什么改?(commit message 说明)
- 效果如何?(关联的评估结果)
环境隔离。 就像代码有 dev/staging/prod 环境,prompt 也应该有。新的 prompt 变更先在开发环境验证,通过评估后再部署到生产。
变更审计。 生产环境的 prompt 变更必须有记录。当 Agent 行为出现异常时,第一件事就是查看最近的 prompt 变更。没有审计记录,排查就是大海捞针。
一种被验证有效的实践是prompt 目录结构:
prompts/
├── base/
│ ├── identity.md # 基础人格
│ └── role.md # 角色指令
├── tools/
│ ├── file_read.md # 文件读取工具描述
│ ├── file_edit.md # 文件编辑工具描述
│ └── bash.md # 命令行工具描述
├── protocols/
│ ├── git.md # Git 操作规范
│ └── security.md # 安全相关规则
├── templates/
│ └── system_prompt.py # 组装逻辑
└── tests/
├── test_identity.py # 人格层测试
└── test_git_protocol.py # Git 规范测试每个 .md 文件是一个 prompt 模块,templates/ 放组装逻辑,tests/ 放评估用例。这种结构一目了然,新人上手成本极低。
8.8 Prompt 的测试与评估
代码有单元测试,prompt 也应该有。但 prompt 测试和传统测试有一个本质区别:输出是非确定性的。 同一个 prompt,同一个输入,模型可能给出不同的回答。
因此 prompt 测试更接近"评估"而非"断言"。核心方法包括:
行为测试(Behavioral Testing)。 给定一个场景,验证 Agent 的行为是否符合预期。不是检查具体输出文本,而是检查行为特征。
例如,要测试 Git Protocols 模块是否生效:
- 输入:"帮我提交代码"
- 期望行为:Agent 应先运行
git status和git diff,而不是直接git commit。 - 验证方式:检查 Agent 的工具调用序列。
回归测试(Regression Testing)。 每次 prompt 变更后,跑一遍核心场景的测试集。如果新改动导致已有场景的通过率下降,就需要审慎评估。
A/B 测试。 在生产环境中对比不同 prompt 版本的效果。随机将一部分流量分配给新 prompt,对比关键指标(任务完成率、用户满意度、工具调用次数等)。
对抗测试。 专门构造试图"破坏"prompt 约束的输入。如果 prompt 规定"不要执行危险的 git 操作",测试用例就应该包括各种引诱 Agent 执行 git push --force 的请求。
一个实用的评估框架骨架:
python
class PromptEvalSuite:
def __init__(self, prompt_builder):
self.prompt_builder = prompt_builder
def eval_case(self, scenario, expected_behavior):
prompt = self.prompt_builder.build()
response = call_model(prompt, scenario)
return expected_behavior.check(response)
def run_suite(self, cases):
results = [self.eval_case(c.scenario, c.expected) for c in cases]
pass_rate = sum(results) / len(results)
return pass_rate关键指标不是 100% 通过率——那在非确定性系统中不现实。而是设定一个可接受的阈值(比如 95%),并监控趋势。如果通过率从 97% 掉到 92%,就需要排查原因。
8.9 反模式清单
最后,列举实践中最常见的 prompt 架构反模式,帮你避坑:
反模式一:巨石 Prompt。 所有指令塞在一个字符串里,没有结构,没有分层。改动困难,测试不可能,冲突频发。这是最常见也最致命的反模式。
反模式二:指令矛盾。 前面说"保持输出简洁",后面又说"给出详细的解释和示例"。模型遇到矛盾指令时的行为是不可预测的——它可能遵循前者,可能遵循后者,可能试图折中,也可能完全忽略两者。
消解矛盾的关键是明确优先级。比如 Claude Code 用 IMPORTANT: 前缀标注高优先级指令,用层级关系隐式建立优先级(动态规则层 > 角色指令层)。
反模式三:过度频繁变更。 每周改一次 system prompt,每次都是大改。结果是没有任何稳定基线,无法做有效的评估对比,也无法积累关于"什么有效什么无效"的工程认知。
好的节奏是:基础层级每季度审视一次,角色指令层按版本迭代,动态规则层可以按需调整但要有测试覆盖。
反模式四:忽视 Token 成本。 不断往 prompt 里加内容,从不做减法。某天突然发现 system prompt 占了上下文窗口的一半,对话能力严重退化。必须定期审计 prompt 的 token 消耗。
反模式五:缺乏可观测性。 不知道当前生产环境跑的是哪个版本的 prompt,不知道某次 prompt 变更是什么时候部署的,出了问题查不到根因。prompt 的变更管理应该和代码部署享有同等的可观测性。
8.10 本章小结
System prompt 的分层设计,本质上是把软件工程中久经验证的架构原则应用到 prompt 领域:
- 分层:每一层有明确的职责和稳定性等级。
- 模块化:独立的模块可以独立演进、独立测试。
- 关注点分离:静态指令、动态上下文、用户自定义,三者各有其管理方式。
- 版本控制:prompt 是代码,不是随手写的便签。
- 可测试性:行为评估框架替代简单的字符串比对。
这些不是高深的理论,而是工程常识在新领域的应用。但正是因为 prompt 看起来"只是一段文本",太多人忽视了对它施加工程纪律的必要性。
下一章,我们将深入探讨分层设计中最棘手的子问题:当多层指令发生冲突时,优先级如何裁决? 这就是第9章"指令优先级"要解决的核心问题。