和AI一起搞事情#9. Jev不就是个 JSON 分类器?我劝你把这篇看完再说

一、Jev究竟是做什么的

先说结论,Jev 本质上就是一台"把状态变成概率"的机器。

你给它一段描述(state),再给它几个问题(questions),它直接还你每个选项的概率分布——全程不生成任何文字。

image

  1. 输入:一个共享的 state(可为字符串/JSON 等)+ 多个运行时定义问题(typed question)。
  2. 问题类型:一个状态可以同时有多个问题
    • choice:多项选择,候选集合上的分布;
    • score:有序等级上的分布/期望;
    • noul:yes 概率
  3. 输出:每个选项直接返回概率分布,整个过程不生成输出 token

关键约束:0 个输出 token 被 decode,所有结果一次forward pass 全部得到。

二、为什么Jev这么火

一句话:它同时做到了三件之前互相矛盾的事。

BERT时代分类器 大模型 Jev
速度/并发 ✅ 极快 ❌ 慢 ✅ 极快
复杂上下文理解 ❌ 弱 ✅ 强 ✅ 强
概率输出可信 ❌ 不可信 ❌ 不可信 ✅ 接近真实

什么叫"概率输出可信"? BERT 时代做分类,输出的 softmax 概率经常是 0.9999 vs 0.9998——你根本不知道这两个数字的差距意味着什么,只能在这两个值之间手动调阈值,本质上退化成了一个排序器。

Jev 的目标是:模型说"60%",现实中这件事就真的发生了约 60% 的时间。 这叫概率校准(calibration)。有了这个,下游决策可以直接用概率值,不再需要玄学调阈值。

三、怎么理解它?

1. 如果你是 LLM 老手

你的第一反应可能是:"不就是个 JSON 分类器?"

……对,任务范式上确实如此。但类比一下:汽车也不过是"带引擎的马车",但你不会因此说它们一样。

Jev 和 LLM 解码的关键差异在于:LLM 是逐字生成,Jev 是一步读数。没有 token-by-token 的自回归循环,hidden state 直接映射成概率——速度和并发差了一个数量级,延迟在毫秒级。

另一个差异:LLM 输出的"概率"其实是 next-token 的条件概率,和"这件事发生的真实概率"没有直接关系。Jev 被专门训练来对齐这两者。

2. 如果你是 BERT 时代老兵

你的第一反应可能是:"NLI 任务卷图重来?"

也有道理,当年 BERT + NLI 那套确实做过类似的事。但有两个本质升级:

升级一:底座更强。 ModernBERT-large(421M)和 Qwen3 级别的解码器,对长上下文、复杂语义的理解远超当年。

升级二:概率校准。 这是最关键的。当年 BERT 分类器的 softmax 概率是"伪概率",训练目标把它推向 0 或 1,你拿到的数字没有概率意义。Jev 专门解决了这个问题——后面的训练章节会详细讲。

四、从两个开源项目还原 Jev 的技术路线

Jev 本身不开源,但社区有两个有代表性的复现:

  • Laya:编码器路线,用 ModernBERT,早于 Jev 推出
  • Kev:解码器路线,基于 Qwen,已被阿里云采用上线

我们从零开始讲:输入怎么拼 → 模型怎么跑 → 概率怎么读出来 → 多题怎么并发。架构和推理先讲,训练最后说。

Laya:BERT 复古,但复古得很精妙

一句话概述:Bert基座,每个选项前插一个[MASK],最后去这些 [MASK]的隐藏向量上logit打分

输入是怎么拼的?

模板长这样:

[CLS] <type> question: <instructions> [SEP] 
[MASK] opt0 [MASK] opt1 ... [SEP] <state> [SEP]

举个具体例子。给定:

state = "Hi, we were billed twice for March. Please refund the duplicate today or we will cancel our plan."
questions = {
    "department": {"type": "choice", "instructions": "Which department should handle this?",
                   "criteria": {"billing": "invoices, payments, refunds",
                                "technical": "bugs, outages, system errors",
                                "other": "everything else"}}
}

进入模型的序列就是:

