研发手记
← 返回博客

Jev 启发的决策模型后训练:RTX 3090 实验记录

作者: • 20 分钟阅读
本文同时提供英文版本: 🌐 Read in English →

在 Agent 工作流里,有些调用只需要做一个判断:用户是在补充信息,还是要求修改之前的决定?一段材料是否支持某个结论?一条工单应该交给哪个团队?答案可能只有一个选项,却决定了程序接下来怎么走。

看到 TypeSafe 的 Jev 与 System One 模型介绍后,我对其中一种能力产生了兴趣:读取上下文、问题和候选答案,直接输出候选项的概率。听起来颇为复杂,我想弄清楚,它背后的基本结构和训练过程,能不能拆成个人开发者也能理解、动手验证的步骤。

实验使用之前跑本地编码 Agent 的 RTX 3090,以 Qwen3.5 Base 为预训练骨干,先用 0.8B、再用 4B 的骨干后训练决策模型。原始骨干权重保持冻结,监督后训练更新新建的 LoRA 参数和 pointer decision head。得到的模型读取上下文、问题及候选答案,输出每个候选项的概率。这两个规模的选择,也受到本地显存和计算资源的限制。这轮尝试主要是个人学习和探索:在手头资源内复现候选项打分的基本路径,理解模型怎样学会这种判断,以及该怎样检查结果。我没有把与企业级集群训练达到同等效果作为这轮实验的目标。

最初,我只想确认这条训练路径能不能工作。模型很快学会了那一小组训练题,但另一组题出现了更值得注意的结果:答对数增加了,平均 NLL 却变差了。 后来检查训练数据时,又发现有些标签来自评论星级,题目实际问的却是评论文本的态度。

这篇记录到最初两版模型的形成:怎样把语言模型改成候选项打分器,为什么准确率不能单独判断训练是否成功,以及怎样让标签与评估对应同一个问题。最后也会介绍 JevBench 公开题如何暴露新的短板,推动我们继续做质量优化。

模型如何读取问题并给候选项打分

以评论判断为例,下面的文本和概率都是虚构的接口示意:

上下文:这家店的食物很好,服务也周到,下次还会来。
问题:仅根据这段文字,作者整体上是否明确正面?
候选答案:是——整体态度明确正面;否——整体态度明确负面,或褒贬混合且没有明确的整体正面判断。
输出形式:{"是": 0.8, "否": 0.2}

候选答案及其说明是输入的一部分。换成工单分流时,可以提供“账单问题”“运输问题”“退货问题”等候选描述;输出仍然是各候选项的概率。

这轮采用的结构有三个部分:

  1. Qwen3.5 Base 主干模型读取输入。 上下文、问题和候选描述被编码成 token,预训练模型为这些位置计算上下文表示。
  2. Pointer decision head 为候选项打分。 这个小型读出模块取出决策位置和各候选位置的表示,投影后做匹配,得到每个候选项的分数,再通过 softmax 转成概率分布。
  3. LoRA 让模型适应判断任务。 冻结原始主干权重,只训练插入其中的低秩参数和新建的读出模块,减少需要优化的参数与优化器状态。

上下文、问题和候选描述进入 Qwen3.5;冻结的主干配合可训练 LoRA 生成表示,pointer head 将候选分数转换成概率。

训练时,带有确定标签的问题以正确候选项为监督目标,交叉熵损失鼓励模型提高这个选项的概率;梯度更新 LoRA 和读出模块。这就把“读文本、给候选项打分、根据标签学习”连了起来。

为什么选择这种结构?固定类别的分类头通常为每个类别保留一个输出位置;这里按请求中的候选描述打分,更符合我想研究的接口。直接读出分数,也省去了先生成自然语言答案、再解析标签的步骤。用现成语言模型提示生成标签,同样可以完成选择题;这轮探索的是直接打分的路径,还没有用对照实验比较两种方案的速度、成本或可靠性。

这轮参考了 Jared Palmer 的 KEV 训练流程和实现,从预训练 Base、新建 LoRA 参数和读出模块开始。结构来自参考实现;我们这阶段的主要改进在监督数据、评估隔离和断点恢复。

答对更多题为什么仍然不够

训练数据混合了分类、问答和规则组合等任务,Yelp 评论判断只是其中一项。第一步是做一个很小的训练检查:我从每个数据来源选最短的两条,共 24 条样本、30 道问题;一条样本可以包含多个问题。用 0.8B 模型反复训练这组数据,完成 80 次参数更新。

训练前后,我同时检查训练题和另一组用于探索性验证的题。后者来自相同的原始数据集,但属于不同的数据分组,没有直接作为这轮训练样本。

