Harness Engineering

第8章 System Prompt 分层设计

作者 杨艺韬 · 7,853 字

第8章 System Prompt 分层设计

“A prompt is not a string — it’s an architecture.” — Claude Code team

“System Prompt 承载的是 Agent 的灵魂。灵魂的复杂度决定了能力的上限,但也决定了维护的难度。” —— 杨艺韬

本章要点

  • System Prompt 是架构问题,不是文案问题——要像写代码一样严谨
  • 五层模型:Base Personality / Role / Tools / Context / Dynamic Rules
  • 三种关注点分离:静态指令 / 动态上下文 / 用户自定义——三者生命周期完全不同
  • CLAUDE.md 模式——用户自定义的优雅解法,值得在所有 Agent 平台借鉴
  • Prompt Caching 对齐——缓存友好的分层设计可节省 70%+ 成本
  • 像代码一样治理:版本控制 + 回归测试 + A/B + 变更审计
  • 六大反模式:巨石 Prompt / 指令矛盾 / 过度变更 / Token 失控 / 缺乏可观测 / 责任模糊

8.1 System Prompt 不是一段字符串

很多开发者第一次接触 Agent 开发时,system prompt 是这样写的:一个字符串常量,塞在代码里,想到什么加什么,越写越长,最后变成一坨没人敢动的文本。改一个词,三个功能崩了。加一条规则,和前面的指令冲突了。

这是 prompt 的泥球架构——和代码世界里的 Big Ball of Mud 如出一辙。

真实的生产级 Agent 系统不会这样做。如果你去读 Claude Code 的源码,会发现它的 system prompt 是一个精心设计的多层架构,由十几个模块在运行时组装而成。每个模块有明确的职责边界,静态内容和动态内容严格分离,用户自定义和系统默认互不干扰。

System prompt 是一个架构问题,不是一个文案问题。

graph TD
    subgraph Assembly["运行时 Prompt 组装"]
        L5["Layer 5: 动态规则<br/>(system-reminder, hooks)"] -.->|"对话中途注入"| Final
        L4["Layer 4: 上下文注入<br/>(git status, CLAUDE.md, 记忆)"] --> Final
        L3["Layer 3: 工具定义<br/>(Read, Edit, Bash...)"] --> Final
        L2["Layer 2: 角色指令<br/>(任务规范, 安全协议)"] --> Final
        L1["Layer 1: 基础人格<br/>(身份, 能力边界)"] --> Final["最终 System Prompt"]
    end

    Final --> LLM["发送给 LLM"]

    style L1 fill:#dbeafe,stroke:#3b82f6
    style L2 fill:#e0e7ff,stroke:#6366f1
    style L3 fill:#f3e8ff,stroke:#a855f7
    style L4 fill:#fef3c7,stroke:#f59e0b
    style L5 fill:#fee2e2,stroke:#ef4444

本章的目标,就是把这个架构拆清楚。

8.2 分层模型:从人格到动态规则

一个设计良好的 system prompt 可以抽象为五个层次,从底层到顶层依次是:

第一层:Base Personality(基础人格层)

这是 Agent 最核心的身份定义。它回答一个问题:你是谁? 包括名字、角色定位、基本行为准则、沟通风格。这一层极少变化,可能整个产品生命周期只改动几次。

你是 Claude,由 Anthropic 开发的 AI 助手。
你诚实、有帮助、无害。
当你不确定时,你会明确说明。

看起来平平无奇,但这一层承担的是”锚定”功能。后续所有层次的指令都建立在这个基础之上。如果基础人格层定义了”你要诚实”,后面的角色指令就不应该让 Agent 编造信息。

变更频率:极低(一年 1-2 次) 所有者:通常由模型提供商或产品负责人决定 Prompt Caching 友好度:⭐⭐⭐⭐⭐(极稳定,天然可缓存)

第二层:Role Instructions(角色指令层)

在基础人格之上,根据具体应用场景定义 Agent 的专业角色。同一个基础人格可以适配不同的角色指令。

Claude Code 的角色指令包括:你是一个编程助手,你在命令行环境中运行,你的任务是帮助用户完成编码工作。这些指令界定了 Agent 的能力范围和行为预期。

你是 Claude Code,一个运行在用户终端中的交互式编程代理。
你的工作环境信息:操作系统、shell、当前工作目录。
你应该完整地完成任务——不要过度设计,也不要半途而废。

Tone and style:
- Short and concise responses
- No emojis unless requested
- Reference file paths as file_path:line_number

角色指令层的变更频率高于人格层,但仍然是相对稳定的。它通常随着产品版本迭代而调整。

变更频率:低(每个产品版本 1-2 次) 所有者:产品团队 + 资深工程师 Prompt Caching 友好度:⭐⭐⭐⭐(稳定,按版本缓存)

第三层:Tool Definitions(工具定义层)

这一层描述 Agent 可以使用哪些工具、每个工具的参数格式和使用约束。工具定义层本身是半动态的——工具集合可能随着用户配置或权限变化。

关键原则:工具定义不仅包括”能做什么”,还必须包括”什么时候该用”和”什么时候不该用”。比如 Claude Code 的文件编辑工具明确指出”你必须先用 Read 工具读过文件才能编辑”,文件搜索工具则强调”不要用 Bash 跑 grep 命令,用专用的 Grep 工具”。

## Edit Tool

Performs exact string replacements in files.

Usage:
- You must use the `Read` tool at least once in the conversation
  before editing. This tool will error if you attempt an edit
  without reading the file.
- The edit will FAIL if `old_string` is not unique in the file.
  Either provide a larger string with more surrounding context
  to make it unique or use `replace_all`.
- ALWAYS prefer editing existing files. NEVER write new files
  unless explicitly required.

这些使用约束就是工具层的 prompt 架构设计,它们直接影响 Agent 的行为质量。

变更频率:中(每个功能迭代) 所有者:平台工程团队 Prompt Caching 友好度:⭐⭐⭐(基础工具稳定,可选工具动态)

