2025年8月

一、引言与项目概览

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%。观察报告还显示更简单的轨迹(指多次运行中重复求解的实例)会降低模型性能。

之前的服务器到期了,续费的价格颇为不菲,于是趁这个机会买了一个新的服务器,也正好重构一下博客。此前网站的主题太为臃肿,加上没有做加速,大量的时间都停留在渲染前端界面上,反而忽略了内容。

一转眼又是一年八月。

我对夏季的感情是复杂的。在能够被想起来的那些记忆碎片里,为数不多的幸福和欢乐大多来自于炎热的夏日。

小时候住在湘江边上,每逢七八月外婆便会带我坐公交车去烈士公园玩,那里有一个很大的湖。游客们会在湖上划船,也有小贩在湖边售卖各式各样的玩具和零食。那时候我年纪还很小,船是划不了的,于是就在湖边吃着糖粒子看船上的游人们来来去去,如是便是一个下午。

再后来长大一些,记忆里的暑假显得是那么的漫长。时间好像被按下了停止键,一眼望不到头。彼时家里的条件并不算太好,加上学校放假前通知单里“请不要让学生在暑假沉迷电子产品”的一纸通告,每日所做的事情也不过是在家里的凳子上望着黑黢黢的没有任何显示的电视屏幕发呆。那时候长沙的夏天似乎还远没有像现在这样炎热,抑或是当时的人们压根也没有这么个意识,但总之空调是没有的,晚上用电高峰的时候家里时不时还会停电(现在回想起来,如今的生活似是好了太多,至少我是不太记得上次停电还是什么时候了)。外婆看我无聊,便拿着扇子坐在我旁边扇风,只说是陪我一路做“苦行僧”。不过当时也没觉得真的有多“苦”,无非也就是玩不上电脑游戏。

我这种坏学生自然是不会写作业的。从小学到高中,暑假的作业再多,于我而言都是废纸。从领回来那天起它们就被丢弃在房间里的某个角落里,惟开学前一周才会把他翻出来胡乱抄上两笔,然后再交上去。从小到大虽然因为这种事受了不少的责骂,但最后往往都不了了之。我自诩深谙其中的道理 —— 老师才没有那个时间精力一本一本去批改这种你知我知天地都知的垃圾(对于我这种学生是如此)。作业发下来的价值还不如直接送到废品站,能卖个三五块钱多少还能买几包豆奶。写作业虽然唤作“写”,但实际都是“抄”,既浪费墨水又浪费钱,横竖都是亏。抱着这种恶劣的学习态度,在浑浑噩噩度过的十几年学生生涯里,我终竟是没有认真写过一次暑假作业。

Intro

一个“好”的基准测试可以用来反应LMs在现实世界应用的表现,以帮助塑造他们未来的的发展。
但构建一个"好”的Benchmark也是困难的,因为这要求:

  1. 任务要有挑战性
  2. 模型预测需要是能够且容易验证的

现有的编码基准测试(如HumanEval)大多涉及自包含问题(Self-contained problem)。自包含问题指的是不依赖外部上下文或复杂依赖关系的问题,所有必要的信息和代码都已在问题本身中完整提供,可以在一个小范围内(如几行代码内)被理解和解决。

一个简单的例子如下:

def factorial(n: int) -> int:
    pass

这是一个典型的 self-contained problem,只需要编写一个 factorial 函数,而不需要了解其他代码文件或上下文。验证测试的方式也较为简单,即通过检查输出是否等于 factorial(5) == 120 等。

对比现实世界的 Bug 修复任务,这种基准测试往往过于简单,因为真实的软件工程场景中问题往往并不是 self-contained 的。一个 Bug 可能出现在 10000 行代码中的某一行,或者修复一个问题可能需要理解多个文件之间的交互,而有些错误与数据结构或类继承结构有关。

现有 Benchmark 还存在着其他限制,例如许多测试的基准较为陈旧(staleness),覆盖的 Repo 不充分(limited repository coverage),以及高度依赖手动构建实例和环境(heavy reliance on manual effort)。(arXiv:2505.23419v2 [cs.SE] 1 Jun 2025 SWE-bench Goes Live)

为了让评估语言模型面对真实软件工程场景的性能(而非仅仅面向于类似 leetcode 等平台),一个新的评估范式被需要提出,这引出了 SWE-Bench.

