☰
SWE-Prime:少而精的代码智能体训练数据筛选方法
2026/10/10 2:03:38 网站建设 项目流程

最近两年,只要涉及代码智能体的训练,几乎绕不开“先疯狂造数据,再拿大量轨迹做 SFT”这条路。规模上来之后,训练效果确实会涨,但成本也在飙升:造数据要算力,去重要算力,训练本身更是全流程最贵的部分。于是出现了一个反常的观察:在某些任务上,把训练轨迹从十几万条砍到两三千条,最终评估分数不降反升,甚至明显更好。

这个现象背后不是“数据量不重要”,而是“数据分布比数据量更重要”。SWE-Prime 这个研究方向正是从“更多轨迹、更好性能”的传统思路转向“更少轨迹、更好性能”的质量筛选思路。它的核心动作不是制造更多样本,而是从已有轨迹里选出那些真正能教会模型“如何解决问题的轨迹”。

这篇文章会拆解 SWE-Prime 类方法的设计逻辑,包括什么是轨迹质量、为什么无效轨迹会拖累 SFT、如何用结果信号做筛选,以及在实际训练流程里怎么落地这套“少而精”的数据流水线。

1. 这篇文章真正要解决的问题

先明确一个痛点:训练一个能够完成真实 Software Engineering 任务的 Agent,通常需要有“问题描述 + 模型中间动作 + 最终补丁”这样的完整轨迹。很多团队把大量算力投入到样本生成上,期望样本越多,模型学会的能力就越强。但这类数据与普通文本数据有一个关键区别:轨迹自带过程噪声。

举几个常见例子:

  • 模型在中间过程多次尝试了错误方案,最终误打误撞通过测试,这种轨迹的“解答过程”并不值得复用。
  • 模型在某个仓库里做了大量无意义的文件扫描和日志查看,最终补丁只改动了两行代码,但整条轨迹却包含了几十个回合。
  • 模型第一次尝试答案错误,第二次尝试又错,等到第五次才找到正确路径。这条轨迹虽然最终结果是成功的,但中间大部分步骤对训练是反向教材。

如果把这些轨迹全部堆给模型训练,模型学到的不只是“如何解决问题”,还顺便学到了“如何低效地绕弯子”“如何在错误方向上反复试探”。这也是为什么在一些代码智能体训练项目中,数据集规模扩大、评测分数反而停滞甚至下降。

SWE-Prime 要解决的问题正是:如何在已有生成结果的基础上,筛选出高价值的训练轨迹,让模型在更少的 SFT 样本上实现更好的评测性能。

这件事对三类读者尤其有价值:

  1. 正在训练代码 Agent、但被数据规模和训练成本困扰的算法工程师。
  2. 做 RAG 或工具调用 Agent、希望优化提示和示例质量的开发者。
  3. 希望在评测集上做快速验证、不想每次训练都烧掉大量 GPU 的研究者。

2. 基础概念与核心原理

2.1 什么是训练轨迹

在代码智能体的场景下,一条训练轨迹通常包含:

  • 输入问题,例如 SWE-bench 里的 GitHub Issue。
  • 模型执行过程中的多轮动作,例如查看文件内容、搜索符号、编辑文件、运行测试。
  • 最终结果,例如生成的 git diff 补丁。
  • 外部反馈,例如测试执行结果、命令报错信息。

SFT 训练时,模型学习的是“给定问题上下文和之前的动作历史,下一个动作应该是什么”。因此,中间动作如果质量很差,模型会把错误的决策过程当作“标准答案”来学习。

2.2 结果信号不等于过程信号

评估一条轨迹是否值得进入训练集,最直接的信号是“最终补丁是否通过了测试”。但通过测试,不代表过程适合教学。这里有几类典型问题:

  • 过程正确、结果正确:这是理想样本。
  • 过程低效、结果正确:模型绕了很多弯路,但因为环境允许,最终还是成功了。训练这样的样本,模型的“路径搜索习惯”会被强化。
  • 过程错误、结果正确:模型先引入了一个 bug,再修复掉,最后侥幸通过。这条轨迹会污染模型对“正确编辑”的认知。
  • 过程失败、结果错误:通常直接丢弃。

SWE-Prime 类方法的关键假设是:结果信号可用来快速剔除一堆明显不合格的轨迹,但更细粒度的过程信号,例如动作回合数、中间测试结果变化、补丁改动范围,也应当参与排序和筛选。简单说就是“结果过滤是底线,过程打分是进阶”。

