SWE-CI: Evaluating Agent Capabilities in Maintaining Codebases via Continuous Integration
Paper: arXiv:2603.03823 Code: SKYLENAGE-AI/SWE-CI Code reference:
main@3f7235de(2026-04-23)
1. Motivation (研究动机)
现有代码 Agent 评测大多把软件工程简化成一次性修复:给定一个静态 issue、一个 base codebase 和一个 golden codebase,模型只需要在当前快照上让测试通过。SWE-CI 指出,这类 snapshot-based evaluation 无法暴露真实维护场景里的核心风险:早期修改如果引入了糟糕接口、过度耦合或局部 patch,短期测试可能通过,但后续需求到来时会让修改成本和回归风险持续累积。
本文要解决的具体问题是:如何评估 Agent 在 repository-level codebase 中持续维护代码的能力,而不是只评估单轮 functional correctness。作者把真实开源项目中相隔较长时间的两个 commit 作为 base/oracle pair,让 Agent 从 base commit 出发,通过多轮 CI-loop 逐步逼近 oracle commit 的测试表现,并在整个演化轨迹上衡量功能正确性、回归和代码风格。
这个问题值得研究,因为 maintainability 本质上是跨时间的属性。一个 Agent 是否真正“会写可维护代码”,只有在新 requirement 不断出现、旧接口被复用、之前的设计选择继续影响后续修改时才可见。SWE-CI 的价值在于把这个长期属性变成可执行、可复现的 benchmark:100 个真实 Python repository maintenance tasks、20 个主流模型、8 个 provider,以及与 CI 流程绑定的多轮测试反馈。
2. Idea (核心思想)
核心洞察:代码维护能力不能从单个终态判断,而应从一条 codebase evolution trajectory 中观察;好的修改不仅要提升当前测试通过数,还要让未来修改更容易继续提升且不破坏已通过测试。
SWE-CI 的关键创新是把 benchmark 从“静态需求 + 一次代码提交”改成“动态测试差距 + Architect/Programmer 双 Agent + 多轮 CI-loop”。Architect 根据当前代码与 oracle 测试报告之间的 gap 生成高层 requirement;Programmer 只基于 requirement 修改代码;外部系统运行 pytest 并把仍未通过的 oracle tests 反馈到下一轮。
与 SWE-Bench、LiveCodeBench、Terminal-Bench 等任务相比,SWE-CI 的根本差异不在于是否使用测试,而在于测试被用来驱动连续演化:第 轮的 requirement 是 ,下一轮状态是 。因此,模型的早期设计选择会影响后续所有轮次,长期可维护性会从轨迹中显现。
3. Method (方法)
3.1 Snapshot evaluation 到 evolution-based evaluation
Figure 1 解读:左侧 snapshot-based evaluation 中,需求只由 和 一次性确定,模型直接尝试完成全部差距;右侧 evolution-based evaluation 中,每一轮都先根据当前代码 与 oracle 的测试差距生成新需求,再把修改后的代码作为下一轮的输入。红色箭头表示 requirement generation,蓝色箭头表示 coding action;虚线部分表示用户不知道 oracle 的完整实现,只能通过测试反馈逐步逼近。
论文形式化定义两个函数: 负责根据测试集合 识别两个 codebase 的功能差距并生成 requirement, 负责根据 requirement 修改 codebase。传统 benchmark 等价于一次性求 ;SWE-CI 则反复执行: 这使得 evaluation target 从“当前 patch 是否通过”变成“模型能否在连续修改中保持未来可扩展性”。直觉上,如果一个模型每轮都用最小局部 hack 过测试,它可能早期涨得很快,但后续会因为结构复杂、接口不稳而更容易回归;如果一个模型愿意做更稳的抽象和模块化,EvoScore 会在后续轮次给它更高权重。
3.2 Normalized Change 与 EvoScore
给定测试集合 ,代码库 的测试通过数定义为: 其中 当且仅当 test 在 codebase 上通过。为了把“改进”和“回归”放到同一个尺度,论文定义 normalized change: 当 时,分母是 base 到 oracle 的总 gap,因此 表示完全追上 oracle test performance;当 时,分母是 base 已通过测试数,因此 表示把初始已通过测试全部打破。这个非对称归一化让不同 repo、不同 test suite size 的改进和回归都落在 上。
EvoScore 将 轮中的 normalized change 聚合为 future-weighted mean:
时就是所有轮次的平均 normalized change; 时后期轮次权重更高,用来强调长期稳定性。论文公式与 released code 实现差异:论文主文使用带 的 EvoScore,并在 Figure 5 分析 变化下的排名;released code 的 src/swe_ci/benchmark/summarize.py::metrics_func 目前只输出 EvoScore(gamma=1),即把每轮 rela_changes 做普通平均,没有实现可调 的汇总入口。
3.3 SWE-CI 数据构造
Figure 2 解读:SWE-CI 的数据构造从 GitHub Python repos 开始,依次筛选活跃仓库、抽取依赖稳定的 commit span、构建 Docker runtime、运行 oracle tests,再经过 case filtering 选出最终 benchmark。图中强调的不是随机采样 issue,而是沿真实 repository 历史选择两个相隔足够远、依赖环境可冻结、测试差距足够明显的 commit。
数据构造分四步:
- Repository Collection:搜索 GitHub 上 Python repositories,要求至少维护 3 年、超过 500 stars、包含
pyproject.toml/lockfiles 等配置与依赖文件、包含 unit tests、使用 MIT/Apache-2.0 等 permissive license。过滤后剩 4,923 个 repositories。 - Commit Span Extraction:只保留 main branch,把历史简化成线性 commits;寻找依赖不变的最大连续子序列,以子序列两端作为 base/oracle candidate pair;丢弃总 modified LOC 少于 1,000 的 pair,得到 8,311 个 candidate pairs。
- Environment Construction:根据 oracle codebase 的配置和依赖自动生成 Dockerfile,构建 runtime snapshot,并在其中运行 oracle test suite;缺失依赖导致测试无法启动时,系统会自动把缺失依赖注入 Dockerfile 并重建环境。该步骤后保留 1,458 个 candidate pairs。
- Case Filtering:在同一 runtime snapshot 中用 oracle tests 跑 base codebase;丢弃 test launch failure;再比较 base/oracle passing test count,要求通过数差距至少 5。自动过滤后剩 137 个 candidates,最后按历史跨度和 intervening commits 数排序,选择 top 100。
最终 SWE-CI 包含 100 个 samples,来自 68 个 distinct repositories;base/oracle pair 平均相隔 233 天和 71 个连续 commits;每个 pair 的 source-code modified LOC 至少 500(不计 test files)。每个 sample 同时提供完整 source code 和预构建 Docker environment,以保证评测可复现。
3.4 Architect–Programmer 双 Agent CI-loop
Figure 3 解读:SWE-CI 用 Architect 和 Programmer 模拟真实软件团队中的 CI 循环。外部系统把 current code 放进 frozen environment,运行 oracle tests 并生成 failed/non-passed report;Architect 只能读当前代码和 non-passed 信息,生成不超过 5 条高层 requirement;Programmer 读取 requirement 并修改 /app/code/;随后系统重新运行 tests,更新 current code 和下一轮 non-passed。oracle code 和 frozen oracle report 只用于生成差距,不直接暴露给 Programmer。
Architect 的三步是:Summarize failing tests、定位 root causes 和需要检查的 files;Locate source-code deficiencies;Design high-level improvement plan 并输出 requirement.xml。论文要求 requirement 增量化和高层化:每轮最多 5 个最紧急需求,只描述 expected behavior 和 verifiable result,不直接给实现细节。
Programmer 的三步是:Comprehend requirement,把自然语言 contract 转成代码规格;Plan 修改路径;Code 实现必要 changes。released code 中 prompt.jinja2 明确禁止 Programmer 主动运行 pytest,也禁止修改 /app/requirement.xml 和 /app/code/tests/;验证完全由外部 CI harness 完成。
3.5 Released code 的实际执行流程
Code reference:
main@3f7235de(2026-04-23) — pseudocode and mapping based on this commit
初始化任务目录与测试差距
from pathlib import Path
def initialize_task(task_metadata, data_dir: Path, task_dir: Path, cfg):
current_dir = task_dir / "current"
target_dir = task_dir / "target"
unzip(data_dir / "code.zip", current_dir / "code")
copytree(current_dir / "code", target_dir / "code")
git_checkout(current_dir / "code", task_metadata["current_sha"])
git_checkout(target_dir / "code", task_metadata["target_sha"])
remove_pattern_files(current_dir / "code", [".git*", "test", "tests"])
remove_pattern_files(target_dir / "code", [".git*", "test"])
copytree(target_dir / "code" / "tests", current_dir / "code" / "tests")
current_report = cold_test(data_dir / "image.tar.gz", cfg.agent.dockerfile, current_dir / "code")
target_report = cold_test(data_dir / "image.tar.gz", cfg.agent.dockerfile, target_dir / "code")
gap = generate_nonpassed_dir(current_report, target_report, current_dir)
update_iteration(gap, task_dir / "iteration.jsonl", current_report)这段对应 src/swe_ci/benchmark/initialize.py:current code 使用 base commit,但 tests 来自 target/oracle commit;target code 使用 oracle commit。这样 Programmer 看到的是“旧代码 + 新测试”,而不是 oracle implementation。
单轮 CI-loop
def evolution_loop(task_dir: Path, cfg):
current_dir = task_dir / "current"
for epoch in range(cfg.evolve.max_epoch):
tmp_dir = task_dir / "tmp"
clear(tmp_dir)
remove_file(current_dir / "requirement.xml")
architect_result = call_cli_agent(
container=current_code_plus_nonpassed(current_dir),
prompt=load_prompt("prompt.jinja2", role="architect"),
timeout=cfg.evolve.architect.timeout,
)
copy_from_container("/app/requirement.xml", current_dir)
programmer_result = call_cli_agent(
container=current_code_plus_requirement(current_dir),
prompt=load_prompt("prompt.jinja2", role="programmer"),
timeout=cfg.evolve.programmer.timeout,
)
copy_from_container("/app/code", tmp_dir / "code")
test_report = run_pytest(tmp_dir / "code", timeout=cfg.pytest.timeout)
if test_report.returncode <= 1 and test_report.exists():
gap = generate_nonpassed_dir(test_report, task_dir / "target" / "test_report.json", tmp_dir)
archive(current_dir)
promote(tmp_dir, current_dir)
else:
gap = -1
archive(tmp_dir)
update_iteration(gap, task_dir / "iteration.jsonl", test_report,
addition={"architect": architect_result, "programmer": programmer_result})
if gap == 0:
break这段对应 src/swe_ci/benchmark/run.py 和 src/swe_ci/benchmark/tools.py。实现中的 termination condition 是达到 CONFIG.evolve.max_epoch 或 gap == 0;默认 config.toml 中 evolve.max_epoch=20。
non-passed report 与指标汇总
def generate_nonpassed_dir(current_report, target_report, output_root):
current_passed = {t["nodeid"] for t in current_report["tests"] if t["outcome"] == "passed"}
target_passed = {t["nodeid"] for t in target_report["tests"] if t["outcome"] == "passed"}
nonpassed_ids = target_passed - current_passed
for node_id in nonpassed_ids:
write_jsonl("non-passed/summary.jsonl", {
"test": node_id,
"description": traceback_or_collection_failure(node_id),
})
return len(nonpassed_ids)
def metrics_func(init_pass, target_pass, evo_seq, max_epoch):
evo_seq = pad_or_truncate(evo_seq, length=max_epoch)
total_gap = target_pass - init_pass
relative_changes = []
for passed in evo_seq:
if passed > init_pass:
change = (passed - init_pass) / total_gap if total_gap > 0 else 1.0
elif passed < init_pass:
change = (passed - init_pass) / init_pass if init_pass > 0 else -1.0
else:
change = 0.0
relative_changes.append(change)
return {
"EvoScore(gamma=1)": sum(relative_changes) / max_epoch,
"solved_rate": float(max([init_pass] + evo_seq) == target_pass),
"zero_regression": float(all(b >= a for a, b in pairwise([init_pass] + evo_seq))),
}non-passed report 是 Architect 的主要输入:它只列出 target 已通过但 current 未通过的 tests,并记录失败 stage 与 traceback。指标实现则会把短于 20 轮的序列用最后一个通过数填充,因此较早解决任务的模型不会因为提前停止而损失后续轮次。
Code-to-paper mapping
| Paper Concept | Source File | Key Class/Function |
|---|---|---|
| Dataset download from Hugging Face | src/swe_ci/download.py | download_dataset, download_hf_folder |
| Task initialization / base-target checkout | src/swe_ci/benchmark/initialize.py | _init, init_tasks |
| CI-loop evolution protocol | src/swe_ci/benchmark/run.py | _run, _run_locked, run_tasks |
| Pytest execution and non-passed generation | src/swe_ci/benchmark/tools.py | run_pytest, generate_nonpassed_dir, update_iteration |
| Architect/Programmer prompts | src/swe_ci/benchmark/prompt.jinja2 | role == "architect", role == "programmer" prompt branches |
| CLI agent adapters | src/swe_ci/benchmark/agents/iflow.py; src/swe_ci/benchmark/agents/opencode.py | call_iflow, call_opencode, valid_and_parse |
| Metrics / released-code EvoScore | src/swe_ci/benchmark/summarize.py | metrics_func, summarize |
| Evaluation defaults | config.toml; src/swe_ci/config.py | pytest.timeout=3600, evolve.max_epoch=20, init/evolve.max_workers=16, architect/programmer.max_try=10 |
4. Experimental Setup (实验设置)
4.1 数据集与规模
SWE-CI 的最终 benchmark 有 100 个 repository-level tasks,来自 68 个 distinct GitHub Python repositories。构造过程中先筛到 4,923 个 repositories,再得到 8,311 个 dependency-stable commit-span candidate pairs,经 Docker environment construction 后保留 1,458 个 pairs,经 test-gap filtering 后剩 137 个 candidates,最后选 top 100。最终样本平均跨越 233 天、71 个 commits,且每个 base/oracle pair 至少包含 500 行非测试 source-code 修改。
4.2 对比模型 / baselines
论文评估 20 个模型、8 个 providers:Claude(claude-opus-4.5, claude-opus-4.6)、Qwen(qwen3-coder-plus, qwen3-max-2025-09-23, qwen3-max-2026-01-23, qwen3.5-plus)、GLM(GLM-4.6, GLM-4.7, glm-5)、MiniMax(MiniMax-M2.1, MiniMax-M2.5)、DeepSeek(deepseek-v3.1, deepseek-v3.2)、GPT(gpt-5.2, gpt-5.4-2026-03-05)、Doubao(doubao-seed-1-8-251228, doubao-seed-2-0-pro-260215)和 Kimi(kimi-k2-instruct-0905, kimi-k2-thinking, kimi-k2.5)。除非特别说明,Architect Agent 与 Programmer Agent 使用同一个 base model。
4.3 评价指标
- Normalized Change / EvoScore:衡量每轮测试通过数相对 base/oracle gap 的归一化变化;EvoScore 用 对未来轮次加权。
- Solved rate:最终维护过程是否曾达到 target/oracle 的全部通过测试;可视为 时对长期完成能力的极端观察。
- Zero regression rate:整个维护过程中是否从未让已通过测试数下降;用于衡量长期维护稳定性。
- Regression trend:回归发生率和回归幅度与迭代轮数的 Pearson correlation。
- Coding style metrics:Pylint score 表示显式风格规则,MI score(Maintainability Index)结合 cyclomatic complexity 等结构性信号衡量更深层 maintainability。
4.4 运行配置
论文实验使用 pytest 与 pytest-json-report,每次 test run timeout 为 3,600 秒;iFlow CLI 是默认 harness;dual-agent protocol 的最大迭代次数为 20。released code 的 config.toml 还给出默认 init.max_workers=16、evolve.max_workers=16、evolve.architect.timeout=3600、evolve.programmer.timeout=3600、evolve.architect.max_try=10、evolve.programmer.max_try=10。README 给出的默认运行环境估计为 32-core CPU、64 GB RAM、约 1 GB/s disk I/O、至少支持 16 并发请求的 LLM API key;dataset 约 50 GB,初始化约 30 分钟,完整 evolution 约 48 小时。本文不是模型训练论文,因此没有 LR、batch size、GPU 训练步数;主要成本来自多模型 API 调用、Dockerized pytest 和 16-worker 并发评测。
5. Experimental Results (实验结果)
5.1 Maintainability / EvoScore