SWE-Bench 的任务来源是解决提交到热门 Github Repo 中的问题(例如 Github PR 或者 issue),每个任务都基于一个 bug 描述,并制定一个具体的 commit (base_commit)作为起点,模型被要求生成一个补丁(patch),描述对现有代码库应用的更改(使用git diff格式),然后调用仓库 的测试框架(Test Harness)来评估修订后的代码库。

![[v2-ee9dba791f4df0c80d3e9b0818831641_1440w.jpg]]

使用 SWE-Bench 对主流先进模型进行评估,令人失望的是,模型在现实世界中解决实际问题的能力仍存在较大的改进空间。使用 SWE-Bench (Full)数据集进行评估,SWE-agent + Claude 3.7 Sonnet 能解决 33.83 % 的实例,而对于 RAG + [ChatGPT]() 3.5,在同样的数据集评估下仅有 0.17 % 的实例被解决。

SWE-bench 的数据集是如何构建的?

![[v2-b5dcb551f8c3aad23e4c1f4fad8132f5_1440w.png]]

SWE-Bench 的数据集构建分为三个阶段。

首先是仓库选择和数据抓取(Repo selection and data scraping):开发人员从 GitHub 上流行的 12 个开源 Python Repo 中拉取 PRs,总共得到了 90,000 个数据。抓取流行仓库的原因是因为这样的仓库往往收到更好的维护,也有着更好的测试覆盖率。对于每个 PR 而言,相关的代码库也会被提供,这可以清晰地了解 PR 合并前后仓库的状态。

其次是基于属性的过滤(Attribute-based filtering):合格的 PR 应该符合两个属性条件(Attribute Condition)。一个是它应该解决了至少一个问题(Bug 或者 Feature request),并且它应当修改了项目的测试文件(这意味着 contributor 在 PR 中添加或修改了测试用例)。满足这两个条件的 PR 更有可能构成一个高质量、可评估的基准测试任务。

第三个阶段是基于执行的过滤(Execution-based filtering):对于每一个候选任务(即实例,instance),开发人员先只应用 PR 中的测试相关更改(即只更新 test 文件,而不变动其他代码逻辑),通过记录应用 PR 前后的 test suite 的测试结果,两项筛选被应用到候选池中。通过候选的 PR 必须至少有一个 Fail-to-pass 测试,意味着补丁至少修复了某一个问题。与此同时,安装或运行出错的任务也会在筛选中被过滤,以保证评估的鲁棒性。

下面是一个流程图:

Stage I: Repository Selection
--------------------------------
选取目标仓库:
├── 满足条件:
│   ├── 活跃维护(有定期提交)
│   ├── 有 issue / PR 活动
│   ├── 包含自动化测试框架
└── 输出:候选项目列表(如 pandas、django、scikit-learn)

        ↓

