第 16 章 模型与代码的分工

第三篇讲完了“让对话拥有状态”的全套设计:字段、证据、归并、闸门、状态机、不变量。

但那还只是设计。这一篇讲怎么把它落成一套工程化运行时。而落地的第一个、也是最根本的问题是:这些机制,哪些交给模型,哪些必须交给确定性执行层。

图 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 章的评测主要测这一部分。

第二,优化会更有针对性。 团队不再需要为了“手机号偶尔数错”反复调整提示词,可以把精力放在真正需要模型的地方:更准确地识别意图、处理改口和组织表达。

分工清晰的另一面,是让每一部分都去做它最难、也最擅长的那件事。 代码做它能做到极致的确定性计算,模型做它不可替代的语言理解——而不是两边都做一半,互相拖累。

九、从“哪些该外置”到“怎么外置”

这一章回答了“什么交给模型、什么交给代码”。但它停在了分工层面——真到了动手那一步,还有一串更细的问题:

  • 模型那一次调用,内部其实还混着好几件事:从输入里抽取、根据状态做判断、把结果说成话。这三件事挤在一次调用里,会互相污染吗?
  • 代码算出了状态和动作,怎么“喂”给模型去表达,才能保证它不擅自改动已经定好的决定?
  • 抽取、决策、渲染如果拆成几次调用,它们之间靠什么约束彼此,才不会一层改一层的结论?

也就是说,“交给模型”这件事本身,还需要被进一步拆开和约束。一次模型调用不是一个黑盒,它内部同样有职责边界。

本章最后只留一条原则,它也是第四篇的总原则:

凡是已经能够被明确写成规则、状态、权限或业务策略的决定,就不要在运行时重新交给模型自由发挥;真正留给模型的,是规则无法替代的语义理解与语言表达。