我把诉求分诊交给了一个不写字的模型


📑 目录

大模型能把工单读完、把要点提炼出来、写一段礼貌的回复,这些我都信。但真到了代码里,我要的往往只是一个判断:这条诉求该派给政策补贴组还是物业组,紧不紧急,要不要人来处理。让生成模型吐一个 JSON,再靠解析器去拆字段,这条路我走过,脆得很——字段名多一个空格、模型心情一波动,下游就崩。我要的是一个直接给数字的东西:选项、概率、置信度,代码拿到就能写 if/else。这就是决策模型的位置。

前阵子我在本地装了一个叫 Laya 的开源决策模型,照着写了个园区企业诉求分诊的测试脚本。趁热把它的底子、跟 TypeSafe 那个托管 API Jev 的差别,还有怎么微调,都理了一遍,记在这里。

什么是 System One 决策模型

System One 这个词来自丹尼尔·卡尼曼的《思考,快与慢》。他把人的思维分成两套:System 1 快、直觉、几乎不费力;System 2 慢、审慎、要动脑。决策模型取的是前者:快速、聚焦的判断,一次前向传播就出结果,不写一个字,也不解释自己为什么这么判。

它对外只暴露三种问题,叫 primitive,Laya 和 Jev 用的是完全同一套接口:

  • choice:从一组选项里挑一个,返回选中项、每个选项的概率、一个置信度。做部门路由、意图分类、主题归类。
  • score:按一条量规给状态打序数分,返回期望档位、各档的概率分布、置信度。做情绪强度、工单紧急度、危害等级。
  • noul:判断一句话成不成立,返回 0 到 1 之间校准过的 P(true)。做钓鱼检测、垃圾过滤、越狱识别、流失风险。

三种问题可以在一次调用里混着问,对同一个 state 并行、独立地求值。因为不生成文本,所有问题在同一个前向传播里一起算完,加问题几乎不加响应时间。每个问题独立求值,所以加问题也不会污染其它问题的上下文。

它不生成文本,所以没有需要解析的输出,也没有可以编造的内容,幻觉无从谈起。它给的概率是用严格适当评分规则训练出来的,针对结果优化过,代码可以直接拿 confidence 分支:高于阈值就自动走,低了转人工。

为什么「校准」值钱,值得单独说。普通分类器只给你一个 argmax,概率是 softmax 顺手算出来的,并不反映真实出错率。System One 模型把概率本身当训练目标,用严格适当评分规则压着它诚实——报的分布越接近真实得分越高,虚高虚低都吃亏。这样的概率才能进 if/else:0.85 以上自动派单,0.5 附近转人工,这个阈值是有语义的。confidence 有明确算法,来自概率分布的归一化香农熵,选项之间越犹豫,置信度越低。

Jev 是什么

Jev 是 TypeSafe 的旗舰模型,他们管它叫「第一个 System One 模型」,同样对外提供 choice、score、noul 三种 primitive,可以在一次调用里混用。官方对这类模型的界定很干脆:不写回复、不生成代码、不解释自己的推理,可能的答案全由 primitive 定义。差别在交付形态:Jev 是托管 API,权重不公开,走官方 SDK 或 POST /v1/systemone,模型字段默认 jev-latest。

官方的设计哲学一句话:原子问题,在代码里组合。一个问题只问一件具体、范围清晰的事,像一个懂行的人几秒内能给出的直觉判断。要权衡多个独立因素,就拆成多个问题,再用自己的代码加权组合。优先级变了,只改代码里的系数,不用重写 prompt。这句话我认同,它把「改提示词」这种玄学动作,换成了「改一个数字」这种可控动作。

Jev 有几个强项,后面会细说:选项数最多能到 255 个;输出的软分布跟老师模型的全概率分布贴得更近;开箱的原始校准也更好。代价也清楚:权重不在自己手里,数据得发给它;价格方面,有第三方站点称 API 于 2026-09-21 开放、每百万输入 token 收 $0.042、版本线 jev-1.13.0。上面这些数字(包括前面的 255 个选项上限)都是第三方说法,不是官方文档原话,在这里标一下。生态上也能看出它是 API 打头:社区已经有 MCP 封装(比如 jkudish/jev-mcp),能接 OpenRouter、Cloudflare、Vercel 各种网关,都是围着 HTTP 调用转的。

Laya 是什么

Laya 是 Convai Innovations 做的,Apache-2.0 开源,pip install laya 就能装。非自回归,单次前向传播出结果:它一次性给所有选项打出分数,不像生成模型那样一个 token 一个 token 地吐,既快,也没有生成过程可以出错。T4 上单问约 33 毫秒,批量摊到 7.2 毫秒一问,吞吐能到每秒 103 到 332 问。

它其实是三个 checkpoint,配一个 Router 按请求挑:

checkpoint encoder 参数量 上下文 用途
laya ModernBERT-large 421M 512 英文
laya-multilingual mmBERT-base 322M 1024 100+ 语言,快一倍
laya-typed-decisions ModernBERT-large 421M 1024 typed-decisions 工作流

Router 在纯 Python 里做脚本检测,亚毫秒级,然后在前向传播之前决定用哪个 checkpoint。

为什么必须路由,这是 Laya 最值得讲的一点。英文 checkpoint 出了英文之后,准确率会塌掉,而且塌得自信。高棉语上准确率 0.000,置信度却有 0.952;它在任何准确率档位上,平均置信度从没掉到 0.885 以下。也就是说,置信度门控救不了你——模型错的时候依然觉得自己对。所以路由得在跑模型之前做,靠脚本和语言,不靠模型的自信。

路由的收益是实打实的。MASSIVE 意图分类上,英文 checkpoint 在英语拿 0.783,多语言版只有 0.657;换到 13 种其他语言,英文版 0.306,多语言版 0.451。XNLI 也一样:英语 0.860 对 0.843,14 种其他语言 0.521 对 0.731。51 种语言里,英文 checkpoint 只有 23 种能过 3 倍随机基线,多语言版有 45 种。对中文本身,两个 checkpoint 也差不多:zh-CN 上英文版 0.620、多语言版 0.630,几乎打平,但脚本检测会稳当地把中文发到多语言版。

两个工程坑,都是看文档才意识到的。

一个是 preload。默认 max_loaded=1,流量里语言一交替,每次请求都要重建模型,实测中位数 CPU 7.4 秒、T4 10.3 秒。生产环境一定 preload=True,让切换语言只花检测那点亚毫秒。

另一个是 head_max_len。choice 的选项共享一个固定预算:英文版 192 token,多语言和 typed-decisions 256 token。77 个选项的 Banking77,每个标签只分到约 3 到 4 个 token,文本都分不清了,准确率掉到 0.425,两个 checkpoint 全是这个数——这是 token 预算顶到了上限,跟能力无关。选项超过 50 个,要么把 head_max_len 调到 512,要么拆成粗到细的两步。

顺带一提,typed-decisions 这个 checkpoint 默认不会被 Router 自动选中,得显式传 model 或开 auto_task_detection——它是针对四个特定工作流微调过的,不该静默当默认。三个 checkpoint 加一起约 11.6 亿参数,所以 Router 默认只留一个热的、按 LRU 淘汰,全预载才占满。

Laya 和 Jev 对比

先说清一件事:Jev 的所有数字都是第三方公开数据。Laya 官方因为拿不到 TypeSafe 的 API,从来没实测过 Jev,两边样本量和 prompt 都对不齐,只能当参考。

项 Jev 1.13.0 Laya(路由后)
typed-decisions 准确率(2000 决策) 0.727 0.766
AG News(4 标签) 0.910 0.950
DAIR Emotion(6 标签) 0.480 0.595
Banking77(77 标签) 0.870 0.425
ECE(越低越好,温度拟合后) 0.246 0.081
单问 p50 延迟 236–276 ms 32.8 ms
可用语言 无公开基准 51 里 45 种
权重 闭源 API Apache-2.0
成本 $0.042 / 1M token 自托管 $0

Laya 领先的地方:几个共享数据集上准确率略高,延迟快 7 到 8 倍,开源能自托管,多语言覆盖有公开基准。在 typed-decisions 那 2000 个决策上,Brier 是 0.062 对 Jev 报告的 0.148,score MAE 是 0.242 对 0.391,概率质量和序数误差都更好。

Jev 领先的地方也得说清。选项多的场景,Jev 默认支持 255 个,Laya 受 head_max_len 预算限制,77 选项只有 0.425。软分布匹配上,Laya 的 argmax 准确率更高(0.766 对 0.727),但 soft accuracy 反而低(0.471 对 0.580),说明它对老师模型整条概率分布的拟合不如 Jev。原始校准上,Laya 基础 checkpoint 的 ECE 是 0.213,比 Jev 报告的 0.144 差,那个 0.081 是拟合温度之后才拿到的;README 对比表里 Jev 的 ECE 写的却是 0.246,这是第三方口径,两个数对不齐,只说明校准这件事 Laya 要靠拟合温度才追得上来。还有一个细节:DAIR Emotion 上,Jev 有 16% 的样本把真实标签的概率判成了 0,任何拿置信度做分支的逻辑,在这 16% 上会直接失效。

稳定性也值得比:把选项顺序打乱,答案变不变。Jev 报告的是 0.13;Laya 在 MASSIVE 意图(20 选项)上是 0.150、多语言版 0.230,20 个选项下 Laya 更不稳,官方自己说该靠训练时更激进地打乱选项顺序来补。

