1. 为什么“合成+清洗”成了训练数据流水线的主战场
过去两年,我参与过好几个从零起步的模型训练项目,从最早的纯人工标注,到后来的规则清洗,再到现在的 agentic 合成加自动清洗,最大的感受就是:数据工程的重心已经从“标得多”转向“造得准、洗得净”。尤其是 SFT、mid-training、RL 这三个阶段对数据的需求差异极大,如果还用一套静态数据集去喂所有阶段,模型表现基本会卡在某个瓶颈上不去。
这个项目标题里的“agentic 方式合成 / 清洗训练数据”,说白了就是让多个具备不同职责的智能体(agent)组成一条流水线,有的负责按目标分布生成候选样本,有的负责质量打分和去重,有的负责改写和增强,最终产出可以直接进入 SFT、mid-training 或 RL 训练循环的数据。它解决的核心问题是:高质量训练数据的供给速度跟不上模型迭代速度。适合谁参考?如果你正在做垂直领域模型微调、正在搭建自己的数据飞轮、或者被 RL 阶段的 reward 数据稀缺卡住,这套思路可以直接抄作业。
我先把结论放在前面:agentic 数据流水线不是让一个大模型随便生成一堆文本就完事,它的关键在于角色分工、质量闭环和阶段适配。下面我会从整体设计、核心细节、实操过程和踩坑记录四个层面,把这条流水线拆开讲清楚。
2. 整体设计与思路拆解:为什么是 agentic,而不是单模型批量生成
2.1 从“单次生成”到“多角色协作”的动机
最早我做数据合成的时候,就是拿一个基础模型,写一个 prompt 模板,批量跑几万条。结果很明显:多样性差、格式漂移严重、错误模式高度重复。比如让模型生成中医问答数据,它会在几十条里反复用同一个句式,甚至把同一个方剂的功效抄来抄去。后来我改成多模型投票,好了一些,但成本上去了,而且投票只能筛选,不能主动补足短板。
Agentic 方式的核心区别在于:每个 agent 有明确的职责边界和可观测的输出契约。我通常会设计至少四类角色:
- Generator Agent:负责按指定领域、难度、风格生成候选样本。
- Critic Agent:负责从事实性、一致性、格式、安全性等维度打分。
- Rewriter Agent:负责对低分但可修复的样本进行定向改写。
- Deduplicator / Cluster Agent:负责语义去重和分布均衡。
这四类角色不是简单串行,而是形成一个带反馈的闭环。Generator 生成一批,Critic 打分,Rewriter 修复边缘样本,Deduplicator 做最终过滤,同时把统计信息反馈给 Generator 调整下一轮的生成策略。这样做的好处是:你不需要一次性把 prompt 写到完美,系统会自己往目标分布收敛。
2.2 SFT、mid-training、RL 三个阶段的数据需求差异
很多人把这三个阶段的数据混在一起处理,这是个大坑。我自己的经验是,它们对数据的要求几乎相反:
| 阶段 | 数据目标 | 样本特点 | 常见错误 |
|---|---|---|---|
| SFT | 教会模型“怎么答” | 高质量、格式统一、覆盖任务类型 | 混入低质样本导致格式崩溃 |
| mid-training | 注入领域知识 | 量大、多样、知识密度高 | 知识重复、事实错误未清洗 |
| RL | 提供偏好信号 | 成对/多路对比、区分度强 | 偏好对太容易区分,reward 学不到东西 |
SFT 阶段最怕的是格式噪声。你辛辛苦苦调好的对话模板,如果合成数据里混入了奇怪的换行、多余的角色标记,模型很快就会学会这些坏习惯。所以 SFT 数据的清洗重点在格式归一化和角色一致性校验。
Mid-training 阶段最怕的是知识重复和事实错误。这个阶段数据量通常很大,如果不去重,模型会在某些高频知识上过拟合,而在长尾知识上表现很差。我的做法是用 embedding 聚类加 MinHash 做两级去重,同时对事实性陈述用 Critic Agent 做抽查。
RL 阶段最怕的是偏好对区分度不足。如果你合成的 chosen 和 rejected 差别太明显,reward model 学不到细粒度偏好。所以 RL 数据的合成要刻意控制难度,让 rejected 样本在某个维度上只差一点,而不是完全胡言乱语。
2.3 流水线的整体架构与数据流
我实际跑通的架构大致是这样的:
种子池 -> Generator(多策略) -> 候选池 | v Critic(多维度打分) | +-------+-------+ | | 高分样本 边缘样本 | | | Rewriter | | +-------+-------+ | v Deduplicator/Cluster | v 阶段适配器(SFT/mid/RL) | v 最终数据集这个架构里,阶段适配器是一个容易被忽略但非常关键的模块。它负责把通用清洗后的数据,按照 SFT、mid-training、RL 的不同要求做最后的格式转换和采样。比如同一批知识问答数据,进 SFT 要转成对话格式,进 mid-training 要转成纯文本续写格式,进 RL 要构造成偏好对。如果没有这个适配层,你会在每个阶段重复做大量转换工作。
3. 核心细节解析与实操要点:每个 agent 到底怎么设计
3.1 Generator Agent 的多样性控制策略
Generator 最大的挑战不是生成不出来,而是生成得太像。我试过几种策略,最后稳定下来的是“多模板+多模型+多温度”的组合:
- 多模板:同一个领域准备 5 到 10 个不同的 prompt 模板,有的侧重问答,有的侧重步骤说明,有的侧重对比分析。
- 多模型:至少用两个不同来源的模型做生成,避免单一模型的偏见。
- 多温度:同一模板下用 0.7、0.9、1.1 三档温度各跑一遍,低温度保质量,高温度保多样性。
这里有个实操细节:不要把所有生成结果直接混在一起。我会给每条样本打上来源标签(模板ID、模型ID、温度档),这样在后续 Critic 打分后,如果发现某个来源的样本普遍低分,可以直接调整该来源的权重或下线。
另外,Generator 的 prompt 里一定要包含负面约束。比如生成中医问答时,我会明确写“不要出现‘综上所述’、‘总之’这类总结词,不要重复同一句式超过两次”。实测下来,加了负面约束后,后续清洗的工作量能减少三成左右。
3.2 Critic Agent 的打分维度与校准方法
Critic 是整条流水线的质量守门员。我一般让它从五个维度打分,每个维度 1 到 5 分:
- 事实一致性:样本内容是否与给定知识源一致。
- 格式合规性:是否符合目标阶段的格式要求。
- 信息密度:是否包含有效信息,还是空话套话。
- 多样性:与同批次其他样本的差异程度。
- 安全性:是否符合内容规范。
这里的关键是校准。Critic 本身也是模型,它的打分会有漂移。我的做法是:先人工标注 200 条样本作为校准集,然后让 Critic 打分,计算它与人工打分的相关性。如果某个维度的相关性低于 0.6,就调整该维度的 prompt 描述,或者换成更细粒度的评分标准。
还有一个实用技巧:让 Critic 输出打分理由。不要只让它给一个分数,而是要求它用一句话说明为什么给这个分。这样在排查问题时,你能快速定位是 Critic 判断错了,还是样本本身有问题。
3.3 Rewriter Agent 的定向修复逻辑
Rewriter 不是万能的,它只应该处理可修复的边缘样本。什么叫可修复?比如格式不对但内容正确、事实基本正确但表述有歧义、信息密度低但可以通过补充细节改善。如果样本的事实完全错误,或者安全性有问题,直接丢弃,不要浪费算力去改写。
我通常给 Rewriter 的输入包含三部分:原始样本、Critic 的打分和理由、以及具体的修复指令。修复指令要具体,比如“把第二段的被动句改成主动句”、“补充一个具体的例子来说明这个方剂的适用场景”。实测下来,带具体指令的 Rewriter 修复成功率比只给原始样本高很多。
注意:Rewriter 改写后的样本必须重新过一遍 Critic,不能直接进入下一环节。我踩过的坑就是有一次为了省算力跳过了复检,结果一批改写样本里混入了事实错误,导致后续 SFT 模型在某个知识点上出现了系统性偏差。
3.4 Deduplicator 的语义去重与分布均衡
去重这件事,字面去重远远不够。我一般做三层:
- 精确去重:MD5 或 SHA1 哈希,去掉完全相同的样本。
- 近邻去重:MinHash + LSH,去掉高度相似的样本,阈值一般设在 0.85 左右。
- 语义聚类:用 embedding 做聚类,然后对每个簇做采样,保证最终数据集的分布均衡。
第三层最容易被忽略,但效果最明显。比如你合成了一万条数据,其中三千条都在讲同一个知识点,如果不做聚类采样,模型就会在这个知识点上过拟合。我的做法是:对每个语义簇,最多保留 N 条(N 根据总数据量和簇数量动态调整),其余丢弃或降权。
4. 实操过程与核心环节实现:从零跑通一条流水线
4.1 环境准备与依赖选型
我用的技术栈比较轻量,核心就是 Python + 几个常用库:
pip install openai datasets sentence-transformers faiss-cpu minhash tqdm如果你要处理中文数据,建议再加一个中文分词和 embedding 模型。我一般用text2vec-base-chinese做语义向量,用jieba做分词辅助。存储方面,中间结果用 JSONL 格式落盘,方便断点续跑。
提示:不要把所有中间结果放在内存里。我早期跑一万条数据的时候,因为把候选池全放内存,跑到一半 OOM 了,前功尽弃。后来改成每处理 500 条就落盘一次,稳很多。
4.2 种子池构建与初始分布设计
种子池是整个流水线的起点。我的经验是:种子不需要多,但一定要准。一般准备 50 到 200 条高质量种子就够了,关键是覆盖你想要的领域和任务类型。
种子来源可以是人工编写的少量示例、已有高质量数据集中的抽样、或者从领域文档中提取的问答对。我会给每条种子打上标签,比如领域、难度、任务类型,这样在后续生成时可以按标签做分层采样,保证初始分布就接近目标分布。
4.3 多轮生成与 Critic 闭环的代码骨架
下面是我常用的一个简化版代码骨架,展示 Generator 和 Critic 的闭环逻辑:
import json from tqdm import tqdm def generate_batch(seeds, generator, batch_size=50): candidates = [] for seed in seeds: for template in generator.templates: for temp in [0.7, 0.9, 1.1]: output = generator.run(seed, template, temperature=temp) candidates.append({ "text": output, "source": f"{template.id}_{temp}", "seed_id": seed.id }) return candidates def critic_filter(candidates, critic, threshold=3.5): high, edge = [], [] for item in tqdm(candidates): scores = critic.score(item["text"]) item["scores"] = scores avg = sum(scores.values()) / len(scores) if avg >= threshold: high.append(item) elif avg >= 2.5: edge.append(item) return high, edge def run_pipeline(seeds, generator, critic, rewriter, dedup, rounds=3): all_high = [] for r in range(rounds): candidates = generate_batch(seeds, generator) high, edge = critic_filter(candidates, critic) rewritten = [rewriter.fix(item) for item in edge] high_rewritten, _ = critic_filter(rewritten, critic) all_high.extend(high + high_rewritten) seeds = adjust_seeds(seeds, high, edge) final = dedup.process(all_high) return final这个骨架里,adjust_seeds是根据本轮高分样本的分布,调整下一轮种子的采样权重。比如某个领域的样本高分多,下一轮就多采样该领域的种子。
4.4 阶段适配器的实现与参数选择
阶段适配器我一般写成三个独立函数,分别处理 SFT、mid-training 和 RL 的数据格式:
def adapt_sft(samples): return [{"messages": [ {"role": "user", "content": s["question"]}, {"role": "assistant", "content": s["answer"]} ]} for s in samples] def adapt_midtraining(samples): return [{"text": f"{s['question']}\n{s['answer']}"} for s in samples] def adapt_rl(samples): pairs = [] for s in samples: if "rejected" in s: pairs.append({ "prompt": s["question"], "chosen": s["answer"], "rejected": s["rejected"] }) return pairsRL 的偏好对构造是最麻烦的。我的做法是:对同一个 prompt,让 Generator 生成多个回答,然后让 Critic 打分,选最高分作为 chosen,选一个分数中等但不太差的作为 rejected。这样构造出来的偏好对区分度适中,reward model 能学到细粒度偏好。
4.5 数据质量抽检与迭代节奏
流水线跑起来之后,抽检不能停。我一般每产出 1000 条数据,就随机抽 50 条人工检查。检查的重点是:事实错误率、格式错误率、以及是否有重复模式。如果事实错误率超过 5%,就要回头调整 Critic 的 prompt 或阈值;如果格式错误率超过 2%,就要检查阶段适配器的转换逻辑。
迭代节奏上,我建议小步快跑。不要一次性生成十万条再清洗,而是每轮生成五千到一万条,清洗后立即用于训练,观察模型表现,再决定下一轮的生成策略。这样虽然麻烦一点,但能避免大量无效数据浪费算力。
5. 常见问题与排查技巧实录
5.1 合成数据多样性不足的排查思路
多样性不足是最常见的问题,表现是模型在某个任务上表现还行,但换个问法就崩。排查思路:
- 先看 Generator 的模板数量,如果少于 5 个,基本可以确定是模板太少。
- 再看温度设置,如果全用 0.7 以下,多样性肯定不够。
- 最后看种子池,如果种子本身就很相似,生成结果也很难多样。
我的解决方法是:强制分层采样。在生成阶段,按种子标签分层,每个标签下至少生成 N 条,保证覆盖度。同时,在 Critic 阶段加入多样性打分,对与已有样本过于相似的候选直接降分。
5.2 Critic 打分漂移与校准技巧
Critic 打分漂移的表现是:同一批数据,今天打分高,明天打分低。这通常是因为 Critic 的 prompt 不够具体,或者评分标准太模糊。
我的校准技巧是:用锚点样本。准备 10 条已知分数的样本,从 1 分到 5 分各两条,每次 Critic 打分前,先把这 10 条喂给它,让它校准评分尺度。实测下来,加了锚点后,打分漂移能减少一半以上。
5.3 RL 偏好对区分度不够的调整方法
RL 偏好对区分度不够,reward model 就会学不到东西。表现是训练 loss 下降很慢,或者 reward 分数分布很窄。
调整方法:控制 rejected 样本的质量。不要让 rejected 太差,也不要让它和 chosen 太接近。我的经验是,rejected 的 Critic 平均分控制在 chosen 的 60% 到 80% 之间比较合适。如果 rejected 分数太低,就重新生成一个中等质量的;如果太接近,就换一个维度构造差异。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 模型格式崩溃 | SFT 数据混入格式噪声 | 抽检 50 条看角色标记 | 加强格式校验,重跑适配器 |
| 知识重复过拟合 | mid-training 数据未去重 | 统计高频知识点占比 | 加语义聚类采样 |
| Reward 学不动 | 偏好对区分度不足 | 看 reward 分数分布 | 调整 rejected 质量 |
| 生成多样性差 | 模板少或温度低 | 统计模板和温度分布 | 增加模板,提高温度档 |
| Critic 打分漂移 | 评分标准模糊 | 对比锚点样本分数 | 加锚点校准 |
5.5 几个我踩过的坑
第一个坑是过早优化 Critic。我一开始花了很多时间调 Critic 的 prompt,想让它的打分和人工完全一致。后来发现,Critic 只要能把明显低质的样本筛掉就够了,剩下的边缘样本交给 Rewriter 和人工抽检。追求 Critic 完美,投入产出比很低。
第二个坑是忽略阶段适配器的测试。有一次我直接拿 mid-training 格式的数据去跑 SFT,结果模型完全学不会对话格式。后来我养成了习惯:每次适配器写完,先拿 100 条数据做小规模训练测试,确认格式没问题再全量跑。
第三个坑是去重阈值设得太高。我一开始把 MinHash 阈值设到 0.95,结果去重后数据量几乎没变,但训练时还是出现过拟合。后来降到 0.85,效果好很多。阈值这个东西,要根据你的数据特点和模型表现来调,没有万能值。
6. 写在最后:一些个人体会
这套 agentic 数据流水线我前后迭代了大概半年,最大的体会是:数据工程没有一劳永逸的方案,只有持续迭代的闭环。你今天调好的 Critic,明天可能因为 Generator 换了模型就失效了;你今天觉得完美的去重阈值,下周可能就因为数据分布变化而不适用了。
所以我的建议是:把流水线做成可配置、可观测、可回滚的。每个环节的参数都放在配置文件里,每次跑完都记录关键指标(生成量、高分率、去重率、抽检错误率),这样出问题的时候能快速定位。另外,不要追求一次性生成海量数据,小步快跑、边训边调,比憋大招靠谱得多。
最后分享一个小技巧:如果你觉得 Critic 的打分不够准,可以试试让它先分类再打分。比如先判断样本属于哪个任务类型,再在该类型下打分。这样比直接打分的准确率高不少,尤其是在多任务混合的数据集上。