<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Calibration on Wilson Wu</title><link>https://wilsonwu.me/zh/tags/calibration/</link><description>Recent content in Calibration on Wilson Wu</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Wed, 14 Oct 2026 00:00:00 -0400</lastBuildDate><atom:link href="https://wilsonwu.me/zh/tags/calibration/index.xml" rel="self" type="application/rss+xml"/><item><title>训练一个自己的 Jev：决策模型的原理、数据、算力、训练与评测全流程</title><link>https://wilsonwu.me/zh/blog/2026/train-your-own-jev/</link><pubDate>Wed, 14 Oct 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/train-your-own-jev/</guid><description>&lt;p&gt;&lt;img alt="训练一个自己的 Jev" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/train-your-own-jev/1-train-your-own-jev.png"&gt;&lt;/p&gt;
&lt;h2 id="引言"&gt;引言&lt;/h2&gt;
&lt;p&gt;过去一个月，我连续写了两篇决策模型的文章：&lt;a href="https://wilsonwu.me/zh/blog/2026/jev-vs-laya/"&gt;Jev vs Laya&lt;/a&gt; 对比了闭源的 Jev 和开源的 Laya，&lt;a href="https://wilsonwu.me/zh/blog/2026/microsoft-decision-1/"&gt;Microsoft-Decision-1 详解&lt;/a&gt; 又加入了微软。写完之后，被问得最多的问题是：&lt;strong&gt;能不能自己训练一个？&lt;/strong&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="训练一个自己的 Jev" loading="lazy" src="/zh/blog/2026/train-your-own-jev/1-train-your-own-jev.png"></p>
<h2 id="引言">引言</h2>
<p>过去一个月，我连续写了两篇决策模型的文章：<a href="/zh/blog/2026/jev-vs-laya/">Jev vs Laya</a> 对比了闭源的 Jev 和开源的 Laya，<a href="/zh/blog/2026/microsoft-decision-1/">Microsoft-Decision-1 详解</a> 又加入了微软。写完之后，被问得最多的问题是：<strong>能不能自己训练一个？</strong></p>
<p>答案是可以，而且门槛比想象中低。看看第三方榜单 JevBench 就知道：截至 10 月中旬，开源权重榜上已经有 160 多个参与排名的系统，绝大多数出自社区和中小团队之手。排在前面的也不神秘：综合分第一的 H2O-Lightning-4B 是在阿里开源的 Qwen3.5-4B 上微调出来的，能力分第一的 Quyet-1.0-Large 则是 Gemma-4-31B 加一个适配器；微软的 Microsoft-Decision-1，底座同样是开源的 Qwen3.5-9B。</p>
<p>这说明一件事：<strong>决策模型的核心竞争力不在模型结构，而在数据、训练目标和评测体系。</strong> 这三样东西，普通团队完全可以自己掌握。</p>
<p>本文按一个真实项目的顺序，把训练一个“自己的 Jev”的全过程走一遍：</p>
<ol>
<li>原理：决策模型到底在学什么，为什么训练目标必须是“严格适当评分规则”</li>
<li>数据：去哪里找，怎么统一格式、清洗、增强和切分</li>
<li>算力：需要多少显存、多少 GPU 小时、多少钱</li>
<li>底座：怎么选基础模型</li>
<li>训练：从零写一套可以直接运行的 LoRA 训练脚本</li>
<li>评测：用哪些指标打分，怎么和 JevBench 上的模型同场比较</li>
<li>调优：分数不理想时，按什么思路改进</li>
</ol>
<p>文中的脚本我都在 Qwen3.5-0.8B 上实际跑通过（CPU 冒烟测试，只用来验证代码正确性），文中引用的榜单和模型数据均来自公开资料，链接列在文末。</p>
<h2 id="一先想清楚决策模型到底在学什么">一、先想清楚：决策模型到底在学什么</h2>
<h3 id="1-本质是一个条件概率">1. 本质是一个“条件概率”</h3>
<p>先回顾一下决策模型的接口：调用方给出一段状态（state）和一个问题，问题的候选答案由调用方提前定义好，模型返回每个候选答案的概率。三种问题原语都可以统一成同一件事：</p>
<table>
	<thead>
			<tr>
					<th>原语</th>
					<th>候选答案</th>
					<th>本质</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>choice</code></td>
					<td>调用方定义的 K 个选项</td>
					<td>K 分类</td>
			</tr>
			<tr>
					<td><code>noul</code></td>
					<td>是 / 否</td>
					<td>2 分类</td>
			</tr>
			<tr>
					<td><code>score</code></td>
					<td>K 个有序等级</td>
					<td>有序的 K 分类</td>
			</tr>
	</tbody>
</table>
<p>所以，训练一个决策模型，就是让模型学会估计 <code>P(选项 | 状态, 问题)</code>。和普通的文本分类模型相比，它有两个关键区别：</p>
<ul>
<li><strong>类别不是固定的</strong>：每次请求的问题和选项都不一样，模型必须读懂选项的含义，而不是记住一组固定的标签。这就是它需要“零样本泛化”能力的原因。</li>
<li><strong>概率本身就是产品</strong>：下游程序会直接拿概率做 <code>if</code> 判断，所以概率必须“诚实”，也就是校准过的。这决定了训练目标的选择，后面会专门讲。</li>
</ul>
<h3 id="2-三条技术路线">2. 三条技术路线</h3>
<p>目前社区的做法大致分成三类：</p>
<table>
	<thead>
			<tr>
					<th>路线</th>
					<th>代表</th>
					<th>做法</th>
					<th>优点</th>
					<th>缺点</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>编码器 + 决策头</td>
					<td>Laya</td>
					<td>在每个选项前放一个 <code>[MASK]</code>，经过双向编码器后，取这些位置的隐状态打分</td>
					<td>极快，4 亿参数，T4 上单题 33～40 ms</td>
					<td>零样本能力弱，上下文短，必须针对任务微调</td>
			</tr>
			<tr>
					<td>解码器 + 选项字母</td>
					<td>Quyet-1.0-Large、H2O-Lightning-4B</td>
					<td>把选项编号成 A、B、C……，模型读完提示词后，直接取下一个 token 在这些字母上的概率</td>
					<td>复用大模型的知识和语言模型头，零样本能力强</td>
					<td>参数量大，选项数受单 token 标签数量限制</td>
			</tr>
			<tr>
					<td>解码器 + 决策头</td>
					<td>jiwo、ClassOne</td>
					<td>取解码器的隐状态，接一个单独训练的打分头</td>
					<td>选项数和输出形式更灵活</td>
					<td>打分头从零开始训练，更依赖数据</td>
			</tr>
	</tbody>
</table>
<p>JevBench 的数据给出了很清楚的信号：</p>
<ul>
<li>几个编码器路线的系统，零样本智能分都只有个位数（Laya 1.8、modernbert-jev v2 2.6、open-jev-deberta-v3-large 2.8），和随机猜测差不多；</li>
<li>完全不做决策微调、直接读取 Qwen3-4B-Instruct-2507 的 logits，智能分就有 34.0；</li>
<li>在 Qwen3.5-4B 上做过决策微调的 H2O-Lightning-4B 达到 60.0，接近 Jev 1.13.0 的 63.6；</li>
<li>更大的 Gemma-4-31B 底座（Quyet-1.0-Large）达到 73.4。</li>
</ul>
<p>结论很明确：<strong>如果目标是 Jev 那样的“零样本通才”，应该走解码器路线</strong>，大模型预训练时学到的知识是零样本能力的来源，决策微调则把这些知识“对齐”到选项概率上；如果只服务一两个固定任务，对延迟要求极高，而且有足够的标注数据，Laya 这样的编码器路线依然是性价比最高的选择。</p>
<p>本文的主线是第二条路线：<strong>解码器 + 选项字母</strong>。它的推理过程是这样的：</p>
<p><img alt="一次前向计算读出选项概率" loading="lazy" src="/zh/blog/2026/train-your-own-jev/2-letter-logit-readout.png"></p>
<ol>
<li>把状态、问题和带字母编号的选项拼成一段对话提示词，以助手回复的开头结束；</li>
<li>模型做一次前向计算（只有预填充，没有逐 token 生成）；</li>
<li>在最后一个位置（答案位）取出 A、B、C……这几个字母 token 的 logits，只在这 K 个 logits 上做 softmax，得到每个选项的概率。</li>
</ol>
<p>H2O-Lightning-4B 的模型卡把这概括为“一次前向，一个输出 token”（one forward pass and one output token）。因为不生成文本，成本几乎只有输入 token；又因为输出空间被限定在 K 个字母上，模型不可能答出选项以外的东西。这条路线还有一个先天优势：大模型在选择题格式下给出的字母概率，本身就有不错的校准性（Kadavath 等人，2022），决策微调是在一个好的起点上继续打磨。</p>
<h3 id="3-训练目标为什么必须是严格适当评分规则">3. 训练目标：为什么必须是“严格适当评分规则”</h3>
<p>决策模型输出的概率要直接进入程序的分支判断，所以我们要的不只是“答对”，还要<strong>校准</strong>：在所有给出 80% 把握的回答里，应该恰好有大约 80% 是对的。</p>
<p><strong>第一个坑：只奖励“答对”会毁掉校准。</strong> 假设一道题的真实分布是 p（比如 70% 选 A、30% 选 B），模型报告的分布是 q。如果把模型当成策略，按 q 抽一个答案，答对得 1 分，期望奖励就是 <code>Σ p_k · q_k</code>，它对 q 是线性的，最优解是把全部概率押在最可能的选项上，也就是让 A 的概率等于 1。模型被训练成“永远 100% 确定”，准确率没变，概率却失去了意义。</p>
<p><strong>解决办法是用严格适当评分规则（strictly proper scoring rule）做目标。</strong> 它的定义是：只有当模型报告的分布 q 等于真实分布 p 时，期望得分才能达到最大值，说真话是唯一的最优策略（经典论述见 Gneiting 和 Raftery 2007 年的论文）。常用的有四种：</p>
<pre tabindex="0"><code>记 q = (q_1, …, q_K) 为模型给出的分布，y 为标签（one-hot 或软标签）

对数评分（越大越好）     S_log(q, y) = Σ y_k · log q_k            就是负的交叉熵
Brier 分数（越小越好）   B(q, y)     = Σ (q_k − y_k)²
球面评分（越大越好）     S_sph(q, y) = Σ y_k · q_k / ‖q‖          取值 0～1，梯度比对数评分温和
排序概率分（越小越好）   RPS(q, y)   = Σ (Q_i − Y_i)² / (K − 1)   Q、Y 是累积分布，用于有序的 score
</code></pre><p>这里有一个经常被忽略的事实：<strong>交叉熵本身就是严格适当的</strong>（它就是对数评分）。那为什么用交叉熵训练出来的神经网络普遍过度自信？原因不在损失函数，而在于有限的训练数据：模型会把训练样本“背”下来，把它们的概率推向 1，到了没见过的数据上，自信程度就高于实际准确率（Guo 等人 2017 年的论文对此有系统的分析）。所以有两点要做到：</p>
<ol>
<li><strong>训练时</strong>：用严格适当评分规则做目标；标签本身有不确定性时（标注员意见不一、多个教师模型投票不一致），用软标签代替 one-hot，让模型学会“诚实地不确定”；不要训练过多 epoch。</li>
<li><strong>训练后</strong>：在留出的开发集上做<strong>温度缩放</strong>（temperature scaling），即 <code>p = softmax(z / T)</code>，按问题类型拟合一个温度 T。它不改变排序，所以不影响准确率，只修正自信程度。</li>
</ol>
<p><img alt="过度自信 vs 校准" loading="lazy" src="/zh/blog/2026/train-your-own-jev/3-calibration.png"></p>
<p>温度缩放的效果往往非常明显。Laya 的出厂检查点按“问题类型 × 选项数”重新拟合温度后，平均 ECE（期望校准误差）从 0.466 降到了 0.081。而 H2O-Lightning-4B 随模型发布的温度是 choice 0.75、noul 0.8、score 0.65，都小于 1，说明它的原始输出反而偏保守，需要“锐化”。方向因模型而异，只能实测，不能拍脑袋。</p>
<h3 id="4-rlcdlaya-公开的强化学习做法">4. RLCD：Laya 公开的强化学习做法</h3>
<p>Jev 把自己的训练方法称为 RLCD（Reinforcement Learning for Calibrated Decisions，面向校准决策的强化学习），但没有公开细节。Laya 沿用了这个名字，并公开了一个具体实现：</p>
<ul>
<li><strong>奖励</strong>：三种严格适当评分的组合，<code>Reward = S_log + 0.5 × S_sph − RPS</code>，其中 RPS 只对有序的 score 题生效；</li>
<li><strong>探索</strong>：每道题对 logits 加 8 组高斯噪声（噪声在选项维度上减去均值，因为给所有 logits 加同一个常数不会改变 softmax），噪声的标准差从 1.0 逐步衰减到 0.3；</li>
<li><strong>优势</strong>：8 个样本的奖励减去组内均值、除以组内标准差，和 GRPO 的组内基线是同一个思路（GRPO 的原理可以参考我之前写的 <a href="/zh/blog/2026/ppo-vs-grpo/">PPO vs GRPO</a>）；</li>
<li><strong>更新</strong>：用高斯分布的对数密度做 REINFORCE 式的策略梯度，完全不使用交叉熵损失。</li>
</ul>
<p>我的理解是：<strong>当模型直接输出完整的概率分布时，评分规则本身就是可微的，直接对它求梯度（也就是把评分规则当损失函数）和用强化学习去估计这个梯度，在期望上指向同一个方向，前者方差更小、更省事。</strong> 强化学习的形式真正不可替代的场景，是奖励无法对输出直接求导的时候，比如奖励来自多步 Agent 任务最终是否成功、用户的真实反馈，或者 Laya 处理的多轮销售对话：奖励是对话最终是否成交，需要用 TD(λ) 把这个结果分摊到每一轮。</p>
<p>为了验证这个判断，我用后文训练脚本里的 <code>reward()</code> 函数做了两个小实验：一是把目标分布设为 [0.7, 0.2, 0.1]，直接对组合评分做梯度上升，模型输出的分布最终收敛到 [0.7, 0.2, 0.1]，说明这个组合评分确实“只奖励说真话”；二是对同一组 logits，用 RLCD 的方式（2 万组扰动，σ = 0.05）估计梯度，它和直接求导得到的梯度方向几乎完全一致（余弦相似度 0.999）。</p>
<p>所以本文的训练脚本两种都支持：默认直接优化评分规则（<code>--objective proper</code>），也可以切换成 Laya 式的 RLCD（<code>--objective rlcd</code>），自己对比。</p>
<h3 id="5-从概率到动作阈值由代价决定">5. 从概率到动作：阈值由代价决定</h3>
<p>概率校准好之后，“什么时候自动执行、什么时候转人工”就不必拍脑袋了。Laya 给它的 Act / Escalate 头设计了一个代价矩阵：判对 +1，判错 −3，转人工 −0.5。自动执行的期望收益是 <code>p × 1 + (1 − p) × (−3)</code>，它大于 −0.5 的条件是 <code>p &gt; 0.625</code>。一般地：</p>
<pre tabindex="0"><code>自动执行的阈值 = (转人工的收益 − 判错的收益) / (判对的收益 − 判错的收益)
              = (−0.5 − (−3)) / (1 − (−3)) = 0.625
</code></pre><p>只要概率是校准的，这个阈值可以直接从业务代价算出来，单独训练一个“该不该执行”的头反而不是必需的（Laya 的 README 也坦承它的 Act 头目前几乎没有信号，建议直接用置信度做门控）。H2O-Lightning-4B 在 3474 道留出题上给出了阈值的实际效果：</p>
<table>
	<thead>
			<tr>
					<th>置信度阈值</th>
					<th>转交比例</th>
					<th>其余题目的准确率</th>
					<th>放行但答错（占全部题目）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>不设阈值</td>
					<td>0%</td>
					<td>约 83%</td>
					<td>约 17%</td>
			</tr>
			<tr>
					<td>0.8</td>
					<td>37.7%</td>
					<td>95.8%</td>
					<td>2.6%</td>
			</tr>
			<tr>
					<td>0.9</td>
					<td>53.1%</td>
					<td>98.5%</td>
					<td>0.7%</td>
			</tr>
	</tbody>
