我把大模型当成一个同样的输入必然给同样输出的普通函数来用,做了缓存、写了断言固定结果的测试,结果缓存老是不命中、测试三天两头挂,排查半天才明白大模型本质是概率采样、压根不保证每次输出一字不差的深度复盘

我在系统里接了个大模型让它根据输入生成结构化结果,下意识把它当普通函数对待——就像 f(x):同样的 x 必然得到同样的 f(x)。基于这个天经地义的假设我做了两件事:给输出做缓存(以为同输入算一次存下来就能复用),写单测断言输入这段输出必须等于那段固定文本。可上线和跑测试后怪事接连不断:测试三天两头失败,同样输入这次输出和我断言的标准答案差了几个字、措辞调整了下断言就挂;缓存逻辑也总出问题。更抓狂的是毫无规律,有时输出和上次一样有时又冒出个意思相同用词不同的版本。我以为是代码有随机性、缓存 key 算错了,查半天对不上,直到把同一段输入连续喂十几次、把输出并排一看才恍然:同样输入大模型每次输出竟都略有不同。复盘才懂:大模型本质是概率模型,每步按概率分布采样下一个词,同输入完全可能走出不同但都合理的措辞路径,它给的是一个符合要求的答案而非那个唯一确定的答案;即便 temperature=0 多数 LLM 也不保证逐字一致(浮点累加、并行、后端版本)。我把概率生成器当成了确定性函数,在不成立的地基上盖了断言固定输出的测试和依赖输出一致的缓存。正解是别依赖逐字一致、改依赖语义结构正确:测试断言不变量(能解析、字段齐、含关键信息)而非固定文本,缓存以输入为 key 自己实现幂等,输出做结构校验与重试容错。这篇复盘从故障现场讲到 LLM 为何非确定、能依赖什么不能依赖什么、怎么诊断,再到断言不变量、输入缓存、容错重试的完整正解,以及格式假设、精确对账、概率当必然、外部依赖等同类坑,和接触新事物最危险的是用旧范式的隐含前提去套它、要识别新事物打破了哪条旧规则的认知。

我把大模型当成一个"同样的输入必然给同样输出"的普通函数来用,做了缓存、写了断言固定结果的测试,结果缓存老是不命中、测试三天两头挂,排查半天才明白大模型本质是概率采样、压根不保证每次输出一字不差的深度复盘

这是一次让我对"同样的输入,是不是一定给同样的输出"这个我从未怀疑过的前提,有了刻骨认知的事故。我在系统里接了个大模型(LLM),让它根据输入生成一段结构化结果。我下意识地把它当成一个普通函数来对待——就像 f(x):同样的 x,必然得到同样的 f(x)。基于这个"天经地义"的假设,我做了两件事:一是给它的输出做了缓存(以为同样输入算一次、存下来就能复用);二是写了单元测试,断言"输入这段、输出必须等于那段固定文本"。

可上线和跑测试后,怪事接连不断:那个测试三天两头失败——同样的输入,这次输出和我断言的"标准答案"差了几个字、调整了下措辞,断言就挂了;而我做的缓存逻辑也总是出问题。更让我抓狂的是它毫无规律:有时输出和上次一模一样、有时又冒出个意思相同但用词不同的版本。我一度以为是我代码哪里有随机性、是缓存 key 算错了,查了半天全对不上。直到我把同一段输入连续喂给大模型十几次、把每次输出并排一看,才恍然大悟:同样的输入,大模型每次的输出竟然都略有不同!——我从一开始就错得离谱:大模型根本不是一个"同样输入必给同样输出"的确定性函数。

故障现场:同样的输入,大模型每次输出都不完全一样

我把那个"同输入、不同输出"的现象还原出来,问题一目了然:

# 我的错误假设: 把 LLM 当成确定性函数 f(x), 同 x 必同 f(x)
prompt = "用一句话总结这段文字: ..."

# 同样的 prompt, 连续调用多次, 看输出:
for i in range(5):
    print(llm(prompt))
# 输出(每次都略有不同!):
#   "这段文字主要讲了 A 和 B。"
#   "本文主要阐述了 A 与 B 两点。"          ← 意思一样, 措辞不同
#   "这段文字主要讲了 A 和 B。"          ← 又和第一次一样了
#   "文章核心是 A、B 两个方面。"            ← 又变了
#   ...
# ↑↑↑ 同样输入, 输出时同时不同 → LLM 不是确定性函数!

# 于是我基于"确定性"做的东西全崩了:

