用提示词控制Agent行为——六种引导模式,以及哪些约束该写进提示词、哪些该写进代码

公司开始做 Agent 了,我先搭了个对话类的原型。Agent Loop 和工具都有前面研究 Claude Code 攒下的思路,搭起来很快。

真正折腾人的是系统提示词。写了"回答简洁一些",模型照样长篇大论;为了防止它乱操作,加了一句全大写的"绝对不要……",它又谨慎过了头,连正常该做的事都开始反复确认。

改来改去,越改越像在跟模型讨价还价。于是我又回头翻了一遍 Claude Code 的提示词,看它是怎么写规则的。

一、提示词是控制面

先说一个结论:同一条规则,用不同的方式写,可能是一堵墙,也可能只是一条模糊的线。

模型生成的每一个字,都是在一个概率空间里采样出来的。行为指令做的事情,就是在这个空间里立起围栏。围栏立得准不准、牢不牢,全看措辞。

Claude Code 把大量的行为控制都放在了提示词里。比如它的 Git 安全协议,"不要 force push""新建 commit 而不是 amend",这些全是文本,没有一行是代码里的 if/else。哪天想放开 amend,删掉一行提示词就行了。

但这不代表所有约束都该写进提示词。Claude Code 的分工很清楚:

  • 行为性的约束,比如风格、策略、偏好,写进提示词;
  • 结构性的约束,比如权限、token 预算,写进代码。

简单的说,提示词管"模型应该怎么想",代码管"模型能做到什么"。模型可以不听提示词,但它绕不过代码。

还有一个细节,提示词本身也分两层。稳定的"宪法"放在系统提示词里,追求稳定、可缓存;短命的运行时开关(比如当前处于什么模式、有什么待办提醒)则按条件塞进消息里,用 system-reminder 这样的标签包起来,相当于一条带外的控制信道。这块和缓存关系很大,后面单独写一篇。

二、六种引导模式

把 Claude Code 的提示词通读下来,能归纳出六种反复出现的写法。

1. 极简主义

模型天生"乐于助人",让它修个 bug,它顺手把周围的代码也重构了,再加几层抽象、几个校验。

Claude Code 的写法是直接给出边界清晰的反面案例:

Three similar lines of code is better than a premature abstraction.

还有"修 bug 不需要顺便清理周围的代码""只在系统边界做校验"。

可以看到,它没有说"保持简单",而是说清楚了哪些事不用做,甚至给了一个具体的数量(三行重复也比过早抽象好)。具体的数字能压过模型默认的 DRY 倾向,"保持简单"这种话就做不到。

2. 渐进式升级

模型遇到失败,常见的毛病有两种:一种是同一个命令换个姿势反复重试,一种是失败一次就放弃,跑来问用户。

Claude Code 两头都堵住了:

Don't retry the identical action blindly, but don't abandon a viable approach after a single failure either.

然后给出顺序:先诊断原因,再有针对性地修,实在不行再求助。

两边都有禁令,模型每次失败后就必须停下来想一想,到底是该换方法还是该坚持。

3. 可逆性 × 影响范围

危险操作是列不完的。与其给一张"不许做"的清单,不如给模型一个判断的框架:这个操作能不能撤销,会影响到谁。

本地的、可逆的操作,比如改文件、跑测试,随便做;破坏性的、难以撤销的、别人看得到的、要上传到第三方的,先确认。

对于最要命的那几条,用的是 NEVER 加 unless:

NEVER run destructive git commands … unless the user explicitly requests these actions.

NEVER 把门关死,unless 精确地开一个口子。最微妙的那条再配上因果解释,比如为什么不要 amend:pre-commit hook 失败的时候,commit 其实根本没有发生,这时候 amend 改的是上一个 commit。

还有一句我觉得特别重要:

一次批准,不等于在所有场景下都批准了。

模型很容易把用户的一次许可推广成永久许可,所以授权的范围必须显式写出来。

4. 工具偏好

上一篇写工具设计时提过:Bash 的描述开头放了一张"用 X,不要用 y"的映射表,比如 Use Grep (NOT grep or rg)。括号里的对照,把一个开放的选择变成了二选一。