第四层:Context Injection(上下文注入层)

这是真正动态的部分。每次对话开始时,系统根据当前状态注入一系列上下文信息:当前日期、git 状态、项目结构、用户偏好、之前的对话摘要等。

Claude Code 在每次交互中会注入:当前工作目录、git 分支和最近提交、操作系统和 shell 环境信息。这些都不是写死在 prompt 模板里的,而是运行时实时采集后拼装进去的。

# Environment
- Primary working directory: /Users/dev/my-project
- Is a git repository: true
- Platform: darwin
- Shell: zsh
- OS Version: Darwin 23.6.0

# gitStatus
Current branch: main
Status:
 M src/app.ts
?? src/new-feature.ts

Recent commits:
a0b32bd book(harness): ch17 rewrite
ef3980f book(harness): ch20 rewrite

上下文注入层的设计难点在于取舍——不是所有可用信息都该注入。每多注入一段文本,都在消耗宝贵的 token 预算。

变更频率:每次会话 所有者:平台工程团队(定义字段),业务逻辑(填充数据) Prompt Caching 友好度:⭐(几乎不可缓存——必须隔离)

第五层:Dynamic Rules(动态规则层)

最顶层是根据特定条件触发的规则。比如用户通过 CLAUDE.md 文件定义的项目级指令,或者根据当前对话内容动态加载的专项规则。

<system-reminder>
The user is working on a production database migration.
Be extra careful with any SQL modification suggestions.
Require explicit user confirmation before any DROP/TRUNCATE.
</system-reminder>

这一层最灵活,也最容易出问题。动态规则可能和底层指令冲突,可能彼此矛盾,可能因为注入时机不对而被模型忽略。因此动态规则层需要格外关注优先级和冲突消解机制——这也是下一章的重点内容。

变更频率:每次会话或每轮对话 所有者:用户(CLAUDE.md)+ 系统(hooks) Prompt Caching 友好度:不可缓存

五层的对比矩阵

维度人格角色工具上下文动态规则
变更频率极低每会话每轮
所有者模型商产品平台工程平台+业务用户+系统
Prompt Caching✅✅✅✅✅
测试覆盖要求极高
版本控制必需

8.3 Claude Code 的 Prompt 模块化实践

让我们以 Claude Code 为具体案例,看看分层模型如何落地。

Claude Code 的 system prompt 并非一个完整的文本文件,而是由多个功能模块在运行时组装而成。通过分析其源码,可以识别出以下关键模块:

模块名称职责层次Token 范围缓存
Identity身份声明、模型信息基础人格~200
EnvironmentOS、shell、cwd 等运行环境上下文注入~100
Tool Descriptions每个工具的描述和约束工具定义~5000
Task Guidelines完成任务的通用方法论角色指令~800
Tone & Style沟通风格要求(简洁、不用 emoji)角色指令~300
Output Efficiency减少不必要输出的规则角色指令~150
Task ToolsTaskCreate/TaskUpdate 指南角色指令~400
Git ProtocolsGit 操作的详细行为规范动态规则~2000⚠️条件加载
CLAUDE.md用户/项目级自定义指令动态规则0-5000
Memory Index跨会话长期记忆索引上下文注入0-1000
System-Reminder运行时动态提示动态规则0-500

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

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

  1. 独立演进:改动 Git 规范不会意外影响工具描述。
  2. 条件加载:如果用户没有 git 仓库,Git Protocols 模块可以不加载。
  3. 可测试性:可以针对单个模块做单元测试,验证它是否正确引导了模型行为。
  4. 可复用性:Tone & Style 模块可以在不同产品之间共享。
  5. 缓存友好:稳定模块可以单独作为缓存 block,动态模块隔离出来。

组装的顺序原则

模块的组装顺序不是随意的——遵循”稳定性从上到下递减”:

1. Identity          ← 最稳定(缓存友好)
2. Role Instructions
3. Tone & Style
4. Task Guidelines
5. Task Tools
6. Tool Descriptions ← 次稳定
7. Git Protocols     ← 条件加载
8. Memory Index      ← 会话级
9. CLAUDE.md
10. Environment      ← 最动态(不缓存)
11. System-Reminder  ← 运行时注入

这个顺序让前面的内容可以被长期缓存,后面动态变化的内容不影响前面的命中。

8.4 关注点分离:静态、动态与用户自定义

prompt 架构的核心设计原则是关注点分离。具体来说,需要把内容按三个维度拆分:

静态指令(Static Instructions)

不随对话变化的部分。身份定义、角色说明、通用行为准则、输出格式要求——这些写一次,所有用户所有对话都一样。静态指令应该存储在代码仓库中,随产品版本一起发布。

管理方式

  • 存储在 code repo
  • 走 PR/review 流程
  • 有回归测试
  • 发布时统一部署

动态上下文(Dynamic Context)

每次对话开始时实时生成的部分。当前时间、环境信息、会话状态、相关文件内容摘要。动态上下文由 harness 层的代码在运行时采集和注入。

管理方式

  • 运行时生成
  • 有 fallback(采集失败不阻塞)
  • Token 预算硬限制
  • 不进入 Prompt Cache

用户自定义(User Customization)

由最终用户或项目维护者提供的指令。这是三者中最不可预测的部分——你无法控制用户会写什么。

管理方式

  • 受信任度低——必须有容错
  • 可能和系统指令冲突——需要优先级
  • 可能包含敏感信息——需要过滤
  • 大小要封顶——防止滥用

为什么分离如此重要?因为每一类内容的生命周期、变更频率和质量保障方式完全不同:

  • 静态指令需要 code review,需要 prompt 回归测试。
  • 动态上下文需要运行时校验,需要容错处理。
  • 用户自定义需要优先级机制和安全过滤。

把它们混在一起,就像把配置文件、环境变量和用户输入全部硬编码到同一个函数里——调试噩梦。

三维分离的可视化

