OFERRY正在进入矩阵

不生成 token 的判决模型开始抢抽取的活:把置信度变成一道运营门槛

十月中旬这半个月,一个不太起眼的品类连着出了三家新品:10 月 15 日 Liquid AI 放出多模态判决模型 d1-ultra,价格写在官网首页——每百万输入 token $0.042;10 月 18 日阿塞拜疆团队 HAL-X 把 THX-01 挂上 Hugging Face,Apache 2.0,322M 参数、单次前向约 10 毫秒,工单分类的平均 ECE 压到 0.003;更早挂出的 laya 把文档上限做到 8192 token,还顺手把「长文档时正确率会掉」写进了自己的模型卡。

它们都不生成一个 token。你给它一个 state(邮件、工单、文档、JSON 记录、agent 轨迹)和几个带类型的问题,它在一次前向里把每个问题的概率分布还给你。

本站在月初写过 TypeSafe 的 Jev 被黑盒复刻那段故事(a445,10 月 2 日)——那时候能问的还只有「选项 / 是或否 / 打分」三种类型,想抽个数字得在服务层用一堆区间桶去模拟。这次要讲的是两件事的变化:问题类型被扩到了抽取和引用,以及「校准」第一次成了可以拿来定价的产品特性。

一、判决模型在做什么#

Liquid 的官方定义最干净:判决模型回答「关于某个情形的问题」,给的是每个可能答案的概率,而不是文字。d1-ultra 支持三种问题类型(noul 是或否、choice 多选一、score 量表位置),能同时读文本和图片,一次前向出结果。

THX-01 的接口更宽,六种:

类型返回关键点
choiceN 个选项各一个概率选项数 >24 时走两轮锦标赛
noulP(yes)是或否
score有序等级 + 分布情感/紧急度这类
number文档里写着的数字,没有则 null原生能力,不做任何换算
excerpt原文片段 + 字符偏移从文档里切出来,编不出新字
cite: true支撑答案的原文部分 + 概率任意问题都能挂

官方给的最小调用长这样(模型卡原文,Apache 2.0,首次运行从仓库下载 322M 权重):

SHELL
pip install thx01                 # 本地推理库
pip install "thx01[server]"       # 需要 REST 服务时
THX01_MODEL=doofz/THX-01 THX01_API_KEY=your-key python -m thx01.server --port 8095
READ / EXECUTE / UNDERSTANDUTF-8
PYTHON
import thx01
agent = thx01.load("doofz/THX-01")              # 有 GPU 就走 GPU

doc = ("Northwind Corp. reported Q3 2026 results. Revenue for the quarter was $48.3 million, up 14%. "
       "Net income was $6.2 million, compared with $4.1 million a year ago. "
       '"We will open two offices in Baku," said CEO Laura Chen.')

result = agent.decide(doc, {
    "kind":    {"type": "choice", "question": "What is this document?",
                "criteria": {"earnings": "earnings report", "complaint": "customer complaint",
                             "invoice": "invoice"}},
    "revenue": {"type": "number", "question": "What was the revenue, in US dollars?", "cite": True},
    "quote":   {"type": "excerpt", "question": "What did the CEO say?"},
})
# kind    -> earnings
# revenue -> 48300000(并回传支撑句)
# quote   -> 从原文切出的片段
READ / EXECUTE / UNDERSTANDUTF-8

注意 revenue 那一问:文档里写的是 $48.3 million,返回的是 48300000——它会做量词归一化(模型卡另举了 1,2 mln → 1200000),但不做单位或币种换算。这个「归一化数字、不换算单位」的边界,是它和生成式模型最不一样的地方:它不会去猜你想问的是哪种单位,它只会告诉你原文写了什么。

这张表里最有信息量的一行是 number。THX-01 的模型卡里写得很直白:number、excerpt、引用不在 TypeSafe 的问题类型里(那边只有 choice/noul/score),服务层要实现只能拿 choice 反复模拟;而它自己把这种模拟调用也兼容了(用区间桶、文档分块来问),所以旧的服务层不用改。