2.3 为什么“少而精”在轨迹数据上成立

普通文本语料会遵循“数据越多越好”的经验法则,因为语言建模的任务分布足够宽,冗余信息会相互补充。但智能体轨迹的任务分布要窄得多,一个数据集里可能包含大量来自相似仓库、相似问题类型的样本。此时冗余不再互补,反而把模型推向“过度拟合某种路径模式”。

更准确地说,轨迹数据里最稀缺的资源不是“数量”,而是“多样且高效的解决方案路径”。SWE-Prime 的做法相当于做一个语义上的“去噪”:把同一条任务路径上所有低价值片段删除。保留的高价值轨迹数量可能变少,但每条样本的信息密度都上升了。

这类方法在工程实现上也有很多现实收益:

  • 训练集变小,数据加载、tokenizer 预处理、checkpoint 存储成本都明显下降。
  • 收敛速度更快,模型更容易在初期就拟合到核心行为模式。
  • 评测波动更小,因为模型没有从低质量轨迹里继承随机摆动。

3. SWE-Prime 方法拆解

从方法特征看,SWE-Prime 不是单个模型,而是一类“数据选择 + 训练策略”的组合。把通用思路拆开,可以分四步。

3.1 第一步:批量生成候选轨迹

要筛选出好轨迹,前提是先有候选池。通常做法是让一个基础模型在 benchmark 训练集或工程任务集上运行 Agent 流程,把每一轮动作、每次工具调用、最终补丁和测试结果全部记录下来。

这里注意,候选轨迹数量不需要特别大。SWE-Prime 的思路本来就不依赖海量生成,规模控制在“每个任务 5 到 10 条轨迹”已经足够观察到质量差异。

3.2 第二步:结果级过滤

先做低成本过滤:轨迹最终成功与否、是否生成合法补丁、补丁是否能通过测试、运行过程中是否出现框架级错误。这一步可以去掉一大批完全无效的样本。

如果缺少评测环境的自动验证条件,至少也要做静态检查:补丁格式是否正确,是否包含二进制文件,是否只修改了允许修改的文件。

3.3 第三步:过程级打分

对通过结果过滤的轨迹进行过程质量评分。可参考的维度包括:

  • 动作回合数,回合越多,路径越绕。
  • 信息获取与编辑动作的比例,徘徊类动作过多则倾向给低分。
  • 中间测试结果变化趋势,例如是否在后期才把错误修复。
  • 补丁改动幅度,改动文件数量和代码行数与问题复杂度是否匹配。

打分不一定需要复杂的奖励模型,简单的规则加权在很多场景下就已经比“全量保留”效果好得多。

3.4 第四步:按任务去重与采样

最终合成训练集时,同一个 GitHub Issue 往往对应多条通过筛选的轨迹。按场景去重,优先选择过程分数高的那一条。同时可以控制每个任务的样本上限,这能有效避免训练集中某个仓库占比过高。

最终效果就是:训练集可能只有原始数据池的 10% 到 30%,但每条样本都是“干净、直接、可学习”的。

4. 环境准备与前置条件

这部分按实际工程需求来准备,版本以项目实际环境为准。下面给出一个典型的实验环境参考。

4.1 基础环境

  • 操作系统:Linux,建议 Ubuntu 22.04。
  • Python:3.10 或 3.11。
  • PyTorch:2.x,具体以模型要求为准。
  • 训练设备:至少 8 卡 A100/H100 级别的 GPU,如果只做小模型实验,4 卡 4090 也可以启动。
  • CUDA:建议使用与你所用 PyTorch 版本匹配的 CUDA 版本。

如果使用 Hugging Face 生态,建议固定 transformers 和 peft 版本,避免训练时出现依赖冲突。

4.2 数据格式约定

为了把轨迹数据统一管理,建议把所有样本处理成 JSON Lines 格式,每条样本至少包含以下字段:

{ "instance_id": "django__django-11099", "problem": "Issue 标题与描述拼接", "patch": "模型最终生成的 diff 内容", "validated": true, "trajectory": [], "rounds": 8, "quality_score": 0.82 }

这个格式的好处是方便后续做筛选、排序和采样。trajectory字段可以只在需要训练时展开,平时筛选只依赖rounds、validated、quality_score等轻量字段。

