分类 开发随想 下的文章

在 LLM Agent 训练中,有时存在需要通过代码行号进行补全的方法。

这个脚本给任意给定的代码行统一添加代码行号。

import json
import re
import argparse

def add_line_numbers_to_input(input_jsonl_path, output_jsonl_path):
    with open(input_jsonl_path, 'r', encoding='utf-8') as fin, \
         open(output_jsonl_path, 'w', encoding='utf-8') as fout:
        
        for line in fin:
            try:
                data = json.loads(line)
                if "input" in data:
                    data["input"] = process_code_block(data["input"])
                fout.write(json.dumps(data, ensure_ascii=False) + '\n')
            except json.JSONDecodeError:
                print(f"JSON 解码错误: {line}")

def process_code_block(code_str):
    # 匹配代码块(包含```c和```, 如果需要匹配其他语言可以之后更改)
    code_block_match = re.search(r'(```c\n)(.*?)(\n```)', code_str, re.DOTALL)

    if not code_block_match:
        print("未找到符合条件的代码块")
        return code_str
    
    # 获取代码块各部分
    start_marker = code_block_match.group(1)  # ```c\n
    code_content = code_block_match.group(2)   # 实际代码内容
    end_marker = code_block_match.group(3)    # \n```

    # 处理行号(保留原始缩进)
    code_lines = code_content.split('\n')
    number_lines = []
    line_num = 1

    for code_line in code_lines:
        number_lines.append(f"{line_num}: {code_line}")
        line_num += 1
    
    # 重新建立代码块
    processed_code = (
        code_str[:code_block_match.start()] +  # 保留代码块前的所有内容
        start_marker +                        # ```c\n
        '\n'.join(number_lines) +           # 带行号的代码
        end_marker +                          # \n```
        code_str[code_block_match.end():]     # 保留代码块后的所有内容
    )

    return processed_code

def main():
    parser = argparse.ArgumentParser(description="Add line numbers to code blocks in a JSONL file.")
    parser.add_argument('-i', '--input', required=True, help='Input JSONL file path')
    parser.add_argument('-o', '--output', required=False, default='output.jsonl', help='Output JSONL filepath, default=output.jsonl')
    args = parser.parse_args()

    add_line_numbers_to_input(args.input, args.output)
    print(f"Processed file saved to {args.output}")


if __name__  == '__main__':
    main()

在修复代码 bug 的 Agent check_list 策略中,一个基本的三步方法如下:

  1. LLM 阅读给定代码块,根据给定的参考错误列表找到于 bug 描述相对应的有问题的“代码行号”
  2. 根据有问题的“代码片段”,判断代码片段是否确实违反代码规范,以 0(正确)和 1(错误)表示。
  3. 对于错误值为 1 的代码片段,进行 bug 修复。

很明显可以看到在阶段 1 和阶段 2 之间需要运行某一个脚本,来根据“代码行号”反向爬取代码块中的“代码片段”。

这么做的原因是在阶段 1 直接让模型输出“代码片段”的策略可能存在大量错误,因为模型的评估标准较为宽松,并不能保证准确无误地找到确切的代码片段。

以下脚本可以完成给定数据的处理工作。

此外,为了方便模型微调,还对于有问题的代码片段增加了一个随机的 snippet_id,使用 uuid.uuid4().hex 方法生成。

import json
import re
import uuid

with open("config/config.json", encoding="utf-8")as conf:
    con = json.loads(conf.read())