成本的账要连着算。$0.042 / 1M token 听着便宜,自托管 Laya 的边际成本是 0,但得自己出机器、管部署、扛运维;单价和隐私、延迟是绑在一起的,单看数字没意义。

用起来是什么样

我的脚本是园区企业诉求分诊:一个 choice、一个 score、两个 noul。核心的问题定义长这样:

questions = {
    "category": {
        "type": "choice",
        "instructions": "这条诉求主要涉及哪一类事务?",
        "criteria": {
            "policy_subsidy": "政策申报、补贴、资质认定",
            "facility": "物业、场地、设施报修",
            "billing": "费用、账单、发票",
            "other": "其他",
        },
    },
    "urgency": {
        "type": "score",
        "instructions": "这条诉求的紧急程度如何?",
        "criteria": ["不急", "一般", "紧急(有明确时限或威胁)"],
    },
    "needs_human": {"type": "noul", "instructions": "是否需要人工介入处理?"},
    "churn_risk": {"type": "noul", "instructions": "企业是否表达了搬离/流失的风险?"},
}

res = router.predict(state, questions)

这段脚本在本机真跑过一次——一台只有 CPU 的机器,torch 2.14.0+cpu,权重已经缓存。Router(preload=True) 冷启加载三个 checkpoint 花了 74.2 秒,导入本身 1.2 秒;单次 predict(一个 state、四个问题)用时 836 毫秒。这是 CPU 上的数字,跟官方在 T4 上量的 33 毫秒单问延迟是两码事。

结果分四块看:

  • category 选的是 policy_subsidy,置信度 0.9268,概率分布是 policy_subsidy 0.9836、facility 0.0067、billing 0.0044、other 0.0052。
  • urgency 是 score,期望档位 1.0604,三档(0/1/2)的质量几乎全压在第 1 档(0.9187),置信度 0.7149。
  • needs_human 的 noul 是 0.2935,churn_risk 的 noul 是 0.8116。
  • usage 里 input_tokens 450、output_tokens 0。输出是 0,这是「不生成文本」最直接的证据——它一个 token 都没吐,只给了判断。

路由也写得清楚:model=multilingual,理由是 non-Latin script (han, 100% of letters)。中文输入被发到多语言 checkpoint,跟前文「路由发生在前向传播之前」对上了。

这份输出还藏着两个点。第一,这条工单里其实装着两件事:高新技术企业认定的补贴申请没进展,办公室漏水三天没人管。choice 只选了一个 policy_subsidy,把漏水那半压了下去。所以要么把问题问成「主要涉及哪一类」,要么拆成两个独立问题分别判,这正是官方说的原子问题、在代码里组合。第二,紧急度只给了 1.06,没因为「要搬走」就冲到最高档;「要搬走」这个威胁被单独的 churn_risk 问题接住了,0.8116。把不同维度拆成独立问题、再由代码定阈值,比逼一个模型吐综合结论更可控。

环境上踩了两个坑,记下来。一是 HF 直连不通,脚本里设 HF_ENDPOINT=https://hf-mirror.com 走镜像。二是机器设了 SOCKS 代理而 venv 里没装 socksio,导入直接 ImportError;权重缓存到本地后,用 HF_HUB_OFFLINE=1 离线跑最省事,顺手把 all_proxy 这类变量清掉。首次运行要下权重,三个 checkpoint 在本机缓存里实测是 2.3 GB。

这四个数落到代码里就是几个 if:category 是 policy_subsidy 且置信度 0.93,直接自动派到政策补贴组;churn_risk 0.81 过了阈值,单独拉起一条流失预警;needs_human 0.29 说明暂时不用转人工。阈值写在哪、怎么调,都是你自己的代码,模型只负责把判断给准。

如何微调 Laya

先破一个预期。基础 checkpoint 在 typed-decisions 基准上是接近随机的:0.362 和 0.342,对比随机基线 0.318、多数类基线 0.461。也就是说,它连「全都猜最大类」都比不过。那个 0.766 的成绩全部来自微调,而且是在这个基准自己的训练集上微调出来的,换到你的领域得重新喂数据。Laya 是个好底子:快、能并行、概率可校准,但它零样本决策引擎的说法站不住,价值几乎全在微调这一步。

数据。官方 notebook 用的是 LocalLLaMA/typed-decisions,1200 个 case、6000 个 decision。把 (state, questions, gold 概率分布) 转成训练项,每个问题类型的目标都是软标签的概率分布,硬标签会丢掉信息。choice 的目标是每个选项标签的概率,按选项顺序从 gold 的 probabilities 里取;noul 的目标是 [P(false), P(true)];score 的目标是每个档位的概率。拿到分布先归一化,和为 0 就退成均匀分布;再用 build_sequence 把 state 和问题拼成带 [MASK] marker 的 token 序列,marker 数量必须和选项数对齐,对不上的样本直接丢掉。