Stage II: Attribute-based Filtering
-------------------------------------
筛选候选 PR(Pull Request):
├── 满足两个条件:
│   ├── PR 关联并关闭了一个 issue(例如 Fixes #123)
│   └── PR 修改了测试文件(表明提交者编写了验证逻辑)
└── 输出:候选任务列表(candidate tasks)

        ↓

Stage III: Execution-based Filtering
---------------------------------------
对每个候选任务进行验证:
├── 步骤 1:仅应用测试代码,运行项目测试
│   └── 至少有一个测试失败
├── 步骤 2:再应用 PR 的修复代码,重新运行测试
│   └── 至少一个测试从 fail → pass
├── 步骤 3:检查运行和安装是否正常
│   └── 无构建或执行错误
└── 输出:有效任务实例(可用于模型评估)

        ↓

数据集的最终组成结构:
instance_id: 实例ID,通常格式为 repo_owner__name-PR-number

//代码基本信息
repo: 仓库名
base_commit: PR 提交之前的代码 commit id,定位代码基线
version: 用于运行的版本号
environment_setup_commit: commit id,安装运行环境的代码基线
created_at: PR 创建时间

//PR基本信息
problem_statement: PR 对应的 issue 内容,也就是要解决的问题
test_patch: 这个 PR 提交的测试 case patch 代码
FAIL_TO_PASS: 这是修复的最终目标,应用修复的 PR 后会通过的测试 case
PASS_TO_PASS: 应用 PR 前和应用后,都应该通过的测试 case
patch: 这个 PR 修复的 patch 代码,相当于标准答案
hints_text: PR 提交之前,github 上对这个 issue 的讨论 comment。可选,如果要上榜单,禁止使用这个数据。

这是一个具体的评估筛选实例:


SWE-bench 构建流程(实际样本:pandas-dev/pandas#43124)

Stage I: Repository Selection
--------------------------------
仓库:pandas-dev/pandas
├── 活跃维护 ✔
├── 包含大量 PR 和 issue ✔
└── 使用 pytest 自动化测试 ✔

        ↓

Stage II: Attribute-based Filtering
-------------------------------------
PR: #43124 - Fix MultiIndex.set_levels with duplicated level names
├── 关联 issue:Fixes #43112 ✔
├── 修改测试文件:pandas/tests/indexes/multi/test_set.py ✔
├── 同时修改核心代码:pandas/core/indexes/multi.py
└── 被纳入候选任务列表(candidate task)

        ↓

Stage III: Execution-based Filtering
---------------------------------------
任务执行验证:
├── 步骤 1:checkout 到 base commit(PR 提交前)
│   └── 仅添加 test_set.py 中的新测试
│   └── 运行 pytest → 新测试失败 X
├── 步骤 2:应用 multi.py 的修复补丁
│   └── 再次运行 pytest → 失败测试通过 O
├── 步骤 3:确认无构建或安装错误 O
└── 通过执行筛选,生成有效任务实例

        ↓

最终任务格式:
---------------------------------------
- instance_id: pandas-dev__pandas-43124
- base_commit: <PR 基准提交 ID>
- problem_statement: issue #43112 内容 + 上下文
- patch: multi.py 的修复 diff
- test_patch: test_set.py 的新增测试 diff
- FAIL_TO_PASS
- PASS_TO_PASS

通过三个阶段的筛选,原始的 90,000 个 PRs 被过滤为 2,294 个 Instance,这些实例构成了 SWE-bench 的完整数据集。

![[Pasted image 20250711152841.png]]

SWE-bench 评估的输入是如何构建的?

理论上说,SWE-bench 的每个任务输入应当包括:

  • 一个 issue 描述 (195 words on average)
  • 一个大型代码库 (438K lines on average, or million tokens)

但是大语言模型通常有上下文窗口限制

  • GPT-3.5 ≈ 4K tokens
  • GPT-4 ≈ 8K–32K tokens
  • Claude 2 ≈ 100K tokens

因此直接将整个代码库喂给模型是不现实的,意味着输入本身必须进行筛选,只有“重要部分”才能够被直接提供给模型。基于这个问题,论文提出了两种策略(Approach):

*(引用自blog.cnbang.net/tech/4178)下面两个检索代码的方法,只是论文中做实验的参考,实际在测试 SWE-Bench 时,各模型会有自己的方法,因为检索代码的准确性对成功率影响也很大,所以榜单上很多是 Agent + 模型 的测试结果,而不是单大模型的。

Claude 3.7 跑分的说明里提到:

SWE-bench Verified: Claude 3.7 Sonnet scores 62.3% out of 500 problems using pass@1 with bash/editor tools plus a "thinking tool" for single-attempt patches — no additional test-time compute used. 70.3% score uses internal scoring and custom scaffold on a reduced subset of problems. Scaffold Deepseek R1 results use the "Agentless" framework. OpenAI results from o3-mini system card cover a different subset of problems with a custom compute.

Claude 没有具体说明怎么做检索代码的,官方blog附录里提到在运行环境上是用了极简的方案,只提供了命令和编辑文件的能力,看起来是只把代码仓库目录扔给 LLM,让它自行去做文件搜索。另外有提到 Deepseek 的跑分是基于 Agentless 框架跑的,Agentless 专门介绍说明了如何跑 SWE-Bench,以及具体是怎么做代码检索的。见 Agentless/README_swebench.md。

可以看到 SWE-Bench 测试集其实是比较局限,这里面全是 Python 代码,也基本是纯逻辑代码,不涉及 UI 渲染相关,也不涉及其他语言,很多实际的软件工程场景没有覆盖到,所以即使 benchmark 到 100%,也不代表能解决绝大多数工程问题。

稀疏检索(Sparse Retrieval)

*特别地,指 BM25 检索算法

Sparse Retrieval(稀疏检索) 是一种经典的信息检索方法,其核心思想是基于词项匹配:文档和查询都被表示为稀疏的词项向量,然后计算它们之间的相似度。

BM25 是一种基于概率检索框架的改进算法,特别是在处理长文档和短查询时表现出色。其核心思想是基于词频(TF)和逆文档频率(IDF),同时引入文档的长度信息来计算文档 $D$ 和查询 $Q$ 之间的相关性。

对于一个文档 $D$ 和查询 $Q$,BM25 的得分为:

$$ \text{BM25}(D, q) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{f(t, D) \cdot (k_1 + 1)}{f(t, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}} \right)} $$

  • $t$:查询中的一个词项(term)
  • $f(t, D)$:词项 $t$ 在文档 $D$ 中出现的次数(Term Frequency)
  • $|D|$:文档 $D$ 的长度(以词数计)
  • $\text{avgdl}$:语料库中所有文档的平均长度
  • $k_1$:控制词频饱和的参数(推荐值 1.2 ~ 2.0)
  • $b$:控制文档长度归一化的参数(推荐值 0.75)
  • $\text{IDF}(t)$:词项 $t$ 的逆文档频率(Inverse Document Frequency)