def extract_code_snippets_from_jsonl(input_jsonl, output_file):
    # 从代码行号提取出代码片段区间,批量处理
    line_count = 0
    with open(input_jsonl, 'r', encoding='utf-8') as f_in:
        with open(output_file, 'w', encoding='utf-8') as f_out:
            for line in f_in:
                line = line.strip()
                if not line:
                    continue

                try:
                    data = json.loads(line)
                    
                    output_rules_str = data["output"]
                    json_match = re.search(r'```json\s*\n(.*?)```', output_rules_str, re.DOTALL)
                    all_rules = json.loads(json_match.group(1) if json_match else json.loads(output_rules_str))

                    code_block = data["input"]
                    code_match = re.search(r'```c\s*\n(.*?)```', code_block, re.DOTALL)
                    if not code_match:
                        continue

                    raw_code_lines = code_match.group(1).split('\n')
                    
                    line_rules = []

                    for rule in all_rules:
                        if not rule.get("output"):
                            continue
                            
                        code_snippets = []
                        
                        for code_range in rule["output"]:
                            start = code_range["start_line"] - 1
                            end = code_range.get("end_line", code_range["start_line"]) - 1
                            
                            if 0 <= start < len(raw_code_lines) and 0 <= end < len(raw_code_lines):
                                snippet_lines = []
                                for line in raw_code_lines[start:end+1]:
                                    cleaned_line = re.sub(r'^\s*\d+[: ]\s*', '', line)
                                    snippet_lines.append(cleaned_line.rstrip())
                                
                                snippet_text = '\n'.join(snippet_lines).strip()
                                
                                if snippet_text:
                                    code_snippets.append({
                                        "snippet_id": uuid.uuid4().hex,
                                        "content": snippet_text
                                    })
                        
                        if code_snippets:
                            line_rules.append({
                                "rule": rule["rule"],
                                "code": code_snippets
                            })
                            line_count += 1
                            
                    if line_rules:
                        line_rules_str = json.dumps(line_rules, ensure_ascii=False)
                        output_data = {
                            "output": line_rules_str
                        }
                        # 将当前行的所有规则写入一行(不换行)
                        f_out.write(json.dumps(output_data, ensure_ascii=False) + '\n')
                        
                except json.JSONDecodeError:
                    continue  # 跳过无效的 JSON 行
    print(f"Processed {line_count} rules")

工作中有清洗 jsonl 文件的需求,原因是 LLM 输出的内容有可能存在错误的补全,不能直接全部用于 Fine-tuning。
这个脚本清洗了 jsonl 文件中 input/output == ""input 行重复 的情况。

import json
import random

input_file = 'file.jsonl'
output_file = 'file_cleaned.jsonl'

# 用于存储唯一 input 的行
input_map = {}

total_lines = 0          # 总有效行数(合法 JSON)
filtered_count = 0       # 被踢掉的行数(output 为空或 input 为空)

with open(input_file, 'r', encoding='utf-8') as f:
    for line in f:
        line = line.strip()
        if not line:
            continue
        try:
            item = json.loads(line)
            total_lines += 1

            # 检查 output 是否为空
            if item.get("output", "").strip() == "":
                filtered_count += 1
                continue

            inp = item.get("input")
            if inp is None:
                filtered_count += 1
                continue

            # 对重复 input 随机选择一条保留
            if inp not in input_map:
                input_map[inp] = [item]
            else:
                input_map[inp].append(item)
        except json.JSONDecodeError:
            continue  # 跳过无效 JSON 行

# 随机保留每组重复 input 中的一条
unique_items = [random.choice(items) for items in input_map.values()]

# 写入输出文件
with open(output_file, 'w', encoding='utf-8') as f:
    for item in unique_items:
        f.write(json.dumps(item, ensure_ascii=False) + '\n')

# 最终统计
retained_count = len(unique_items)
print(f"总处理条数(有效 JSON): {total_lines}")
print(f"被踢掉的条数(空 output 或无 input): {filtered_count}")
print(f"最终保留条数(按 input 去重): {retained_count}")
print(f"文件被保存为 {output_file}")

一、引言与项目概览

DeepSWE 是一个全新的、完全开源的强化学习训练项目,其目标是构建一个具备多步代码理解与修改能力的大型语言模型智能体(coding agent)。该项目训练起点是 Qwen3-32B 模型,不依赖任何监督微调(SFT)或教师模型蒸馏,仅通过强化学习完成整个 agent 的建构。最终模型 DeepSWE-Preview 在 SWE-Bench-Verified 基准任务上取得 42.2% 的 Pass@1 成绩,并通过测试时轨迹扩展(Test-Time Scaling)将 Pass@16 提升至 59%,在所有开源模型中表现最佳。

Pasted image 20250722142932.png

图 1:经 SWE-Bench 验证的 LLM 代理的性能与模型大小对比。通过仅使用强化学习 (RL)从头开始训练,结合测试时间扩展 (TTS) 的 DeepSWE-Preview 解决了 59% 的问题,大幅超越所有开源代理。我们注意到,DeepSWE-Preview 的 Pass@1 性能(42.2%,16 次运行的平均值)在开放权重编码代理中名列前茅.