graph TD
    Assembly[Prompt 组装器]
    Assembly --> S[静态模块<br/>from code repo]
    Assembly --> D[动态上下文<br/>from runtime collectors]
    Assembly --> U[用户自定义<br/>from CLAUDE.md + settings]

    S --> SA[Identity]
    S --> SB[Role]
    S --> SC[Tools]
    S --> SD[Style]

    D --> DA[Environment]
    D --> DB[Git State]
    D --> DC[Memory Index]

    U --> UA[Project CLAUDE.md]
    U --> UB[User CLAUDE.md]
    U --> UC[settings.json hooks]

    SA & SB & SC & SD --> Cache[Prompt Cache<br/>命中]
    DA & DB & DC --> Fresh[每次重建<br/>不缓存]
    UA & UB & UC --> PartialCache[稳定用户配置<br/>可缓存]

    Cache --> Final[最终 Prompt]
    Fresh --> Final
    PartialCache --> Final

    style S fill:#dbeafe,stroke:#3b82f6
    style D fill:#fef3c7,stroke:#f59e0b
    style U fill:#dcfce7,stroke:#22c55e

8.5 CLAUDE.md 模式:用户自定义的优雅解法

CLAUDE.md 是 Claude Code 引入的一个精巧设计,值得深入分析其工程思想。

核心思路很简单:在项目根目录放一个 CLAUDE.md 文件,其内容会被自动注入到 system prompt 中。但简单背后有一系列精心的设计决策:

四个设计决策

1. 层级覆盖机制

CLAUDE.md 可以存在于多个位置:

  • 用户级:~/.claude/CLAUDE.md——跨所有项目
  • 项目级:项目根目录的 CLAUDE.md——当前项目
  • 目录级:子目录的 CLAUDE.md——特定模块

内层配置可以覆盖外层配置,就像 CSS 的层叠规则或 Git 的 .gitignore 层级。

~/.claude/CLAUDE.md               # 用户全局偏好
  └─ /project/CLAUDE.md           # 项目级(覆盖全局)
      └─ /project/mobile/CLAUDE.md  # 模块级(覆盖项目)

2. 声明式优先于命令式

CLAUDE.md 的内容是声明式的——“这个项目使用 TypeScript”、“提交消息用中文”、“不要修改 vendor 目录”。它描述约束和偏好,而不是编写执行流程。

# 项目约定

- 使用 TypeScript 严格模式
- 提交消息用中文,格式:type(scope): subject
- 不要修改 vendor/ 目录下的文件
- 测试使用 vitest,不要用 jest

3. 零侵入性

不需要修改任何核心代码,不需要了解 prompt 的内部结构。用户只需要会写 Markdown 就能定制 Agent 行为。这大幅降低了自定义的门槛。

4. 版本可控

CLAUDE.md 可以提交到 Git 仓库,团队成员共享同一套项目级约束。新成员 clone 仓库后自动获得一致的 Agent 行为——这是最容易被低估的价值。

平台工程的通用范式

从架构角度看,CLAUDE.md 模式解决了一个经典的平台工程问题:

如何在不暴露系统内部实现的前提下,给用户提供足够的定制能力?

答案是提供一个定义良好的注入点,配合明确的优先级规则

这个模式具有高度的可迁移性。如果你在构建自己的 Agent 平台,完全可以借鉴这种设计:

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(load_static("tone_style.txt"))

    # === 用户级自定义(次稳定)===
    user_config = load_user_config(user_id)       # ~/.agent/config.md
    if user_config:
        layers.append(user_config)

    # === 项目级自定义(次稳定)===
    project_config = find_project_config(project_path)  # .agent.md
    if project_config:
        layers.append(project_config)

    # === 动态上下文层(不缓存)===
    layers.append(collect_environment(project_path))
    layers.append(collect_git_state(project_path))
    layers.append(load_memory_index(user_id, project_path))

    # === Hooks 层 ===
    for hook in load_hooks(user_id):
        layers.append(execute_hook(hook))

    # 组装
    return "\n\n".join(layers)

同类模式横评

产品用户自定义机制核心特性
Claude CodeCLAUDE.md 多层覆盖Markdown, 层级覆盖, Git 友好
Cursor.cursorrulesYAML 格式, 项目级
Windsurf.windsurfrulesMarkdown, 项目级
Continue.continue/config.jsonJSON, 配置+指令混合
Zed AI内置 settingsGUI 配置

Claude Code 的 Markdown + 层级覆盖是被验证最平衡的方案——既保留了可读性,又支持细粒度控制。

8.6 模板组装:运行时拼装的工程细节

理解了分层模型之后,下一个问题是:这些层如何在运行时组装成最终的 system prompt?

最朴素的方式是字符串拼接。但生产级系统需要考虑更多:

条件化加载

不是所有模块在所有场景下都需要。如果当前任务不涉及 git 操作,加载 Git Protocols 模块就是在浪费 token。Claude Code 会根据当前工作目录是否是 git 仓库来决定是否注入 git 相关指令。

class PromptAssembler {
  async assemble(context: Context): Promise<string[]> {
    const blocks: PromptBlock[] = []

    // 总是加载的基础模块
    blocks.push(this.identity)
    blocks.push(this.role)

    // 条件加载
    if (context.isGitRepo) {
      blocks.push(this.gitProtocols)
    }
    if (context.hasDockerfile) {
      blocks.push(this.dockerProtocols)
    }
    if (context.hasCI) {
      blocks.push(this.ciProtocols)
    }

    // 用户偏好
    if (context.userConfig) {
      blocks.push(context.userConfig)
    }

    // 工具(按可用性)
    for (const tool of context.availableTools) {
      blocks.push(this.toolDescriptions[tool])
    }

    return blocks.map(b => b.content)
  }
}

顺序敏感性

大模型对 prompt 中信息的位置是敏感的。一般来说:

  • Primacy effect:越靠前的内容权重越高
  • Recency effect:最末尾的内容也有较高关注度
  • Lost in the middle:中间部分最容易被忽略