其中 $\text{IDF}$ 的计算公式为

$$ \text{IDF}(t) = \log\left( \frac{N - n_t + 0.5}{n_t + 0.5} + 1 \right) $$

  • $N$:语料库中的文档总数
  • $n_t$:包含词项 $t$ 的文档数量

不难注意到:

  • $f(t, D)$ 越大,表示词频高,模型认为该词与文档强相关;
  • 然而随着频率上升,这种增益逐渐减弱,极限由 $k_1$ 控制(忽略长度归一化,取$b=0$,$|D|=\text{avgdl}$ ,当 $f(t, D) \to \infty$ 时,$\frac{f(t, D) \cdot (k_1 + 1)}{f(t, D) + k_1} \to k_1 + 1$,表示词频增益最终趋于饱和);
  • 文档越长($|D|$ 越大),得分会被归一化降低;
  • 稀有词的 $\text{IDF}(t)$ 越高,更具区分度(当 $n_t$ 越小,IDF 趋近于 $\log(N)$,得分也就越高)

SME-Bench 使用 BM25 检索算法,将 issue 描述(problem_statement)作为输入,在代码文件中检索相关性高的文件,然后按照模型的 context 限制,挑选能放下的最多文件作为上下文输入。

然而,与期望不同的是,在多个 context 长度(8K,16K,27K tokens)的试验下,结果发现:

短上下文窗口反而结果更好

![[Pasted image 20250711160530.png]]
出现这种情况的可能原因包括太长的上下文会让模型难以聚焦(focus),或者 BM25 的排序误差在长窗口内更容易累计噪声。

预知式检索(Oracle Retrieval)

作为稀疏检索的对比分析,研究人员还引入了一个“理想化设置”:

  • 查看这个任务的 GitHub PR **实际修改了那些文件
  • 直接把这些文件作为上下文喂给模型

这种方式被称为 Oracle 式的检索,虽然在真实场景中它们并不那么 realistic,因为工程师事实上事先并不知道要更改哪些文件。

结果

结果显示,对比 BM25 和 Oracle 方法在 27K tokens 下的表现,约莫 40% 的任务中,BM25 的检索结果召回(Recall)了 Oracle 中的全部文件,但也有接近一半的任务 BM25 没有找回哪怕一个 Oracle 中的文件。

这说明:

  • BM25 是一个可用但不稳定的方案,部分任务表现好,部分表现差;
  • 说明上下文选择在 SWE-bench 中是一个重要影响模型表现的变量。

![[Pasted image 20250711161155.png]]
通过将模型在两种检索方式下进行评估对比,总体而言,模型在解决问题方面遇到了显著的困难,对于真实仓库场景下的问题解决率并不让人满意。

![[Pasted image 20250711161443.png]]

大致上,以下结论可以被提出:

