为什么你读到的那份 prompt 文件,常常不是模型真正收到的 system
一条 system message 往往由多源拼成;规则该写文件还是写代码,看作用域、优先级、健壮性三连。
立靶:你照着 prompt 文件审了一遍,线上还是出那个老毛病
你接手一个语音 Agent,反复出一个故障:它调完挂断工具之后,还会自己多吐一句「已完成对话,无需回复」,把通话拖出一截尴尬的尾巴。你打开它的系统提示文件,从头到尾读了三遍,里面明明白白写着「调用结束工具后不要再说话」。规则在那儿,措辞也没歧义,可线上就是不听。
更怪的是:这毛病时灵时不灵。普通通话基本正常,可一旦中途触发了某个「切换模式」的动作——比如环境太吵、Agent 升级了一段降噪话术——故障立刻复活。
你怀疑是模型不行,于是把那句规则又写得更重、挪到段首、加了三个感叹号。没用。
真问题不在那段话怎么写,而在一件你没意识到的事:你读到的那份 prompt 文件,根本不是模型这一轮真正收到的 system。 模型每轮看到的 system message,是框架替你从好几个来源拼出来的一整条——文件里的话术只是其中一块,另外还有代码里写死、每轮硬追加上去的铁律,以及运行时按用户画像填进去的字段。你照着文件审,自然审不出问题:那条让故障消失的关键规则,压根不在文件里,它是在代码里拼上去的。而那个「切换模式」的动作,恰好在拼的时候把它弄丢了。
这一讲就讲清楚两件事:模型这一轮的 system 到底是谁、在哪、怎么拼出来的;以及一条规则该写进文件(L1)还是用代码追加(L2),判据是什么。
框架:一条 system 由谁拼出来,规则该落在哪一层
先校准一个最容易混的点。很多框架管「传给 LLM 的系统提示」叫 instructions(指令)——名字不同,本质就是 L1 系统提示。而真正的暗坑在于:L1 和 L2 在 API 里其实进的是同一条 system message,模型看到时分不出彼此。它们的区别不在「模型看到的位置」,而在「谁来维护、什么作用域、什么优先级」。
下面这张表是这一讲的核心框架,叫「L1 / L2 判据表」:
| 维度 | L1 系统提示 | L2 指令 |
|---|---|---|
| 装什么 | 角色、人设、行为准则、能力边界(相对稳定) | 任务特定规则、优先级约束、动态追加的铁律 |
| 在哪里 | 多半是磁盘上的文件(话术文档) | 多半在代码里拼(常量、模板、运行时追加) |
| 谁来改 | 产品 / 运营手改文件 | 改代码逻辑 |
| 怎么喂 | 每轮固定带 | 有的恒定追加、有的整体替换、有的单轮旁路 |
| 性质 | 行为指导 | 堵一个顽固故障、压一个模型本能 |
关键认知:模型每轮看到的那一条 system,常常由三个来源拼成——① 文件里的话术(L1),② 运行时按用户画像 .format() 填进去的字段(来自 L6 记忆 / 画像,填进 L1),③ 代码里写死、每轮硬追加在末尾的铁律(L2)。只读文件,你只看到第一块。
那么一条规则到底该留在 L1 文件,还是抽到 L2 用代码追加?用判据三连:
| 判据 | 留在 L1(文件话术) | 抽到 L2(代码追加) |
|---|---|---|
| 作用域 | 可能只对某个阶段 / 某条通路生效 | 跨所有通路、所有阶段都必须在 |
| 优先级 | 普通规则,模型本来就肯听 | 必须放在末尾,靠 recency 压制模型的相反本能 |
| 健壮性 | 偶尔没遵守,无伤大雅 | 一旦丢失,直接出系统级故障 |
三个判据里只要有一个吃重,就该把这条规则从 L1 文件抽出来、用代码恒定追加到 system 末尾。这里的「优先级」指的是位置不是重要度——问的是「要不要靠末尾位置压住模型一个相反的本能」,而不是「这事在业务上重不重要」。判这一项之前,先问一句:模型对这条规则,有没有一个相反的自发倾向?有,优先级就吃重。
数据 / case:三个脱敏场景,三种手感
手感一:同一件事,可能有两套规则,分管两件事。 在某语音 Agent 里,跟「挂断」相关的规则其实有两套,管的事完全不同。一套写在 L1 话术文件里,管业务:什么时候该结束通话、再见话怎么说。另一套写在代码里,管机械时序:先说告别 → 再调结束工具 → 调完之后禁止再吐任何字。后者不是「生成」出来的,它就是源码里手敲的一段字符串常量,每次创建 Agent 时被固定拼到 system 末尾。两套规则唯一的区别:一个写文件(人手改),一个写代码(每轮固定追加)。审计时把它们当成一回事,就会漏掉真正堵故障的那一套。
手感二:顽固故障,要靠末尾位置压,不靠把话写重。 上面那条「调完工具禁止吐字」的规则,为什么非得用代码抽出来、钉死在 system 末尾?因为工程实测发现:哪怕把这条要求写进工具描述里,模型仍然会自作主张多吐一句状态说明——它有一个「向用户如实交代我刚做了什么」的强本能。对抗这种本能,靠的不是措辞更狠,而是位置:把规则放在整条 system 的最末尾,利用 recency(模型对最近 token 最敏感)压住本能。这就是判据三连同时吃重的典型——作用域跨所有通路、优先级必须末尾、健壮性丢了就出故障——三项全中,所以它必须是 L2。
手感三:用整体替换切模式,却把末尾的铁律弄丢了。 这是暗物质最阴的一种。某团队的代码里,创建 Agent 时给 LLM 的 system 是「文件话术 + 末尾铁律」,没问题。但它另外存了一份「基线 instructions」用于后续切换模式,存的却是拼铁律之前的版本。于是当通话中途触发降噪升级、需要整体替换 system 时,代码用的是「基线 + 新话术」——末尾那条铁律就这么没了。故障当场复活。
创建时给 LLM 的 system = 文件话术 + 末尾铁律 ✅ 有铁律
另存的「基线」 = 文件话术 ❌ 不含铁律
切模式时整体替换 = 基线 + 降噪话术 = 文件话术 + 降噪话术 ❌ 铁律丢了
这三个手感指向同一句话:规则写了,不等于每一轮真的拼进了模型收到的 messages。 你读文件读不出这个差额,只有追到代码、看清每一轮 system 实际怎么装配,才看得见。
顺带,这里也带出了 L2 的三种注入方式,各自对历史的影响完全不同——切模式那个 bug,根子就在「整体替换」这一种:
| 注入方式 | 行为 | 对历史的影响 | 适用 |
|---|---|---|---|
| 恒定追加 | 每轮都带、固定拼在末尾 | 稳定常驻 | 跨通路、最高优先级的铁律 |
| 整体替换 | 换掉整条 system | 改写持久 system,最易丢东西 | 升级 / 切模式 |
| 单轮旁路 | 只这一轮注入,不进持久历史 | 不污染历史、不累积 token | 一次性临时话术 |
可操作做法:PM 视角与工程视角
PM 视角——用它做判断和沟通:
- 出现「规则写了却不听」的故障,先别让工程师把 prompt 改重。先问一句:「模型这一轮实际收到的 system,是哪几个来源拼的?这条规则真在里面吗?」
- 拿到一条新规则要落地,用判据三连一起过:作用域跨不跨通路、有没有要压的模型本能、丢了会不会出系统级故障。三项里有一项吃重,就该工程用代码追加,而不是塞进话术文件。
- 警惕「把所有重要规则都堆到末尾」。末尾是 recency 的稀缺高地,堆多了互相稀释,临时话术常驻还会污染行为(比如本是一次性的提示,被钉成每轮都带,模型就会无端反复念叨)。
- 审 prompt 别只审文件。要工程师给你一张「这条 system 由哪几个来源拼成」的装配图,光给文件不算交付。
工程视角——用它做架构决策:
- 为每个 Agent 画一张 instructions 装配数据流:从磁盘文件起,到「每轮发给 LLM 的 system」止,标清楚每一步谁拼了什么——文件话术、画像
.format()注入、代码追加的铁律,一个都别漏。 - 判一条规则放 L1 还是 L2,走判据三连。三项全不吃重就留文件;任一吃重就用代码恒定追加,并钉死在 system 末尾。
- 一次性临时话术走单轮旁路注入,绝不用整体替换——后者会把临时话术变成往后每轮都带的常驻 system。
- 凡是「另存一份基线 instructions 供后续替换」的地方,确保基线始终含末尾铁律;更稳的写法是每次替换时把铁律重新收尾拼回去(基线 + 新话术 + 铁律),保证它永远在最后。
- 别把画像原始数据整坨灌进 system。用
.format()只填必要字段,避免大段结构化数据挤占 L1。
收口
顽固的规则不靠写得更重,靠放得更后;而你能不能放对位置,前提是先看清这一轮的 system 到底由谁拼成。审 prompt 永远先审装配,再审措辞——你读到的文件,从来只是模型收到的那条 system 的一部分。