2  不确定性到底在哪里

在讨论怎么对付不确定性之前,得先搞清楚它住在哪。这听起来像一个务虚的问题, 但它决定了一件很实际的事:你会把工程投入放在哪一侧。如果不确定性住在采样里, 那么该做的是更好的解码;如果它住在别处,那么在解码上花的每一分钟都是浪费。

我要先讲一个几乎正确、但错在关键处的模型。之所以从错的讲起,是因为它正是 大多数人(也包括我自己很长一段时间)实际在用的模型,而且它错的那种方式 很有教育意义 —— 它错得足够”对”,以至于能生成正确的行为,却预测不了下一步。

2.1 有诱惑力的那个模型:模型是编解码器

这个模型是这么说的:

大模型本质还是编解码,和视频、音频一样。不一样的是解码过程不确定, 所以才有了提示词和 harness 这类技术。

这个说法比大多数人的直觉更接近对,而且它有几分不是比喻,是字面事实。 Transformer 最初就是一个编码器-解码器结构,GPT 那一系在文献里的正式名称 就叫 decoder-only;自回归生成在论文里的术语就是 decoding, 贪心解码、束搜索、核采样、温度,全都叫解码策略。

还有更深的一层。训练一个语言模型等价于学一个概率分布,而学一个概率分布 等价于构造一个最优编码 —— 交叉熵损失的单位就是比特,它字面上就是期望码长。 所以”这是一个编解码器”这句话,在信息论的意义上是准确的,不是打比方。

「和视频、音频一样」这半句比它看起来的还要准。把一个大语言模型当作 概率模型接上算术编码,它压缩图像的效果可以好过专门的图像编解码器, 压缩音频的效果可以好过专门的音频编解码器。一个语言模型能在图像和音频 编解码器自己的赛道上赢它们 —— 这不是类比,这是同一件事。

2.1.1 这个模型的实践价值:它剥掉了拟人化

在指出它错在哪之前,得先说清楚它对在哪,因为这部分的价值被严重低估了。

Agent 工程里最普遍的失败,是把模型当成一个”能理解”的同事。一旦这么想, 修法就会滑向一个方向:我再解释清楚一点、让它仔细一点、在提示词里加一句 “请认真思考”。这条路的收益递减得非常快,而且它会让人在系统失败时归因错误 —— 把”我没有验证手段”读成”我没说清楚”。

编解码器这个框架让那类修法一眼看上去就很蠢:你不会请求一个解码器再努力一点。 它直接导向了我的一条核心主张 —— 把规则写进结构,而不是写进文档。 如果对面是一个解码器,那你能控制的只有两样:喂进去的东西, 和对吐出来的东西做什么检查。“让它记住”根本不在选项里。

这条直觉是对的,整个第二部都建立在它上面。

2.2 但它错在三处,一处比一处要紧

2.2.1 一、编解码器的编解码是对某条消息的互逆,这里不是

视频编解码器编的是这一段视频,解出来还是那段视频 —— 存在一个真值消息, 而编解码器是它上面的一个双射(无损时严格互逆,有损时在某个度量下逼近)。 你可以问”解码正确吗”,因为有一个东西可以拿来比。

语言模型没有这个东西。压缩发生在训练时,压的是一个分布,不是一条消息; 推理时,提示词不是输出的压缩表示,你没法”解出原本想要的那个答案”, 因为从来就不存在那个答案。这个差别可以画出来:

flowchart TB
    subgraph CODEC["视频编解码器"]
        direction LR
        M1[原始视频] -->|编码| S1[码流] -->|解码| M2[还原的视频]
        M2 -.->|"可以和原始比对<br/>有真值"| M1
    end
    subgraph LLM["语言模型"]
        direction LR
        C[训练语料] -->|训练时压缩| W[(权重<br/>= 一个分布)]
        P[提示词] -->|条件化| W
        W -->|采样| O[输出]
        O -.->|"和什么比对?<br/>没有真值"| X(( ))
    end
    style X fill:none,stroke:none

编解码器这个框架暗示着”有一个正确答案正在被重建”。没有。 只有一个分布正在被采样,而”正确”这个词在那个分布上没有定义。

2.2.2 二、不确定性不是解码器的缺陷,而是构成性的

这是三处里最要紧的一处,而且它有一个干净的实验可以证伪那个模型。