Figure 4 解读:EvoScore 随模型发布时间整体上升,说明新一代模型在长期 code maintenance 上的能力提升比传统单轮 coding benchmark 更明显。Claude Opus 系列在全区间领先,其中 claude-opus-4.6 最高;glm-5 也是强模型。图中没有给每个点的表格数值,因此这里不臆造精确 EvoScore;可确定的结论是 provider 内 newer model consistently better,且 2026 年后的模型增幅更大。

Figure 5 解读:横轴可理解为 从低到高时模型排名变化。MiniMax、GPT、DeepSeek 的排名随 增大而下降,说明这些模型更偏向早期收益;Kimi、GLM、Qwen 的排名上升,说明它们在后期 maintainability 上相对更好;Doubao 和 Claude 因整体优势/劣势明显,排名对 更稳定。
5.2 Regression 控制仍是主要瓶颈

Figure 6 解读:zero regression rate 最高的是 claude-opus-4.6 的 76%,其次是 claude-opus-4.5 的 51%;只有这两个 Claude Opus 模型超过 50%。大多数模型低于 25%,例如 gpt-5.2 和 gpt-5.4-2026-03-05 都是 23%,qwen3.5-plus、deepseek-v3.2、MiniMax-M2.5 都是 20%,而 qwen3-max-2025-09-23 只有 7%。这说明即便模型能解决一部分新需求,也经常在后续轮次破坏先前通过的测试。
Figure 7 解读:左侧回归发生率与迭代轮数的相关性中,20 个模型里有 12 个为正相关,说明越到后期越容易发生 regression;右侧回归幅度与迭代轮数的相关性中,20 个模型里有 11 个为负相关,说明后期每次 regression 影响的测试比例反而变小。作者的解释是:早期容易需求先被解决,后期模型开始在更难需求上试错,于是 regression 更频繁但更局部。
5.3 Solved rate 与 average turns