因此关键指令应该放在 prompt 的开头或结尾。次要约束放中间。

分隔符设计

模块之间需要清晰的视觉分隔,帮助模型理解结构边界。常见做法:

# Markdown 标题(最常用)
# Identity
...
# Tool Usage
...

# XML 标签(结构化更强)
<identity>
  ...
</identity>

# 自定义分隔线
═══ IDENTITY ═══
...
═══ TOOLS ═══

Claude Code 使用 XML 风格的标签(如 <system-reminder><system>)来标注动态注入的内容块,让模型能够区分核心指令和运行时上下文。

Token 预算管理

这是模板组装中最务实的考量。system prompt 和对话历史共享同一个上下文窗口。prompt 越长,留给对话的空间越小。一个 200K 窗口的模型,如果 system prompt 占了 30K,对话就只剩 170K(还要预留输出空间)。

实践中的常见策略:

class TokenBudgetManager {
  private maxPromptTokens = 30_000    // 上限
  private compactThreshold = 25_000   // 超过就压缩

  async fitInBudget(blocks: PromptBlock[]): Promise<PromptBlock[]> {
    let total = blocks.reduce((s, b) => s + countTokens(b.content), 0)

    if (total <= this.compactThreshold) return blocks

    // 策略 1: 降级低优先级模块
    blocks = blocks.map(b => {
      if (b.priority === "low" && b.compactVersion) {
        return { ...b, content: b.compactVersion }
      }
      return b
    })

    total = blocks.reduce((s, b) => s + countTokens(b.content), 0)
    if (total <= this.maxPromptTokens) return blocks

    // 策略 2: 丢弃可选模块
    blocks = blocks.filter(b => b.priority !== "optional")

    total = blocks.reduce((s, b) => s + countTokens(b.content), 0)
    if (total <= this.maxPromptTokens) return blocks

    // 策略 3: 告警并截断
    logger.warn(`Prompt budget exceeded: ${total}, truncating dynamic context`)
    return this.truncateDynamic(blocks)
  }
}

核心策略清单:

  • 设定 system prompt 的 token 上限(比如不超过窗口的 15%)
  • 对动态内容做截断和摘要——git log 只取最近 5 条,文件列表只展示前两层目录
  • 对低优先级模块实施”压缩模式”——当 token 紧张时,用精简版替代完整版
  • 监控各模块的 token 占比,识别”膨胀”的模块

8.7 Prompt Caching 对齐:为缓存而设计

2024 年 Anthropic 推出的 Prompt Caching 机制,对 prompt 分层架构提出了新要求。

缓存命中的铁律

  1. 前缀匹配——Cache 是按前缀匹配的。前面改了,后面全部失效。
  2. 字节级一致——完全一致才能命中。
  3. 5 分钟 TTL——最后访问后 5 分钟失效。

这意味着:稳定的内容必须在前,动态内容必须在后

缓存友好的分层

┌─ System Prompt ──────────────────────┐
│ ┌─ Cache Block 1 ─────────────────┐  │
│ │ Identity (static, 200 tokens)    │  │
│ │ Role (static, 800 tokens)         │  │
│ │ Tone & Style (static, 300 tokens) │  │  ← cache_control 标记
│ │ Tools (static, 5000 tokens)       │  │
│ │ Task Guidelines (static, 500)     │  │
│ └──────────────────────────────────┘  │
│                                        │
│ ┌─ Cache Block 2 ─────────────────┐  │
│ │ User CLAUDE.md (semi-static)      │  │
│ │ Project CLAUDE.md (semi-static)   │  │  ← cache_control 标记
│ └──────────────────────────────────┘  │
│                                        │
│ ┌─ Uncached ──────────────────────┐  │
│ │ Environment (dynamic)             │  │
│ │ Git State (dynamic)                │  │  ← 不标记
│ │ Memory Index (dynamic)             │  │
│ │ System-Reminder (dynamic)          │  │
│ └──────────────────────────────────┘  │
└──────────────────────────────────────┘

缓存命中率优化策略

策略 1:Cache Block 粒度

每个 cache_control 标记都有开销。实践中 2-4 个 block 就够:

  • Block 1: 静态系统内容
  • Block 2: 用户配置
  • Block 3(可选): 稳定的对话历史

策略 2:避免”小改动大失效”

如果在静态部分加一个换行,整个缓存就失效了。要严格控制静态内容的”无意义变动”:

// ❌ 容易失效
function buildStaticPrompt() {
  return `${identity}\n${role}\n${new Date().getFullYear()} version`  // 每年会变
}

// ✅ 稳定
function buildStaticPrompt() {
  return `${identity}\n${role}`  // 不要把动态信息混进静态
}

策略 3:合并稳定模块

把多个小静态模块合并成一个 cache block,减少 cache_control 标记的开销。

成本估算

假设一个 Agent:

  • 系统 Prompt 静态部分 20K tokens
  • 每轮用户消息 + 响应 5K tokens
  • 对话 30 轮

不缓存成本(Opus $15/1M input):

30 轮 × (20K + 历史增长) tokens × $15/1M ≈ $15-30

带缓存成本:

第 1 轮: 20K × $18.75/1M (写缓存溢价) = $0.375
后续 29 轮: 20K × $1.50/1M (缓存读取) + 动态部分 × $15/1M
           ≈ 29 × $0.03 = $0.87
总计: ≈ $1.25,节省 90%+

Prompt Caching 改变了长对话 Agent 的商业模型——分层设计的稳定性回报,直接变成了成本回报

8.8 把 Prompt 当代码管理

如果 system prompt 是架构,那它就应该享受和代码同等的工程待遇。

版本控制

所有 prompt 文本必须入版本管理。不是放在数据库里由产品经理在后台随便改,而是放在代码仓库里,走 PR 流程,有 review、有 changelog。

每一次 prompt 变更都应该能回答三个问题:

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

环境隔离

就像代码有 dev/staging/prod 环境,prompt 也应该有。新的 prompt 变更先在开发环境验证,通过评估后再部署到生产。