1. 不同仓库任务难度差异
  • 模型在不同仓库上表现趋势相似,但解决的问题不完全重叠。
  • 例如,在 "oracle" 设置下:

    • Claude 2 成功解决 110 个任务;
    • SWE-Llama 13B 成功解决 91 个任务;
    • 但 Claude 2 仅覆盖了 SWE-Llama 成功实例的 42%。

      2. 图像嵌入对模型有干扰
  • 某些 issue 使用 Markdown 形式嵌入图像,如 ![image](https://...)
  • 出现图像的比例:

    • matplotlib:32%
    • seaborn:10%
    • 全体任务平均:2%
  • 这类问题可能需要多模态模型或外部图像理解模块来处理。
3. 上下文长度越长,性能反而下降
  • 模型虽然能处理长上下文,但长代码段会引入干扰。
  • Claude 2 在上下文长度增长时性能显著下降。
  • 即使 BM25 检索在更长上下文中覆盖更多 oracle 文件,模型定位问题代码仍然困难,导致效果下降。
    ![[Pasted image 20250711161846.png]]
4. 精简上下文可提升性能
  • 对 oracle 检索到的文件进行输入消融,仅保留实际修改的行及其上下文(±15 行)。
  • 表现提升:

    • GPT-4:从 1.3% 提高到 3.4%
    • Claude 2:从 4.8% 提高到 5.9%
  • 精简上下文有助于模型聚焦关键位置。
    ![[Pasted image 20250711161906.png]]
5. 模型不会因 PR 日期作弊
  • 将任务按 PR 创建时间分为 2023 年前后,比较模型表现。
  • 大多数模型在新旧任务上的性能差异很小。
  • GPT-4 是唯一一个在 2023 年后表现下降的模型,但因仅在子集上评估,结论不确定。
  • 表明模型不是通过“记住历史代码变化”来作弊。
    ![[Pasted image 20250711161923.png]]
6. 微调模型对上下文变化敏感
  • SWE-Llama 在 oracle 检索下微调,但在 BM25 检索下效果明显下降。
  • 推测原因:

    • 训练时模型被引导对上下文中所有文件都进行修改;
    • BM25 提供的上下文中许多文件其实不需要更改,导致误操作。
7. 补丁生成优于文件重写
  • 将任务设为“生成补丁”效果优于“重写整个文件”。
  • 例如 Claude 2:

    • 补丁生成得分:4.8%
    • 文件重写得分:2.2%
  • 即使控制输入 token 长度,补丁生成仍更有效(7.8% vs 3.9%)。
8. 模型偏好简短、局部的修改
  • 正确生成的补丁长度比金补丁显著更短:

    • 平均编辑行数:30.1 vs 74.5
  • 模型通常只编辑一个文件,回避大范围改动。
  • 说明模型生成风格偏向“保守编辑”,不擅长结构性重构。

初始 LLM 接受的输入通常只包含文本信息,而不具备多模态理解的能力。如果需要LLM接受单元格数据并进行处理,我们通常需要先将其文本化以与 LLM 的输入接口对齐。

Excel 文件(.xls / .xlsx)在本质上是一种富结构的二进制或压缩 XML 文件,其内容不仅包括了基本的表格数据(即单元格中的文字和数值),还可能包含:

  • 合并单元格
  • 图表、图像、批注
  • 单元格格式(颜色、字体、对齐方式)
  • 数学公式(如 =SUM(A1:A5))
  • 宏脚本(如 VBA)

上述内容对于传统的文本 LLM 来说是不可直接解析的非文本模态,因此我们需要一个合理的文本化方法,既能保留足够信息,又不过度引入 token 噪声。

首先被提出的方法是将 .xls/.xlsx 文件转换为.csv文件。.csv 文件是一种纯文本格式的结构化数据文件,用于以 逗号分隔的方式 存储表格数据。一个简单的 example.csv 文件内容如下:

Name,Age,Occupation
Alice,30,Engineer
Bob,25,Designer
Charlie,28,Teacher

这表示一个表格,含有三列:姓名、年龄、职业,每行是一条记录。

.csv 格式的优点在于:纯文本格式,适合 LLM 处理,且其易于读取与生成,兼容广泛(Pandas, Excel, SQL)。

但.csv 格式的主要缺点之一是它无法表达 Excel 等高级表格中的结构性信息,比如合并单元格、单元格样式、公式、注释、图表等。因此其仅适用于结构规整、样式不重要的表格处理,如财务数据、名册、日志导出等,但在对于信息保留较为严格的应用场景下并非最佳适用。

另一个可能的方法是将 .xls/.xlsx 文件转换为 .htm l或 .xml 的标记语言格式。Excel 内部实际就是由嵌套的 XML 标签描述的,因此可以将其转换为 .html 或 .xml 格式进行呈现,在浏览器中保留完整排版、结构和嵌套逻辑。与 .csv 相比,这种方式最大程度上保留了一切结构化和多模态的信息,可以在浏览器中被充分渲染和加载。然而,对于针对 LLM 训练为导向的应用场景,引入大量冗余 tag 和嵌套结构会导致 token 数量激增,同时对 LLM 来说,HTML tag 不是自然语言的一部分,干扰模型理解。这种方式适用于结构保留要求极高的任务(如结构重建、格式恢复、文档重构),但不适合直接作为LLM输入使用。

基于这两种方式的优缺点,一个折中的方案被提出:如果我们每一个 Excel 文档都视为一个矩阵,那么每一个有效单元格都是矩阵中的一个元素。可以通过基于统计的方法来进行文档的文本化。将每个 Excel 表格视为一个二维矩阵,提取出有效内容单元格的位置和值,并可选地补充合并区域或样式元数据,以结构化标记方式保存。

我们统计每个特定元素出现的位置,并进行表示。

[
  {"cell": "A1", "value": "Name"},
  {"cell": "B1", "value": "Age"},
  {"cell": "C1", "value": "Occupation"},
  {"cell": "A2", "value": "Alice"},
  {"cell": "B2", "value": "30"},
  {"cell": "C2", "value": "Engineer"},
  {"cell": "A3", "value": "Bob"}
]

对于合并单元格的情况,这种方式也可以处理:

{
  "value": "Test",
  "range": "A1:A3",
}

这样,在文本化的同时,我们仍然保留了单元格的空间位置、合并关系、有效值等核心信息。这种形式也易于转换为 Markdown 表格、JSON 或简化 HTML 片段,以供不同类型 LLM 消化。

具体的实现代码如下:

from openpyxl import load_workbook
from openpyxl.utils import get_column_letter

wb = load_workbook("SWE1.xlsx", data_only=True)
ws = wb.active  # or wb['SheetName']

merged_ranges = list(ws.merged_cells.ranges)
used_cells = set()
cell_contents = []

# 处理合并单元格
for mr in merged_ranges:
    top_left_cell = ws.cell(row=mr.min_row, column=mr.min_col)
    value = top_left_cell.value
    cell_range = f"({get_column_letter(mr.min_col)}{mr.min_row}:{get_column_letter(mr.max_col)}{mr.max_row})"
    for r in range(mr.min_row, mr.max_row + 1):
        for c in range(mr.min_col, mr.max_col + 1):
            used_cells.add((r, c))
    cell_contents.append(((mr.min_row, mr.min_col), cell_range, value))

# 处理非合并单元格
for row in ws.iter_rows():
    for cell in row:
        coord = (cell.row, cell.column)
        if coord not in used_cells and cell.value is not None:
            col_letter = get_column_letter(cell.column)
            cell_range = f"({col_letter}{cell.row}:{col_letter}{cell.row})"
            cell_contents.append((coord, cell_range, cell.value))

# 按照行列排序
cell_contents.sort(key=lambda x: (x[0][0], x[0][1]))

# 最终打印
print(f"Sheet: {ws.title}")
for _, cell_range, value in cell_contents:
    print(f"{cell_range}{value}")

另外,对于 xls 此类旧式文件的情况,可以先使用 xls2xlsx 进行处理:

import os
from xlrd import open_workbook
from openpyxl import Workbook

xls_file_dir = './test_xls_path' 

for xls_file in os.listdir(xls_file_dir):
    if xls_file.endswith('.xls'):
        xls_file_path = os.path.join(xls_file_dir, xls_file)

        # 读取 xls 文件
        workbook = open_workbook(xls_file_path, formatting_info=True)  # 保留格式信息
        sheet = workbook.sheet_by_index(0)

        # 创建 xlsx 文件
        new_workbook = Workbook()
        new_sheet = new_workbook.active

        # 复制数据
        for row_idx in range(sheet.nrows):
            for col_idx in range(sheet.ncols):
                new_sheet.cell(row=row_idx + 1, column=col_idx + 1, value=sheet.cell_value(row_idx, col_idx))

        # 处理合并单元格
        if sheet.merged_cells:
            for (rlow, rhigh, clow, chigh) in sheet.merged_cells:
                cell_range = f"{new_sheet.cell(row=rlow + 1, column=clow + 1).coordinate}:{new_sheet.cell(row=rhigh, column=chigh).coordinate}"
                new_sheet.merge_cells(cell_range)

        # 保存路径
        new_xlsx_file_name = os.path.splitext(xls_file)[0] + '.xlsx'
        new_xlsx_file_path = os.path.join(xls_file_dir, new_xlsx_file_name)

        # 保存文件
        new_workbook.save(new_xlsx_file_path)
        print(f"Converted {xls_file} to {new_xlsx_file_name}")