为什么你读到的那份 prompt 文件,常常不是模型真正收到的 system

一条 system message 往往由多源拼成;规则该写文件还是写代码,看作用域、优先级、健壮性三连。

Module E · 第 1 讲

立靶:你照着 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 视角——用它做判断和沟通:

  1. 出现「规则写了却不听」的故障,先别让工程师把 prompt 改重。先问一句:「模型这一轮实际收到的 system,是哪几个来源拼的?这条规则真在里面吗?」
  2. 拿到一条新规则要落地,用判据三连一起过:作用域跨不跨通路、有没有要压的模型本能、丢了会不会出系统级故障。三项里有一项吃重,就该工程用代码追加,而不是塞进话术文件。
  3. 警惕「把所有重要规则都堆到末尾」。末尾是 recency 的稀缺高地,堆多了互相稀释,临时话术常驻还会污染行为(比如本是一次性的提示,被钉成每轮都带,模型就会无端反复念叨)。
  4. 审 prompt 别只审文件。要工程师给你一张「这条 system 由哪几个来源拼成」的装配图,光给文件不算交付。

工程视角——用它做架构决策:

  1. 为每个 Agent 画一张 instructions 装配数据流:从磁盘文件起,到「每轮发给 LLM 的 system」止,标清楚每一步谁拼了什么——文件话术、画像 .format() 注入、代码追加的铁律,一个都别漏。
  2. 判一条规则放 L1 还是 L2,走判据三连。三项全不吃重就留文件;任一吃重就用代码恒定追加,并钉死在 system 末尾。
  3. 一次性临时话术走单轮旁路注入,绝不用整体替换——后者会把临时话术变成往后每轮都带的常驻 system。
  4. 凡是「另存一份基线 instructions 供后续替换」的地方,确保基线始终含末尾铁律;更稳的写法是每次替换时把铁律重新收尾拼回去(基线 + 新话术 + 铁律),保证它永远在最后。
  5. 别把画像原始数据整坨灌进 system。用 .format() 只填必要字段,避免大段结构化数据挤占 L1。

收口

顽固的规则不靠写得更重,靠放得更后;而你能不能放对位置,前提是先看清这一轮的 system 到底由谁拼成。审 prompt 永远先审装配,再审措辞——你读到的文件,从来只是模型收到的那条 system 的一部分。