你有没有想过,为什么 ChatGPT 问一句答一句很聪明,但让它连续干 50 轮活就开始"发癫"?
答案藏在两种 Prompt 的本质差异里。
一、Chat Prompt vs Agent Prompt:两种完全不同的生物
先做个不太恰当但很好记的类比:
- Chat Prompt 像相亲:你精心打扮,把最好的一面展示出来,目标是一击命中,拿下高质量的一次对话。
- Agent Prompt 像婚姻:你要跟这个人(模型)过日子,跑 50 轮、100 轮,不能第 3 轮就掀桌子。
| 维度 | Chat Prompt | Agent Prompt |
|---|---|---|
| 目标 | 单次回答质量 | 多轮决策稳定性 |
| 生命周期 | 一问一答 | 可能连续跑 50+ 轮 |
| 失败代价 | 这次答得不好 | 整个任务链崩盘 |
| 优化方向 | 措辞、示例、结构 | 行为边界、工具规范、状态管理 |
核心认知升级 :Agent 里的 system prompt 不是"一段话",而是一套行为控制系统。它约束的不是"怎么回答",而是"怎么行动"。
写 Chat Prompt 你在做文案,写 Agent Prompt 你在做架构。
二、问题一:Prompt 怎么拆成可维护的模块?
先看反面教材
很多人写 Agent Prompt 是这样的------一坨意大利面:
js
arduino
const systemPrompt = `
你是一个代码助手,帮用户完成编码任务,先读文件再修改。
不要加没被要求的功能。执行危险命令要确认。
输出要简洁,不要用"好的我这就帮您"这种废话。
哦对了,环境是 Node 20,包管理器用 pnpm。
如果用户开了 auto-commit 就自动提交,否则不要。
......(此处省略 800 字)
`;
改一个"输出风格"要翻半天,团队里三个人改出三个版本,合并冲突能打起来。
正确姿势:模块化组装
js
csharp
const systemPrompt = [
identitySection(), // "你是 XX,负责 YY"
systemRulesSection(), // 环境约束与硬性规则
taskGuidelinesSection(), // 做事方式:先读再写,不过度发挥
riskGuidelinesSection(), // 行动准则:危险命令要确认
toolUsageGuide(tools), // 工具指南:根据实际工具列表动态生成
outputStyle(), // 输出风格:简洁,别啰嗦
].join('\n\n');
模块化的三重心智模型
1. 独立修改
输出风格出问题?只动 outputStyle()。身份定义要换?只动 identitySection()。每个模块是一个独立的责任单元,改动影响面被锁死。
2. 条件组装
不是所有场景都需要所有模块。比如一个"只读分析 Agent"根本不需要 riskGuidelinesSection()(它又不执行命令)。模块化让你可以按场景拼装:
js
css
const sections = [ identitySection(), systemRulesSection(), ...(canWrite ? [taskGuidelinesSection(), riskGuidelinesSection()] : []),
toolUsageGuide(tools),
outputStyle(),
];
3. 缓存友好
这是最容易被忽略、但对成本影响最大的一点------静态模块可以整体缓存,动态模块才需要每轮重算。这就引出了下一个问题。
三、问题二:什么该缓存,什么每轮都变?
一条分界线切开两个世界
js
arduino
const systemPrompt = [
// ========== 静态区:全局可缓存,所有用户共享 ==========
identitySection(), // "你是 XX,负责 YY"
systemRulesSection(), // 环境约束
taskGuidelines(), // 做事方式
riskGuidelines(), // 行动准则
toolUsageGuide(tools), // 工具指南(工具集不变就可缓存)
outputStyle(), // 输出风格
// ================= 分界线 =================
// ========== 动态区:每会话甚至每轮变化 ==========
envInfo(cwd, gitStatus), // 工作目录、Git 状态
userConfig(claudeMd), // 用户自定义规则
languagePref(lang), // 语言偏好
memoryContext(memories), // Memory 内容
];
为什么这么分?
现代 LLM 的 Prompt Caching 机制(Anthropic、OpenAI 都有)本质是:前缀不变就能命中缓存。
所以排列顺序至关重要:
text
css
[静态前缀] → [动态后缀]
↑ ↑
能缓存 每次重算
如果你把"今天日期"这种每秒都变的东西塞在开头,恭喜你,整个 Prompt 的缓存全部失效,token 成本原地起飞。
缓存决策清单
| 内容 | 变化频率 | 放哪 | 缓存? |
|---|---|---|---|
| 身份、规则、工具说明 | 几乎不变 | Prompt 前部 | ✅ 全局缓存 |
| 输出风格 | 很少变 | Prompt 前部 | ✅ |
| 用户配置(CLAUDE.md) | 会话级 | Prompt 后部 | ⚠️ 会话缓存 |
| cwd、git 状态 | 每轮可能变 | Prompt 尾部/消息注入 | ❌ |
| 当前时间、打开的文件 | 每秒变 | 消息里注入 | ❌ |
记住一句口诀:静态的往前放,动态的往后放,超高频的踢出 system prompt。
四、问题三:用户自定义 Agent 行为的可扩展性设计
用户想改 Agent 行为,你总不能让他去改你的源码吧?所以要有分层配置。
核心原则:通用的优先级低,具体的优先级高
text
markdown
优先级从低到高:
平台默认规则
↓
组织/团队规范
↓
项目配置(如 .cursorrules / CLAUDE.md)
↓
用户个人偏好
↓
当前会话临时指令
为什么高优先级要放后面?
因为 模型对 Prompt 末尾的内容更敏感(recency bias)。这不是玄学,是 Transformer 注意力机制的实证现象------越靠近生成位置的信息,影响越大。
所以组装逻辑应该是:
js
csharp
const finalPrompt = [
...lowPrioritySections, // 先加载:平台默认
...midPrioritySections, // 再加载:项目配置
...highPrioritySections, // 后加载:用户指令(覆盖前面)
].join('\n\n');
低优先级先出现、高优先级后出现,模型读到最后时,用户的最新意图占据"近因优势",自然覆盖掉前面的默认行为。
这也是为什么 CLAUDE.md 这种用户配置文件,应该注入在动态区的靠后位置。
五、问题四:高频变化的信息放哪?
答案:注入到对话消息里,不要写进 system prompt。
原因很简单:
- 写进 system prompt → 每次变化都导致缓存失效
- 写进消息 → system prompt 缓存稳如泰山,动态信息随消息走
实战示例
用户实际发送的消息:
js
ini
const userMessage = "帮我看看 auth.ts 有什么问题";
系统在发送给模型前,把它"夹心"一下:
js
scss
const enrichedMessage = `
<system-context>
当前 IDE 打开的文件:src/auth.ts (第 42 行)
相关 Skill:@security-review(安全审查最佳实践)
今天日期:2026-04-07
</system-context>
帮我看看 auth.ts 有什么问题
`;
这种设计的三个好处
- 缓存友好:system prompt 一动不动,动态上下文随消息走,不污染前缀
- 语义清晰 :用
<system-context>标签包裹,模型能明确区分"这是环境注入"而非"用户原话" - 可追溯:出问题时你能清楚看到"这一轮到底喂了什么上下文"
六、一张图总结整套设计
text
sql
┌─────────────────────────────────────────┐
│ System Prompt │
│ ┌───────────────────────────────────┐ │
│ │ 静态区(全局缓存) │ │
│ │ · identity │ │
│ │ · systemRules │ │
│ │ · taskGuidelines │ │
│ │ · riskGuidelines │ │
│ │ · toolUsageGuide │ │
│ │ · outputStyle │ │
│ ├───────────────────────────────────┤ │
│ │ 动态区(会话级) │ │
│ │ · envInfo / userConfig / memory │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
↓ 每轮拼接
┌─────────────────────────────────────────┐
│ 对话消息流 │
│ ┌───────────────────────────────────┐ │
│ │ <system-context> │ │
│ │ 高频变化信息(时间/文件/状态) │ │
│ │ </system-context> │ │
│ │ │ │
│ │ 用户真实消息 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
七、写在最后
写 Agent Prompt 和写 Chat Prompt,是两种职业。
- 前者是产品经理:思考边界、流程、异常、可扩展性
- 后者是文案大师:打磨措辞、节奏、感染力
当你开始把 system prompt 当"代码"而不是"文本"来设计时------拆模块、分缓存、定优先级、管状态------你就从"会写 Prompt 的人"变成了"会设计 Agent 的人"。
这两者之间的差距,大概就是"会用 ChatGPT"和"能做出 Claude Code"的差距。
所以下次再写 Agent Prompt 时,别急着敲字。先问自己四个问题:
- 这段该拆成哪个模块?
- 它变化频率如何,该缓存还是该动态注入?
- 用户能不能覆盖它,优先级排第几?
- 它是"长期规则"还是"这轮上下文"?
想清楚这四个问题,你的 Agent 就能稳定跑完 50 轮不"发癫"了。