你今天就可以把解码完全确定化:温度设零、贪心解码、固定随机种子、 用确定性的算子。做完之后同样的输入会给出逐字节相同的输出 —— 在”解码”这个层面上,不确定性被彻底消灭了。

然后问题一点都没有解决。贪心解码的模型照样会给出错误的实现,照样会在 提示词的微小改动下给出完全不同的答案,照样会自信地写出一个编译得过、 跑得起来、逻辑是错的函数。把解码器变确定,并不带来结果的确定 —— 这是那个模型预测不了的事实,而它是整件事的关键。

真正的不确定性来源不是采样在掷骰子,而是从提示词到输出分布的这个映射 本身就是你无法完整指定的。这是一个规格问题,不是一个解码问题。 打个不那么严谨但很说明问题的比方:一个随机数生成器的不确定性, 换个种子就没了;而一个你不知道它在算什么的函数,你把输入固定住, 它依然会给你一个你预测不了的结果 —— 只是这次它每次都给同一个 你预测不了的结果。

2.2.3 三、提示词不是编码,harness 也不是在补偿解码噪声

编码器是有规范的:给定这一帧,按标准产出这段码流,任何符合标准的实现 产出的结果都一样,而且这个过程有逆。提示词没有这样的规范,也没有逆 —— 两个”意思一样”的提示词会给出不同的分布,而你无法从输出反推出应该给什么提示词。

harness 这一侧的偏差更明显。如果问题真的出在”解码器会掷骰子”, 那修法就应该在解码器上:约束解码、语法约束、结构化输出、logit 偏置。 这些东西确实有用,但请注意 —— 它们不是我讲的那套系统在做的事。

那套系统做的是拿外部的真值去验证输出:测试、类型检查、依赖图查询、退出码。 这不是在修编解码器,这是把编解码器放进一个带测量的回路里。

2.3 换一个模型

把上面三条错误反过来,就得到一个能用的模型:模型是一个对分布的有损压缩, 而推理不是解码某条消息,是从一个你无法完整指定的条件分布里采样。

在这个模型下,三个概念各归其位。提示词不是编码,是条件化 —— 它选中分布的一个区域,没有逆,也没有规范。采样确实是解码,这一条原来的 说法是对的。harness 不是在降噪,它是给一个开环对象闭上环 —— 加测量、加反馈。

最后一条是全书的枢纽,用控制论的语言说就是:

模型是一个高增益、非线性、无法完整建模的被控对象。 你不试图让它变确定,你在它外面闭一个回路。

flowchart LR
    R[("你想要什么<br/>参考输入")] --> E{比较}
    E -->|误差| CTL[条件化<br/>提示词 · 上下文 · 规则]
    CTL --> P["被控对象<br/>(模型)"]
    P --> O[输出]
    O --> S["测量<br/>测试 · 类型 · 依赖图 · 退出码"]
    S -->|"实测值"| E
    FF[["前馈<br/>把规则写进结构"]] -.->|"在误差出现前就补偿"| CTL
    style P fill:#fdf3e3
    style S fill:#e8f4e8
    style FF fill:#eef2fb

这张图里有三样东西值得先记住,第四部会逐个展开。测量那一格是这套系统 存在的理由 —— 没有它,整条回路是断的,你只是在开环地往外发指令。前馈 那条虚线是性能的主要来源,它在误差出现之前就把大部分错误路径堵掉了。 参考输入(输出应该是多少,见 小节 20.2)那一格 来自回路之外,这一点在 章节 20 会成为一整章的主题。

这句话不是修辞,它有一个可以检验的推论:如果这个说法成立,那么这套系统 应该长得像一个标准的控制回路,包括控制论里那些不显然的部件 —— 传感器的故障管理、观测器(小节 19.3)、死区、抗积分饱和。第四部会逐条检验, 而结论是它确实长成了那个样子,甚至长出了几个我当时并不知道有名字的部件。

(这段话里的每个术语在 附录 D 都有一句话的解释。 现在不需要全懂,只需要接受一件事:这是一个有名字的东西, 而那个名字背后有七十年的现成经验。)

2.4 常见的反驳,以及它为什么不成立

前面那个”把温度调到零”的论证,最常见的反驳是:

贪心解码确实还会出错,但错误变得可复现了 —— 而可复现的错误是可以被逐个修掉的。

这个反驳听起来有力,但它在实践中不成立,原因有两层。

第一层是可复现性绑定在整个输入上,而输入每次都不一样。同一个提示词 加同样的种子,输出确实一样 —— 但真实的 Agent 工作流里,提示词从来不是 同一个:它包含了当前的文件内容、之前几轮的对话、工具返回的结果, 而这些每次都在变。所以你得到的不是”可复现的错误”,是”在一个你永远不会 再次遇到的精确输入下可复现的错误”,而这种可复现性没有工程价值。

第二层更根本:它假设错误的数量是有限且可枚举的。修掉一个可复现的错误, 前提是这类错误的总数不太多;而模型的错误不是从一个有限清单里抽出来的, 它们是从一个连续空间里生成的 —— 你修掉这一个,下一个是它的邻居, 长得不一样但成因相同。你会一直修下去,而剩下的数量不减。

两层合起来,“确定化解码”这条路为什么走不通就清楚了:它把不确定性 从输出层移到了输入层,而输入层的变化你控制不了。

2.5 那些真正有用的确定化手段在哪一层

说清楚这一点很重要,因为这一章不是在反对所有的确定化努力。 有一类确定化是真正有效的,而它们的共同点是: 它们作用在输出的形状上,不作用在输出的内容上。

手段 它保证什么 它不保证什么
结构化输出 / 语法约束 输出能被解析 输出是对的
工具调用的参数校验 调用格式合法 调用该不该发生
有限的动作空间 它只能做这几件事 它做的是对的那件

这些都值得做,而且成本很低。 它们消除了一整类噪声 —— 解析失败、格式错误、参数缺失。

但它们消除的是形式上的不确定性,而我讲的是 语义上的不确定性 —— 一个格式完美、参数合法、 在允许的动作空间之内、而且完全错误的输出。

后者是形式手段够不着的,而它需要的是外部的真值。 这就是全书的方向。

2.6 三个模型的对照

把三种常见的心智模型放在一起,看它们各自会导向什么动作:

模型 它把模型看成 于是修法是 它的盲区
拟人化 一个会理解的同事 解释得更清楚、让它更认真 它不会因为被说服而改变行为
编解码器 一个解码器,只是解码不确定 把解码确定化、优化提示词 确定化解码不解决问题
被控对象 一个高增益非线性系统 在外面闭一个带测量的回路 需要外部真值,而真值不总是有

第一行是最普遍的,也是最贵的。

它贵在两个地方:一是它导向的动作(改提示词、加强调) 收益递减得非常快;二是它会让人在系统失败时归因错误 —— “是我没说清楚”而不是”我没有验证手段”。

这两个模型的差别在实践中有一个很好的判别式:

当一个 Agent 做错了一件事,你的第一反应是什么?

  • “我得把要求写得更清楚” → 你在用拟人化模型
  • “我得让它做错这件事变得不可能” → 你在用被控对象模型

第二种反应会导向结构性的解法,第一种不会。

2.7 这个模型对读者的直接用处

理论部分容易读起来像装饰,所以说清楚它的三个直接用处:

一、它告诉你哪些努力是徒劳的。

优化提示词的收益有上限,而且上限比大部分人以为的低。 一旦你接受”这是一个规格问题而不是解码问题”, 你就不会在提示词工程上无限投入。

二、它告诉你该建什么。

一个开环系统需要的是测量。 所以问题从”怎么让它输出更好”变成”怎么知道这次输出好不好”—— 而后者是一个工程问题,有确定的解法。

三、它告诉你什么时候可以停。

小节 16.11 讲过:当瓶颈不在判定这一侧时, 继续加固判定是在优化一个不是瓶颈的环节。 “哪里是瓶颈”这个问题,只有在有了系统模型之后才能回答。

没有模型的时候,人们优化的是最容易优化的那个部分, 而那通常不是瓶颈。

2.8 拿这个模型做一次具体的决策

一个模型的价值要在它改变一次具体决策的时候才兑现,所以走一次。

处境:你的 Agent 在生成 API 客户端代码时, 偶尔会漏掉一个必填的请求头。三次里大概错一次。 你手上有三个选项,而三个模型会给出三个不同的答案。