题目与指标训练前训练后
30 道训练题答对数7/3029/30
30 道训练题平均 NLL2.18450.1030
28 道验证题答对数7/2813/28
28 道验证题平均 NLL2.21533.9161

准确率只看概率最高的选项有没有选对。NLL,即负对数似然,还会检查模型给正确答案分配了多少概率;对有确定答案的单道题,它是 −ln(正确答案的概率),越低越好。

举一个说明指标的假设例子:二选一时,模型给正确答案 0.4 或 0.001,两次都会被记为答错。但后一种情况几乎把正确答案排除,NLL 的惩罚会大得多。因此,答对的题数增加,平均 NLL 仍然可能变差。这个小实验观察到了两项指标背离;汇总表本身还不能定位具体哪些题造成了变化。

训练题从 7/30 到 29/30,说明模型能够学会这组反复出现的数据。我还检查了主干权重保持不变、LoRA 和读出模块发生更新,以及保存再加载后的输出一致。它们一起验证了训练与保存路径。至于那 28 道验证题,样本太小、来源也有限,只适合用来发现问题。

如果程序要根据概率决定是否自动处理,还得进一步检查校准:例如模型报出约 80% 把握的一组判断,实际是否也大约答对 80%。softmax 只保证概率加起来为 1,NLL 也不能单独替代校准检查。这个实验让我开始同时看答对数和概率指标,而不是只看训练损失是否下降。

标签是否真的回答了题目

扩大训练前,我检查了混合数据中的 Yelp 评论任务。它原先根据星级阈值生成二元标签,但问题问的是:“仅根据这段评论文本,作者整体上是否明确正面?”

这两种标签不一定表达同一件事。假设有一条虚构评论:“上菜很快,但食物让我失望,整体不推荐。”再假设它附带四星评分,旧规则将这个星级映射为“是”。按星级规则,这个标签没有算错;按题目要求的文本标准,答案却应当是“否”。模型收到的是互相冲突的指令与监督。

我们把判断标准写清楚:只读提供的文本,整体明确正面才标“是”;整体明确负面,或褒贬混合且没有明确正面判断,标“否”。如果信息不足、文本截断或仍有实质歧义,则暂缓使用这道题。例如“还在等菜,之后再补评价”,不足以支持整体态度判断。

标注时隐藏原始星级和旧标签,用模型辅助标注,并交叉检查标签变化、疑点及一部分未变化样本。不确定是标注时的暂缓状态,不是这项二分类任务的第三个输出类别。 最终有 38 道相关问题暂不用于监督,所在样本及其他问题可以保留。

这项修改对我最有启发的地方是:格式正确的数据,也可能在教另一个任务。检查 JSON 格式、输入长度和标签范围,都发现不了这种错位;必须把题目、候选说明和标签放在一起读。更多训练轮次不能替我们解决任务定义的矛盾。

这里的“修复”是让监督更符合明确的文本标准。模型辅助标签仍可能有共同错误;后面的实验也没有单独测出这项修改带来的因果收益。这项审查发现同样不能直接解释前面小实验的 NLL 变化,两者之间尚未做归因验证。

让训练和验证使用同一个判断标准

除 Yelp 标签问题外,结构检查、重叠筛查和抽样语义检查还隔离了 56 条完整样本,涉及近似拷贝风险、未解决的歧义和标签冲突。合入 Yelp 修复后,正式训练使用 12,520 条样本、15,476 道问题。每个版本单独保存,并记录校验值,避免拿中间数据版本解释最终模型。

评估也要跟着任务定义调整。旧的星级标签回答不了新问题,所以我们另建了使用相同文本标准的开发验证集:从 200 条 Yelp 候选评论开始,做历史材料重叠筛查,再进行两轮模型辅助审查;194 条被纳入,6 条因歧义或分歧暂缓纳入。

开发验证集用于观察模型、比较方案;校准数据用于拟合概率调整参数;最终测试则用于方案定下后的确认。这 194 条只承担第一种用途。训练入口通过允许的数据来源清单,明确拒绝把它们当训练数据使用。

这样的隔离让比较更有意义,但筛查不能排除所有语义重叠或预训练接触。两轮模型辅助审查也不等于独立人工标准答案;暂缓纳入难例,会让分数少覆盖一部分困难情况。读后面的 Yelp 结果时,需要记住这个范围。

两个训练版本的结果能说明什么

修复后,我从 Base 分别重新训练 0.8B 和 4B,记作 B1 和 B2。两者都新建 LoRA 参数和读出模块,使用同一份修复数据,训练两轮;学习率分别为 0.0001 和 0.00005。它们是两套训练配方的比较,不能单独估计“参数变大”带来的收益。

