Agentic合成与清洗:SFT、mid-training、RL训练数据管线实战
1. 为什么“合成清洗”成了训练数据的新主线过去两年我参与过好几个从零起步的模型训练项目从最早的纯人工标注到后来的规则清洗再到现在的 agentic 合成加自动清洗最大的感受就是数据工程的重心正在从“标”转向“造筛”。尤其是 SFT、mid-training、RL 这三个阶段对数据的需求差异极大靠一套人工标注流程根本喂不饱。先把这个标题拆开看。“agentic 方式合成 / 清洗训练数据”本质上说的是用具备自主决策能力的智能体agent去完成两件事——一是生成训练样本二是过滤训练样本。它服务的对象是三个训练阶段SFT监督微调、mid-training中期训练介于预训练和微调之间的持续训练阶段、RL强化学习这里主要指 RLHF 或可验证奖励的 RL。为什么现在大家都在往这个方向走我总结下来有三个现实原因。第一人工标注的成本和速度已经跟不上模型迭代。一个中等规模的 SFT 数据集动辄几万到几十万条纯人工标注周期以月计而模型版本可能两周就换一次。合成数据可以把周期压缩到天级别。第二通用合成数据的质量参差不齐。早期用大模型直接批量生成出来的东西同质化严重、事实错误多、格式漂移。agentic 方式的核心改进在于让 agent 带着工具、带着验证步骤去生成生成完自己先过一遍而不是一次性吐出来。第三不同训练阶段对数据的要求完全不同。SFT 要的是“指令-回答”配对的质量和多样性mid-training 要的是领域知识的密度和覆盖面RL 要的是可验证、有明确奖励信号的样本。用同一套数据管道硬套三个阶段效果一定打折。这篇文章我会按我实际搭过的一套流程来讲覆盖 agentic 合成的设计思路、清洗管线的搭建、三个阶段各自的数据处理要点以及我在实操中踩过的坑。适合正在做模型训练数据工程、或者准备从人工标注转向合成清洗路线的同学参考。哪怕你只做其中某一个阶段里面的清洗策略和排查技巧也能直接拿去用。2. 整体方案设计agentic 数据管线的骨架2.1 从“一次性生成”到“生成-验证-修正”闭环传统合成数据的做法很简单写一个 prompt 模板调模型批量生成然后做去重和长度过滤就完事。这套做法在 2023 年还能凑合用现在基本不行了。问题出在没有反馈回路——模型生成时不知道自己对不对生成完也没有机制去纠正。agentic 方式的核心区别在于把“生成”拆成了一个多步闭环。我实际用的骨架是这样的规划Planagent 先根据目标数据规格决定这批数据要覆盖哪些子任务、哪些难度层级、哪些格式。生成Generate按规划逐条或逐批生成生成时可以调用外部工具检索、计算器、代码执行。验证Verify用规则校验 模型自检 交叉验证三层机制判断样本是否合格。修正Revise不合格的样本不是直接丢弃而是带着验证反馈让 agent 重写通常一到两轮就能救回大部分。归档Archive合格的进正式数据集不合格但接近合格的进“待人工复核”池。这个闭环的价值在于把丢弃率降下来。我实测过纯一次性生成的样本合格率大概在 40% 到 60% 之间取决于任务难度加上验证-修正闭环后最终可用率能到 75% 以上。别小看这十几个百分点放到十万条规模上就是几万条有效数据的差距。2.2 三个阶段的数据规格差异在动手写 agent 之前必须先明确三个阶段各自要什么。我整理了一张对照表这是我反复调整后觉得最实用的版本维度SFTmid-trainingRL数据形态指令-回答配对长文本/领域语料问题-候选回答-奖励核心诉求多样性、指令遵循知识密度、覆盖度可验证、奖励区分度单条长度短到中几百到几千 token长几千到上万 token中含推理链质量门槛高中高极高错误样本会污染奖励合成占比可较高中等需谨慎优先真实验证清洗重点格式统一、去重、去模板化去噪、去重、知识准确性奖励信号一致性、去偏这张表是我做方案设计时的起点。不同阶段不能用同一套清洗规则这是很多人容易犯的错。比如 SFT 阶段特别怕“模板化”因为模型会学到千篇一律的开头而 mid-training 阶段更怕“知识错误”因为那会直接污染模型的领域知识。2.3 工具选型与 agent 框架的取舍agent 框架这块我用过几种不同的组织方式最后落在一个比较朴素的方案上不追求复杂框架用轻量的编排 明确的工具接口。原因很实际——数据合成是批处理任务不是交互式应用引入重型 agent 框架反而增加调试成本。我的选型逻辑是这样的编排层用简单的状态机或 DAG 描述流程每一步的输入输出都是可序列化的。这样出问题能精确定位到某一步而不是在一个黑盒 agent 里瞎找。模型层生成用一个较强的模型验证可以用稍弱的模型省钱且够用关键验证步骤再上强模型。工具层检索、代码执行、格式校验器都封装成统一接口agent 通过函数调用触发。存储层中间产物全部落盘每一步都可回溯。这点非常重要后面排查问题全靠它。提示不要一上来就追求全自动。我建议先把“生成”和“验证”两步跑通人工抽检确认质量后再逐步加“修正”和“归档”环节。一次性搭全流程出问题时你根本不知道是哪一环的锅。3. 核心细节解析合成与清洗的关键环节3.1 合成阶段怎么让 agent 生成“不像 AI 写的”数据合成数据最大的通病是“AI 味”——开头永远是“当然可以”结尾永远是“希望对你有所帮助”中间结构高度雷同。这种数据拿去训 SFT模型会学出一身毛病。我在合成阶段做了几件事来对抗这个问题第一给 agent 注入风格多样性。不是简单地在 prompt 里写“请用不同风格”而是准备一个风格池每条数据随机采样一种风格标签比如“口语化”“学术严谨”“简短直接”“带反问”“分点陈述”等。agent 拿到具体标签后再生成多样性明显提升。第二用真实数据做 few-shot 锚点。纯靠 prompt 描述风格模型理解会漂移。我会从真实数据里挑几条高质量样本作为参考让 agent 模仿其语气和结构但内容必须不同。这一步对“去 AI 味”效果最明显。第三控制生成长度分布。模型默认倾向于生成中等长度的回答导致长度分布过于集中。我会在规划阶段就指定每条数据的长度区间强制覆盖短、中、长三档。实测下来长度分布的方差对下游训练效果有肉眼可见的影响。第四引入“反向生成”。对于指令类数据除了“给指令生成回答”我还会让 agent“给回答反推指令”。两个方向生成的数据混合使用指令的多样性会好很多。3.2 清洗阶段三层过滤机制的设计清洗是整条管线里最容易被低估的环节。很多人以为清洗就是去重加长度过滤实际上远不止。我用的三层过滤机制是这样的第一层硬规则过滤。这一层是确定性的不涉及模型判断速度快、成本低。包括长度过滤低于阈值或超过阈值的直接丢格式校验JSON 是否合法、字段是否齐全、特殊字符是否异常精确去重哈希比对完全相同的直接去敏感词与乱码检测正则匹配异常字符第二层模型打分过滤。用一个专门的打分模型对每条数据打质量分维度包括相关性、准确性、完整性、流畅度。低于阈值的进待复核池。这一层的成本比第一层高但比人工便宜太多。第三层交叉一致性过滤。对同一指令让 agent 生成多个候选回答然后比较它们的一致性。如果多个回答在关键事实上矛盾说明这条数据不可靠直接丢弃或标记。这一层对 mid-training 和 RL 数据尤其重要。三层过滤的阈值不是拍脑袋定的。我的做法是先人工标注一小批比如 500 条作为金标准然后调整各层阈值让过滤结果和金标准的一致性达到可接受水平再全量跑。3.3 去重不只是精确匹配那么简单去重这件事我踩过的坑最多。精确去重只能去掉完全一样的但合成数据里大量存在“换汤不换药”的近似重复——换个说法、调个顺序、改几个词语义几乎一样。我用的去重策略是分级的精确去重哈希处理完全相同的近似去重用 MinHash 或 SimHash 做指纹处理高相似度的语义去重用 embedding 计算余弦相似度超过阈值的聚成一类每类只保留质量最高的几条语义去重的阈值需要按任务调。太松了去不干净太紧了会把本来有价值的相似样本也删掉。我的经验是SFT 数据阈值可以设紧一点比如 0.92因为指令多样性很重要mid-training 数据可以松一点比如 0.95因为知识本身就有重复出现的合理性。注意去重一定要在清洗的后期做不要一上来就去重。因为前面的过滤会改变数据分布早期去重可能把一些“看起来重复但过滤后只剩一条”的样本误删。3.4 三个阶段的数据配比与混合策略合成数据和真实数据的配比是另一个需要反复实验的点。我的经验值是这样的SFT合成数据占比可以到 50% 到 70%但必须保证真实数据打底。纯合成训出来的模型容易“飘”在真实场景下表现不稳定。mid-training合成占比建议控制在 30% 到 50%。这个阶段模型在吸收领域知识合成数据的知识准确性风险更高真实语料的权重应该更大。RL合成占比要更谨慎尤其是奖励信号相关的部分。我的做法是问题可以用合成的但奖励必须来自可验证的信号比如代码执行结果、数学答案比对不能靠模型自己打分。混合的时候还要注意难度分层。我通常把数据按难度分成易、中、难三档训练时按课程学习的思路逐步引入。这个在 SFT 和 RL 阶段效果都比较明显。4. 实操过程从零搭一条可复现的管线4.1 环境与依赖准备先把基础环境列一下。这套管线对硬件要求不算高主要是模型推理的开销。我用的配置是# 核心依赖 python3.10 pydantic # 数据结构校验 datasets # 数据加载与处理 faiss-cpu # 语义去重的向量检索 simhash # 近似去重 tqdm # 进度显示模型推理部分生成和验证可以走同一套接口只是模型不同。我建议把模型调用封装成一个带重试和限流的客户端因为批量生成时网络抖动和限流是常态。class ModelClient: def __init__(self, model_name, max_retries3): self.model_name model_name self.max_retries max_retries def generate(self, prompt, temperature0.8): for attempt in range(self.max_retries): try: # 实际调用逻辑带超时控制 return self._call(prompt, temperature) except Exception as e: if attempt self.max_retries - 1: raise time.sleep(2 ** attempt) # 指数退避提示批量生成时一定要做断点续跑。我早期没做这个跑到一半挂了要从头再来浪费了大量时间和额度。做法很简单每条数据的处理结果落盘重跑时先检查是否已处理。4.2 合成 agent 的编排实现合成 agent 我拆成了四个可独立测试的模块每个模块的输入输出都是明确的 JSON 结构。规划模块负责生成“数据规格清单”。输入是任务描述和目标条数输出是一个规格列表每条规格包含子任务类型、难度、长度区间、风格标签。def plan_batch(task_desc, total_count): prompt f 任务{task_desc} 需要生成 {total_count} 条训练数据。 请输出一个规格清单每条规格包含 - subtask: 子任务类型 - difficulty: easy/medium/hard - length_range: [min_tokens, max_tokens] - style: 风格标签 要求覆盖不同子任务和难度输出 JSON 数组。 return model_client.generate(prompt)生成模块按规格逐条生成。这里的关键是把规格作为强约束传给模型而不是让模型自由发挥。验证模块做三层检查返回一个结构化的验证结果包含是否通过、失败原因、修正建议。修正模块接收原始样本和验证反馈重写样本。我限制最多修正两轮超过就丢弃避免无限循环。4.3 清洗管线的代码骨架清洗管线我用的是流水线式设计每一步是一个独立的处理函数数据在步骤间流转。def clean_pipeline(raw_data): # 第一层硬规则 data filter_by_length(raw_data, min_len50, max_len8000) data filter_by_format(data, required_fields[instruction, response]) data filter_by_regex(data, blacklist_patterns) # 第二层模型打分 data score_by_model(data, threshold0.6) # 第三层交叉一致性 data check_consistency(data, min_agreement0.7) # 去重放在最后 data dedup_exact(data) data dedup_near(data, threshold0.9) data dedup_semantic(data, threshold0.92) return data每一步都要记录输入条数、输出条数、丢弃原因分布。这个统计信息是后续调优的依据。我一般会把统计结果存成表格跑完一轮就能看出哪一步过滤太狠或太松。4.4 参数计算阈值怎么定才不拍脑袋阈值定不好整条管线要么漏放要么误杀。我的做法是用小样本金标准 网格搜索。具体步骤人工标注 300 到 500 条样本标为“合格”或“不合格”。对每个阈值参数在候选区间内网格搜索计算过滤结果与金标准的 F1。选 F1 最高的阈值同时看误杀率和漏放率是否在可接受范围。以模型打分阈值为例我试过 0.5 到 0.8 的区间步长 0.05。结果发现 0.6 附近 F1 最高但误杀率偏高0.65 时 F1 略降但误杀率明显下降。最后我选了 0.65因为误杀高质量数据的代价比漏放低质量数据更高——漏放的还能在后续环节被拦误杀的就永远丢了。这个权衡逻辑在数据清洗里很通用宁可漏放不可误杀。因为漏放有补救机会误杀没有。4.5 实操现场一次完整的批次运行记录我拿一个实际的批次跑一遍把关键数字记下来方便你对照。任务合成 5000 条 SFT 指令数据领域是技术问答。规划阶段生成 5000 条规格耗时约 3 分钟生成阶段5000 条全部生成耗时约 40 分钟并发 8验证阶段通过 3120 条不通过 1880 条修正阶段1880 条中修正后通过 1240 条最终通过 4360 条硬规则过滤4360 条剩 4180 条丢弃 180 条主要是长度和格式问题模型打分4180 条剩 3620 条丢弃 560 条质量分低于 0.65交叉一致性3620 条剩 3410 条丢弃 210 条多候选矛盾去重3410 条剩 2980 条精确去重 90 条近似去重 180 条语义去重 160 条最终可用 2980 条从 5000 条原始生成到 2980 条可用整体可用率约 60%。这个数字在合成数据里算不错的主要归功于验证-修正闭环。5. 常见问题与排查技巧实录5.1 合成数据“同质化”严重怎么办这是最常见的问题。表现是生成的数据读起来都差不多开头结尾高度雷同换个指令但回答结构一模一样。排查思路先看是不是 prompt 模板太死。如果模板里把结构写死了模型当然只会照着填。解决方法是把结构约束改成风格约束给模型更多自由度。如果 prompt 已经比较灵活但还是同质化那可能是温度参数太低。生成阶段温度可以设到 0.8 到 1.0验证阶段再调低。我见过有人生成和验证用同一个温度结果生成多样性不足。还有一个隐蔽原因few-shot 示例太单一。如果参考样本都是同一种风格模型会过度模仿。解决办法是准备多组示例每组风格不同随机采样。5.2 清洗把好数据也过滤掉了误杀是清洗里最让人心疼的问题。我遇到过一次模型打分阈值设了 0.7结果把一批“简短但精准”的回答全滤掉了因为打分模型偏好长回答。排查方法把被过滤的样本随机抽 50 条人工看一遍统计误杀率。如果误杀率超过 10%说明阈值或打分维度有问题。解决思路有两个一是调整打分模型的维度权重不要让它过度偏好长度二是对短样本单独设阈值因为短样本的绝对分数天然偏低。提示打分模型本身也可能有偏。我建议定期用人工标注的样本校准打分模型发现偏差及时修正。别把打分模型当成绝对真理。5.3 语义去重把有价值的样本删了语义去重的阈值如果设得太松会把“主题相同但角度不同”的样本误判为重复。比如两条都是讲“如何优化数据库查询”但一条讲索引、一条讲缓存这俩其实都有价值。我的处理办法是语义去重时不仅看整体相似度还看关键信息差异。具体做法是先抽取每条样本的关键实体和要点如果要点差异超过一定比例即使整体相似度高也保留。另外语义去重前先做聚类而不是两两比对。聚类能更好地识别“一组相似样本”然后每组保留质量最高的 1 到 2 条而不是简单地删掉所有相似度超标的。5.4 RL 阶段奖励信号不一致RL 数据最怕奖励信号自相矛盾。表现是相似的候选回答奖励分数差异很大或者同一个回答在不同批次里奖励不一致。排查把奖励分数和人工判断做相关性分析。如果相关性低于 0.6说明奖励信号不可靠。解决优先用可验证的奖励比如代码题看执行结果、数学题看答案比对。如果必须用模型打分那打分模型要和生成模型分离且打分前要做一致性校准——同一批数据打两次分看方差大不大。5.5 常见问题速查表问题典型表现排查方向解决思路合成同质化数据雷同、结构一致prompt 模板、温度、示例放宽结构约束、提高温度、多样示例清洗误杀好数据被过滤阈值、打分模型偏好调阈值、分档设阈、校准打分模型语义去重过度有价值样本被删阈值、去重粒度提高阈值、聚类去重、保留要点差异奖励不一致相似样本奖励差异大奖励来源、打分一致性用可验证奖励、分离打分模型、一致性校准生成中断批次跑到一半失败网络、限流、超时断点续跑、重试机制、限流控制格式漂移输出格式不统一prompt 约束、后处理强格式约束、后处理校验、格式修复5.6 几个我踩过的坑坑一验证和生成用同一个模型。一开始我图省事生成和验证都用一个模型结果验证形同虚设——模型自己生成的东西自己当然觉得对。后来换成不同模型验证才真正起作用。坑二清洗顺序搞反。我早期先做语义去重再做质量过滤结果去重时把一些低质量样本当代表保留了高质量样本反而被删。正确顺序是**先过滤再
上一篇/下一篇内容由系统自动关联
返回资讯列表 →