[CLS] choice question: Which department should handle this? [SEP] 
[MASK] billing: invoices, payments, refunds 
[MASK] technical: bugs, outages, system errors
[MASK] other:everything else
[SEP] Hi, we were billed twice for March. Please refund the duplicate today or we will cancel our plan.[SEP]

每个选项前面各顶着一个 [MASK]。这个设计的巧妙之处在于:MASK 的位置在 Encoder 里能双向 attend 所有 token——它同时"看"到了问题、自己选项的描述、其他选项的描述,以及完整的 state。所以 MASK 位置的 hidden state,天然就是"在当前语境下,这个选项的综合表示"。

Forward 之后,怎么读出概率?

b80bb6a53538d5babb915d3bb0470e4f_a9d673a6-2bcb-4550-9d15-bb1057ae3d7a_timestamp=1790729733&nonce=e79970a81edc076973aff24d0b39dd9f&sign=37wDNHu9oLPO8wPLGRiasvg9m9VXV0AlDjCUQxq0Fg8=

四步走:

  1. Encoder 编码
h = encoder(input).last_hidden_state  # shape: [Batch, Length, Hidden]
  1. 注入题型信息
h = h + type_embedding(question_type)  # choice/score/noul 语义注入
  1. 决策头:再过一个2层的transformer,从 Encoder 的通用表示里蒸馏出决策信号

  2. 只读取MASK位置,按不同问题类型进行归一化计算

mask_hiddens = h[:, mask_positions, :]  # 只取 MASK 位置
logits = scorer(mask_hiddens)           # hidden → scalar

# 按题型分别处理
if type == "choice":
    logits_scaled = logits / t_scale    # t_scale 按题型×选项数分桶取温度
    probs = softmax(logits_scaled)

elif type == "score":
    probs = softmax(logits / t_scale)
    expected_score = sum(i * p_i for i, p_i in enumerate(probs))

elif type == "noul":
    probs = softmax(logits / t_scale)
    yes_prob = probs[1]                 # 直接取"是"的概率

注意那个 t_scale——按题型 × 选项数分桶的温度系数,这是 Laya 概率校准的关键手段,后面训练章节会详细展开。

  1. 额外输出:置信度

Laya 还会从 [CLS] 的 hidden state + 4 个概率统计指标(熵、最大概率等)预测一个置信度分数,用于判断"这道题 AI 有没有把握,要不要转人工"。这个设计实际上是对"没有正确选项"问题的一种工程解法。

多题并发怎么做?

一个 state 有 N 个问题时,Laya 用 Batch 推理:N 个问题各自构建一条序列,state 在每条序列里都完整复制一份,然后一起打包进模型。显存换速度,架构本身不支持 state 的 KV cache 共享。

Kev:解码器变身并行决策机

一句话概述:把因果LM“改造成并行决策机”,关键是 block-causal 隔离 + pointer head

输入是怎么拼的?

Kev 基于 Qwen,是因果解码器,序列是线性的:

[ state tokens ] 
[ <q> instr1 <opt> A </opt> <opt> B </opt> <opt> C </opt> <decide> ]
[ <q> instr2 <opt> X </opt> <opt> Y </opt>                 <decide> ]

State 在前面,各道题依次排列,每道题以 <decide> 结尾。

除了 token id,Kev 还维护了几个并行数组,记录序列中每个 token 的"身份信息":

数组名 含义 例子(5个state token + 1道选择题ABC)
ids token id 序列 [s0,s1,s2,s3,s4, <q>,i0,i1, <opt>,A,</opt>, <opt>,B,</opt>, <opt>,C,</opt>, <decide>]
seg 每个 token 属于哪道题(0=state) [0,0,0,0,0, 1,1,1, 1,1,1, 1,1,1, 1,1,1, 1]
pos position id state: [0..4];每个问题都重置 重新从 5 开始
opt 属于该题的第几个选项(OPT_NONE/0/1/2/OPT_DECIDE) [-, -, -, ..., NONE, NONE, 0, 0, 0, 1, 1, 1, 2, 2, 2, DECIDE]
decide_idx <decide> 在 ids 中的位置 [17]
opt_idx 每个 </opt> 的位置 [[10, 13, 16]](对应 A/B/C 的边界)