训练 4B 前,先在 3090 上实测代表性输入和压力输入的前向、反向及参数更新。LoRA 减少了训练参数,仍然要为主干权重、中间激活和输入长度留下显存;能加载模型只是容量检查的一部分。

评估使用两组开发验证数据:一组是刚才的 194 道文本态度判断题;另一组是原有的多任务验证数据,覆盖分类、问答和规则组合,用来观察这些任务上的表现。第二组表中只统计 1,184 道原始题目,另外 204 道选项重排等构造变体未计入此表。

开发验证数据0.8B(B1)答对数4B(B2)答对数B1 平均 NLLB2 平均 NLL
Yelp 文本态度判断184/194192/1940.096200.04024
原有多任务的原始题目951/1,1841,033/1,1840.601330.46749

这些是未做温度校准的输出,温度参数为 1。B2 在表中的答对数和平均 NLL 上都更好。这里比较的是两个训练后的版本;没有相同面板上的未训练对照,不能据此计算微调本身带来了多大提升,也不能证明原有通用能力没有遗忘。

再看一个更贴近自动处理风险的切面:在 Yelp 这组题里,B1 给出至少 0.9 概率的预测共 181 道,其中错 1 道;B2 共 188 道,其中错 2 道。样本很小,覆盖的题也不完全相同,不能把这个差异当成显著结论。但它提醒我:平均指标改善,并不自动保证高置信错误一起减少,0.9 也不能直接作为生产阈值。

长训练还需要能真正续训

从小实验转向长训练,还补上了一项工程能力:断点恢复。只加载 LoRA 和读出模块的权重,可以继续做预测;要继续同一次训练,还要恢复优化器、学习率进度、随机状态和数据读取位置。

我们用“连续训练”和“中途终止进程后恢复”做对照。调整实际 CUDA 路径的确定性设置后,两边各完成 8 次参数更新,训练参数和训练状态逐位一致,重新加载后 30 道检查题的概率也完全一致。

这验证了当时单 GPU 环境下的进程中断恢复路径。它给后续迭代提供了可恢复的起点,验证范围不包括换硬件重训或物理断电。更细的保存与对照方法放在文末。

JevBench 暴露了下一阶段的问题

训练、保存和加载都能工作,原有开发验证集上的表现也更好了。接下来,我想知道:换一组更接近实际决策的题,模型还能做对多少?于是,我们把未继续优化的 4B 版本放到 JevBench 的固定公开版本上做了一次本地评估。

这次使用的是该版本实际可下载的 231 道公开题。全部请求都成功返回有效概率,没有截断输入;结果按题目所属分组拆开后,差距很明显:

JevBench 公开题分组答对数
Easy48/48
Original69/72
Hard53/111

总共答对 170/231,但 61 道错题中,58 道来自 Hard。再按题型细分,时间与数值计算题为 0/15,长政策判断题为 4/19。对简单题能做出判断,不代表遇到日期计算、多个条件、例外条款和分散在长文本里的证据时,仍然可靠。只看总分,会把这些短板藏起来。

这也给后续训练提供了更具体的方向。先逐题检查任务转换、选项和标签,确认模型到底错在哪里;再补充答案可以独立校验的日期计算、规则组合与例外判断数据,逐步增加需要处理的上下文长度。同时保留原有任务的数据与评估,观察补强困难题时,是否损失了原先能做对的判断。

长度是需要检查的因素之一:原始 4B 训练配置把上下文限制在 384 token,而这组长政策题的上下文约为 2,203–3,666 token。不过,较长输入也都属于 Hard,长度和任务难度混在一起,不能据此认定失败只是因为文本变长。这些小题型分组的标签和转换也仍需逐题审查。

这里报告的是本地公开题诊断,不是官方封闭评测或榜单名次;不同题型之间的分数差距,也不等于训练造成了能力退步。公开题已经被查看,后续可以用来追踪已知问题,但改善能否泛化,还需要独立保留的数据来确认。质量优化目前仍在继续,后续文章会围绕这些短板,记录哪些训练改动有效、哪些没有,以及是否保住了其他任务的表现。

模型下载与数据复现

这轮的模型和配套材料已发布到 Hugging Face:

模型仓库包含 LoRA adapter、决策读出模块、原生加载脚本和训练配置,加载时会另外获取固定版本的 Qwen Base。它们是用于推理的 checkpoint,不包含优化器、随机状态等续训快照;请按仓库说明加载,以免漏掉决策读出模块。