这里补充一点:同样的映射表,Claude Code 在系统提示词里刻意又放了一遍。上下文一长,模型对前面内容的注意力会衰减,重要的规则说两遍不是啰嗦,是对抗遗忘。

5. 委托协议

主 agent 把任务交给子 agent 时,有两条规矩:

  • Don't peek:子任务没完成之前,不要去偷看它的中间输出;
  • Don't race:结果回来之前,不能以任何形式编造或者预测结果。

注意"任何形式"四个字,原文是 not as prose, summary, or structured output。模型很会钻字面意思的空子,只说"不要编造结果",它可能就换成"预计结果如下"了。所以禁令要把形式穷举出来。

多 agent 的话题比较大,后面单独写。

6. 数值锚定

这一条最简单,也最立竿见影。Claude Code 里有这么一句:

keep text between tool calls to ≤25 words. Keep final responses to ≤100 words unless the task requires more detail.

和"be concise"相比,输出 token 少了大约 1.2%,而且不用改一行代码。

用数字代替形容词。"简洁"是什么意思,每个模型、每一次理解都可能不一样,但"不超过 100 字"是确定的。后面再跟一个 unless,给真正需要展开的场景留个余地。

三、跨模式的五条原则

六种模式看下来,背后其实是同几条原则在起作用:

  1. 反面定义优于正面描述:"不要做 X"配一个具体的反例,比"要做 Y"清楚得多;
  2. 具体的例子用来校准抽象的规则;
  3. 讲清楚因果,模型才能举一反三。只写命令不写原因,遇到新场景模型就不知道该怎么判断;
  4. 冗余是刻意的,重要的规则在不同位置说两遍;
  5. 提示词也要灰度。Claude Code 的新指令都是先在内部员工中验证,量化效果之后再推广。

第 5 条展开说一下。Claude Code 会针对每一代模型的"性格缺陷"打补丁。比如某一代模型很爱虚报结果,测试明明挂了,它说全部通过。补丁写的是"如实报告结果"。

但光写"不要撒谎",模型会走向另一个极端:处处加免责声明,确实通过了也不敢说通过。所以这条补丁是对称的,后面紧跟着一句:结果确认通过了,就直接说,不要给已经确认的结果加无谓的保留。

目标是准确报告,不是防御性报告。 这句话我印象很深,因为它正好解释了我那条全大写"绝对不要"为什么会让模型谨慎过头:我只堵了一头,没告诉它什么情况下可以放心去做。

四、回到我们的原型

照着这些原则,我把原型的系统提示词改了一遍。挑两条说。

第一条是"简洁",改之前:

请回答得简洁一些。

改之后:

回复默认控制在 3 句话、100 字以内;用户明确要求详细说明,或者需要列出操作步骤时除外。
不要复述用户的问题,不要在结尾总结你刚说过的内容。

前一句是数值锚定加 unless,后一句是反面定义。

第二条是那条"绝对不要",改之前:

绝对不要帮用户执行任何修改操作!

改之后:

查询类的操作可以直接执行。
修改类的操作(如修改资料、取消订单)先向用户复述将要做的事并得到确认,
因为这些操作执行后无法自动撤销。
用户确认一次只对当前这一个操作有效,下一个修改操作需要重新确认。

可以看到,它不再一刀切,而是按可逆性分了档,讲了原因,也写明了授权的范围。

不过,改完提示词之后我又多做了一件事:修改类工具在代码里也加了一道确认。原因就是第一节说的分工,"修改前要确认"是一个结构性的约束,模型偶尔不听提示词是常态,真正兜底的只能是代码。提示词负责让模型大部分时候做对,代码负责保证它做错的时候也出不了事。

总结

  1. 行为性约束写进提示词,结构性约束写进代码,提示词管"该怎么想",代码管"能做到什么";
  2. 用反面案例和具体数字代替形容词,"不超过 100 字"比"简洁一些"有效得多;
  3. 失败处理要两头约束:既不盲目重试,也不轻易放弃;
  4. 高风险操作按"可逆性 × 影响范围"来判断,用 NEVER + unless 精确地开口子,并写明授权的范围;
  5. 讲清楚因果,模型才能举一反三;约束要对称,否则模型会从一个极端走向另一个极端。

相关资料