Pasted image 20250722143000.png

图 2:SWE-Bench-Hard 的验证分数,如果代理提交最终答案并通过所有测试,它将获得正向奖励。仅需 200 步强化学习训练,SWE-Bench-Verified 的Pass@1分数就从 23% 提升至 42% (+20%)。

二、问题定义与环境构建

在 DeepSWE 中,软件工程任务(如修复 GitHub Issue)被建模为一个强化学习环境。该环境依托 R2E-Gym 框架实现,将每个任务封装为一个完整的 Docker 镜像,包含代码仓库、测试框架与构建脚本。模型作为 agent 与环境交互,拥有类 IDE 的工具接口:包括 Bash 执行、代码搜索、文件编辑以及“完成”指令。当 agent 判断任务完成后,会调用 finish 操作提交补丁,此时环境会运行测试套件以判定补丁有效性,并据此给予奖励。

此类任务天然是长时序的(long-horizon)、多步骤(multi-turn)的问题,不仅涉及程序逻辑的理解,还包括测试驱动的验证、文件结构的浏览与代码编辑等细粒度操作。这要求 agent 能够逐步积累上下文并动态调整决策策略。

三、数据集与训练基础设施

为避免训练集与评估集重叠,DeepSWE 使用 R2E-Gym 中约 4,500 个任务实例,并明确排除如 sympy 等 SWE-Bench-Verified 中出现的项目。这些问题各自独立包装为 Docker 镜像,确保环境的一致性与可控性。

在大规模训练中,训练系统需支持高并发容器调度。初期使用 Docker API 直接启动容器,但在每轮需并发运行 512 容器(64 任务 × 8 个 rollouts)的情况下,Docker daemon 容易崩溃。因此,项目改用 Kubernetes 管理容器调度,配合集群弹性扩缩容机制,实现上千核 CPU 与 TB 级本地 SSD 资源的分布式训练。每台工作节点预热镜像层以加速启动,集群调度逻辑则自动按负载动态增减节点,显著降低训练中断率与资源浪费。

四、强化学习算法设计:GRPO++

一、什么是强化学习(Reinforcement Learning, RL)?

在机器学习中,强化学习是一个研究“如何通过试错,让智能体(agent)学习在一个环境中采取行动以最大化长期收益”的方向。

强化学习的基本框架可以用五个元素表示:

- Agent:智能体(比如一个 AI 模型),它要在环境中“行动”。
- Environment:环境,即 agent 所处的世界(比如一个游戏、软件项目、机器人控制系统等)。
- State:某一时刻环境的状态(比如当前文件夹下的代码结构、终端输出等)。
- Action:agent 在该状态下可执行的动作(比如“查看文件”、“运行 bash”、“提交代码”等)。
- Reward:agent 每次行动后,环境给予的反馈(比如修复 bug 成功 = +1,失败 = 0)。

强化学习的目标是学会一个策略(policy),这个策略能告诉 agent 在每种状态下应该做什么,以获得最大的长期总奖励。

---

二、为什么在 DeepSWE 中使用强化学习?

DeepSWE 是一个“多步代码智能体”,也就是说它的目标不是生成一个答案,而是一步步地完成一个修复软件 Bug 的任务。这个任务非常像人类程序员 debug 的过程:

1. 阅读报错日志
2. 搜索错误位置
3. 编辑代码修复 bug
4. 运行测试验证结果
5. 如果失败,返回第一步重新尝试

这样一个过程很难用传统的“监督学习”方法解决。因为监督学习只是告诉模型:“这是输入,这是输出”。但在真实开发中,没有唯一正确的过程,只有最终结果是否成功。因此,这种“只有成功/失败反馈、没有明确中间答案”的情况,正是强化学习最适合的场景。

---

三、传统 RL 方法在 LLM 上的挑战

在使用 LLM(大型语言模型,如 ChatGPT、Qwen3)作为 RL 的 agent 时,会面临几个新挑战:

1. 输出是离散的自然语言,而不是数值动作
2. 每条轨迹很长(动辄几千个 token,数十步交互)
3. 奖励非常稀疏(只有在任务完全成功时才有 +1,否则全为 0)
4. 模型参数非常庞大,梯度容易爆炸或消失

这些问题导致传统 RL 方法(比如 PPO)在 LLM 上经常出现训练不稳定、策略坍塌、奖励失效等问题。

为了解决这些问题,DeepSWE 团队基于 GRPO(Generalized Reward Policy Optimization)提出了一个适用于多轮、长轨迹、大模型的强化学习方法,叫做 GRPO++。

---

四、什么是 GRPO(Generalized Reward Policy Optimization)?

GRPO 是一种专门为语言模型设计的强化学习方法,原始思想类似于 PPO(Proximal Policy Optimization),但做了多项适配性修改。

GRPO 的核心思想是:

- 让 LLM 输出序列时,不是简单地“模仿人类写法”,而是根据是否能完成任务来学习;
- 每一个序列的 log-prob(生成的概率)都会根据是否获得奖励,来调整策略;
- 这种方法对语言模型的 tokenizer 和多步结构做了改良。

不过 GRPO 也并不完美,它对一些因素仍然比较敏感,比如轨迹长度、策略偏移幅度、奖励不均衡等。

---

五、DeepSWE 的 GRPO++ 做了哪些改进?为什么?

为了在复杂、多轮的 Agent 场景下稳定训练,DeepSWE 对 GRPO 做了系统性的改造,形成了 GRPO++。下面我们逐项解释这些改进的内容和原因。

1. Clip High:鼓励探索,避免早期收敛

在 PPO/GRPO 中,为了防止策略在训练中变化太大,通常会对策略更新的幅度做“裁剪”,也就是限制新旧策略的比值不超过一个阈值。

然而,在 DeepSWE 中,一开始 agent 对环境几乎一无所知,如果裁剪得太严格,它将永远学不到新东西。
    
于是 GRPO++ 采用 Clip High 策略:只限制劣势样本的策略退步,但允许优势样本(即获得奖励的)策略放开更新。这样 agent 能在早期大胆尝试,找到正确的路径。

2. No KL Loss:取消对原模型的约束

一般的 RL 方法会对策略加一个 KL 距离惩罚项,目的是让它“不要离初始模型太远”。但在 DeepSWE 中,初始模型是 Qwen3,它并不懂如何 debug,所以如果一直围绕它打转,就永远学不会新策略。

因此 GRPO++ 直接移除 KL 惩罚项,让策略可以完全跳出初始行为,自由调整。

3. No Reward Std:保留原始奖励差异

原始 GRPO 会对所有样本的 reward 做标准化(减去均值除以方差),这样“容易的”和“难的”任务对模型影响一样。

但 DeepSWE 团队认为这是不合理的:如果一个补丁特别难修好,模型就应该花更多力气去优化它。于是他们保留了原始奖励数值,不做标准差处理。

4. Length Normalization:抵消长回复的劣势

在语言模型中,如果你生成的回复越长,累积的 loss 会越大。这样会导致模型偏向于写短代码、不完整的补丁。

为解决这个问题,GRPO++ 对每条序列的 loss 做了长度归一化(除以 token 数量),让长序列不会在训练中被惩罚。

5. Leave-One-Out Advantage(LOO):降低梯度波动

在 RL 中我们需要估计每个样本的“优势”(即它比平均水平好多少),这个值太抖动会让训练不稳定。

GRPO++ 使用了Leave-One-Out技术:计算平均 reward 时,先把当前样本排除在外。这样可以减少估计偏差,降低训练震荡。

6. Compact Filtering:屏蔽掉无效的“坏轨迹”

在多步环境中,agent 经常会出现:

- 一直卡在某个死循环里
- 想了很多步却没提交补丁
- 提交后挂掉测试但仍被计算为“有用轨迹”

这些轨迹如果参与训练,会导致模型学到错误行为。

GRPO++ 引入了 Compact Filtering 策略,过滤掉:

- 超过上下文长度限制的轨迹(max context)
- 超时未提交的轨迹(timeout)
- 走到最大步数还没提交的轨迹(max steps)

只有“有头有尾”的有效轨迹才会被用于训练,避免奖励污染。