4.3 评测集准备

如果要复现“轨迹数量变少、评测性能更好”的效果,最直接的做法是使用 SWE-bench 作为评测集。注意,评测集与训练集必须隔离,训练样本不能包含评测集里的 issue,否则指标的参考意义就消失了。

5. 核心流程拆解

下面把从原始轨迹到训练集的过程拆成可执行的步骤。

5.1 轨迹日志入库

Agent 每跑一个任务,都会产生一个 session 日志。推荐把日志 JSON 化后写入本地目录,目录结构可以使用tasks/{instance_id}/{session_id}.json。这样做的原因是后期筛选可以并发读取,不会互相锁文件。

每个 session 里必须记录:

  • 每个回合的工具调用类型。
  • 工具调用的输入和输出摘要。
  • 执行状态,例如是否报错、是否超时。
  • 每轮之后的文件状态变化。
  • 最终执行命令和补丁输出。

没有这些信息,只靠最终补丁无法判断轨迹质量。

5.2 结果过滤模块

先写一个扫描脚本,遍历所有 session,检查以下几项:

  1. 是否生成了非空补丁。
  2. 补丁是否满足格式要求,例如不能包含绝对路径。
  3. 若能执行测试,最终测试是否通过。
  4. 若不能执行测试,是否有 Debug 日志可以间接验证。

通过过滤的样本标记为validated: true,其余样本直接丢弃或放入低价池观察。

5.3 过程过滤与排序

结果过滤是布尔判断,过程过滤需要打分。先把轨迹按回合数切分,再统计每个回合的工具调用序列。常见规则如下:

def score_trajectory(traj): score = 0.0 # 回合越短,分数越高 if traj["rounds"] <= 6: score += 0.3 elif traj["rounds"] <= 10: score += 0.15 # 编辑动作占比 edits = sum(1 for s in traj["actions"] if s.startswith("edit")) total = len(traj["actions"]) if total > 0 and edits / total > 0.4: score += 0.3 # 最后几步才成功的,减分 if traj.get("first_success_round") and traj["first_success_round"] > traj["rounds"] * 0.7: score -= 0.2 # 带明显错误恢复痕迹的,减分 if traj.get("error_recovery_count", 0) > 3: score -= 0.15 return max(0.0, min(1.0, score))

这只是一个启发式版本。更严格的方案可以训一个小奖励模型给轨迹打分,但大多数情况下,规则版已经能把“绕弯子”和“低效探索”的样本压下去。

5.4 训练集构造

按instance_id聚合排序,每个任务只取质量分最高的 1 到 2 条轨迹。如果某个任务所有轨迹质量分都低于阈值,直接丢弃,不要强行保留。

随后把轨迹转换成 SFT 输入形式。通常是把结构化 JSON 序列化成模型对话模板:

<问题描述> <Assistant 第 1 轮工具调用> <Observed 结果> <Assistant 第 2 轮工具调用> ...

转换过程需要进行长度截断,保证单条样本不超过模型上下文窗口,否则训练时会出现大量truncated样本,造成隐性数据浪费。

6. 完整示例代码实现

6.1 示例一:轨迹筛选脚本

# 文件路径:scripts/filter_trajectories.py import json from pathlib import Path def load_session(path: Path) -> dict: with open(path, "r", encoding="utf-8") as f: return json.load(f) def is_valid(session: dict) -> bool: patch = session.get("patch", "") if not patch or "diff --git" not in patch: return False if session.get("test_passed") is False: return False return True def process_dir(raw_dir: str, out_file: str): raw_path = Path(raw_dir) candidates = [] for session_path in raw_path.glob("*/session_*.json"): session = load_session(session_path) if not is_valid(session): continue # 轻量过程信息补充 session["rounds"] = len(session.get("actions", [])) session["edits"] = sum( 1 for a in session.get("actions", []) if a.get("type") == "edit" ) candidates.append(session) with open(out_file, "w", encoding="utf-8") as f: for s in candidates: f.write(json.dumps(s, ensure_ascii=False) + "\n") if __name__ == "__main__": process_dir("./trajectories_raw", "./filtered_trajectories.jsonl")

这段代码的作用是把原始 session 里明显不合格的轨迹先淘汰掉。判断标准很朴素:补丁为空、测试没通过,一律不进训练候选池。以后想增加过滤条件,只需要在is_valid函数里扩展。

