原创教学案例 · 最近核验:2026-10-10。下文的回答、通过率和日志都是教学设定,不是模型实测。代码只复现数据处理,不调用模型。

跑完训练,最想看的通常是“涨了多少”。但总体从 60% 到 70%,还没告诉我们:哪些问题变好了?哪些原来能做的反而做错了?是不是评测时多给了一段提示?

这篇按一次实验的顺序往下走:保存设置,比较新旧结果,打开错例,决定下一步改哪里。只想学怎么排错,可以直接看“资料找到了,为什么还答错?”;准备跑实验,再回来读前面的记录和配对比较。

先把实验设置存下来

先不用搭复杂的平台。把模型、数据和评估设置一起保存,之后才知道两次实验差在哪里。下面是一份简化的实验清单(manifest):

{
  "run_id": "citation-sft-demo-01",
  "base_revision": "local-fixture-v1",
  "data_snapshot": "examples-v3",
  "split_rule": "question-family-v1",
  "template_revision": "chat-template-v2",
  "objective": "assistant-target-token-mean",
  "eval_snapshot": "fixed-evidence-v2",
  "decode": {"sampling": false, "max_new_tokens": 128}
}

这是字段示意,字符串不是可下载的真实 checkpoint。正式运行还要存实际内容 hash、依赖版本、随机种子、优化器配置、训练步数和有效 token 数。实验 ID 是索引,不能代替内容快照。

如果 A 用了 128 个输出 token,B 用了 512 个,比较仍有意义,但回答的是“两个不同预算系统谁更好”,不是纯粹的训练收益。

总分涨了,哪些题变好了?

1 表示通过事先定义的检查,0 表示未通过。同一道题的新旧结果放在同一行:

问题 分组 旧模型 新模型 变化
q1 有证据 0 1 改善
q2 有证据 0 1 改善
q3 有证据 1 1 不变
q4 有证据 1 1 不变
q5 缺证据 1 0 回退
q6 缺证据 1 1 不变

总体从 4/6 到 5/6,但看 q5:原来没资料时会说明情况,现在反而处理错了。缺证据这组从 2/2 降到 1/2。新模型可能更会回答,也可能只是更愿意回答。需要打开 q5 的具体输出,再检查训练中缺证据的示范是否足够、是否写得合适。

配对比较(paired comparison)保留同一道题的变化。这里两胜一负三平;六道题远不足以估计稳定提升,只适合说明怎么发现回退。真实比较还要报告样本量和不确定性;问题来自同一用户或文档时,重采样也要考虑组内相关性(within-group correlation)。

资料找到了,为什么还答错?

假设有人问:“北馆周日几点闭馆?”知识库写着周日 18:00、工作日 20:00。助手却答了 20:00。看到这一条,很容易想“它没理解周日,要不要补些训练数据?”先别急,把这次请求展开看:

一条虚构的排错记录:检索命中,不代表模型最后看到了那段资料。
  1. 01 / 检索结果两段都找到了工作日:20:00
    周日:18:00
  2. 02 / 最终输入只留下工作日那段它排在前面,占完了证据预算。周日那段没有放进 prompt。
  3. 03 / 示意回答“周日 20:00 闭馆。”答案错了,但不能仅凭这一句认定模型学不会区分日期。

这条记录先暴露的是证据拼接的问题:检索(retrieval)日志里有正确段落,最终输入里却没有。模型在缺少周日资料时还给出确定答案,也是需要测的行为,但不能拿这次结果证明“给了正确资料也不会用”。两种问题可能同时存在,要分开查。

最值得保存的不是一张错误答案截图,而是这条请求经过的几个版本:原始问题、命中的文档与 revision、最终拼好的输入、实际输出、判错依据。部署支持时再留 tokenizer 后的 token IDs、截断位置、生成终止原因和工具状态。涉及用户数据的日志要做权限控制、脱敏和保留期限管理,不要原样发到公共 issue。

只改一处,看看问题跟着哪一步走

固定模型和解码设置,先做几个小对照。下表是实验计划,不是假装已经测出的结果:

这次只改什么 如果现象变了,说明什么 还不能说明什么
同样的预算下,手动放入正确的周日资料 值得继续查证据选择与拼接 真实检索已经变好了,或所有问题都能答对
不改输入,改评测中的过期标准答案 原来至少有一部分“错误”来自评测 模型质量有提升
给正确资料,再改聊天模板 值得检查训练与服务的输入格式差异 换任何模板都会更好
保持模板、资料、预算不变,只换 checkpoint 在这些条件下比较模型差异 收益一定来自某个训练技巧

手动提供正确资料,常叫 oracle context 对照。它帮助判断“如果上游交对了,后面能不能做好”,不能混进端到端成绩里。即使这次答对了,也要用更多问题重跑;如果仍然错,也别立刻宣布是能力上限,模板、指令冲突和判题标准都还没排除。

有采样时,要保存多次结果,不能挑一次答对的截图。即使用 greedy decoding,也要固定版本和运行条件;跨硬件、算子或服务更新不保证逐位复现。一次只改一项便于找线索,最后仍要测组合后的完整系统。