紧凑过滤有利于训练,原因有二:

- 防止或延迟训练期间的奖励崩溃(图 6)。LLM 代理可能会在不知情的情况下偶然发现正确的补丁并通过所有测试。使用这些积极的奖励进行训练会强化各个步骤中的不良行为(例如,LLM 在前 10 步中正确回答,但之后会修补随机文件),当此类行为累积时会导致崩溃。确保仅在 LLM 代理主动提交时才分配奖励,可以鼓励进行严格的测试,从而使 LLM 对其最终提交的内容更有信心。
- 减少每一步的过度思考,鼓励跨步骤进行长篇推理。图 7 说明了这一现象。在训练过程中,平均响应长度缩短,但平均环境步骤数增加,这表明每步的平均思考时间急剧下降。

Pasted image 20250722143642.png
Pasted image 20250722143633.png

奖励设计采用极简的稀疏策略奖励模型(ORM):只有当补丁通过回归测试(包括 Fail2Pass 和 Pass2Pass)且在 5 分钟时间限制内完成时,才给予奖励 1;否则奖励为 0。相较 SWE-Bench 的 30 分钟窗口,5 分钟训练限制更有利于加速并发采样。

## 奖励函数详解:结果导向的稀疏奖励机制(Outcome Reward Model, ORM)

在强化学习中,奖励函数(Reward Function)是智能体(Agent)学习的关键来源。每次 agent 与环境交互后,环境会根据 agent 的行为反馈一个数值(reward),这个数值就像“打分”一样,告诉 agent 刚才的行为是否正确、是否值得学习。

在 DeepSWE 中,agent 的任务是修复一个真实的软件问题,也就是对代码做出修改(补丁 patch)并提交,然后让环境判断:这次修改成功了吗?

为了让这个判断过程简单而可靠,DeepSWE 使用了一个叫做 Outcome Reward Model(结果奖励模型)的策略,它是一种非常稀疏(sparse)的奖励函数。

---

一、奖励定义(Reward Definition)

奖励的逻辑如下:

- 如果 agent 生成的补丁在规定时间内通过了所有选定的测试用例(包括 Fail2Pass 和 Pass2Pass),那么:
  - 奖励 = 1

- 如果补丁有任何一个测试用例失败或在规定时间内没有完成测试超时),那么:
  - 奖励 = 0

这就形成了一个“非黑即白”的奖励机制:只有成功才算奖励,部分正确不算

---

二、什么是 Pass2Pass 和 Fail2Pass?

要理解奖励机制,我们必须先明白两个测试集合的含义:

1. Pass2Pass(P2P):
这是指问题提交之前,项目本身已有的测试用例,也就是“原来就应该能通过”的测试。

agent 修改代码之后,这些测试仍然必须通过,否则就意味着 agent 可能引入了新的 bug。

2. Fail2Pass(F2P):
这是指该问题的特定回归测试用例,也就是为了确认这个 bug 是否被修复而新增的测试。如果这个测试没通过,说明 agent 根本没解决问题本身。

只有当 agent 生成的 patch 同时通过 F2P 和 P2P 的测试时,我们才可以认为它“完美地修好了这个 bug”。

---

三、为什么奖励函数要设计成这么“苛刻”?

这是因为在真实的软件开发中:

- “修了 bug 但引入新 bug”是非常危险的;
- “只部分修复”是无法通过代码审查的;
- “运行超时”在真实 CI/CD 系统中会直接 fail。

因此,奖励函数必须高度对齐现实世界的评判标准,否则 agent 可能学到“做表面功夫”的策略,比如只为了让新测试通过而破坏旧逻辑。

此外,这种 0/1 奖励也带来了设计上的简洁性:它可以直接用作强化学习的标量反馈,而无需复杂的多维评分系统。

---

四、时间限制为什么是 5 分钟,而 SWE-Bench 是 30 分钟?

官方评估系统 SWE-Bench 给 agent 每个问题最长 30 分钟时间来运行测试。这么长的时间主要是为了兼容大型项目构建时间,例如运行 `pytest`、构建 Cython、安装依赖等。

但在训练期间,agent 需要和成千上万个问题交互,每一个都等 30 分钟根本不现实,训练会变得极其缓慢。

