从 0 到 1 构造大模型微调数据11 篇法规 → 662 条 QA 全流程本文是安全生产法规领域大模型微调系列第 1 篇背景实习时发现通用大模型在安全生产法规场景经常把《安全生产法》的条款安到《消防法》上、编造不存在的条文编号、回答看似流畅实则胡说八道。RAG 能补知识但解决不了术语对齐和表达规范——你检索到了原文模型还是可能用自己那套话术乱编。于是决定基于公开法规做一次垂域 QLoRA 微调顺便把踩坑过程记录下来。做微调第一关不是训练是数据。这玩意儿就是 Garbage in, Garbage out —— prompt 再好喂垃圾数据进去模型学到的还是垃圾。本文就把数据构造的全流程拆开讲一遍。一、整体 Pipeline整个数据流分四步11 篇法规 txt → 切分 chunks → 强模型蒸馏生成 QA → 清洗过滤 → 最终训练数据每一步都有坑下面逐个说。二、法规收集与文档切分2.1 数据来源从应急管理部官网mem.gov.cn下了 11 篇公开法规包括《安全生产法》《消防法》《特种设备安全法》《危险化学品安全管理条例》等。全部手动整理成纯文本 txt。这里有个决策点为什么用公开法规而不是内部文档两个原因——数据量够用11 篇蒸馏完能出 600 条公开数据放 GitHub 没合规问题内部文档碰都不能碰。2.2 文档切分策略切分用的是最朴素的滑动窗口CHUNK_SIZE800# 每块 800 字CHUNK_OVERLAP80# 重叠 80 字防止条款被拦腰切断为什么不是按条切法规原文的第 X 条格式并不统一有的用中文数字、有的用阿拉伯数字正则匹配容易漏。滑动窗口虽然粗暴但省事——800 字一个块足够容纳 2-3 个条款重叠 80 字保证不会在条款中间断开。切分脚本见 split_documents.py核心逻辑就这么几行defchunk_text(text:str,size:int800,overlap:int80)-list[str]:chunks,start[],0whilestartlen(text):chunks.append(text[start:startsize])startsize-overlapreturn[c.strip()forcinchunksiflen(c.strip())100]2.3 训练/测试文档级隔离这是数据构造里最重要的一个坑——不能随机打散所有 QA 对再划分训练集/测试集。同一个文档蒸馏出来的 QA 对问题和答案都和原文强相关。如果你把同文档的 QA 对一部分分到训练集、一部分分到测试集等于让模型提前见过测试集的知识点评测结果会虚高——你以为是模型学会了其实是数据泄漏了。所以按文档级别做隔离11 篇文档9 篇归训练侧158 个 chunk2 篇归测试侧37 个 chunk随机种子固定为 42。隔离清单保存在split_manifest.json里训练侧9 篇法规消防法、特种设备安全法等测试侧2 篇法规安全生产法、生产安全事故应急条例三、强模型蒸馏让 35B 当出题老师3.1 为什么用蒸馏而不是人工标注用强模型我用的 qwen3.6:35b 跑在 Ollama 上做蒸馏蒸馏脚本见 distill_generate.py。3.2 Prompt 设计这是整个 pipeline 最需要打磨的地方。我试了三版 Prompt最终版长这样你是安全生产领域的资深专家。请基于以下法规原文片段生成 4 个高质量问答对。 【法规来源】《{法规名}》 【原文片段】 {chunk 文本} 【要求】 1. 问题类型混合法规知识问答、术语解释、场景案例 2. 答案必须严格依据原文禁止编造原文没有的条款编号和内容 3. 答案开头注明出处如根据《{法规名}》第X条 4. 问题要具体、有实际业务价值避免本文讲了什么这类空泛问题 5. 以 JSON 数组返回 直接输出 JSON不要任何其他文字。几个关键点指定注明出处这步极其重要。如果不说35B 会自由发挥编造条款编号。加上这个约束后答案会变成根据《安全生产法》第 21 条……的格式可验证性大大提升。要求不要任何其他文字35B 有时候会在 JSON 前后加一段好的以下是生成的问答对……直接导致json.loads()报错。虽然加了这句话还是会偶尔翻车但概率低了很多。n-per-chunk 4试过 3 和 53 条太少浪费 chunk 信息量5 条太多容易重复。3.3 蒸馏结果158 个训练侧 chunk × 4 条/chunk 每 10 个 chunk 额外生成 2 条拒答样本 662 条训练 QA。37 个测试侧 chunk × 4 148 条加拒答 154 条测试 QA。四条真实数据长这样字段脱敏处理{instruction:根据《X 管理条例》什么是X设备设施其目录由哪些部门共同制定,output:根据《X 管理条例》第二条规定X 设备设施是指……该设施的目录由……共同制定。,type:term,source_doc:X 管理条例}{instruction:某省属国有企业新购建了一批 X 设备设施并已投入使用该单位应向哪个部门办理登记,output:根据《X 管理条例》第七条和第八条规定该省属企业……应在 30 日内向负责登记的部门提交材料。,type:case,source_doc:X 管理条例}四种类型分布类型数量说明domain_qa260法规知识问答“第 X 条规定了什么”case212场景案例“某企业…应如何处理”term160术语解释“什么是 X”reject30拒答样本伪造审批怎么操作→ 拒绝3.4 踩坑JSON 解析失败 断点续跑蒸馏过程最大的坑是 35B 偶尔不按规矩输出 JSON。有时多一段废话前缀有时输出空内容有时 JSON 结构对不上外层是个 dict 而不是 list。处理方式指数退避重试 兜底逻辑。defcall_teacher(prompt:str,retries:int3)-list[dict]:forattemptinrange(retries):try:respCLIENT.chat.completions.create(modelMODEL,messages[{role:user,content:prompt}],temperature0.7,timeout120,)textresp.choices[0].message.content datajson.loads(text)ifisinstance(data,dict):datanext(vforvindata.values()ifisinstance(v,list))returndataexceptjson.JSONDecodeErrorase:print(f JSON 解析失败:{e})print(f 模型原始输出前200字:{text[:200]})time.sleep(2**attempt)exceptExceptionase:print(f [{type(e).__name__}]{e})time.sleep(2**attempt)return[]另一个重要的功能是断点续跑。158 个 chunk 逐个调 35B跑一半如果网络抖动或 Ollama 崩了从头开始太蛋疼。所以每处理完一个 chunk 就往.ckpt文件里记一行索引# 每处理完一个 chunk追加断点ckpt_file.write_text(\n.join(str(x)forxinsorted(done|{i})))下次重跑时读取 ckpt跳过已处理的 chunkifckpt_file.exists():doneset(int(l.strip())forlinckpt_file.read_text().splitlines())print(f断点续跑跳过{len(done)}个已处理的 chunk)这个设计在实际使用中救了我至少 5 次——Ollama 时不时就超时或 OOM有断点续跑可以随时重来。四、清洗过滤数据质量最后一道防线4.1 清洗规则蒸馏出来的数据不一定都靠谱需要过三道筛子clean_filter.py① 精确去重按问题文本归一化去空格后去重。本次 662 条蒸馏数据没有重复因为每个 chunk 内容不同、Prompt 也带了随机性。defdedup(records):seen,outset(),[]forrinrecords:keynormalize(r[instruction])# 去空格ifkeynotinseen:seen.add(key);out.append(r)returnout② 长度过滤答案长度在 20~1500 字之间。太短说明 35B 偷懒了只输出了一句话太长可能是把原文复述了一遍。ifnot(20len(r[output])1500):returnFalse③ 低质模式剔除用正则过滤掉那些一看就是 AI 在划水的回答。BAD_PATTERNSre.compile(r作为(?:一个)?AI|作为语言模型|我无法确定|很抱歉?我(?:不能|无法))如果蒸馏模型说出了作为 AI我无法确定……这种话说明它对这块内容不够确定这条数据本身质量就存疑直接丢掉。4.2 清洗结果说实话这次蒸馏质量比我预期的好——662 条全部通过过滤一条没丢。去重也没发现重复。这说明 Prompt 设计和 35B 的表现都在线。但这不代表清洗没价值。实际项目中数据量更大、来源更杂时清洗环节通常会砍掉 15%-30% 的数据。这里的清洗 pipeline 是可复用的。最终产出的训练数据是标准的 alpaca 格式[{instruction:问题,input:,output:答案},...]直接丢进 LLaMA-Factory 的dataset_info.json注册一下就能训练。五、类型配比为什么是 60/20/10/10这个配比不是拍脑袋定的类型占比实际数量为什么这个比例domain_qa60%260 条核心任务是回答法规问题这是主力term20%160 条术语解释能让模型学会专业表达不是每个概念都要查词典case—212 条场景案例的实际比例偏高32%因为 35B 天然倾向生成 case 类reject10%30 条安全合规场景下拒答能力很重要用户问怎么伪造审批必须拒绝注意实际蒸馏产出的 case 类偏多212 条占 32%如果后续发现模型过度倾向输出案例式回答可以在最终导出时做分层抽样压回 10%。但从 662 条的总量来看数据太少做抽样意义不大所以保留了全量。六、11 篇法规够不够为什么不需要几万条数据看到这里你可能有个疑问662 条数据11 篇原文是不是太少了大模型微调不都得上万条吗分三层解释这个问题第一法规文本的密度远高于对话文本。一篇《安全生产法》全文上万字、上百个条款每个条款都是一个独立的知识点。相比之下一条通用对话数据“今天天气怎么样”“我心情不好”的信息量几乎为零。所以 11 篇法规 ≈ 上千个知识点信息的浓度完全不同。第二蒸馏放大了数据量。11 篇原文 → 195 个 chunk → 每个 chunk 蒸馏 4 条 QA → 662 条训练数据。蒸馏过程不是简单的复制粘贴35B 模型会对原文做理解、重组、场景化改写相当于把法条原文翻译成了问答对话。数据量放大了约 60 倍多样性也提升了。第三这是领域适配不是从零学说话。Qwen2.5-7B 本身就有扎实的中文理解和推理能力我要做的是让它学会法规语言范式——怎么引用条款、怎么表述罚则、什么场景适用什么法条。这个目标不需要几万条数据几百条高质量领域数据就够了。类似的例子医疗领域微调用几千条、法律领域微调用几百条都有成功案例。反过来想如果 662 条不够那评测结果自然会暴露问题。下一篇的对照实验会给出量化答案——基座模型和微调模型在测试集上的差距到底有多大。如果差距很小说明数据量确实不足如果差距显著就证明数据质量比数量更重要。七、总结与下一步数据构造全流程踩坑总结文档级隔离 随机划分评测泄漏比模型效果差更致命因为你会高估模型能力Prompt 要强制 JSON 禁止废话蒸馏模型很聪明但也很有主见不说清楚格式它真的会自由发挥断点续跑是必需品调远程模型蒸馏几十上百次请求没有断点续跑心态会炸清洗规则要根据实际数据调我的 662 条一条没丢但你用在不同领域可能不一样下一步就是把这份数据喂给 Qwen2.5-7B 做 QLoRA 微调看基座模型的张冠李戴毛病能改掉多少。下一篇见。