简介:这份PPT课件面向希望掌握大模型微调语料自动化构建的开发者与AI应用实践者,聚焦传统语料工程中人工标注成本高、Python与JSONL格式门槛高、数据转换繁琐等痛点,给出基于Dify的低代码落地路径。压缩包共1个pptx文件,约15.21MB,以图文幻灯片形式系统讲解从概念到实战的完整流程。内容覆盖微调与语料工程核心价值、Dify工具与五节点工作流架构解析,并逐步演示开始节点、文档提取器、代码执行、LLM节点与结束节点的配置方法,包括利用Qwen2.5-72B-Instruct-128K合并文本、截取前80000字符并生成标准JSONL语料,最后以《少年歌行》文本为例展示测试验证与语料质量评估。已有110人学习,适合想快速搭建微调语料流水线、降低技术门槛的读者参考。
1. 语料自动化构建:为什么手工整理微调数据是第一个翻车点
做过大模型微调的人都有一个共识:模型效果的上限,八成由语料质量决定,剩下两成才是超参和算力。但现实里,绝大多数团队的精力分配恰好反过来——花三天调 learning_rate 和 lora_rank,却只花三小时从业务系统里导出几千条脏数据。我见过最典型的场景是:一个客服问答微调任务,语料来自工单系统,字段里混着内部编号、HTML 标签、客服口头禅和重复提问,人工清洗两周还没搞完,模型上线后答非所问。
这篇要讲的是用 Dify 把「语料采集 → 清洗 → 结构化 → 质检 → 导出训练格式」这条链路自动化跑起来。Dify 在这里不是拿来当聊天机器人,而是当编排引擎:用工作流把多个处理节点串起来,用知识库做去重和检索增强,用代码节点做格式转换。适合两类人:一是手里有业务数据、想微调但卡在数据准备阶段的工程师;二是已经在用 Dify 搭应用、想把知识库里的内容反向变成训练语料的团队。读完你能拿到一条可复现的流水线,而不是一堆零散脚本。
2. 用 Dify 工作流搭语料流水线:节点怎么排、变量怎么传
2.1 为什么选 Dify 而不是写一堆 Python 脚本
纯脚本方案的问题不在写不出来,而在维护成本。数据源一换、清洗规则一改,脚本就得重跑一遍调试,中间结果散落在各个临时文件里,出了问题很难定位是哪一步脏的。Dify 工作流的价值在于把每一步变成可视化节点,输入输出挂在变量上,哪一步产出异常一眼能看出来。
更实际的一点是,Dify 的代码节点支持 Python,意味着你既可以用现成的 LLM 节点做语义清洗,也可以在代码节点里写正则和格式转换,两种能力混用。比如「把口语化提问改写成标准问法」交给 LLM 节点,「去掉手机号和身份证号」交给代码节点,各干各擅长的。
选型上要注意:Dify 社区版和工作流能力在持续迭代,不同版本节点类型和变量聚合器的行为有差异。如果你是从旧版本升级上来的,导入 DSL 时可能遇到版本不兼容,常见做法是先在测试环境导入验证,确认节点没丢再迁到生产。
2.2 一条最小可跑的语料构建工作流
下面这条工作流的目标是:输入一批原始问答对,输出清洗后的 JSONL 训练格式。整体节点顺序是「开始 → 代码节点(预处理)→ LLM 节点(语义清洗)→ 代码节点(去重+格式化)→ 结束」。
先看开始节点的变量定义,这是整条链路的入口契约:
{ "inputs": { "raw_text": "string", "source_type": "string", "batch_id": "string" } }raw_text放原始语料文本,source_type标记来源(工单/FAQ/文档),batch_id用于后续追溯这批数据是哪次跑出来的。参数说明:source_type不是必须的,但强烈建议保留,因为不同来源的清洗规则往往不同,后面代码节点里可以按它分支。
第一个代码节点做基础预处理,去掉明显噪声:
import re def main(raw_text: str, source_type: str) -> dict: # 去掉 HTML 标签 text = re.sub(r'<[^>]+>', '', raw_text) # 去掉连续空白和换行 text = re.sub(r'\s+', ' ', text).strip() # 去掉内部工单编号,形如 TK-20240101-001 text = re.sub(r'TK-\d{8}-\d{3}', '', text) # 按问答分隔符切分,常见是 "问:" "答:" pairs = re.split(r'(?=问[::])', text) cleaned = [p.strip() for p in pairs if len(p.strip()) > 10] return {"cleaned_pairs": cleaned, "pair_count": len(cleaned)}逻辑说明:先用正则剥掉 HTML 和工单编号这类结构化噪声,再按「问:」切分成问答对。len(p.strip()) > 10这个阈值是过滤掉「问:你好」这种无信息量的短句,具体数值按你的业务调,客服场景一般 10 到 15 比较合适。返回的pair_count方便后面判断这批数据量是否正常。
第二个节点是 LLM 节点,负责语义层面的清洗。提示词这样写:
你是一个语料清洗助手。下面是一批客服问答对,请对每一条做三件事: 1. 把口语化、带错别字的提问改写成标准书面问法,保持原意不变; 2. 如果回答里包含具体的订单号、手机号、姓名,用 [占位符] 替换; 3. 如果某条问答对语义重复或答非所问,直接删除。 输出格式为 JSON 数组,每个元素包含 question 和 answer 两个字段。这里的关键参数是温度,语料清洗任务建议设成 0 到 0.3,太高会让改写偏离原意。另外要控制单次输入长度,Dify 工作流有上下文长度限制,如果一批数据太大,常见做法是在代码节点里先分批,每批 20 到 30 条,再循环调 LLM 节点。
第三个代码节点做去重和最终格式化:
import json import hashlib def main(llm_output: str, batch_id: str) -> dict: try: pairs = json.loads(llm_output) except json.JSONDecodeError: return {"error": "LLM 输出不是合法 JSON", "valid": False} seen = set() result = [] for p in pairs: q = p.get("question", "").strip() a = p.get("answer", "").strip() if not q or not a: continue # 用问题文本的 hash 做去重 h = hashlib.md5(q.encode()).hexdigest() if h in seen: continue seen.add(h) result.append({ "messages": [ {"role": "user", "content": q}, {"role": "assistant", "content": a} ], "metadata": {"batch_id": batch_id} }) # 输出 JSONL 格式,每行一条 jsonl = "\n".join(json.dumps(r, ensure_ascii=False) for r in result) return {"jsonl": jsonl, "final_count": len(result), "valid": True}逻辑说明:先解析 LLM 返回的 JSON,解析失败直接返回错误标记,避免脏数据往下流。去重用问题文本的 MD5,比逐条比对快得多。最终输出成 OpenAI 微调常用的 messages 格式,metadata里带上 batch_id 方便回溯。参数上,ensure_ascii=False必须加,否则中文会被转义成 unicode 码点,训练时读出来是乱码。
2.3 变量聚合器在语料合并里的用法
当你有多个数据源并行处理时,会用到变量聚合器把多路输出合并成一路。典型场景是:工单数据和 FAQ 文档走两条不同的清洗分支,最后要合并成一个训练集。变量聚合器的配置要点是选对聚合类型——如果两路输出都是列表,用「数组」类型;如果都是字符串,用「字符串拼接」。踩过的坑是聚合类型选错,下游节点拿到的变量类型对不上,工作流直接报错但不提示具体是哪个变量的问题,只能逐个节点排查。
3. 语料质检与去重:把脏数据拦在训练之前
3.1 三类必须拦掉的脏数据
第一类是格式脏:JSON 解析失败、字段缺失、编码错误。这类在代码节点里用 try-except 就能拦住,关键是拦住之后要有记录,不能静默丢弃,否则你不知道丢了多少。
第二类是内容脏:包含个人隐私信息、内部系统地址、无意义重复。隐私信息用正则加 LLM 双重过滤,正则兜底手机号身份证,LLM 处理「帮我查一下张三的订单」这种自然语言里的姓名。
第三类是语义脏:问题和答案不匹配、答案是从别的文档复制过来的。这类最难自动检测,常见做法是用一个 LLM 节点做质检打分,给每条语料打 1 到 5 分,低于 3 分的进人工复核队列。
3.2 用知识库做跨批次去重
单批次内去重靠 hash 就够了,但跨批次去重需要持久化。Dify 知识库在这里能派上用场:把已入库的语料问题存进一个知识库,新批次处理时先检索一遍,相似度超过阈值的就标记为疑似重复。
具体做法是在工作流里加一个知识库检索节点,输入是当前问题文本,检索已有语料库,返回相似度分数。参数上,相似度阈值设 0.9 以上比较安全,太低会误杀正常的不同问法。检索结果里如果最高分超过阈值,就在代码节点里把这条标记为duplicate并跳过。
要注意知识库检索有延迟,大批量处理时如果每条都实时检索,工作流会跑得很慢。常见优化是先在代码节点里做本地 hash 去重,剩下的再批量检索知识库。
3.3 质检结果的可视化与人工复核
自动化质检不可能 100% 准确,必须留人工复核的口子。我的做法是在工作流最后加一个分支:质检分数低于阈值的语料,输出到一个单独的 JSONL 文件,同时生成一个简单的 HTML 预览页,把问题、答案、质检分数并排列出来,人工过一遍决定保留还是删除。
这个 HTML 生成也可以在代码节点里做,用 Python 的字符串拼接就行,不需要额外依赖。复核完的语料再合并回主训练集,这样既保证了自动化效率,又不会让脏数据漏进训练。
4. 避坑与排查:Dify 语料流水线最常见的五个问题
4.1 工作流跑一半报 credentials validation 错误
现象:工作流执行到 LLM 节点时报「an error occurred during credentials validation」,整个流程中断。
原因:模型供应商的 API Key 失效或额度耗尽,也可能是网络策略调整导致请求发不出去。Dify 在调用模型前会做一次凭证校验,校验不过直接抛这个错。
解决:先到模型供应商设置里点一次「测试连接」,确认 Key 本身有效。如果 Key 没问题,检查 Dify 所在服务器的出网策略,特别是本地部署场景,容器内 DNS 解析失败也会报类似错误。本地部署时常见做法是把模型服务地址从公网域名改成内网 IP 或 host 映射。
4.2 上下文超长导致 LLM 节点截断输出
现象:语料批次稍大一点,LLM 节点返回的内容就不完整,JSON 解析失败。
原因:Dify 工作流对单次 LLM 调用的上下文长度有限制,输入加输出超过上限就会被截断。语料清洗场景里,一批 50 条问答对很容易超。
解决:在代码节点里做分批,每批控制在 20 条以内,然后用循环节点逐批调用。另一个办法是缩短提示词,把清洗规则写得更紧凑。如果用的是本地模型,检查模型的 context window 配置是否和 Dify 里填的一致。
4.3 代码节点输出中文变成乱码
现象:代码节点返回的 JSONL 里中文显示为\u4f60\u597d这种转义形式,训练时读出来是乱码。
原因:json.dumps默认ensure_ascii=True,会把非 ASCII 字符转义。
解决:所有json.dumps调用都加ensure_ascii=False。另外检查代码节点的输出编码设置,确保是 UTF-8。这个坑很小但很隐蔽,因为转义后的内容在 JSON 层面是合法的,只有实际读进训练脚本才会暴露。
4.4 知识库检索节点返回空结果
现象:跨批次去重时,知识库检索节点总是返回空,导致去重失效。
原因:知识库文档还没完成索引,或者检索的 query 和入库时的文本差异太大。Dify 知识库的索引是异步的,刚导入的文档不会立刻可检索。
解决:导入文档后等索引完成再跑工作流,界面上能看到索引进度。如果索引已完成还是检索不到,检查检索模式是「向量检索」还是「全文检索」,语料去重场景建议用向量检索,对同义不同形的问法更敏感。
4.5 工作流迁移到另一台机器后节点丢失
现象:在测试环境跑通的工作流,导出 DSL 后导入生产环境,部分节点变成未知类型或配置丢失。
原因:两个环境的 Dify 版本不一致,新版本引入的节点类型在旧版本里不存在。热词里提到的「导入 DSL 提示版本不兼容」就是这个情况。
解决:迁移前先对齐版本,生产环境升级到和测试环境一致。如果没法升级,常见做法是手动把 DSL 文件里的版本号降级,同时删掉旧版本不支持的节点配置,但这需要对照两个版本的节点定义逐个改,比较费时。更稳妥的做法是迁移前在目标环境先建一个最小工作流验证节点兼容性。
5. 从语料到微调:导出格式与训练侧对接的实操细节
5.1 三种训练格式的转换与选择
语料清洗完只是半成品,最终要转成训练框架认识的格式。常见三种:
| 格式 | 适用场景 | 关键字段 |
|---|---|---|
| OpenAI messages | 对话微调、API 兼容 | messages 数组 |
| Alpaca | 指令微调、开源框架 | instruction/input/output |
| ShareGPT | 多轮对话、社区通用 | conversations 数组 |
转换逻辑不复杂,但要注意字段映射。比如 Alpaca 格式里input可以为空,但很多训练脚本要求它必须是字符串而不是 null,转换时给个空字符串兜底。
5.2 训练前的最后一道校验
导出 JSONL 后,别急着丢给训练脚本。我一般会跑一个校验脚本,检查三件事:每行是否是合法 JSON、messages 里 role 顺序是否 user 在前、有没有空 content。这个脚本很短:
import json def validate(path): errors = [] with open(path, 'r', encoding='utf-8') as f: for i, line in enumerate(f, 1): try: obj = json.loads(line) except json.JSONDecodeError: errors.append(f"第 {i} 行 JSON 非法") continue msgs = obj.get("messages", []) if not msgs: errors.append(f"第 {i} 行 messages 为空") continue if msgs[0].get("role") != "user": errors.append(f"第 {i} 行首条不是 user") for m in msgs: if not m.get("content", "").strip(): errors.append(f"第 {i} 行存在空 content") return errors if __name__ == "__main__": errs = validate("train.jsonl") print(f"发现 {len(errs)} 个问题") for e in errs[:20]: print(e)这个脚本跑一遍,能拦掉九成以上的格式问题。剩下的语义问题只能靠抽样人工看,我一般随机抽 50 条读一遍,重点看答案有没有答非所问。
5.3 一个我踩过的坑
早期做语料自动化时,我图省事把去重阈值设得很低,结果把「怎么退款」和「退款流程是什么」当成重复删掉了一条。后来才明白,语料去重不能只看字面相似度,同义不同形的问法恰恰是训练时需要的多样性。现在我的习惯是:hash 去重只删完全相同的,语义去重阈值设到 0.95 以上,宁可留一点冗余,也不误杀有效样本。语料构建这件事,自动化能省掉八成体力活,但剩下两成的判断,还是得靠人对业务的理解。希望帮到你。
本文还有配套的精品资源,点击获取