拟人化模型会说:在提示词里强调这个请求头很重要, 最好加粗、加感叹号、放在最前面。这条路的问题不是它无效 —— 它通常真的能把错误率从三分之一降到十分之一 —— 问题是它的收益到此为止,而且你不知道它到此为止了。 你会继续加强调,然后发现没用,然后归因成”这个模型不够聪明”。

编解码器模型会说:把温度调到零,固定种子, 让输出变成确定的。这条路的问题在 小节 2.4 讲过 —— 它让同一个输入的输出确定了,而你真实的处境是输入每次都不一样。 错误率不会降,它只是从”随机出现”变成了”在某些输入上稳定出现”, 而后者更难被发现。

被控对象模型会说:这不是让它别漏的问题, 是让”漏了”这件事变得可判定的问题。 具体的动作是加一条检查 —— 扫描生成的客户端代码, 断言每个请求都带那个头,缺了就失败并指出是哪个请求。 这条路的成本大概两小时,而它的性质和前两条完全不同: 它把错误率从”三分之一,而且你不知道是哪三分之一” 变成了”三分之一,而且每一个都被拦下来了”。

三条路里,前两条在试图降低 p(出错),第三条在试图把 p(出错未被发现) 降到零。只有第三条是可以被验证的—— 你可以故意漏掉一个头,看检查响不响(小节 18.2.4)。 前两条的效果你只能靠感觉。

这个例子里还有一个容易被跳过的推论:第三条路做完之后, 前两条路的价值反而上升了。有了检查之后, 把错误率从三分之一降到十分之一意味着返工轮数减少三分之二 —— 这是实打实的收益。顺序是:先让它可判定,再让它更少出错。 反过来做,你会在一个看不见的错误率上做优化, 而那和没做没有区别。

2.9 为什么”外部真值”是这套方法的前提

被控对象模型有一个前提,而这个前提值得被明确说出来, 因为它决定了这套方法的适用范围

你需要一个不依赖模型的真值来源。

在写代码这个场景里,这个真值是: 测试的执行结果、编译器的判断、依赖图的查询、退出码。 它们的共同点是:它们的正确性不依赖于模型说了什么。

这个前提在某些场景下不成立:

场景 有没有外部真值
写代码 —— 编译器、测试、类型检查
数据处理 —— schema、约束、对拍
写文档 部分 —— 链接有效性、术语一致性可测,内容质量不可测
创意写作 基本没有
开放式研究 基本没有

这套方法在有真值的场景里有效,在没有的场景里不适用。 这解释了一个现象:为什么”用 Agent 写代码”比 “用 Agent 写别的东西”更早成熟。

不是因为代码更简单,是因为代码这个领域自带了一整套 不依赖人的判断的验证工具 —— 而这套工具是几十年积累出来的。

2.9.1 那没有真值的场景怎么办

我给不出完整答案,但可以指出一个方向:

把不可判定的目标,拆成一部分可判定的约束。

写这些就是一个例子。“这些写得好不好”没有真值。 但下面这些有:

  • 每章有没有声明一个可证伪的承诺
  • 交叉引用有没有指向不存在的东西
  • 术语有没有前后不一致
  • 每个形状有没有至少一个非 Agent 领域的实例

这些约束不保证书写得好,但它们排除了一批确定写得不好的形态。 这正是 小节 20.1 那一节的核心: 约束满足不等于目标达成,但它是在没有真值时能做的最好的事。

2.10 更早的那个类比也失败了

在编解码器模型之前,还有一个更常见的类比值得一并处理, 因为很多人现在还在用它:

“把 Agent 当成一个初级工程师。”

这个类比比拟人化好一些 —— 它至少承认了需要审查、需要指导、 需要明确的规范。

但它在三个地方失败,而三个都很关键:

一、初级工程师会积累。 你指出一次问题,他下次会记得。Agent 不会 —— 每个会话都是第一天(小节 5.2)。

这一条直接推翻了”带新人”那套方法: 口头指导、逐步放权、犯错后复盘 —— 全都依赖积累。

二、初级工程师知道自己不知道。 他会问、会犹豫、会说”我不确定这样对不对”。 Agent 的置信度和它的正确性相关性很弱 —— 它会用同样的语气给出正确和错误的实现。

三、你不会同时带四十个初级工程师。 这正是这套系统的实际状态(260 个并行的工作区)。