所以,DeepSWE 使用了一个加速训练的简化策略:把每次测试的最长运行时间限制为 5 分钟。如果 5 分钟内 agent 的补丁没有跑完或通过测试,那就视作失败(奖励 = 0)。

> 这个 5 分钟限制是一种 trade-off:牺牲了部分训练时的拟真性,但换来了训练速度的大幅提升。

---

五、这种奖励方式对训练带来了什么影响?

这种“1 或 0”的稀疏奖励设计,虽然简单,但对训练提出了很大挑战,尤其是:

1. agent 很难一开始就拿到正奖励
   - 初期几乎所有 patch 都失败,agent 看不到任何“正确例子”,学习信号为 0。
   - 为了解决这个问题,DeepSWE 采用了“clip high”鼓励探索策略,让 agent 大胆尝试。

1. 成功轨迹的误强化风险
   - agent 可能“误打误撞”修复了 bug,但补丁过程本身有问题(比如瞎改、乱删代码)。
   - 如果直接用这类轨迹训练,会强化错误行为。
   - 为此,DeepSWE 引入了 Compact Filtering,只使用“成功且干净”的轨迹参与 loss 计算。

1. reward variance 高,training 不稳定
   - 所有样本的 reward 只有两个值:0 或 1,这使得每一轮训练的 reward 波动非常剧烈。
   - DeepSWE 通过 Leave-One-Out Advantage、去除 reward std 等方法来抑制这种震荡。

---

六、和其他 RL 方法的奖励函数相比有什么不同?

| 方法 | 奖励类型 | 适用场景 | DeepSWE 对比 |
|------|----------|----------|---------------|
| PPO + KL | dense reward + KL 惩罚 | 语言建模、偏好学习 | DeepSWE 没有 KL loss,更自由探索 |
| RLHF | 人类偏好打分(如 1-5 分) | 对话质量优化 | DeepSWE 用客观 pass/fail 作为唯一评判 |
| Score-based RL | BLEU/ROUGE 分数 | 翻译、摘要任务 | 不适用于代码补丁,没有明确参考答案 |
| DeepSWE ORM | 完全稀疏的 Pass/Fail 奖励 | 软件工程任务 | 更贴近现实软件开发逻辑 |

---

七、小结

DeepSWE 的奖励函数采用了最简单但最贴合实际开发场景的设计:要么你修好了 bug 且没有破坏别的功能(奖励 1),要么你失败了(奖励 0)。没有中间地带。

这个奖励设计虽然稀疏,训练起来更难,但结合 GRPO++ 的多项改进(clip high、compact filtering、advantage 校正等),它最终让 DeepSWE 成功学会了一个通用的、多步 debug 策略,在真实的代码仓库中实现了自动修复能力。

这也说明了一个重要事实:复杂任务并不一定要有复杂奖励,有时简单的“是否成功”就足以引导模型学会非常复杂的行为,只要训练方法设计得当。

五、测试时扩展(Test-Time Scaling)与推理机制

与传统模型通过增加 token 数量提高推理表现不同,DeepSWE 的测试性能依赖的是 trajectory 数量扩展,即在推理时生成多个完整问题求解轨迹,并通过 Verifier 模型或程序测试集对它们进行打分排序,最终选择最优答案。

DeepSWE 综合使用了两种评估机制:execution-based verifier(通过代码测试自动评估补丁)和 execution-free verifier(用训练过的 LLM 直接判断补丁正误)。混合型策略(hybrid TTS)结合两者优点,在 K=16 rollout 情况下将 Pass@1 从基础的 42.2% 提升至 59.0%,远超其它开源方法(如 Skywork-TTS 的 47.0%)。值得注意的是,实验显示 context 长度从 16K 提升至 128K 所带来的性能提升非常有限(≤2%),远不如 trajectory 数量的增长所带来的效益。

六、涌现行为与学习分析

有趣的是,在纯强化学习的训练过程中,模型展现出若干“涌现行为”。一方面,agent 在提交补丁前往往会主动生成多个 edge case 测试并运行已有的 regression test,以确保修改不会破坏现有逻辑。(相比之下,当前 SWE 代理面临的最大挑战之一便是虽然它们可能修复了提出的错误,但生成的补丁可能不会考虑极端情况,或者会引入新的错误,从而破坏代码库的现有功能。)此行为并未显式引导,纯属在 RL 奖励驱动下自然学习到的通用测试意识。

