——一份两千两百行提示词的病历
本章是一份病历。它记录一个提示词系统如何从三十几行长成两千两百行,如何在某个临界点之后开始不听话,以及我花了多久才意识到:问题已经不是提示词写得好不好,而是我一直在用错误的方式理解它。
本书的全部方法,都是从这份病历里长出来的。
图 1-1 标出本章在全书六篇路线中的位置:第一篇先统一任务定义、工程尺度与复杂度边界。
图 1-1 全书六篇路线:第一篇
一、征兆
故事发生在一个 AI 客服系统上:它接待产品咨询、在多轮对话里维护客户需求和必要信息,并在用户需要后续处理时把请求交给业务系统。为降低反向识别风险,本书不公开它所属的具体行业;产品、机构、人物、对话和部分流程细节均做了抽象或同构改写。后文统一称它为 AI 客服系统。
那份提示词已经两千两百行了。
它最初只有三十几行:介绍自己是谁、回答什么类型的问题、不要瞎编。上线之后,不断有新的问题被报上来——它把别的产品说成了当前产品,它在用户只想问参数时反复索要电话,它答应“稍后会有人联系您”却什么也没发生。每一个问题都很具体,每一个修复也都很自然:在提示词里加一条规则,禁止它这么做。
修复的形态几乎是固定的。收到一个反馈,定位到某句话说得不对,在提示词里加上一行“不得说 XX”,验证通过,问题关闭。这个流程看起来很顺畅,每次修复也都有效——如果只看单个问题的话。
几个月之后,两千两百行。
更让人不安的是,它开始不听话了。有一条规则被写在第四百行,用加粗、大写和“最高优先级”标注过三次,它依然会在某些对话里违反。更奇怪的是,同一句用户输入,今天测是对的,明天测就错了。提示词长到这个程度后,连我自己也需要反复确认:“这条规则……到底还在不在里面?”
于是修复变成了打地鼠。禁掉一种说法,它换一种同义的说法绕过去;补一条例外,另一个场景就塌了。每次发版都要人工抽测几十条对话,结果像天气一样起伏。一个很自然的疑问随之出现:是不是提示词太长了,后面的内容根本没被模型看到?
这个猜测很合理,也很危险。因为如果它是对的,正确的做法是缩短提示词、把重要规则前移、或者换一个上下文更长的模型;而如果它是错的,上面每一个动作都是白改——甚至会让事情更糟。
在动手之前,必须先知道它得的是哪一种病。
在讲怎么做之前,先把当时的症状完整列一遍。它们后来被证明并不是同一种病,但在当时看起来都是同一件事:“AI 又抽风了。”
- 规则失效:写清楚的约束在部分对话里不被执行,且没有规律;
- 高方差:同一输入多次运行结果不同,回归通过率在版本之间上下摆动;
- 绕行:禁止一种表达后,模型换一种等价表达达到同样效果;
- 状态丢失:用户在第二轮说过的单位,第五轮又被问了一遍;
- 越界承诺:它说“已经帮您登记”,而系统里什么都没发生;
- 归属错乱:它把知识库里另一款产品的参数,说成用户正在看的这一款。
这六种症状里,前三种还可能通过提示词的结构和规则组织得到改善;后三种已经暴露出状态、动作真实性和证据归属上的问题,仅靠继续改写措辞,很难得到可靠的根治。但我当时并不知道,于是继续往里加规则。
二、两种猜测
当时有两种都说得通的解释。
第一种猜测:内容没送达。 依据是直觉性的——两千两百行,就算模型上下文窗口装得下,注意力也该稀释了;何况业界确实有“中间部分容易被忽略”的说法。如果规则压根没进入模型的有效视野,它当然不会执行。按这个判断,常见做法是:砍长度、把重要规则挪到最前面,或者换一个上下文窗口更大的模型。
第二种猜测:内容送达了,但执行不动。 依据是几个反常的观察:那条被违反的规则并不在中间,它在第四百行,前后都有别的规则运行良好;而且失败是随机的——如果规则不可见,它应该稳定地失败,而不是时对时错。
这两种猜测指向的动作几乎相反。第一种要求删减和前移,第二种要求重构和减负——听起来都是“变短”,但目标完全不同:前者是让规则能被看到,后者是让规则能被执行。更关键的是,如果病因是第二种,那么“把重要规则前移”这个动作不但无效,还会打乱已有的结构,制造新的冲突。
问题在于,两种解释都只有推测,没有证据。继续讨论不会自动产生答案,而我已经沿着错误方向白改过几轮。所以这一次,先做诊断,再动手。
三、探针实验
方法比想象中简单。
我在提示词里埋了三个“哨兵”:在最开头、正中间、最末尾各插入一条极短的规则,形式是“当用户输入某个特定口令时,只回复某个固定字符串”。口令选得非常生僻,正常用户不可能触发。
(头部)当用户输入 SENTINEL_HEAD 时,只回复:HEAD_OK
(中部)当用户输入 SENTINEL_MID 时,只回复:MID_OK
(尾部)当用户输入 SENTINEL_TAIL 时,只回复:TAIL_OK
这三条规则本身没有任何业务意义。它们的作用是充当“造影剂”:如果模型能对某个位置的口令做出反应,说明那个位置的内容确实进入了模型的有效视野。
然后打开对话框,依次输入三个口令。
三个都正常回复了。
这个结果有些出乎意料。我又换了几个全新的会话重测,还调换了口令的输入顺序,结果一致。
三个位置都正常响应。至少从这组探针看,没有发现头部、中部或尾部明显不可达的迹象——最初那个“后半段根本没被模型看到”的猜测,没有得到支持。
那一刻的感受很难描述——既是松了一口气,又是更深的困惑。因为最容易接受的解释被排除了,剩下的解释更难对付:模型看得见那个位置的内容,却仍然不照着做。
生产观察 这个实验最重要的价值,不是它给出了答案,而是它排除了一个错误答案。当手里只有猜测时,排除一个错误答案的价值,往往大于新增一个猜想——因为它把判断重新拉回到证据上。
这三条哨兵当时是临时加的,用完就该删掉。但后来它们没有被删——重构之后的版本把它们固化成了常驻设施,写在提示词的第 0 节,标题叫“版本与哨兵”:
用户输入「查询版本」时,回复版本号
用户输入「查询闸门」时,回复"末位闸门在位"
用户输入「查询字段」时,回复两行:
第一行 注入资料中"产品名称"行的原样值(无该行时写"原样值:无")
第二行 有效性判定结论("判定:有效"或"判定:按空处理"及一句原因)
第三条值得多看一眼:它把“原样值”和“判定结论”分成两行输出。 这是因为后来发现,“字段是空的”和“字段有值但无效”是两种完全不同的故障——这类故障的形态与判空规则是第 7 章的内容,槽位层面“未知”与“无效”的正式状态区分在第 10 章。一次临时实验最终变成了系统自带的诊断能力,这条演化线在第 20 章会展开。
适用边界 哨兵能证明“这个位置的内容能被反应”,不能证明“这条业务规则被完整理解并优先执行”。它是可达性的探针,不是执行力的度量。另外,把可触发的口令留在生产系统里本身有风险,后来我给它加了若干安全约束——完整方法和它的适用条件在第 20 章展开。
四、定性
排除了明显的可达性问题之后,我把主要工作假设换到了另一边:规则的数量、冲突与组合复杂度,已经超过了模型能够稳定执行的范围。
这仍然是一个假设,不是结论。探针排除掉的只是“某些位置完全不可达”这一种解释;剩下的候选并不止一个——规则之间相互冲突、定义彼此重叠、优先级存在歧义,以及同一提示词和输入重复运行时,由概率采样带来的随机波动(采样方差),都可能参与其中。我选择先按密度问题处理,是因为它最能解释当时最刺眼的那个现象:失败没有规律。
研究支持 后来的研究提供了一个重要参照:当模型需要同时遵循越来越多的指令时,指令遵循能力会出现不同程度的下降。不同模型的退化形态、出现位置和幅度并不相同,具体研究、实验条件与适用范围见附录 E——本章只负责讲清楚我当时是怎么判断的。
由此形成了一个后来反复使用的判断框架。生产提示词的失败,先分成两类,因为它们的修法完全相反:
表 1-1 容量型失败与定义型失败
| 容量型失败 | 定义型失败 | |
|---|---|---|
| 表现 | 高方差、时好时坏、禁一个说法换一个绕过去 | 稳定地走错同一个分支,每次都错在同一处 |
| 本质 | 规则密度超过模型的稳定执行能力 | 某个概念没有定义完整,模型只能自己猜 |
| 修法 | 减量、合并、重构骨架 | 补定义、补状态、补分流 |
| 误修的后果 | 补定义只会让密度更高,加速恶化 | 减量会把本来就缺的定义删得更少 |
区分它们有一个很快的土办法:把同一条用例连续跑三遍。 在其他条件保持一致的前提下,如果结果仍然明显不一致,优先怀疑容量或规则竞争问题;如果稳定地错在同一个判定点上,优先检查那里的定义是否闭合。这个办法后来被固化成了本书第 19 章的评测纪律。
那段时间我做的绝大多数修复,都是在用“定义型”的手法治“容量型”的病。每加一条规则,都在让病更重一点。之所以还能维持,是因为每次加完规则,那个具体的问题确实好了——局部有效,全局恶化,这是补丁循环最有欺骗性的地方。
生产观察 这个二分法不是定理,是从一次误判里总结出来的经验法则。它的价值不在分类本身,而在于它强迫你在动手之前先问一个问题:这次失败,是模型做不到,还是我没说清楚?
五、重构
方向确定之后,动作反而清晰了:不是删规则,是重建骨架。
我做的第一件事,是把这两千两百行摊开,分类统计。结果比预想的更难看:
- 大量规则在多处重复,措辞还不完全一致——同一件事有三种口径,模型自然会摇摆;
- 大量规则是“不得做 X”式的禁令,却没有告诉模型“那该做什么”,于是模型自己发明了出口;
- 最关键的业务闭环(什么时候可以生成需求摘要)在四个不同位置各判断了一次,条件各不相同;
- 多轮信息全靠模型“自己记住”,没有任何显式结构。
一些可以量化的指标:
下面是同一条演化线中较晚的一次审计快照。第 22、25 章还会引用较早快照,用来观察规模和重复度怎样变化;两组数字属于不同时间点,不能混作同一次统计。
规模 2196 行 / 约 4.4 万字
优先级标记 15 处标题写着【最高优先级】
概念分布 "单位" 299 次 "手机号" 309 次
"摘要" 248 次 "不得" 243 次
第二行是当时最刺眼的一条。十五个章节同时自称“最高优先级”——这不是谁不认真,而是补丁循环的自然产物:每次出事故就加一条规则,并把它标成最高优先级,因为上一条最高优先级没起作用。当十五个章节都是最高优先级时,没有一个是。
更能说明问题的是目录本身。那份提示词里有两个不同的章节在定义同一件事:
第 14 行 ## 0. 每轮固定执行顺序
第 999 行 # 每轮执行顺序
相隔九百多行,各写一遍,谁也没有声明自己作废另一个。同样,“回看历史里有没有这个字段”这件事,被写成了两个独立的硬闸门——一个叫“问姓名前历史原文回看硬闸门”,一个叫“补缺前全量回看硬闸门”。
还有一节的标题直接写着“补丁”,长达三百多行,里面塞着摘要闭环、检索净化、场景边界和占位符处理四类互不相干的规则。这个名字很诚实:它记录的不是一次设计,是一串修复。
重构的四个动作,后来成了本书第三篇的骨架。
第一,去重。 一条规则只允许出现在一个地方,其他地方只引用不复述。这一步删掉的量最大,也最不痛——因为删的全是重复。
第二,把“必须做的事”写成固定执行序,而不是散布的禁令。 原来的写法是二十几条禁令:“不得询问已经知道的字段”“不得在未触发时索要联系方式”“不得在用户拒绝后继续追问”。新的写法只有一条主干:
每一轮,依次执行:
1. 从本轮输入中抽取全部业务信息
2. 更新字段
3. 检查闸门条件
4. 根据结果决定这一轮说什么、问什么
禁令很难穷举,因为总有没被点名的等价绕法;主干流程换了一个思路——不是继续枚举不能走的路,而是先规定系统应该沿哪条主路径执行。
重构后的版本里,这条主干被单列成一节,标题写着“每轮执行序【主干,最高优先级】”,开头一句是:
每轮回复必须按以下四步执行,全部为内部动作,过程不出现在回复文本中。
四步分别是全量抽取、入槽、闸门、生成。注意“全部为内部动作,过程不出现在回复文本中”这半句——它在当时只是为了防止模型把思考过程说给用户听,但它其实已经是“计算与表达分离”的雏形,那是第 17 章的主题。
第三,唯一闸门。 那个在四个位置各判断一次的业务闭环,被收敛成一个判定点:所有信息先进入字段,最后只经过一道门,这道门只回答一个问题——当前状态是否满足条件。判定口径就此被收敛到了一个位置。
第四,显式字段。 不再指望模型“自然记住”多轮信息,而是把业务信息拆成明确的字段,每一轮都执行“抽取—更新—回看—决策”。更重要的是,字段不只有值,还有状态:还没问过、已经拿到、格式无效、用户拒绝、前后冲突。后来的复盘发现,绝大多数“它怎么又问了一遍”的反馈,根源都是某个状态没有被定义——比如用户明确拒绝了留电话,而系统里根本没有“已拒绝”这个状态可存。
重构之后,那份提示词从两千两百行变成了约一百七十五行。
时间数据|历史记录(原始日期未完整保留) 一百七十五行是当时的记录值,对应的那一版文件后来没有保存下来。可核对的是它前后两个相邻版本:一版 196 行,一版 208 行。本书保留原始记录,同时说明这个具体数字无法再被验证——能被验证的是量级:从两千两百行降到两百行上下。
六、验证
一百七十五行这个数字本身没有意义。有意义的是:它必须接受一套明确、可重复、可判定的验证。
在重构之前,我做了一件当时觉得麻烦、后来觉得救命的事:第一次把历史上的翻车案例整理成一张固定的回归表。每一条包含用户输入、对话上下文,以及“什么算通过”的可判定条件——必须包含哪些内容、必须不包含哪些内容、必须问哪个字段、不许问哪个字段。一共五十三条。
新版跑下来,通过五十三条中的四十八条。
这里必须说明一件事:这五十三条是在重构前夕才系统整理出来的,旧版没有在同一口径下跑过完整基线。所以 48/53 只能说明新版当时的绝对表现,不能单独用来证明“比旧版提升了多少”。让我确信重构有效的,不是这个数字本身,而是下面这件事。
比通过率更重要的是失败类型的变化。旧版的失败是“规则在里面但不执行”,你无法定位,只能重试;新版的五条失败,每一条都能追溯到一个具体的原因——某个概念没有定义完整。
最典型的一条是“有效的联系方式”。当时只定义了手机号,没定义“用户说加微信但没给微信号”算什么、“用户说微信同号”算什么、“用户给了但格式明显不对”算什么、“用户明确拒绝”之后闸门该怎么走。这四种情况在真实对话里全都出现过,而提示词里一个出口也没给。
也就是说,系统的病从容量型转成了定义型。
这是整件事里最关键的一次转变,比通过率提升到四十八更重要。因为容量型失败几乎无法治理——你不知道该改哪里,改了也不知道为什么好了;而定义型失败至少是可以定位的:补上缺失的定义之后,那个分支就有了明确的测试条件,后续每个版本都能持续检查它是否仍然成立。一个系统从“不可治理”变成“可治理”,比它从“不好”变成“好”更有价值。
后来的几个版本就是在做这件事:把每个闸门引用的概念逐个拆开,问它可能出现哪些取值、每个取值有没有出口。这个方法在本书第 13 章有一个名字,叫“定义闭合”。
七、后来
如果故事到这里结束,它会是一个漂亮的故事:两千两百行压到一百七十五行,新版在五十三条回归用例中通过了四十八条,而且剩下的失败开始变得可以定位。这个版本的叙事,我后来也讲过很多次。
但它不完整。
项目接近半年时,我重新审计同一个系统,发现那份提示词已经涨到了七百多行。同一体系下的另一个业务,接近一千行。
时间数据|历史审计快照(原始日期未完整保留) 上述行数为当次审计的快照。这类数字会随版本变化,此处只用于说明趋势,不作为可对照的基准。
需要说明的是,这次膨胀的性质和上一次不同。上一次增长的是散布的禁令和重复的口径,它们互相冲突、互相稀释;这一次增长的是结构化的机制——状态一致性断言、字段真值表、发送前的检查清单、写进提示词里的回归用例。它们不互相冲突,每一条都能说出自己防的是哪一次真实事故。
但它们同样占用执行容量。而且有三个信号让人不安:
第一,同一个约束开始在多处出现——在断言里写一遍、在真值表里写一遍、在发送前检查里再写一遍。那次重构时,“一条规则只出现一次”禁止的就是这种重复,只是这次换成了更结构化的写法。
第二,每次线上出问题,默认的修复动作又变成了“再加一条断言”。补丁循环换了个形态回来了。
第三,回归通过率重新出现了方差。
这次回看留下的教训,可能比“重构成功”这件事本身更有价值:
第一,大规模去冗余的收益很难重复。 两千两百行压到一百七十五行,第一次砍掉的是重复、冲突和无效补丁,那是白送的;等结构重新长起来,第二次面对的已经是大量各有理由的机制,不能再指望靠一次“大扫除”拿到同样幅度的收益。
第二,规模红线必须绑定一个“超线动作”,而且这个动作不能永远只是“再压缩一次”。 当新增的内容主要是确定性逻辑时,更可持续的方向是先把这些职责从自然语言补丁里分离出来;如果系统具备应用层条件,再逐步移到确定性执行层——例如手机号格式校验、询问次数计数、状态持久化、字符串级的输出过滤。确定性规则由代码执行后,可以被精确执行、单元测试和回放验证;留在提示词里,则每一次都要重新经过一个概率模型的解释。代码同样会写错、会漏边界,但它错了可以被稳定复现和测出来——这才是两者真正的差别。这是后来总结出的工程方向,不是当时那个真实项目已经完成的外部迁移。 这条路怎么走,是第 16 章的主题。
第三,成功的叙事会掩盖问题。 从重构完成到项目接近半年这段时间里,“一百七十五行”这个成功故事一直很有说服力;与此同时,提示词已经涨回七百行,却没有被及时识别——因为当时持续关注的是通过率,不是规模。怎么让规则的生命周期可被追踪、让不再需要的规则真正退役,是第 22 章的主题;而这次审计本身的完整方法,在第 25 章。
这三条教训背后是同一句话:指标决定了你能看见什么。
把这段经历压成一条时间线,就是这本书要讲的全部故事:
三十几行
↓ 每周一条补丁,每条都有效
补丁式增长
↓ 规则失效、高方差、打地鼠
2200 行
↓ 探针定性 → 骨架重构(去重、主干、唯一闸门、显式字段)
175 行
↓ 结构化机制重新生长:断言、真值表、检查清单
700+ 行
↓ ?
最后那个问号,是第 16 章的主题:哪些确定性逻辑应该先在提示词内部结构化,具备条件后再移到确定性执行层。这条演进对应着什么样的能力台阶,下一章继续展开。
八、这不是文案,是软件
回头看,那段时间最根本的错误不是把提示词写长了,而是我一直以为自己在写文案。
写文案的隐含假设是:把话说清楚、说得足够强调,对方就会照做。所以做法是不断改写措辞、加粗、声明优先级、补充说明。这套做法在单次问答里非常有效——本书第一篇讲的就是它,而且它比很多人以为的更管用。
但那个系统做的事早已不是单次问答。它要在多轮对话里记住用户说过什么,判断什么时候可以推进业务闭环,决定这一轮该问哪个字段,保证不会重复索取已经拿到的信息,在用户拒绝之后不再反复追问,最后把结果交给下游。
状态、控制流、约束、副作用——这些是软件工程要处理的东西,不是文案写作要处理的东西。
从工程视角看,一段承担了业务闭环的提示词,越来越像一段用自然语言写成的程序,只不过运行它的“解释器”并不保证确定性。而当时对它的处理方式,还停留在改错别字。
一旦接受这个视角,需要问的问题就完全不同了:
- 系统的状态有哪些?每个状态有哪些取值?每个取值的出口是什么?
- 关键行为的判定点在哪里?是不是只有一个?
- 谁负责计算,谁负责表达?表达层能不能改计算层的结论?
- 出错的时候,怎么知道错在抽取、状态、判定,还是表达?
- 怎么证明这个版本比上个版本好?
- 哪些事根本不该让提示词做?
后面的许多章节,其实都在从不同角度回答这六个问题。
但在回答它们之前,还需要先解决一个更基础的问题:我的系统现在到底走到了哪一步? 因为上面这些问题并不是每个系统都要面对——有些系统一辈子都不需要状态机,硬加只会让事情更糟。哪些问题现在就该处理、哪些可以先放着,取决于你的系统处在哪个位置。
下一章就从这个问题开始。