数据仓库提供来源索引、标签修复、重建脚本和训练复现包,不直接分发原始评论或问答文本。重建脚本会获取固定版本的上游数据,并核对最终文件的 SHA-256;训练与各评估集的用途分别记录。数据来源和各组件的使用条款也保留在仓库中。

数据版本与评估范围

一条样本可以包含多个问题。数据版本分别是:

数据阶段样本数问题数
最初通过输入检查的训练数据12,57615,576
剔除有问题样本后的临时数据12,52015,514
合入 Yelp 标签修复后的 B1/B2 训练数据12,52015,476

隔离的 56 条样本中,6 条涉及公开数据不同划分之间的近似拷贝风险,39 条存在未解决的歧义,11 条存在审查发现的标签冲突。15,514 到 15,476 的变化来自暂缓监督的 38 道 Yelp 判断题,保留样本数不变。这些检查减少了已发现的风险,不能估计整个数据集的标签噪声比例。

训练文件使用独立校验值和数据说明文件。曾有一个复制来的说明文件仍然描述旧数据,在正式启动训练前被替换。开发验证数据另有仅限评估的声明,训练入口也检查允许的数据来源;没有允许清单的旧入口不能用作隔离保障。

原有多任务验证数据在记录中称为 retention panel,总共 1,388 道问题。表中采用 1,184 道原始题目(评分记录中的 clean);另外 204 道包括 84 道选项重排变体、60 道正确答案在候选项中的 none 对照,以及 60 道正确答案不在候选项中的 none 对照。这里的 none 表示“以上皆非”,这些变体没有计入表中的答对数与平均 NLL。历史标签和数据重叠仍有限制,不能据此声称通过了独立外部测试。

训练配置与断点恢复
配置0.8B(B1)4B(B2)
预训练模型Qwen3.5-0.8B-BaseQwen3.5-4B-Base
初始化新 LoRA 参数和读出模块新 LoRA 参数和读出模块
训练样本12,520同一文件,字节一致
训练轮次与参数更新数2;3,1302;3,130
每批样本数与梯度累积步数1;81;8
LoRA rank1616
学习率0.00010.00005
主干权重存储与混合精度计算FP32;BF16FP32;BF16
每次计算的 token 预算4,0968,192
随机种子33

原始比较记录确认:4B 这次实际接纳的输入在 4,096 预算内已能容纳,8,192 没有改变实际分批方式。每套配方只有这次训练运行,不能用它估计不同训练随机种子的波动。最早的小实验每批 1 条样本、累积 4 步,完成 80 次更新。

训练时,一条样本可以产生加入干扰候选项或“以上皆非”对照等增强版本。训练损失先在每个版本内部对问题求平均,再按版本权重累加。结果表的平均 NLL 则按入选的原始问题统计,两者的聚合单位不能直接混用。早于 B1 的完整数据 0.8B 版本称为 B0;为避免延续其旧监督历史,这里选取从 Base 重新训练的 B1。

恢复点除训练参数外,保存优化器状态、学习率调度器、随机状态、计数器和下一批数据位置。只在完整参数更新后保存,不把未完成的梯度累积当成已完成更新;多个带校验的保存版本可用于回退。第一次 CUDA 恢复对照出现小幅数值漂移,启用实际路径的确定性设置后,连续与恢复运行的参数、优化器、调度器、随机状态和数据位置一致。

实现与测量来源

本地训练以 Jared Palmer 的 KEV 为起点,采用固定版本 commit 2ecafc49fe1e8945cd600443e6c04c1cf0b3a78f,增加本地断点恢复支持,没有加载已训练好的 KEV adapter。预训练模型来自 Qwen3.5-0.8B-Base 与 Qwen3.5-4B-Base。LoRA 方法见 Hu 等人的论文;概率校准与温度缩放的背景可参阅 Guo 等人的论文。

Qwen3.5 的混合主干在这条实现中,把每个问题与其上下文组成独立的因果序列,避免把其他问题当作上下文。批处理不等于复现了 Jev 的架构或延迟。小实验使用实际验证过的参考 PyTorch 计算路径。

另附选定的聚合测量,便于核对文中的计数和配置。它只包含汇总值;模型、数据重建和训练复现材料见上方 Hugging Face 仓库。

本文由 AI 辅助,根据本地实验记录整理。范围包括最初 B1/B2、对应的开发验证结果,以及用于引出后续优化的 4B JevBench 公开题诊断。

分享文章

微信扫一扫分享

用微信扫描上方二维码,即可在手机端阅读或分享到朋友圈。

链接已复制到剪贴板!