用几行代码看“检索命中,但没进输入”

下面复用本站 RAG 项目的 pack_evidence。它把英文按空格分词,再按词数预算,只是方便手算的教学实现,不是 tokenizer。两段各有 7 个词,预算只能放下一段;这种计数也不适用于没有空格分词的中文。正式代码应按真实 token 计数,并给指令、问题、角色标记及输出留空间。

在仓库根目录运行:

from pathlib import Path
import sys

sys.path.insert(0, str(Path("practice/rag/code").resolve()))
from evidence_pipeline import Chunk, pack_evidence

weekday = Chunk(
    "weekday", "hours", 1,
    "North Hall closes at 20:00 on weekdays.",
    frozenset({"public"}),
)
sunday = Chunk(
    "sunday", "hours", 1,
    "North Hall closes at 18:00 on Sundays.",
    frozenset({"public"}),
)
retrieved = [weekday, sunday]
provided, used = pack_evidence(retrieved, budget=7)
oracle, oracle_used = pack_evidence([sunday, weekday], budget=7)

assert sunday in retrieved and sunday not in provided
assert sunday in oracle
assert used == oracle_used == 7
print("retrieved:", [chunk.chunk_id for chunk in retrieved])
print("provided:", [chunk.chunk_id for chunk in provided])
print("oracle:", [chunk.chunk_id for chunk in oracle])

输出依次是 ['weekday', 'sunday']、['weekday']、['sunday']。这里只证明了预算和顺序会影响输入;代码没有生成答案,也没有证明手动换顺序能提高模型准确率。自动选择哪段更相关,还要单独验证。

真要训练,先看它到底学到了哪些 token

回到 q5 那种“没资料也硬答”的错误。先翻一小批真实训练样本,而不只是配置文件:缺资料时,示范有没有说明“不知道”?同一种问题有没有互相矛盾的标签?这些回答经过模板和截断后,还剩哪些位置在算 loss?

比如你本来想教的是“资料里没有周日时间,暂时无法确认”,但长输入把回答截掉了,剩下的有效 target 为零。配置里写了 SFT,模型却没从这条样本学到那句回答。另一种情况是 mask 用反了:loss 在用户问题上下降,日志很好看,真正想教的回答却没有参与训练。

先解码一条 batch,把 input、target、loss mask 和有效 target 数量放在一起看。causal LM 的 labels 与 logits 通常还要错开一位,计数按实际参与 loss 的位置算。这里的项目以 assistant 回答为监督目标;不是说所有预训练(pre-training)或微调(fine-tuning)都必须采用相同 mask。

TRL v0.29.0 的说明要求相应聊天模板能返回 assistant mask,assistant_only_loss=True 才有适用基础。参数名字不是检查结果,最终 batch 才是。细节见数据到 batch和mask 与 loss 分母。

loss 降了,生成却没好,接着查什么?

先用少量、无冲突的示范做 tiny overfit,检查可训练参数、非零有限梯度和更新前后权重。连这些示范都学不下来,就先查目标、优化和实现。能记下来后,再看未见过的问题;这两步回答的是不同问题。

  • 训练能接着正确前文写,自己生成就跑偏。 Teacher forcing 给了正确的历史 token,生成时前文可能已经错了。既看 held-out loss,也看完整生成,不把两者当一项指标。
  • 本地能用,部署后不对。 检查加载的基座、adapter、tokenizer、模板和停止条件是不是同一套;不要拿本地 checkpoint 的名字代替服务端 revision。
  • 总是提前结束或重复。 看 EOS、终止原因、最大输出长度和真实上下文,再比较解码设置。提高 temperature 可能增加变化,但不会修复丢掉的证据或错误的标签。

一个常见的小坑是先渲染 chat template,再次分词时又自动加了特殊 token。Transformers 的模板文档专门提醒过这种重复。查这些便宜的小问题,通常比立刻换训练方法更有方向。

改哪里,要由错例来决定

排查不是必须按“prompt → SFT → RL”通关。已经确认是文档过期,就更新文档;已经确认是重复执行工具,就修执行逻辑。训练不会替其他部件补上它们该做的事。

找到的原因 合适的下一步 代价和容易忽略的地方
资料缺失、过期或被丢掉 更新版本选择、检索、重排、上下文预算 增加 top-k 会花更多预算,也可能带入干扰
有资料,但格式或行为经常不合要求 比较清晰提示、格式约束;必要时补 SFT 示范 JSON 合法不代表事实正确;多教拒答也可能误拒正常问题
两份回答都合理,偏好却不稳定 先统一标注标准,再考虑偏好学习(preference learning) DPO 需要可信的比较,不是把任意错误都塞进 rejected
多步决策有可验证的成功标准 先修环境和工具,再比较 SFT / RL rollout 成本、奖励(reward)漏洞和失败恢复都要算进去
输入、实现都核对过,任务仍超出能力 比较更合适的模型、工具或任务拆解 更大模型未必解决;工具也增加调用和权限风险

选择 LoRA、QLoRA 还是全参更新,则是另一层取舍。不能因为“错误很严重”就推导出“必须全参”;LoRA 原论文在其测试任务上就展示了与全参有竞争力的结果,但也不是所有任务都保证一样好。固定数据和调参预算,再比较关键分组与资源成本,见训练方案。

