60 条样本查出 6 条有问题,可用 54 条(90.0%)。 六类错误里 5 类能自动查出来(结构、首尾角色、空回答、重复、长度), 第六类「答非所问」只能人看。 最长一条 562 字,占全部字数的 11.7%——长度这项要看最大值,不是平均值。
上一节确认了「该做微调」。这一节是动手前的最后一道关:把数据整理成平台能直接吃的样子。
一、一份能训的数据长什么样
绝大多数微调平台吃 jsonl:一行一个 JSON,每行一条样本。样本里是一个 messages 数组, 按对话顺序排:
{"messages": [
{"role": "system", "content": "你是客服助手。"},
{"role": "user", "content": "这个能退吗?"},
{"role": "assistant", "content": "可以退,请提供订单号。"}
]}
五条硬要求:结构统一(每条都是 messages 数组)、首条 system 末条 assistant、内容非空、 没有完全重复、长度在上下文窗口内。 第一条和第二条是平台强制的,违反了直接报错。后三条平台不查,但它们才是真正让模型学歪的原因。 为什么必须「末条 assistant」?因为训练时只有 assistant 那段参与算损失。 末条是 user 的样本,等于给了一个没有答案的问题——有的平台会静默截断, 有的会把它当成「教模型继续提问」。
二、六类错误,五种能自动查
拿 60 条样本灌进去,查出 6 条有问题:
| 问题类型 | 条数 | 能不能自动查 |
|---|---|---|
| 不是 messages 结构 | 1 | 能 |
| 首条不是 system | 1 | 能 |
| 末条不是 assistant | 2 | 能 |
| 回答为空 | 1 | 能 |
| 完全重复 | 1 | 能 |
| 答非所问 | 0(查不出) | 不能,只能人看 |
| 前五类都是结构问题,一个脚本跑一遍就够。第六类是内容问题:问「能退吗」答「请稍等」, | ||
| 格式完全合法,机器判断不了——只能抽检。 | ||
| 所以正确的分工是:自动校验拦住格式错,人工抽检拦住内容错。少了任何一半,数据都会带着病进训练。 | ||
| 完全重复那一类值得单独说:重复样本在训练里等于被反复加权,效果等同于悄悄调大了学习率, | ||
| 而且会让模型对那几句话过拟合。查法很简单,把 messages 序列化成字符串去重就行。 |
三、长度和占比:重点看最大值
Demo 里 54 条可用样本,长度中位 69 字,但最长一条 562 字,一条就占了全部字数的 11.7%。 这一条是怎么来的?一份正常的三轮对话里,user 那一段被贴进了整篇退换货条款。 这在真实数据里非常常见——用户把整段合同复制进对话框。 超长样本有三个后果:拉高平均成本(训练按 token 计费)、 可能被窗口截断(截断后答案对不上问题)、梯度被它主导(一条顶几十条正常样本)。 处理办法不是简单删掉,而是先看它为什么长:如果是用户真会这样问,就该拆成多轮; 如果是复制粘贴的垃圾,直接删。判据是「这条样本代表真实使用场景吗」。
四、抽检怎么做才有效
按比例抽,别只抽前面的。 前 10 条往往是精挑细选的样例,问题都藏在后面。 按条数均匀抽 5% 到 10%。 抽检要带问题清单,不是随便读。 四个问题够用:答案对不对、有没有答非所问、有没有编造事实、 口吻是否符合要求。 抽到问题要回查同类。 发现一条「答非所问」, 就去搜同一个人标的所有样本——错误往往是成批出现的,不是随机的。 最后一条纪律:抽检记录要留。 谁抽的、抽了多少、发现几条、怎么处理的。 下次数据换了一批,这份记录就是对比的基准。
五、动手 Demo:60 条查出 6 条问题:五种能自动查,第六种只能人看
造 60 条客服样本,往里塞六类典型错误(结构不对、首条角色不对、末条角色不对、空回答、完全重复、 超长),然后逐条跑五项自动检查,统计每类问题几条、可用多少条。 第六类「答非所问」故意不塞——它本来就不是脚本能查的。
for ln in open(path, encoding='utf-8'):
m = json.loads(ln).get('messages')
if not isinstance(m, list):
err('不是 messages 结构')
elif m[0].get('role') != 'system':
err('首条不是 system')
elif m[-1].get('role') != 'assistant':
err('末条不是 assistant')
elif not m[-1].get('content', '').strip():
err('回答为空')
elif key(m) in seen:
err('完全重复')
else:
seen.add(key(m))
lens.append(sum(len(x['content']) for x in m))
实跑输出:
--------------------------------------------------------------------
训练数据规范:六类格式错误,五种能自动查出来
--------------------------------------------------------------------
总条数 60
查出问题 6 条
可用 54 条(90.0%)
问题类型 条数
------------------------
末条不是 assistant 2
不是 messages 结构 1
首条不是 system 1
回答为空 1
完全重复 1
六类错误里,**五种能自动查出来**(上面这些)。第六种是「回答答非所问」,机器查不出来,只能人看。
所以抽检不能省:**自动校验拦住格式错,人工抽检拦住内容错。**
样本长度 中位 69 字
样本长度 最大 562 字
最长一条占比 11.7%(占全部字数)
估算 token 总数 约 0.7 万
**长度这一项要特别看最大值**:最长那一条就吃掉全部字数的一成多,它还会顶到上下文窗口边上被截断。
一份能直接拿去训的数据,要满足五条:
① 结构统一 每条都是 messages 数组,role 只有三种
② 首 system 末 assistant 这是绝大多数微调平台的要求
③ 内容非空 空回答等于教模型「可以不答」
④ 没有完全重复 重复样本会被反复加权,等于变相调大学习率
⑤ 长度在窗口内 超长的直接截断或剔除,别赌模型能接住
输出第一屏:60 条查出 6 条,可用 54 条(90.0%)。六条分布在五类错误上,
其中「末条不是 assistant」有 2 条。
注意这里的检查是 elif 串联的:一条样本只记一次错,不重复计数。真实项目里也可以改成「全查」,
那样能一次列出所有毛病,但统计口径要跟着改,否则「问题条数」会大于「坏样本数」。
第二屏是长度:中位 69 字、最大 562 字、占比 11.7%。一条样本吃掉一成多的字数,
它在训练里的权重相当于十几条正常样本。
第三屏的五条要求里,④「没有完全重复」最容易被忽略。
很多数据集是分批攒出来的,同一批对话被导过两次,去重前看不出来——但训练时那几句话会被学两遍。
诚实说明:这 60 条和那六类错误都是我按真实数据的毛病造的,比例(90% 可用)也是照着常见水平设的。
要看的是检查逻辑和统计口径,不是这几个数。
30 秒实验
- 把「完全重复」那条改成「同一问题的两个不同答案」,再跑一遍。 它不会被查出来,但这是真实数据里最伤模型的一种——答案自相矛盾。
- 把超长那条从 562 字改成 50 字,看「最长一条占比」掉到多少。 一条样本对总量的影响,就是这么直接。
- 加一项检查:assistant 的 content 里有没有出现「作为一个AI」这类套话。 这类套话会教模型在正式回答里也带上它。
常见误区
- 「格式合法就能训」。格式合法只说明能读进去,内容有问题照样把模型教歪。 格式校验和人工抽检缺一不可。
- 「超长的直接删掉最省事」。先看它为什么长——如果是真实用户会这么问,删掉等于丢掉最重要的场景。
- 「数据越多越好」。同样 3000 条,干净的和掺了 10% 错标的,训出来差距很大。 质量投入比数量投入划算得多。
自测三问(点开对答案)
1. 一份能直接拿去训的数据要满足哪五条? —— 结构统一、首条 system 末条 assistant、内容非空、没有完全重复、长度在窗口内。 2. 为什么「末条必须是 assistant」? —— 因为训练时只有 assistant 那段参与算损失。 末条是 user 的样本等于给了问题没有答案,模型会学成「答完再问」。 3. 为什么长度要看最大值而不是平均值? —— 因为超长样本会拉高成本、可能被截断,还会主导梯度。平均值被大量正常样本摊平,看不出个别异常。数据干净了,接下来要把它传上去。 下一节从登录 OpenAI Platform 开始,先分清 401 和 403 这两种「连不上」。
- ☐ (本节完,下一节 5.2.3 登录OpenAI Platform平台)