# 1) 测试: 断言输出等于固定文本 → 时灵时不灵
def test_summary():
    assert llm(prompt) == "这段文字主要讲了 A 和 B。"   # 经常 != → 测试挂 ✗

# 2) 缓存: 我曾想"对输出做指纹当 key" 或 "比对两次输出是否一致"
#    → 输出不稳定, 指纹对不上 / 比对总不相等 ✗

# 注: 即便设 temperature=0, 多数 LLM 也【不保证】100% 逐字确定
#     (浮点累加顺序、并行计算、后端实现等都会引入细微非确定性)

看着那五次"意思相同、措辞各异"的输出,我才彻底明白:大模型本质上是一个概率模型——它在每一步根据概率分布采样下一个词,所以同样的输入,完全可能走出不同(但通常都合理)的措辞路径。它给的是"一个符合要求的答案",而不是"那个唯一确定的答案"。哪怕我把随机性参数 temperature 调成 0、让它尽量选概率最高的词,大多数大模型在工程实现上仍不保证 100% 逐字一致(浮点运算顺序、并行、后端版本等都会带来细微差异)。我却理所当然地把它当成了 f(x)=同样结果 的确定性函数,在这个根本不成立的地基上,盖起了"断言固定输出的测试"和"依赖输出一致的缓存"——地基一塌,上面全垮。我要的是"每次都一字不差",而它给的是"每次都对、但不保证一字不差"。

第一件事:搞懂大模型的非确定性——它是概率采样,不是确定性函数

冷静下来,我去把"LLM 的非确定性"这一课认真补了,才明白我错在了哪个最根本的假设上:

【大模型为什么"同样输入、不保证同样输出"】

LLM 的本质:
  - 它是个【概率模型】: 给定上文, 它算出"下一个词"的【概率分布】,
    然后从这个分布里【采样】一个词, 如此逐词生成
  - temperature 等参数控制采样的"随机程度":
      temperature 高 → 更随机、更有创造性、输出更发散
      temperature 0  → 尽量选概率最高的词(贪心), 趋于稳定
  - 但即使 temperature=0, 多数生产级 LLM 也【不保证】逐字完全一致:
      浮点运算的累加顺序、GPU 并行、批处理、后端版本变化……
      都会引入细微的非确定性, 可能让某一步选了另一个近似概率的词
      → 一步不同, 后续可能"差之毫厘谬以千里", 整段措辞都变了

关键结论:
  LLM 不是 f(x)=确定结果 的【确定性函数】, 而是【概率性生成器】:
  - 同样输入, 它给的是"一个合理的答案", 不保证是"那个唯一的、逐字固定的答案"
  - 你能依赖的, 是输出的【语义/意图/结构大致正确】,
    【不能】依赖输出【逐字逐句完全一致】

我的根本错误:
  把一个【概率性、非确定】的东西, 当成了【确定性函数】,
  并在这个错误前提上, 建了"断言固定输出""依赖输出一致"的逻辑 → 必崩。

这一下点醒了我:我所有问题的根源,都在一个我从未质疑过的前提上——"同样的输入,一定得到同样的输出"。这个前提对传统的、确定性的函数成立,我用了这么多年、早已当成空气一样的常识;可大模型恰恰打破了它:它是概率采样的产物,同样的输入,给的是"一个对的答案",而非"那个一字不差的答案"。我没意识到自己接入的是一个全新性质的东西,仍用对待普通函数的老习惯去对待它——于是"缓存复用""断言固定输出"这些建立在确定性之上的做法,全都失了根基。不是大模型"不稳定"有毛病,是我用错了它的"说明书",把一个概率生成器当成了确定性函数。

第二件事:正解——别依赖逐字一致,按语义/结构校验,缓存按输入而非输出

找到根因,正解就清晰了:承认大模型输出的非确定性,把一切依赖"逐字一致"的逻辑,改成依赖"语义/结构正确"——测试断言不变量(JSON 能解析、字段齐全、含关键信息)而非固定文本;缓存以"输入"为 key(而非比对输出);要更稳就降 temperature、约束输出格式。让系统建立在大模型真正能提供的保证之上。

# 错误: 断言输出逐字等于固定文本(依赖确定性, 必然时灵时不灵)
def test_bad():
    assert llm(prompt) == "这段文字主要讲了 A 和 B。"   # ✗ 措辞一变就挂

# 正解1: 测试断言"不变量/语义/结构", 而非逐字文本
def test_good():
    out = llm("提取信息, 返回 JSON: ...")
    data = json.loads(out)                 # 结构: 能解析成 JSON
    assert set(data) >= {"name", "age"}    # 字段齐全
    assert isinstance(data["age"], int)    # 类型正确
    # (要校验语义可再用一个 LLM/规则判断"是否包含关键信息", 而非比字符串)