另一方面,模型能根据任务复杂度动态分配“思考 token”。当任务需要深入理解代码结构或定位复杂 bug 时,模型往往会生成长达 2K tokens 的响应;而在执行脚本、搜索关键词等低复杂度任务中,其响应精炼至不足 200 tokens。这种 token 调度策略进一步提高了 RL agent 的样本效率与策略合理性。

七、结语

DeepSWE 是首次在无任何监督指导(无 SFT、无蒸馏)情况下,使用纯强化学习成功训练出表现卓越的多步智能体模型。其训练机制、调度基础设施与奖励设计共同构建了一个可拓展、高可复现的代码代理训练方案。借助 rLLM 框架和 R2E-Gym 环境,该项目已将全部数据、模型、日志、源码开源,为社区在 RL-agent 方向的研究奠定了重要基础。未来的智能体系统,将可能正是从 DeepSWE 这样严谨而开放的 RL 训练体系中诞生。

引言

收集数据一直是训练语言模型解决软件工程问题的一大难题。在此前的SWE-Bench的工作中,从10个左右的 GitHub 仓库里人工手动构造出了最多 1000 条高质量的实例用于进行测试和研究,但人工操作显然限制了扩展性和可用性。现有的方法存在如下几个问题:

  • 数据集的规模有限
  • 创建成本较高(人力成本、存储成本等)
  • 开源模型发展较为受限

![[Pasted image 20250714154807.png]]

基于这个痛点,SWE-Smith 框架被提出,这个框架可以被用来大规模自动化生产软件工程训练数据。和传统方法(手动寻找PRs相反),SWE-Smith 先为整个代码仓库构造一个统一且可执行的开发环境、然后在此环节中批量认为制造出 Task Instance (即 Bug)。使用这个框架,一个 50K 实例大小、跨越 128 个仓库的数据集被提出。基于这个数据集,通过基于 Claude 3,7 Sonnet产生的对话轨迹和对 Qwen 2,5 的微调,一个新的模型(SWE-agent-LM-32B)被提出,在没有推理时间缩放的情况下于 SWE-Bench-Verfied 验证器中得到了 40.2% 的求解分数。

SWE-Smith 的工作流程

SWE-smith的核心工作在于对于给定仓库,定义一个可执行环境,然后在这个环境中制造测试实例。(这种方法与 SWE-Bench 相反的)。

对于具有通过测试(Passing Test)的仓库创造执行环境

首先,为选定的Python代码仓库创建一个稳定的、超过80%的测试都能通过的Docker执行环境。以PyPI中下载次数最多的5000个软件包作为目标仓库,并按 GitHub 星号对 PyPI 软件包进行排序,然后从考虑范围中移除所有星号少于 1,000 的 PyPI 软件包,以及所有 12 个 SWE-bench 测试仓库。

![[Pasted image 20250714154754.png]]

创建 Task Instance 候选

在已构建的仓库环境中,四种不同的策略被采用来自动生成 Task Instance:

![[Pasted image 20250714155913.png]]

LM Generation

对于给定仓库,识别所有的程序实体(Entity),两种不同的基于语言模型的修改被引入:

  1. LM Modify: 向 LM 提供函数,并提示引入错误的修改
  2. LM Rewrite: 仅向 LM 提供函数头和文档字符串,要求 LM 对其重写

Procedural Modification

对于每一个函数,引入其抽象语法树(Abstract Syntax Tree),然后随机地进行一个或多个修改(例如移除某些条件/循环,或改变运算符等)

Combine Bugs

LM Generation 和 Procedural Modification 都仅通过编辑少量实体来制造实例。对于更复杂的要求,可以累积地对同一个文件进行多次变造,从而引入更大的运行实例。

Invert PRs (or "PR Mirror")

通过撤销(Undo)仓库的PRs更改(基于每次变更的 .diff)文件来重写受影响的文件,从而还原 PR 的编辑。

Task Instance 过滤