</table>
<p>这张表就是你训练完之后要为自己的模型画出来的东西。</p>
<h3 id="6-全流程一览">6. 全流程一览</h3>
<p>把上面的原理落到工程上，一个完整的训练项目大致是这样的循环：</p>
<pre tabindex="0"><code>① 定义题目和验收标准：指标、阈值、上线门槛，先写下来再看结果
        │
② 找数据 → 统一格式 → 去重、去污染 → 按任务切分
        │
③ 零样本基线：直接读底座模型的选项字母概率，先知道起点在哪里
        │
④ LoRA 微调：选项随机乱序 + 严格适当评分规则（或 RLCD）
        │
⑤ 在开发集上拟合温度，在留出任务、公开集和私有封存集上打分
        │
⑥ 按切片做误差分析 → 补数据 → 回到 ④
        │
⑦ 合并权重，接入 /v1/systemone 接口，灰度上线，持续监控
</code></pre><p>接下来按这个顺序逐一展开。</p>
<h2 id="二数据从哪里找怎么准备">二、数据：从哪里找，怎么准备</h2>
<p>数据决定了决策模型的上限。H2O-Lightning-4B 在模型卡里总结的几条经验，几乎都和数据有关：留一份从不参与训练的封存评测集、上线标准先写下来再看结果、训练数据和所有评测集做精确和近似去重、按主题和题型找短板再补数据、标签只来自人工标注或多个独立强模型的一致意见。</p>
<h3 id="1-数据从哪里来">1. 数据从哪里来</h3>
<table>
	<thead>
			<tr>
					<th>来源</th>
					<th>例子</th>
					<th>优点</th>
					<th>注意事项</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>公开的人工标注数据集</td>
					<td>意图：Banking77、CLINC150、MASSIVE（51 种语言）；主题：AG News、TNEWS；推理：BoolQ、MNLI、XNLI、OCNLI；情感与打分：SST-5、Yelp 评论星级、HelpSteer2；安全：毒性检测、提示词注入</td>
					<td>标签可靠，数量大，覆盖面广</td>
					<td>逐一核对许可证，不少学术数据集只允许研究用途；格式各异，需要改写成决策题</td>
			</tr>
			<tr>
					<td>自己的业务数据</td>
					<td>工单、邮件、审核记录、Agent 执行日志，加上人工复核的结论</td>
					<td>和线上分布一致，价值最高</td>
					<td>脱敏；早期标签可能有噪声，需要抽检</td>
			</tr>
			<tr>
					<td>合成数据</td>
					<td>用开源大模型批量生成状态和问题，再由多个独立的模型分别作答，只保留意见一致的样本，或把投票比例作为软标签</td>
					<td>能快速补齐长尾场景和短板</td>
					<td>只用一个模型打标，学到的就是那个模型的错误；部分商业 API 的条款限制用其输出训练竞争模型，使用前务必确认</td>
			</tr>
			<tr>
					<td>程序生成的可验证数据</td>
					<td>日期计算、计数、按规则判断（如“金额超过阈值且无审批则拒绝”）、游戏和模拟中的状态追踪</td>
					<td>标签由代码算出，100% 正确，数量不受限</td>
					<td>题目容易模板化，要在措辞和结构上多做变化</td>
			</tr>
	</tbody>
</table>
<p>几个经验值得参考：</p>
<ul>
<li><strong>Laya 坚持“零合成”</strong>：训练数据全部来自人工标注的公开数据集（客服意图、自然语言推理、内容安全、越狱与注入、评分细则打分、多轮对话结果等），理由是用模型生成的标签训练校准模型，只会把模型校准到“老师”的错误上。</li>
<li><strong>H2O-Lightning-4B 的数据配比</strong>：按场景分，文档、政策与规则 37%，数字、日期与概率 22%，游戏与模拟 12%，日常语言与知识 10%，客服与运营 10%，安全与护栏 7%，代码与工具调用 2%；按题型分，choice 54%、noul 29%、score 16%；超过 99% 是英文。训练分三个阶段：先广泛覆盖，再集中强化长文档和数字推理，最后把所有类型重新混合。</li>
<li><strong>任务的多样性比单个任务的样本量更重要</strong>：FLAN 系列研究发现，指令微调时覆盖的任务种类越多，模型在没见过的任务上表现越好，这一点对决策模型同样成立。在总量相同的情况下，覆盖更多不同的任务，通常比在少数几个任务上堆样本更有利于零样本泛化。</li>
</ul>
<p>如果只是做某个垂直领域的专用模型，几千到几万条高质量标注就能有明显提升：Laya 的 typed-decisions 检查点用约 3 万道题微调，准确率从 0.362 提升到 0.766；社区有人用单张 16 GB 显卡把 Laya 微调成浏览器 Agent 的操作决策器，约 45 个候选元素中选对目标的比例从 0.10 提升到 0.66。</p>
<h3 id="2-统一的数据格式">2. 统一的数据格式</h3>
<p>建议直接沿用 Jev 的请求格式，每行一道决策题：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;task&#34;</span>: <span style="color:#e6db74">&#34;ticket_routing&#34;</span>, <span style="color:#f92672">&#34;state&#34;</span>: <span style="color:#e6db74">&#34;Checkout returns HTTP 500 for every customer since 09:10.&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;question&#34;</span>: {<span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;choice&#34;</span>, <span style="color:#f92672">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;Which team should handle this ticket?&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">&#34;criteria&#34;</span>: {<span style="color:#f92672">&#34;billing&#34;</span>: <span style="color:#e6db74">&#34;Charges, invoices and refunds&#34;</span>,
</span></span><span style="display:flex;"><span>                           <span style="color:#f92672">&#34;engineering&#34;</span>: <span style="color:#e6db74">&#34;Bugs, errors and outages&#34;</span>,
</span></span><span style="display:flex;"><span>                           <span style="color:#f92672">&#34;support&#34;</span>: <span style="color:#e6db74">&#34;How-to and account questions&#34;</span>}},
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;answer&#34;</span>: <span style="color:#e6db74">&#34;engineering&#34;</span>}
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;task&#34;</span>: <span style="color:#e6db74">&#34;refund_check&#34;</span>, <span style="color:#f92672">&#34;state&#34;</span>: {<span style="color:#f92672">&#34;subject&#34;</span>: <span style="color:#e6db74">&#34;Charged twice&#34;</span>, <span style="color:#f92672">&#34;body&#34;</span>: <span style="color:#e6db74">&#34;Please refund the duplicate.&#34;</span>},
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;question&#34;</span>: {<span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;noul&#34;</span>, <span style="color:#f92672">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;Does the customer ask for a refund?&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">&#34;criteria&#34;</span>: {<span style="color:#f92672">&#34;true&#34;</span>: <span style="color:#e6db74">&#34;yes, a refund is requested&#34;</span>, <span style="color:#f92672">&#34;false&#34;</span>: <span style="color:#e6db74">&#34;no refund is requested&#34;</span>}},
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;answer&#34;</span>: <span style="color:#66d9ef">true</span>}
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;task&#34;</span>: <span style="color:#e6db74">&#34;urgency&#34;</span>, <span style="color:#f92672">&#34;state&#34;</span>: <span style="color:#e6db74">&#34;Payroll export fails and payroll runs today.&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;question&#34;</span>: {<span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;score&#34;</span>, <span style="color:#f92672">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;How urgent is this request?&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">&#34;criteria&#34;</span>: [<span style="color:#e6db74">&#34;not urgent&#34;</span>, <span style="color:#e6db74">&#34;soon&#34;</span>, <span style="color:#e6db74">&#34;today&#34;</span>, <span style="color:#e6db74">&#34;blocking right now&#34;</span>]},
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;answer&#34;</span>: <span style="color:#ae81ff">2</span>}
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;task&#34;</span>: <span style="color:#e6db74">&#34;intent_soft&#34;</span>, <span style="color:#f92672">&#34;state&#34;</span>: <span style="color:#e6db74">&#34;I was billed twice and the app keeps crashing.&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;question&#34;</span>: {<span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;choice&#34;</span>, <span style="color:#f92672">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;What is the main topic?&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">&#34;criteria&#34;</span>: {<span style="color:#f92672">&#34;billing&#34;</span>: <span style="color:#e6db74">&#34;payments&#34;</span>, <span style="color:#f92672">&#34;technical&#34;</span>: <span style="color:#e6db74">&#34;bugs&#34;</span>}},
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;target&#34;</span>: {<span style="color:#f92672">&#34;billing&#34;</span>: <span style="color:#ae81ff">0.6</span>, <span style="color:#f92672">&#34;technical&#34;</span>: <span style="color:#ae81ff">0.4</span>}}
</span></span></code></pre></div><p>几点约定：</p>
<ul>
<li>为了便于阅读，上面的示例做了换行，实际文件中每条记录占一行（JSONL）；</li>
<li><code>answer</code> 对 choice 是选项名，对 noul 是布尔值，对 score 是等级下标；有不确定性的样本用 <code>target</code> 给出软标签；</li>
<li><code>task</code> 字段记录样本来自哪个任务，后面切分和按切片分析都要用到；</li>
<li>这个格式和 JevBench 公开题集的格式几乎一致（它用 <code>expected</code> 字段存答案），后面的评测脚本可以直接读取 JevBench 的公开题。</li>
</ul>
<h3 id="3-清洗与增强">3. 清洗与增强</h3>
<p><strong>清洗</strong>要做三件事：</p>
<ol>
<li><strong>去重</strong>：先做规范化后的精确去重，再用 MinHash 之类的方法做近似去重，避免同一条数据换个标点就出现好几遍；</li>
<li><strong>去污染</strong>：把所有评测集（自己的封存集、JevBench 公开题等）都和训练集做比对，有重叠的从训练集里删掉。JevBench 要求在公开题上训练过的模型必须主动声明，而且公开题成绩明显高于封存题的模型还会被扣分，“刷”公开题没有意义；</li>
<li><strong>配平</strong>：给每个任务设上限，防止某个大数据集主导训练；同时控制题型、语言、选项数的分布。</li>
</ol>
<p><strong>增强</strong>的目标是让模型学会“看内容，而不是看形式”：</p>
<table>
	<thead>
			<tr>
					<th>增强方式</th>
					<th>要防止的问题</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>每次训练都随机打乱选项顺序</td>
					<td>位置偏差：Laya 实测，5 个文字完全相同的选项，仅仅因为位置不同，logit 就从 +1.68 一路降到 −1.91</td>
			</tr>
			<tr>
					<td>改写问题的措辞，选项名在有语义的词和中性标签之间切换</td>
					<td>只认关键词，或被 <code>yes</code>、<code>true</code> 之类的选项名带偏</td>
			</tr>
			<tr>
					<td>state 在纯文本和嵌套 JSON 之间切换，字段名同义替换</td>
					<td>只认识某一种输入格式</td>
			</tr>
			<tr>
					<td>在 state 里加入无关字段和干扰段落</td>
					<td>被无关内容稀释注意力</td>
			</tr>
			<tr>
					<td>加入“无法判断”选项，并构造信息不足的样本</td>
					<td>信息不够时也被迫硬选</td>
			</tr>
			<tr>
					<td>构造否定问题对：同一状态分别问 Q 和“非 Q”</td>
					<td>两个概率之和不等于 1</td>
			</tr>
			<tr>
					<td>在 state 中插入提示词注入文本，标签保持不变</td>
					<td>被 state 里的指令劫持</td>
			</tr>
			<tr>
					<td>从大量选项中随机抽取“正确答案 + 若干干扰项”</td>
					<td>选项数量单一，也解决了选项数超过字母数的问题</td>
			</tr>
	</tbody>
</table>
<p>要注意顺序：<strong>先切分，再增强</strong>，而且增强只作用于训练集，否则同一道题的不同变体会同时出现在训练集和评测集里，评测分数就虚高了。</p>
<p><img alt="数据流水线" loading="lazy" src="/zh/blog/2026/train-your-own-jev/4-data-pipeline.png"></p>
<h3 id="4-切分一定要按任务留出">4. 切分：一定要按“任务”留出</h3>
<p>决策模型最重要的能力是泛化到没见过的问题，所以评测集要分层次：</p>
<table>
	<thead>
			<tr>
					<th>评测集</th>
					<th>怎么来</th>
					<th>回答的问题</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>dev（开发集）</td>
					<td>训练任务中随机留出 5% 左右</td>
					<td>训练是否收敛，拟合温度</td>
			</tr>
			<tr>
					<td>held-out tasks（留出任务）</td>
					<td>整个任务都不参与训练</td>
					<td>零样本泛化能力，最接近 Jev 的使用方式</td>
			</tr>
			<tr>
					<td>公开基准</td>
					<td>JevBench 公开的 231 道题</td>
					<td>和其他模型同场比较</td>
			</tr>
			<tr>
					<td>lockbox（封存集）</td>
					<td>自己写的、从不参与任何调参的题</td>
					<td>最终上线前的一次性验收，防止对评测集过拟合</td>
			</tr>
	</tbody>