# 正解2: 缓存以【输入】为 key, 而不是比对/指纹化输出
import hashlib
def cached_llm(prompt):
    key = hashlib.sha256(prompt.encode()).hexdigest()   # key 来自【输入】
    if key in cache:
        return cache[key]                  # 同输入直接复用上次那个答案
    out = llm(prompt)
    cache[key] = out                       # 存下来, 锁定一个答案供复用
    return out
# ↑ 这样"同输入同输出"是【我用缓存强制实现的】, 而非指望 LLM 天生保证

# 正解3: 要更稳定/可控, 从源头约束:
#   - temperature 调低(趋稳, 但仍不保证逐字)
#   - 用结构化输出 / function calling / JSON schema 约束格式
#   - 给固定 seed(部分 API 支持, 提高可复现性但仍非绝对)

这套做法的精髓,是把"我对系统的依赖",从"大模型给不了的保证(逐字确定)",挪到"大模型能给的保证(语义/结构大致正确)"上:测试不再赌它每次吐出一模一样的字,而是校验"它是不是干了对的事"(JSON 合法、字段齐、含关键信息);缓存不再幻想它天生幂等,而是我自己用缓存把"同输入复用同一个答案"这件事实现出来。这样,大模型的非确定性就不再是 bug,而是被我接纳、并妥善安置的一个特性。不是去消灭它的不确定,而是不在它不确定的地方,押上我系统的确定性。

【和 LLM 的非确定性相处, 几条原则】

1. 别依赖"逐字一致": 它保证语义/意图大致对, 不保证每次一字不差

2. 测试断言"不变量": 能否解析、字段是否齐全、类型/范围、是否含关键信息
   —— 而不是 assert 输出 == 某段固定文本

3. 缓存以【输入】为 key: "同输入同输出"由你的缓存实现, 别指望 LLM 天生幂等

4. 要更稳: temperature 调低 + 结构化输出约束(JSON schema/function calling)
   + 固定 seed(若支持)—— 提高可复现性, 但仍非 100% 保证

5. 凡是后续逻辑"假设 LLM 输出可预测/可对账/可逐字比较"的地方, 都要重新设计

6. 心智转变: LLM 是"概率生成器", 不是"确定性函数"——按它的真实性质来用

第三件事:其他"误把非确定的东西当确定来用"的同类坑

顺着"别假设它确定/可预测"这条线,我把系统里同类的坑都排查了一遍,它们都源于"把一个有不确定性的东西,当成了稳定可预测的":

第一个,依赖 LLM 输出的格式永远正确。让它返回 JSON,就假设它一定是合法 JSON,不做解析容错——结果偶尔多句解释、少个括号就崩。要校验+重试+兜底。

第二个,用 LLM 输出做精确对账/去重。拿两次生成结果比对是否一致来判断"有没有变",可它本就不保证一致,对账永远报"变了"。要按语义而非逐字比。

第三个,把"概率正确"当成"一定正确"。LLM 大概率给对答案,但有小概率幻觉/出错,关键决策若全盘信任、不加校验或人审,迟早被那个小概率坑。

第四个,外部 API/网络/时间等本就含不确定性的依赖。把"调用一定成功""耗时一定很短""顺序一定固定"当成铁律,不做超时/重试/乱序处理——本质同样是把不确定当成了确定。

第四件事:确定性函数 vs 大模型,一张表分清能依赖什么

我把"传统确定性函数"和"大模型"的关键差别整理成一张表,这是我现在接入大模型时,提醒自己"能依赖它什么、不能依赖它什么"的依据:

维度 传统确定性函数 f(x) 大模型 LLM
同样输入 必得同样输出 输出语义相近,但常不逐字一致
本质 确定性映射 概率采样生成
能依赖 输出逐字可预测 语义/意图/结构大致正确
不能依赖 逐字一致、可精确对账、可指纹去重
缓存 天然可缓存 需以输入为 key 自己缓存
测试 assert 等于固定值 assert 不变量/语义/结构

这张表让我看清:大模型和传统函数是两类性质不同的东西——我可以依赖传统函数"逐字可预测",但只能依赖大模型"语义大致正确"。把对前者的依赖习惯,原封不动套到后者身上,就会在"逐字一致"这种它给不了的保证上栽跟头。用对它,就得只在它能担保的地方依赖它。

第五件事:我对"大模型输出"的几个想当然