这些数组在 Forward 之后用于精准定位"去哪个位置读概率"。

问题隔离:Block-Causal Mask

Decoder 默认是因果掩码——每个 token 只能看到自己前面的 token。但这会导致:第二道题的 <decide> token 能看到第一道题的所有选项,两道题的概率会互相污染。

Kev 的解法是 Block-Causal Mask:

题目1内部:完整因果注意力(可以看 state + 题目1自己的 token)
题目2内部:完整因果注意力(可以看 state + 题目2自己的 token)
题目1 → 题目2:完全屏蔽(attention mask = -inf)

效果类似滑动窗口注意力,每道题只在自己的"小世界"里做注意力,题目之间完全隔离。

Kev 还提供了更激进的 Option Isolation:题目内部的各个选项之间也互相屏蔽,每个选项独立 attend state 和问题指令,选项之间完全不知道彼此的存在。

⚠️ 注意:Option Isolation 有适用限制。如果选项中有"其他"这类相对性选项,选项之间需要"知道彼此"才能正确理解语义,这时不能用 Option Isolation。

概率怎么读出来:Pointer Head

a4c703f199e4a6ba67f313dc7a80a10f_19c1a521-280f-41e2-b686-f759c073bc01_timestamp=1790758303&nonce=d50fdcb9ffafe2117ea6869a65f154bd&sign=iKaIfgh1TkYJTBjP4r7M9AID_dqDKsKph0Ifj-goVMI=

Forward 完成后,每个 token 都有一个 hidden state 向量。Kev 用 Pointer Head 把这些向量变成概率:

# <decide> 位置的 hidden → 决策 query("我想选哪个")
q = proj_q(hidden[decide_idx])            # shape: [D]

# 每个 </opt> 位置的 hidden → 选项 key("我是第 k 个选项")
k = proj_k(hidden[opt_idx])              # shape: [num_options, D]

# 点积打分 → softmax → 概率
logits = (q @ k.T)                       # shape: [num_options]
probs = softmax(logits / temperature)

为什么用 <decide> 而不是其他位置?

因果解码器里,只有序列中"最后一个能看到所有选项"的 token,才能做全局比较。<decide> 被设计在每道题的最末尾,正是这个角色——它 attend 到了该题的完整选项列表后,再综合判断。

Pointer Head 对 Option Count 问题的天然优势:

普通 joint softmax 有个问题:选项越多,每个选项的分母越大,概率被稀释。Pointer Head 的每个 logit 是 <decide> 和 </opt> 的点积,每个选项独立计算 logit,选项数不影响 logit 的绝对量级,最后才做 softmax。这比 Laya 的联合 softmax 对选项数的变化更鲁棒。

五、训练:决策模型要解决哪些问题?

Jev 的训练方案未开源,以下基于 Kev/Laya 实现进行分析推断。

把推理层搭好只是第一步。让模型真正输出"有意义的概率",训练阶段需要专门解决五个问题。

Calibration:选项概率更接近真实概率分布

猜测方案:合成数据 + soft labels 蒸馏 + soft cross-entropy训练 + 后验温度系数

问题的根源:Cross-entropy 损失函数的理论最优解确实是真实概率分布。但理论归理论,实践中神经网络在 loss 降到很低时,logit 会被推得越来越大,softmax 变得越来越尖——argmax 很对,但概率的刻度已经失真。

最典型的症状:模型对一道 60/40 开的问题,输出 [0.9999, 0.0001]。方向对了,但概率值骗人。

可能的解法

① Soft Label + Soft Cross-Entropy

根本矛盾是标签不诚实。解法是换掉标签:

# 硬标签(过度自信的根源)
hard_label = [0, 1, 0]

# Soft Label(更接近真实世界)
soft_label = [0.1, 0.8, 0.1]

# Soft Cross-Entropy
loss = -sum(soft_label[i] * log(pred[i]) for i in range(K))

Soft Label 从哪来?最可能的路线是:用大模型或多个模型对同一个样本重复采样,统计各选项被选的频率,作为该样本的 soft label。本质上是对真实分布的蒙特卡罗估计。

