1. 反常识语料为什么值得进微调数据池前阵子在一个小群里聊数据构造有人甩过来一句拿弱智吧的帖子训模型这不是明摆着把模型带沟里吗。这个反应非常正常因为大部分人对这类语料的第一印象就是段子、玩梗、没有信息密度。但我自己真正把几千条帖子扒下来逐条读过之后结论完全相反这批文本是整个互联网上少有的、天然带有语义陷阱的高密度对抗样本。弱智吧这个社区的核心玩法是反逻辑问答——提问者用一个看似正常的句式埋一个谐音、双关或者字面歧义答案是出人意料的。比如什么东西越洗越脏字面看是在问清洁实际考的是洗这个动作的对象能否是水本身。这类题目的结构非常稳定一个诱导性的前提 一个需要跳出常规语义框架的答案。这恰好是大模型最容易栽跟头的地方模型靠统计先验补全答案先验越强被带偏的概率越高。所以这篇东西讲的不是怎么用段子逗模型玩而是一个正经的数据构造实验把这种非标准语料加工成可以喂进 InternLM 做微调的高质量指令数据然后看模型在抗诱导这件事上到底有没有变化。适合两类人看——正在做小规模指令微调、手上数据不够但你想要难样本的以及想搞清楚数据构造这条链路每一环具体在干什么的。哪怕你手上的语料跟这个完全无关清洗、改写、构造偏好对、泄漏检测这套流程也是通用的。1.1 弱智吧语料的真实分布三类题、两种噪声我把扒下来的样本按答案的生成机制分了三类这个分类直接决定了后面每类数据该怎么加工不能一锅炖。第一类是谐音双关型。答案的正确性完全依赖读音重合比如某个问句的字面意思和答案之间唯一的桥梁是发音。这类样本对模型的意义在于考察它能不能在语义路径走不通的时候切换到语音路径。第二类是字面歧义型。题干里的某个词在常识语境下有一个默认含义但在答案里被强行拉回到字面义。举个例子什么门永远关不上这种题考的是把门从一个物理构件扩展到一个抽象概念。模型如果只在物理语义空间里搜就会一直往坏了的门没装门框的门上靠。第三类是纯粹的语言游戏型靠断句、词性转换、量词错配制造荒诞感。这类题严格来说没有唯一正确答案价值最低但在构造负样本的时候特别好用——因为它能让模型产生大量看起来合理其实跑偏的输出。噪声主要两种。一种是水楼纯表情、哈哈哈、楼上说得对这类占原始抓取量的六成以上必须清掉。另一种更隐蔽是**梗中梗**答案引用了社区内部的特定上下文脱离那个语境完全无法理解。这种样本如果直接进训练集等于给模型灌入一批无法从题干推导出答案的样本会污染它对可解性的判断。1.2 这个实验真正想验证的假设我在动手之前给自己定了三个可证伪的问题没有这三个问题做完了也没法判断成败。假设一在少量高质量反诱导样本上做 LoRA 微调能提升模型在同类陷阱题上的正确率且不明显损伤通用能力。假设二带思维链的样本比纯答案样本效果更好但收益存在上限样本量超过某个点之后模型会开始模仿解释的格式而不是解释的逻辑。假设三偏好对chosen / rejected数据的收益在陷阱题这个场景下比 SFT 更明显因为模型缺的不是知识是不要顺着题干往下滑的习惯。这三个假设里第二个是最容易被忽略的。很多人做 CoT 数据只看到了推理过程更完整这一面没注意到小数据微调里模型学格式的速度远快于学逻辑的速度。1.3 直接拿段子当训练集会翻车的三个原因我早期偷懒试过一次把清洗后的问答对直接拼成问题xxx 答案xxx不做任何改写就开训。结果很典型值得单独说。第一个问题是指令格式缺失。模型学到的分布是看到一句话后面接一句抖机灵而不是收到一个任务并完成它。你后面用标准指令去问它反而答不准了。这就是典型的格式不对齐。第二个问题是答案多解。同一个题目在评论区可能有五六个不同角度的回答全都有道理。如果不过滤直接混进去同一道题在训练集里对应多个不同答案模型会学到这类题可以随便答熵被抬高输出稳定性下降。第三个问题最坑网络梗泄漏。这批题目里相当一部分在网上流传极广基座模型在预训练阶段早就见过。你拿它做训练集模型表现变好但你根本分不清是微调起了作用还是它只是背出了原文。这个坑后面第 5 节我会专门讲怎么检测。2. 从社区文本到JSONL清洗与结构化还原数据构造这件事八成的功夫花在清洗上只有两成在模型侧。我这次的结构是四步原始帖子采集 → 楼层结构还原 → 质量过滤 → 结构化输出。每一步都有具体的判断规则靠感觉筛数据最后一定会翻车。先说清楚一点采集环节我只取了公开可访问的页面文本且只保留问答正文不保留用户标识、发布时间、地区等任何字段。个人信息不入库这是底线不是可选项。2.1 楼层结构还原成问答对的核心规则原始的帖子结构是一个标题句 若干楼层回复。标题句一般是题目回复里混杂着正确答案、错误猜测、玩梗、水楼。要从这一堆里还原出干净的题干-答案对我用了三条规则。规则一答案锚定在得赞最高的非水楼回复上。这个规则很朴素但很有效。社区的自净能力在这个场景下比任何启发式算法都准。我试过纯按文本相似度挑答案效果差得多。规则二题干必须满足可独立成句。很多标题是残缺的比如只有一个名词短语必须结合正文才能理解。这类样本我直接丢弃因为脱离上下文的题干喂给模型它学不到任何模式。规则三答案长度控制在 2 到 60 字之间。太短的是哈哈哈或者单个词没有解析价值太长的一般是长篇科普或者辩论已经偏离了陷阱题的形态会污染数据分布。这里有个细节值得说归一化必须做在分词之前。中文社区文本里全角半角混用、繁体简体混用、夹杂大量无意义符号的情况非常普遍。我遇到过一条题目因为里面有个全角问号导致后面按标点切分时完全错位整批数据的问答对都串了行。这种 bug 排查起来非常费劲因为数据是批量处理的你看到的是一千条里有一条错但实际错位造成的影响是相邻两条一起被污染。2.2 用脚本做第一轮机械过滤清洗脚本我是用 Python 写的核心逻辑其实很简单就是把一堆规则串起来逐条打分过阈值才留下。下面这段是骨架实际跑的时候我在每个 filter 里都打了日志方便回头看是哪条规则杀掉的样本。import re, json, hashlib def is_water(text: str) - bool: t text.strip() if len(t) 4: return True # 纯符号 / 纯表情名义字符 if not re.search(r[\u4e00-\u9fa5a-zA-Z0-9], t): return True water_pat [哈哈哈, 楼上, 沙发, 顶, 楼主, 正解, 笑死] return any(p in t for p in water_pat) def norm(text: str) - str: t text.strip() t re.sub(r\s, , t) # 全角转半角 t .join( chr(ord(c) - 0xFEE0) if 0xFF01 ord(c) 0xFF5E else c for c in t ) t re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , t) return t def sim_key(text: str) - str: # 简易去重指纹去掉标点后取前 16 字符 raw re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) return hashlib.md5(raw[:16].encode()).hexdigest() def build(records): seen, out set(), [] for r in records: q, a norm(r.get(title, )), norm(r.get(answer, )) if is_water(q) or is_water(a): continue if not (8 len(q) 40) or not (2 len(a) 60): continue key sim_key(q) if key in seen: continue seen.add(key) out.append({instruction: q, output: a, src: trap_qa}) return out这里最容易犯的错是去重指纹设计得太粗。我第一版用的是原句哈希结果发现同一个题目会被改一两个字重新发一遍哈希完全挡不住。后来改成去标点后取前 16 字召回率立刻上去了。但也不能取整句因为有些题目的后半段是变体取整句会漏掉重复。这个长度是试出来的没有理论依据你也可以按自己的数据分布调。2.3 去重之外还要做一次梗泄漏预筛这一步我在第 2 节里提前说一下因为它属于数据构造阶段的工作不是评测阶段才做的。具体做法很土但管用把基座模型未微调直接拿来跑一遍全量题目让它输出答案然后用关键词匹配 人工抽查判断对错。凡是基座模型能直接答对的标记为leakedTrue。这批被标记的样本有两个用途。一是不能进测试集否则评测结果毫无意义。二是可以进训练集但权重降低因为它们对模型来说太简单属于已经会做的题练了没增量。我第一次跑这个预筛的时候基座模型的直接正确率是 34%。这个数字比我想的高不少说明网上流传度高的题目确实已经被预训练语料吃进去了。如果你不做这步微调完看到正确率从 34% 涨到 60%会误以为数据构造很成功实际上可能只是训练集和测试集撞了。3. 三条数据构造主线指令对、思维链、偏好对清洗完得到大约 2400 条干净的问答对接下来才是真正花心思的部分。我把数据拆成三条线并行构造因为它们训练目标和数据形态完全不同混在一起训反而互相干扰。数据线形态规模训练方式主要作用指令对instruction / output~1800SFT建立收到任务就作答的格式思维链instruction / reasoning / output~600SFT同批教会模型先拆题再答偏好对prompt / chosen / rejected~900DPO纠正顺着题干滑的倾向3.1 指令化改写把题目变成模型认识的任务原始数据最大的问题是没有任务感。一句什么东西越洗越脏模型看到它可能以为是闲聊也可能以为是接龙。改写的目的就是给它一个明确的角色和一个明确的输出契约。我用了三类模板随机分配避免模型把某一个固定句式当成触发条件{instruction: 下面是一个有陷阱的问题请给出正确答案只回答问题本身。\n问题什么东西越洗越脏, output: 水。} {instruction: 回答下面的问题。注意题干可能存在语义误导不要被字面意思带走。\n问题什么东西越洗越脏, output: 水因为洗东西的时候水本身会变脏。} {instruction: 请判断题目中的关键词是否被偷换了含义然后给出答案。\n问题什么东西越洗越脏, output: 关键词是“洗”正常语境下“洗”的对象是物品这里把水也当成被洗的对象偷换了语义关系。答案水。}三个模板的答案长度差异很大这是故意的。只给短答案模型会变得惜字如金只给长答案模型会变得啰嗦。混着给它才能学会按指令里隐含的长度要求调整输出。这里踩过一个坑模板里的提示语不能写得太剧透。我一开始在提示里直接写了这是一道脑筋急转弯结果模型学到的是看到脑筋急转弯这四个字才开始找陷阱普通问句它照样栽。后来把提示语改成中性描述让它必须自己判断题目类型效果才对。3.2 思维链样本的写法与解释膨胀风险思维链数据的收益是真的但写法很讲究。我见过不少 CoT 数据集推理过程写得像教科书一二三四列得整整齐齐结果模型学完之后输出全是这种格式化的废话真正需要它推理的时候反而更慢更不准。我的写法是模仿人的真实思考路径允许有反复和排除指令什么东西越洗越脏 推理先看“洗”这个动作。正常情况洗东西物品变干净水变脏。题目说“什么东西越洗越脏”如果按正常逻辑被洗的东西应该变干净所以它问的对象不是被洗的那个而是洗的过程里承担脏东西的那个。那就是水。 答案水。注意这段推理里有一句如果按正常逻辑……所以不是这是排除过程我认为这是整条 CoT 里最有价值的部分。模型缺的不是知识是先假设再证伪的习惯。提示CoT 样本的规模一定要控制住。我实测下来CoT 占比超过总 SFT 数据的三成之后模型在普通问答上的平均输出长度会明显上升而那些场景不需要长输出。这个副作用在小模型上尤其明显。另外还有个很隐蔽的问题CoT 数据会放大幻觉。模型一旦学会先解释再回答在遇到它其实不会的题目时它会倾向于编一段看似合理的推理来支撑一个错误答案而不是直接说不确定。我在评估阶段专门统计了自信错误的比例微调 CoT 之后这个比例确实上升了几个点。这是必须接受的代价但你要知道它的存在。3.3 偏好对构造错误答案从哪来偏好对数据是整个实验里我认为收益最高的一环。核心思路很直接模型缺的不是知道答案是不要在题干设定的框架里打转。所以 rejected 样本要专门挑那种顺着题干合理推演但结论错误的回答。rejected 有四个来源我按优先级排基座模型的真实错误输出。最有价值因为这就是模型当前的坏习惯DPO 直接纠偏。人工构造的合理误导答案。比如把题干的错误前提当真认真回答一个不存在的东西。正确答案但推理过程错。这类样本很妙答案对但路径错DPO 会让模型学会重视过程。答非所问的敷衍回答。用来压制不确定就糊弄的倾向。chosen 侧我倾向于用简洁答案 一句关键解释不用完整 CoT。原因是 DPO 训练时 chosen 和 rejected 的长度差异如果太大模型会学到长的一定是好的这个错误信号。{ prompt: 请回答问题什么东西越洗越脏, chosen: 水。因为洗的过程中污渍转移到水里水本身变脏了。, rejected: 越洗越脏的东西通常是抹布、拖把这类清洁工具因为它们反复接触污渍如果不及时更换会积累细菌。 }上面这个 rejected 就是典型的顺着题干滑。它读起来非常合理甚至比正确答案更像正经回答但完全没识别出题目在玩语义游戏。这种高质量的错误才是 DPO 真正需要的负样本随手编一个胡言乱语当 rejected训练效果差得远。4. InternLM微调的实操配置数据准备好之后进训练。我用的是 InternLM 系列的开源基座规模选的中等档位原因很简单太小了学不动这种需要跳出分布的能力太大了我手上的算力跑不动全参数只能上 LoRA而 LoRA 在大模型上的容量又不够覆盖这种偏行为模式的改变。中等规模是性价比最合适的区间。4.1 数据配比与基座选择单纯用这 2400 条陷阱题训模型一定会跑偏。通用能力是靠海量数据撑起来的你拿两千条窄分布数据去覆盖它等于用一杯水冲淡一缸水冲不动还搅浑了。我的配比是陷阱数据 : 通用指令数据 ≈ 1 : 3。通用数据我用的是公开的通用指令集覆盖写作、问答、摘要、代码几类保证模型不会因为只练抖机灵而丧失正常表达能力。这个比例不是拍脑袋定的。我做了个小规模的梯度实验分别用 1:1、1:3、1:6 训三个 checkpoint然后在一个 200 条的小规模通用能力测试集上看掉点情况。1:1 的掉点最明显1:6 的掉点在噪声范围内但陷阱题提升很弱1:3 是拐点。你手上的数据分布不一样这个比例一定要自己试别照抄。4.2 LoRA 参数为什么这么设配置这块我列一下实际用的关键项附上理由因为很多人抄配置只抄数字不知道为什么。参数取值设定理由lora_rank32数据量小rank 太高容易过拟合低于 16 在行为模式改变上容量不够lora_alpha64保持 alpha 2 * rank 的常用比例缩放稳定lora_dropout0.05小数据必须加 dropout我试过 0 和 0.05后者 eval loss 更平learning_rate2e-4LoRA 常用区间再高 loss 会震荡batch_size4梯度累积 8显存限制下的折中等效 batch 32max_length2048CoT 样本最长约 700 token2048 留足余量epochs3第 4 轮开始 eval loss 明显抬头配置文件长这样格式是通用的对话微调配置# 关键片段非完整配置 model_name_or_path internlm-base # 替换为你实际使用的基座路径 data_path ./data/trap_mix.jsonl max_length 2048 batch_size 4 accumulative_counts 8 max_epochs 3 lr 2e-4 weight_decay 0.01 warmup_ratio 0.03 lora dict( typeLoraConfig, r32, lora_alpha64, lora_dropout0.05, biasnone, task_typeCAUSAL_LM, )warmup_ratio这个参数在小数据微调里容易被忽视。我一开始设的 0.1结果前几十步基本没在学因为总步数本来就不多warmup 占掉的比例太大。改成 0.03 之后曲线正常了。4.3 训练监控loss 正常但效果不正常的排查这里我要说一个最折磨人的情况loss 曲线漂亮得不行但模型效果没变化甚至更差。我第一次训完就是这个状态。train loss 从 1.8 平稳降到 0.6eval loss 到 0.9 之后开始抬头典型的过拟合信号。但真正的诊断不能只看 loss我做了三件事才定位到问题。第一件事逐条对比基座和微调模型在同一批 prompt 上的输出。我发现微调后的模型在陷阱题上确实会答对了但语气变了变得非常自信且句式固定全是关键词是X这里偷换了……答案Y这个模板。它学的是模板不是推理。第二件事把训练集里的 CoT 样本全部拿掉重训一版。结果模板化输出消失了但陷阱题正确率只掉了 4 个点。这说明绝大部分提升来自指令对数据CoT 的边际收益远没有我想的那么大反而贡献了主要的副作用。第三件事查数据里的重复。我用文本相似度扫了一遍训练集发现 2400 条里大约有 180 条是同一题目的不同变体这些变体在训练时被重复采样多次等于给这批数据加了权。把它们合并之后过拟合明显缓解。提示小数据微调一定要做去重后统计每题出现次数。重复样本在小数据集里的加权效应被严重低估一条重复三次的样本影响力相当于三条不同样本但泛化价值只有一条。最后我把 CoT 占比从 25% 压到 12%epochs 保持 3重新训了一版。这版的效果是所有版本里最平衡的也是后面评估用的模型。5. 评估设计怎么判断这批数据真起了作用评估这块我要多花点篇幅因为这是整个实验里最容易自欺欺人的环节。你做了一堆工作主观上非常希望它有效这时候如果评估设计不严谨你会不自觉地挑选那些能证明自己有效的指标。5.1 分维度测试集与难度分层测试集我单独构造了 300 条和训练集严格无重叠。构造方式是从清洗后的池子里随机抽但抽样前先用基座模型测一遍按答对与否分成两档。难档基座答错150 条易档基座答对150 条。分开统计的意义在于如果一个模型只在易档上提升那它很可能是在拟合格式而不是在学能力只有在难档上也有稳定提升才能说明它真的获得了某种识别陷阱的能力。测试维度我拆成四个每个维度单独算分维度考察点样本数判定方式语义陷阱识别能否识别题干中的关键词偷换90答案关键词匹配 人工复核双关谐音理解能否走通语音路径70答案完全匹配抗过度推理会不会给出冗长但错误的解释80答案正确且长度 40 字拒答合理性遇到无解题目能否说不确定60人工判定第四个维度是我特意加的。因为反逻辑题目里有一部分本身就设计成了无解或者多解模型如果强行给一个答案其实是一种更严重的失败。我观察到微调之后模型在这个维度上反而略有退步因为它被训练成了总要给个答案。5.2 泄漏检测与基座对照前面提过这批题目网上流传度很高。我在测试集构造时做了一次严格的泄漏检查流程是这样的用基座模型跑测试集记录答案。对答对的样本人工看是否属于直接背出答案表现为答案与网络流传版本逐字相同。对判定为泄漏的样本从测试集中剔除。剔完之后 300 条剩下 236 条。这个损耗率接近 20%说明如果不做这步你的评测结果会虚高一大截。另外必须有的对照是基座模型基线。所有指标都是相对于基座的增量绝对值意义不大。因为同一批数据换个基座绝对值可能差十几个点。对照结果大致是这样数字做了归一化处理只反映趋势指标基座SFT(仅指令对)SFT(指令对少量CoT)语义陷阱识别34%58%63%双关谐音理解41%52%55%抗过度推理61%57%54%拒答合理性48%43%40%通用能力对照集基线-1.2%-2.1%这张表里最值得注意的是后两行。抗过度推理和拒答合理性都掉了也就是说模型变敢答了但也变不肯认怂了。这是陷阱题微调的必然代价因为训练信号本身就在鼓励它给出确定答案。如果你的应用场景里不知道就说不知道比答对率更重要那这套数据的配比必须往回调。5.3 人工盲评与模板化观察自动指标永远有盲区我抽了 100 条做人工盲评。做法是把基座和微调模型的回答打乱顺序去掉来源标记让两个人独立打分只看这个回答是否真的理解了题目的陷阱。盲评结果和自动指标基本吻合但暴露了一个自动指标看不见的问题微调模型有 12% 的回答是正确但套模板。具体表现是即使题目根本没有语义偷换它也会硬套一句关键词是X这里偷换了含义。这种无差别识别陷阱其实是一种新的失败模式比答错更难被发现因为自动评分只匹配答案。我在实际操作中的体会是这类语料做微调最大的收益不在于把正确率从 34% 提到 63%而在于你能通过这个实验看清楚模型的失败模式到底长什么样。它不会告诉你我不知道它会给你一个结构完整、语气笃定、逻辑上差一步的答案。这个认知比指标提升有价值得多。如果你也想在自己的场景里复现这套流程我给一个最后的小建议先别急着训先花半天时间把 200 条样本逐条读一遍。读的过程里你会发现大量自动清洗脚本抓不到的规律比如某类题目的答案分布极度集中、某类题干其实是同一个模板批量生成的。这些东西写不进代码但对后面每一步的决策影响都很大。我这次实验里最关键的几个判断包括 CoT 占比和配比调整都是读样本读出来的不是跑实验跑出来的。