这次事故,本质是我把大模型想当然地当成了一个确定性函数。把这些想当然列出来,每一条都值得警惕:

我曾经的想当然 事故教我的真相
"同样输入,大模型必给同样输出" 它是概率采样,同输入常给语义相近但措辞不同的输出
"temperature=0 就完全确定了" 趋稳但仍不保证逐字一致(浮点/并行/后端等)
"可以断言它的输出等于某段固定文本" 措辞一变断言就挂;该断言语义/结构不变量
"它输出一致,可以拿来对账/去重" 本就不保证一致,精确对账必然失效
"它大概率对,就当它一定对" 有小概率幻觉/出错,关键处要校验/人审
"接 LLM 和调个普通函数没区别" 性质完全不同,依赖确定性的逻辑都要重设计

第六件事:在系统里接大模型时,我现在的自检习惯

现在每当我要在系统里接入大模型、或排查"大模型相关的逻辑时灵时不灵",我都会先按这张图问自己:

这张图的精髓,是"先查我的逻辑是不是偷偷假设了'同输入同输出';是就把依赖从逐字一致改成语义/结构正确"设计就测试断言不变量、缓存以输入为 key、输出做解析容错、排查就看时灵时不灵是不是因为我把概率生成器当成了确定性函数这套习惯,让我从"把 LLM 当普通函数"变成了"按概率生成器的真实性质来用它"——核心始终是:大模型本质是概率模型、逐词采样生成,同样输入给的是"一个语义合理的答案"而非"那个唯一逐字固定的答案",即便 temperature=0 也不保证 100% 逐字一致(浮点/并行/后端等带来细微非确定性);它不是确定性函数 f(x),你能依赖它语义/意图/结构大致正确、不能依赖逐字一致/可精确对账/可指纹去重;正解是测试断言不变量而非固定文本、缓存以输入为 key 自己实现幂等、输出做结构与语义校验容错,把系统建在它真能提供的保证上。

我立下的几条规矩

这场"把大模型当确定性函数、缓存测试全崩"的事故,换来了我做 AI 应用时,刻进骨子里的几条铁律:

  1. 大模型是概率生成器、不是确定性函数:同样输入给的是语义合理的答案,不保证逐字一致。
  2. 即便 temperature=0 也别指望 100% 逐字确定(浮点累加、并行、后端版本都会引入细微差异)。
  3. 能依赖它的语义/意图/结构大致正确,不能依赖逐字一致、精确对账、输出指纹去重。
  4. 测试断言"不变量"(能解析、字段齐、类型对、含关键信息),而非断言等于某段固定文本。
  5. 缓存以"输入"为 key,"同输入同输出"由我的缓存实现,别指望大模型天生幂等。
  6. 要更稳:降 temperature、用结构化输出/JSON schema 约束、给 seed,但仍非绝对保证。
  7. 凡是后续逻辑假设了 LLM 输出可预测/可逐字比较的地方,都要按"它是非确定的"重新设计。

附:我现在给 LLM 调用统一套的"非确定性容错包装"

这是我现在调用大模型时固定套的一层"非确定性容错包装"——它把"承认输出非确定、按结构校验、失败重试、以输入缓存"这几条教训,一次性焊进了一个函数里,让上层代码再也不用直接面对那个会变来变去的原始输出:

import json, hashlib

def robust_llm_json(prompt, schema_keys, max_retry=3):
    """ 调 LLM 要 JSON: 以输入缓存 + 解析容错 + 结构校验 + 重试 """
    key = hashlib.sha256(prompt.encode()).hexdigest()
    if key in cache:                       # 缓存以【输入】为 key, 由我实现幂等
        return cache[key]

    last_err = None
    for attempt in range(max_retry):       # 非确定 → 失败就重试, 别指望一次到位
        raw = llm(prompt, temperature=0)   # 降随机(趋稳, 但仍不保证逐字)
        try:
            # 容错: LLM 可能在 JSON 前后多说几句, 抽取出 JSON 段再解析
            text = raw[raw.find("{"): raw.rfind("}") + 1]
            data = json.loads(text)        # 校验"能否解析"这个结构不变量
            if not set(schema_keys) <= set(data):   # 校验字段齐全这个不变量
                raise ValueError(f"缺字段: {set(schema_keys) - set(data)}")
            cache[key] = data              # 校验通过才缓存, 锁定一个答案
            return data
        except Exception as e:
            last_err = e                   # 这次输出不合格(措辞/格式偏了), 重试
    raise RuntimeError(f"LLM 输出 {max_retry} 次都不合格: {last_err}")

