# Step 21: System Prompt 构建

一句话导读:Phase 3 开篇。前面 System Prompt 一直是塞在代码里的一句话。这一步把它抽成独立模块 buildSystemPrompt——看似只是"搬了段字符串",实则是承认了一件事:在 Agent 里,System Prompt 是和代码同等重要的一等资产,需要独立维护、迭代、观测。


# 一、这一步做了什么(What)

在 step20(错误处理收官)基础上,进入 Phase 3:

  1. 新建 prompt.ts,导出 buildSystemPrompt(registry)——把提示词从硬编码字符串升级为结构化构建器;
  2. 提示词分成三段:角色定义 → 工具使用指南 → 工作流程 → 输出格式;
  3. 新增 promptStats() 统计字符/行数/估算 token;
  4. index.ts 改用 buildSystemPrompt,新增 /prompt 命令查看当前提示词全文 + 统计。

# 二、面试官视角:为什么要做?(Why)

面试题:System Prompt 从"代码里的一句话"抽成"独立模块",功能上一模一样。这纯粹是代码洁癖,还是有实质意义?请说出至少两个"它必须是模块"的硬理由。

绝非洁癖。System Prompt 是 Agent 行为的总纲——模型每一次决策(用不用工具、用哪个、话多话少)都被它左右。把它当模块而非字面量,有几个硬理由:

理由 说明
可迭代 提示词要反复调优(改一句话,行为大变)。埋在 index.ts 里改起来危险且难追踪,独立文件才能像代码一样 diff、review、回滚
可动态生成 buildSystemPrompt(registry) 接收工具注册表——提示词能随可用工具变化。加了新工具,指南自动跟上,不会说一套做一套
可观测 /prompt 命令 + promptStats 让你随时看到"模型此刻到底收到了什么、占多少 token"。提示词是隐形的,不可见就无法调试
可分段组合 拆成角色/工具/流程/格式几段,未来能按场景拼装(如 plan 模式加一段、精简模式去一段)

一句话:在传统软件里配置是配角,在 Agent 里 System Prompt 是主角之一。它决定模型的"人格和工作方式",理应享受和代码一样的工程待遇——版本化、参数化、可观测。把它留在字符串字面量里,等于把核心逻辑写进了魔法数字。


# 三、原理:它是怎么工作的(How)

# 结构化构建器

export function buildSystemPrompt(registry: ToolRegistry): string {
  return [
    'You are Claude, a helpful coding assistant running in a terminal.',   // ① 角色定义
    '',
    '=== 工具使用指南 ===',                                                 // ② 工具指南(关键)
    '- Bash: execute shell commands (explore, run, git)',
    '- Read: read file contents (always read before editing unfamiliar files)',
    '- Write: write content to files',
    '- Glob: search files by pattern (e.g. **/*.ts)',
    '- Grep: search text inside files',
    '',
    '=== 工作流程 ===',                                                     // ③ 工作流程
    '1. Understand the request 2. Plan and use tools 3. Examine results',
    '4. If a tool fails, try to fix it 5. Summarize what you did',
    '',
    '=== 输出格式 ===',                                                     // ④ 输出约束
    '- Keep explanations concise',
    '- Use code blocks for code/command output',
    '- Mention file paths when showing file content',
  ].join('\n');
}