Figure 10 解读:solved rate 最高的是 claude-opus-4.6 91%、claude-opus-4.5 67%、glm-5 64%、qwen3.5-plus 62%、MiniMax-M2.5 52%;然后是 deepseek-v3.2 43%、doubao-seed-1-8-251228 41%、GLM-4.7 40%、gpt-5.4-2026-03-05 40%。多数模型低于 50%,最低的是 deepseek-v3.1 8%、qwen3-coder-plus 19%、MiniMax-M2.1 和 qwen3-max-2025-09-23 各 23%。这与 EvoScore 排名大体一致,说明 EvoScore 能反映长期完成能力。

Figure 11 解读:强模型不一定用满 20 轮,例如 claude-opus-4.6 平均 5.3 turns、claude-opus-4.5 9.8 turns、glm-5 12.2 turns;弱模型更接近耗尽预算,例如 deepseek-v3.1 19.6 turns、qwen3-max-2025-09-23 18.5 turns、qwen3-coder-plus 18.0 turns。这支持作者的 Observation 8:瓶颈主要是模型能力,而不是“多给几轮就能补回来”。
5.4 LLM patch 更短,但 maintainability 不如 human oracle
Figure 8 解读:左侧 MI score 上,20 个模型全部输给 human oracle:即便最好的 claude-opus-4.5 也只有 42% cases 中 LLM 更好、58% 更差;gpt-5.4-2026-03-05 最差,只有 16% 更好、84% 更差。右侧 Pylint score 上,15/20 个模型优于 human oracle,例如 gpt-5.4-2026-03-05 是 72% 更好,MiniMax-M2.1、MiniMax-M2.5、gpt-5.2 都是 68% 更好。这说明 LLM 更会学习显式局部风格规则,但不擅长形成低复杂度、可扩展的整体结构。

