第三篇讲完了“让对话拥有状态”的全套设计:字段、证据、归并、闸门、状态机、不变量。
但那还只是设计。这一篇讲怎么把它落成一套工程化运行时。而落地的第一个、也是最根本的问题是:这些机制,哪些交给模型,哪些必须交给确定性执行层。
图 16-1 标出第四篇的位置:本篇把前面的状态与控制设计落成运行时契约。
图 16-1 全书六篇路线:第四篇
一、一个手机号,模型也可能数错
先看一件小得不能再小的事。
系统要判断用户给的手机号格式对不对:清洗掉空格和分隔符、数够十一位、首位是 1、基本结构成立。这类纯本地的结构判断,写进提示词里绝大多数时候都对。(注意这里只说结构——“这个号段现在是否还在启用”是另一回事,那要查权威数据,属于下一节的第②档。结构合法,不等于现实有效。)但即便是这么简单的结构判断,放到大量会话里,仍然可能偶发出错:
下面的 X 只代表任意一位数字,用来演示格式,不对应可拨打号码。
- 用户输入
138 XXXX XXXX,中间有空格,模型有时算成“位数不对”; - 用户输入
+86 138XXXXXXXX,模型有时把国家码也数进去; - 用户一次给了两个号码,模型有时只挑一个、有时拼在一起;
- 就算格式完全标准,同一条输入重跑十次,也可能有一次栽在别的地方。
问题不在于模型笨,而在于这件事根本不该由它来做。 “数十一位数字”是一个确定性计算:给定输入,答案唯一,没有任何需要理解、判断、权衡的地方。把一个确定性计算交给一个概率生成器,就等于给一件本可以百分之百做对的事,人为引入了一个错误率。
第 1 章那份提示词回涨到七百行,很大一部分就是这类东西堆出来的:格式校验、次数计数、字符串匹配、状态标记——每一条都在用自然语言,费力地向模型描述一件用三行代码就能精确做到的事。
这一章讨论的,就是哪些东西应该被请出提示词,以及在暂时不能外置时,怎样先把它们从自然语言补丁中分离出来。
二、判据:这件事需不需要理解语言
什么该交给代码,什么该留给模型?先给一个初步的判据,再把它打磨准。
初步的说法是“这件事有没有唯一正确答案”。它抓住了一部分——“手机号是不是十一位”有唯一答案,交给代码;“用户这句话是在提供还是拒绝”需要理解语言,交给模型。但这个说法很快会漏。
漏在哪?看一个例子:单位和联系人同时缺,这一轮先问哪个?
这件事没有宇宙唯一的答案——先问哪个都能说得通。但它绝不该每一轮都让模型临时权衡:模型这次觉得先问单位、下次觉得先问联系人,系统的行为就飘了。正确的做法是业务定一个明确的优先级(比如缺单位优先于缺联系人),然后由代码稳定执行这条策略。
所以判据不能停在“有没有唯一答案”,要升级成四档:
① 能由结构化输入按明确规则算出 → 代码计算 (格式、位数、比较、计数)
② 答案在权威的外部事实源里 → 代码去查 (数据库、接口、检索)
③ 本有多种选择,但业务策略已定 → 代码执行策略(问询优先级、路由、分支)
④ 必须理解模糊语言或生成表达 → 模型 (意图、归类、改口、措辞)
前三档都归代码,只有第四档归模型。把它们合起来,本章真正的轴是这一句:
模型处理语义的不确定性;代码执行已经明确的规则、状态、权限和业务策略。
注意这句话比“有没有唯一答案”准得多。第三档就是它多出来的部分——业务策略没有客观唯一答案,但它一旦被确定下来,就是“明确的”,因此归代码。 模型该出场的,只有“这句话到底什么意思”这种语言本身就模糊、无法被规则穷尽的地方。
这里顺带把一个常见误解说清楚:代码和模型都能测试,区别不在能不能测。 给“手机号先不留了”配上期望输出 decline(phone),模型这一条同样能判 PASS/FAIL。真正的区别是——确定性代码同版本同输入结果稳定复现、失败能精确定位;而模型对没见过的语言变体的可靠性,只能靠覆盖性测试集和统计指标去估计,压不到零。也别把代码想成不会错:代码也会写错,它的优势不是不会错,而是错误可以稳定复现、定位和修复(与第 1 章的判断口径一致)。
语义判断,不是所有“做选择”
顺着这条轴,要破除一个很常见的误解:“需要做选择”不等于“需要模型”。
把“选择”分成两类就清楚了:
语义判断——“手机号先不留了”这句话,到底算不算拒绝?这需要理解语言的言外之意,无法用规则穷尽:
→ 模型
业务判断——已知 phone 已拒绝、wechat 还未知,接下来是问微信还是停止收集?这有明确的业务策略可依:
→ 代码执行策略
两者都是“做选择”,但只有前者需要模型。后者一旦业务把规则定下来,就该由代码稳定执行——否则就等于把一个已经有答案的决定,重新丢回给概率去掷骰子。
需要模型的是语义判断,不是所有需要“做选择”的地方。业务选择一旦形成明确策略,就应该由代码稳定执行。
三、一张分工矩阵
把第三篇那套机制逐个过一遍,用上面四档判据一分,归属其实很清楚。
先纠正一个容易混的地方,它直接关系到第 11 章的边界。第 11 章冻结过:证据是“系统观察到了什么”,事件是“这次观察该怎么解释”。 所以 set / decline / correct 这些不是证据,而是对证据的解释,属于事件层。分工要按这个边界来切:
表 16-1 模型与代码的分工矩阵
| 环节 | 模型负责 | 代码负责 |
|---|---|---|
| 证据 | 必要时辅助语义绑定(这个号是不是这个人的) | 原文、定位、来源、时间戳、机械候选锁定、入账 |
| 事件 | 判断这次观察是 set / correct / decline / 新作用域 | 枚举校验、事件编号、入账 |
| 归并 | —— | 折叠、冲突检测、作用域分派(第 12 章) |
| 闸门 | —— | 条件判断、真值表、原因码(第 13 章) |
| 策略 | —— | 决策表选主动作、执行业务优先级(第 13 章) |
| 状态机 | —— | 转换合法性、Guard 判断(第 14 章) |
| 不变量 | —— | 全部检查(第 15 章) |
| 渲染 | 把已批准的动作说成自然语言 | 模板拼接、字段填充、白名单过滤 |
| 验证 | 必要时可加语义检查(不能自证放行) | 逐字否决、格式、白名单、状态一致性(第 18 章) |
这张表暴露出一件事:第三篇那套机制的绝大部分,落在代码那一列。
归并器是纯折叠函数(第 12 章特意强调它不重新理解语言);闸门、决策表、状态机都是查表和条件判断;不变量是断言;连“先问哪个”这种选择,也因为业务策略已定而归了代码。它们里面没有一处需要理解自然语言——需要理解的部分,早在证据和事件阶段就被模型处理完、结构化好了。
在这条核心状态与控制链上,模型集中在两个语义边界:
输入侧 把自然语言理解成结构化的证据和事件 (理解)
中间 状态计算、许可、业务策略、提交 (代码,尽量确定)
输出侧 把已批准的决定表达成自然语言 (表达)
需要说清楚:这说的是核心状态控制链。一个完整的运行时里,模型还可能出现在别处——作用域和实体消歧、检索前的查询改写、工具参数的生成、模糊实体的绑定。这些也都是“语义判断”,符合第四档。所以准确的说法不是“模型只在一头一尾”,而是:在核心状态与控制链上,模型主要集中在输入理解和输出表达两个语义边界,中间的状态、许可、策略和提交尽量保持确定。
浓缩成一句便于记忆的话:
模型管两头的语义翻译,代码管中间的确定性计算。
四、工作流放在哪里
讲到这里,一个问题几乎一定会冒出来:那些可视化工作流平台算什么? 它们既能拖模型节点,又能拖条件判断和接口调用——按前三节的划分,它算模型还是算代码?
问题本身有点问错了方向。因为这两件事讨论的不是同一根轴:
模型 与 代码 讨论的是:这项职责由谁承担
提示词、普通代码、工作流 讨论的是:这项职责在哪种载体中实现
工作流不是第三种责任主体,它是一种编排与运行载体——同时承载模型节点和确定性节点。前三节那张分工矩阵不会因为换了实现载体而改变;变的只是每一格落在哪个节点上。
把它对上来看:
表 16-2 工作流职责的实现位置
| 系统职责 | 适合的实现位置 |
|---|---|
| 理解用户意图、识别改口和拒绝 | 模型节点 |
| 提取结构化事件 | 模型节点 + 结构校验 |
| 字段归并与状态更新 | 代码节点或外部服务 |
| 闸门与决策表 | 条件分支、代码节点 |
| 知识库检索 | 检索节点 |
| 接口与工具调用 | HTTP 或工具节点 |
| 输出渲染 | 模型节点 |
| 格式与状态一致性验证 | 代码节点 |
| 长期状态、并发、原子提交 | 数据库或外部服务 |
| 回归测试与发布门禁 | 独立测试系统或持续集成 |
这张表里最值得注意的是最后两行。它们往往需要平台提供更强的基础能力,或者干脆接入外部基础设施——跨会话持久化、并发控制、原子提交、可回放的版本化测试,本来就属于另一类设施。
倒数第三行则是另一种情况:格式与状态一致性验证既可以放在工作流的代码节点里,也可以交给外部验证器,放在哪里取决于平台能力和业务风险。关键不是它在不在平台内,而是它必须有一个独立、确定、可重复执行的权威实现。
而对于包含跨轮状态或外部副作用的系统,能不能上生产往往取决于最后两行有没有着落,而不是前面几行画得漂不漂亮。
还有一件事这一节必须说清楚:换了载体,第 1 章那个病不会自动痊愈。
那份两千两百行提示词的问题——同一个判断在四处各写一遍、字段只有“有值/没值”、没有可验证的提交边界——搬到工作流平台上,也可能变成几十个节点、谁也不敢动的画布:同一个条件在三条分支里各判一次、连线交叉到看不清、改一个节点说不准会影响哪几条路径。症状换了形式,病因一模一样。
所以这一节的结论是:
可视化工作流能够承载提示词系统,但不能代替提示词系统的设计。画出节点解决的是“流程放在哪里运行”,槽位、证据、状态、闸门和契约解决的是“每个节点依据什么运行、以及怎么证明它运行正确”。
五、真实项目停在哪里:提示词内的代码式结构
现在回到贯穿全书的那个项目。第 1 章讲过,它从两千两百行压到约一百七十五行,后来又涨回七百多行。那次骨架重构确实做了减法,也确实改变了系统的组织方式,但需要把事实说准:格式校验、计数、槽位、状态转换和检查清单并没有整体迁入外部代码或持久化存储。它们仍在提示词里,只是从散乱的自然语言补丁,变成了更接近代码的显式结构。
真实发生的变化更接近下面这样:
改造前 改造后
───────────────────────────────── ─────────────────────────────
同一规则散落在多个章节 按槽位、闸门、状态机集中定义
靠"最高优先级"压住冲突 建立相对明确的执行顺序
字段只有有值/没值 显式区分未知、有效、拒绝等状态
故障后继续追加自然语言补丁 用规则表、伪代码和检查清单表达
这些结构让规则更容易查找,让冲突更容易暴露,也让回归用例更容易绑定到具体机制。但它仍然有一个根本限制:最终读取状态、执行规则、计算次数并完成自检的,还是模型。 伪代码写得再像程序,也不会自动获得程序的确定性;写在提示词里的“状态”,也不会自动变成跨会话持久化状态。
生产观察 贯穿案例能够证明的是:把职责在提示词内部显式分开,比继续堆叠自然语言补丁更容易维护和定位问题。它不能证明项目已经完成了外部代码迁移,也不能证明外置之后通过率提高了多少。
因此,提示词内的代码式结构不是失败方案。对平台能力有限、暂时无法改应用层的项目,它是一种有价值的中间形态;更重要的是,它为后续迁移准备了边界。只有先说清哪些是证据、哪些是状态、哪个闸门拥有权威,才知道将来应该把哪一块搬到哪里。
把这套内部结构继续迁往目标运行时,职责会发生第二次变化:
提示词内的中间形态 目标运行时
───────────────────────────────── ─────────────────────────────
用文字要求模型检查格式 代码函数校验并返回明确结果
用上下文提醒模型记住计数 状态存储累加并可回放
用伪代码描述闸门与状态转换 确定性组件执行并单独测试
用发送前清单要求模型自检 独立验证器拥有最终否决权
右边是本书提出的工程方向,也是配套参考实现演示的形态;它不是原项目已经完成的历史记录。
六、搬家不是把逻辑藏起来
有一个常见的误解要澄清:外置不是“把规则从提示词里删掉”,而是“把规则搬到一个能保证做对的地方”。
搬错了地方,会出两种问题。
第一种:把需要判断的东西也硬搬进代码。 “用户这句话算不算拒绝”——如果为了“确定性”用一堆关键词匹配(含“不”、含“不方便”就算拒绝),很快会崩:用户说“不然还是留个电话吧”也会被匹配成拒绝。判断性的工作硬写成规则,得到的不是确定性,是一个更脆的模型。 这类判断就该留给模型。
第二种:把该外置的逻辑留在提示词里,只是嘴上说“交给代码”。 比较隐蔽的形态是——代码里确实做了格式校验,但提示词里“以防万一”又写了一遍。两处逻辑一旦不一致,就会出现“代码放行、模型拒绝”或者反过来的诡异现象,而且极难排查。同一条规则只能有一个权威实现,这和第 8 章“状态只有一个权威来源”是同一条原则。
判断搬得对不对,回到第二节那条四档判据就行:能按明确规则计算的、能从权威事实源查询的,以及业务已经定下策略的,适合交给代码;必须理解开放语言的,留给模型。 搬反了,两边都会更差。
七、建议的外置顺序:先搬最疼的
一个正在运行的系统,不可能停下来一次性重构。外置要有优先级。优先搬那些又高频、又容易出错、又完全确定的:
第一批 格式校验(手机号、邮箱、金额、日期)
—— 高频、纯确定、错了就可能漏掉后续处理,收益最大
第二批 计数与预算(问了几次、还能不能问)
—— 第 15 章反复用到,留在提示词里必然不准
第三批 状态标记与流转(已发、已拒绝、已过期)
—— 第 10-14 章的整套状态,本就该是代码
第四批 组合判断(闸门、决策表、不变量)
—— 逻辑最密,收益最大,但依赖前三批先就位
顺序背后有个依赖关系:闸门要读状态,状态要靠归并,归并要靠结构化的证据。 所以不能从闸门开始搬——底下的状态层还在提示词里,闸门搬出去也没有可靠的东西可读。从数据往控制搬,不能反过来。 这和第 5 章那条“下层没做扎实,上层机制只是把混乱组织得整齐些”是同一个道理。
但有一类东西不受这个“先搬最疼、慢慢来”的节奏约束:
权限、安全、合规,以及任何不可逆的外部副作用,不需要等提示词变长才外置。只要系统开始产生真实的外部影响,这些就应该从第一天起放在确定性的执行层。 这属于第 5 章说的“证据驱动不是事故驱动”——它们是已知风险,不是等出了事才处理。
除此之外的确定性逻辑,才适用下面这条成本权衡。
适用边界 外置有它的成本:多一层代码、多一套测试、多一处要维护。对一个还在停靠点 A、B 的简单系统,把一切都外置是过度工程——它的提示词本来就不长,也没有多少确定性逻辑可搬。外置的收益,通常要到系统已经积累了较多确定性逻辑、且提示词开始因此膨胀时才真正显现。 在此之前,先把职责显式分开,可能就是更合适的停点。
八、搬完之后,模型评测会发生什么变化
如果把确定性逻辑真正搬走,会出现一个容易被忽略的结果:留给模型的任务,平均难度反而可能上升。
道理不难理解。原来模型的工作里混着一批它本来就能轻松做对的简单事(照着格式念、机械地计数),这些事拉高了它的整体正确率,也掩盖了它在真正困难的判断上的表现。把简单的确定性工作拿走之后,剩下的全是“这句话到底什么意思”“这两个需求是不是一回事”这种硬骨头。
这会带来两个值得预期、也需要在迁移后重新测量的变化:
第一,模型评测会更聚焦。 它的通过率不再被一批确定性任务稀释,更直接地反映语义判断和表达能力。哪些判断它做不好,会更容易暴露;第 19 章的评测主要测这一部分。
第二,优化会更有针对性。 团队不再需要为了“手机号偶尔数错”反复调整提示词,可以把精力放在真正需要模型的地方:更准确地识别意图、处理改口和组织表达。
分工清晰的另一面,是让每一部分都去做它最难、也最擅长的那件事。 代码做它能做到极致的确定性计算,模型做它不可替代的语言理解——而不是两边都做一半,互相拖累。
九、从“哪些该外置”到“怎么外置”
这一章回答了“什么交给模型、什么交给代码”。但它停在了分工层面——真到了动手那一步,还有一串更细的问题:
- 模型那一次调用,内部其实还混着好几件事:从输入里抽取、根据状态做判断、把结果说成话。这三件事挤在一次调用里,会互相污染吗?
- 代码算出了状态和动作,怎么“喂”给模型去表达,才能保证它不擅自改动已经定好的决定?
- 抽取、决策、渲染如果拆成几次调用,它们之间靠什么约束彼此,才不会一层改一层的结论?
也就是说,“交给模型”这件事本身,还需要被进一步拆开和约束。一次模型调用不是一个黑盒,它内部同样有职责边界。
本章最后只留一条原则,它也是第四篇的总原则:
凡是已经能够被明确写成规则、状态、权限或业务策略的决定,就不要在运行时重新交给模型自由发挥;真正留给模型的,是规则无法替代的语义理解与语言表达。