效果差距写在自家表里:数字查找 69.6% → 93.4%,用区间桶模拟 0.0% → 95.1%,原文抽取 F1 从 20.1% 到 84.1%,引用命中 12.7% → 94.0%。那个 0.0% 是全表最值得看的一格:把一个需要「读数字」的问题硬塞给只会选桶的分类模型,旧方案不是差,是完全做不了——模型只能选一个桶,不会做区间到具体值的推理。

顺带说一句不好听但必须说的:同一个数字查找任务上,TypeSafe Jev 1.13 做到 96.6%,比 THX-01 的 93.4% 高。这是模型卡自己写出来的对照,不是我从别处挖的。厂商愿意在自家优势表里塞一行自己输的对手分数,这种账本的可信度比全绿的表格高。

二、为什么「准」不够:校准才是这个品类的核心竞争力#

一个模型 98% 的准确率,听起来可以全自动了。但如果它在答错的那 2% 上给出的概率是 0.97,你没有任何办法在下游把它们拦下来——因为它的「自信」和「正确」不挂钩。

校准(calibration)说的就是这件事:把样本按置信度分箱,每箱的平均置信度应该等于该箱的实际准确率。量化指标是 ECE(Expected Calibration Error),越小越好。THX-01 在工单任务上的 ECE 是 0.003,Kev-4B 在同一任务是 0.202——后者意味着它在你最需要它说真话的时候,系统性高估自己二十个百分点。

Liquid 在博客里把这件事说成了产品卖点:「大多数判决模型对自己的回答过度自信,所以我们专门训练 d1-ultra 给出更可靠的银率」,并声称在 112 项评测上胜过自家 d1 与 Jev 1.14,且比所有其它判决模型在 Artificial Analysis Decision Index 上高至少 8 分。

关于那个 8 分:这是厂商引用第三方榜单的说法,我没法在本机复核——AA 的榜单页是前端渲染的,抓 HTML 检索不到不等于没上榜。要引用就按「厂商引用第三方榜单,未经本机复核」来标。

校准能被训练出来,靠的是把奖励函数设计成严格合适评分规则(strictly proper scoring rule)。THX-01 的模型卡把公式摊开了:

TEXT
R(q, t) = Σ_k t_k·log q_k + 0.5·(Σ_k t_k·q_k)/‖q‖₂ − 1[ordinal]·RPS(q, t)
READ / EXECUTE / UNDERSTANDUTF-8

对数是 log score、第二项是 spherical score、最后一项是按序问题的 ranked probability score。严格合适的含义是:这个奖励的期望只有在报出真实分布时才最大。也就是说,如果模型心里觉得「是」的概率是 0.7,报 0.9 会让长期收益下降——它没有说谎的动机。这不是靠提示词求它「请诚实」,是靠目标函数让它诚实。

再叠两个细节,整套逻辑才完整:没有合适选项时给均匀分布(于是模型报「我不确定」,而不是自信地瞎选一个),有序量表上给近似命中部分分。训练是 8 个阶段的 RLCD,累计约 210 万条判决,最后两个阶段专门练抽取和引用。

三、纸面数字,以及哪些能核、哪些不能#

以下是模型卡自报(第三方无法独立复核,我按原表抄)。工单分类,15 个类目、4 个测试集、2843 张阿塞拜疆语/俄语/英语/土耳其语工单:

模型CleanCorruptedMessyIndependent平均ECE延迟
THX-0199.297.798.897.998.40.003~10 ms
Claude Sonnet 5.5 †98.598.098.599.098.5–1.5 s
Wahoo 1.599.896.398.097.297.8–145 ms
GPT-6-Luna98.697.496.798.297.7–1.9 s
TypeSafe Jev 1.1399.295.096.299.097.40.007331 ms
Kev-4B96.188.490.895.892.80.202830 ms

读之前先看脚注,这张表的可比性是有洞的:† 号那行只跑了每个测试集 200 条的抽样;ECE 只在 Independent 集上算,而且大模型根本不返回概率,所以它们的 ECE 栏是「–」。也就是说「THX-01 准确率和 Sonnet 5.5 打平、延迟快 150 倍」这句话成立的前提是:你只信 200 条的子集,并且承认大模型给不出概率。后者才是重点——选这个品类,本来就是冲着概率去的。

