
踩坑实录微调 Laya 才发现数据质量碾压参数——决策数据构造的 5 个深坑把 Laya 当作普通 LLM 微调是第一次踩坑的开始。Laya 是一个多语言、非自回归的 System 1 决策引擎给它一段状态文本、邮件、工单或 JSON和一组 typed questionschoice/score/noul它在一次前向传播内返回类型化答案与经过校准的概率单题延迟约 33 ms不生成任何文本。社区里的热度集中在它的延迟与 ECE 指标上但真正动手微调之后你会发现延迟是架构给的准确率是数据给的。本文结合仓库源码与实测评测记录复盘决策数据构造阶段最容易踩的 5 个深坑——它们全部和数据有关和 LoRA 的r是多少、lora_alpha设多大无关。坑 1状态描述不完整——模型看到的根本不是你以为的状态Laya 的状态输入没有任何魔法。rl_common.py里的build_sequence把一次决策渲染成一条固定结构的序列[CLS] type instructions [SEP] [MASK] opt0 [MASK] opt1 ... [SEP] state [SEP]注意顺序选项和指令在最前面状态排在最后。状态占用的空间是max_len - head_max_len。以仓库根目录的英文 checkpointrl_agent_config.json为例{ encoder: answerdotai/ModernBERT-large, max_len: 512, head_max_len: 192, ... }512 token 的总预算里192 个 token 被问题头指令 选项吃掉留给状态的只有约 320 token。中文、印地语等非拉丁脚本经 BERT 分词后 token 密度更低实际能容纳的状态更短。email_utils.py的开头注释把这个问题说得非常直白Jev-style models lose accuracy on long, noisy state, and RL Agent reads at most max_len (512) tokens, so strip quoted replies, signatures and disclaimers in code before asking questions.也就是说仓库作者在生产环境里是先在代码里清洗邮件剥离引用的历史回复、签名、免责声明clean_email_body一套正则干完再喂给模型而不是指望模型自己从噪声里分辨关键信息。但很多微调者在构造数据时把状态当成了可以随意堆料的字段——原样贴整封邮件、整段对话、整个工单 JSON。更隐蔽的一个截断方向build_sequence在truncate_leftFalse时取st[:room]也就是从开头截断多轮对话走episode分支时truncate_leftTrue从开头保留、尾部截断。你在标注时看到的完整状态和模型实际看到的被截断的状态可能根本不是一个东西——你以为模型读到了退款金额和日期其实它只看到了邮件前 320 token 的问候语。解法构造数据时把状态当作模型视野上限来设计。先用build_sequence把每条状态 token 化确认关键决策信息落在有效窗口内长文本场景参考仓库 README 的做法显式传max_len8192给多语言 checkpoint并在自己的数据上单独验证长文档准确率仓库实测 4000 token 输入在 Apple GPU 上约 1.7 秒准确率随长度波动明显。坑 2动作空间不一致——选项顺序与措辞本身就是泄漏的信号决策模型的输出层是请求时动态定义的每个选项在序列里拥有自己的[MASK]token模型给所有 marker 打分后 softmax。这意味着同一道题选项怎么写、怎么排序会直接改变模型看到的输入。三个仓库源码里的实锤第一训练时选项会被随机打乱。encode_record里if train and rng is not None and q[t] ! score: rng.shuffle(order) ids, markers build_sequence(tok, rec[state], q, cfg[max_len], cfg[head_max_len], option_orderorder)score序数题不打乱因为序数选项自带语义顺序choice 题打乱防止模型学到第一个选项正确的位置捷径。反过来如果你的训练数据选项顺序恒定、而评测时顺序变了准确率会真实地掉——数据构造阶段就必须让选项顺序在训练中多样化。第二选项文本会被截断到 48 token。build_sequence里每个选项只取前 48 个 tokenopt_ids.append([tok.mask_token_id] tok( opts[i].replace(mask_tok, ), add_special_tokensFalse)[input_ids][:48])选项空间超预算时还会整体收缩opt_budget 16时每个选项压到max(4, (head_max_len - 16) // len(opt_ids))个 token。README 的诚实限制里给过一个极端案例Banking77 的 77 个选项在默认 256 token 的head_max_len下每个标签只有 3~4 个 token准确率直接跌到 0.425。选项越多每个选项的区分度预算越少——这就是为什么rl_agent_config.json里校准温度要按选项数量分桶temperature_by_options: { choice:2: 1.9063563346862793, choice:3-5: 1.7601518630981445, choice:6-10: 1.0000158548355103, choice:11: 0.10058280825614929, ... }选项数从 2 涨到 11温度从 1.9 跌到 0.1一个数量级的差距。这说明模型的打分行为在选项规模不同时是质变的——你的训练数据如果只在 2~3 个选项上抽样却指望它在 10 选项线上稳定评测时会摔得很惨。第三noul的选项是固定的false: .../true: ...。render_options硬编码了这个渲染方式。README 的 Honest Limits 明确指出这个 checkpoint 上noul可能跟着选项标签走——给出明确的 positive 输入却返回自信的 no。如果你在数据里把false标签当成随便填的占位模型学到的就是标签措辞而不是状态语义。作者建议的解法是把是非题改写为双选项choice并用中性 key{type: choice, instructions: Is this review positive?, criteria: {A: yes, the review is positive, B: no, the review is negative}}动作空间一致性的检查清单训练与评测的选项措辞一致、顺序策略一致、选项数量分布一致高基数 choice 题要么调大head_max_len要么拆成两级 coarse-to-fine而不是硬塞。坑 3边界样本缺失——模型没见过的分界就靠猜Laya 的训练奖励是严格适当的评分规则proper_rewardlog score spherical score序数题再加 ranked probability score。严格适当的含义是只有报出真实概率才能最大化期望奖励。这是 Laya 置信度可信的根基但也是对数据分布极其诚实的一面镜子——模型只在它见过的分布上校准。看仓库自带的评测数据eval/results.json里 in-task 与 zero-shot 的对比就一目了然指标in-task13 个任务族zero-shot4 个任务族准确率0.7530.651ECE0.0300.204Brier0.3080.532零样本任务族上准确率掉 0.1ECE 从 0.030 恶化为 0.204——置信度几乎失效了一个数量级。这就是边界样本缺失的代价任务族内样本再多也只能让模型在该族的高密度区域校准分界线上没见过的输入模型报出的高置信度是不可信的。更极端的证据在 eval/results.md 的任务族细分里。几个在训练内表现就差的族恰恰是决策边界最模糊的族conversation outcomes对话结果判断0.482response quality scoring回复质量打分0.581search relevance搜索相关性0.628sentiment and rating情感与评分0.442且 NLL 高达 3.545、ECE 0.438情感评分的 ECE 0.438 意味着模型的自信程度和真实正确率几乎脱节。如果你的业务恰好是这类主观性决策训练数据里不刻意补边界样本例如略带不满但未威胁、相关性一半一半的中间态模型在线上就会稳定输出高置信度的错误。解法把边界样本当成一等公民来收集而不是顺手标注。每个任务族里显式划出接近阈值的样本配额用encode_record里支持的soft目标target list(q[soft]) if q.get(soft) else one-hot给模糊样本打软标签让奖励信号反映不确定性本身。坑 4「理由」字段偷懒——你在训练一个不存在的字段这是决策数据构造里最隐蔽的一个坑也是社区实战帖反复提到理由合理性却很少有人讲清的地方。Laya 的决策原语是三类choice多选一、score序数打分、noul布尔判断。它不生成文本输出里没有解释字段——推理依据既不是模型的输出也不是训练目标。rl_agent_api.py的返回结构是{type, choice/score/noul, probabilities, confidence, rl_agent}仅此而已。但很多微调者受 LLM 习惯影响在构造数据时让标注员同时写决策 理由然后把理由拼进状态污染输入见坑 1把理由写进 instructions每道题的指令变成一段散文吃掉head_max_len预算直接挤压选项的 48 token 上限最糟的一种——把理由当成score的附加维度导致标注员在该打几分和理由写得够不够好之间摇摆目标本身不再干净。仓库训练逻辑里目标是每个选项上的概率分布target: [1.0 if i y else 0.0 ...]或 soft 分布奖励只对报出的概率 vs 真实标签评分。理由文本根本不在损失函数里——你在理由字段上花的每一分钟标注成本都不会直接变成梯度信号。它只能间接通过让标注员把题读懂来提升质量而这个效果完全可以用一份高质量标注规范替代。正确的做法把「理由」从数据里拿掉改成「标注规范」。每条指令写清判据参考email_questions里criteria的写法invoices, payments, refunds、bugs, outages, system errors让标注员只回答一个干净的决策目标。评测时若要审计理由合理性单独跑一个 LLM 评判器去打分不要混进决策模型的训练与评测管线——这正是坑 5 的主题。坑 5评测口径混乱——动作准确率与理由合理性必须分开看最后这个坑不在数据构造而在验收——但它决定你前面四个坑有没有白踩。Laya 评测体系里有两套完全正交的指标动作准确率答案 argmax 是否命中标签和校准质量ECE、Brier、NLL、置信度。仓库的 eval/results.md 把两者分开列是有原因的## In-task | task family | questions | accuracy | ECE | NLL | ... Overall: accuracy 0.753, ECE 0.030, Brier 0.308, accuracy at 50% coverage 0.947注意最后一项 accuracy at 50% coverage 0.947——在置信度最高的 50% 样本上准确率达到 0.947。这说明置信度门控confidence gating是这个模型唯一有实际信号的兜底手段。README 的 Honest Limits 里专门警告过action.act_probability字段对几乎所有输入都读 1.0原始 logits 与正确性的 AUROC 只有 0.30而confidence在同一批样本上 AUROC 达到 0.77。也就是说评测时必须用confidence做门控指标而不是用那个看起来更懂该不该自动执行的 act 概率。社区文章常把动作准确率和理由合理性混在一个总分里这是评测口径混乱的典型症状。两者应该分开验收动作准确率决策模型的唯一硬指标直接决定业务正确率校准质量ECE / Brier决定置信度门控能不能用能不能做低置信度转人工理由合理性如果业务要求可解释性单独用评判器审计与决策得分并列展示禁止加权合并成一个分数——一旦合并你无法区分决策准但理由差和决策差但理由像样两种完全不同的线上表现。仓库两张可靠性图正好给出这个口径的视觉证据in-task 评估eval/reliability_eval_in.png的校准曲线贴合对角线而 zero-shot 评估eval/reliability_eval_zs.png的校准明显偏离——同一模型、同一套指标仅仅因为评测集口径不同结论判若两人。回到数据为什么「数据质量碾压参数」不是一句口号仓库里有一个数字可以当注脚typed-decisions 基准400 个案例、2000 个决策、四条工作流上微调后的 checkpoint 准确率0.766而基座英文 checkpoint 只有0.362多语言基座0.342——后两者甚至低于 0.461 的逐题多数类基线。同一套权重微调与否差了 0.4 个点而 LoRA 参数从 r8 换到 r16 能带来的提升通常在一个点以内。这 0.4 个点的差距来自哪里来自 2000 个决策样本的构造质量状态是否完整、选项空间是否一致、边界样本是否覆盖、目标是否干净。微调 Laya 这件事参数配置一小时就能跑通数据构造才是决定上限的工程。坑 1 到坑 4 是数据侧的四个具体动作坑 5 是验收侧的口径纪律——把它们都做对0.766 这类数字才会轮到你。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考