前后置检查也不是万能补丁:规则快、好检查,但覆盖面有限;再加一个模型更灵活,也会带来延迟、漏判和误拒。对实际执行操作的权限,应该由工具或服务端可靠地校验,而不是只让另一个模型说“安全”。

修好这一题,还要试试它的邻居

“周日 18:00”这条能答对后,换一种问法还行吗?只改一个条件,答案会跟着变吗?下面借用 CheckList(ACL 2020)的行为测试思路,例子是我们自己写的:

怎么试 北馆例子 检查什么
最小功能测试(MFT) 只给周日规则,直接问闭馆时间 最简单的情况是否能处理
不变性测试(INV) 同时把问题和资料中的“北馆”换成“南馆”,其他规则不变 时间仍应为 18:00;引用 ID 可以相应改变
定向变化测试(DIR) 资料里两种规则都在,只把问题的“周日”换成“工作日” 时间应从 18:00 变成 20:00

再补缺资料、规则冲突和无关长文本,检查模型能否区分“该回答”与“该保留判断”。这不是越多改写越好:没有变化含义的十种措辞,未必比一个关键条件变化更有用。

修复用过的错例放进回归集,确保以后不再犯;它们已经被开发过程看过,不能再当成独立测试结果。同一文档或问题模板的近似改写要按组划分。最终效果另用未参与调参的样本验证,不能把“这道背熟了”写成“泛化提高了”。

排优先级时,也别直接拿手里错例的数量当线上发生率。十个投诉不等于随机抽样的十个请求。可以在有分母的样本上看频率,再结合影响、修复成本和证据是否充分;权限越界等严重问题不应只靠平均收益决定要不要修。

没跑完的题,单独记录

旧模型跑了 100 道,新模型只完成 80 道,直接对各自成功返回的部分取均值可能有选择偏差。我们的小程序遇到缺失 ID 会报错,要求先处理不完整的配对。

实际评测可以给每题保留 ok / timeout / invalid_output 等状态,再分两种角度报告:

  • 用户能否得到可用结果? 用全部请求作分母,超时、格式错误都影响成功率。
  • 两者都正常完成时,哪个答得更好? 比较双方均有有效输出的题,同时列出排除了多少题、为什么排除。这只是这个子集的表现,不能直接推广到所有请求。

两种角度都需要。超时不等于知识答错,但也不能在报告中消失;若任务明确要求 JSON,输出格式不合规本身也可以算一次任务失败。

python3 practice/post-training/code/experiment_checks.py

代码会打印上面六道题的总体和分组结果。删掉一条新模型结果,会得到缺失配对错误;把一个问题族同时放进 train 和 test,会得到泄漏错误。

先小跑,再扩大训练

阶段 先检查什么 过关后才做什么
单条 / 单 batch token、mask、loss、梯度有限 极小数据训练
小数据 是否能学会几个明确示范;缺证据行为是否被破坏 扩大数据与训练时长
保存恢复 连续跑与恢复跑的下一步更新对照、优化器/调度器与必要随机状态 长训练与中断恢复
完整离线评估 固定协议、分组回退、资源代价 有明确回滚方案的受控发布

小数据上学不会,先排查输入、目标和实现,不要马上再加更多卡。反过来,小数据上背下来了也不证明能泛化;那只是训练路径的一次检查。

具体做法见训练选型、多卡与有效 batch和断点恢复对照。不要只记录“用了几张卡”:实际有效 target token、端到端耗时、显存峰值和失败重跑的代价也属于实验结果。

如果只能离线测,就停在离线结论。可以设计 user simulation 做压力测试,但模拟用户不能替真实用户证明产品收益。

换模型时,别漏掉模板和配置

除了模型权重,还包括 tokenizer、模板、生成参数、输入预处理和工具/输出 schema。只回滚权重而保留新模板,未必回到了旧系统。

先让新版本在不影响用户的 shadow 请求上跑同样输入,检查格式、延迟、错误类型;再按场景决定是否做受控在线验证。shadow 没有改变用户看到的内容,因此不能测出用户行为收益。预先写清停止条件,例如关键分组明显回退、输出格式错误超限或延迟超过产品预算。阈值由业务风险决定,不从这张六题表里拍出来。

如果关键问题还没解决,这轮就先保留旧版,把下一步要验证的假设写下来。实验的价值不只在于分数上涨,也在于弄清楚哪种改动值得继续做。

返回项目导读 · 对照推荐系统的发布路径

本页术语 7
实验清单 manifest
记录一次实验所用数据、模型、代码版本和配置的文件
组内相关性 within-group correlation
同一用户、文档等分组内的观测可能相关,不能默认彼此独立
检索 retrieval
从外部语料里找出相关内容
预训练 pre-training
先用大量数据学习通用表示和能力,再适配具体任务
微调 fine-tuning
在预训练权重上继续训练以适配具体任务
偏好学习 preference learning
从「A 比 B 好」这类比较里学习
奖励 reward
RL 里指导策略更新的标量信号