速度表也是自报:单问约 9 ms,同一个 state 连问三个约 10 ms,150 个请求批处理约 156 ms,峰值约 3000 判决/秒。laya 那侧的数字是约 33 ms/次,而且它自己在卡里承认:4000 token 的输入在 Apple GPU 上要约 1.7 秒,速度跟着真实长度走。

厂商自己承认的边界,往往比成绩更有用。 THX-01 的 Limitations 写了三条:77 类意图的 Banking77 只到 65.9%、六类情绪(DAIR Emotion)只到 44.6%、number/excerpt 只负责「找出文档里写着的字」,不做算术、不做单位换算、不做同义改写——问公里而文档写英里,答案就是 null。laya 的卡里则写着:长文档 20 题里能对 16–18 题,再长就变成 8–17 题,「请在你自己的数据上验证长文档准确率」。

这两句比任何 SOTA 声明都值钱:它告诉你这个品类的输入上限是按问题算的——THX-01 是每个问题最多 1024 token(问题、选项、state 全算进去),超了直接截断。长文档得先切,切法直接决定准确率。

四、成本结构:值得单独算一遍#

d1-ultra 的定价是只算输入 token,没有输出 token——因为它不生成。官网把计价细节写全了:图片按 32×32 像素的 patch = 1.5 token 计,一张 1024×1024 的图 = 1536 token。

关键的一句在后面:每个问题各按一次 prompt 计费,包含它自己的文字和所有图片。

这条把单价结构改了形。一张 1024×1024 的图挂 5 个问题:

TEXT
图片:1536 token/问 × 5 问 = 7680 token
文字:假设 state 200 token + 每问 20 token → 200×5 + 20×5 = 1100 token
合计 ≈ 8780 输入 token ≈ 0.00037 美元/次请求
READ / EXECUTE / UNDERSTANDUTF-8

换成生成式模型问同样五件事,通常是五轮生成,或者一轮生成里塞五个问题(然后你还要解析它输出的 JSON,还得处理它偶尔编一个字段)。判决模型这侧的好处不是单价,是没有解析层、没有幻觉面:excerpt 是从文档里切出来的,number 没找到就返回 null,结构本身堵住了「编一个看起来合理的答案」。

但「每问重复计费」这件事也意味着:把问题拆得越碎越贵。3 个问题合 1 个粗问题(比如「这封邮件属于哪个部门且紧急度如何」)省 token,代价是准确率下降——这个权衡得在你自己的数据上量,别照抄别人的问题清单。

五、自己算校准:一套能直接跑的脚本#

厂商的 ECE 是厂商算的。你要上线选择性自动化,必须在自己数据上算一遍——因为做运营决策的不是准确率,是「覆盖率 × 自动处理部分的准确率」这条曲线。

下面这份 calib.py 只用标准库,把四个最常用的度量和一条门槛策略都实现好了。输入是一行一条的 JSONL,{"p": 0.83, "y": 1},p 是模型给的「答对」概率、y 是实际对错。没数据就直接跑,它会造一批**「真实概率 q、模型报 sigmoid(2.2·logit(q))」的过度自信预测**给你看方法怎么用:

PYTHON
import json, math, random, sys
from collections import defaultdict

def brier(pairs):                      # 越小越好;一律报 0.5 相当于 0.25
    return sum((p - y) ** 2 for p, y in pairs) / len(pairs)

def ece(pairs, bins=10):               # 等宽分箱,返回 ECE 与可靠性表
    buckets = defaultdict(list)
    for p, y in pairs:
        buckets[min(int(p * bins), bins - 1)].append((p, y))
    n, out, table = len(pairs), 0.0, []
    for idx in range(bins):
        rows = buckets.get(idx, [])
        if not rows: continue
        conf = sum(p for p, _ in rows) / len(rows)
        acc = sum(y for _, y in rows) / len(rows)
        out += len(rows) / n * abs(conf - acc)
        table.append((idx / bins, (idx + 1) / bins, len(rows), conf, acc, conf - acc))
    return out, table

def mce(pairs, bins=10):               # 最坏那一箱的偏差,比 ECE 更能暴露尾部风险
    return max(abs(g) for *_, g in ece(pairs, bins)[1])