6.2 示例二:SFT 训练数据构造

# 文件路径:scripts/build_sft_samples.py import json def to_sft_sample(session: dict, template: dict) -> dict: problem = session["problem"] messages = [] # 系统提示 messages.append({"role": "system", "content": template["system"]}) # 用户初始问题 messages.append({"role": "user", "content": "Solve issue:\n" + problem}) # 轨迹动作序列 for act in session["actions"]: messages.append({"role": "assistant", "content": act["formatted"]}) if act.get("observation"): messages.append({"role": "user", "content": act["observation"]}) # 最终补丁 messages.append({ "role": "assistant", "content": "Here is the patch:\n" + session["patch"] }) return {"messages": messages} def main(): with open("prompt_template.json", "r", encoding="utf-8") as f: template = json.load(f) sft_samples = [] with open("filtered_trajectories.jsonl", "r", encoding="utf-8") as f: for line in f: session = json.loads(line) sft_samples.append(to_sft_sample(session, template)) with open("sft_train.jsonl", "w", encoding="utf-8") as f: for sample in sft_samples: f.write(json.dumps(sample, ensure_ascii=False) + "\n") print(f"generated {len(sft_samples)} samples") if __name__ == "__main__": main()

这里比较关键的是消息序列化方式。每个工具调用都拆成assistant动作和user观察结果,模型才能学会“看到什么输出后再决定下一个动作”。不要把多轮动作全部塞进一条 assistant 消息里,那会破坏 SFT 样本的因果关系。

6.3 示例三:模型训练启动命令

如果使用基于 transformers 的训练框架,配置可以参考以下示例:

# 文件路径:configs/sft_config.yaml model_name_or_path: /models/base-model train_file: ./sft_train.jsonl output_dir: ./output/swe-prime-sft per_device_train_batch_size: 4 gradient_accumulation_steps: 8 learning_rate: 2e-5 num_train_epochs: 3 max_seq_length: 8192 logging_steps: 10 save_steps: 200 fp16: true

训练启动命令:

accelerate launch \ --num_processes 8 \ train_sft.py \ --config configs/sft_config.yaml

训练结束后,用验证集做一次快速检查。建议保留一份不经过筛选的基线模型,和 SWE-Prime 筛选后的模型做对照,否则很难判断性能提升到底来自数据筛选还是训练参数变化。

7. 运行结果与效果验证

在验证阶段,重点观察两组指标:

  • 评测指标:pass@1、Patch 通过率、平均轨迹长度。
  • 数据指标:原始轨迹数量、筛选后数量、训练集 token 总量。

做一轮完整的对比实验,至少需要三个实验组:

实验组训练数据说明关注指标
全量基线所有结果成功的轨迹全部保留pass@1 与训练成本
随机采样成功轨迹中随机抽取相同数量排除数量因素
SWE-Prime 筛选按结果和过程质量两层筛选pass@1 与稳定性

预期结果不是“一定能涨分”,而是“在同等或更少训练样本下,SWE-Prime 组的效应不弱于全量基线”。如果出现全量基线明显更好,那通常说明原始模型生成的轨迹多样性不足,筛选池本身质量偏低,需要先提升生成阶段的基础模型能力。

判断验证是否成功,可以按以下步骤:

  1. 在评测集上分别评估三个模型。
  2. 对比 pass@1 分数。
  3. 检查模型生成的平均动作回合数是否下降。
  4. 检查失败样本的“失败模式”是否改变。

如果模型在筛选后仍然出错,关注错误类型。常见的问题是模型“学会了只补丁不重跑测试”,这往往是因为筛选后的轨迹里,很多成功样本没有包含最终测试执行动作。此时需要检查 SFT 样本末尾是否保留了“运行验证命令”这一轮动作。

8. 常见问题与排查思路

这里把实操中容易踩到的坑整理成一个表格。