② 后验温度缩放

光靠训练还不够。Kev 和 Laya 都在训练完成后,用验证集拟合一个温度系数 T:

# T > 1:概率分布变软(修正过度自信)
# T < 1:概率分布变尖(修正过度保守)
calibrated_probs = softmax(logits / T)

T 不是一个全局值,而是按题型 × 选项数分桶拟合。"3个选项的 choice 题"和"5个选项的 choice 题"用不同的 T。Laya 在校准前 ECE 高达 0.466,分桶拟合后降到 0.081——几乎全部的校准效果来自这一步。

Permutation:让选项概率与顺序无关

引入Permutation KL(置换一致性),把同一道题的选项随机打乱,要求输出分布(对应到选项)不变

perm_logits = model(permuted_input)
perm_kl = KL(softmax(logits[original_order]) || softmax(perm_logits[inv_perm]))

并且在训练时对选项顺序做shuffle,再推理层让所有选项的位置编码一致不引入偏差等等。

Ordianl prediction:有序打分的分布如何训练

Score 题(1-5分)如果当普通分类训练,损失函数对所有错误一视同仁——预测 2 分而真实是 3 分,和预测 5 分而真实是 3 分,loss 一样大。但显然后者错得更离谱。

解法:RPS(Ranked Probability Score)

用累积分布函数(CDF)的平方差作为损失:

rps = sum((CDF_pred[j] - CDF_target[j])**2 for j in range(K-1))

这个损失的本质是:惩罚"分布形状"的错误,而不只是"峰值位置"的错误。预测集中在 2-3 分的模型,比预测集中在 1 分的模型,RPS 更小——即使两者的 argmax 都是"错的"。

option count:如何降低选项个数对真实选项概率的影响

这个问题的本质是 softmax 的归一化耦合:所有选项共享一个归一化分母,选项数越多,单个选项的概率天然被稀释。

kev的架构受到的影响较小,因为是每个选项和<decide>独立计算logit之后再归一化。

残余问题靠分桶温度补救:温度系数按选项数分桶,2选题和10选题用不同的 T,在后验层面对齐不同选项数下的概率刻度。

但需要接受选项数影响是天然存在的问题,跨选项数直接比较两道题的概率,在语义上本来就不合适,不是能完全修好的Bug。

no correct option:如果没有正确选项怎么选

比较朴素的,Kev是默认在训练和推理中都加入“ None-of-Above 选项”

options_augmented = options + ["[NONE_OF_ABOVE]"]

Laya的方案是同时输出选项概率和置信度分数(基于 [CLS] hidden state + 熵等统计指标)。当置信度低于阈值时,直接拒绝所有概率输出,标记为"无可信答案"。

Jev in Harnesss

了解了原理,最后我们看看在Harness框架搭建中,有哪些可以利用Jev进行优化、提速的点。

这里我们抛砖引玉,给大家几种在harness中可以使用Jev进行效率优化的方向,部分场景还未验证,哈哈毕竟AI写代码太快但是很多场景验证比较复杂。有些指令较长,感兴趣的可以直接看代码jev_harness_tool

这里使用用户query和agent所有候选技能作为输入,判断解决用户问题是否需要使用技能,如果需要应该使用哪个技能。

skill_search_motion

类似的思路也可以用在mcp search、effort routing、memory search等等。不过model routing在我的使用场景中往往效率提升不明显但质量下降很明显,因为对场景复杂度的评估需要更全面的上文,所以实现很简单,但效果比较难评。

围栏类:LLM Guardrail

拦截用户的各类风险输入和prompt注入,这里提供了9种基础的风险类型,每种风险可以提供不同的处置方案block、review etc。

llm_guardrail_motion

类似的思路也可以用于模型输出检查、tool call检查etc。

校验类:reference verify

以下适用于inline 引用,提供模型推理段落,inline推理+对应reference正文,判断搜索上文能否支持推理输出。
reference_verify_motion
类似的思路也可以同于ool result校验agent结果,用在loop中检查task completed等等。

posted @ 2026-10-19 08:07  风雨中的小七  阅读(67)  评论(0)    收藏  举报