# prompt-versions.yaml
prompts:
  identity:
    dev: v1.2.3
    staging: v1.2.2
    prod: v1.2.1
  role:
    dev: v2.1.0
    staging: v2.0.5
    prod: v2.0.5

变更审计

生产环境的 prompt 变更必须有记录。当 Agent 行为出现异常时,第一件事就是查看最近的 prompt 变更。没有审计记录,排查就是大海捞针。

interface PromptChangelogEntry {
  version: string
  module: string
  author: string
  timestamp: Date
  diff: string
  reason: string
  evalResults?: EvalMetrics
  rolloutPlan: {
    canary: number  // 初始灰度比例
    fullAt: Date    // 全量时间
  }
}

Prompt 目录结构

一种被验证有效的实践是 prompt 目录结构

prompts/
├── base/
│   ├── identity.md          # 基础人格
│   └── role.md              # 角色指令
├── tools/
│   ├── file_read.md         # 文件读取工具描述
│   ├── file_edit.md         # 文件编辑工具描述
│   └── bash.md              # 命令行工具描述
├── protocols/
│   ├── git.md               # Git 操作规范
│   └── security.md          # 安全相关规则
├── dynamic/
│   ├── environment_tpl.md   # 环境注入模板
│   └── memory_tpl.md        # 记忆注入模板
├── templates/
│   └── system_prompt.py     # 组装逻辑
└── tests/
    ├── test_identity.py     # 人格层测试
    ├── test_git_protocol.py # Git 规范测试
    └── fixtures/            # 测试场景

每个 .md 文件是一个 prompt 模块,templates/ 放组装逻辑,tests/ 放评估用例。这种结构一目了然,新人上手成本极低。

跨团队协作模式

大型团队中,prompt 的不同层往往归属不同团队:

所有团队审批流
Base Personality产品 + Legal产品总监 + 法务
Role Instructions产品团队PM
Tool Definitions平台工程架构组
Context Injection平台 + 数据SRE
Dynamic Rules用户 / 运营N/A (用户自主)

这种分工有助于避免”所有人都能改系统 prompt”的混乱局面。

8.9 Prompt 的测试与评估

代码有单元测试,prompt 也应该有。但 prompt 测试和传统测试有一个本质区别:输出是非确定性的。同一个 prompt,同一个输入,模型可能给出不同的回答。

因此 prompt 测试更接近”评估”而非”断言”。核心方法包括:

行为测试(Behavioral Testing)

给定一个场景,验证 Agent 的行为是否符合预期。不是检查具体输出文本,而是检查行为特征

例如,要测试 Git Protocols 模块是否生效:

  • 输入:“帮我提交代码”
  • 期望行为:Agent 应先运行 git statusgit diff,而不是直接 git commit
  • 验证方式:检查 Agent 的工具调用序列
class GitProtocolTest:
    scenario = "help me commit the changes"

    def verify(self, agent_trace: list[Action]) -> bool:
        # Assert: first action is git status or git diff
        assert agent_trace[0].tool in {"git status", "git diff"}
        # Assert: commit is NOT first action
        assert agent_trace[0].tool != "git commit"
        # Assert: if commit happens, must come after status/diff
        for action in agent_trace:
            if action.tool == "git commit":
                prior_tools = [a.tool for a in agent_trace[:agent_trace.index(action)]]
                assert "git status" in prior_tools or "git diff" in prior_tools
                return True
        return False

回归测试(Regression Testing)

每次 prompt 变更后,跑一遍核心场景的测试集。如果新改动导致已有场景的通过率下降,就需要审慎评估。

维护一个”黄金测试集”:

  • 50-100 个精心挑选的场景
  • 覆盖所有关键行为
  • 每次 PR 都跑一遍
  • 通过率阈值(如 95%)作为 merge gate

A/B 测试

在生产环境中对比不同 prompt 版本的效果。随机将一部分流量分配给新 prompt,对比关键指标:

  • 任务完成率
  • 用户满意度
  • 工具调用次数
  • 重试率
  • 平均响应 tokens

对抗测试(Red-Teaming)

专门构造试图”破坏”prompt 约束的输入。如果 prompt 规定”不要执行危险的 git 操作”,测试用例就应该包括各种引诱 Agent 执行 git push --force 的请求。

对抗测试覆盖:

  • Prompt 注入攻击
  • 角色扮演诱导
  • 分步诱导(每步看起来无害,组合起来有害)
  • 紧迫性压力(“紧急”、“不要问”)

评估框架骨架

class PromptEvalSuite:
    def __init__(self, prompt_builder):
        self.prompt_builder = prompt_builder

    def eval_case(self, scenario, expected_behavior):
        prompt = self.prompt_builder.build()
        trace = run_agent(prompt, scenario)
        return expected_behavior.check(trace)

    def run_suite(self, cases):
        results = {
            "total": len(cases),
            "passed": 0,
            "failed": [],
            "metrics": defaultdict(list),
        }
        for case in cases:
            passed = self.eval_case(case.scenario, case.expected)
            if passed:
                results["passed"] += 1
            else:
                results["failed"].append(case.name)
        results["pass_rate"] = results["passed"] / results["total"]
        return results

关键指标不是 100% 通过率——那在非确定性系统中不现实。而是设定一个可接受的阈值(比如 95%),并监控趋势。如果通过率从 97% 掉到 92%,就需要排查原因。

8.10 六大反模式

最后,列举实践中最常见的 prompt 架构反模式,帮你避坑:

反模式一:巨石 Prompt(Monolithic Prompt)

现象:所有指令塞在一个字符串里,没有结构,没有分层。改动困难,测试不可能,冲突频发。这是最常见也最致命的反模式。

对策:按五层模型重构,每层独立文件,组装器负责拼接。

反模式二:指令矛盾(Conflicting Instructions)