通过基于执行的候选补丁验证:将每个候选补丁应用到相应的代码库,运行测试套件,并仅保留那些破坏了一个或多个现有已通过测试(称为“Fail-to-Pass”测试)的补丁。测试运行时间限制为两分钟以提高效率;导致测试运行时间超过此时间限制的候选错误将被丢弃。
这确保了生成的实例是真实且可验证的,而非只是语法上的改变或无效的改动,排除了那些并没产出实际影响的补丁。从而保证评估标注的一致性和科学性。

生成 Problem Statement

Problem Statement的描述会直接影响到测试实例的困难度(Difficulty)和可行性(Feasibility),从而影响代理解决问题的能力。SWE-smith 使用一种简单的策略:对于每个任务实例,LM都会被提供.diff补丁、随机 Fail-to-Pass 的源码以及应用错误补丁后运行存储库测试套件的执行输出。LM被要求输出GitHub问题风格的文本,其中包含基于 Fail-to-Pass 的复现代码。

核心方法

利用 SWE-agent 和 LM 的推理能力:

SWE-agent 的角色:SWE-smith 不直接编写安装脚本,而是让 SWE-agent 扮演一个自动化的“程序员”角色。它运行 SWE-agent(一个基于语言模型 Agent-Computer Interface, ACI 的系统),并指示其完成代码库的安装和测试。

![[Pasted image 20250714155842.png]]

语言模型 (LM) 的自主操作:在 SWE-agent 框架下,作为核心的语言模型(如 Claude 3.5 Sonnet)会:

  • 阅读文档,获取安装和依赖信息
  • 执行 $\text{bash}$ 命令,执行推理出的安装命令
  • 运行测试(如 $\text{pytest}$ 或者 $\text{unittest}$ ,验证安装是否成功,即现有测试是否都通过)
  • 迭代修正,LM 对于安装失败的仓库会分析原因,并迭代调整操作,直到所有测试通过。

哪些事情是在构建 SWE-Smith 仓库时候依然需要人工介入的?

即使 SWE-smith 可以自动化地执行许多步骤(生成 bug patch,构建 Docker 镜像,运行 patch 和评估 F2P 等),仍有部分步骤是在引入新仓库时需要人工完成的:

  • 为仓库准备安装脚本(install_repo.sh)

    • SWE-smith 中用语言模型(agent)来探索安装步骤(例如:安装依赖、构建项目)
    • 但 agent 给出的“安装轨迹”可能不总是标准或结构化的
  • 配置测试输出解析(为测试实现 parser)

    • SWE-smith 用测试结果判断 patch 是否有效
    • 不同项目的测试输出格式不同

SWE-Smith 的生成成本

通过对 128 个 Python 仓库进行 SWE-smith 生成,得到了总共 50K 的测试实例。平均来说,每个仓库得到了 381 个测试实例,总成本为 $\$1360$. 实例生成的成本与策略直接相关。Invert PRs 由于更加依赖大语言模型,需要改写整个仓库,相比之下成本是最高的。
![[Pasted image 20250714153419.png]]

SWE-Smith 的执行成本

相较于 SWE-Bench,SWE-Smith 的磁盘空间要求更低,这是因为 SWE-Smith 扩展了执行环境,来自同一仓库的任务将共享相同的环境(相比之下,SWE-Bench 要求为每个任务实例创建单独的 Docker 镜像)。

![[Pasted image 20250714153821.png]]

基于 SWE-Smith 的实验微调

SWE-Smith 数据集的实验核心是依据拒绝采样微调(Rejection Sampling Fine-Tuning)的思想, 基本流程是使用一个非常强大的专家模型(Expert Model,Claude 3,7 Sonnet, based on SWE-agent)在 SWE-Smith 数据集上执行任务。通过收集记录所有 Expert Model 成功解决任务的完整操作步骤(被称为轨迹,Trajectories),对学生模型(Student Model,Qwen 2,5)进行微调。最后在标准的 SWE-Bench 测试集上评估经过微调的学生模型的性能。

难度评级的结论显示,SWE-Smith 生成的任务实例并不简单,专家模型在选定的 8686 个任务实例中的 17906 次尝试里,最终的池化样本为 6457 个,解决率为 36%。观察报告还显示更简单的轨迹(指多次运行中重复求解的实例)会降低模型性能。