Skip to content

第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身份声明、模型信息基础人格
EnvironmentOS、shell、cwd 等运行环境上下文注入
Tool Descriptions每个工具的描述和约束工具定义
Task Guidelines完成任务的通用方法论角色指令
Tone & Style沟通风格要求(简洁、不用 emoji)角色指令
Output Efficiency减少不必要输出的规则角色指令
Git ProtocolsGit 操作的详细行为规范动态规则
CLAUDE.md用户/项目级自定义指令动态规则

每个模块是一个独立的文本片段,有自己的维护者和变更节奏。Identity 模块可能一年改一次,Environment 模块每次对话都重新生成,Git Protocols 模块随着最佳实践积累不断完善。

这种模块化设计带来的好处是显而易见的:

  1. 独立演进:改动 Git 规范不会意外影响工具描述。
  2. 条件加载:如果用户没有 git 仓库,Git Protocols 模块可以不加载。
  3. 可测试性:可以针对单个模块做单元测试,验证它是否正确引导了模型行为。
  4. 可复用性: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 变更都应该能回答三个问题:

  1. 改了什么?(diff 可见)
  2. 为什么改?(commit message 说明)
  3. 效果如何?(关联的评估结果)

环境隔离。 就像代码有 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 statusgit 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章"指令优先级"要解决的核心问题。

基于 VitePress 构建