现象:前面说”保持输出简洁”,后面又说”给出详细的解释和示例”。模型遇到矛盾指令时的行为是不可预测的——它可能遵循前者,可能遵循后者,可能试图折中,也可能完全忽略两者。

对策

  • 明确优先级。比如 Claude Code 用 IMPORTANT: 前缀标注高优先级指令
  • 用层级关系隐式建立优先级(动态规则层 > 角色指令层)
  • 矛盾检测工具——扫描所有 prompt 模块,发现潜在冲突

反模式三:过度频繁变更(Over-churn)

现象:每周改一次 system prompt,每次都是大改。结果是没有任何稳定基线,无法做有效的评估对比,也无法积累关于”什么有效什么无效”的工程认知。

对策:不同层采用不同节奏。基础层级每季度审视一次,角色指令层按版本迭代,动态规则层可以按需调整但要有测试覆盖。

反模式四:忽视 Token 成本(Token Bloat)

现象:不断往 prompt 里加内容,从不做减法。某天突然发现 system prompt 占了上下文窗口的一半,对话能力严重退化。

对策

  • 定期审计各模块 token 消耗
  • 设定每模块的 token 上限
  • 每 PR 报告 token delta

反模式五:缺乏可观测性(No Observability)

现象:不知道当前生产环境跑的是哪个版本的 prompt,不知道某次 prompt 变更是什么时候部署的,出了问题查不到根因。

对策

  • Prompt 版本号嵌入到日志
  • 每次请求记录使用的 prompt 版本
  • 变更通知接入告警系统

反模式六:责任模糊(Ownership Crisis)

现象:所有人都能改系统 prompt——产品经理加一段、实习生加一段、老板随意加一段。最后没人知道为什么有这些指令,也不敢删。

对策

  • 按层分配所有者(参考 8.8 节跨团队协作模式)
  • 每个模块有 CODEOWNERS
  • 无 owner 的模块定期清理

8.10.1 实测:§8.3 Claude Code 的”模块化 prompt”在源码里就是 getSimple*Section() 函数链

§8.3 给出 11 个模块的表格——把它在 claude-code-main/src/constants/prompts.ts 里精确定位——

getSystemPrompt(tools, model, ...) 函数(line 444、实测)——返回 string[]、每个元素一个模块——核心是 line 564-578 的 array literal,按顺序拼装——

return [
  // === Static content (cacheable) ===
  getSimpleIntroSection(outputStyleConfig),       // ← Identity
  getSimpleSystemSection(),                       // ← System role
  getSimpleDoingTasksSection(),                   // ← Task guidelines
  getActionsSection(),                            // ← 10.8.1 提到的 git 安全协议在这里
  getUsingYourToolsSection(enabledTools),         // ← Tool descriptions
  getSimpleToneAndStyleSection(),                 // ← Tone & Style
  getOutputEfficiencySection(),                   // ← Output Efficiency
  // === BOUNDARY MARKER - DO NOT MOVE OR REMOVE ===
  ...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),
  // === Dynamic content (registry-managed) ===
  ...resolvedDynamicSections,
].filter(s => s !== null)

对照 §8.3 表格、模块对应关系——

章节表格模块源码函数实测位置
IdentitygetSimpleIntroSection()constants/prompts.ts:175
System rolegetSimpleSystemSection()line 186
Task GuidelinesgetSimpleDoingTasksSection()line 199
Tool DescriptionsgetUsingYourToolsSection(enabledTools)line 269
Tone & StylegetSimpleToneAndStyleSection()line 430
Output EfficiencygetOutputEfficiencySection()line 403
Git ProtocolsgetActionsSection()line 255(ch10 §10.8.1 实测的 7 条 git 规则不直接在 prompts.ts 里、而在 BashTool/prompt.ts:88-94——getActionsSection 把 BashTool 的 prompt 引入,是引用而不是 inline)
Dynamic(CLAUDE.md / system-reminder / hooks)getSystemRemindersSection / getHooksSectionconstants/prompts.ts:127-131 + resolvedDynamicSections 异步注入

