资讯动态

数据量越大越好?Code Agent训练数据多样性Scaling的复盘

发布时间:2026/10/20 5:38:27 来源:尧图企业网站定制
今年年初的一次版本复盘让我对“数据量越大越好”这条经验产生了严重怀疑。我们组的 Code Agent 训练管线做了一次常规扩充训练数据从 200 万条提升到 260 万条中间几乎没有新增数据清洗逻辑就是纯纯地往里面加样本。结果评测一出来主要基准测试的 pass1 只涨了 0.3 个百分点——按以往的经验这个数据量涨幅对应的收益远不该这么寒酸。我花了两天时间排查最后把新增样本逐条拉出来看才意识到问题根本不在模型、不在超参而在于数据本身的高度同质化。新增的 60 万条样本里接近八成集中在我们原本就已经覆盖得很好的任务类型上而那些真正稀缺的长尾任务几乎没补进去多少。这件事直接催生了我们内部命名为 TDScaling 的一套改造方案——把训练数据的缩放对象从“数量”改成“任务空间的多样性覆盖”并围绕这个思路重新设计了采样、调度与评估方式。如果你是做代码智能体Code Agent训练的或者你的工作涉及更广义的 tool-use agent 数据构造这篇文章里的复盘思路、方案设计和踩坑记录应该能给你省下不少时间。如果你只做纯文本预训练那这套东西不一定直接适用但其中关于“数据分布结构”的思考方式多少也有点参考价值。1. 一次版本复盘如何让我对数据量产生怀疑1.1 数据量涨了 30%通过率只涨了 0.3%先说清楚当时的具体情况。我们手里的 Code Agent 底座模型是内部基于某个开源底座继续训练的训练阶段主要靠“任务描述 → 规划轨迹 → 代码变更 → 测试结果”这样的行为数据。上一个版本我们用 200 万条样本训出来的模型在内部评测集上的 pass1 大约是 31.2%。这一轮我们攒了半年的数据涨到 260 万条想着怎么也能往上走两三个点。结果评测跑完31.5%。30% 的数据增量换来了 0.3 个百分点的提升放在任何训练任务里都算得上“收益急剧递减”。当时团队内部第一反应是训练超参出了问题比如学习率不对、batch size 偏大、或者是多轮重复数据把模型带偏了。我们翻了一整天日志换了好几个学习率重新跑小规模实验指标纹丝不动。这种“怎么调都没用”的状态反而让我觉得问题可能不是出在训练过程而是出在数据本身。1.2 新增数据的“同质化”远比想象中严重我随手抽了 1000 条新增样本做人工标注维度包括任务类型、涉及修改的文件数量、是否需要跨模块理解、是否属于多步规划。标注结果让我有点后背发凉接近 78% 的新增样本集中在“实现单个函数”或“修复单个函数级 bug”这两类任务上而且绝大多数只改一个文件、只碰一个函数。真正难搞的跨文件修改、跨模块依赖理解、多步规划型任务新增占比不到 5%。进一步看文本相似度问题更突出。不少样本就是把同一个 issue 换了换变量名、改了改措辞底层逻辑和解决路径几乎完全一致。也就是说我们以为自己在“扩数据”实际上只是把同一个数据点附近的密度抬高了一些。模型在这个区域早就过拟合了再多的同质样本对它来说都是重复内容的低效复习。我这时候才意识到一个有点反常识的事在 Code Agent 这种后训练场景里数据的“条数”是一个很容易骗人的指标。真正决定模型能力上限的是数据在任务空间里的覆盖程度。2. 数量 Scaling 与多样性 Scaling它们究竟在缩放什么2.1 预训练时代的 Scaling Law 不能直接搬到 Agent 训练大语言模型预训练里确实存在非常漂亮的 Scaling Law模型参数量、数据量、计算量按照幂律关系提升下游损失稳定下降。这个规律长期有效但很多人忽略了一个前提——预训练优化的是“下一个 token 预测”这种无监督目标衡量的是模型对整个训练分布的平均拟合程度。只要数据量持续增加哪怕分布不变损失也会继续下降。但 Code Agent 后训练不是这个逻辑。它更像是在做一种行为匹配给模型一个当前仓库状态和任务描述让它学会输出一条能解决问题的轨迹。模型需要拟合的是“在特定状态下采取正确动作”的条件分布。简单增加数据量等价于在训练分布中已有的高密度区域继续加样本。同质样本之间的梯度方向高度相似10 万条本质相同的样本对参数更新的贡献可能还不如 1 万条分布在 10 个不同任务方向上的样本。说得再直白一点如果训练集里 80% 都是单函数任务那么模型参数更新的大部分步长都在强化“单函数任务的解题能力”。就算数据量翻到 500 万条只要分布比例不变模型在跨文件、多步规划上的能力也不会有什么起色。数量 Scaling 缩放的是“密度”多样性 Scaling 缩放的是“覆盖”。2.2 代码任务的空间结构长尾和维度爆炸代码任务的空间是个高维离散空间而且长尾极长。从我能想到的维度来看至少可以拆出以下几层任务类型新增功能、修复缺陷、重构代码、补充测试、解答 issue、代码审查等。修改范围单文件、多文件、跨目录、跨仓库。解决路径长度单步改写、两步修改、多步规划、长链路调试。上下文依赖空仓库、既有架构、复杂依赖、文档缺失。技术栈分布不同语言、框架、构建工具、包管理器和运行环境。这些维度组合起来的空间非常大真实世界的任务又往往落在长尾部分。一个“修复跨模块的类型错误且需要先理解三个服务之间的调用关系”的任务和“新增一个独立的工具函数”的任务在任务空间里离得很远。数量 Scaling 的典型做法是把主流区域的数据反复加厚模型学到的其实是一条从主流区域指向主流解法的映射。多样性 Scaling 的思路相反它把缩放的单位从“样本条数”换成“被覆盖的任务区域数”追求的是让训练数据在每个任务区域都有足够的存在感尤其是在长尾区域。2.3 用“读课文”类比说清两种 Scaling 的差别这个类比我跟很多同事聊过大家反馈都比较直观。数量 Scaling 像是把同一篇课文读无数遍字词句都滚瓜烂熟但真到写作文遇到一个新题目还是不知道从哪下笔。多样性 Scaling 是让你去读大量不同类型、不同写法的文章每篇不一定要读得很深但读得够广新题目来了才有的可迁移。代码 Agent 面对的“作文题”就是用户在真实仓库里提的各种千奇百怪的需求。我们当时在复盘里发现一个现象模型在某些基准测试上表现波动很大同一类任务换个仓库、换套依赖就崩。原因就是训练数据里这些任务形态的分布太过集中模型其实是在背答案而不是在学解题。多样性 Scaling 想解决的就是这个“会背不会用”的问题。3. TDScaling 的落地设计从任务建模到动态调度3.1 第一步把训练样本映射到任务空间TDScaling 的第一步不是改采样器而是给每个训练样本打一套“任务指纹”。我们为每个样本提取一组结构化元数据包括任务类型、修改的文件数、是否跨模块、是否跨仓库、是否需要先读文档、测试体量、语言和框架类型、以及一个由规则加小模型联合标定的难度等级。这些标签拼在一起就构成这个样本在任务空间里的坐标。不需要每个标签都绝对精确允许有一定噪声但维度一定要选全。我们刚开始只打了“任务类型”一个标签结果聚类出来 80% 的样本都挤在同一个簇里等于没分。后来补上修改范围、难度、上下文依赖这几个关键维度任务空间的结构才真正显形。有了任务指纹之后我们对全量训练数据做一次聚类得到若干个任务区域同时统计每个区域的样本量。这一步输出的“覆盖热力图”非常关键它会直接告诉你数据空洞在哪里。我们当时看到的结果是最主流区域的样本量是长尾区域的几十倍这种失衡程度已经超出了正常自然分布该有的样子。# 示意样本 - 任务指纹 - 聚类区域 sample { text: ..., task_type: bug_fix, # 任务类型 file_span: 3, # 涉及文件数 cross_module: True, # 是否跨模块 lang: python, # 语言 difficulty: 4, # 难度 1-5 context_len: 12000, # 上下文长度 } fingerprint normalize(sample) # 归一化这些字段 region_id cluster_model.predict(fingerprint)3.2 第二步多样性感知的 batch 采样传统的训练管线里采样器基本上是按原始数据分布随机抽。这样做的结果是主流区域的样本在每一个 batch 里都大量出现长尾区域依然经常轮空。TDScaling 的做法是让每个 batch 都成为一个“覆盖包”——在构造 batch 时先保证每个活跃任务区域至少有一定数量的样本然后剩余配额再按动态权重分配。我在这里用了一个比较朴素的实现思路。先把当前数据缓冲池里所有样本按区域分组每轮采样先给每个区域固定抽 k 条保证“雨露均沾”抽完之后剩余名额再根据不同区域当前的“缺口状态”加权抽取。所谓缺口状态就是看这个区域在最近一次评估中的通过率和目标之间的差距差距越大权重越高。def build_coverage_batch(buffer, region_assignment, k4): selected [] regions buffer.regions() # 当前 buffer 覆盖的任务区域 # 第一轮每个区域至少抽 k 条 for region in regions: selected.extend(region.sample(min(k, region.size()))) # 第二轮剩余配额按缺口动态加权 remaining buffer.config.batch_size - len(selected) weights compute_dynamic_weights(buffer, region_assignment) selected.extend(buffer.sample(remaining, weightsweights)) return selected代码很简单但效果立竿见影。跑了一个小规模对比实验后同样是 50 万条数据覆盖式采样训出来的模型在长尾评测集上的表现明显优于按原始分布采样。3.3 第三步训练过程中的动态难度调度光有区域级别的采样还不够。我们后来在训练过程中加了一层“难度课程调度”思路类似课程学习但调度的对象从单个样本换成了任务区域。训练早期多放单文件、单任务类型的低难度区域让模型先稳定掌握基础能力中期逐步引入跨文件、多模块区域后期再把高难度的长尾区域拉满。每个区域都配了一个表现跟踪器记录模型在该区域的当前通过率和历史最好表现。如果某个区域在最近几个评估周期内完全没有进步我们会临时降低它在采样中的权重防止模型被某几块硬骨头反复拉偏。这个设计借鉴了一点“自适应学习率”的想法——按区域动态调整数据分布而不是全局统一。对应到训练配置上大概长这样{ diversity_sampling: { strategy: coverage_pack, region_quota: { fixed: 4, dynamic: 0.4 }, max_region_ratio: 0.10, hard_region: { max_ratio: 0.03, lr_scale: 0.5 } }, curriculum: { enabled: true, stage: [ { region_hint: easy_single_file, weight: 0.6 }, { region_hint: cross_module, weight: 0.3 }, { region_hint: longtail_hard, weight: 0.1 } ] } }这套组合设计下来才算真正把 TDScaling 从“一个想法”变成了“一套可跑的训练方案”。4. 实操记录三个坑和一个兜底方案4.1 坑一多样性度量一开始就选错了我们最早做多样性度量时用的是 n-gram 重叠度加编辑距离。这个方案在纯文本去重场景里挺好用但在代码任务数据上出了大问题。同一个 issue 的两种完全不同的合法解决路径文本层面往往大量重叠会被误判成同一类样本直接丢掉。后来我们改成在任务指纹加语义 embedding 空间里做聚类才算绕开了“表面相似”的干扰。但聚类粒度也不好调粒度太粗两三种不同任务被分到同一个簇里粒度太细每个簇都只剩几十条样本采样的统计意义都没了。最后试下来用 8 到 12 个簇在这个数据量级上效果比较合适但这个数字没有普适性换数据集还得重新调。这里最核心的经验是多样性必须定义在“任务结构”层面而不是“文本表面”层面。文本长得像不代表任务是同一个文本完全不同也不代表任务有差异。要抓的是底层行为空间是否被覆盖不是表面句子是否重复。4.2 坑二伪多样性数据浪费预算为了快速扩充多样性我们试过用模型改写 issue、同义改写代码注释、把英文任务翻译成中文再翻回来。做了一轮消融之后发现这些操作产生的数据在文本层面确实多样了但模型的最终评测指标几乎没动提升不到 0.1%。后来想明白了这些改写改变的只是“皮”底层的推理路径、上下文依赖和解决步骤还是一模一样。模型在这些样本上学到的梯度信息和原始样本高度相关本质上就是同一道题换了三种颜色的纸打印。我们在内部开玩笑说这是“同一份菜换了三个摆盘”完全不产生新的养分。这个坑给我们的教训很实在在动手扩充数据之前先把现有数据按任务区域画一张覆盖热力图看清楚哪个区域是真正的空洞。然后针对空洞去定向构造数据才叫多样性扩展否则只是在已经饱和的区域里做表面文章。4.3 坑三训练稳定性变差多样性采样铺开之后训练过程开始出现明显的波动。batch 之间的分布方差变大损失曲线不再平滑个别区域通过率甚至出现了倒退。一开始我们以为哪里写错了后来复盘发现这是多样性增强的必然代价高难度、长尾区域的样本数量少解决路径分散模型在这些区域上的更新信号噪声天然就大。如果直接对全部区域一视同仁地采样训练很容易被几块硬骨头带着走。我们的兜底方案是三层防护第一给每个任务区域设定最大采样比例防止某个长尾区域喧宾夺主第二对高难度区域采用更低的学习率缩放让模型在这些区域“小步慢走”第三保留区域级表现监控如果一个区域连续三个评估周期都没有进展就临时降低它的采样权重等模型在其他区域稳定后再逐步加回来。还有一个切身感受训练后期多样性采样权重需要适度回落否则模型迟迟收不住、稳定不下来。我们最终的策略是前中期靠多样性拉开覆盖面最后四分之一阶段逐步把权重转回主流区域让模型“考前回归重点”收敛速度和最终指标都好了不少。5. 效果验证从总分到分布从曲线到长尾5.1 分层指标比总指标更能说明问题训练结束后我们对比了上一版纯数量扩充方案和 TDScaling 方案在内部评测集上的表现。只看总指标TDScaling 版的 pass1 是 35.8%比上一版的 31.5% 提升了 4 个点左右。但我觉得真正有价值的不是这个总数而是分层之后的差异。指标上一版数量扩充TDScaling 版总体 pass131.5%35.8%单函数任务52.1%53.0%跨文件任务18.4%24.6%多步规划类任务12.7%19.3%高难度长尾任务8.2%14.1%单函数任务基本持平说明 TDScaling 并没有牺牲主流能力跨文件、多步规划和长尾任务的大幅提升才是这套方案真正的价值所在。模型不是变得只会在冷门任务上逞强而是把短板补上之后整体解题能力都上了一个台阶。5.2 训练中后期出现的“先抑后扬”有一个现象必须提前给读者打预防针TDScaling 版本的训练初期总通过率可能比数量版本还低。原因不难理解——模型被迫同时适应更多任务区域早期的参数分布更“散”在原本擅长的主流任务上表现会短暂回落。如果不看区域级指标容易误以为方案失败了然后直接回滚。我们当时大概到了训练中期总分曲线才反超数量版本之后差距越拉越大。所以我的建议是评估维度里一定要加上“区域覆盖度”指标也就是模型在每个任务区域上的通过率是否都高过该区域的历史 baseline。只要覆盖度在提升、主流区域没有明显退步初期总分的短暂下滑就是“多样性代价”可以接受。5.3 关于数据预算的反思最后聊一个更实在的事。假设我们手中只有 60 万条新增数据的预算两种用法带来的收益差距非常大。一种是把这些数据按自然分布随机扩充下去结果大概率和我们最初那版一样收效甚微另一种是先通过覆盖热力图找出空洞区域再有针对性地构造数据去补缺收益大约是前者的两三倍。这背后的逻辑其实很简单在 Code Agent 后训练阶段数量 Scaling 并没有失效只是“覆盖率”已经成了比“总条数”更稀缺的资源。同样的预算花在填补空洞上比花在加厚主流区域上划算太多。这个思路后面我们还打算迁移到 reward 模型数据和评测集设计上核心原则不变先看清分布再决定往哪里使力。如果你也正被类似的问题困扰我的建议是不要一上来就重写整个训练管线。先做一件成本最低的事——把你现有的训练数据按任务空间画一张覆盖热力图找到最大的空洞在哪里。哪怕只是把采样权重朝空洞方向调一调都可能看到肉眼可见的变化。这一步走通之后再考虑要不要引入区域级动态调度和难度课程。TDScaling 这个名字听起来像是在和数据量较劲其实本质上是在和数据分布结构较劲。谁更早看清自己的数据长什么样谁就能在后面的迭代里少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