在那个规模下,任何依赖”人来指导”的机制都会成为瓶颈 —— 这正是第一堵墙(小节 1.1)。

2.11 三个类比的共同错误

拟人化、初级工程师、编解码器 —— 三个类比错在不同的地方, 但它们有一个共同点:

三个都把注意力引向了”怎么让它输出更好”, 而不是”怎么知道输出好不好”。

  • 拟人化 → 说得更清楚
  • 初级工程师 → 指导得更好
  • 编解码器 → 解码得更确定

三条路都是在优化生成,而不是在建立验证。 我的全部主张就是:在这个问题上, 验证的边际收益远高于生成。

理由在 小节 2.9:生成的正确性没有上界可言, 而验证有一个明确的、可以被工程化的目标 —— 让每一次产出都能被判断。 而后者是一个有限的、可以完成的工程问题。

2.12 这个模型的一处代价

把模型当成被控对象,有一个必须先付的代价:你得承认自己 没有它的传递函数。 也就是说,你不知道 “输入变化多少会导致输出变化多少”, 而经典控制里几乎所有定量的设计方法都建立在那个函数上。

这个承认不是形式上的。它意味着几件很具体的事做不了: 你没法算出”提示词改这一处,输出质量会提高多少”; 没法设计一个最优的控制器; 甚至没法可靠地判断两次改动哪一次更有效 —— 因为你无法把结果的变化归因到那次改动上, 而不是归因到输入分布本身的变化上。

这一处代价换来了什么?换来的是一整套关于结构的结论仍然可用: 有延迟的系统里提高增益会失稳、前馈比反馈便宜、 测量本身需要故障管理、不可观测的状态不可控、设定点必须在环外 (小节 17.14 把这条边界画得更细)。 这五条都不需要传递函数,它们只需要”这是一个闭环”这个前提。

所以这个模型的正确用法是拿它做定性推理, 而不是拿它做定量优化。而这个区分很重要, 因为一旦有人开始说”我们给提示词做 PID 调优”, 这个类比就从一个有用的框架变成了一层看起来很专业的包装 —— 而后者比不用这套语言更糟,因为它会让人以为自己在做工程。

2.13 这个模型改变什么

一个模型的价值在于它改变你的动作。这个替换至少改变三件事:

第一,它告诉你工程投入该放在哪一侧。

如果不确定性是解码器的属性,那前沿就是更好的解码。 如果它是规格问题,那前沿是更便宜、更密的验证 —— 因为你无法指定要什么, 就只能不断地问”这是不是我要的”。后者是书里全部内容的方向。

第二,它说清了”环境”为什么比”检查”更重要。

控制论里有一个基本结论:前馈决定性能,反馈只提供鲁棒性。 翻译过来就是第二部那句话 —— 检查本身不产生质量,环境才产生质量。 小节 5.4 会把这个换算做完,包括它为什么不是修辞。 这句话在实践里是撞出来的,但它在理论上是可以被证明的。

第三,它把”不确定”和”不可控”分开了。

一个系统可以是随机的,但完全可控(比如一个带反馈的温控器, 外界扰动是随机的,但室温稳在设定点)。也可以是确定的,但完全不可控 (比如一个你没有测量手段的确定性过程)。

Agent 属于第一类。 它的输出是随机的,这件事永远不会变, 也不需要变。你要建的不是一个让它不随机的东西, 是一个让随机性不影响结论的东西。

2.14 被混淆的一组:不确定和不可靠

这一章讲了很多”不确定”,而它和”不可靠”是两件事, 混淆它们会导向完全不同的投入。

不可靠是指错误率高。 一个不可靠的组件, 你可以通过冗余、重试、更好的实现把错误率压下去 —— 这是一个可以被工程优化的量,而且它的优化方向是明确的。

不确定是指同样的输入不产生同样的输出。 它和错误率没有必然关系:一个不确定的系统可以非常准 (每次给出的都是一个正确的、但不同的实现), 一个确定的系统也可以每次都错。

这个区分决定了修法。面对不可靠,你会去提高单次成功率; 而面对不确定,提高单次成功率不解决问题 —— 因为你要的不是”这一次对”,是”每一次都能被判断对不对”。

