Skip to content

把不确定性当缺陷

最隐蔽的反模式

前面五篇文章讨论的反模式都有明确的外在表现:拼接字符串、绑定单一模型、忽略成本、误用模型能力、过度依赖框架。它们是可以通过代码审查或架构审计发现的。

本文讨论的反模式不同。它藏在开发者的心智模型里,不体现为任何一行能挑出来的错误代码,而是体现为一种普遍的错误倾向:把 LLM 输出的不稳定当作必须消灭的缺陷,然后投入大量工程努力试图消灭它。

这种倾向的外在表现是:prompt 越写越长,规则越堆越多,约束越加越紧,temperature 设为 0,重试逻辑越来越复杂——一切都是为了让模型的输出"稳定下来"、"可预测"、"每次都一样"。

这个目标不现实。

不确定性是天生的

第一章详细分析了自回归生成的本质:模型在每一步选择下一个 token 时,基于的是一个条件概率分布。即使 temperature 设为 0(贪心解码),不同的硬件、不同的批处理大小、不同的精度设置仍然可能导致不同的输出——因为浮点运算在不同环境下的舍入行为不完全一致。

更根本的是:即使你在某个特定环境下实现了完全确定性的输出,你得到的也只是"概率最高的 token 序列"。概率最高不等于最优。在很多任务中,采样带来的多样性恰恰是产生高质量输出的必要条件。

用一个计算数学的类比:概率性对 LLM 来说,就像舍入误差对浮点运算一样。你不能消除浮点运算的舍入误差——这是 IEEE 754 表示的本质属性。你能做的是理解误差的传播规律,设计数值稳定的算法,在关键点进行误差检查。试图消除舍入误差本身,是对计算本质的误解。同样,试图消除 LLM 输出的不稳定性本身,是对生成模型本质的误解。

无限堆叠约束的失败模式

当开发者试图通过堆叠更多规则来消除输出的不稳定性时,会进入一个恶性循环。

第一阶段:发现模型的输出不够"稳定",于是在 prompt 中添加更多规则。"必须按以下格式输出"、"不得包含任何额外信息"、"严格遵循以下模板"。

第二阶段:更多的规则产生了新的问题。规则之间出现冲突——满足规则 A 的输出可能违反规则 B。模型在多个约束之间"挣扎",输出质量反而下降。第一章讨论过这个现象:复杂指令的遵从率随指令数量增加而下降。

第三阶段:为了应对规则冲突导致的质量下降,添加更多的规则来处理冲突。Prompt 从 200 token 膨胀到 2000 token。开发者花费大量时间调优 prompt 的措辞,每一个词的修改都可能引发蝴蝶效应。

第四阶段:维护成本失控。一个 2000 token 的 prompt 变成了"碰不得的遗留代码"——没有人敢修改它,因为没有人完全理解每一条规则的存在理由和它们之间的相互作用。这和传统软件中的"大泥球"架构是同一个问题。

这个循环的根源是目标就错了:试图在 prompt 层面实现确定性控制,但 prompt 本来就提供不了确定性控制。

正确的思路:在架构层面处理不确定性

第二章的核心主张是:不确定性是约束条件。这个主张在这里有最直接的工程含义。

正确的思路是设计一个能够容忍输出波动的系统架构。

结构化输出 + 验证。 不要试图让模型每次都输出完美的结果,而是定义一个输出的结构规格(用 Pydantic 模型或 JSON Schema),让验证器检查输出是否满足规格。不满足时重试或回退。这把"消灭不稳定性"的不可能任务转化为"检测并处理不合格输出"的确定性问题。

python
from pydantic import BaseModel, Field, ValidationError
from typing import Literal


class AnalysisResult(BaseModel):
    category: Literal["positive", "negative", "neutral"]
    confidence: float = Field(ge=0.0, le=1.0)
    key_phrases: list[str] = Field(min_length=1, max_length=5)


def robust_analyze(text: str, max_retries: int = 3) -> AnalysisResult | None:
    """不追求每次输出完美,而是在架构层面处理不完美。

    模型可能输出不同的 key_phrases,confidence 可能波动——
    这些变异是可接受的,只要它们落在类型约束定义的合法范围内。
    真正需要拦截的是结构性错误:格式不对、字段缺失、值越界。
    """
    for attempt in range(max_retries):
        raw_output = call_llm(text)
        try:
            return AnalysisResult.model_validate_json(raw_output)
        except ValidationError:
            continue  # 结构不合格,重试
    return None  # 多次重试仍失败,触发降级逻辑

多次采样 + 聚合。 对于关键决策,不依赖单次输出,而是多次采样后聚合结果。分类任务取多数票,数值估计取中位数,生成任务用另一个模型评选最佳。样本量越大,估计越可靠,这是统计学的基本原理。

概率性输出 + 确定性后处理。 LLM 生成的是初稿,确定性代码负责校验和修正。日期格式不对?用正则表达式修正。数值超出合理范围?用规则裁剪。引用的实体不存在?查数据库验证。确定性后处理是对两种计算范式的合理分工。

与不确定性共存

这个反模式的根源不完全是技术问题,也有心理因素。

从确定性编程背景转向 LLM 开发的工程师,习惯了"相同输入 -> 相同输出"的世界。sort([3,1,2]) 永远返回 [1,2,3],不会因为运气不好返回 [1,3,2]。这种确定性是传统软件工程的基石,是调试、测试、推理的前提。

面对一个本质上概率性的系统,第一反应是恐惧和排斥——"它不可靠"、"它不可控"、"它不可测试"。这些判断套用的是确定性系统的标准,但这套标准在概率性系统里不管用。

第二章讨论的投资决策框架在这里提供了一个类比:波动性是市场的本质属性。试图消除波动性的投资者(频繁交易、过度对冲、追求零风险)通常比接受波动性的投资者(持有优质资产、容忍短期波动、关注长期收益)获得更差的结果。道理一样:跟系统的本质属性对着干,代价很大, 在 LLM 工程中,与其花 80% 的精力试图消除输出的不稳定性(不可能完全成功),不如花 20% 的精力设计容忍输出波动的架构(确定性的、可测试的、可维护的),把剩下的精力用于提升系统在真正重要的维度上的质量。

关键是认清标准应该设置在哪里。标准不应该是"每次输出完全相同"——这对概率性系统来说是一个错误的目标。标准是"每次输出都落在可接受的范围内,不可接受的输出被可靠地识别和处理"。后者是一个正确且可实现的目标。