# 上层只依赖"拿到一个结构合法、字段齐全的 dict"(语义/结构层面的保证),
# 完全不依赖"每次返回逐字一样"(LLM 给不了的保证)

这个包装把我这次的教训钉死在了调用层:它不再奢求大模型"每次一字不差"(那是它给不了的),而是只依赖"能解析成结构合法、字段齐全的 JSON"(那是可校验、可重试逼出来的);解析失败/字段缺失就重试,通过校验才缓存,缓存以输入为 key。有了它,大模型的非确定性被牢牢圈在了这一层之内,上层拿到的永远是一个干净、合法、可放心使用的结构——我终于不必再在每个调用点,提心吊胆地面对那个会变来变去的原始字符串。把对一个新事物性质的认知,沉淀成一层稳稳兜住它的代码,这本身就是真正接纳了它的标志。

写在最后

回头看,这场由"把大模型当确定性函数"引发的"缓存不命中、测试乱挂"事故,真正教给我的,远不止"测试断言别写死"这一个技巧。它让我对"当我们接触一个'新事物'时, 最危险的, 不是它哪里难懂, 而是我们不自觉地用'旧事物的脾性'去套它——我们带着一身对旧世界的'常识'(比如'同样的因必有同样的果'), 把它当成想当然的前提, 而没意识到这个新事物, 恰恰在某个根本性质上, 和旧事物截然不同",有了一次刻骨的体会。我栽跟头,是因为我用'确定性函数'这个旧范式, 去套'概率生成器'这个新事物——"同样的输入必给同样的输出", 这个在我过往全部编程经验里都成立的'铁律', 早已内化成我不假思索的前提;我接入大模型时, 压根没想过去问"它还遵守这条铁律吗", 就理所当然地把它当成了又一个 f(x);我没意识到, 我面对的是一个性质全新的东西——它给的是'一个合理的答案'而非'那个确定的答案'; 我用旧地图, 走进了新大陆这让我领悟到一个关于"新事物、旧范式与隐含前提"的深刻认知:我们对世界的理解, 是由一整套'习以为常、几乎从不拿出来检视'的隐含前提(范式)支撑的; 这套前提在它适用的旧领域里无往不利, 也正因如此, 我们会忘记它只是'前提'而非'绝对真理';当一个新事物出现、而它恰好打破了某条我们视为天经地义的前提时, 真正的陷阱不在于'新事物的特性'本身, 而在于我们仍用旧前提去理解它、并把建立在旧前提上的一整套做法照搬过来——于是处处碰壁, 还百思不得其解;所以拥抱一个新事物, 最关键的一步, 不是急着学它'能做什么', 而是先搞清楚它'打破了我哪条原来视为理所当然的前提'——因为正是那条被打破的前提之上, 藏着我即将踩的所有坑这给了我一种看待"一切'接触/采用一个新技术、新工具、新范式'之事"时的清醒:每当我开始用一个新事物时,要追问"我正在用什么'旧的常识/前提'来理解它?这个新事物, 是不是恰恰在某条我从未怀疑的前提上, 和我熟悉的旧事物根本不同?"——主动去识别并检视那些被自己当成空气的隐含前提, 看清新事物到底改写了哪一条, 而不是把旧范式不假思索地套上去;"识别并校验自己理解新事物时所用的隐含前提、看清它改写了哪条旧规则",是用对新工具、也是真正拥抱任何新范式的关键认清大模型是概率生成器而非确定性函数、它打破了"同输入同输出"这条旧铁律、要依赖它的语义而非逐字——这,是我用一次缓存测试全崩的事故,换来的、关于 AI、也关于如何用新眼光看待新事物的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次把大模型接进系统、顺手想给它做缓存或写断言时,先想想"它真的同输入同输出吗?我是不是又把它当成普通函数了?",并把依赖建到它的语义而非逐字上,那我对着那个"同样输入、输出却总差几个字"的诡异现象折腾的大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的服务器突然报磁盘空间不足、写不了任何文件,可我 df 一看磁盘明明还有一大半空闲,百思不得其解,排查半天才发现真正被耗尽的不是磁盘空间、而是一个我从来没关注过的东西——inode 的深度复盘

2026-6-3 4:32:54

技术教程

我把一个下单操作拆成了先调库存服务扣库存、再调订单服务建订单,本地测试一路绿灯,可上线后偶尔出现库存扣了、订单却没建成,钱货两空、数据对不上,排查半天才明白跨服务的操作根本没有我以为的那种原子性的深度复盘

2026-6-3 4:43:36

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索