问题现象可能原因排查方式解决方案
筛选后训练集过小,模型欠拟合结果过滤太严格,把大量“过程差但结果可学”的样本也删掉了查看被过滤样本的详细结果字段放宽结果过滤,增加过程分档,低分样本作为负样本或辅助样本保留
数据量减少后评测分反而下降候选轨迹本身质量不高,好样本数量不够统计原始轨迹的成功率和平均回合数先提升生成阶段模型的探索能力,再扩展候选池
SFT 模型产生大量重复动作筛选后的轨迹中“查看文件、浏览日志”类动作占比偏高检查训练样本里工具调用类型的分布对动作序列做裁剪,只保留关键编辑动作前后两轮
训练不稳定,loss 出现尖刺个别轨迹过长,序列被截断或填充不一致查看 loss 尖刺对应的样本编号和长度分布按长度分桶,超长样本单独处理或直接剔除
评测结果波动大,多次运行分数不一致评测使用了随机采样或模型解码超参未固定固定 temperature、top_p、随机种子设置确定性解码参数,采样多次取平均
模型生成的补丁无法通过测试,但训练时测试是通过的评测环境与训练环境依赖版本不一致对比训练环境和评测环境中的依赖列表统一 Docker 镜像,固定关键包版本

排查时一定要先看数据,再看模型。很多“训练效果不行”的问题,回溯到最后都是数据筛选规则没吃透。建议在筛选早期就把被删样本单独存储,而不是直接覆盖原始目录,方便后续追踪筛选逻辑的影响。

9. 最佳实践与工程建议

9.1 筛选规则要可解释

不要直接把模型打分当作唯一标准。每条筛选规则都应该有名字、有阈值、有统计结果。例如:

  • “rounds <= 10”,这条规则筛掉了多少样本。
  • “edits / total_actions > 0.4”,筛掉了多少样本。
  • “最终测试通过”,保留了多少样本。

只有规则可解释,你才能在效果下降时快速定位是哪一条筛选规则误伤了高质量样本。

9.2 保留一个 Holdout 验证集

不要反复拿 SWE-bench 测试集做验证。多次评测同一份测试集,会产生选择偏差。更稳妥的做法是从训练数据中按仓库维度划分出 holdout 验证集,只有最后阶段才碰公开测试集。

9.3 控制器动作类型

如果你的 Agent 支持任意 shell 命令,轨迹里会出现大量无效动作。筛选时可以考虑给动作类型设白名单,例如只允许file_view、grep_search、edit_file、test_run这几类进入训练样本,其余一律过滤。这是一种非常实用的降噪手段。

9.4 数据去重

轨迹类数据也需要去重。两个不同 session 可能最终补丁完全一样,只是中间步骤不同。建议按补丁的diff内容计算 hash,同一补丁只保留一条高质量轨迹。这样可以减少训练集里的冗余输入,也让模型更关注路径多样性和结果正确性。

9.5 训练与生成的边界要清晰

SFT 阶段的目标是让模型学会按训练轨迹形式去生成动作,而不是替模型做规划。不要把“筛选轨迹”和“自动生成最终答案”混为一谈。如果模型在推理阶段表现出的探索能力不足,应该回到生成侧,调整 Agent 的温度、搜索次数或工具集配置。

9.6 监控训练样本的 token 分布

在做长文本 SFT 时,样本长度差异过大会影响 batch 效率和收敛稳定性。建议在数据预处理时加入长度分桶,按长度比例混合加载 sample,避免长样本把 batch 撑大、短样本把效率拉低。

10. 总结

SWE-Prime 这个方向真正有意思的地方,不是它发明了多么复杂的训练策略,而是它挑战了一个在 LLM 时代被广泛接受的直觉:数据越多,模型越强。放到代码智能体场景里,这个直觉并不总是成立,因为轨迹数据和文本数据有本质区别,前者的“过程信息”会被模型一并学到。如果你喂给模型大量低质量的绕行轨迹,模型就会变成一个喜欢绕行的解题者。

在实际项目中,从“追求更多轨迹”转向“筛选更少轨迹”,本质上是把数据工程的重心从生成侧转移到评估侧。它的门槛不算高:你不需要设计新的模型结构,也不需要收集新的外部知识库,只需要把你已经生成的轨迹管理好、打上结果标签、做一轮过程筛选,然后重新跑一次 SFT。整套流程一两天就能在内部数据集上验证完,但带来的训练成本下降和评测稳定性提升,会直接影响后续每一轮迭代。

如果你想在项目里试这套思路,建议不要第一轮就把规则定得太严。先以“结果成功 + 回合数阈值”做一次粗筛,对比粗筛前后的 SFT 模型效果;确认粗筛有效后,再加入编辑动作占比、中间测试结果变化这类过程指标。每加一层筛选,就保留一组日志,这样最终得到的不是一条拍脑袋总结出的经验,而是一条可衡量、可复用的数据流水线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询