System Prompt 如何设计
请使用电脑访问
https://myblog-ad4.pages.dev理论学习:System Prompt 如何设计?
同一台发动机,为什么跑出两种车?
上一章我们把 Agent Loop 跑起来了。MewCode 有了一颗心脏:while 循环驱动 LLM 反复思考和行动,直到任务完成。
但你有没有发现一个问题?第四章里的 buildSystemPrompt() 只有三行:
你是 MewCode,一个终端环境中的 AI 编程助手。 |
你拿这三行 prompt 让 MewCode 去做一件真实的事,比如「帮我修一下 auth.py 里的空值引用 bug」,看看会发生什么。
MewCode A,用的是这三行 prompt。它会干什么?上来 ReadFile 把整个 auth.py 读一遍。然后 WriteFile 把整个文件重写了一遍,顺便给每个函数加了注释,还重命名了三个变量。接着它写了 500 字的总结,解释自己做了什么,为什么做这个选择,还附带了三条改进建议。最后你发现,bug 确实修了,但代码评审的同事要花半小时看它到底改了什么。
MewCode B,同一个模型,同一个 Agent Loop,同一套工具,但 System Prompt 有几十行指令。它的行为完全不同:先 Grep 搜 空值引用 相关的报告,定位到具体函数。ReadFile 只读了那个函数附近 30 行。EditFile 改了 3 行代码。跑了一遍测试确认通过。最后一句话:「auth.py:47 的 user.profile 在未登录时为 None,已加空值检查。测试通过。」
这就好比同一台发动机装在两辆车上。一辆配了「稳健驾驶手册」,一辆配了「暴走操作指南」。发动机一模一样,工具一模一样,但上路表现天差地别。System Prompt 就是 Agent 的驾驶手册。
第四章用三行 prompt 是为了让你先跑起来。但要让 MewCode 真正好用,三行远远不够。这一章要把这三行展开成一套完整的指令体系,让 MewCode 从「能干活」进化到「干得好」。
三行 prompt 为什么不够
在展开之前,我们先搞清楚:三行 prompt 到底差在哪?
LLM 是一个概率模型。你给它的指令越模糊,它的行为空间就越大,输出就越不可预测。三行 prompt 只告诉了模型「你是谁」和「你擅长什么」,但没告诉它:
- 怎么跟用户交互:该简洁还是详细?该先确认还是直接动手?
- 怎么用工具:用 ReadFile 还是用
cat?多个工具该串行还是并行? - 代码该写成什么样:该不该加注释?该不该顺便重构?
- 什么事不能做:能不能引入安全漏洞?能不能猜 URL?
- 不同任务该怎么处理:修 bug 和加功能的策略一样吗?
没有这些指令,模型就按照它自己的「默认倾向」来。而模型的默认倾向,本质上是一种泛化的「好助手」行为模式,对任何特定场景都不是最优的。典型表现是:输出偏长偏全面、倾向于展示更多内容、喜欢顺手做额外的事情。
但这些默认倾向在 Agent 场景下全是反模式。**Agent 需要的是精准、克制、可预测。**你不需要它给你写 500 字总结,你需要它改完 3 行代码说一句话。你不需要它顺便重构三个函数,你需要它只做你让它做的事。
System Prompt 的本质不是教模型新能力,而是约束它的默认倾向,限制它的行为范围,让它按你想要的方式工作。
一个生产级 System Prompt 长什么样
那到底该写多少?要写哪些东西?
我们来设计 MewCode 的完整 System Prompt。参考 Claude Code 等生产级 Agent 的做法,一个好的 System Prompt 可以拆成七个模块,每个模块解决一个具体问题。
角色设定
角色设定解决的问题是:模型在做决策时,以什么身份自居?
你是 MewCode,一个终端环境中的 AI 编程助手。 |
只有两句话,但每个词都不是随便写的。
「终端环境中的」不是废话。这四个字帮助模型理解输出环境的限制,避免生成不适合终端渲染的内容。
「AI 编程助手」比「AI 助手」更好。后者太宽泛,模型会觉得自己什么都应该帮忙,比如写诗、做翻译、聊天。前者把范围锁定在编程任务上。当用户问一个跟编程无关的问题时,模型的行为会更克制。
「修 bug、添加功能、重构代码、解释代码」列出了四种核心任务。这不是在告诉模型它「能」做这些事,它本来就能,而是在告诉它这些事的优先级最高。当任务模糊时,模型会倾向于把用户需求往这四个方向上靠。
角色设定越模糊,模型越倾向于「什么都试试」。角色越聚焦,模型的行为越可预测。
行为准则
行为准则解决的问题是:模型该怎么跟用户沟通?
这个模块通常包含这些内容:
- 回复尽量简短。一个简单问题配一个直接回答,不要分段加标题。 |
为什么需要这些?因为 LLM 的默认输出风格是「尽可能详细和完整」。在对话场景下这是优点,但在 Agent 场景下是缺点。
想象一下,MewCode 帮你改了一个 bug,然后输出了 20 行解释、5 条改进建议、3 个注意事项。你还得花时间读这些输出,找到真正重要的信息:改了哪几行?测试过了没有?这些被淹没在 Agent 的「热心肠」里。
更重要的是 token 成本。Agent 的每一个字都是 output token,都要花钱。如果你的 Agent 每次都输出 500 字总结,一天下来多烧的 token 费用足够再跑几十轮有意义的工具调用了。
「探索性问题回 2-3 句建议,不要直接动手」也很关键。没有这条规则,用户说「这个 API 的错误处理怎么办?」,Agent 可能直接把整个错误处理重写了。但用户可能只是想听听建议,还没决定要不要改。区分「问」和「做」是 Agent 行为准则里最重要的一条。
工具使用指南
工具使用指南解决的问题是:模型该怎么选工具、怎么用工具?
你可能觉得这不用教吧?工具的 JSON Schema 里已经定义了参数和说明,模型看了就知道怎么用。
不够。远远不够。
模型的训练数据里充满了用 cat、head、tail 读文件的例子。如果你不明确告诉它「用 ReadFile 工具而不是 Bash cat」,它大概率会选 Bash。不是因为它不知道 ReadFile 的存在,而是 cat 在训练数据里出现的频率太高了,模型的默认偏好就是用 cat。
- 优先用专用工具而不是 Bash。读文件用 ReadFile,别用 cat。 |
为什么要强调「并行执行」?因为模型的默认行为是一个一个调工具。它想看两个文件,会先 ReadFile 第一个,等结果回来,再 ReadFile 第二个。两轮 API 调用。如果你告诉它「独立的工具调用放在同一轮」,它就会在一次响应里同时请求两个 ReadFile,你并发执行完一次性返回。少了一轮 API 往返,不但省时间还省 token。
「编辑文件之前必须先读一遍」则是一条防错规则。EditFile 需要一个 old_string 参数做精确匹配替换。如果模型没读过文件就猜 old_string 的内容,大概率匹配不上,工具会报错,白白浪费一轮循环。
工具使用指南不是在教模型新技能。它是在纠正模型的默认偏好,把「能用」变成「用得好」。
代码质量规范
代码质量规范解决的问题是:模型写出来的代码应该长什么样?
这是 Agent 场景下最容易翻车的地方。LLM 写代码有一个根深蒂固的倾向:**展示自己很聪明。**加注释证明自己理解了代码,加抽象层证明自己有设计能力,加错误处理证明自己考虑周全。在面试或者代码教程里这些都是加分项,但在 Agent 干活的场景下全是噪音。
- 不要添加超出任务需求的功能、抽象或重构。 |
为什么不写注释?因为 Agent 写的注释几乎都是在描述「what」而不是「why」。// 检查用户是否为空 这种注释比没有注释更差,它占了一行空间,说了一句代码本身已经说了的话。当你的 Agent 每次改代码都留下一堆这种注释,代码库很快就变成注释垃圾场。
「三行相似代码比一个提前抽象好」这条规则直击 LLM 的另一个坏习惯。模型看到两三段结构相似的代码,本能反应就是抽取一个公共函数。但在很多场景下,这些代码只是恰好长得像,未来演化方向完全不同。Agent 没有这个上下文判断力,所以不如告诉它:别抽象,除非用户明确让你抽。
安全边界
安全边界解决的问题是:在 Prompt 层面划出模型不能碰的红线。
- 不要引入安全漏洞:命令注入、XSS、SQL 注入等 OWASP Top 10。 |
你可能会问:下一章不是要做权限系统吗?为什么这里还要在 Prompt 里写安全规则?
因为这两层解决的是不同层面的问题。
**Prompt 里的安全边界是「软约束」。**模型大多数时候会遵守,但在某些边界条件下可能被绕过。比如一个精心构造的 prompt 注入可能让模型忘记「不要跳过 hook」这条规则。
**权限系统是「硬约束」。**代码层面强制执行,模型无论怎么想都绕不过去。rm -rf / 会被黑名单拦截,不管 System Prompt 里有没有写「不要删根目录」。
两层互补。Prompt 安全边界在前面「劝」模型不做危险的事,权限系统在后面「拦」模型真的要做危险的事。大多数情况下前面那层就够了,模型看到指令就知道不该这么做。但安全不能靠大多数情况,所以后面还要有一道硬防线。
任务执行模式
任务执行模式解决的问题是:面对不同类型的任务,Agent 的策略应该有什么不同?
模型不会自己区分任务类型。用户说「帮我改一下这个函数」,模型不会自动判断这是 bug 修复还是功能迭代还是重构。如果你不给指引,它就用同一套策略处理所有任务,往往是最激进的那种:把函数重写一遍。
- Bug 修复:先定位、最小修改、验证。不要顺便重构。 |
「先问」可能是最重要的一条。没有这条规则,模型在面对模糊需求时会自己做假设然后直接干。有了这条规则,它会先停下来问用户。Agent 在这个环节多花 30 秒确认需求,能省下 10 分钟的返工。
输出风格
输出风格解决的问题是:模型的回复应该长什么样?
- 引用代码时用 file_path:line_number 格式,让用户能直接跳转。 |
这些看似细节,但累积效果很大。main.py:47 比「在 main.py 文件的第 47 行」更简洁,也更方便在 IDE 里跳转。不用 emoji 看似小事,但如果你的 Agent 动不动就「修复完成啦 😊✨」,专业感会打折扣。
放在一起看:完整的 System Prompt 长什么样
七个模块拆开讲完了,现在把它们拼在一起,看看一个完整的 System Prompt 用伪代码怎么构建:
function buildSystemPrompt(config): |
每个常量背后就是前面讲的一段文本。这些文本不是一次性写好的。它们是经过几十轮迭代、几百个边界 case 打磨出来的。每次你发现 Agent 做了一件不该做的事,就回去看 System Prompt 里有没有覆盖这个场景。没有就加一条。有但模型没遵守就把措辞改得更明确。这个过程永远不会结束,就像软件永远有 bug 要修。
System Prompt 不是写完就完了的文档。它是一个活的、持续演化的系统。
Prompt 组装管线:七个来源,三个字段
到目前为止我们只讲了 System Prompt 本身。但 Agent 每次调 API 时发给模型的信息远不止 System Prompt。
回忆第二章讲的 Claude API 结构:每次请求有三个字段,system、messages、tools。Agent 需要从多个来源收集信息,放进正确的字段。
七个信息来源
- 静态 System Prompt — 角色设定、行为准则、安全边界等七个模块。本章的核心内容。
- 环境上下文 — 工作目录、操作系统、当前时间、Git 状态。第四章已经实现了基础版。
- 工具描述 — 每个工具的 JSON Schema 和 description 字段。第三章已经定义了。
- 项目指令文件 — MEWCODE.md,用户为特定项目写的 Agent 指令。后续章节会详细讲。
- 自动记忆 — Agent 自动提取的用户偏好和项目知识。后续章节会详细讲。
- System Reminder — 动态注入的上下文,比如 MCP Server 的使用说明。本章会讲。
- 对话历史 — 之前的 user / assistant / tool 消息。第二章已经实现了。
你不需要现在就实现所有来源。4 和 5 是后续章节的内容。但你需要从一开始就设计好组装管线的结构,让后续的来源能顺畅地接入。
放进哪个字段?
每类信息该放 system 还是 messages 还是 tools?这不是随便放的,背后有明确的考量。
| 信息来源 | 字段 | 原因 |
|---|---|---|
| 静态 System Prompt | system | 全局指令,每轮都生效,内容稳定可被缓存 |
| 环境上下文 | system | 每次会话确定后不再变化,可利用缓存分层 |
| 工具描述 | tools | API 规范要求 |
| MEWCODE.md | messages | 内容可能很长,放 system 会稀释注意力 |
| 自动记忆 | messages | 动态内容,每次不同 |
| System Reminder | messages | 需要在特定时机注入 |
| 对话历史 | messages | API 规范要求 |
你可能会问:为什么不把所有东西都塞进 system?反正 system 字段的优先级最高,模型最听 system 里的话。
三个原因。
第一,Prompt Cache。几乎所有主流 LLM API 都支持 Prompt Cache 机制:如果 system 字段的内容跟上一次请求完全一样,API 会复用缓存,大幅降低 input token 的计费。Agent Loop 每轮都要调 API,如果 system 内容稳定不变,每次都能命中缓存,成本可以降低 90%。但只要 system 里有一个字符变了,缓存就失效。所以稳定的内容放 system,变化的内容放 messages。MEWCODE.md、记忆这些动态内容放进 system 会频繁让缓存失效。环境上下文虽然每次会话不同,但在一次会话内是稳定的,可以利用缓存分层(全局缓存 vs 会话级缓存)放在 system 里管理。
很多 Prompt Cache 是“前缀匹配”。也就是说,只要前面部分完全一样,就能复用。
缓存分层就是:把 Agent Prompt 按稳定程度拆开,静态规则放最前面长期缓存,会话环境放中间做会话级缓存,每轮变化的内容放最后,避免因为一点动态内容导致整个 prompt cache 失效。
第二,注意力稀释。system 字段放太多内容会稀释模型对每条指令的注意力。如果 system prompt 有一万字,中间夹着一条「不要用 emoji」,模型可能注意不到。关键指令放在前面、放在后面,效果远好于放在中间。这就是 LLM 研究中常说的 Lost in the Middle 现象:输入开头和结尾得到的注意力最多,中间最容易被忽略。
**第三,可压缩性。**放在 messages 里的内容,后续可以被上下文压缩机制处理(第八章会讲)。如果一段 MEWCODE.md 的内容在对话进行到后期已经不再需要,压缩器可以把它删掉或缩减。但 system 字段的内容不受压缩影响,每次都完整发送。
组装伪代码
把上面的规则落成代码:
function assembleAPIPayload(config, conversationHistory): |
这个函数就是 Prompt 组装管线的核心。每次 Agent Loop 调 API 之前,都先跑一遍这个函数,拿到完整的 payload 再发送。
注意动态上下文放在对话历史的后面。这是有意为之的。动态上下文包含最新的系统状态(比如刚连上的 MCP Server),放在最后面能利用近因效应,让模型更容易注意到。
工具描述也是 Prompt 工程
说完了 system 和 messages,别忘了第三个字段:tools。
第三章讲工具系统时,你给每个工具写了一个 description 字段。当时可能随手写了一句话了事。但工具描述对模型的行为影响之大,可能超出你的预期。
**工具描述不是注释。它是 prompt 的一部分。**模型根据 description 做两个关键决策:什么时候使用这个工具,怎么用这个工具。
来对比两个 ReadFile 工具的描述:
差: |
差的描述只说了「是什么」,模型不知道什么时候该用它而不是 cat。好的描述还告诉了模型:路径格式要求、大文件处理策略、跟 Bash 的优先级关系、跟 EditFile 的配合要求。
你可能发现了,好的工具描述跟 System Prompt 里的「工具使用指南」有重叠。「优先用 ReadFile 而不是 Bash cat」在 System Prompt 里说了,在工具描述里又说了一遍。这是故意的。
**关键规则在两处都说,模型遵守的概率远高于只说一处。**这不是写作的冗余,而是 prompt 工程的双重强化。System Prompt 是全局指令,模型在做总体决策时参考。工具描述是局部指令,模型在选择具体时参考。两处呼应,模型从两个角度都被「提醒」了同一条规则。
动态指令注入:system-reminder
到目前为止,我们聊的信息来源都有一个特点:要么在会话开始时就确定了(System Prompt、环境上下文),要么随对话逐步产生(对话历史)。
但有一类信息不属于这两种:它在会话进行过程中突然出现,需要立刻让模型知道。
举个例子。用户在对话进行到一半时,通过配置连接了一个 Grafana MCP Server。这个 Server 提供了二十几个新工具,每个工具有自己的使用说明。Agent 需要立刻知道这些工具的存在和用法。但你不能改 System Prompt,改了就让 Prompt Cache 失效了。你也不能假装这是用户说的话,放进一条 user 消息里,模型可能会「回复」它。
这就是 system-reminder 要解决的问题。
什么是 system-reminder
system-reminder 是一种特殊的消息标记。它放在 messages 字段里,但用 XML 标签包裹,告诉模型「这不是用户说的话,而是系统给你的补充指令」。
<system-reminder> |
模型看到 <system-reminder> 标签,就知道这段内容要当指令对待,而不是当用户对话对待。它不会去「回复」这段话,而是把它纳入自己的工作上下文。
典型使用场景
**MCP Server 上线或下线。**用户可能随时连接或断开外部工具服务。每次变化时,通过 system-reminder 更新可用工具列表和使用说明。
**可用 Skill 列表。**用户安装了新的 Skill 包,或者卸载了旧的。system-reminder 通知模型哪些 Skill 可以调用。
**Agent 类型声明。**当系统支持子 Agent 时(第十三章会讲),通过 system-reminder 告诉主 Agent 可以派遣哪几种子 Agent。
**温和提醒。**比如「任务工具最近没用过,如果你在做复杂任务,考虑用 TaskCreate 跟踪进度」。这种提醒放在 system-reminder 里,模型会参考但不会当成硬性要求。
**MEWCODE.md 内容注入。**项目指令文件的内容也通过 system-reminder 注入到 messages 中。这样它不影响 system 字段的缓存,又能让模型在对话过程中随时参考。
为什么不直接改 system prompt?
这个问题在组装管线那一节已经提过了,但值得再强调一遍:改 system prompt 会让 Prompt Cache 失效。
Agent Loop 的每一轮都要调 API。假设一个任务跑了 20 轮,如果 system prompt 每轮都不变,19 轮都能命中缓存,只有第一轮是全价。但如果你因为连了一个 MCP Server 就改了 system prompt,从那一轮开始缓存失效,后面每一轮都是全价。
成本差距可能是 10 倍。对于长时间运行的 Agent 任务,这不是小数字。
system-reminder 的实现
实现很简单。就是一个带 XML 标签的 user 消息:
function systemReminder(content): |
一些设计约束:
- **不要在一条消息里混合 system-reminder 和真正的用户输入。**分开放,更清晰。
- **如果 system-reminder 的内容可能包含来自外部的文本(比如 MCP Server 返回的说明),要警惕 prompt 注入。**外部文本可能试图伪装成指令。
常见陷阱和应对策略
System Prompt 写好了,组装管线搭好了,是不是就完事了?远没有。Prompt 工程是一个持续迭代的过程,以下是你一定会踩到的坑。
Prompt 太长,中间的指令被忽略
LLM 的注意力不是均匀分布的。输入开头和结尾的内容得到的注意力最多,中间的最容易被忽略。如果你的 System Prompt 有 5000 字,夹在中间的「不要用 emoji」很可能被模型无视。
应对:把最关键的指令放在开头或结尾。用 markdown 标题(##、###)分段,帮助模型定位内容。如果某条规则总是被忽略,不是多写几遍就能解决的。试着把它移到更显眼的位置,或者在工具描述里双重强化。
指令冲突
System Prompt 说「默认不写注释」,但项目的 MEWCODE.md 说「所有公开函数必须写 docstring」。两条指令矛盾了,模型该听谁的?
如果你不明确优先级规则,模型会随机挑一个执行。有时候听 System Prompt,有时候听 MEWCODE.md,行为不可预测。
应对:在 System Prompt 中明确声明优先级。比如:
当项目指令文件(MEWCODE.md)与本 System Prompt 的默认行为冲突时, |
这样模型就知道:System Prompt 里的代码规范是默认值,MEWCODE.md 可以覆盖它。就像 CSS 的 !important,你得告诉模型谁能覆盖谁。
负面指令堆砌
「不要写注释。不要加 emoji。不要过度设计。不要添加多余功能。不要猜 URL。不要……」
一连串的「不要」会产生一个反直觉的效果:模型反而更容易触发这些行为。这跟「不要想大象」是一个道理,你越强调不要做什么,模型越倾向于把注意力放在这个事情上。
应对:把负面指令改写成正面指令。
| 负面指令 | 正面指令 |
|---|---|
| 不要写注释 | 默认不写注释。只在 why 不明显时加一行 |
| 不要过度设计 | 只实现任务要求的功能 |
| 不要写长总结 | 结束时一两句话总结 |
| 不要猜 URL | 只使用用户提供的 URL 或本地文件中的 URL |
正面指令告诉模型该做什么,而不是让它去猜「什么算过度」。
只在一处说
你在 System Prompt 里写了「用 ReadFile 不要用 cat」。但模型还是时不时用 cat。不是模型故意忽略你,而是在选择工具的那个决策点上,模型参考的是工具描述,不一定会回头看 System Prompt。
应对:关键规则双重强化:System Prompt 里说一遍,对应工具的 description 里再说一遍。前面已经详细讲过了。
Prompt 与成本的关系
这一章反复提到了 token 成本。在结束之前,把 Prompt 设计对成本的影响做一个系统总结。
Agent Loop 每一轮都要调 API。每次调 API 都要发送 system + messages + tools。其中 system 和 tools 的内容每轮几乎不变,messages 随对话增长。
成本公式简化来看:
单轮成本 ≈ input_tokens × input_price + output_tokens × output_price |
Prompt 设计在三个地方影响成本:
第一,System Prompt 的长度。每一轮都完整发送。5000 token 的 system prompt 比 500 token 的贵 10 倍。不是贵一次,是每一轮都贵 10 倍。但 Prompt Cache 可以大幅缓解:如果 system 内容不变,缓存命中后 input 成本降低到原来的 10%。所以关键不是 system prompt 短不短,而是是否在多轮调用间保持不变。
**第二,Output 的长度。**行为准则里要求「结束时一两句话总结」直接影响每轮的 output token。如果模型每轮都输出 500 字解释,20 轮下来就是 10000 字的 output token。如果控制在每轮 50 字,总量降到 1000 字,output 成本降 10 倍。
**第三,工具调用的效率。**工具使用指南里要求「多个独立调用并行执行」,减少了 API 往返次数。每少一轮,就少一次完整的 input token 传输。如果一个任务从 20 轮降到 15 轮,input 成本降 25%。
Prompt 设计不只是在设计行为,也是在设计成本结构。
本章小结
这一章做了一件事:把第四章的三行 buildSystemPrompt() 展开成一套完整的 Prompt 工程体系。
System Prompt 由七个模块组成:角色设定、行为准则、工具使用指南、代码质量规范、安全边界、任务执行模式、输出风格。每个模块解决一个具体问题,每条指令都不是随便写的,而是在纠正模型的某个默认倾向。
Prompt 组装管线从七个来源收集信息,通过三个字段(system / messages / tools)发给模型。稳定指令和环境上下文放 system 以利用 Prompt Cache,动态内容放 messages 以保护缓存,工具描述放 tools。动态指令通过 system-reminder 注入 messages,既传达了新信息,又不破坏 system 字段的缓存。
工具描述是 Prompt 工程的一部分。关键规则在 System Prompt 和工具描述中双重强化,模型遵守的概率远高于只说一处。
最后,Prompt 设计直接影响 token 成本。system prompt 的稳定性决定了 Cache 命中率,output 控制决定了每轮的生成成本,工具调用效率决定了总轮次。
System Prompt 是 Agent 行为质量的最大杠杆。你可以换模型版本、调温度参数、优化工具实现,但这些加起来的影响可能不如一个写得好的 System Prompt。它是 Agent 的灵魂,而灵魂值得反复打磨。
下一章我们给 MewCode 装上硬约束:权限系统。System Prompt 里的安全边界是「劝」模型不做危险的事,权限系统是代码层面「拦」模型真的要做危险的事。软硬结合,多层防御。