def _logit(p):
    p = min(max(p, 1e-6), 1 - 1e-6)
    return math.log(p / (1 - p))

def fit_scale(pairs):                  # p' = sigmoid(a·logit(p)),网格搜 a 最小化 NLL
    best, best_a = None, 1.0
    for i in range(281):
        a = 0.2 + 2.8 * i / 280
        cur = -sum((y * math.log(max(1/(1+math.exp(-a*_logit(p))), 1e-12))
                    + (1-y) * math.log(max(1-1/(1+math.exp(-a*_logit(p))), 1e-12)))
                   for p, y in pairs) / len(pairs)
        if best is None or cur < best: best, best_a = cur, a
    return best_a, best

def apply_scale(pairs, a):
    return [(1 / (1 + math.exp(-a * _logit(p))), y) for p, y in pairs]

def coverage_table(pairs, thresholds=(0.5, 0.7, 0.8, 0.9, 0.95)):
    for t in thresholds:
        kept = [(p, y) for p, y in pairs if max(p, 1 - p) >= t]
        if kept:
            print(f"  p>={t:.2f}  覆盖 {len(kept)/len(pairs)*100:5.1f}%  "
                  f"准确率 {sum(y for _, y in kept)/len(kept)*100:6.2f}%")

if __name__ == "__main__":
    data = [(float(json.loads(l)["p"]), int(json.loads(l)["y"]))
            for l in open(sys.argv[1], encoding="utf-8") if l.strip()] if len(sys.argv) > 1 else None
    if data is None:                   # 演示数据:真实的过度自信
        rng, pairs = random.Random(7), []
        for _ in range(4000):
            q = min(max(rng.choice((0.55, 0.7, 0.85, 0.95, 0.99)) + rng.gauss(0, 0.06), 0.5), 0.999)
            y = 1 if rng.random() < q else 0
            pairs.append((min(max(1/(1+math.exp(-2.2*_logit(q))), 0.001), 0.999), y))
        data = pairs
    err, table = ece(data)
    print(f"样本 {len(data)}  准确率 {sum(y for _, y in data)/len(data):.4f}")
    print(f"Brier {brier(data):.4f}  ECE {err:.4f}  MCE {mce(data):.4f}")
    print("  区间       样本    平均置信   实际准确     差")
    for lo, hi, cnt, conf, acc, gap in table:
        print(f"  {lo:.1f}-{hi:.1f}  {cnt:6d}    {conf:8.4f}  {acc:8.4f}  {gap:+.4f}")
    a, _ = fit_scale(data)
    cal = apply_scale(data, a)
    print(f"缩放 a={a:.3f}(<1 即过度自信)→ ECE {ece(cal)[0]:.4f}  Brier {brier(cal):.4f}")
    print("自动处理门槛:"); coverage_table(cal)
READ / EXECUTE / UNDERSTANDUTF-8

本机实跑输出(Python 3.13,纯标准库,几秒钟):

TEXT
样本 4000  准确率 0.8157
Brier 0.1336  ECE 0.0755  MCE 0.1540
  区间       样本    平均置信   实际准确     差
  0.5-0.6     395      0.5307    0.5646  -0.0338
  0.6-0.7     260      0.6503    0.5808  +0.0696
  0.7-0.8     296      0.7521    0.6250  +0.1271
  0.8-0.9     473      0.8538    0.6998  +0.1540
  0.9-1.0    2576      0.9833    0.9212  +0.0621
缩放 a=0.540(<1 即过度自信)→ ECE 0.0183  Brier 0.1254
自动处理门槛:
  p>=0.50  覆盖 100.0%  准确率  81.58%
  p>=0.70  覆盖  73.4%  准确率  89.51%
  p>=0.80  覆盖  60.4%  准确率  93.13%
  p>=0.90  覆盖  45.0%  准确率  96.78%
  p>=0.95  覆盖  32.4%  准确率  97.99%
READ / EXECUTE / UNDERSTANDUTF-8

三条结论直接可用:

一、可靠性表才是诊断书。 ECE 0.0755 是个孤立数字,但按箱摊开就看到病根在 0.8–0.9 那一箱:模型说 85%,实际只有 70%,偏了 15.4 个点。要改进模型就往这一档加数据,不需要看别的。

