微调之后,我的小型路由模型更擅长返回预期字段,却略微更不擅长输出合法 JSON。
在 576 条留出的合成请求上,严格全字段通过率从 7.6% 升到 47.2%,JSON 合法率却从 98.6% 降到 95.5%。提高总分的同一个适配器,也制造了更多严格解析器读不懂的回答。
我公开了 MLX 适配器和复现包、合成数据集,以及检查冻结输出的 Kaggle Notebook。这次实验值得留下的,是这个取舍,以及可以逐项检查它的证据。
任务:把请求变成一个小型路由决策
实验用 Qwen2.5-0.5B-Instruct 处理英文、中文和中英混合请求。输出契约覆盖笔记、日程、计算、需要澄清的请求和不支持的请求。
一次回答要通过好几层检查:能被严格解析为 JSON,符合允许的 schema,选对路由,并且所有字段值与预期完全一致。通过前一层,不保证通过后一层。解析器能读懂一个编造的路由,schema 校验器也可能放过一个类型正确、值却错误的字段。
基座模型和适配器的提示词都包含 schema,双方都不需要猜一个隐藏接口。
这是一项个人合成实验。不同划分的模板仍共享词汇,各语言版本也由同一个作者编写。它不能衡量开放领域助手的能力或生产路由系统的可靠性。
按模板家族划分,还不够
第一版先划分模板家族,再展开语言变体。这样避免了同一个例子的直接译文同时进入训练集和测试集,却留下了另一个问题:相关家族仍可能描述几乎相同的场景。
完整的日程请求和缺少时间的同类请求,应归为一组。家族编号不同,并不会让它们成为独立场景。
在 v2 中,我把 77 个家族归入 23 个明确的场景簇,纳入改写,以及信息完整和信息缺失的同类请求。先划分场景簇,再展开英文、中文和混合语言版本。最终得到 1,026 条训练数据、291 条验证数据和 576 条测试数据。测试集覆盖七个留出场景簇中的 23 个家族。
这些场景簇是有记录的人工判断,并不是语义独立性的证明。我在 v2 运行前冻结了划分清单,将 v1 保留为单独的历史。v1 和 v2 的测试分数不能直接比较。划分变化及审计见公开复核记录。
有一个结果影响很大:整个 percent_of 场景被分到了测试集,训练集从未教过模型这项运算。因此,计算成绩里包含了训练之外的探测,不能把它当成对熟悉计算任务的普通抽样。
结果取决于到底在算什么
冻结的 v2 测试得到以下四舍五入后的百分比:
| 检查项 | 基座模型 | LoRA 适配器 |
|---|---|---|
| 严格 JSON 合法 | 98.6% | 95.5% |
| 符合 schema | 40.6% | 95.0% |
| 路由正确 | 35.6% | 72.2% |
| 严格全字段匹配 | 7.6% | 47.2% |
| 跨语言一致且正确 | 2.6% | 43.1% |
基座模型通常能输出可解析的 JSON,却经常使用契约之外的字段或路由。适配器学会了更多接口约束,也更常选对路由;与此同时,可解析性退步了。
严格全字段匹配要求输出符合 schema、通过类型检查,并且值与预期完全相同。评分器拒绝重复键、非有限数值、Markdown 代码围栏和额外文字,也拒绝 Python 中容易混过去的类型关系:true 不算整数 1,3.0 也不能满足只接受整数的字段。
冻结评分器将操作数顺序作为标准答案契约的一部分。另设的 commutative_ok 指标用于诊断,不会悄悄取代主指标。在这次 v2 运行中,它没有挽回任何仅由操作数交换造成的失败。
验证集的严格准确率几乎没有变化:基座为 25.4%,适配器为 25.8%。测试集的大幅提升主要集中在笔记和日程。把 47.2% 的总通过率理解为普遍改善,会掩盖这种不均衡。
失败改变了下一轮实验的方向
适配器的严格计算成绩为零。面对留出的百分比请求,它输出了熟悉的 div 或 add 等运算。它学会了一些有用的路由模式,却没有通过这份训练集获得缺失的运算。
澄清和拒绝也很弱,两者的路由准确率约为 24% 和 47%。信息不足的请求,仍经常被当成可以直接执行的动作。即使总分看起来不错,这种行为在真实系统里也很要紧。
我最初把适配器的全部 26 条非法 JSON 输出归因于未见运算。后来一次追问促使我重新统计原始冻结预测,发现这个解释说过头了。更正后的分类如下:
| 标准任务 | 非法 JSON 输出数 |
|---|---|
使用 percent_of 的计算 |
13 |
| 缺少一个数值、需要澄清的百分比请求 | 10 |
| 信息不足、需要澄清的笔记请求 | 3 |
最后一组包含重复键或截断。测试中的其他家族没有产生非法 JSON。按语言计,英文八条、中文九条、混合语言九条。
这个更正会影响诊断。只给未见运算增加训练样本,不能解释信息不足的笔记请求为何失败。我会单独测试缺失信息的处理,并检查重复键输出,再决定改什么。
上述分类以仓库根目录更正后的复核记录为准。现有 experiment.zip 和 Kaggle 冻结包仍包含较早的复核文字,输出、权重和总体分数没有变化。这是一次重新计数,没有重新训练。
实际运行了什么
训练使用 MLX,在本地 Apple silicon 上完成。公开适配器采用 MLX-LM 格式,既不是独立模型,也不是 PEFT 适配器。
基座为 Qwen/Qwen2.5-0.5B-Instruct,固定修订为 7ae557604adf67be50417f59c2c2f167def9a775。LoRA rank 为 8、scale 为 20、层数为 16,学习率 1e-4,batch size 为 1,迭代 1,500 次,seed 为 42。我按声明使用最终 checkpoint,没有根据测试成绩挑选。
评测在新进程中重新加载保存的适配器。两组使用相同提示词、贪心解码和 96 token 输出上限。相同 seed 的第二次训练得到逐比特一致的适配器,没有重复评测。这验证了该设置下的可重复性,没有验证跨 seed 或跨环境的稳健性。
测试集只有七个场景簇。公开的按家族 bootstrap 估计中,严格准确率区间很宽:基座约为 1--16%,适配器约为 28--67%。576 条数据包含语言三元组和相关例子,不能把它们视为 576 个独立的真实场景。
两种不同的复现方式
如果只想检查评分、不运行模型,可以使用 Kaggle 冻结输出 Notebook。它读取保存的预测,检查冻结产物,并在 CPU 上重新计算分数。成功的云端运行证明了这条评分路径可行,没有证明云端训练或推理性能。
要在兼容的 Apple silicon 上重跑训练,请遵循模型仓库说明。下载仓库后,在仓库根目录解压 experiment.zip:
bash
unzip experiment.zip
cd reproduction
bash scripts/run_all.sh
脚本会安装固定版本的依赖、下载固定修订的基座、重新生成数据、检查评分器、训练、重新加载适配器,并评测两组。新运行的输出与公开的冻结输出分开保存。耗时和内存取决于机器。
代码、数据和适配器以 Apache-2.0 发布,附许可和 notice 文件,不重新分发基座权重。理解失败归因时,应同时阅读更正后的根目录复核记录和历史包。
接下来我会改什么
我会保留这份测试不动,另做实验研究它暴露的弱点:缺失信息的请求、不支持的请求,以及训练里没有出现的运算。我也会在同一批请求上比较普通解码和受约束输出,分别报告格式合规和字段正确性。
这些实验尚未运行,它们回答的问题也与这次发布不同。
这次适配器对既定路由契约的整体表现更好,却仍未通过任何严格留出计算测试,而且合法 JSON 更不稳定。在决定是否把它接到 API 后面之前,我希望这三个事实都摆在眼前。
作者:Renchao Wu | 原文:我的 LoRA 更会选路由了,JSON 却更不可靠了