Figure 9 解读:散点图比较 solved cases 中 LLM patch 与 human oracle patch 的 changed lines,大多数点落在对角线下方,表示 LLM 通常修改更少行。结合 Figure 8,这不是单纯“更简洁更好”,而是模型倾向用最少改动满足当前测试,缺少人类工程师会投入的抽象、封装和模块化。
5.5 后期收益递减

Figure 12 解读:不同 provider 的 cumulative maximum relative change 都在前 1–4 轮增长最快,之后收益明显放缓。曲线单调不降是因为画的是截至当前 turn bin 的最大 relative change;趋势说明模型优先解决容易 requirement,难问题和结构性维护问题在后期积累。
5.6 结论与局限
整体结论是:SWE-CI 能揭示 snapshot benchmark 看不到的 maintainability gap。最强模型在 solved rate、EvoScore 和 zero regression 上明显领先,但 20 个模型普遍仍难以长期避免 regression;LLM 代码在 Pylint 等表层风格上常胜过 human oracle,却在 MI score 和结构复杂度上全面落后。
作者没有单独设置 Limitations section;从 benchmark 设计可见的注意点是:SWE-CI 目前聚焦 Python repositories 和 pytest-style unit tests,maintainability 被 operationalized 为未来测试正确性与静态代码指标,不能覆盖所有架构质量;运行成本高(约 50 GB 数据、Docker、16 并发、约 48 小时);Architect 的需求来自 oracle test gap,因此更适合评估 CI-driven maintenance,而不是开放式产品需求理解。