二、一个系数就能修掉大部分偏差。 拟合出的 a = 0.540(不到 1,正是过度自信的方向),缩放后 ECE 从 0.0755 降到 0.0183、Brier 从 0.1336 降到 0.1254。注意这是在验证集上拟合的系数——上线前必须另留一段数据复核,否则只是把偏差背到了测试集上。

三、门槛把概率变成运营策略。 不设门槛时自动处理 100%、准确率 81.58%;设 p≥0.9 变成覆盖 45%、准确率 96.78%;设 p≥0.95 是覆盖 32%、准确率 98%。用同一份脚本在验证集上定阈值(我在演示数据上把「准确率 ≥99%」的最大覆盖率算出来是 25.5%,阈值落在 0.970),剩下的转人工——这样你就能对业务方说出那句真正有用的话:「自动处理四分之一,错一个都算事故;其余进人工队列」。

对比一下 THX-01 自报的那条运营曲线:约四分之三的工单零错误自动处理,覆盖 90% 时准确率仍 ≥99.5%。厂商的数字是厂商数据上的,你的数字得你自己跑。方法在,结论自己算。

六、四个坑#

  1. ECE 怎么分箱会变数。 等宽分箱在样本少时会被尾部几个点带跑,等频分箱更稳;两套都算一遍,差得远就说明样本不够。另外 ECE 是加权平均,会被大箱掩盖——所以一定要把可靠性表和 MCE 一起看。
  2. 别用平均分掩盖单个任务。 THX-01 的 39 个多语言任务平均准确率从 58.3% 抬到 84.2%、平均 ECE 从 0.204 降到 0.066,但逐任务看:Banking77 只有 65.9%、DAIR Emotion 只有 44.6%(ECE 0.284,全表最差)、SIB-200 的哈萨克语 ECE 0.158。平均分好看,不代表你要用的那个任务能用。
  3. 输入上限是按问题算的,不是按文档算的。 THX-01 每问 1024 token 全包(问题+选项+state),超出截断;number 不做单位/币种换算,问法不对就是 null;THX-01 支持 18 种后训练语言、经编码器覆盖 100+ 语言,但非它后训练过的语言别指望同样水准。
  4. 阈值必须配漂移监控。 上半年的数据定下的门槛,到了下半年工单分布一变就作废。把「每日自动处理覆盖率 + 自动部分的准确率 + ECE」三项打进监控,掉出区间就回退到全人工——这比调模型参数便宜得多。

总结#

这个品类的价值不在「比大模型准」,而在它把不确定性变成了一个接口字段。生成式模型给你一段文字,你得靠解析和事后校验去猜它有多确定;判决模型直接给你概率,你就可以写一行 if p > 0.97: 自动处理。

半个月三家新品里,THX-01 是最值得拿来读的一份:322M 参数、Apache 2.0、六种问题类型、模型卡把奖励函数和 8 个训练阶段都摊开,还主动写了自己输给 Jev 的那一行。Liquid 的 d1-ultra 把这条路线推到了多模态和 API 定价($0.042/百万输入 token、按 patch 折算图片、每个问题单独计费);laya 补上了 8192 token 的长文档和 MCP 集成。

要注意的成本账是「每问重复计费」,要注意的技术账是「校准必须在自己数据上量」。真要做起来,顺序很简单:拿 500 条自己业务的历史样本跑一遍,用上面的脚本出可靠性表,拟合一个缩放系数,再用验证集定门槛。产出不是一篇评测,是一句能给业务方看的承诺。

参考与致谢#

文中 THX-01、d1-ultra、laya 的分数、延迟与价格均为厂商官方自报,未经本机独立复核;calib.py 的输出为本机 Python 3.13 实跑结果(合成数据,可复现)。

不生成 token 的判决模型开始抢抽取的活:把置信度变成一道运营门槛

https://www.oferry.com/posts/a481/
作者
摆渡
发布于
许可协议
CC BY-NC-SA 4.0

除另有声明外,本文采用上述许可协议。转载或改编请注明作者与原文地址,并遵守许可条款。