我把重心放在判定而不是生成上,就是因为这个。 如果问题是不可靠,那正确的投入是让模型更准、提示词更好、 上下文更全;而如果问题是不确定,那些投入都有一个天花板, 因为它们改变的是分布的形状,而不是”你能不能读出这次落在哪”。

值得注意的是,两者在实践中会同时存在, 而它们的相对大小决定了你该先做哪一样。 一个判据是:把同一个任务跑五次, 看那五次里有几次是对的、以及那五次彼此有多不同。 五次全错说明你面对的主要是不可靠; 五次都对但写法完全不同,说明你面对的主要是不确定 —— 而后者恰好是我讲的这套东西的适用范围。

2.15 这个模型对”提示词”的定位

说清楚这些对提示词的态度,因为它容易被误读成”提示词不重要”。 提示词是重要的,它就是那个条件化的动作小节 2.3)—— 它决定了你在分布的哪个区域采样,而区域之间的差别可能很大。

但它有三个性质,决定了它不能是系统的主要保障:

一、它的效果无法被验证,除非有外部真值。 你怎么知道提示词 A 比提示词 B 好? 只能通过输出的质量,而那正是你要验证的东西。

二、它的收益递减很快。 从”没有提示词”到”一份清楚的提示词”,收益巨大。 从”清楚”到”非常清楚”,收益小得多。 从那之后,多写的每一句都在稀释前面的(小节 8.11)。

三、它不随规模复用。 一份好的提示词是针对一类任务的。一个仓库里有几十类任务, 每一类都需要它自己的提示词 —— skill 就是从这里来的 这种载体(小节 8.4)。

所以正确的定位是:提示词是条件化的手段, 而按到达时机分的那五种载体(小节 8.2)是提示词工程的工程化形态。

一份好的常驻文件加上一批 skill,本质上就是 “把提示词工程从一次性的手艺,变成一个有结构、 有取舍、可维护的系统”。

我讲的其余部分,是当条件化做到极限之后, 仍然需要的那些东西。

我的立场

我原本用的就是编解码器模型。这套系统是在那个模型下建成的, 而且建对了 —— 这说明一个错的模型也可以生成正确的行为,只要它错的方向凑巧 指向对的一侧。

但错的模型有代价,而且代价是具体的:它预测不了下一个失败类在哪。 章节 21 会讲这个代价在哪几个地方已经显现了。

2.16 自己动手验一次

这一章的核心论断 —— “把解码确定化不解决问题” —— 可以被读者自己验证,成本大概半小时。

步骤:

  1. 挑一个你的 Agent 最近做错的任务
  2. 把温度设成零、固定种子(如果你的接口支持)
  3. 完全相同的输入跑三次 → 确认输出逐字节相同
  4. 现在把输入改一个字:加一个空格、换一个同义词、 调整一句话的顺序
  5. 再跑一次

如果第 5 步的输出和第 3 步显著不同 —— 而它通常会 —— 那么你刚刚验证了:确定性住在”输入到输出的映射”里, 而不是住在采样里。

这个实验还有一个副产品:它会让你对”提示词调优” 的收益上限有一个直观的感受 —— 因为你会看到一个字的改动能造成多大的差别, 而你没法穷举那个空间。

这就是 小节 2.15 那三条性质的第一条在实验室里的形态: 提示词的效果无法被验证,除非有外部真值。

2.17 错的模型为什么能生成对的行为

我花了一半篇幅讲一个错的模型。

因为那个模型正是我建这套系统时用的模型,而它建对了。

所以有两件看起来矛盾的事要同时说清楚:

  1. 那个模型是错的(三处,小节 2.2
  2. 它生成的行为是对的(剥掉拟人化,导向”写进结构”)

调和这两点的是一个更一般的观察:

一个错的模型,如果它错的方向凑巧指向对的一侧, 可以生成正确的行为 —— 但它无法预测下一步。

编解码器模型把注意力从”模型的意图”移到了”输入和输出”, 这一步移对了。 它只是把移动的距离算错了 —— 它以为终点是”更好的解码”,而实际的终点是”外部的验证”。

这个区分对读者的实际价值是

如果你现在用的是编解码器模型(很多人是), 你不需要推翻你已经建的东西 —— 大概率它们是对的。 你需要的是换一个模型来决定下一步建什么。

小节 2.13 那三条就是换模型之后 立刻能得到的三个答案。