简介:这是一份基于Dify平台的大模型微调语料自动化构建实战课程PPT,面向希望降低语料构建门槛的AI工程师、算法研究员及大模型应用开发者。内容系统梳理了微调的核心价值与高质量语料标准,针对传统人工标注成本高、技术门槛高、格式不统一等痛点,重点讲解如何借助Dify低代码可视化工作流实现“文档输入→自动处理→智能生成→标准输出”的全流程自动化。课程详细拆解开始、文档提取器、代码执行、LLM与结束五个核心节点,并给出SiliconCloud Qwen2.5-72B-Instruct-128K模型调用、文本截断、JSONL格式生成等实战配置方法,同时涵盖测试验证与语料质量评估要点。资源为单个PPT课件,共1个pptx文件,压缩包大小15.21MB,结构清晰、案例完整,适合快速上手。目前已有109人浏览学习,可作为企业内训或个人技术进阶的参考。
1. 为什么微调前要先解决语料,而 Dify 就是那个“语料自动化工厂”
微调大模型翻车的人,绝大多数不是挂在模型选型上,而是挂在语料上。拿 0.6B 级别的小模型举例,它对训练集的格式和噪声敏感得离谱,同样是几百条指令-回答对,人工精修和管道自动生成的效果能差一倍。算力可以租,模型可以换,但语料不干净,后面的 LoRA 参数再怎么调都是白费。
基于 Dify 的微调语料自动化构建,核心思路是把“原始文档 → 清洗文本 → 指令-回答对 → 标准 JSONL”这条链路搬进 Dify 工作流,用 LLM 节点做清洗和样本生成,用代码节点做格式化和过滤,跑通一次之后,再来一千份新文档也只是重新执行管道。这篇文章就是一套完整的实战方案,适合手上有几十份领域文档、想微调垂直模型、又不想靠人工一页页整理样本的开发者和数据工程师。我会把节点怎么配、参数怎么设、哪些地方容易踩坑,一条条讲透。
2. 微调语料自动化构建的主链路:四层拆分与 Dify 节点选型
2.1 不要把所有文本一把丢给大模型:先拆出抽取、清洗、样本化、格式化四层
我见过最省事也最容易翻车的做法,是把一百份 PDF 直接丢给一个 LLM 节点,告诉它“帮我生成训练数据”。输出看起来有模有样,细看一半样本在复述原文,指令覆盖全是高频段落,低频知识根本没进训练集。问题不在模型,而在于这道工序没有分层。
我在实际项目里常用的四段式拆分是这样的:
- 抽取层:把 PDF、Word、HTML 里的文本解析出来,只保留正文,这一步交给 Dify 的文档提取节点。
- 清洗层:用 LLM 按规则去掉页眉页脚、目录残留、重复段落和口语杂讯,得到干净的 Markdown。
- 样本化层:基于清洗后的文本,让 LLM 生成指令-回答对,并限定不许外推。
- 格式化层:用代码节点把 LLM 输出解析成统一 schema,做长度过滤和 JSON 容错。
这四个层面的任务不能混在一个节点里。清洗和样本化消耗的 token 最多,也最需要模型有足够能力。我一般不会用将要微调的小模型来生成语料,而是用平台上的大模型,比如 Qwen-Max、DeepSeek 或 GPT 系列。语料构建是离线管道,慢一点没关系,成本完全可控。
2.2 Dify 里能承担语料流水线的三种载体
Dify 不是为数据清洗而生的,但它有工作流、知识库和对话流三个入口,组合起来刚好能覆盖语料构建的完整需求。
工作流是绝对主力。开始节点接收文件或 URL,文档提取节点把文本抽出来,后面串联 LLM 节点和代码节点。整个流程可视化,哪一步堵住了直接在画布上看到。知识库在我这里的定位是“已处理样本的存储和检索”,不建议用它做在线清洗,因为知识库文档切分会按固定 chunk 走,你控制不了粒度。对话流则适合事后人工抽检,把生成结果接成对话样式,人工扫一眼比盯着原始 JSON 舒服得多。
2.3 主链路的节点编排示例
把上面四层对应到 Dify 工作流实际节点,顺序大致是这样:
| 链路阶段 | Dify 中的载体 | 输入输出 |
|---|---|---|
| 原材料入口 | 开始节点 | 上传文件或传入 URL |
| 文本抽取 | 文档提取节点 | 文件 → 纯文本 |
| 规则清洗 | LLM 节点(温度 0.1) | 纯文本 → 干净 Markdown |
| 样本生成 | LLM 节点(温度 0.7) | 文本 → JSON 数组 |
| 格式整理 | 代码节点(Python) | JSON → 标准样本数组 |
| 输出 | 结束节点 / HTTP 节点 | JSON 供导出或落库 |
文档提取这一步让 Dify 做而不是自己写解析,是因为 PDF 和 Word 的排版差异太大,自己维护解析库的成本不低。Dify 的文档提取节点通常接的是外部抽取服务,开箱即用,但对扫描版 PDF 无效,这个坑我留到第 5 章专门说。
2.4 目标模型规模决定语料策略
语料“自动化构建”不等于“千篇一律地生成”。目标模型的参数量直接决定单条样本应该怎么写:
- 目标是 0.6B~1.5B 的小模型:单条样本要短,指令-回答对控制在 200~600 字,格式严格统一。小模型对格式泛化能力弱,你给它三种不同的 JSON 风格,它学的就是一团乱麻。
- 目标是 7B~14B 的模型:可以接受 1000~2000 字的长样本,指令可以复杂一些,比如多轮对话和条件判断。
- 目标是 70B 以上的模型:语料宽泛一点反而好,但要更关注去重和多样性,防止过拟合。
这个判断会直接影响后面章节的 prompt 模板和过滤参数,先想清楚再动手。
3. 在 Dify 工作流里跑通语料构建的最小链路:从单文档到批量迭代
3.1 最小可跑链路:开始节点、文档提取、清洗、生成、代码节点
先在 Dify 画布上新建一个空白工作流,按“开始 → 文档提取 → LLM 清洗 → LLM 样本生成 → 代码格式化 → 结束”的顺序把节点拖出来。第一次跑通不需要做得很复杂,目标是验证整条链路的数据流是通的。
开始节点里添加一个变量raw_file,类型选“文件”,用于上传测试文档。文档提取节点挂在它后面,输出变量命名为doc_text。如果你要处理的是数据库导出的文本而不是文件,也可以直接在开始节点加一个 text 变量,跳过文档提取这步。
接下来是两个 LLM 节点,我习惯给它们分别命名clean_llm和generate_llm,这样排错时一眼就能看出是哪一段出了问题。最后是一个代码节点,输入是样本生成节点的输出,我们稍后在代码节点里做 JSON 解析和过滤。
提示:不同 Dify 版本对节点名称可能略有差异,文档提取节点在部分社区版里叫“文档提取器”,功能一致,按你当前版本的实际节点为准。
3.2 清洗节点的提示词模板与三条参数设定
清洗节点的输入变量引用doc_text,提示词模板我一般这样写:
你是一个文本清洗器。把输入内容处理成适合做微调语料的干净 Markdown 文本。 规则: 1. 去掉页眉、页脚、二维码引导、目录章节号残留。 2. 删除连续重复段落,同一段落出现两次以上时只保留一次。 3. 将表格保留为 Markdown 表格,不要转成自然语言。 4. 不要改写原文,不要补充信息,不要输出任何解释。 直接输出清洗后的文本。这个节点的三个参数要特别留意。温度设 0.1,因为清洗是确定性任务,温度越高越容易让模型“发挥”,把原文给改了。最大 Token 设成至少 4096,如果文档很长,清洗输出可能比原文短不少,但模型窗口越大越不容易在长文中间截断。最后,提示词里那句“不要输出任何解释”很关键,很多模型会在清洗结果前后加“已清洗结果:”之类的废话,后面接代码解析时这些都是噪声。
3.3 样本生成节点的 JSON 约束与采样参数
样本生成节点是整条链路里最核心的环节,提示词直接决定语料质量。我常用的模板是这样:
根据下面的文本,生成 5 个指令-回答对,用于微调一个领域问答模型。 要求: 1. 指令应覆盖文本里不同的知识点,避免 5 条都问同一主题。 2. 回答必须基于原文信息,禁止编造原文没有的结论。 3. 回答控制在 80~300 字,不重复指令内容。 4. 如果原文有表格数据,至少一条指令要涉及表格提取。 只输出一个 JSON 数组,格式为: [{"instruction": "...", "input": "", "output": "..."}] 不要输出注释,不要输出 Markdown 代码块。模型选大一点的,这里的输出质量直接决定你的训练集下限,省不得。温度设 0.7 左右,太低会让 5 条样本高度雷同,太高又容易跑题。最大 Token 设 2048 到 4096,如果文档较长、模型输出被截断,后面解析会失败,第 5 章我会讲怎么切块绕开。
3.4 代码节点里做 JSON 解析与长度过滤
LLM 输出偶尔会带 ```json 标记,或者输出到一半截断,所以在代码节点里做一层容错是必须的。Dify 代码节点要求必须有一个main函数作为入口,入参是前面定义的变量。我的实现大致如下:
import json import re def main(llm_output: str) -> dict: # 去掉模型可能包裹的 ```json ``` 代码块标记 text = llm_output.strip() text = re.sub(r'^```(?:json)?|```$', '', text, flags=re.MULTILINE).strip() data = json.loads(text) samples = [] for item in data: instruction = str(item.get("instruction", "")).strip() input_text = str(item.get("input", "")).strip() output = str(item.get("output", "")).strip() # 过滤过短样本,防止空转训练 if len(instruction) < 8 or len(output) < 10: continue samples.append({ "instruction": instruction, "input": input_text, "output": output }) return {"samples": samples}这段逻辑分三层:先去掉代码块包裹,再json.loads解析,最后对每条样本做长度过滤。过滤门槛不是拍脑袋定的,太短的回答在训练时会让模型学会“糊弄”,比如输出只有十几个字。如果你发现解析经常失败,优先怀疑是模型输出被截断,把最大 Token 调大,而不是反复改正则。
3.5 从单文档到批量:迭代节点与并发控制
单文档跑通之后,批量处理是下一步。常见做法是在开始节点增加一个file_list变量,类型选“文件列表”,然后用迭代节点把它包起来。迭代节点内部就放“文档提取 → 清洗 → 样本生成 → 代码格式化”这串逻辑,每次迭代处理一个文件。
迭代节点的并行数一定要从 1 开始调。你调用的 LLM 服务本身有并发限制,并行太高会频繁触发 429 限流。我在项目里的一般做法是并行设为 2 到 3,每个文件生成 5 条样本,一批 20 个文件的语料构建任务跑下来,既不会太慢,也不会把平台打挂。
还有一个做法是外部循环调度:不把批量塞进一个工作流调用,而是写一个外层脚本循环调用 Dify 的应用 API,每次传入一个文件,拿回结果后写入本地 JSONL。这个方式的优点是好暂停、好续跑,失败了只需要重试当前文件。
4. 从“生成了不少样本”到“能拿去训练”:去重、多样性与人工抽检
4.1 生成后的样本为什么还要再过一遍规则
很多人在代码节点拿到samples数组后就以为完工了,直接拿去训练。实际上这时语料还有三个系统性问题:指令高频重复、模型自带的话术残留、以及“生成到后面越来越水”的衰减。
模型在批量生成时会有惯性,前面几条样本用了某种句式,后面会跟着模仿。所以第一道规则是去模板痕迹。我常在代码节点里加一个清洗函数:
import re def clean_model_artifacts(text: str) -> str: # 去掉常见模型习惯性前缀与后缀 text = re.sub(r"^(好的|好的,|根据上文|根据原文|以下是|综上所述)", "", text) # 合并多余空行 text = re.sub(r"\n\n+", "\n", text).strip() return text这个函数的目的不是替代 LLM 清洗,而是兜底。LLM 清洗层解决的是原文噪声,这里解决的是生成阶段的模型习惯。你去看自动生成的语料,十条里有四五条可能都以“根据原文”开头,这会让微调后的模型在回答时也带上这个腔调。
4.2 哈希去重加语义去重:双通道过滤
语料去重分两层。第一层是字符级去重,用 MD5 或 SHA1 对回答文本做哈希,完全相同的直接丢弃。第二层是语义去重,两段回答文字不同、但表达同一个意思,哈希去重发现不了,需要靠嵌入向量算相似度。
我先给一个基于外部嵌入服务的语义去重代码,放在 Dify 代码节点里,或者放到外部脚本里都可以:
import requests def embed(text: str) -> list[float]: # 调用你部署的嵌入服务,模型可以是 bge-m3 或同类中文向量模型 resp = requests.post( "http://your-embedding-service/v1/embeddings", json={"model": "bge-m3", "input": text}, timeout=30 ) return resp.json()["data"][0]["embedding"] def cosine(a, b): return sum(x * y for x, y in zip(a, b)) / ( (sum(x * x for x in a) ** 0.5) * (sum(y * y for y in b) ** 0.5) + 1e-9 ) def dedup_by_similarity(samples, threshold=0.92): kept = [] for s in samples: if not kept: s["_vec"] = embed(s["output"]) kept.append(s) continue vec = embed(s["output"]) too_similar = False for k in kept: if cosine(vec, k["_vec"]) > threshold: too_similar = True break if not too_similar: s["_vec"] = vec kept.append(s) return kept这里的阈值 0.92 是我常用的起点,低于它会把同一知识点的不同问法都误杀,高于它又放过了大量近似样本。这个值本身有玄学成分,建议你拿自己领域的数据抽几十条人工判断一下再定。还有一点要注意,这个实现是 O(n²) 复杂度,几千条样本可以接受,上了万条就先用聚类或者向量库,别硬算。
4.3 多样性不足的根源往往在输入,不在生成
清洗、去重做完之后,还经常发现样本覆盖度不够。排查到最后,问题往往出在原始输入文档本身的覆盖就不均匀:某一份文档写得详细,模型自然从里面挖出大量样本;另外几份文档内容单薄,生成的样本寥寥无几。
解决思路有两个。第一个是改写输入侧:在样本生成前,用代码节点估算每份文档的字数和知识点密度,给长文档分配更少的生成条数,给短文档分配更多条数,让最终样本的分布更平均。第二个是在提示词里做显式约束,比如把前几轮已经生成的指令列表作为上下文传给下一次生成,让模型避开已覆盖的主题,这在 Dify 工作流里可以通过引入一个全局变量来累积实现,但会额外增加 token 消耗。
4.4 把清洗后的语料导入知识库:用检索视角做人工抽检
教大家一个我常用的技巧:把生成好的一批样本作为一份文档上传到 Dify 知识库,然后开一个临时对话流关联这个知识库,直接问它“这批样本里有没有表达过于接近的”“哪些指令集中在同一主题”。Dify 知识库的语义检索会把最相近的片段捞出来,你一眼就能看到重复密度。
有人会问,这不就是多此一举吗?直接读 JSON 也能看出来。但真实场景里,一份三千条的 JSONL 靠肉眼扫是扫不出分布的,知识库检索能把“相似片段”聚合展示出来,效率完全不一样。这就是“dify 知识库流水线”在语料构建里真正的用途。
5. 语料构建避坑:Dify 环境里五个让管道停摆的典型报错
5.1 “An error occurred during credentials validation”:不一定是 Key 错了
现象:配置好模型供应商后,在工作流里运行 LLM 节点,平台直接报An error occurred during credentials validation。
原因:绝大多数时候是 base_url 填错了。很多人默认所有兼容 OpenAI 的接口都长一个样,实际上不同厂商的 API 路径差异不小,有的后端需要带/v1,有的不能带。也有的是因为 API Key 只绑定了某个模型,但你在 Dify 里配置的是另一个模型名。
解决:先去模型厂商控制台验证 Key 本身能不能调通,再用一个最小请求测 base_url 是否正确,最后回到 Dify 的模型供应商页面重新保存凭据。不要反复点重试,凭据校验接口本身也会被限流。
5.2 SSL 错误:本地部署最常见的环境问题
现象:Dify 部署在内网服务器上,调用外部大模型 API 时客户端报 SSL 相关错误,日志里能看到证书链或 hostname mismatch 的信息。
原因:大部分是公司网络出口做了代理,代理用自签名证书替换了原证书;也有的是 Dify 服务本身的域名证书过期,内部模块之间互相调用时失败。
解决:不要一上来就关闭证书校验。先确认是哪个环节报的错,如果是 Dify 容器访问外部 API,就把公司根证书导入 Dify 容器;如果是平台自身的 HTTPS 域名证书问题,更新证书后重启容器。只有当目标服务明确不使用有效证书的纯测试环境里,我才会考虑临时绕开校验,生产环境不建议这么干。
5.3 “unstructured api url is not configured”:文档处理服务没配对
现象:知识库或文档提取节点处理.pdf、.docx时,直接提示unstructured api url is not configured for doc file processing.。
原因:Dify 的文档抽取支持多种后端,当你在系统设置里把解析方式切到 Unstructured 后,没有配置对应的服务地址和 API Key,后端就会拒绝处理。
解决:如果你没有扫描版 PDF 的 OCR 需求,就把文档解析方式切回 Dify 内置解析器,不用引入额外服务。如果你确实要处理扫描版 PDF,那才是 Unstructured 的适用场景,需要自部署一个 Unstructured 服务并把 URL 填进配置。这里容易绕弯路,记住一个原则:内置解析器能处理的,就不要为了“高级”去外接服务。
注意:扫描版 PDF 就算换了 Unstructured,默认配置下没有 OCR 也一样抽不出文字。有这种需求时,要确认你部署的 Unstructured 带 OCR 组件。
5.4 长文档生成样本越到后面越乱:上下文超限与输出截断
现象:跑一份几十页的文档,前几个样本质量不错,到后边开始出现 JSON 解析失败,或者回答明显变短、格式走样。
原因:LLM 有上下文窗口限制,输入越长,留给输出空间就越小。样本生成节点设了 4096 最大 Token,但文档全文可能早就超过模型窗口,后段内容根本没有被模型看到,生成的样本自然越来越水。
解决:在工作流里加一个代码节点,按标题##或段落标记把清洗后的文本切块,每块控制在 2000 到 3000 字。然后让迭代节点按块去生成样本,而不是按整篇文档生成。切块之后,每个块最多生成 3 到 5 条样本,覆盖度反而比整篇硬跑高不少。
5.5 批量迭代触顶限流:并发不是越大越好
现象:批量任务跑起来之后,Dify 界面没有报错,但样本生成速度越来越慢,日志里出现 429 或 timeout;社区版开了多租户的话,其他项目还会来投诉。
原因:LLM 供应商的 API 是按并发数和每分钟请求数双维度限流的,工作流迭代并行度设置太高,直接把请求打满了。另外社区版本身并发能力有限,语料构建这种批量任务非常吃资源,和多租户其他业务共用一套环境,很容易互相拖垮。
解决:把迭代节点的并行度降下来,维持在 1 到 2 比较稳妥。大批量任务放到业务低峰期跑,或者像我前面说的,外部脚本串行调用应用 API,每个文件跑完再提交下一个,失败了单独重试。语料构建不是线上服务,不需要实时响应,慢就是快。
6. 语料验收与微调框架对接:用一百条数据先跑通 LLaMA-Factory 冒烟测试
6.1 训练前先算五个指标,别急着烧卡
语料生成完之后,先写几行脚本把基础指标算出来。我每次训练前都会过一遍这五个数:总样本数、指令平均长度、回答平均长度、完全重复率、以及字段缺失率。任何一项异常都先处理再训练。
import json import hashlib import statistics samples = [json.loads(line) for line in open("train.jsonl", encoding="utf-8")] lengths = [len(s["output"]) for s in samples] hashes = [hashlib.md5(s["output"].encode()).hexdigest() for s in samples] print("总样本数:", len(samples)) print("回答平均长度:", round(statistics.mean(lengths), 1)) print("完全重复样本数:", len(samples) - len(set(hashes))) missing = [s for s in samples if not s.get("instruction") or not s.get("output")] print("字段缺失样本数:", len(missing))输出只要有一项明显偏高,就先回头查生成节点。比如重复样本占比超过 5%,说明语义去重还没做好;回答平均长度远低于你设定的 80 字下限,则要检查是不是代码节点的过滤条件写错了。
6.2 转换成 LLaMA-Factory 能直接读取的 ShareGPT 格式
我习惯把 Dify 导出成 Alpaca 格式的样本,在进入 LLaMA-Factory 前转成 ShareGPT 格式,因为多轮对话数据用 ShareGPT 表示更自然。转换脚本很简单:
import json alpaca_samples = json.load(open("samples.json", encoding="utf-8")) sharegpt_samples = [] for s in alpaca_samples: sharegpt_samples.append({ "conversations": [ {"from": "human", "value": s["instruction"]}, {"from": "assistant", "value": s["output"]} ] }) with open("train.jsonl", "w", encoding="utf-8") as f: for item in sharegpt_samples: f.write(json.dumps(item, ensure_ascii=False) + "\n")转换之后,在 LLaMA-Factory 的dataset_info.json里注册这个数据集,格式声明为sharegpt并映射好角色标签。如果你手里的 LLaMA-Factory 版本不同,字段名可能有差异,以你自己跑的工程提示为准。
6.3 用一百条样本先做冒烟测试
我的习惯是永远先用小批量跑通训练链路,再上全量数据。在 LLaMA-Factory 里配一个最小训练配置,模型用你下载好的 Qwen3-0.6B 本地路径,数据集只保留train.jsonl里的前一百条,LoRA rank 设 8,学习率 1e-4,训 3 个 epoch。这轮训练的意义完全不在效果,而是验证数据格式、模板匹配和训练脚本这三件事有没有问题。
model_name_or_path: /models/Qwen3-0.6B dataset: train.jsonl template: qwen lora_rank: 8 learning_rate: 1e-4 num_train_epochs: 3.0 output_dir: ./output/smoke_test然后执行llamafactory-cli train train.yaml。如果跑完没有报错,再打开output/smoke_test里保存的日志看 loss 是否正常收敛。冒烟测试通过后,我会把全量语料重新跑一遍统计脚本,再正式训练。这个顺序帮我挡下过至少三次因数据格式错误导致的返工。
我现在的习惯是,不管语料生成管道跑得多顺,每次训练前都抽五十条人工读一遍,重点看指令覆盖和回答是否忠于原文。真实踩过的教训是,有一版自动生成的语料九成都在重复同一批高频问答,低频业务概念全被漏掉,改完生成提示词之后模型才真正“懂业务”。把验证写进流程,比多堆几千条样本重要得多。希望帮到你。
本文还有配套的精品资源,点击获取