</table>
<p>真实项目里，每种题型下面都应该有多个任务，留出整个任务之后，训练集里仍然有同题型的其他任务。</p>
<p>下面是一个最小的数据准备脚本，把 Banking77（意图，choice）、BoolQ（是否题，noul）、SST-5（情感，score）和 OCNLI（中文推理，choice）四个公开数据集改写成决策题，完成去重、去污染和按任务切分：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># prepare_data.py</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Turn a few public datasets into decision records, remove duplicates and eval overlap, split by task.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> hashlib
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> random
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pathlib <span style="color:#f92672">import</span> Path
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> datasets <span style="color:#f92672">import</span> load_dataset
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>random<span style="color:#f92672">.</span>seed(<span style="color:#ae81ff">13</span>)
</span></span><span style="display:flex;"><span>SST5_LEVELS <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;very negative&#34;</span>, <span style="color:#e6db74">&#34;negative&#34;</span>, <span style="color:#e6db74">&#34;neutral&#34;</span>, <span style="color:#e6db74">&#34;positive&#34;</span>, <span style="color:#e6db74">&#34;very positive&#34;</span>]
</span></span><span style="display:flex;"><span>OCNLI <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;entailment&#34;</span>: <span style="color:#e6db74">&#34;前提为真时，假设一定为真&#34;</span>,
</span></span><span style="display:flex;"><span>         <span style="color:#e6db74">&#34;neutral&#34;</span>: <span style="color:#e6db74">&#34;前提为真时，假设可能为真也可能为假&#34;</span>,
</span></span><span style="display:flex;"><span>         <span style="color:#e6db74">&#34;contradiction&#34;</span>: <span style="color:#e6db74">&#34;前提为真时，假设一定为假&#34;</span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">banking77</span>():
</span></span><span style="display:flex;"><span>    ds <span style="color:#f92672">=</span> load_dataset(<span style="color:#e6db74">&#34;mteb/banking77&#34;</span>, split<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;train&#34;</span>)
</span></span><span style="display:flex;"><span>    intents <span style="color:#f92672">=</span> sorted(set(ds[<span style="color:#e6db74">&#34;label_text&#34;</span>]))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> ds:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># 77 intents do not fit 26 letters: show the gold intent among 9 random distractors</span>
</span></span><span style="display:flex;"><span>        shown <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>sample([i <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> intents <span style="color:#66d9ef">if</span> i <span style="color:#f92672">!=</span> row[<span style="color:#e6db74">&#34;label_text&#34;</span>]], <span style="color:#ae81ff">9</span>) <span style="color:#f92672">+</span> [row[<span style="color:#e6db74">&#34;label_text&#34;</span>]]
</span></span><span style="display:flex;"><span>        random<span style="color:#f92672">.</span>shuffle(shown)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">yield</span> {<span style="color:#e6db74">&#34;task&#34;</span>: <span style="color:#e6db74">&#34;banking77&#34;</span>, <span style="color:#e6db74">&#34;state&#34;</span>: row[<span style="color:#e6db74">&#34;text&#34;</span>],
</span></span><span style="display:flex;"><span>               <span style="color:#e6db74">&#34;question&#34;</span>: {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;choice&#34;</span>,
</span></span><span style="display:flex;"><span>                            <span style="color:#e6db74">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;Which banking intent does the customer&#39;s message express?&#34;</span>,
</span></span><span style="display:flex;"><span>                            <span style="color:#e6db74">&#34;criteria&#34;</span>: {i: i<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;_&#34;</span>, <span style="color:#e6db74">&#34; &#34;</span>) <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> shown}},
</span></span><span style="display:flex;"><span>               <span style="color:#e6db74">&#34;answer&#34;</span>: row[<span style="color:#e6db74">&#34;label_text&#34;</span>]}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">boolq</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> load_dataset(<span style="color:#e6db74">&#34;google/boolq&#34;</span>, split<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;train&#34;</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">yield</span> {<span style="color:#e6db74">&#34;task&#34;</span>: <span style="color:#e6db74">&#34;boolq&#34;</span>, <span style="color:#e6db74">&#34;state&#34;</span>: row[<span style="color:#e6db74">&#34;passage&#34;</span>],
</span></span><span style="display:flex;"><span>               <span style="color:#e6db74">&#34;question&#34;</span>: {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;noul&#34;</span>, <span style="color:#e6db74">&#34;instructions&#34;</span>: row[<span style="color:#e6db74">&#34;question&#34;</span>]<span style="color:#f92672">.</span>capitalize() <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;?&#34;</span>,
</span></span><span style="display:flex;"><span>                            <span style="color:#e6db74">&#34;criteria&#34;</span>: {<span style="color:#e6db74">&#34;true&#34;</span>: <span style="color:#e6db74">&#34;yes, according to the passage&#34;</span>,
</span></span><span style="display:flex;"><span>                                         <span style="color:#e6db74">&#34;false&#34;</span>: <span style="color:#e6db74">&#34;no, according to the passage&#34;</span>}},
</span></span><span style="display:flex;"><span>               <span style="color:#e6db74">&#34;answer&#34;</span>: row[<span style="color:#e6db74">&#34;answer&#34;</span>]}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">sst5</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> load_dataset(<span style="color:#e6db74">&#34;SetFit/sst5&#34;</span>, split<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;train&#34;</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">yield</span> {<span style="color:#e6db74">&#34;task&#34;</span>: <span style="color:#e6db74">&#34;sst5&#34;</span>, <span style="color:#e6db74">&#34;state&#34;</span>: row[<span style="color:#e6db74">&#34;text&#34;</span>],
</span></span><span style="display:flex;"><span>               <span style="color:#e6db74">&#34;question&#34;</span>: {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;score&#34;</span>, <span style="color:#e6db74">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;How positive is this movie review?&#34;</span>,
</span></span><span style="display:flex;"><span>                            <span style="color:#e6db74">&#34;criteria&#34;</span>: SST5_LEVELS},
</span></span><span style="display:flex;"><span>               <span style="color:#e6db74">&#34;answer&#34;</span>: row[<span style="color:#e6db74">&#34;label&#34;</span>]}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ocnli</span>():
</span></span><span style="display:flex;"><span>    ds <span style="color:#f92672">=</span> load_dataset(<span style="color:#e6db74">&#34;clue/clue&#34;</span>, <span style="color:#e6db74">&#34;ocnli&#34;</span>, split<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;train&#34;</span>)
</span></span><span style="display:flex;"><span>    names <span style="color:#f92672">=</span> ds<span style="color:#f92672">.</span>features[<span style="color:#e6db74">&#34;label&#34;</span>]<span style="color:#f92672">.</span>names
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> ds:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> row[<span style="color:#e6db74">&#34;label&#34;</span>] <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">0</span>:  <span style="color:#75715e"># -1 marks unlabeled rows</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">yield</span> {<span style="color:#e6db74">&#34;task&#34;</span>: <span style="color:#e6db74">&#34;ocnli&#34;</span>, <span style="color:#e6db74">&#34;state&#34;</span>: {<span style="color:#e6db74">&#34;前提&#34;</span>: row[<span style="color:#e6db74">&#34;sentence1&#34;</span>], <span style="color:#e6db74">&#34;假设&#34;</span>: row[<span style="color:#e6db74">&#34;sentence2&#34;</span>]},
</span></span><span style="display:flex;"><span>                   <span style="color:#e6db74">&#34;question&#34;</span>: {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;choice&#34;</span>, <span style="color:#e6db74">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;假设和前提是什么关系？&#34;</span>, <span style="color:#e6db74">&#34;criteria&#34;</span>: OCNLI},
</span></span><span style="display:flex;"><span>                   <span style="color:#e6db74">&#34;answer&#34;</span>: names[row[<span style="color:#e6db74">&#34;label&#34;</span>]]}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">key</span>(record):
</span></span><span style="display:flex;"><span>    text <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>dumps([record[<span style="color:#e6db74">&#34;state&#34;</span>], record[<span style="color:#e6db74">&#34;question&#34;</span>][<span style="color:#e6db74">&#34;instructions&#34;</span>]], ensure_ascii<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> hashlib<span style="color:#f92672">.</span>sha1(<span style="color:#e6db74">&#34; &#34;</span><span style="color:#f92672">.</span>join(text<span style="color:#f92672">.</span>lower()<span style="color:#f92672">.</span>split())<span style="color:#f92672">.</span>encode())<span style="color:#f92672">.</span>hexdigest()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">write</span>(path, rows):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> open(path, <span style="color:#e6db74">&#34;w&#34;</span>, encoding<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;utf-8&#34;</span>) <span style="color:#66d9ef">as</span> f:
</span></span><span style="display:flex;"><span>        f<span style="color:#f92672">.</span>writelines(json<span style="color:#f92672">.</span>dumps(r, ensure_ascii<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span> <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> rows)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
</span></span><span style="display:flex;"><span>    held_out_tasks <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;banking77&#34;</span>}  <span style="color:#75715e"># whole tasks never trained on (choice is still covered by ocnli)</span>
</span></span><span style="display:flex;"><span>    banned <span style="color:#f92672">=</span> {key(json<span style="color:#f92672">.</span>loads(line)) <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> Path(<span style="color:#e6db74">&#34;eval&#34;</span>)<span style="color:#f92672">.</span>glob(<span style="color:#e6db74">&#34;*.jsonl&#34;</span>)
</span></span><span style="display:flex;"><span>              <span style="color:#66d9ef">for</span> line <span style="color:#f92672">in</span> open(p, encoding<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;utf-8&#34;</span>)}  <span style="color:#75715e"># JevBench public files, your lockbox, ...</span>
</span></span><span style="display:flex;"><span>    seen, train, dev, held_out <span style="color:#f92672">=</span> set(), [], [], []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> source <span style="color:#f92672">in</span> (banking77, boolq, sst5, ocnli):
</span></span><span style="display:flex;"><span>        rows <span style="color:#f92672">=</span> list(source())
</span></span><span style="display:flex;"><span>        random<span style="color:#f92672">.</span>shuffle(rows)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> rows[:<span style="color:#ae81ff">20000</span>]:  <span style="color:#75715e"># cap every task so no single dataset dominates the mix</span>
</span></span><span style="display:flex;"><span>            k <span style="color:#f92672">=</span> key(r)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> k <span style="color:#f92672">in</span> seen <span style="color:#f92672">or</span> k <span style="color:#f92672">in</span> banned:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>            seen<span style="color:#f92672">.</span>add(k)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> r[<span style="color:#e6db74">&#34;task&#34;</span>] <span style="color:#f92672">in</span> held_out_tasks:
</span></span><span style="display:flex;"><span>                held_out<span style="color:#f92672">.</span>append(r)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">elif</span> random<span style="color:#f92672">.</span>random() <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0.05</span>:
</span></span><span style="display:flex;"><span>                dev<span style="color:#f92672">.</span>append(r)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                train<span style="color:#f92672">.</span>append(r)
</span></span><span style="display:flex;"><span>    random<span style="color:#f92672">.</span>shuffle(train)
</span></span><span style="display:flex;"><span>    Path(<span style="color:#e6db74">&#34;data&#34;</span>)<span style="color:#f92672">.</span>mkdir(exist_ok<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    write(<span style="color:#e6db74">&#34;data/train.jsonl&#34;</span>, train)
</span></span><span style="display:flex;"><span>    write(<span style="color:#e6db74">&#34;data/dev.jsonl&#34;</span>, dev)
</span></span><span style="display:flex;"><span>    write(<span style="color:#e6db74">&#34;data/heldout_tasks.jsonl&#34;</span>, held_out)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;train </span><span style="color:#e6db74">{</span>len(train)<span style="color:#e6db74">}</span><span style="color:#e6db74">  dev </span><span style="color:#e6db74">{</span>len(dev)<span style="color:#e6db74">}</span><span style="color:#e6db74">  held-out tasks </span><span style="color:#e6db74">{</span>len(held_out)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    main()
</span></span></code></pre></div><p>把 JevBench 的公开题文件和自己的封存集放进 <code>eval/</code> 目录后运行，就会得到 <code>data/</code> 下的三个文件。我实际运行的结果是：训练集 36,044 条，开发集 1,917 条，留出任务（Banking77）9,993 条。这只是一个演示，真正的项目需要几十到几百个任务，但流程完全一样：<strong>每加一个数据源，就是再写一个这样的转换函数。</strong></p>
<h2 id="三gpu-资源到底需要多少卡">三、GPU 资源：到底需要多少卡</h2>
<h3 id="1-显存怎么估">1. 显存怎么估</h3>
<p>训练时的显存主要由三部分组成：模型权重、梯度和优化器状态、激活值。前两部分可以按“每个参数占多少字节”来估：</p>
<table>
	<thead>
			<tr>
					<th>训练方式</th>
					<th>每个参数约占</th>
					<th>构成</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>全参微调</td>
					<td>16 字节</td>
					<td>bf16 权重 2 + bf16 梯度 2 + fp32 主权重 4 + AdamW 两个动量 8</td>
			</tr>
			<tr>
					<td>LoRA</td>
					<td>2 字节</td>
					<td>冻结的 bf16 权重；适配器及其优化器状态通常不到 1 GB</td>
			</tr>
			<tr>
					<td>QLoRA</td>
					<td>0.5～0.6 字节</td>
					<td>冻结的 4-bit 权重</td>
			</tr>
	</tbody>
</table>
<p>按这个规则，不含激活值的显存大约是：</p>
<table>
	<thead>
			<tr>
					<th>底座</th>
					<th>全参微调</th>
					<th>LoRA</th>
					<th>QLoRA</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Qwen3.5-0.8B</td>
					<td>约 13 GB</td>
					<td>约 1.6 GB</td>
					<td>约 0.5 GB</td>
			</tr>
			<tr>
					<td>Qwen3.5-2B</td>
					<td>约 32 GB</td>
					<td>约 4 GB</td>
					<td>约 1.1 GB</td>
			</tr>
			<tr>
					<td>Qwen3.5-4B</td>
					<td>约 64 GB</td>
					<td>约 8 GB</td>
					<td>约 2.2 GB</td>
			</tr>
			<tr>
					<td>Qwen3.5-9B</td>
					<td>约 144 GB</td>
					<td>约 18 GB</td>
					<td>约 5 GB</td>
			</tr>
			<tr>
					<td>Gemma-4-31B</td>
					<td>约 500 GB</td>
					<td>约 62 GB</td>
					<td>约 17 GB</td>
			</tr>
	</tbody>
</table>
<p>激活值和 batch 大小、序列长度成正比。开启梯度检查点后，主要剩下每一层边界的隐状态：以 Qwen3.5-4B（32 层，隐藏维度 2560）为例，一条 4K token 的样本约 <code>32 × 4096 × 2560 × 2 字节 ≈ 0.7 GB</code>，batch 为 4 时约 3 GB，再加上单层重算的峰值和 CUDA 自身的开销。所以 <strong>4B 模型做 LoRA，单张 24 GB 的卡通常就能跑</strong>（序列在 4K 以内、batch 不大的情况下），80 GB 的卡则可以放开 batch 和序列长度。</p>
<p>这里有一个决策模型特有的大坑：<strong>语言模型头的 logits</strong>。Qwen3.5 的词表有 248,320 个 token，如果像普通 SFT 那样对每个位置都算 logits，一条 4K token 的样本光 fp32 的 logits 就要 <code>4096 × 248320 × 4 字节 ≈ 4 GB</code>，反向传播时还要再来一份。而决策模型只需要最后一个位置的 logits，前向时传入 <code>logits_to_keep=1</code>，这部分就只剩 1 MB 左右。后面的训练脚本就是这么做的。</p>
<h3 id="2-时间怎么估">2. 时间怎么估</h3>
<p>训练的计算量可以用一个经验公式估算：每个训练 token 约 <code>6 × 参数量</code> 次浮点运算（LoRA 加梯度检查点：前向 2N、重算 2N、反向 2N；全参微调加梯度检查点约为 8N）。举个例子，用 Qwen3.5-4B 做 LoRA，20 万道题、平均每题 600 token、训练 2 个 epoch：</p>
<pre tabindex="0"><code>训练 token 数  ≈ 200,000 × 600 × 2 = 2.4e8
训练计算量    ≈ 6 × 4e9 × 2.4e8 ≈ 5.8e18 FLOPs
H100 实际吞吐 ≈ 989 TFLOPS（bf16 稠密峰值）× 35% 利用率 ≈ 350 TFLOPS
训练时间      ≈ 5.8e18 / 3.5e14 ≈ 16,500 秒 ≈ 4.6 小时
</code></pre><p>同样的任务在 A100 上大约要 3 倍时间。实际利用率和序列长度、padding 比例、算子实现都有关系，要以自己的 profiler 数据为准。Qwen3.5 是“Gated DeltaNet 线性注意力 + 门控全注意力”的混合架构，训练前一定要装上 <code>flash-linear-attention</code> 和 <code>causal-conv1d</code>，否则会退回到纯 PyTorch 实现，慢很多（加载模型时会打印提示）。</p>
<h3 id="3-推荐配置">3. 推荐配置</h3>
<p><img alt="从编码器到全参微调的硬件阶梯" loading="lazy" src="/zh/blog/2026/train-your-own-jev/5-gpu-ladder.png"></p>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>底座与训练方式</th>
					<th>推荐硬件</th>
					<th>说明</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>编码器路线</td>
					<td>ModernBERT-large / mmBERT + 决策头</td>
					<td>2 张 T4（Kaggle 免费额度）或任意 16 GB 显卡</td>
					<td>Laya 的官方 Notebook：约 3 万道题、4 个 epoch，耗时 4～5 小时</td>
			</tr>
			<tr>
					<td>快速验证</td>
					<td>Qwen3.5-0.8B / 2B + LoRA</td>
					<td>单张 24 GB（RTX 4090、L4、A10）</td>
					<td>跑通流程、做数据实验</td>
			</tr>
			<tr>
					<td>主力方案</td>
					<td>Qwen3.5-4B + LoRA</td>
					<td>1 张 H100 / A100 80 GB</td>
					<td>性价比最高的档位，短序列时 24 GB 也能跑</td>
			</tr>
			<tr>
					<td>进阶</td>
					<td>Qwen3.5-9B + LoRA</td>
					<td>1～2 张 H100</td>
					<td>长上下文训练时用 2 张</td>
			</tr>
			<tr>
					<td>全参微调</td>
					<td>4B～9B 全参</td>
					<td>8 张 H100（FSDP 或 ZeRO-3）</td>
					<td>数据量很大、追求极致效果时</td>
			</tr>
	</tbody>
</table>
<p>成本方面，按 H100 按需价格每小时 2～3 美元估算（JevBench 的 GPU 成本计算器默认按 H100 PCIe 每小时 2.89 美元），一次 4B 的 LoRA 训练大约 15 美元。真正花钱的是迭代：一个项目通常要跑几十轮“改数据、重训、评测”，GPU 总花费在几百美元量级。<strong>算力不是最贵的，数据和评测才是。</strong></p>
<p>部署的成本更低：H2O-Lightning-4B 的 bf16 权重约 9.1 GB，在一张 32 GB 的 RTX PRO 4500 上用 vLLM 服务，标准题单次判断的 p50 约 30 ms；按 JevBench 计算器的估算，在 RTX 5090 上自托管，每千次决策约 0.008 美元，大约是 Jev 在 JevBench 上实测成本（每千次 0.032 美元）的四分之一。</p>
<h2 id="四基础模型怎么选">四、基础模型怎么选</h2>
<h3 id="1-选择标准">1. 选择标准</h3>
<ol>
<li><strong>零样本底子</strong>：优先选指令版（经过对齐的对话模型），它们本来就会做选择题，起点高；Base 版需要更多数据才能追上。JevBench 专门放了几个“底座对照组”（不做任何决策微调，直接读 logits），能直接看出不同底座的起点。</li>
<li><strong>语言</strong>：中文场景优先考虑 Qwen；注意 H2O-Lightning-4B 的训练数据 99% 以上是英文，直接拿来处理中文要先自测。</li>
<li><strong>尺寸与延迟</strong>：决策模型的定位是几十毫秒级的快速判断，4B 左右是目前延迟、成本和能力的甜点。</li>
<li><strong>上下文长度</strong>：JevBench 里有 23 道 7.7 万～8.1 万 token 的超长题，Jev 和不少模型因为超出上下文而拒答，按答错计分。Qwen3.5 原生支持 262K，但序列越长，训练的显存和时间成本上涨得越快。</li>
<li><strong>许可证</strong>：Qwen3.5 全系列、Gemma 4、ModernBERT 都是 Apache 2.0，mmBERT 是 MIT，商用比较省心。</li>
<li><strong>生态</strong>：训练要有 Transformers 和 PEFT 支持，部署要有 vLLM、SGLang、llama.cpp 支持；另外，选项字母必须各自是一个独立的 token（训练脚本里有断言检查）。</li>
</ol>
<h3 id="2-候选清单">2. 候选清单</h3>
<table>
	<thead>
			<tr>
					<th>底座</th>
					<th>规模</th>
					<th>许可证</th>
					<th>适合</th>
					<th>社区代表作</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Qwen3.5-0.8B / 2B</td>
					<td>0.8B / 2B</td>
					<td>Apache 2.0</td>
					<td>端侧、超低延迟、流程验证</td>
					<td>jiwo 0.8b、ClassOne Qwen 3.5 2B</td>
			</tr>
			<tr>
					<td>Qwen3.5-4B</td>
					<td>4B</td>
					<td>Apache 2.0</td>
					<td>主力推荐，中文友好</td>
					<td>H2O-Lightning-4B、Kahn1 4B</td>
			</tr>
			<tr>
					<td>Qwen3.5-9B</td>
					<td>9B</td>
					<td>Apache 2.0</td>
					<td>更强的零样本能力</td>
					<td>Microsoft-Decision-1、ClassOne Qwen 3.5 9B</td>
			</tr>
			<tr>
					<td>Gemma-4-12B-it</td>
					<td>12B</td>
					<td>Apache 2.0</td>
					<td>多语言，能力与成本平衡</td>
					<td>decider-12b、torchcast-decision-12b</td>
			</tr>
			<tr>
					<td>Gemma-4-31B-it / Qwen3.5-27B</td>
					<td>27B～31B</td>
					<td>Apache 2.0</td>
					<td>冲击榜单头部</td>
					<td>Quyet-1.0-Large、deck-31B</td>
			</tr>
			<tr>
					<td>ModernBERT-large / mmBERT</td>
					<td>0.3B～0.4B</td>
					<td>Apache 2.0 / MIT</td>
					<td>编码器路线，单一任务极速推理</td>
					<td>Laya</td>
			</tr>
	</tbody>
</table>
<h3 id="3-我的建议">3. 我的建议</h3>
<ul>
<li><strong>起步用 Qwen3.5-4B（指令版）+ LoRA</strong>：中文能力好，社区已经证明它能做到接近 Jev 的水平，单卡就能训练和部署；</li>
<li><strong>先在 0.8B / 2B 上把流程跑通</strong>：数据格式、评测脚本、阈值策略都在小模型上验证完，再换大底座，可以省掉大量试错成本；</li>
<li><strong>想冲击更高的能力分，再上 9B 到 31B</strong>：收益是真实的（Quyet-1.0-Large 的智能分比 H2O-Lightning-4B 高 13 分以上），但训练和推理成本也会成倍增加；</li>
<li><strong>使用 Qwen3.5 时关闭思考模式</strong>：它默认会先输出思考过程，构造提示词时要传 <code>enable_thinking=False</code>，让助手回复的第一个 token 就是答案字母。</li>
</ul>
<h2 id="五训练脚本一步步写出来">五、训练脚本：一步步写出来</h2>
<p>这一节给出一套完整、可以直接运行的代码。它刻意保持精简，没有依赖任何专门的训练框架，方便读懂每一步在做什么。</p>
<h3 id="1-准备环境">1. 准备环境</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install torch transformers peft accelerate datasets fastapi uvicorn
</span></span><span style="display:flex;"><span><span style="color:#75715e"># GPU 机器上再装 Qwen3.5 线性注意力层的加速算子</span>
</span></span><span style="display:flex;"><span>pip install flash-linear-attention causal-conv1d
</span></span></code></pre></div><p>项目结构如下：</p>
<pre tabindex="0"><code>my-jev/
├── prepare_data.py      # 数据准备（上一节）
├── decision_format.py   # 提示词渲染与一次前向打分，训练、评测、服务共用
├── train.py             # LoRA 训练
├── evaluate.py          # 打分测试与温度拟合
├── serve.py             # /v1/systemone 服务
├── eval/                # JevBench 公开题、自己的封存集
└── data/                # prepare_data.py 的输出
</code></pre><h3 id="2-共用模块把一道题变成一次前向">2. 共用模块：把一道题变成一次前向</h3>
<p>训练、评测和线上服务必须用<strong>完全相同</strong>的提示词格式，否则训练出来的概率到了线上就不再准确。所以先把这部分抽成一个共用模块：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># decision_format.py</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Shared by training, evaluation and serving: render a decision as a prompt and read option probabilities.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> string
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> torch
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> transformers <span style="color:#f92672">import</span> AutoModelForCausalLM, AutoTokenizer
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>LETTERS <span style="color:#f92672">=</span> string<span style="color:#f92672">.</span>ascii_uppercase  <span style="color:#75715e"># up to 26 options per question</span>
</span></span><span style="display:flex;"><span>SYSTEM <span style="color:#f92672">=</span> (<span style="color:#e6db74">&#34;You are a decision model. Read the state and the question, &#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#34;then answer with the letter of exactly one option.&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">load</span>(base, dtype<span style="color:#f92672">=</span>torch<span style="color:#f92672">.</span>bfloat16):
</span></span><span style="display:flex;"><span>    tok <span style="color:#f92672">=</span> AutoTokenizer<span style="color:#f92672">.</span>from_pretrained(base)
</span></span><span style="display:flex;"><span>    tok<span style="color:#f92672">.</span>padding_side <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;left&#34;</span>  <span style="color:#75715e"># every row then ends at its answer slot</span>
</span></span><span style="display:flex;"><span>    model <span style="color:#f92672">=</span> AutoModelForCausalLM<span style="color:#f92672">.</span>from_pretrained(base, dtype<span style="color:#f92672">=</span>dtype)  <span style="color:#75715e"># Qwen3.5 loads text-only here</span>
</span></span><span style="display:flex;"><span>    ids <span style="color:#f92672">=</span> [tok<span style="color:#f92672">.</span>encode(c, add_special_tokens<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>) <span style="color:#66d9ef">for</span> c <span style="color:#f92672">in</span> LETTERS]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">assert</span> all(len(i) <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> ids), <span style="color:#e6db74">&#34;every option letter must be a single token&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> tok, model, torch<span style="color:#f92672">.</span>tensor([i[<span style="color:#ae81ff">0</span>] <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> ids])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">options</span>(question):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;[(key, description)] in the caller&#39;s order; noul is shown as yes / no.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    kind, crit <span style="color:#f92672">=</span> question[<span style="color:#e6db74">&#34;type&#34;</span>], question<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;criteria&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> kind <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;noul&#34;</span>:
</span></span><span style="display:flex;"><span>        crit <span style="color:#f92672">=</span> crit <span style="color:#f92672">or</span> {}
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> [(<span style="color:#e6db74">&#34;yes&#34;</span>, crit<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;true&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>)), (<span style="color:#e6db74">&#34;no&#34;</span>, crit<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;false&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>))]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> kind <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;score&#34;</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> [(str(i), desc) <span style="color:#66d9ef">for</span> i, desc <span style="color:#f92672">in</span> enumerate(crit)]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> list(crit<span style="color:#f92672">.</span>items()) <span style="color:#66d9ef">if</span> isinstance(crit, dict) <span style="color:#66d9ef">else</span> [(c, <span style="color:#e6db74">&#34;&#34;</span>) <span style="color:#66d9ef">for</span> c <span style="color:#f92672">in</span> crit]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">target</span>(record, opts):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Target distribution over opts: a soft label if the record has one, else one-hot.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    keys <span style="color:#f92672">=</span> [k <span style="color:#66d9ef">for</span> k, _ <span style="color:#f92672">in</span> opts]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;target&#34;</span> <span style="color:#f92672">in</span> record:  <span style="color:#75715e"># e.g. {&#34;billing&#34;: 0.7, &#34;technical&#34;: 0.3}</span>
</span></span><span style="display:flex;"><span>        dist <span style="color:#f92672">=</span> [float(record[<span style="color:#e6db74">&#34;target&#34;</span>]<span style="color:#f92672">.</span>get(k, <span style="color:#ae81ff">0.0</span>)) <span style="color:#66d9ef">for</span> k <span style="color:#f92672">in</span> keys]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        ans <span style="color:#f92672">=</span> record<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;answer&#34;</span>, record<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;expected&#34;</span>))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> record[<span style="color:#e6db74">&#34;question&#34;</span>][<span style="color:#e6db74">&#34;type&#34;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;noul&#34;</span>:
</span></span><span style="display:flex;"><span>            ans <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;yes&#34;</span> <span style="color:#66d9ef">if</span> ans <span style="color:#f92672">in</span> (<span style="color:#66d9ef">True</span>, <span style="color:#e6db74">&#34;yes&#34;</span>, <span style="color:#e6db74">&#34;true&#34;</span>) <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;no&#34;</span>
</span></span><span style="display:flex;"><span>        dist <span style="color:#f92672">=</span> [<span style="color:#ae81ff">1.0</span> <span style="color:#66d9ef">if</span> k <span style="color:#f92672">==</span> str(ans) <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">0.0</span> <span style="color:#66d9ef">for</span> k <span style="color:#f92672">in</span> keys]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> sum(dist) <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;answer is not one of the options: </span><span style="color:#e6db74">{</span>record<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;id&#39;</span>, record[<span style="color:#e6db74">&#39;question&#39;</span>])<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> [d <span style="color:#f92672">/</span> sum(dist) <span style="color:#66d9ef">for</span> d <span style="color:#f92672">in</span> dist]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">prompt</span>(tok, record, order, max_state_tokens<span style="color:#f92672">=</span><span style="color:#ae81ff">6000</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Chat prompt whose next token is the answer letter; slot s shows option order[s].&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    q, opts, state <span style="color:#f92672">=</span> record[<span style="color:#e6db74">&#34;question&#34;</span>], options(record[<span style="color:#e6db74">&#34;question&#34;</span>]), record[<span style="color:#e6db74">&#34;state&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> isinstance(state, str):
</span></span><span style="display:flex;"><span>        state <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>dumps(state, ensure_ascii<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>, indent<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    ids <span style="color:#f92672">=</span> tok<span style="color:#f92672">.</span>encode(state, add_special_tokens<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> len(ids) <span style="color:#f92672">&gt;</span> max_state_tokens:  <span style="color:#75715e"># cut the state, never the options or the answer slot</span>
</span></span><span style="display:flex;"><span>        state <span style="color:#f92672">=</span> tok<span style="color:#f92672">.</span>decode(ids[:max_state_tokens]) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">[truncated]&#34;</span>
</span></span><span style="display:flex;"><span>    lines <span style="color:#f92672">=</span> [<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>LETTERS[s]<span style="color:#e6db74">}</span><span style="color:#e6db74">. </span><span style="color:#e6db74">{</span>opts[i][<span style="color:#ae81ff">0</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">+</span> (<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;: </span><span style="color:#e6db74">{</span>opts[i][<span style="color:#ae81ff">1</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#66d9ef">if</span> opts[i][<span style="color:#ae81ff">1</span>] <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span>             <span style="color:#66d9ef">for</span> s, i <span style="color:#f92672">in</span> enumerate(order)]
</span></span><span style="display:flex;"><span>    user <span style="color:#f92672">=</span> (<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;&lt;state&gt;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">{</span>state<span style="color:#e6db74">}</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&lt;/state&gt;</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">Question (</span><span style="color:#e6db74">{</span>q[<span style="color:#e6db74">&#39;type&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">): </span><span style="color:#e6db74">{</span>q[<span style="color:#e6db74">&#39;instructions&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;Options:</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span><span style="color:#f92672">.</span>join(lines) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">Answer with the letter of one option.&#34;</span>)
</span></span><span style="display:flex;"><span>    messages <span style="color:#f92672">=</span> [{<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;system&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: SYSTEM}, {<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: user}]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> tok<span style="color:#f92672">.</span>apply_chat_template(messages, tokenize<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>, add_generation_prompt<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>                                   enable_thinking<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">option_logits</span>(model, tok, letter_ids, prompts, ks, device):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;One forward pass for a batch: logits of the shown option letters at the answer slot.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    enc <span style="color:#f92672">=</span> tok(prompts, return_tensors<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pt&#34;</span>, padding<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>, add_special_tokens<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)<span style="color:#f92672">.</span>to(device)
</span></span><span style="display:flex;"><span>    out <span style="color:#f92672">=</span> model(input_ids<span style="color:#f92672">=</span>enc<span style="color:#f92672">.</span>input_ids, attention_mask<span style="color:#f92672">=</span>enc<span style="color:#f92672">.</span>attention_mask, logits_to_keep<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    z <span style="color:#f92672">=</span> out<span style="color:#f92672">.</span>logits[:, <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, letter_ids[: max(ks)]<span style="color:#f92672">.</span>to(device)]<span style="color:#f92672">.</span>float()
</span></span><span style="display:flex;"><span>    mask <span style="color:#f92672">=</span> torch<span style="color:#f92672">.</span>arange(z<span style="color:#f92672">.</span>shape[<span style="color:#ae81ff">1</span>], device<span style="color:#f92672">=</span>device)[<span style="color:#66d9ef">None</span>, :] <span style="color:#f92672">&lt;</span> torch<span style="color:#f92672">.</span>tensor(ks, device<span style="color:#f92672">=</span>device)[:, <span style="color:#66d9ef">None</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> z<span style="color:#f92672">.</span>masked_fill(<span style="color:#f92672">~</span>mask, <span style="color:#f92672">-</span><span style="color:#ae81ff">1e4</span>), mask
</span></span></code></pre></div><p>几个关键点：</p>
<ul>
<li><strong>左侧 padding</strong>：一个 batch 里的提示词长短不一，左侧补齐之后，每一行的最后一个位置都是答案位，配合 <code>logits_to_keep=1</code>，语言模型头只需要算一个位置。Qwen3.5 有线性注意力层，我专门验证过：同一道题单独计算和放进左侧补齐的 batch 里计算，logits 的最大差异只有 6×10⁻⁵；</li>
<li><strong>只加载文本部分</strong>：Qwen3.5 是原生多模态模型，但用 <code>AutoModelForCausalLM</code> 加载时得到的是纯文本的 <code>Qwen3_5ForCausalLM</code>，不会带上视觉编码器，省显存，LoRA 也不会挂到视觉层上；</li>
<li><strong>只在 K 个字母上做 softmax</strong>：取出 A、B、C……对应的 logits，多余的槽位填一个很小的值，模型的输出永远落在合法选项里；</li>
<li><strong>state 过长时只截断 state</strong>：问题、选项和答案位必须完整保留；</li>
<li><strong>noul 统一渲染成“yes / no”两个选项</strong>，返回的 P(yes) 就是 noul 的答案；score 的各个等级按顺序排列，便于计算 RPS；</li>
<li><strong>断言每个字母都是单独的 token</strong>：换其他底座时，这一行能第一时间发现问题。在 Qwen3.5 的分词器上，A 到 Z 都是单个 token，而且在答案位后面追加一个字母，恰好多出一个 token。</li>
</ul>
<h3 id="3-训练主脚本">3. 训练主脚本</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># train.py</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;LoRA fine-tuning of a decision model with strictly proper scoring rules, or Laya-style RLCD.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> argparse
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> math
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> random
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> torch
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> accelerate <span style="color:#f92672">import</span> Accelerator
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> peft <span style="color:#f92672">import</span> LoraConfig, get_peft_model
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> torch.utils.data <span style="color:#f92672">import</span> DataLoader
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> transformers <span style="color:#f92672">import</span> get_cosine_schedule_with_warmup
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> decision_format <span style="color:#f92672">import</span> load, option_logits, options, prompt, target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">reward</span>(z, mask, y, ordinal):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Laya&#39;s composite of strictly proper scores: log + 0.5 * spherical - RPS (RPS for score only).&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    logq <span style="color:#f92672">=</span> z<span style="color:#f92672">.</span>log_softmax(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    q <span style="color:#f92672">=</span> logq<span style="color:#f92672">.</span>exp()
</span></span><span style="display:flex;"><span>    s_log <span style="color:#f92672">=</span> (y <span style="color:#f92672">*</span> logq)<span style="color:#f92672">.</span>sum(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)                      <span style="color:#75715e"># log score = minus cross-entropy</span>
</span></span><span style="display:flex;"><span>    s_sph <span style="color:#f92672">=</span> (y <span style="color:#f92672">*</span> q)<span style="color:#f92672">.</span>sum(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) <span style="color:#f92672">/</span> q<span style="color:#f92672">.</span>norm(dim<span style="color:#f92672">=-</span><span style="color:#ae81ff">1</span>)        <span style="color:#75715e"># spherical score, bounded in [0, 1]</span>
</span></span><span style="display:flex;"><span>    k <span style="color:#f92672">=</span> mask<span style="color:#f92672">.</span>sum(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)<span style="color:#f92672">.</span>clamp(min<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>    rps <span style="color:#f92672">=</span> ((q<span style="color:#f92672">.</span>cumsum(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) <span style="color:#f92672">-</span> y<span style="color:#f92672">.</span>cumsum(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)) <span style="color:#f92672">**</span> <span style="color:#ae81ff">2</span>)<span style="color:#f92672">.</span>sum(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) <span style="color:#f92672">/</span> (k <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>)  <span style="color:#75715e"># ranked probability score</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> s_log <span style="color:#f92672">+</span> <span style="color:#ae81ff">0.5</span> <span style="color:#f92672">*</span> s_sph <span style="color:#f92672">-</span> ordinal <span style="color:#f92672">*</span> rps
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">rlcd_loss</span>(z, mask, y, ordinal, sigma, group<span style="color:#f92672">=</span><span style="color:#ae81ff">8</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Policy gradient over Gaussian-perturbed logits with a group-mean baseline, as Laya describes RLCD.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    m <span style="color:#f92672">=</span> mask<span style="color:#f92672">.</span>float()
</span></span><span style="display:flex;"><span>    eps <span style="color:#f92672">=</span> torch<span style="color:#f92672">.</span>randn(group, <span style="color:#f92672">*</span>z<span style="color:#f92672">.</span>shape, device<span style="color:#f92672">=</span>z<span style="color:#f92672">.</span>device) <span style="color:#f92672">*</span> m
</span></span><span style="display:flex;"><span>    eps <span style="color:#f92672">=</span> (eps <span style="color:#f92672">-</span> eps<span style="color:#f92672">.</span>sum(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, keepdim<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>) <span style="color:#f92672">/</span> m<span style="color:#f92672">.</span>sum(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, keepdim<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)) <span style="color:#f92672">*</span> m  <span style="color:#75715e"># softmax ignores a common shift</span>
</span></span><span style="display:flex;"><span>    sample <span style="color:#f92672">=</span> z<span style="color:#f92672">.</span>detach() <span style="color:#f92672">+</span> eps
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> torch<span style="color:#f92672">.</span>no_grad():
</span></span><span style="display:flex;"><span>        r <span style="color:#f92672">=</span> reward(sample, mask, y, ordinal)            <span style="color:#75715e"># [group, batch]</span>
</span></span><span style="display:flex;"><span>        adv <span style="color:#f92672">=</span> (r <span style="color:#f92672">-</span> r<span style="color:#f92672">.</span>mean(<span style="color:#ae81ff">0</span>)) <span style="color:#f92672">/</span> (r<span style="color:#f92672">.</span>std(<span style="color:#ae81ff">0</span>) <span style="color:#f92672">+</span> <span style="color:#ae81ff">1e-6</span>)
</span></span><span style="display:flex;"><span>    logp <span style="color:#f92672">=</span> <span style="color:#f92672">-</span>(((sample <span style="color:#f92672">-</span> z) <span style="color:#f92672">**</span> <span style="color:#ae81ff">2</span>) <span style="color:#f92672">*</span> m)<span style="color:#f92672">.</span>sum(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) <span style="color:#f92672">/</span> (<span style="color:#ae81ff">2</span> <span style="color:#f92672">*</span> sigma<span style="color:#f92672">**</span><span style="color:#ae81ff">2</span>)  <span style="color:#75715e"># Gaussian log-density up to a constant</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#f92672">-</span>(adv <span style="color:#f92672">*</span> logp)<span style="color:#f92672">.</span>mean()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Collate</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">__init__</span>(self, tok, max_state_tokens):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>tok, self<span style="color:#f92672">.</span>max_state_tokens <span style="color:#f92672">=</span> tok, max_state_tokens
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">__call__</span>(self, records):
</span></span><span style="display:flex;"><span>        prompts, ks, ys, ordinal <span style="color:#f92672">=</span> [], [], [], []
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> records:
</span></span><span style="display:flex;"><span>            opts <span style="color:#f92672">=</span> options(r[<span style="color:#e6db74">&#34;question&#34;</span>])
</span></span><span style="display:flex;"><span>            order <span style="color:#f92672">=</span> list(range(len(opts)))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> r[<span style="color:#e6db74">&#34;question&#34;</span>][<span style="color:#e6db74">&#34;type&#34;</span>] <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;score&#34;</span>:  <span style="color:#75715e"># score levels keep their order, RPS relies on it</span>
</span></span><span style="display:flex;"><span>                random<span style="color:#f92672">.</span>shuffle(order)
</span></span><span style="display:flex;"><span>            y <span style="color:#f92672">=</span> target(r, opts)
</span></span><span style="display:flex;"><span>            prompts<span style="color:#f92672">.</span>append(prompt(self<span style="color:#f92672">.</span>tok, r, order, self<span style="color:#f92672">.</span>max_state_tokens))
</span></span><span style="display:flex;"><span>            ys<span style="color:#f92672">.</span>append([y[i] <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> order])
</span></span><span style="display:flex;"><span>            ks<span style="color:#f92672">.</span>append(len(opts))
</span></span><span style="display:flex;"><span>            ordinal<span style="color:#f92672">.</span>append(float(r[<span style="color:#e6db74">&#34;question&#34;</span>][<span style="color:#e6db74">&#34;type&#34;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;score&#34;</span>))
</span></span><span style="display:flex;"><span>        y <span style="color:#f92672">=</span> torch<span style="color:#f92672">.</span>zeros(len(records), max(ks))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> row, t <span style="color:#f92672">in</span> enumerate(ys):
</span></span><span style="display:flex;"><span>            y[row, : len(t)] <span style="color:#f92672">=</span> torch<span style="color:#f92672">.</span>tensor(t)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;prompts&#34;</span>: prompts, <span style="color:#e6db74">&#34;ks&#34;</span>: ks, <span style="color:#e6db74">&#34;y&#34;</span>: y, <span style="color:#e6db74">&#34;ordinal&#34;</span>: torch<span style="color:#f92672">.</span>tensor(ordinal)}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
</span></span><span style="display:flex;"><span>    ap <span style="color:#f92672">=</span> argparse<span style="color:#f92672">.</span>ArgumentParser()
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--base&#34;</span>, default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Qwen/Qwen3.5-4B&#34;</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--train&#34;</span>, default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;data/train.jsonl&#34;</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--out&#34;</span>, default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;out/my-jev&#34;</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--objective&#34;</span>, choices<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;proper&#34;</span>, <span style="color:#e6db74">&#34;rlcd&#34;</span>], default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;proper&#34;</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--epochs&#34;</span>, type<span style="color:#f92672">=</span>int, default<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--lr&#34;</span>, type<span style="color:#f92672">=</span>float, default<span style="color:#f92672">=</span><span style="color:#ae81ff">1e-4</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--batch-size&#34;</span>, type<span style="color:#f92672">=</span>int, default<span style="color:#f92672">=</span><span style="color:#ae81ff">4</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--grad-accum&#34;</span>, type<span style="color:#f92672">=</span>int, default<span style="color:#f92672">=</span><span style="color:#ae81ff">8</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--lora-r&#34;</span>, type<span style="color:#f92672">=</span>int, default<span style="color:#f92672">=</span><span style="color:#ae81ff">32</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--max-state-tokens&#34;</span>, type<span style="color:#f92672">=</span>int, default<span style="color:#f92672">=</span><span style="color:#ae81ff">3000</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--mixed-precision&#34;</span>, default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;bf16&#34;</span>)
</span></span><span style="display:flex;"><span>    args <span style="color:#f92672">=</span> ap<span style="color:#f92672">.</span>parse_args()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    acc <span style="color:#f92672">=</span> Accelerator(mixed_precision<span style="color:#f92672">=</span>args<span style="color:#f92672">.</span>mixed_precision, gradient_accumulation_steps<span style="color:#f92672">=</span>args<span style="color:#f92672">.</span>grad_accum)
</span></span><span style="display:flex;"><span>    tok, model, letter_ids <span style="color:#f92672">=</span> load(args<span style="color:#f92672">.</span>base)
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">.</span>gradient_checkpointing_enable(gradient_checkpointing_kwargs<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;use_reentrant&#34;</span>: <span style="color:#66d9ef">False</span>})
</span></span><span style="display:flex;"><span>    model <span style="color:#f92672">=</span> get_peft_model(model, LoraConfig(
</span></span><span style="display:flex;"><span>        r<span style="color:#f92672">=</span>args<span style="color:#f92672">.</span>lora_r, lora_alpha<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#f92672">*</span> args<span style="color:#f92672">.</span>lora_r, lora_dropout<span style="color:#f92672">=</span><span style="color:#ae81ff">0.05</span>, target_modules<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all-linear&#34;</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> acc<span style="color:#f92672">.</span>is_main_process:
</span></span><span style="display:flex;"><span>        model<span style="color:#f92672">.</span>print_trainable_parameters()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    records <span style="color:#f92672">=</span> [json<span style="color:#f92672">.</span>loads(line) <span style="color:#66d9ef">for</span> line <span style="color:#f92672">in</span> open(args<span style="color:#f92672">.</span>train, encoding<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;utf-8&#34;</span>)]
</span></span><span style="display:flex;"><span>    loader <span style="color:#f92672">=</span> DataLoader(records, batch_size<span style="color:#f92672">=</span>args<span style="color:#f92672">.</span>batch_size, shuffle<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>                        collate_fn<span style="color:#f92672">=</span>Collate(tok, args<span style="color:#f92672">.</span>max_state_tokens))
</span></span><span style="display:flex;"><span>    opt <span style="color:#f92672">=</span> torch<span style="color:#f92672">.</span>optim<span style="color:#f92672">.</span>AdamW([p <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> model<span style="color:#f92672">.</span>parameters() <span style="color:#66d9ef">if</span> p<span style="color:#f92672">.</span>requires_grad], lr<span style="color:#f92672">=</span>args<span style="color:#f92672">.</span>lr, weight_decay<span style="color:#f92672">=</span><span style="color:#ae81ff">0.0</span>)
</span></span><span style="display:flex;"><span>    total <span style="color:#f92672">=</span> args<span style="color:#f92672">.</span>epochs <span style="color:#f92672">*</span> math<span style="color:#f92672">.</span>ceil(len(loader) <span style="color:#f92672">/</span> args<span style="color:#f92672">.</span>grad_accum)
</span></span><span style="display:flex;"><span>    sched <span style="color:#f92672">=</span> get_cosine_schedule_with_warmup(opt, max(<span style="color:#ae81ff">1</span>, int(<span style="color:#ae81ff">0.03</span> <span style="color:#f92672">*</span> total)), total)
</span></span><span style="display:flex;"><span>    model, opt, loader, sched <span style="color:#f92672">=</span> acc<span style="color:#f92672">.</span>prepare(model, opt, loader, sched)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    step, n_batches <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>, args<span style="color:#f92672">.</span>epochs <span style="color:#f92672">*</span> len(loader)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> epoch <span style="color:#f92672">in</span> range(args<span style="color:#f92672">.</span>epochs):
</span></span><span style="display:flex;"><span>        model<span style="color:#f92672">.</span>train()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> batch <span style="color:#f92672">in</span> loader:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">with</span> acc<span style="color:#f92672">.</span>accumulate(model):
</span></span><span style="display:flex;"><span>                z, mask <span style="color:#f92672">=</span> option_logits(model, tok, letter_ids, batch[<span style="color:#e6db74">&#34;prompts&#34;</span>], batch[<span style="color:#e6db74">&#34;ks&#34;</span>], acc<span style="color:#f92672">.</span>device)
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> args<span style="color:#f92672">.</span>objective <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;rlcd&#34;</span>:
</span></span><span style="display:flex;"><span>                    sigma <span style="color:#f92672">=</span> <span style="color:#ae81ff">1.0</span> <span style="color:#f92672">-</span> <span style="color:#ae81ff">0.7</span> <span style="color:#f92672">*</span> step <span style="color:#f92672">/</span> n_batches  <span style="color:#75715e"># exploration decays from 1.0 to 0.3</span>
</span></span><span style="display:flex;"><span>                    loss <span style="color:#f92672">=</span> rlcd_loss(z, mask, batch[<span style="color:#e6db74">&#34;y&#34;</span>], batch[<span style="color:#e6db74">&#34;ordinal&#34;</span>], sigma)
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                    loss <span style="color:#f92672">=</span> <span style="color:#f92672">-</span>reward(z, mask, batch[<span style="color:#e6db74">&#34;y&#34;</span>], batch[<span style="color:#e6db74">&#34;ordinal&#34;</span>])<span style="color:#f92672">.</span>mean()
</span></span><span style="display:flex;"><span>                acc<span style="color:#f92672">.</span>backward(loss)
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> acc<span style="color:#f92672">.</span>sync_gradients:
</span></span><span style="display:flex;"><span>                    acc<span style="color:#f92672">.</span>clip_grad_norm_(model<span style="color:#f92672">.</span>parameters(), <span style="color:#ae81ff">1.0</span>)
</span></span><span style="display:flex;"><span>                opt<span style="color:#f92672">.</span>step()
</span></span><span style="display:flex;"><span>                sched<span style="color:#f92672">.</span>step()
</span></span><span style="display:flex;"><span>                opt<span style="color:#f92672">.</span>zero_grad()
</span></span><span style="display:flex;"><span>            step <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> step <span style="color:#f92672">%</span> <span style="color:#ae81ff">20</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>                acc<span style="color:#f92672">.</span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;epoch </span><span style="color:#e6db74">{</span>epoch <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> step </span><span style="color:#e6db74">{</span>step<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">{</span>n_batches<span style="color:#e6db74">}</span><span style="color:#e6db74"> loss </span><span style="color:#e6db74">{</span>loss<span style="color:#f92672">.</span>item()<span style="color:#e6db74">:</span><span style="color:#e6db74">.4f</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        acc<span style="color:#f92672">.</span>wait_for_everyone()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> acc<span style="color:#f92672">.</span>is_main_process:
</span></span><span style="display:flex;"><span>            acc<span style="color:#f92672">.</span>unwrap_model(model)<span style="color:#f92672">.</span>save_pretrained(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>args<span style="color:#f92672">.</span>out<span style="color:#e6db74">}</span><span style="color:#e6db74">/epoch</span><span style="color:#e6db74">{</span>epoch <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    acc<span style="color:#f92672">.</span>end_training()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    main()
</span></span></code></pre></div><p>这段代码做了这几件事：</p>
<ol>
<li><strong>训练目标</strong>：<code>reward()</code> 就是前面介绍的 Laya 组合评分（对数评分 + 0.5 × 球面评分 − RPS），默认的 <code>--objective proper</code> 直接把它的相反数当损失；<code>--objective rlcd</code> 则按 Laya 的方式，用 8 组高斯扰动和组内基线做策略梯度，探索噪声从 1.0 衰减到 0.3。因为加了球面评分，choice 和 noul 题的损失下限是 −0.5（完全确定且答对），日志里出现负数是正常的；RLCD 的损失则是策略梯度的代理目标，数值本身没有意义，两种目标都要以评测指标为准；</li>
<li><strong>选项乱序</strong>：<code>Collate</code> 每次取 batch 都重新打乱 choice 和 noul 的选项顺序，标签跟着一起换位，这是对抗位置偏差最简单有效的办法；score 题保持等级顺序；</li>
<li><strong>LoRA</strong>：<code>target_modules=&quot;all-linear&quot;</code> 会覆盖注意力、Gated DeltaNet 和 MLP 中的所有线性层。在 Qwen3.5-0.8B 上，r=8 时可训练参数约 541 万，只占 0.71%；</li>
<li><strong>省显存</strong>：开启梯度检查点，只计算答案位的 logits；</li>
<li><strong>单卡、多卡同一份代码</strong>：Accelerate 负责设备、混合精度和梯度累积，多卡时自动切换成数据并行。</li>
</ol>
<h3 id="4-启动训练">4. 启动训练</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 单卡：有效 batch = 4 × 8 = 32 道题</span>
</span></span><span style="display:flex;"><span>python train.py --base Qwen/Qwen3.5-4B --train data/train.jsonl --out out/my-jev
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 8 卡数据并行：有效 batch = 4 × 2 × 8 = 64 道题</span>
</span></span><span style="display:flex;"><span>accelerate launch --num_processes <span style="color:#ae81ff">8</span> train.py --base Qwen/Qwen3.5-4B <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --train data/train.jsonl --out out/my-jev --grad-accum <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 对比实验：换成 Laya 式的 RLCD</span>
</span></span><span style="display:flex;"><span>python train.py --base Qwen/Qwen3.5-4B --train data/train.jsonl --out out/my-jev-rlcd --objective rlcd
</span></span></code></pre></div><p>每个 epoch 结束会保存一份 LoRA 适配器（<code>out/my-jev/epoch1</code>、<code>epoch2</code>……），挑哪一份由下一节的评测决定。超参数可以从这里起步：</p>
<table>
	<thead>
			<tr>
					<th>参数</th>
					<th>起点</th>
					<th>说明</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>学习率</td>
					<td>LoRA 1e-4，全参 1e-5</td>
					<td>余弦衰减，3% 步数预热</td>
			</tr>
			<tr>
					<td>LoRA rank / alpha</td>
					<td>32 / 64</td>
					<td>数据量大、任务多时可以加到 64</td>
			</tr>
			<tr>
					<td>有效 batch</td>
					<td>32～64 道题</td>
					<td><code>batch-size × grad-accum × GPU 数</code></td>
			</tr>
			<tr>
					<td>epoch</td>
					<td>1～3</td>
					<td>以留出任务的 log loss 为准，开始变差就停</td>
			</tr>
			<tr>
					<td>state 截断长度</td>
					<td>3000 token</td>
					<td>长文档任务单独加大，并在训练数据里放足长样本</td>
			</tr>
			<tr>
					<td>选项顺序</td>
					<td>每个 batch 重新打乱</td>
					<td>score 题保持等级顺序</td>
			</tr>
	</tbody>
</table>
<p>我在 Qwen3.5-0.8B 上用 CPU 跑过这份脚本的冒烟测试（几十道题、两种训练目标各跑一遍），确认能正常训练和保存。真正的训练请在 GPU 上进行。</p>
<h2 id="六打分测试怎么知道模型好不好">六、打分测试：怎么知道模型好不好</h2>
<h3 id="1-看哪些指标">1. 看哪些指标</h3>
<p>只看准确率是远远不够的。一个合格的决策模型评测，至少要包含下面这些指标：</p>
<table>
	<thead>
			<tr>
					<th>指标</th>
					<th>含义</th>
					<th>怎么看</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>准确率</td>
					<td>概率最高的选项是否正确</td>
					<td>一定要和“永远猜最常见答案”的基线一起看</td>
			</tr>
			<tr>
					<td>去随机准确率</td>
					<td><code>(准确率 − 随机猜中率) / (1 − 随机猜中率)</code></td>
					<td>JevBench 的智能分就是这个思路，0 相当于瞎猜，100 为全对</td>
			</tr>
			<tr>
					<td>对数损失</td>
					<td>正确答案概率取对数的负值的均值</td>
					<td>训练目标本身，越小越好，对“自信地答错”惩罚最重</td>
			</tr>
			<tr>
					<td>Brier 分数</td>
					<td>概率与标签的平方误差之和</td>
					<td>越小越好，比对数损失更稳健</td>
			</tr>
			<tr>
					<td>ECE</td>
					<td>按置信度分成 10 个区间，置信度与准确率之差的加权平均</td>
					<td>校准的核心指标，越接近 0 越好</td>
			</tr>
			<tr>
					<td>RPS</td>
					<td>score 题累积分布的误差</td>
					<td>等级题专用，差一级比差三级惩罚小</td>
			</tr>
			<tr>
					<td>选项倒序翻转率</td>
					<td>把选项顺序倒过来，答案发生变化的比例</td>
					<td>位置偏差的直接度量</td>
			</tr>
			<tr>
					<td>阈值曲线</td>
					<td>不同置信度阈值下的转交比例和放行准确率</td>
					<td>用来确定上线阈值，也就是前面 H2O 的那张表</td>
			</tr>
			<tr>
					<td>延迟</td>
					<td>单题 p50 / p95</td>
					<td>决策模型的生命线</td>
			</tr>
	</tbody>
</table>
<p>位置偏差绝不是小问题。学术界早有研究表明，大模型做选择题时会偏好特定的选项位置和选项字母（Zheng 等人，2023）。JevBench 也记录过一个例子：open-alternative-jev 在“判断回答是否合格”这类题上，按作者给的选项顺序（<code>A. yes, B. no</code>）准确率是 72%，把两个选项倒过来就只剩 21%。除了脚本里的倒序翻转率，上线前最好再补几项一致性检查：同一问题换种问法答案是否一致、问题和它的否定形式概率之和是否接近 1、state 里插入注入文本后答案是否被带偏。</p>
<h3 id="2-评测脚本">2. 评测脚本</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># evaluate.py</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Evaluate a decision model: accuracy, chance-corrected accuracy, calibration, order robustness, latency.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> argparse
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> statistics
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> torch
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> peft <span style="color:#f92672">import</span> PeftModel
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> decision_format <span style="color:#f92672">import</span> load, option_logits, options, prompt, target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@torch.no_grad</span>()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">predict</span>(model, tok, letter_ids, records, batch_size<span style="color:#f92672">=</span><span style="color:#ae81ff">8</span>, reverse<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Logits over each record&#39;s options, in the caller&#39;s order.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">0</span>, len(records), batch_size):
</span></span><span style="display:flex;"><span>        chunk, orders <span style="color:#f92672">=</span> records[i : i <span style="color:#f92672">+</span> batch_size], []
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> chunk:
</span></span><span style="display:flex;"><span>            order <span style="color:#f92672">=</span> list(range(len(options(r[<span style="color:#e6db74">&#34;question&#34;</span>]))))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> reverse <span style="color:#f92672">and</span> r[<span style="color:#e6db74">&#34;question&#34;</span>][<span style="color:#e6db74">&#34;type&#34;</span>] <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;score&#34;</span>:
</span></span><span style="display:flex;"><span>                order<span style="color:#f92672">.</span>reverse()
</span></span><span style="display:flex;"><span>            orders<span style="color:#f92672">.</span>append(order)
</span></span><span style="display:flex;"><span>        prompts <span style="color:#f92672">=</span> [prompt(tok, r, o) <span style="color:#66d9ef">for</span> r, o <span style="color:#f92672">in</span> zip(chunk, orders)]
</span></span><span style="display:flex;"><span>        z, _ <span style="color:#f92672">=</span> option_logits(model, tok, letter_ids, prompts, [len(o) <span style="color:#66d9ef">for</span> o <span style="color:#f92672">in</span> orders], model<span style="color:#f92672">.</span>device)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> row, order <span style="color:#f92672">in</span> enumerate(orders):
</span></span><span style="display:flex;"><span>            logits <span style="color:#f92672">=</span> torch<span style="color:#f92672">.</span>empty(len(order))
</span></span><span style="display:flex;"><span>            logits[order] <span style="color:#f92672">=</span> z[row, : len(order)]<span style="color:#f92672">.</span>cpu()  <span style="color:#75715e"># slot s showed option order[s]</span>
</span></span><span style="display:flex;"><span>            result<span style="color:#f92672">.</span>append(logits)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ece</span>(conf, correct, bins<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>):
</span></span><span style="display:flex;"><span>    groups <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> c, ok <span style="color:#f92672">in</span> zip(conf, correct):
</span></span><span style="display:flex;"><span>        groups<span style="color:#f92672">.</span>setdefault(min(int(c <span style="color:#f92672">*</span> bins), bins <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>), [])<span style="color:#f92672">.</span>append((c, ok))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> sum(len(g) <span style="color:#f92672">/</span> len(conf) <span style="color:#f92672">*</span> abs(statistics<span style="color:#f92672">.</span>mean(o <span style="color:#66d9ef">for</span> _, o <span style="color:#f92672">in</span> g) <span style="color:#f92672">-</span> statistics<span style="color:#f92672">.</span>mean(c <span style="color:#66d9ef">for</span> c, _ <span style="color:#f92672">in</span> g))
</span></span><span style="display:flex;"><span>               <span style="color:#66d9ef">for</span> g <span style="color:#f92672">in</span> groups<span style="color:#f92672">.</span>values())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">summary</span>(rows):
</span></span><span style="display:flex;"><span>    conf <span style="color:#f92672">=</span> [p<span style="color:#f92672">.</span>max()<span style="color:#f92672">.</span>item() <span style="color:#66d9ef">for</span> _, p, _ <span style="color:#f92672">in</span> rows]
</span></span><span style="display:flex;"><span>    correct <span style="color:#f92672">=</span> [float(p<span style="color:#f92672">.</span>argmax() <span style="color:#f92672">==</span> y<span style="color:#f92672">.</span>argmax()) <span style="color:#66d9ef">for</span> _, p, y <span style="color:#f92672">in</span> rows]
</span></span><span style="display:flex;"><span>    acc <span style="color:#f92672">=</span> statistics<span style="color:#f92672">.</span>mean(correct)
</span></span><span style="display:flex;"><span>    chance <span style="color:#f92672">=</span> statistics<span style="color:#f92672">.</span>mean(<span style="color:#ae81ff">1</span> <span style="color:#f92672">/</span> len(p) <span style="color:#66d9ef">for</span> _, p, _ <span style="color:#f92672">in</span> rows)
</span></span><span style="display:flex;"><span>    out <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;n&#34;</span>: len(rows), <span style="color:#e6db74">&#34;accuracy&#34;</span>: acc, <span style="color:#e6db74">&#34;chance_corrected&#34;</span>: (acc <span style="color:#f92672">-</span> chance) <span style="color:#f92672">/</span> (<span style="color:#ae81ff">1</span> <span style="color:#f92672">-</span> chance),
</span></span><span style="display:flex;"><span>           <span style="color:#e6db74">&#34;log_loss&#34;</span>: statistics<span style="color:#f92672">.</span>mean(<span style="color:#f92672">-</span>(y <span style="color:#f92672">*</span> p<span style="color:#f92672">.</span>clamp_min(<span style="color:#ae81ff">1e-12</span>)<span style="color:#f92672">.</span>log())<span style="color:#f92672">.</span>sum()<span style="color:#f92672">.</span>item() <span style="color:#66d9ef">for</span> _, p, y <span style="color:#f92672">in</span> rows),
</span></span><span style="display:flex;"><span>           <span style="color:#e6db74">&#34;brier&#34;</span>: statistics<span style="color:#f92672">.</span>mean(((p <span style="color:#f92672">-</span> y) <span style="color:#f92672">**</span> <span style="color:#ae81ff">2</span>)<span style="color:#f92672">.</span>sum()<span style="color:#f92672">.</span>item() <span style="color:#66d9ef">for</span> _, p, y <span style="color:#f92672">in</span> rows),
</span></span><span style="display:flex;"><span>           <span style="color:#e6db74">&#34;ece&#34;</span>: ece(conf, correct)}
</span></span><span style="display:flex;"><span>    ordinal <span style="color:#f92672">=</span> [(p, y) <span style="color:#66d9ef">for</span> kind, p, y <span style="color:#f92672">in</span> rows <span style="color:#66d9ef">if</span> kind <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;score&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ordinal:
</span></span><span style="display:flex;"><span>        out[<span style="color:#e6db74">&#34;rps&#34;</span>] <span style="color:#f92672">=</span> statistics<span style="color:#f92672">.</span>mean((((p<span style="color:#f92672">.</span>cumsum(<span style="color:#ae81ff">0</span>) <span style="color:#f92672">-</span> y<span style="color:#f92672">.</span>cumsum(<span style="color:#ae81ff">0</span>)) <span style="color:#f92672">**</span> <span style="color:#ae81ff">2</span>)<span style="color:#f92672">.</span>sum() <span style="color:#f92672">/</span> (len(p) <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>))<span style="color:#f92672">.</span>item()
</span></span><span style="display:flex;"><span>                                     <span style="color:#66d9ef">for</span> p, y <span style="color:#f92672">in</span> ordinal)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> out
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">metrics</span>(records, logits, temps):
</span></span><span style="display:flex;"><span>    rows <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> r, z <span style="color:#f92672">in</span> zip(records, logits):
</span></span><span style="display:flex;"><span>        kind <span style="color:#f92672">=</span> r[<span style="color:#e6db74">&#34;question&#34;</span>][<span style="color:#e6db74">&#34;type&#34;</span>]
</span></span><span style="display:flex;"><span>        p <span style="color:#f92672">=</span> (z <span style="color:#f92672">/</span> temps<span style="color:#f92672">.</span>get(kind, <span style="color:#ae81ff">1.0</span>))<span style="color:#f92672">.</span>softmax(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>        rows<span style="color:#f92672">.</span>append((kind, p, torch<span style="color:#f92672">.</span>tensor(target(r, options(r[<span style="color:#e6db74">&#34;question&#34;</span>])))))
</span></span><span style="display:flex;"><span>    report <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;all&#34;</span>: summary(rows)}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> kind <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;choice&#34;</span>, <span style="color:#e6db74">&#34;noul&#34;</span>, <span style="color:#e6db74">&#34;score&#34;</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> any(row[<span style="color:#ae81ff">0</span>] <span style="color:#f92672">==</span> kind <span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> rows):
</span></span><span style="display:flex;"><span>            report[kind] <span style="color:#f92672">=</span> summary([row <span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> rows <span style="color:#66d9ef">if</span> row[<span style="color:#ae81ff">0</span>] <span style="color:#f92672">==</span> kind])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> report
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fit_temperatures</span>(records, logits):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;One temperature per question type, chosen to minimise log loss (fit on dev data only).&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    grid, temps <span style="color:#f92672">=</span> [round(<span style="color:#ae81ff">0.3</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">0.05</span> <span style="color:#f92672">*</span> i, <span style="color:#ae81ff">2</span>) <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">55</span>)], {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> kind <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;choice&#34;</span>, <span style="color:#e6db74">&#34;noul&#34;</span>, <span style="color:#e6db74">&#34;score&#34;</span>):
</span></span><span style="display:flex;"><span>        pairs <span style="color:#f92672">=</span> [(z, torch<span style="color:#f92672">.</span>tensor(target(r, options(r[<span style="color:#e6db74">&#34;question&#34;</span>]))))
</span></span><span style="display:flex;"><span>                 <span style="color:#66d9ef">for</span> r, z <span style="color:#f92672">in</span> zip(records, logits) <span style="color:#66d9ef">if</span> r[<span style="color:#e6db74">&#34;question&#34;</span>][<span style="color:#e6db74">&#34;type&#34;</span>] <span style="color:#f92672">==</span> kind]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> pairs:
</span></span><span style="display:flex;"><span>            temps[kind] <span style="color:#f92672">=</span> min(grid, key<span style="color:#f92672">=</span><span style="color:#66d9ef">lambda</span> t: sum(<span style="color:#f92672">-</span>(y <span style="color:#f92672">*</span> (z <span style="color:#f92672">/</span> t)<span style="color:#f92672">.</span>log_softmax(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>))<span style="color:#f92672">.</span>sum()<span style="color:#f92672">.</span>item()
</span></span><span style="display:flex;"><span>                                                      <span style="color:#66d9ef">for</span> z, y <span style="color:#f92672">in</span> pairs))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> temps
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@torch.no_grad</span>()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">latency_ms</span>(model, tok, letter_ids, records, n<span style="color:#f92672">=</span><span style="color:#ae81ff">50</span>):
</span></span><span style="display:flex;"><span>    times <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> records[:n]:
</span></span><span style="display:flex;"><span>        k <span style="color:#f92672">=</span> len(options(r[<span style="color:#e6db74">&#34;question&#34;</span>]))
</span></span><span style="display:flex;"><span>        text <span style="color:#f92672">=</span> prompt(tok, r, list(range(k)))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> torch<span style="color:#f92672">.</span>cuda<span style="color:#f92672">.</span>is_available():
</span></span><span style="display:flex;"><span>            torch<span style="color:#f92672">.</span>cuda<span style="color:#f92672">.</span>synchronize()
</span></span><span style="display:flex;"><span>        start <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>        option_logits(model, tok, letter_ids, [text], [k], model<span style="color:#f92672">.</span>device)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> torch<span style="color:#f92672">.</span>cuda<span style="color:#f92672">.</span>is_available():
</span></span><span style="display:flex;"><span>            torch<span style="color:#f92672">.</span>cuda<span style="color:#f92672">.</span>synchronize()
</span></span><span style="display:flex;"><span>        times<span style="color:#f92672">.</span>append((time<span style="color:#f92672">.</span>perf_counter() <span style="color:#f92672">-</span> start) <span style="color:#f92672">*</span> <span style="color:#ae81ff">1000</span>)
</span></span><span style="display:flex;"><span>    times<span style="color:#f92672">.</span>sort()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;p50&#34;</span>: times[len(times) <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>], <span style="color:#e6db74">&#34;p95&#34;</span>: times[min(len(times) <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>, int(<span style="color:#ae81ff">0.95</span> <span style="color:#f92672">*</span> len(times)))]}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
</span></span><span style="display:flex;"><span>    ap <span style="color:#f92672">=</span> argparse<span style="color:#f92672">.</span>ArgumentParser()
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--base&#34;</span>, default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Qwen/Qwen3.5-4B&#34;</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--adapter&#34;</span>, help<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;LoRA directory; omit it to score the raw base model&#34;</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--data&#34;</span>, nargs<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;+&#34;</span>, required<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--fit-temperature&#34;</span>, help<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;fit temperatures on --data (dev only) and save them here&#34;</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--temperatures&#34;</span>, help<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;temperatures fitted earlier&#34;</span>)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--batch-size&#34;</span>, type<span style="color:#f92672">=</span>int, default<span style="color:#f92672">=</span><span style="color:#ae81ff">8</span>)
</span></span><span style="display:flex;"><span>    args <span style="color:#f92672">=</span> ap<span style="color:#f92672">.</span>parse_args()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    tok, model, letter_ids <span style="color:#f92672">=</span> load(args<span style="color:#f92672">.</span>base)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> args<span style="color:#f92672">.</span>adapter:
</span></span><span style="display:flex;"><span>        model <span style="color:#f92672">=</span> PeftModel<span style="color:#f92672">.</span>from_pretrained(model, args<span style="color:#f92672">.</span>adapter)<span style="color:#f92672">.</span>merge_and_unload()
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">.</span>to(<span style="color:#e6db74">&#34;cuda&#34;</span> <span style="color:#66d9ef">if</span> torch<span style="color:#f92672">.</span>cuda<span style="color:#f92672">.</span>is_available() <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;cpu&#34;</span>)<span style="color:#f92672">.</span>eval()
</span></span><span style="display:flex;"><span>    records <span style="color:#f92672">=</span> [json<span style="color:#f92672">.</span>loads(line) <span style="color:#66d9ef">for</span> path <span style="color:#f92672">in</span> args<span style="color:#f92672">.</span>data <span style="color:#66d9ef">for</span> line <span style="color:#f92672">in</span> open(path, encoding<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;utf-8&#34;</span>)]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    logits <span style="color:#f92672">=</span> predict(model, tok, letter_ids, records, args<span style="color:#f92672">.</span>batch_size)
</span></span><span style="display:flex;"><span>    temps <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>load(open(args<span style="color:#f92672">.</span>temperatures)) <span style="color:#66d9ef">if</span> args<span style="color:#f92672">.</span>temperatures <span style="color:#66d9ef">else</span> {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> args<span style="color:#f92672">.</span>fit_temperature:
</span></span><span style="display:flex;"><span>        temps <span style="color:#f92672">=</span> fit_temperatures(records, logits)
</span></span><span style="display:flex;"><span>        json<span style="color:#f92672">.</span>dump(temps, open(args<span style="color:#f92672">.</span>fit_temperature, <span style="color:#e6db74">&#34;w&#34;</span>), indent<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>    flipped <span style="color:#f92672">=</span> predict(model, tok, letter_ids, records, args<span style="color:#f92672">.</span>batch_size, reverse<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    flips <span style="color:#f92672">=</span> [bool(a<span style="color:#f92672">.</span>argmax() <span style="color:#f92672">!=</span> b<span style="color:#f92672">.</span>argmax())
</span></span><span style="display:flex;"><span>             <span style="color:#66d9ef">for</span> r, a, b <span style="color:#f92672">in</span> zip(records, logits, flipped) <span style="color:#66d9ef">if</span> r[<span style="color:#e6db74">&#34;question&#34;</span>][<span style="color:#e6db74">&#34;type&#34;</span>] <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;score&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    report <span style="color:#f92672">=</span> metrics(records, logits, temps)
</span></span><span style="display:flex;"><span>    report[<span style="color:#e6db74">&#34;flip_rate_reversed_options&#34;</span>] <span style="color:#f92672">=</span> sum(flips) <span style="color:#f92672">/</span> max(<span style="color:#ae81ff">1</span>, len(flips))
</span></span><span style="display:flex;"><span>    report[<span style="color:#e6db74">&#34;temperatures&#34;</span>] <span style="color:#f92672">=</span> temps
</span></span><span style="display:flex;"><span>    report[<span style="color:#e6db74">&#34;latency_ms&#34;</span>] <span style="color:#f92672">=</span> latency_ms(model, tok, letter_ids, records)
</span></span><span style="display:flex;"><span>    print(json<span style="color:#f92672">.</span>dumps(report, indent<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    main()
</span></span></code></pre></div><p>典型的用法分三步：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1) 零样本基线：不加适配器，直接读底座模型的字母概率，先知道起点</span>
</span></span><span style="display:flex;"><span>python evaluate.py --base Qwen/Qwen3.5-4B --data data/dev.jsonl data/heldout_tasks.jsonl
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2) 在开发集上评测训练好的模型，并按题型拟合温度</span>
</span></span><span style="display:flex;"><span>python evaluate.py --base Qwen/Qwen3.5-4B --adapter out/my-jev/epoch2 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --data data/dev.jsonl --fit-temperature out/my-jev/temperatures.json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3) 带上温度，在留出任务和 JevBench 公开题上打分（温度只能在开发集上拟合）</span>
</span></span><span style="display:flex;"><span>python evaluate.py --base Qwen/Qwen3.5-4B --adapter out/my-jev/epoch2 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --temperatures out/my-jev/temperatures.json --data data/heldout_tasks.jsonl
</span></span><span style="display:flex;"><span>python evaluate.py --base Qwen/Qwen3.5-4B --adapter out/my-jev/epoch2 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --temperatures out/my-jev/temperatures.json --data eval/easy.jsonl eval/original.jsonl eval/hard.jsonl
</span></span></code></pre></div><p>作为示例，下面是 Qwen3.5-0.8B 原始模型（不加适配器）在 JevBench 公开的 easy 和 original 两组、共 120 道题上的结果（节选，保留三位小数）。这是在我的 Mac 上用 CPU 跑的，延迟没有参考意义，就不列出来了：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;all&#34;</span>:    {<span style="color:#f92672">&#34;n&#34;</span>: <span style="color:#ae81ff">120</span>, <span style="color:#f92672">&#34;accuracy&#34;</span>: <span style="color:#ae81ff">0.642</span>, <span style="color:#f92672">&#34;chance_corrected&#34;</span>: <span style="color:#ae81ff">0.488</span>, <span style="color:#f92672">&#34;log_loss&#34;</span>: <span style="color:#ae81ff">1.326</span>, <span style="color:#f92672">&#34;brier&#34;</span>: <span style="color:#ae81ff">0.523</span>, <span style="color:#f92672">&#34;ece&#34;</span>: <span style="color:#ae81ff">0.108</span>},
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;choice&#34;</span>: {<span style="color:#f92672">&#34;n&#34;</span>: <span style="color:#ae81ff">72</span>,  <span style="color:#f92672">&#34;accuracy&#34;</span>: <span style="color:#ae81ff">0.694</span>, <span style="color:#f92672">&#34;chance_corrected&#34;</span>: <span style="color:#ae81ff">0.614</span>, <span style="color:#f92672">&#34;ece&#34;</span>: <span style="color:#ae81ff">0.117</span>},
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;noul&#34;</span>:   {<span style="color:#f92672">&#34;n&#34;</span>: <span style="color:#ae81ff">36</span>,  <span style="color:#f92672">&#34;accuracy&#34;</span>: <span style="color:#ae81ff">0.583</span>, <span style="color:#f92672">&#34;chance_corrected&#34;</span>: <span style="color:#ae81ff">0.167</span>, <span style="color:#f92672">&#34;ece&#34;</span>: <span style="color:#ae81ff">0.220</span>},
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;score&#34;</span>:  {<span style="color:#f92672">&#34;n&#34;</span>: <span style="color:#ae81ff">12</span>,  <span style="color:#f92672">&#34;accuracy&#34;</span>: <span style="color:#ae81ff">0.500</span>, <span style="color:#f92672">&#34;chance_corrected&#34;</span>: <span style="color:#ae81ff">0.333</span>, <span style="color:#f92672">&#34;rps&#34;</span>: <span style="color:#ae81ff">0.151</span>},
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;flip_rate_reversed_options&#34;</span>: <span style="color:#ae81ff">0.620</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>几个值得注意的地方：</p>
<ul>
<li>noul 题的准确率只有 58.3%，去掉随机猜中的部分只剩 16.7%，基本是在猜；</li>
<li><strong>选项倒序翻转率高达 62%</strong>：只是把选项顺序倒过来，六成以上题目的答案就变了。我还单独试了一道题：state 是“结账页面对所有客户返回 HTTP 500”，问题是“是否有生产系统宕机”，选项按 yes、no 排列时 P(yes) 是 0.18，换成 no、yes 排列就变成了 0.45。小模型的位置偏差比想象中严重得多，这正是训练时要不断打乱选项、评测时一定要报告这个指标的原因；</li>
<li>作为对照，H2O-Lightning-4B 在同样这 120 道题上答对了 119 道。两者规模不同，差距不能全部归功于微调，但足以说明决策微调要补的课有多少。</li>
</ul>
<h3 id="3-和-jevbench-上的模型同场比较">3. 和 JevBench 上的模型同场比较</h3>
<p>JevBench 的 GitHub 仓库公开了 231 道题（<code>datasets/public/</code> 下的 easy 48 道、original 72 道、hard 111 道），格式和本文的数据格式一致，<code>evaluate.py</code> 可以直接读取。作为参照，H2O-Lightning-4B 在这 231 道题上答对 205 道（88.7%）：easy 48/48，original 71/72，hard 86/111。</p>
<p>不过要注意三点：</p>
<ol>
<li><strong>公开题只是参考</strong>：榜单分数基于每期 1500 道题（1200 道不公开的封存题加 300 道公开题，智能分里两部分各占一半），还做了去随机、分难度加权和公开 / 封存差距惩罚，公开题上的准确率不能直接换算成榜单分数；</li>
<li><strong>绝不能拿公开题训练</strong>：JevBench 要求在公开题上训练过的模型必须主动声明，<code>prepare_data.py</code> 里的去污染就是为此准备的；</li>
<li><strong>想上榜就提交</strong>：在 JevBench 网站提交模型后，会由维护者在他们自己的 GPU 上跑完整的封存题。</li>
</ol>
<p>如果想完全按 JevBench 官方的方式测，可以先把模型包装成和 Jev 一样的 HTTP 接口，再用官方的评测工具去调用它。</p>
<h3 id="4-服务化兼容-jev-的-v1systemone">4. 服务化：兼容 Jev 的 /v1/systemone</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># serve.py</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;A minimal /v1/systemone server (Jev wire format) in front of the fine-tuned model.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> torch
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> FastAPI, HTTPException
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> peft <span style="color:#f92672">import</span> PeftModel
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> decision_format <span style="color:#f92672">import</span> LETTERS, load, option_logits, options, prompt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>BASE <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;BASE_MODEL&#34;</span>, <span style="color:#e6db74">&#34;Qwen/Qwen3.5-4B&#34;</span>)
</span></span><span style="display:flex;"><span>ADAPTER <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;ADAPTER&#34;</span>, <span style="color:#e6db74">&#34;out/my-jev/epoch2&#34;</span>)
</span></span><span style="display:flex;"><span>TEMPS <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>load(open(os<span style="color:#f92672">.</span>environ[<span style="color:#e6db74">&#34;TEMPERATURES&#34;</span>])) <span style="color:#66d9ef">if</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;TEMPERATURES&#34;</span>) <span style="color:#66d9ef">else</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>tok, model, letter_ids <span style="color:#f92672">=</span> load(BASE)
</span></span><span style="display:flex;"><span>model <span style="color:#f92672">=</span> PeftModel<span style="color:#f92672">.</span>from_pretrained(model, ADAPTER)<span style="color:#f92672">.</span>merge_and_unload()
</span></span><span style="display:flex;"><span>model<span style="color:#f92672">.</span>to(<span style="color:#e6db74">&#34;cuda&#34;</span> <span style="color:#66d9ef">if</span> torch<span style="color:#f92672">.</span>cuda<span style="color:#f92672">.</span>is_available() <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;cpu&#34;</span>)<span style="color:#f92672">.</span>eval()
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> FastAPI()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.post</span>(<span style="color:#e6db74">&#34;/v1/systemone&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@torch.no_grad</span>()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">systemone</span>(body: dict):
</span></span><span style="display:flex;"><span>    questions <span style="color:#f92672">=</span> body<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;questions&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> isinstance(questions, dict) <span style="color:#f92672">or</span> <span style="color:#f92672">not</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">&lt;</span> len(questions) <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">32</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> HTTPException(<span style="color:#ae81ff">422</span>, <span style="color:#e6db74">&#34;questions must be an object with 1 to 32 entries&#34;</span>)
</span></span><span style="display:flex;"><span>    names, prompts, ks <span style="color:#f92672">=</span> list(questions), [], []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> name <span style="color:#f92672">in</span> names:
</span></span><span style="display:flex;"><span>        q <span style="color:#f92672">=</span> questions[name]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            k <span style="color:#f92672">=</span> len(options(q))
</span></span><span style="display:flex;"><span>            ok <span style="color:#f92672">=</span> q[<span style="color:#e6db74">&#34;type&#34;</span>] <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;choice&#34;</span>, <span style="color:#e6db74">&#34;noul&#34;</span>, <span style="color:#e6db74">&#34;score&#34;</span>) <span style="color:#f92672">and</span> isinstance(q[<span style="color:#e6db74">&#34;instructions&#34;</span>], str)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> (<span style="color:#a6e22e">KeyError</span>, <span style="color:#a6e22e">TypeError</span>, <span style="color:#a6e22e">AttributeError</span>):
</span></span><span style="display:flex;"><span>            ok <span style="color:#f92672">=</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> ok <span style="color:#f92672">or</span> <span style="color:#f92672">not</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">&lt;=</span> k <span style="color:#f92672">&lt;=</span> len(LETTERS):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> HTTPException(<span style="color:#ae81ff">422</span>, <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">: malformed question or unsupported option count&#34;</span>)
</span></span><span style="display:flex;"><span>        prompts<span style="color:#f92672">.</span>append(prompt(tok, {<span style="color:#e6db74">&#34;state&#34;</span>: body<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;state&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>), <span style="color:#e6db74">&#34;question&#34;</span>: q}, list(range(k))))
</span></span><span style="display:flex;"><span>        ks<span style="color:#f92672">.</span>append(k)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    z, _ <span style="color:#f92672">=</span> option_logits(model, tok, letter_ids, prompts, ks, model<span style="color:#f92672">.</span>device)
</span></span><span style="display:flex;"><span>    answers <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> row, name <span style="color:#f92672">in</span> enumerate(names):
</span></span><span style="display:flex;"><span>        q, k <span style="color:#f92672">=</span> questions[name], ks[row]
</span></span><span style="display:flex;"><span>        p <span style="color:#f92672">=</span> (z[row, :k] <span style="color:#f92672">/</span> TEMPS<span style="color:#f92672">.</span>get(q[<span style="color:#e6db74">&#34;type&#34;</span>], <span style="color:#ae81ff">1.0</span>))<span style="color:#f92672">.</span>softmax(<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>)<span style="color:#f92672">.</span>tolist()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> q[<span style="color:#e6db74">&#34;type&#34;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;noul&#34;</span>:
</span></span><span style="display:flex;"><span>            answers[name] <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;noul&#34;</span>, <span style="color:#e6db74">&#34;noul&#34;</span>: p[<span style="color:#ae81ff">0</span>]}  <span style="color:#75715e"># P(yes)</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> q[<span style="color:#e6db74">&#34;type&#34;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;score&#34;</span>:
</span></span><span style="display:flex;"><span>            answers[name] <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;score&#34;</span>, <span style="color:#e6db74">&#34;score&#34;</span>: sum(i <span style="color:#f92672">*</span> x <span style="color:#66d9ef">for</span> i, x <span style="color:#f92672">in</span> enumerate(p)),
</span></span><span style="display:flex;"><span>                             <span style="color:#e6db74">&#34;probabilities&#34;</span>: {str(i): x <span style="color:#66d9ef">for</span> i, x <span style="color:#f92672">in</span> enumerate(p)}}
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            probs <span style="color:#f92672">=</span> {key: x <span style="color:#66d9ef">for</span> (key, _), x <span style="color:#f92672">in</span> zip(options(q), p)}
</span></span><span style="display:flex;"><span>            best <span style="color:#f92672">=</span> max(probs, key<span style="color:#f92672">=</span>probs<span style="color:#f92672">.</span>get)
</span></span><span style="display:flex;"><span>            answers[name] <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;choice&#34;</span>, <span style="color:#e6db74">&#34;choice&#34;</span>: best, <span style="color:#e6db74">&#34;probabilities&#34;</span>: probs,
</span></span><span style="display:flex;"><span>                             <span style="color:#e6db74">&#34;confidence&#34;</span>: (k <span style="color:#f92672">*</span> probs[best] <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>) <span style="color:#f92672">/</span> (k <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>)}
</span></span><span style="display:flex;"><span>    used <span style="color:#f92672">=</span> sum(len(ids) <span style="color:#66d9ef">for</span> ids <span style="color:#f92672">in</span> tok(prompts, add_special_tokens<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)<span style="color:#f92672">.</span>input_ids)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;answers&#34;</span>: answers, <span style="color:#e6db74">&#34;usage&#34;</span>: {<span style="color:#e6db74">&#34;input_tokens&#34;</span>: used, <span style="color:#e6db74">&#34;output_tokens&#34;</span>: <span style="color:#ae81ff">0</span>}}
</span></span></code></pre></div><p>启动服务，然后用 curl 验证一下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ADAPTER<span style="color:#f92672">=</span>out/my-jev/epoch2 TEMPERATURES<span style="color:#f92672">=</span>out/my-jev/temperatures.json <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  uvicorn serve:app --host 127.0.0.1 --port <span style="color:#ae81ff">8741</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>curl -s http://127.0.0.1:8741/v1/systemone -H <span style="color:#e6db74">&#39;Content-Type: application/json&#39;</span> -d <span style="color:#e6db74">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;state&#34;: &#34;Checkout returns HTTP 500 for every customer since 09:10.&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;questions&#34;: {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;team&#34;: {&#34;type&#34;: &#34;choice&#34;, &#34;instructions&#34;: &#34;Which team should handle this?&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">             &#34;criteria&#34;: {&#34;billing&#34;: &#34;charges and refunds&#34;, &#34;engineering&#34;: &#34;bugs and outages&#34;}},
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;outage&#34;: {&#34;type&#34;: &#34;noul&#34;, &#34;instructions&#34;: &#34;Is a production system down?&#34;}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">}&#39;</span>
</span></span></code></pre></div><p>返回结构沿用 Jev 的格式：每个答案都带 <code>type</code>，choice 返回 <code>choice</code>、<code>probabilities</code> 和按 Jev 公式 <code>(K·p_max − 1)/(K − 1)</code> 计算的 <code>confidence</code>，score 返回期望等级和以等级下标为键的概率，noul 直接返回 P(yes)，<code>usage</code> 里只有输入 token。这正是 JevBench 官方评测工具（<code>typesafe</code> 适配器）解析的字段，所以可以直接拿它来测，已有的 Jev 客户端通常也只需要改一下 <code>baseUrl</code>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone https://github.com/fstandhartinger/jevbench <span style="color:#f92672">&amp;&amp;</span> cd jevbench
</span></span><span style="display:flex;"><span>python -m jevbench.cli run <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --tasks datasets/public/easy.jsonl,datasets/public/original.jsonl,datasets/public/hard.jsonl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --adapter typesafe --endpoint http://127.0.0.1:8741 --key-env <span style="color:#e6db74">&#39;&#39;</span> --model my-jev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --cost-basis self_hosted_gpu --reserve-usd <span style="color:#ae81ff">0</span> --delay-s <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --results RUN/results.jsonl --raw-dir RUN/raw --ledger RUN/ledger.jsonl --manifest RUN/manifest.json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 汇总成绩</span>
</span></span><span style="display:flex;"><span>python -m jevbench.cli summarize <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --tasks datasets/public/easy.jsonl,datasets/public/original.jsonl,datasets/public/hard.jsonl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --results RUN/results.jsonl --public-export RUN/summary.json
</span></span></code></pre></div><p>这个服务只是为了评测和演示：它一次处理一个请求，没有认证。正式上线时，推荐像 H2O-Lightning-4B 那样把合并后的模型交给 vLLM（连续批处理、前缀缓存），前面放一个很薄的协议转换层，并加上认证、限流和请求大小限制，不要直接暴露在公网上。</p>
<h2 id="七调整思路分数不理想怎么办">七、调整思路：分数不理想怎么办</h2>
<h3 id="1-先有基线再按切片找问题">1. 先有基线，再按切片找问题</h3>
<p>动手调参之前，先确认两件事：一是零样本基线（不微调、直接读底座的字母概率）是多少，二是“永远猜最常见答案”能拿多少分。训练后的模型必须和这两个数字比，否则很容易被虚高的准确率骗到。</p>
<p>然后按切片拆开看：题型、任务、语言、选项数、state 长度、置信度区间。总分掩盖的问题，往往在切片里一目了然。H2O 把这叫作“找短板”（gap hunting）：按主题和题型和其他系统对比，跑对抗性题集，然后针对性地补充经过验证的新数据。</p>
<h3 id="2-症状与对策">2. 症状与对策</h3>
<table>
	<thead>
			<tr>
					<th>症状</th>
					<th>可能的原因</th>
					<th>对策</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>训练任务上很好，留出任务上很差</td>
					<td>过拟合了训练任务的措辞和模板</td>
					<td>增加任务种类和问法；减少 epoch、降低学习率；用留出任务的 log loss 做早停</td>
			</tr>
			<tr>
					<td>准确率还行，但 ECE 很高</td>
					<td>训练过头导致过度自信，或标签有噪声</td>
					<td>温度缩放；减少 epoch；有争议的样本改用软标签</td>
			</tr>
			<tr>
					<td>选项倒序后答案改变</td>
					<td>位置偏差</td>
					<td>训练时每步打乱选项；推理时用多种顺序取平均（Laya 实测可把顺序导致的答案变化从 16.3% 降到 6.1%）；或改用选项之间互不可见的并行布局</td>
			</tr>
			<tr>
					<td>noul 总是偏向“是”或“否”</td>
					<td>标签词偏置、正反样本不均衡</td>
					<td>平衡正反样本；构造“问题 + 否定问题”成对的数据；检查 <code>criteria</code> 的措辞是否带有倾向</td>
			</tr>
			<tr>
					<td>score 误差大</td>
					<td>有序数据少，等级描述模糊</td>
					<td>增加有序打分数据，提高 RPS 的权重；每个等级写清楚判定标准</td>
			</tr>
			<tr>
					<td>选项一多就不准</td>
					<td>选项超过字母数，或选项彼此相似</td>
					<td>先用 embedding 或分组淘汰缩小候选（Laya 的分组淘汰把 BANKING77 从 0.430 提到 0.610）；或拆成“粗分类 + 细分类”两级问题</td>
			</tr>
			<tr>
					<td>长文档准确率下降</td>
					<td>训练时没见过长样本，或被截断</td>
					<td>加一个专门的长文档训练阶段（H2O 的第二阶段就是这么做的）；检查截断比例，调大 <code>max_state_tokens</code></td>
			</tr>
			<tr>
					<td>计数、日期、算术题出错</td>
					<td>语言模型的固有短板</td>
					<td>补充程序生成的可验证数据；更根本的办法是把计算交给代码，只把语义判断留给模型</td>
			</tr>
			<tr>
					<td>中文等非英语表现差</td>
					<td>训练数据以英文为主</td>
					<td>补充对应语言的数据；选多语言能力更强的底座；分语言看指标</td>
			</tr>
			<tr>
					<td>微调后零样本能力反而下降</td>
					<td>灾难性遗忘</td>
					<td>降低学习率和 LoRA rank；混入少量通用指令数据；对比微调前后在留出任务上的分数</td>
			</tr>
			<tr>
					<td>延迟太高</td>
					<td>底座太大，提示词太长</td>
					<td>换更小的底座；精简 state；开启前缀缓存；做 FP8 量化（H2O 实测 FP8 版本只少答对 3 道题，差异在统计上不显著）</td>
			</tr>
	</tbody>
</table>
<h3 id="3-让迭代形成闭环">3. 让迭代形成闭环</h3>
<p><img alt="训练、标定、评测、分析、补数据的迭代飞轮" loading="lazy" src="/zh/blog/2026/train-your-own-jev/6-eval-and-iterate.png"></p>
<p>最后是几条流程上的建议，它们比任何一个超参数都重要：</p>
<ol>
<li><strong>上线标准先写下来</strong>：比如“封存集上的去随机准确率不低于上一版，温度缩放后 ECE 不高于 0.05，选项倒序翻转率不高于 5%，p95 延迟不超过 100 ms”。先定标准再看结果，才能避免“看着结果改标准”。</li>
<li><strong>封存集只在发版时用一次</strong>：日常调参只看开发集和留出任务，封存集一旦被反复使用，它就变成了另一个开发集。</li>
<li><strong>每次只改一个变量</strong>：数据、超参数、底座同时改，就无法判断哪一个起了作用。</li>
<li><strong>固定版本，持续监控</strong>：底座或数据变化之后，温度和阈值都要重新标定；上线后持续记录概率分布并抽样复核，一旦分布漂移就回到飞轮的起点。</li>
</ol>
<h2 id="总结">总结</h2>
<p>把全文浓缩成几句话：</p>
<ol>
<li><strong>路线</strong>：想要 Jev 那样的零样本通才，走“解码器 + 选项字母”，从 Qwen3.5-4B + LoRA 起步；只服务固定任务、追求极致速度，编码器路线依然划算。</li>
<li><strong>目标</strong>：用严格适当评分规则训练，再用温度缩放校准。RLCD 是其中一种实现方式，而不是魔法。</li>
<li><strong>数据</strong>：任务的多样性、标签的质量、严格的去污染和按任务切分，比模型结构重要得多。</li>
<li><strong>算力</strong>：4B 模型的一次 LoRA 训练，单张 H100 几个小时、十几美元，钱主要花在一轮又一轮的迭代上。</li>
<li><strong>评测</strong>：准确率之外还要看校准、位置偏差、阈值曲线和延迟；用 JevBench 公开题对标，用自己的封存集验收。</li>
</ol>
<p>Jev 定义了“决策模型”这个品类，而短短一个月里，开源社区已经复刻出了上百个替代品，其中最好的一批在榜单上已经和 Jev 不相上下。训练一个自己的决策模型，已经从“能不能做到”变成了“值不值得做”。如果你有自己的数据、有合规要求，或者对延迟和成本格外敏感，答案多半是值得的。</p>
<h2 id="参考资料">参考资料</h2>
<ul>
<li><a href="https://benchmarkheaven.com/jev-models">JevBench：开源权重决策模型榜单</a></li>
<li><a href="https://benchmarkheaven.com/jev-models/methodology">JevBench：评测方法说明</a></li>
<li><a href="https://github.com/fstandhartinger/jevbench">JevBench：评测工具与公开题集（GitHub）</a></li>
<li><a href="https://huggingface.co/h2oai/h2o-lightning-4b">H2O-Lightning-4B 模型卡</a></li>
<li><a href="https://github.com/NandhaKishorM/laya">Laya GitHub 仓库</a></li>
<li><a href="https://dev.to/nandakishor_m_6cc0adfde9f/i-built-non-autoregressive-decision-models-a-year-ago-then-a-frontier-lab-called-it-a-18me">Nandakishor M：I Built Non-Autoregressive Decision Models a Year Ago. Then a Frontier Lab Called It a &ldquo;Breakthrough&rdquo;.</a></li>
<li><a href="https://huggingface.co/Qwen/Qwen3.5-4B">Qwen3.5-4B 模型卡</a></li>
<li><a href="https://doi.org/10.1198/016214506000001437">Gneiting &amp; Raftery：Strictly Proper Scoring Rules, Prediction, and Estimation（2007）</a></li>
<li><a href="https://arxiv.org/abs/1706.04599">Guo et al.：On Calibration of Modern Neural Networks（2017）</a></li>
<li><a href="https://arxiv.org/abs/2207.05221">Kadavath et al.：Language Models (Mostly) Know What They Know（2022）</a></li>
<li><a href="https://arxiv.org/abs/2309.03882">Zheng et al.：Large Language Models Are Not Robust Multiple Choice Selectors（2023）</a></li>
<li><a href="https://arxiv.org/abs/2109.01652">Wei et al.：Finetuned Language Models Are Zero-Shot Learners（2021）</a></li>
<li><a href="https://arxiv.org/abs/2106.09685">Hu et al.：LoRA: Low-Rank Adaptation of Large Language Models（2021）</a></li>
<li><a href="/zh/blog/2026/jev-vs-laya/">Jev vs Laya：闭源与开源 System One 决策模型的对比与选型</a></li>
<li><a href="/zh/blog/2026/microsoft-decision-1/">Microsoft-Decision-1 详解：微软入局决策模型，与 Jev、Laya 的三方对比</a></li>
<li><a href="/zh/blog/2025/llm-fine-tuning/">玩转大语言模型：大语言模型（LLM）微调主流方式和使用场景全面解析对比</a></li>
<li><a href="/zh/blog/2026/ppo-vs-grpo/">PPO vs GRPO：两种主流 LLM 强化学习范式的对比与选择</a></li>
</ul>
]]></content:encoded></item></channel></rss>