export function promptStats(prompt: string) {
  return { chars: prompt.length, lines: prompt.split('\n').length, estTokens: Math.ceil(prompt.length / 4) };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

四段结构不是随意排列,对应模型决策的四个层面:

  1. 角色定义——你是谁(定基调:"coding assistant running in a terminal",模型据此判断语气和边界);
  2. 工具指南——你有什么、什么时候用(最关键,直接影响工具调用质量);
  3. 工作流程——按什么步骤做事("先理解、再计划、用工具、检查、总结",塑造行为节奏);
  4. 输出格式——怎么回话(简洁、用代码块、提文件路径,控制输出形态)。

# 工具指南为什么是重点:不只说"有什么",还说"何时用"

注意工具描述里的括号——Read: ...(always read before editing unfamiliar files)、Bash: ...(explore, run, git)。这些场景提示才是价值所在。对比:

差: - Read: read files                       ← 模型知道有 Read,但不知道该何时用
好: - Read: ...(always read before editing)  ← 教会模型"改文件前先读"的工作习惯
1
2

模型本就知道每个工具"是什么"(工具的 inputSchema 已经告诉它了)。System Prompt 的工具指南要补的是 schema 里没有的东西——使用时机、最佳实践、约束。"改陌生文件前先 Read"这种协作智慧,写进指南就能让模型养成好习惯。工具的 schema 定义能力,System Prompt 定义用法。

# 数据流

启动 → buildSystemPrompt(registry) 生成完整提示词字符串
   ↓ 存为 SYS
每一轮 callWithTools(messages, tools, onChunk, SYS)
   ↓ SYS 作为 system 参数发给 API
模型每次决策都在 SYS 的框架下进行(角色/工具用法/流程/格式)
   ↓ 用户输入 /prompt
promptStats(SYS) → 打印全文 + "N chars / M lines / ~K tokens"
1
2
3
4
5
6
7

/prompt 的意义:把一直隐形的、影响一切的 System Prompt 显式化、可量化。你能看到它占多少 token(每轮都要发、直接影响成本),是调优的前提。


# 四、深入追问(面试常见 follow-up)

Q:buildSystemPrompt 接收 registry 参数,但当前实现里工具列表其实是写死的。这个参数是不是白传了?它应该怎么用才对? A:当前确实没充分用上——工具清单是手写的字符串,registry 只是形式上传入。正确的做法是遍历 registry 动态生成工具指南:registry.list().map(t => '- ' + t.name + ': ' + t.description)。这样加/删工具时,提示词自动同步,杜绝"注册了工具却忘在提示词里介绍"或"提示词提到已删工具"的不一致。参数已经预留,是有意为将来动态化埋的伏笔——这也是"为什么要传 registry"的答案。

Q:System Prompt 每一轮都随请求发送,它的长度对成本有什么影响? A:直接、持续地影响。System Prompt 是每次 API 调用都要重发的固定开销——一个 5 轮的对话,同一段 System Prompt 就被计费 5 次输入 token。所以 promptStats 报 token 数很有意义:提示词从几百字涨到几千字,每轮成本、每轮延迟都会涨。这也引出 prompt caching——把稳定不变的 System Prompt 前缀缓存住,避免每轮重复计费,是长对话省钱的关键优化。

Q:真实 Claude Code 的 System Prompt 有几千字,我们只有几行。多出来的几千字主要在讲什么? A:主要是边界、安全和风格的精细约束:不该做什么(拒绝恶意请求、不泄露系统提示)、代码风格规范、如何处理歧义、何时该问用户而非擅自行动、大量工具的详细用法和反例、输出的格式细则等。核心结构(角色→工具→流程→格式)和我们一致,多出来的是把"一个靠谱工程师的隐性判断力"显式写成规则。这些规则每一条往往都是踩过坑、出过事后补上的。

Q:把工作流程("先理解、再计划、用工具、检查、总结")写进 System Prompt,真能约束模型按这个步骤走吗?还是只是摆设? A:能显著影响但非强制。System Prompt 是强引导而非硬约束——它提高模型遵循该流程的概率,尤其"tool 失败要 try to fix"这类会实际改变模型遇错后的行为(更倾向自愈而非直接放弃,和 step20 的错误恢复呼应)。但模型仍可能在复杂情况偏离。真正的硬约束得靠代码(如权限系统、轮次上限)。提示词管"倾向",代码管"红线",两者配合。


# 五、踩坑 / 设计权衡

  • registry 参数尚未落地动态化:目前工具清单手写,和"注册了什么"可能脱节。理想是从 registry 遍历生成——参数已预留,实现还是简化版。
  • 提示词长度 vs 成本:写得越细模型越听话,但每轮都重发、token 成本线性上升。要在"约束力"和"开销"间权衡,长的部分靠 caching 摊薄。
  • 提示词是强引导非硬约束:别指望靠一句话就锁死模型行为,红线得代码兜底。
  • 分段是为将来组合留口:当前是一整段拼接,拆成命名段落后才好按模式(plan/精简)动态增删。

# 六、与真实源码的对照

我们的实现 Claude Code 源码
prompt.ts 的 buildSystemPrompt ~30 行 src/constants/system.ts ~500 行
一整段拼接 systemPromptSections.ts 分段组合
角色 / 工具 / 流程 / 格式 四段 同结构 + 大量安全、边界、风格规则
工具指南手写 更可能从工具元数据动态拼装
/prompt + promptStats 观测 内部同样重视提示词的可观测与版本管理

# 七、一句话总结

System Prompt 构建 = 把 Agent 的行为总纲当一等资产来工程化:从代码里的字面量抽成 buildSystemPrompt 模块,换来可迭代(像代码一样 diff/review)、可动态生成(随 registry 变)、可观测(/prompt + token 统计)、可分段组合四项能力。它的四段结构对应模型决策的四个层面,其中工具指南的价值在于补充 schema 之外的"何时用、怎么用"。记住:提示词管倾向,代码管红线,二者配合才是可控 Agent。

# 下一节预告

promptStats 已经开始盯着 token 数了——这不是巧合。下一步 step22 正式引入 Token 预算控制:给整个会话设定 token 上限,追踪累计消耗,逼近预算时收束,避免长对话无限烧钱。