SYSTEM_PROMPT_DYNAMIC_BOUNDARY 哨兵 在 line 575(实测、与 ch10 §10.8.1 已揭示的 line 114 SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__' 对应)——是 §8.7 “Prompt Caching 对齐:为缓存而设计” 的实际实现:字符串数组里插一个特殊字面量、然后 splitSysPromptPrefix() 按它切前后两段、前段贴 cache_control: ephemeral 后段不贴

两条值得记住的物理事实——

  1. prompts.ts 22 个 function get*Section()(实测 grep)——getSimpleIntroSection / getActionsSection / getUsingYourToolsSection / getSimpleToneAndStyleSection / getOutputEfficiencySection / getMcpInstructions / getKnowledgeCutoff / getShellInfoLine / getFunctionResultClearingSection / getBriefSection / getProactiveSection 等——§8.3 抽象的”模块化 prompt”在源码里就是这 22 个函数 + getSystemPrompt 的 array filter 拼装机制——印证 §8.3 “模块化是真实的、不是图示
  2. getSystemPrompt 函数本身只有约 130 行(line 444-573)——整个 system prompt 的”装配指挥”代码极少——其余都是各 Section 函数的实现——这是 ch10 §10.8.1 测得的”总 prompt 文件 6271 行 / 22 个 Section 函数”在装配层的具体体现:模块化让装配代码极简、复杂度全在各模块内部

串联 ch10 §10.8.1 + §10.4 + §9.4.4——

  • ch10 §10.8.1 揭示 SYSTEM_PROMPT_DYNAMIC_BOUNDARY + <system-reminder> 是动态注入语义根基
  • ch10 §10.4 揭示 prompt 工程量级(22 个 section 函数 / 6271 行)
  • ch09 §9.4.4 揭示 7 条 git 规则在 BashTool/prompt.ts
  • 本节揭示 §8.3 模块化在 getSystemPrompt 的 array literal + boundary marker 协作

——四节合起来给出 Claude Code prompt 架构的完整源码地图:装配(getSystemPrompt 130 行)+ 模块(22 个 Section 函数 6271 行)+ 缓存机制(DYNAMIC_BOUNDARY 字面量切割)+ 工具 prompt 分散(BashTool/PowerShellTool/GrepTool 各自 prompt.ts)。

8.11 本章小结:prompt 架构的七条原则

System prompt 的分层设计,本质上是把软件工程中久经验证的架构原则应用到 prompt 领域:

  1. 分层:每一层有明确的职责和稳定性等级
  2. 模块化:独立的模块可以独立演进、独立测试
  3. 关注点分离:静态指令、动态上下文、用户自定义,三者各有其管理方式
  4. 缓存对齐:Prompt Caching 奖励稳定设计——分层本身就是成本优化
  5. 版本控制:prompt 是代码,不是随手写的便签
  6. 可测试性:行为评估框架替代简单的字符串比对
  7. 责任明确:每一层、每一模块都有所有者

这些不是高深的理论,而是工程常识在新领域的应用。但正是因为 prompt 看起来”只是一段文本”,太多人忽视了对它施加工程纪律的必要性。

核心口号:

Prompts are not strings. They are architecture.

像写代码一样写 prompt,像管代码一样管 prompt。

下一章,我们将深入探讨分层设计中最棘手的子问题:当多层指令发生冲突时,优先级如何裁决? 这就是第 9 章”指令优先级”要解决的核心问题。


延伸阅读:Prompt 架构的演化史

Prompt 作为架构”这个观念、经历了快速演化2022 年、Prompt 还是”随手写的字符串”——开发者在代码里直接拼接字符串、没有版本管理、没有测试2023 年、一些前沿团队开始把 Prompt 从代码里抽出来——放进独立的文件、用模板引擎管理、像对待 SQL 一样对待 Prompt2024 年、完整的”Prompt 工程化”体系浮现——分层设计(system / developer / user 三层)、版本管理(类似 Git)、A/B 测试、性能监控——Prompt 从”字符串”进化为”软件组件2025-2026 年、开始出现”Prompt 架构”这个更高层的概念——讨论如何让多层指令协同、如何处理冲突、如何保持一致性

这段演化史、本质上是”任何复杂软件都要经历的成熟化过程从第一版 SQL(字符串拼接)、到 ORM(结构化查询)、到 migration(版本化 schema)——SQL 用了 30 年走完这段路;Prompt 可能只需要 5 年——因为前人已经趟过很多坑、经验可以直接借鉴作为读者、你赶上 Prompt 架构成熟化的关键期——现在投入、未来几年都能受益

延伸阅读:多层指令的冲突处理

本章反复提到”多层 Prompt 之间可能冲突”——这是 Prompt 架构最棘手的问题之一典型场景——“system prompt 要求简洁、developer prompt 要求详细、user 问’请解释清楚“——LLM 该听谁的?传统软件里、这种冲突通过”作用域 + 优先级”明确解决(比如 CSS 的 specificity);Prompt 里、LLM 是”感性的裁判”、可能给出任何答案

解决冲突的几种思路——第一、“显式声明优先级”(prompt 里写”如果 user 要求和 system 冲突、优先遵守 system”);第二、“分层物理隔离”(system 指令硬编码不暴露给 user、user 无法直接覆盖);第三、“运行时裁决层”(在 LLM 外面加一个 policy checker、审查 LLM 输出是否违反上层指令)每种方法都有优劣——方法一简单但不可靠(LLM 可能不听话)、方法二安全但不灵活(某些场景 user 应该能覆盖)、方法三复杂但最稳健大规模 Agent 系统往往需要三种结合使用《OpenClaw 源码》第 13 章、《LangGraph 源码》第 8 章都讨论过类似的”多层安全”——对比阅读能帮你建立全面认识

延伸阅读:Prompt 的测试与评估

Prompt 工程化的一个核心挑战——如何测试 Prompt传统代码的测试是”输入 X、期望输出 Y、断言相等”——简单直接Prompt 的测试是”输入 X、期望输出满足某些属性”——“满足属性”本身就难定义(用户体验、正确性、一致性都要考虑)目前业界的几种实践——“示例对比测试”(收集典型输入输出对、回归测试 LLM 响应)、“LLM-as-judge”(让另一个 LLM 评判输出质量)、“属性检查”(比如”不应该有中文脏话”、“必须包含引用”、“JSON 格式必须合法”)、“A/B 在线测试”(生产环境两个版本对比真实用户反馈)

这些测试方法各有局限——示例对比可能漏掉新情况、LLM-as-judge 本身可能偏见、属性检查覆盖不全、A/B 测试周期长真实项目往往组合使用——开发阶段用示例对比 + LLM-as-judge 做快速反馈、上线前用属性检查做安全 gate、上线后用 A/B 测试持续优化这种”多种方法组合使用”的策略、是 Prompt 测试的成熟姿态——不依赖任何单一方法

延伸阅读:System Prompt 与模型能力的耦合

一个容易忽略的话题——System Prompt 和 LLM 模型是深度耦合的为 GPT-4 优化的 prompt、换到 Claude 3 上可能效果骤降;为 Claude Opus 写的 prompt、在 Haiku 上可能效果不够原因是不同模型的”指令遵循风格”不同——Claude 更喜欢 XML 标签、GPT 更喜欢 markdown、Llama 对长 system prompt 敏感、Gemini 对示例顺序敏感这种差异、让”模型无关的 prompt”成为一个美好但未实现的理想

面对这种耦合、生产级 Agent 系统的对策——“每个模型维护独立的 prompt 集”——针对 GPT 有一套、针对 Claude 有一套、针对 Llama 有一套——框架层面做路由(根据模型选对应的 prompt)这增加了维护成本、但保证了各模型上的质量LangChain 的 Partner Packages(见《LangChain 源码》第 17 章)、就是在不同提供商之间抽象差异——理论上可以屏蔽 prompt 差异、实际上生产应用还是需要针对各模型定制这是 AI 应用开发里一个长期无解的问题——只能通过工程手段管理

延伸阅读:Prompt 架构对团队协作的影响

Prompt 架构化之后、团队协作模式也会改变以前——“Prompt 是个人创作”——谁写就谁负责、别人看不懂、改起来没头绪Prompt 架构化后——“Prompt 是团队资产”——有分层规范、有命名约定、有版本历史——新成员能快速上手、不同人能协作修改

这种变化、和 SQL 在数据库领域的变化类似——早期 SQL 是”某个 DBA 的私房菜”、现在 SQL 是”团队共享的数据资产要完成这种变化、需要几件事——团队里有 prompt 架构师(设计整体分层)、有 prompt 工程师(实现具体 prompt)、有 prompt reviewer(审查 prompt 变更)——就像代码有 architect、engineer、reviewer 一样这些角色在小团队里可能由同一个人承担、大团队里会专业化分工——和软件工程的成熟路径完全一致——历史不会重演、但会押韵

延伸阅读:Prompt 与组织知识的关系

一个组织的 prompt 库、是这个组织的”数字化知识它反映了组织对”什么问题该怎么回答”的集体智慧——法务团队的 prompt 沉淀了他们对合规的理解、客服团队的 prompt 沉淀了他们对用户沟通的技巧、技术团队的 prompt 沉淀了他们对代码审查的标准这些 prompt 加在一起、构成组织独特的”集体知识资产”——这是组织最难被竞争对手复制的部分

意识到这一点、Prompt 就不只是”工具”、而是”战略资产”——需要像对待机密文档一样对待它、需要持续投资维护它、需要让组织的最优秀人才参与共建聪明的公司、已经在把”最佳实践”编码为 prompt、让每个员工都能借助 AI 享受资深专家的判断水平——这是 AI 时代”组织能力放大”的具体落地OpenClaw 的 Skill 系统(见《OpenClaw 源码》第 16 章)、就是在技术层面支持这种”组织知识资产化”——值得每个企业读者深入研究

延伸阅读:Prompt 架构的未来

展望未来——Prompt 架构会走向何方?几个可能方向第一、“可视化 IDE”——像 VSCode 之于代码一样、出现专门的 Prompt IDE——带语法高亮、自动补全、跨 prompt 跳转、实时 preview第二、“形式化规约”——用形式化语言描述 prompt 应该满足的属性、用自动工具验证第三、“Prompt-Code 一体化”——Prompt 和代码混合编辑、互相引用、一起部署第四、“自动优化”——给定评估函数、让工具自动搜索最优 prompt(DSPy 已经在做)

这些方向的哪个会成真、现在还难说但可以确定的是——Prompt 会越来越像传统软件组件、工程化程度越来越高、个人手工写 prompt 的时代正在过去、工具辅助写 prompt 的时代正在到来作为 AI 工程师、你需要提前为这个趋势做好准备——投资于”prompt 工程化思维”、而不是停留在”字符串拼写技巧”——这是决定你在 AI 时代走多远的关键分水岭

延伸阅读:Prompt 的”文化负载

一个经常被忽略的话题——Prompt 是有文化负载的英文 prompt 在英文 LLM 上效果最好、但在处理中文输入时可能效果打折;中文 prompt 在中文场景效果好、但某些精确指令用英文更清晰不同文化背景的 user、对同一个 LLM 响应的反应可能完全不同——直接的英美风格可能让东亚用户觉得”不够礼貌”、委婉的东亚风格可能让西方用户觉得”不够直接

面对这种文化差异、设计跨地区 Agent 系统时需要额外考虑”本地化 prompt”——不只是翻译、而是根据目标文化调整语气、示例、判断标准这让”全球化 Agent”成为一个比”全球化普通软件”更复杂的工程——本地化的维度多了好几个这也是 AI 时代产品经理的新挑战——需要懂技术也懂文化、能把不同文化的沟通习惯编码到 prompt 里——这是一种全新的产品能力

延伸阅读:Prompt 工程的职业前景

Prompt 工程师”作为一个新兴职业、在 2023-2024 年一度被热炒——有公司给出 30 万美元年薪招”Prompt Engineer到 2025-2026 年、这个职业的形态开始分化——纯粹的”写 prompt”逐渐被工具化(DSPy、自动优化)、但”懂 prompt 架构、懂 LLM 心理、懂业务”的综合型人才依然稀缺未来几年、可能不会再有”Prompt Engineer”这个专门职位、但”懂 Prompt 的产品工程师/AI 工程师”会成为主流

这种演化、和历史上的一些专门技能一致——早期”网页设计师”、“SEO 专家”、“DevOps 工程师”都经历过”先独立成职位、再融入其他职位”的过程核心技能不会消失、但会融入更广的职责范围作为工程师、与其追逐”Prompt Engineer”这个标签、不如把 prompt 工程作为自己综合能力的一部分——和架构能力、编程能力、产品能力一起、构成 AI 时代的复合型工程师——这种定位、比专业化的单一技能更抗风险

延伸阅读:为 Prompt 写单元测试

给 prompt 写单元测试、是 2024 年以来兴起的实践典型做法——用 pytest 这样的测试框架、把 prompt 当作被测对象、输入一组测试用例、验证 LLM 响应满足某些断言断言的写法比较特殊——不能用严格相等(LLM 输出有随机性)、要用属性检查(包含关键词、长度在范围内、结构符合 schema)pytest 有专门的插件 pytest-llm、promptfoo 等工具——让 prompt 测试接近传统单元测试的体验

这种”把 prompt 纳入 CI”的实践、能显著提升 prompt 质量——每次修改 prompt 都自动跑测试、有回归立刻发现代价是测试成本(每次 CI 都要调 LLM、成本不菲)——折中方案是”关键 prompt 跑真实 LLM、大部分 prompt 跑 mock 或 LLM-as-judge这种”测试分层”的思路、和传统软件的测试金字塔(unit → integration → e2e)异曲同工——Prompt 测试也会形成自己的金字塔