算法是 RLCD:策略梯度加严格适当评分规则当奖励。适当评分规则的意思是,如实报出自己的概率分布,期望得分才最高,模型被逼着对不确定性诚实,光会 argmax 拿不到最高奖励。proper_reward 里是对数得分加球形得分,score 型问题再减一个秩概率得分。训练时做 group sampling:给 logits 加零均值投影噪声,采出一组分布,用适当评分规则算奖励,再减均值、除标准差做优势归一化当基线。最终损失等于策略梯度项加 1.0 倍软交叉熵,软交叉熵是对整条概率分布的直接监督。

关键超参,照 notebook 抄,不改数字:

超参 值
epochs 4
micro batch 8
grad accum 4(2×T4 等效 64 条序列)
GROUP_SIZE 4
encoder LR 2.5e-5
head LR 1e-4
weight decay 0.01
退火 cosine 到 1e-6
梯度裁剪 1.0
探索噪声 σ 0.4 → 0.1
精度 fp16 autocast + GradScaler
max_tokens_per_batch 4096
max_len / head_max_len 1024 / 256
gradient checkpointing 开

这堆工程参数(gradient checkpointing、fp16、max_tokens_per_batch)各有各的用处,都是让 421M 参数的模型在 16GB 显存的 T4 上跑得动、还要同时塞下 4 个噪声采样的代价。显存吃紧就减 micro batch,别硬扛。

训练脚本用 torchrun 拉起,两张 T4 各跑一个进程:

torchrun --standalone --nproc_per_node=2 train_ddp.py "$MODEL_DIR" "$OUTPUT_DIR"

校准是收益最高、也最容易被漏掉的一步。训练完,在留出数据上按(问题类型、选项数)分桶,用 LBFGS 最小化负对数似然去拟合温度,写回配置。notebook 里是三个问题类型各拟合一个温度,拟合完 clamp 在 0.1 到 10 之间;README 报告的更细粒度是按(类型、选项数)分桶。官方数据:laya 的 ECE 从 0.466 降到 0.081,多语言版从 0.314 降到 0.106。两个 checkpoint 出厂都偏自信,多语言版甚至没带任何拟合过的温度。这一步不做,前面训练出的概率就不可用,代码按 confidence 分支等于拿一个系统偏差在赌。

评估看五样:accuracy、soft accuracy、Brier、ECE、score MAE,并且分工作流看。要提一个参照系:老师自一致性上限。基准的标签来自一个老师模型给的概率分布,0.735 是老师自己再判一遍时跟自己一致的比例,也就是这条数据的实际天花板;微调后的 Laya 是 0.766,已经贴着天花板了。分工作流看也稳:invoice processing 0.804、security incidents 0.766、customer service 0.764、agent-trace observability 0.730,四个都过了 0.73。按 primitive 拆:noul 0.857、choice 0.733、score 0.723,score 确实是最弱的一头。这套参照系搬到自己领域也一样:先量老师自一致性,再量微调模型,才知道你追的是数据上限还是模型上限。

时间成本两个口径,分开说:README 写的规模是 2×T4、4 epochs、约 3 万道题,4 到 5 小时;notebook 自己的目标规模是 1200 个 case、6000 个 decision,单次 4 到 6 分钟。两者规模差着几十倍,别拿一个当另一个。

保存与加载:训练脚本每轮存一个滚动 checkpoint,最后保存 model.safetensors 加 encoder、tokenizer,外加一份改写过的 rl_agent_config.json——写进 fine_tuned=True、model_name、拟合出的 temperature。加载就是 laya.Agent(本地路径) 一行。

整个微调闭环就五步:造数据、跑 RLCD 训练、拟合温度、评估、保存加载。最容易被跳过的是拟合温度,最容易被高估的是零样本能力。把这两件事记住了,剩下的照 notebook 抄就行。

什么时候该用

判断里要的是概率可校准、延迟低、能并行、能进代码分支,而且数据想留在本地,就值得试 Laya。选项数超过 20 的 choice,Laya 的 head_max_len 预算会顶到,优先考虑 Jev 或者拆成两步;score 是三个 primitive 里最弱的,别把关键的序数判断全押在它上面;如果不想碰训练和部署,Jev 的托管 API 开箱就能用,代价是权重和数据都在别人手里,按 token 付钱。选哪个,取决于你更在意延迟和成本,还是让数据出本地。

最后说句实在的。这个领域还年轻:Laya 是 0.3.4 的 beta,Jev 的公开数字也才一版。别指望它替你判断,它替你把判断变成一串能进代码的数字;阈值、组合、兜底,还是你自己的活。

来源:Laya 的 README、BENCHMARKS 与官方微调 notebook,以及 TypeSafe 官方文档。Jev 的性能数字均为第三方公开数据,本文未做实测。