fltech - 富士通研究所の技術ブログ

富士通研究所の研究員がさまざまなテーマで語る技術ブログ

ハーネスエンジニアリングのすすめ: 27BモデルでSWE-bench VerifiedのSLM SOTAを達成 (TTS@8=74.8%)

Qwen3.5-27B を追加学習なしで使い、GitHub上に実在する OSS issue をどこまで直せるかを測る SWE-bench Verified で、8本の候補から最終パッチを選ぶ構成により 229B 未満のローカル LLM としては SOTA*1である 74.8% を達成しました。

概要

富士通研究所の木村 功作、宗像 聡*2、中島 哲、石川 優、前田 耕輔、相馬 菜生、小林 健一、宮崎 桂輔、加藤 圭造、福田 茂紀、熊野 達夫、今村 信貴、Mehdi Bahrami、Kevin Takeshi Musgrave、Wei-Peng Chen、Shahbaz Abdul Khader、Kwun Ho Ngan、Joseph Townsend、Fayas Asharindavida、Matthieu Parizy、酒井 彬、市川 佑馬、Yang Zhao、滝澤 慶招、福井 琢、大辻 弘貴、小橋 博道です。

私達は mini-swe-agent の改造*3による SWE-bench Verified のスコア改善に取り組みました。SWE-bench Verified は、GitHub 上の実在する OSS issue を題材に、モデルがどこまで修正できるかを測る代表的なベンチマークです。本記事では、Qwen3.5-27B をそのままベースモデルとして使い、TTS@8*4374/500 = 74.8% を出した構成を紹介します。あわせて本記事は、SWE-bench/experiments に私たちが提出する実行結果・実行ログ・軌跡の技術的な補足説明も兼ねています。

この結果は、私たちが把握している範囲では 229B 未満のローカル LLM で SOTA*5 です。一番伝えたいのは、この種のベンチマークではモデルの訓練だけでなく、エージェントと評価系を含むハーネスエンジニアリングが同じくらい重要だという点です。ここでのハーネスエンジニアリングとは、複数のAIモデルやツール、データソースを連携・統合し、エージェントが効率的に動作できるようにパイプラインや実行基盤を設計・構築するエンジニアリングのことです。以下では結果の概要を確認したあと、mini-swe-agent と評価パイプラインで効いた設計を順に見ていきます。

モデルの公開総パラメータ数を横軸、解決率を縦軸に並べると、今回の構成は左上のパレートフロント上に位置します。27B という比較的小さなモデルで 74.8% を出しており、少なくとも今回整理した範囲では、これより小さいモデルでこれを上回るスコアは見当たりません。

表1: 競合比較(公開値ベース、抜粋)

構成 LLM 解決率 (%) 総パラメータ数 (B) 備考
Kozuchi mini-swe-agent Qwen3.5-27B 74.8 27 公式 sb-cli 計測値 / OSS LLM
不明 Qwen3.5-27B 72.4 27 OSS LLM
不明 Code World Model 65.8 32.6 OSS LLM
mini-SWE-agent MiniMax M2.5 75.8 229 比較上限
不明 Qwen3.5-397B-A17B 76.4 397 OSS LLM
OpenHands Qwen3-Coder-480B-A35B-Instruct 69.6 480 OSS LLM
Lingxi v1.5 Kimi K2 Instruct 71.2 1024 OSS LLM
不明 Kimi-K2.5-1T-A32B 76.8 1100 OSS LLM

冒頭の散布図と表1では、本文との一貫性のため Kozuchi mini-swe-agent の値に公式クラウド環境での 74.8% を使っています。公開値の測定方法は必ずしも統一されていないため、この図表は厳密な同条件比較というより、モデル規模と解決率の位置関係を大まかに把握するためのものとして読んでください。

SWE-bench Verified は、GitHub 上の実在する OSS issue を集めた SWE-bench から、問題文の明確さと評価テストの妥当性を人手で点検して選んだ 500 問のサブセットです。各課題では、エージェントが issue 文とコードベースを受け取り、修正パッチを生成します。公式評価では、そのパッチが issue を解決しているかを FAIL_TO_PASS テストで、既存機能を壊していないかを PASS_TO_PASS テストで判定し、両方を通ったものが resolved となります。

注: 上で述べた FAIL_TO_PASS / PASS_TO_PASS は SWE-bench Verified の公式評価で使われるデータセット付属のテストです。一方、本文中で単に FAIL_TO_PASS / PASS_TO_PASS と書く場合は、特記がない限り各 run でエージェントが自ら生成する検証用テストを指しています。

目次

1. 結果概要

Qwen3.5-27B による 8 runs の候補生成と後段の選抜器を組み合わせて、SWE-bench Verified の TTS@8 で次のスコアを得ました。

  • TTS@8: 374/500 = 74.8%(SWE-bench 公式クラウド環境 / sb-cli 計測値)
  • 位置づけ: 229B 未満のローカル LLM で SOTA
  • 選抜規則: simple weighted pass-rateF2P=0.3, P2P=0.7、同点時は shortest_patch_raw

提出スコアの測定系は sb-cli ベースに統一されているため、本記事でもその計測値を公開値として使います。比較対象として参照される公開値には別系統の評価が混ざることがあるので、厳密に同条件で比較する際は注意が必要です。それでも今回の 74.8% は、私たちの整理範囲では 229B 未満のローカル LLM として最高水準です。

今回の構成は、単発で 1 回生成して終わりではなく、複数の候補を生成してから最終提出パッチを 1 本に絞る形を取っています。

  1. エージェントが複数の修正候補を生成する
  2. 候補の中から最終提出パッチを選抜する

探索と選抜を分離することで、候補の多様性を保ちながら提出時の成功率を高めています。

ここで重要なのは、mini-swe-agent が直接「最終提出」を出すわけではない点です。候補生成 run はあくまで前段であり、公開した TTS@8 の数値は後段の選抜器と公式評価を含むシステム全体で決まります。今回の結果も、エージェント単体の性能だけでなく、候補の多様性と選抜の精度が合わさって出たものです。

以降では、具体的な修正事例を見たあと、エージェント設計、特化ツール、フェーズベースのスキル挿入、評価運用の順に構成を説明していきます。

1.1 公開に使う TTS@8 の数値

本記事で対外的に報告する主結果は TTS@8 です。「8本の候補生成 run を走らせ、その中から 1 本を選ぶ」という測定です。

  • 候補生成: Qwen3.5-27Borchestra 構成による 8 runs
  • 候補比較: 各 run が生成した FAIL_TO_PASS / PASS_TO_PASS を使ったエージェント間相互テスト
  • 最終的な選抜規則: simple weighted pass-rateF2P=0.3, P2P=0.7、同点時は shortest_patch_raw
  • 公開値: 374/500 = 74.8%(SWE-bench 公式クラウド環境 / sb-cli 計測値)

選抜規則は複雑なものが有利とは限りませんでした。最終的には、F2P/P2P の通過率を重み付きで集計し、同点時は shortest_patch_raw で決める構成を採用しています。本記事では投稿時の計測系に合わせ、対外比較に使いやすい値として、この sb-cli 計測値だけを扱います。

1.2 具体例: フォーム検証と HTML 描画をまたぐ Django の修正

個別の課題で何が起きていたかを見ると、この仕組みの意味がよくわかります。実際に解けた軌跡のひとつでは、Django の MultiValueField が「親は required=False だが、子の一部は required=True」というケースを正しく扱えていませんでした。結果として、空入力を通してしまうだけでなく、HTML 側でも本来付くべき required 属性が落ちていました。

一見するとフォーム検証のバグに見えますが、実際には「サーバ側のバリデーション」と「ブラウザへ渡す属性生成」の両方にまたがっています。こういった複数責務にまたがる不整合は、単一ファイルの場当たり的な修正では収まりにくく、原因の切り分けと影響範囲の把握が鍵になります。

問題の要点:

MultiValueField ignores a required value of a sub field

Form is valid: True
Expected is_valid=False but got True.

Number of 'required' attributes in HTML: 0
Expected 1 required attribute but got 0.

この課題は 1 箇所の修正では済みません。検証ロジックの穴を塞ぐだけでは不十分で、フォーム描画側でも「どの sub field が必須か」を正しく伝える必要があります。実際の最終パッチは、バリデーション・属性付与・ウィジェット描画の 3 箇所をまたいで修正しています。

最終パッチ(抜粋):

diff --git a/django/forms/fields.py b/django/forms/fields.py
@@
-                    return self.compress([])
+                    if not self.require_all_fields and isinstance(self.widget, MultiWidget):
+                        for field in self.fields:
+                            if field.required:
+                                raise ValidationError(self.error_messages['incomplete'], code='incomplete')
+                    return self.compress([])

diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py
@@
-        if widget.use_required_attribute(self.initial) and self.field.required and self.form.use_required_attribute:
-            attrs['required'] = True
+        if widget.use_required_attribute(self.initial) and self.form.use_required_attribute:
+            if hasattr(self.field, 'require_all_fields') and not self.field.require_all_fields:
+                ...
+            elif self.field.required:
+                attrs['required'] = True

diff --git a/django/forms/widgets.py b/django/forms/widgets.py
@@
+            if hasattr(self, '_field_requirements') and i < len(self._field_requirements):
+                widget_attrs['required'] = bool(self._field_requirements[i])

適用後テスト(軌跡ログより。SWE-bench の正解テストではなく、エージェントが実行中に自動生成した検証用テストです):

FAIL_TO_PASS: 9/9 passed
PASS_TO_PASS: 145/145 passed

ここで重要なのは、最終パッチが単一ファイルの場当たり的な修正ではなく、バリデーション・属性付与・ウィジェット描画という複数の責務をまたいで整合を取り直している点です。そのうえで、エージェントが自ら生成した FAIL_TO_PASSPASS_TO_PASS の両方を通しており、「不具合を閉じること」と「既存挙動を壊さないこと」を同時に満たしているのがわかります。ツールレベルの具体的な探索・修正の流れは 3 章と 4.4 で改めて示します。

1.3 TTS@8 測定結果の詳細

公開に使う主値は sb-cli374/500 = 74.8% ですが、submission bundle に残った採用軌跡を見ると「どこでターンを使ったか」まで分析できます。ここでは各インスタンスの総ターン数を、フェーズごとのエージェント出力ターン数の積み上げとして可視化します。

以降の軌跡図表の母集団は 500 件全体ではなく、エージェント間相互テストで採用ラベルが得られた 495 インスタンスです。残り 5 インスタンスは利用可能な相互テスト表や候補がないため、この分析からは除いています。採用元は 8 runs 全体にまたがっており、各 run からの採用件数は 43 件から 69 件でした。最終提出の候補群は特定の 1 run に偏っておらず、複数 run の探索結果を広く拾えています。

図1a: TTS@8 で採用された軌跡の総ターン数箱ひげ図

図1b: TTS@8 で採用された軌跡の総ターン数分布(フェーズ別積み上げ)

図中の resolved / unresolved の分割は、採用元 run 側の bundled report.json を使っています。submission bundle には sb-cli のインスタンスごとの判定が入っていないため、この図では採用元 run の判定を軌跡解析用のラベルとして使い、error 2 件は unresolved 側に含めました。したがって、この図は公開値そのものではなく「同じ採用軌跡がどのフェーズでターンを使ったか」を見るための補助分析です。公開値は引き続き sb-cli374/500 = 74.8% です。

まず図1aで全体像を見ると、採用された 495 件全体の中央値は 266 ターンp90449 ターン、最大は 925 ターン でした。解決側は中央値 259 ターンp90=428 ターン に収まる一方、未解決側は中央値 280 ターンp90=522 ターン と尾が重く、長い探索に入っても閉じ切れないケースが目立ちます。四分位範囲は全体で 231-330 ターン、解決側で 228-324 ターン、未解決側で 243-336 ターン で、未解決側の箱が全体として上にずれています。

そのうえで図1bを見ると、総ターン数の差がどのフェーズで生まれたかを追えます。

表2: フェーズごとのターン数分布(各セルは 中央値 [Q1, Q3] / p90、単位はエージェント出力ターン)

フェーズ 解決 未解決
ISSUE_REPRODUCT 35 [29, 46] / 57 35 [29, 44] / 54 39 [31, 49] / 63
TEST_SYNTHSIZE 33 [29, 40] / 47 33 [28, 40] / 47 34 [30, 40] / 49
CODE_LOCALIZE 26 [23, 30] / 38 26 [23, 29] / 36 28 [23, 34] / 44
TEST_LOCALIZE 49 [42, 59] / 69 49 [42, 59] / 69 51 [43, 60] / 68
CODE_FIX 41 [30, 75] / 138 40 [30, 69] / 134 46 [34, 80] / 168
VERIFY_PATCH 25 [23, 52] / 114 25 [23, 54] / 107 25 [23, 37] / 125
ISSUE_CLOSE 23 [21, 24] / 26 22 [21, 24] / 26 23 [21, 24] / 26
FINAL_REPORT 10 [10, 12] / 13 10 [10, 11] / 13 10 [10, 12] / 13

採用された 495 軌跡では、全フェーズで全インスタンスが少なくとも 1 回はエージェント応答を返していました。そのため、この表ではフェーズごとの「到達率」ではなく、「到達後にどれだけ長く滞在したか」の差に着目してください。

表2と図1bを合わせると、最も差が大きいのは CODE_FIX だとわかります。未解決側では中央値が 46 ターンp90168 ターン まで伸び、解決側の 40 ターン / 134 ターン より明確に重くなっています。VERIFY_PATCHp90 では 107 -> 125 ターン と伸びており、未解決インスタンスでは「最初の原因探索」よりも「修正と再検証を繰り返す終盤」でターンを消費しやすいことがわかります。

2. エージェント設計

2.1 今回採用した設計上のポイント

本取り組みでは、次の8つを主要な工夫として設計しました。

  1. フェーズ分割と明示遷移
  2. ファイルシステムを使ったフェーズ間・ワークフロー間の情報伝達
  3. ハンドオーバー機構によるコンテキスト圧縮
  4. Orchestra 実行系(conductor + tool-specialist
  5. 特化ツール(line_trace / caller_trace / coedit_localize / line_edit
  6. フェーズベースのスキル挿入
  7. エージェント間相互テストに基づく候補選抜
  8. 運用安定化(シャーディング + 再試行)

性能が上がったのは単一の工夫によるものではなく、探索を制御する設計、状態をファイルシステムに外出しして引き継ぐ共有領域、同一フェーズのハンドオーバーで文脈を圧縮する実行制御、失敗箇所を素早く見つけるツール、必要な手順だけを提示するプロンプト構成、エージェント間相互テストに基づく候補比較、そして大規模評価を回し切る運用、これらを組み合わせた結果です。以下で順に見ていきます。

TTS@8 では、候補生成のあとに各 run の結果を比較して最終提出パッチを 1 本に絞る段階も重要でした。この後段の選抜については 4.1 でまとめて説明します。

2.2 工夫1: フェーズ分割 + ワークフロー分割 + 明示的な遷移制御

ISSUE_REPRODUCT から FINAL_REPORT までを段階分割し、各フェーズの責務を固定しました。さらに各フェーズの内部も W0..Wn のワークフローに分割し、各ターンの先頭で WORKFLOW: Wn を自己申告させます。フェーズ終了は WORKFLOW: COMPLETE / WORKFLOW: GIVEUP に統一し、実行系が事後検証を行ってから実際の遷移を確定させます。

この設計の狙いは、1 つの長い会話の中でエージェントが「今何をしているか」わからなくなるのを防ぐことです。再現・原因特定・修正・検証・報告を混ぜると、必要な作業を飛ばしたり、逆に同じ探索を繰り返したりしやすくなります。そこで「フェーズで大きな責務を分け、その中をワークフローで細かく刻む」二段構えにしました。フェーズを明示すると各段階で求める成果物がはっきりし、ワークフローを明示すると「今どの手順をやっているか」「まだ踏んでいない手順は何か」を実行系が追跡できます。

図2a: フェーズ遷移(縦向き)

flowchart TD
  ISSUE_REPRODUCT["ISSUE_REPRODUCT<br/>不具合を再現"] -->|complete| TEST_SYNTHSIZE["TEST_SYNTHSIZE<br/>FAIL_TO_PASSテストを作成"]
  ISSUE_REPRODUCT -->|giveup| CODE_LOCALIZE["CODE_LOCALIZE<br/>原因箇所を特定"]
  TEST_SYNTHSIZE -->|complete| CODE_LOCALIZE
  TEST_SYNTHSIZE -->|giveup| CODE_LOCALIZE
  CODE_LOCALIZE -->|complete| TEST_LOCALIZE["TEST_LOCALIZE<br/>PASS_TO_PASSテストを選定"]
  CODE_LOCALIZE -->|giveup| ISSUE_REPRODUCT
  TEST_LOCALIZE -->|complete| CODE_FIX["CODE_FIX<br/>本体コードを修正"]
  TEST_LOCALIZE -->|giveup| CODE_FIX
  CODE_FIX -->|complete| VERIFY_PATCH["VERIFY_PATCH<br/>パッチを検証"]
  CODE_FIX -->|giveup| CODE_LOCALIZE
  VERIFY_PATCH -->|complete| ISSUE_CLOSE["ISSUE_CLOSE<br/>差分整理と提出前確認"]
  VERIFY_PATCH -->|giveup| CODE_FIX
  ISSUE_CLOSE -->|complete| FINAL_REPORT["FINAL_REPORT<br/>監査向け最終報告を作成"]
  ISSUE_CLOSE -->|giveup| CODE_FIX
  FINAL_REPORT -->|giveup| FINAL_REPORT

各フェーズの役割:

フェーズ 一文説明 主な成果物
ISSUE_REPRODUCT 不具合を再現し、現象を言語化する 再現手順と観測結果をまとめた再現スクリプト群
TEST_SYNTHSIZE FAIL_TO_PASSテストを設計・安定化する 修正前に失敗し修正後に通ることを確認する、エージェント生成の検証テスト群
CODE_LOCALIZE 根本原因と修正候補箇所を特定する 原因仮説と修正候補箇所を整理した分析メモ
TEST_LOCALIZE デグレッション検出に使うPASS_TO_PASSテストを選定する 修正後も維持すべき既存挙動を確認する、エージェント生成の回帰テスト群
CODE_FIX 原因に対応する本体コード修正を実装する 根本原因に対応した本体コードの修正差分
VERIFY_PATCH パッチの妥当性とテスト結果を点検する 修正内容と検証結果の整合性チェック記録
ISSUE_CLOSE 提出前に差分を整理し最終確認する 提出前チェックと不要差分除去の最終記録
FINAL_REPORT 監査向けに原因・修正・検証結果をまとめる 原因・修正・検証結果を統合した監査向け最終報告

図2aにフェーズ遷移の全体像を示します。 フェーズを切っただけでは不十分で、各フェーズの内部も W0..Wn に分けています。重要なのは、ワークフローが単なる進捗ラベルではなく、設定ファイルの中で「その段階で何を読み、何を書き、次に何へ進むか」まで定義されている点です。実行系は各応答の WORKFLOW: Wn を事後検証し、W3 の次にいきなり W5 へ進むような前方ジャンプは拒否します。一方、失敗して W5 -> W2 のように前のワークフローへ戻るのは許容しており、修正と再試行だけを繰り返せる構造になっています。

特化ツールがワークフローの中でどう並ぶかは 3.1 の冒頭で扱います。ここではまず、ワークフローが単なるラベルではなく戻り先まで含む実行手順になっていることが分かりやすい CODE_FIX の抜粋を見ます。

CODE_FIX では、修正ループそのものが定義に書かれています。

W2. Review the handed-over PASS_TO_PASS tests and extract invariants
W3. Modify the code based on the handed-over candidate root causes and fix locations
W4. Check whether the FAIL_TO_PASS tests pass, and if they fail, return to W2
W5. Check whether the PASS_TO_PASS tests pass, and if they fail, return to W2
W6. Based on the code changes, test untested boundary values and edge cases, and if any fail, return to W2

「W2 で守るべき不変条件を整理し、W3 で修正し、W4-W6 で落ちたら必ず W2 に戻る」という修正ループそのものが設定に書かれているわけです。W3 -> W4 -> W2 は正当な再試行ですが、W3 -> W7 のような手番飛ばしは拒否されます。

VERIFY_PATCH はこの思想をさらに強くしたフェーズです。ワークフロー自体に「FAIL_TO_PASS / PASS_TO_PASS が落ちたら WORKFLOW: GIVEUP」という分岐が入っており、そのうえで実行系が verify_*.log の存在と終了コードまで厳格な通過条件として再検査します。ワークフロー定義が一次の制約をかけ、実行系の事後検証が二次の検査をかける二重構造です。

フェーズ終了もエージェントの自己申告だけでは通しません。エージェントが出せる終了宣言は WORKFLOW: COMPLETE / WORKFLOW: GIVEUP だけで、実行系はそれを各フェーズの on_complete / on_giveup に対応付けて遷移させるため、途中のフェーズを任意に飛ばせません。WORKFLOW: COMPLETE が出たあと実行系は required_assets を検査し、足りない成果物があれば同じフェーズに引き継いでやり直させます。さらに VERIFY_PATCH では /_share/verify_fail_to_pass.log/_share/verify_pass_to_pass.log の存在に加え、それらを作る検証コマンドの終了コードまで確認します。「手順を踏んだと言ったか」ではなく「必要なファイルと検証結果が揃っているか」をフェーズ遷移の直前に機械的に見ているわけです。 図2bにフェーズ終了後の事後検証と引き継ぎ手順を示します。

図2b: フェーズ終了後の事後検証と引き継ぎ

flowchart TD
  classDef phase fill:#eef6ff,stroke:#2563eb,stroke-width:1.2px,color:#111827;
  classDef check fill:#f8fafc,stroke:#64748b,stroke-width:1px,color:#111827;
  classDef decision fill:#fff7ed,stroke:#ea580c,stroke-width:1.2px,color:#111827;

  A["エージェントが出力<br/>WORKFLOW: COMPLETE / GIVEUP"]:::phase
  B["実行系がフェーズ終了の宣言を解釈"]:::check
  C{"COMPLETE か?"}:::decision
  D["必須成果物 (`required_assets`) を検査"]:::check
  E["厳格な通過条件を検査<br/>共有資産の固定 / verify log / 終了コード"]:::check
  F["同一フェーズの引き継ぎ<br/>現在のフェーズをやり直す"]:::phase
  G["強制 GIVEUP による引き継ぎ<br/>on_giveup 先へ戻す"]:::phase
  H["通常の引き継ぎ<br/>on_complete / on_giveup 先へ進む"]:::phase
  I["引き継ぎ処理を実行<br/>LLM がメモを書いてファイルシステムへ保存<br/>必要なら共有テスト資産を固定<br/>次フェーズ用のプロンプトを再構築"]:::check

  A --> B --> C
  C -->|はい| D
  C -->|いいえ| E
  D -->|成果物不足| F
  D -->|OK| E
  E -->|通過条件を満たさない| F
  E -->|失敗が閾値回数続いた| G
  E -->|OK| H
  F --> I
  G --> I
  H --> I

フェーズ終了を宣言した時点では、まだ次のフェーズへの遷移は確定していません。実行系が事後検証を行い、required_assets や厳格な通過条件を満たしたときだけ次へ進めます。満たさない場合は同じフェーズへ引き継いでやり直し、失敗が閾値を超えたときだけ強制 GIVEUP で前段へ戻します。引き継ぎ処理自体も単なるフェーズ名の切り替えではなく、LLM に引き継ぎメモを書かせてファイルシステムへ保存し、次フェーズ用のプロンプトとスキルを再構築するところまで含んでいます。

再試行にも閾値を設けています。各フェーズには turn_handover_threshold があり、32 から 192 ターンの範囲で、長引いたら同一フェーズの引き継ぎを強制して文脈を圧縮し、局所的なループを切ります。VERIFY_PATCH にはさらに hard_gate_giveup_threshold=3 があり、通過条件の失敗が 3 回続くと実行系が強制的に WORKFLOW: GIVEUP へ切り替えて CODE_FIX に戻します。全体設定には同一コマンドの 5 連続実行で GIVEUP とする閾値も入れており、ワークフローでもフェーズでも「飛ばして進む」のではなく「失敗を明示して戻る」方向に寄せています。

CODE_LOCALIZETEST_LOCALIZE を分けたことは、今回の構成で特に重要でした。原因箇所を絞る作業と、修正後も守るべき既存挙動を特定する作業は、似ているようで目的が違います。これを分けることで、修正の正しさだけでなく「壊してはいけないもの」も明示的に扱えるようになりました。

これはベンチマーク特有の話ではありません。現実の大規模ソフトウェア開発では、仕様書に明示されていない現行仕様が複雑かつ大量に存在し、小さな修正でもどこかを落とすとすぐに不具合化します。原因箇所を見つけることと同じくらい、「どの既存挙動に反してはいけないか」を先に押さえることが重要なのはそのためです。TEST_LOCALIZE は、その暗黙の現行仕様の一部を既存テストからエージェントに拾わせ、狭い不具合修正を安全に成立させるためのフェーズでもあります。

2.3 工夫2: ファイルシステムを使ったフェーズ間・ワークフロー間の情報伝達

実行系は、コンテナ内ファイルシステム上に共有作業領域を作ります。具体的には /_share/ というパスですが、本質は「会話履歴の外に状態を置くこと」です。長いタスクで状態を会話履歴だけに載せると、ターン制限や引き継ぎで文脈が落ちやすくなります。そこで今回は、進捗・仮説・再現スクリプト・テスト・trace ログをファイルシステム上のファイルとして残し、次のワークフローや次のフェーズがそれを読む形にしました。情報を「思考の中」ではなく「共有ファイル」に置くのがポイントです。

共有資産 スコープ 役割
/_share/{PHASE}.md 同一フェーズ内のワークフロー間 戦略、観測、候補、進捗を蓄積する作業メモ
/_share/Kanban.md 全フェーズ横断 後続メンバー全員に共有したい知見の要約
/_share/handover_{from}_to_{to}.md フェーズ間 / 同一フェーズの再引き継ぎ 次の担当が迷わず再開するための圧縮メモ
/_share/repro_*.py, test_FAIL_TO_PASS_*.py, test_FAIL_TO_PASS_all.sh, test_PASS_TO_PASS_all.sh ワークフロー間 / フェーズ間 後続がそのまま再実行できる検証スクリプト群
/_share/line_trace_*.log, caller_trace_*.log, coedit_*.log, verify_*.log ワークフロー間 / フェーズ間 後続がそのまま再読できる調査・検証ログ

図3: フェーズ群と共有資産の関係

flowchart TD
  classDef phase fill:#eef6ff,stroke:#2563eb,stroke-width:1.2px,color:#111827;
  classDef asset fill:#f8fafc,stroke:#64748b,stroke-width:1px,color:#111827;
  classDef selector fill:#fff7ed,stroke:#ea580c,stroke-width:1.2px,color:#111827;

  P1["探索フェーズ群<br/>ISSUE_REPRODUCT / TEST_SYNTHSIZE / CODE_LOCALIZE / TEST_LOCALIZE"]:::phase
  P2["修正フェーズ<br/>CODE_FIX"]:::phase
  P3["評価・提出フェーズ群<br/>VERIFY_PATCH / ISSUE_CLOSE / FINAL_REPORT"]:::phase

  M["/_share/*.md<br/>フェーズメモ / Kanban / 引き継ぎ"]:::asset
  T["/_share/test_FAIL_TO_PASS*.py<br/>/_share/test_FAIL_TO_PASS_all.sh<br/>/_share/test_PASS_TO_PASS_all.sh"]:::asset
  L["/_share/trace / coedit / 検証ログ"]:::asset
  X["エージェント間相互テスト<br/>各 run が残したテスト資産を相互適用"]:::selector

  P1 --> P2 --> P3

  P1 -->|書き込む| M
  P1 -->|書き込む| T
  P1 -->|書き込む| L

  P2 -->|読む / 更新する| M
  P2 -->|読み取り専用で実行| T

  P3 -->|読む / 追記する| M
  P3 -->|再利用する| T
  P3 -->|書き込む| L

  T -->|比較証拠| X

図3では、個別フェーズを「探索」「修正」「評価・提出」の3群に集約しています。

ISSUE_REPRODUCT を例にすると、W1 でフェーズメモに戦略を書き、W6-W7 で再現スクリプトを作り、W9 で重要知見を Kanban に追記します。次フェーズの担当は引き継ぎメモだけでなくファイルシステム上の共有資産一式を入力として読み始めるため、「前の担当の頭の中」ではなく「残されたファイル」から作業を再開できます。CODE_LOCALIZETEST_LOCALIZE で trace ログや coedit ログを保存するのも同じ考え方です。

特に TEST_SYNTHSIZETEST_LOCALIZE が残す /_share/test_FAIL_TO_PASS_*.py/_share/test_FAIL_TO_PASS_all.sh/_share/test_PASS_TO_PASS_all.sh は、後段のエージェント間相互テストで各 run の修正パッチに相互適用する比較証拠そのものです。つまりこの共有領域は、フェーズ引き継ぎのためのメモ置き場であるだけでなく、TTS の選抜器が後から集計する実行可能な比較証拠の保管場所でもあります。

この共有領域は、フェーズ間の引き継ぎだけでなく、同一フェーズ内のループ抑制にも使っています。あるフェーズが turn_handover_threshold を超えると実行系は同一フェーズの引き継ぎを強制し、引き継ぎメモを残したうえで「ファイルシステム上の現行資産から続ける」よう指示します。文脈を圧縮して局所的なループを切りながら、途中まで作った再現スクリプト・テスト・分析メモは捨てずに済みます。どのタイミングで圧縮をかけるか、またツール実行結果をどこまで履歴に残すかは、次節のトークン予算制御で決まります。

もう 1 つ重要なのは、この共有領域を「何でも書ける置き場」ではなく、共有契約の置き場として使っている点です。たとえば CODE_FIX に入る前に /_share/test_PASS_TO_PASS* は読み取り専用に固定されるので、修正担当は回帰確認用の共有テストを勝手に弱めることができません。一方で Git 管理は /testbed/ だけに効くため、共有領域のメモやログは最終提出パッチを汚さずに積極的に残せます。さらに一括実行側で回収対象の許可リストに入った共有資産をインスタンスごとの成果物として回収しているため、後から引き継ぎメモや trace の中身を監査しやすいのも運用上効きました。

2.4 工夫3: ハンドオーバー機構によるコンテキスト圧縮

ファイルシステムに中間成果物を出している理由の 1 つは、会話履歴をそのまま持ち続けなくても作業を再開できるようにするためです。今回の実行系では、ハンドオーバーを単なる担当交代ではなくコンテキスト圧縮の仕組みとしても使っています。フェーズを跨ぐときだけでなく、同一フェーズの途中でも handover(current_phase) を使い、要点をまとめたメモをファイルシステムへ保存してから次ターンのメッセージ列を作り直します。

ここで重要なのは、「長そうだから圧縮する」のではなく、実行系がモデルごとの tokenizer でトークン数を見積もってから判断している点です。MODEL_ID に対応する Hugging Face tokenizer と chat template で現在の messages をトークン化し、max_prompt_tokens=150000max_new_tokens=16384MAX_MODEL_LENcontext_margin を使って安全側の予算を計算します。tokenizer を初期化できない場合は「安全なコンテキスト管理ができない」として、その時点で処理を止めます。モデル側が usage.prompt_tokens / usage.completion_tokens を返す場合は、その実測値も診断ログに残します。

実際の判定は概ね次の形です。

prompt_budget =
  min(max_prompt_tokens - context_margin,
      MAX_MODEL_LEN - max_new_tokens - context_margin)

prompt_est = estimate_tokens(messages)
if prompt_est > prompt_budget:
    handover(current_phase)
    prompt_est = estimate_tokens(messages_after_handover)
    if prompt_est > prompt_budget:
        raise LimitsExceeded()

この同一フェーズの引き継ぎでは、長い会話履歴そのものを次ターンへ持ち越す代わりに、LLM に引き継ぎメモを書かせてファイルシステムへ保存し、そのメモと共有資産を読ませる形に縮約します。再現スクリプト・生成テスト・trace ログ・Kanban は保持したまま、会話履歴だけを短くできます。turn_handover_threshold もこの圧縮機構と一体で使っており、フェーズごとに 32 から 192 ターンの閾値を超えると、トークン予算の超過前でも同一フェーズの引き継ぎを強制して局所ループを切ります。

図4: ハンドオーバーを使ったコンテキスト圧縮の判断フロー

flowchart TD
  classDef step fill:#eef6ff,stroke:#2563eb,stroke-width:1.2px,color:#111827;
  classDef decision fill:#fff7ed,stroke:#ea580c,stroke-width:1.2px,color:#111827;
  classDef warn fill:#f8fafc,stroke:#64748b,stroke-width:1px,color:#111827;

  A["ターン開始"]:::step --> B["プロンプト長を見積り"]:::step
  B --> C{"予算内か"}:::decision
  C -->|いいえ| D["同一フェーズの引き継ぎで圧縮"]:::step
  D --> E{"圧縮後も超過か"}:::decision
  E -->|はい| F["LimitsExceeded で終了"]:::warn
  C -->|はい| G["LLM 応答を取得して実行<br/>観測を生成"]:::step
  E -->|いいえ| G
  G --> H{"観測込みで予算内か"}:::decision
  H -->|はい| I["履歴へ追加"]:::step
  H -->|いいえ| J["出力を段階的に短縮"]:::step
  J --> K{"収まったか"}:::decision
  K -->|はい| I
  K -->|いいえ| L["最小 stub に置換"]:::warn
  L --> I
  I --> M{"手数閾値を超えたか"}:::decision
  M -->|はい| D
  M -->|いいえ| N["次ターンへ"]:::step

図4では、トークン数の見積りに使う tokenizer と予算式の詳細は本文に出し、図の中では「圧縮するか」「出力を短縮するか」「長引いたので同一フェーズの引き継ぎを入れるか」という分岐だけを残しています。

もう 1 つ効いているのは、ツール実行結果もトークン予算で管理している点です。コマンド実行後は action_observation_template で観測文字列を作りますが、会話履歴へ入れる前に messages + observation 全体のトークン数を再見積もりします。予算を超える場合は max_output_length を半分にしながら最大 6 回まで再描画し、それでも収まらなければ returncode と空の output だけを持つ最小 stub に置き換えます。巨大な trace やテスト出力が 1 回で文脈を押し流さないようにするためです。

この設計により、圧縮の単位が「とにかく古い履歴を切る」ではなく「共有資産を残したまま、次フェーズや次メンバーが再開できる要約へ変換する」になっています。フェーズ設計・ファイルシステム・ハンドオーバー・出力切り詰めが別々の部品ではなく、1 つのコンテキスト管理機構として噛み合っているのが今回の構成では重要でした。

2.5 工夫4: Orchestra 実行系(conductor + tool-specialist

Qwen3.5-27B を使った最新の 8-run 系では、図5に示すようなconductortool-specialist を分ける Orchestra 実行系を使っています。conductor は仮説探索や次の一手の判断を担当し、tool-specialist は厳密なツール呼び出し形式を崩さずにコマンド列を出す役割です。

この分離は、異なるモデル・異なる vLLM サーバの組み合わせでも実現できます。実際にいくつかの組み合わせを試しましたが、今回のベスト構成では、同じ Qwen3.5-27B を同じ vLLM サーバ上で共有しつつ、conductortool-specialist の温度だけを分ける形に落ち着きました。

重要なのは、実行系として「1 ターンを二段に分ける」点です。conductor は次に何をするかを決め、tool-specialist はその意図を保ったまま実行コマンドを整形します。

図5: Orchestra 実行系の基本連携

flowchart TD
  classDef agent fill:#e0f2fe,stroke:#0284c7,stroke-width:1.4px,color:#0f172a;
  classDef data fill:#fff7ed,stroke:#ea580c,stroke-width:1.4px,color:#0f172a;
  classDef runtime fill:#f8fafc,stroke:#64748b,stroke-width:1.2px,color:#0f172a;

  A["入力データ<br/>現在のフェーズ + 会話状態"]:::data --> B("conductor エージェント<br/>仮説探索と次の一手の決定"):::agent
  B --> C["全体草案<br/>WORKFLOW / THOUGHT / 草案コマンド"]:::data
  C --> D{{"実行系の結合部<br/>action block を抽出"}}:::runtime
  D --> E("tool-specialist エージェント<br/>コマンドだけを整形"):::agent
  E --> F["整形後の action block"]:::data
  F --> G["最終応答<br/>思考部分は conductor<br/>コマンドは tool-specialist"]:::data

今回採用した最良構成での役割分担を表にすると次の通りです。

役割 共有する接続先 生成設定 主な責務
conductor 同じ Qwen3.5-27B を同じ vLLM 接続先で共有 temperature=0.6, top_p=0.95, top_k=20 WORKFLOW / THOUGHT を含む全体方針の生成
tool-specialist 同じ Qwen3.5-27B を同じ vLLM 接続先で共有 temperature=0.0, top_p=0.95, top_k=20 action block だけの整形、ラッパー・引用符・パイプの安定化

この温度差を入れた理由は、両者が抱える失敗モードが逆だからです。conductor 側は温度を下げすぎると方策の多様性が狭まり、同じ行動パターンに閉じてループしやすくなります。一方で tool-specialist 側は温度を上げると、ツールの呼び方やシェル制御を崩しやすくなります。探索を担う conductortemperature=0.6 で動かし、整形だけを担う tool-specialisttemperature=0.0 に固定することで、このトレードオフを分離しました。

実際の処理はシンプルです。まず conductorWORKFLOW / THOUGHT と草案コマンドブロックを含む全体応答を作ります。次に実行系がその草案からコマンドブロックだけを抜き出し、現在のフェーズと検出したツール名を添えて tool-specialist に渡します。tool-specialist は思考部分をやり直すのではなく、そのコマンドブロックだけを実行しやすい形に整えます。最終的な assistant 応答では、WORKFLOW / THOUGHTconductor のものを残し、FINAL_ANSWER のコマンドだけを tool-specialist が整形した版に差し替えます。

役割分担が実際に効いている例として、r01matplotlib__matplotlib-20826VERIFY_PATCH で次のターンがありました。conductor は「必要な成果物を確認し、その後 FAIL_TO_PASS / PASS_TO_PASS の両方を検証する」という意図で、まず次の全体草案を返しています。

WORKFLOW: W7
THOUGHT: W7 is to update the team's shared Kanban. I've updated the Kanban with my verification findings. Now I need to verify that all required phase assets exist and both test commands exit with code 0 before completing the phase.

FINAL_ANSWER:
<tool: bash>
ls -la /_share/VERIFY_PATCH.md /_share/verify_fail_to_pass.log /_share/verify_pass_to_pass.log && echo "All required assets exist" && bash /_share/test_FAIL_TO_PASS_all.sh > /dev/null 2>&1; echo "FAIL_TO_PASS exit code: $?" && bash /_share/test_PASS_TO_PASS_all.sh > /dev/null 2>&1; echo "PASS_TO_PASS exit code: $?"
</tool>

このままだと、FAIL_TO_PASS が失敗しても echo 自体は成功するので、その後の PASS_TO_PASS 検証まで進んでしまいます。実行系はこの全体草案のうち FINAL_ANSWER の action block だけを抜き出して tool-specialist に渡し、tool-specialist は同じ意図を保ったまま次のように書き換えました。

<tool: bash>
ls -la /_share/VERIFY_PATCH.md /_share/verify_fail_to_pass.log /_share/verify_pass_to_pass.log && echo "All required assets exist" && bash /_share/test_FAIL_TO_PASS_all.sh > /dev/null 2>&1 && echo "FAIL_TO_PASS exit code: 0" && bash /_share/test_PASS_TO_PASS_all.sh > /dev/null 2>&1 && echo "PASS_TO_PASS exit code: 0"
</tool>

差分は 2 点です。

- ... bash /_share/test_FAIL_TO_PASS_all.sh > /dev/null 2>&1; echo "FAIL_TO_PASS exit code: $?" && bash /_share/test_PASS_TO_PASS_all.sh > /dev/null 2>&1; echo "PASS_TO_PASS exit code: $?"
+ ... bash /_share/test_FAIL_TO_PASS_all.sh > /dev/null 2>&1 && echo "FAIL_TO_PASS exit code: 0" && bash /_share/test_PASS_TO_PASS_all.sh > /dev/null 2>&1 && echo "PASS_TO_PASS exit code: 0"

1 つ目は ;&& に変えたことで、FAIL_TO_PASS が失敗した時点でコマンド列が止まり PASS_TO_PASS まで進まなくなります。2 つ目は "$?" を固定文字列の 0 に変えたことで、「成功したときだけこの echo を出す」制御に変わっています。失敗時は終了コードを表示するのではなく、その場でチェーン全体を止める意図です。

最終的な assistant 応答では、WORKFLOW / THOUGHTconductor のまま残り、FINAL_ANSWER のコマンドだけが tool-specialist 版に置き換わります。conductor が決めるのは「何を確認するか」で、tool-specialist がやるのは「その意図をシェルの制御フローとして破綻なく表すこと」です。

別の種類の補正として、専門ツールの使い方を直した例もあります。r01astropy__astropy-13453CODE_FIX では、conductorline_edit を使って self.data.cols = cols を挿入しようとしましたが、最初の草案は「既存 1 行を 3 行ブロックで置き換える」形でした。

<tool: bash>
python /_tools/line_edit.py --file /testbed/astropy/io/ascii/html.py --line 356 --expected "self.data._set_fill_values(cols)" --text "self.data._set_fill_values(cols)\n        self.data.cols = cols\n        self.data._set_col_formats()"
</tool>

tool-specialist はこれを、line_edit の契約に沿った「既存行の直後へ必要な 1 行だけを追加する」呼び方へ直しました。

<tool: bash>
python /_tools/line_edit.py --file /testbed/astropy/io/ascii/html.py --line 356 --expected "self.data._set_fill_values(cols)" --text "self.data.cols = cols" --append
</tool>

差分だけを見ると次の通りです。

- python /_tools/line_edit.py --file /testbed/astropy/io/ascii/html.py --line 356 --expected "self.data._set_fill_values(cols)" --text "self.data._set_fill_values(cols)\n        self.data.cols = cols\n        self.data._set_col_formats()"
+ python /_tools/line_edit.py --file /testbed/astropy/io/ascii/html.py --line 356 --expected "self.data._set_fill_values(cols)" --text "self.data.cols = cols" --append

変わっているのはコードの意図ではなく、ツールの使い方です。line_edit は編集対象の行と --expected を基準にして編集するツールなので、既存行を含む複数行ブロックで置換するより「一致確認した 1 行の直後へ 1 行だけ足す」方が安全です。ここでも conductor は「何を足すべきか」を決め、tool-specialist は「その編集をこのツールでどう安全に表すか」を担っています。

Orchestra 実行系の本質は設定ファイルの見た目ではなく、1 ターンの中で「探索」と「コマンド整形」を分離している点にあります。今回のベストでは同じ Qwen3.5-27B を温度違いで使う構成が、軌跡の多様性とツール呼び出しの安定性の両立に最も効きました。

3. ツールとスキル

3.1 工夫5: 特化ツールの活用

エージェント設定で line_tracecaller_tracecoedit_localizeline_edit を有効化しています。この 4 つを使い分けることで、「どこが壊れたか」「どこまで影響するか」「次にどの周辺ファイルやテストを見るべきか」「どう直すか」を短いループで回せます。

一般的なエージェントは greppytest を繰り返しながら広く探索することはできますが、実行経路や影響範囲を精密に取り、そこから隣接する修正候補まで系統的に広げるのは得意ではありません。そこで今回の mini-swe-agent には、調査と編集を支援する特化ツールをあらかじめ載せています。重要なのは単にツールを増やしたことではなく、フェーズごとに「使うべきタイミング」がはっきりしている点です。

特化ツールがワークフローの中でどう並ぶかは、TEST_LOCALIZE の定義を見るとわかりやすいです。実際にはより長いワークフローが書かれていますが、以下は説明のための要約・抜粋です。

- name: TEST_LOCALIZE
  definition: |-
    **Workflow (You MUST follow it!!):**
    * W2. List without omissions the test cases that currently pass before any code changes
      - Input: Test assets under `/testbed/`
      - Output: `/_share/TEST_LOCALIZE.md`

    * W3. Investigate with `line_trace` which passing tests may be affected by the code change
      - Input: `/_share/CODE_LOCALIZE.md`, `/_share/TEST_LOCALIZE.md`
      - Output: `/_share/line_trace_of_test_PASS_TO_PASS.log`

    * W4. Investigate with `caller_trace` which passing tests may be affected by the code change
      - Input: `/_share/CODE_LOCALIZE.md`, `/_share/TEST_LOCALIZE.md`
      - Output: `/_share/caller_trace_of_test_PASS_TO_PASS.log`

    * W5. Run `coedit_localize` on the strongest fix candidates and preserve the raw output
      - Input: `/_share/CODE_LOCALIZE.md`, `/_share/TEST_LOCALIZE.md`
      - Output: `/_share/coedit_TEST_LOCALIZE.log`

    * W6. Identify via code review the tests that should be affected by the code change
      - Input: `line_trace` / `caller_trace` / `coedit_localize` のログ群
      - Output: `/_share/TEST_LOCALIZE.md`

    * W7. Develop a script that can run all selected PASS_TO_PASS tests at once
      - Input: `/_share/TEST_LOCALIZE.md`
      - Output: `/_share/test_PASS_TO_PASS_all.sh`

この抜粋からわかるのは、TEST_LOCALIZE が単なる「回帰テストを探す」フェーズではない点です。既存の passing test を列挙し、line_tracecaller_tracecoedit_localize という異なる観点の特化ツールで候補を絞り込み、最後に 1 本の PASS_TO_PASS 実行スクリプトにまとめる、具体的な手順列として定義されています。特化ツールは個別にぶら下がっているのではなく、ワークフローの中で役割分担された状態で使われています。

3.1.1 line_trace: 実行行と変数変化の可視化

動作例(実際に解けた Django 事例):

line_trace django/forms/fields.py <failing test>

効果を発揮した例(軌跡出力):

[trace] /testbed/django/forms/fields.py:1024 | if not value or isinstance(value, (list, tuple)):
[trace] /testbed/django/forms/fields.py:1025 | if not value or not [v for v in value if v not in self.empty_values]:
[trace] /testbed/django/forms/fields.py:1026 | if self.required:
[trace] /testbed/django/forms/fields.py:1029 | return self.compress([])

このログにより、「required な sub field があるのに、空入力でそのまま compress([]) に抜けている」という実際の逸脱経路を行単位で確認できました。コードを読むだけでは見落としやすい分岐を、実行経路として押さえられたのが大きい点です。

不具合修正では、最終的に落ちた行よりも「その値がどうやって作られたか」が重要なことが多くあります。line_trace はそこを行レベルで追えるため、雰囲気でファイルを当てにいく探索をかなり減らせました。

3.1.2 caller_trace: 呼び出し経路と影響範囲の特定

動作例(実際に解けた Django 事例):

caller_trace django/forms/boundfield.py:BoundField.build_widget_attrs <failing test>

効果を発揮した例(軌跡出力):

=== caller_trace output ===
Target function: /testbed/django/forms/boundfield.py:BoundField.build_widget_attrs
Target hits: 1
Unique transitive callers (<full_path>:<qualname>): 5
- /testbed/django/forms/boundfield.py:BoundField.as_widget [direct]
- /testbed/django/forms/forms.py:BaseForm._html_output
- /testbed/django/forms/forms.py:BaseForm.as_table

この結果で、問題が単なる clean() の検証漏れではなく、フォームの描画経路にも波及していることがはっきりしました。「空入力を弾く」だけでなく「required 属性をどこで付与するか」まで見ないと不十分だとわかります。

単に直すだけでなく、何が影響を受けるかを把握するのがこのツールの役目です。SWE-bench Verified では「修正は通ったが既存挙動を壊した」という失敗も多いため、呼び出し経路を動的に拾えることがそのまま回帰防止に効きます。

3.1.3 coedit_localize: 共編集履歴から隣接候補を広げる

動作例(実際に解けた scikit-learn 事例):

python /_tools/coedit_localize.py /testbed/sklearn/cluster/optics_.py 2>&1 | tee /_share/coedit_CODE_LOCALIZE.log

このコマンドは、解けた scikit-learn__scikit-learn-14496CODE_LOCALIZE で実際に実行したものです。このインスタンスでは共編集データがなく、warning: no pair data for seed path: sklearn/cluster/optics_.py が返りました。ここで重要なのは、候補が出ないこと自体が「履歴ベースでは広げない方がよい」という否定的な証拠になる点です。無理に周辺ファイルへ探索を広げず、line_tracecaller_trace で得た強い起点に寄せ続けられます。

効果を発揮した例(ツール検証で固定している実出力):

pkg/helpers.py
  coedit_insight: The helper participates in the same execution path as core.
  coedit_decision_reason: Both files implement the same feature area.

pkg/shared.py
  coedit_insight: Shared helpers are often updated together with core logic.
  coedit_decision_reason: Shared abstractions couple the affected behavior.

tests/test_core.py
  coedit_insight: Tests for the core module tend to move with core behavior changes.
  coedit_decision_reason: The test validates the public contract of pkg/core.py.

docs/guide.md
  coedit_insight: Documentation sometimes follows feature changes.
  coedit_decision_reason: Docs are not executable code paths.

coedit_localize は、現在の SWE-bench インスタンスに対応する共編集データを使って、ある起点ファイルの周辺で一緒に直されやすい関連ファイルを優先順で返します。CODE_LOCALIZE では修正候補の拡張に、TEST_LOCALIZE では PASS_TO_PASS 候補テストの拡張に使います。

この共編集データは、解く対象インスタンスの base_commit より古い履歴だけを対象に、同一コミットに含まれていたファイル組を列挙し、相関ルールマイニングをかけて「偶然よりも高い確率で同時に編集されている」組に絞り込んだものです。将来の修正を見てしまうリークを避けつつ、その時点までの履歴だけで「この起点の周辺では何が一緒に動きやすいか」を取り出しています。

さらに、絞り込まれた各ファイル組については Devstral-Small-2-24B-Instruct-2512 を使って、なぜ同時編集されやすいのかを事前にテキスト化しています。ツール出力の coedit_insightcoedit_decision_reason はこの説明に対応しており、単にファイル名を列挙するだけでなく、「同じ実行経路にある」「公開 API とそのテストが結び付いている」といった関係をその場で読めます。

共編集データがあるインスタンスでは、実装ファイル・共通 helper・回帰を見たいテスト・優先度の低いドキュメントまでを理由つきで返せます。line_tracecaller_trace で「今強そうな起点」を見つけたあと、次にどの隣接ファイルやテストを見るべきかを履歴ベースで絞り込めるわけです。特に SWE-bench Verified では原因ファイルだけ直して周辺の既存挙動を壊す失敗が多く、隣接候補を優先順で補う価値が大きいです。

3.1.4 line_edit: 単一行修正の確実化

動作例(実際に解けた Django 事例):

python /_tools/line_edit.py --file /testbed/django/forms/fields.py --line 1029 --expected "                    return self.compress([])" --text "                    ... raise ValidationError(...) ...\n                    return self.compress([])"

効果を発揮した例(軌跡出力):

Updated /testbed/django/forms/fields.py:1029
Old: '                    return self.compress([])'
New: '                    ... raise ValidationError(...) ...\n                    return self.compress([])'

この修正を起点に、同じ軌跡では boundfield.pywidgets.py にも修正が広がり、最終的にエージェント自身が生成した FAIL_TO_PASS テストは 9/9、PASS_TO_PASS テストは 145/145 を通しました。局所修正を安全に積み重ねて、複数ファイルの整合を崩さずに進められるのが line_edit の強みです。

編集ツールを専用化した理由は、差分の大きさではなく失敗コストにあります。1 行修正のつもりがずれて別の行を書き換えると、原因調査そのものがやり直しになりかねません。地味なツールですが、長い実験ループの中ではかなり効く部品でした。

3.2 工夫6: フェーズベースのスキル挿入(フェーズ × ツール)

各スキルに phasestools 条件を持たせており、エージェントは現在フェーズと有効ツールを見て、必要なスキルだけをプロンプトへ挿入します。

プロンプト設計で起きがちな問題は、役に立つ手順を増やすほど全体が長くなり、かえって重要な指示が埋もれることです。そこで本構成では、すべての知識を毎回入れるのではなく、現在のフェーズと利用可能ツールに応じて必要なスキルだけを出すようにしました。

if current_phase in skill.phases and required_tools ⊆ agent.tools:
    inject(skill.content)

これは単なるプロンプト短縮ではありません。エージェントにとっては、今やるべき作業の選択肢を減らすこと自体が性能改善になります。特に長いタスクでは、「何を知っているか」以上に「今どの知識を前面に出すか」が効きます。

フェーズベース挿入の例(エージェント設定):

スキル フェーズ条件 ツール条件
FAIL_TO_PASS triage (partial pass) TEST_SYNTHSIZE, CODE_LOCALIZE, TEST_LOCALIZE, CODE_FIX, VERIFY_PATCH, ISSUE_CLOSE line_trace, caller_trace
List lines of code actually executed CODE_LOCALIZE, TEST_LOCALIZE line_trace
List functions that progressively call the given code (dynamic) CODE_LOCALIZE, TEST_LOCALIZE caller_trace
Expand fix candidates with co-edit history CODE_LOCALIZE, TEST_LOCALIZE coedit_localize
Edit a specific line reliably (no git apply) CODE_FIX line_edit

4. 評価と運用

4.1 工夫7: エージェント間相互テストに基づく候補選抜

候補生成(複数の生成 run)と最終提出候補の選抜(TTS)を分けることで、単発生成のぶれを吸収しました。多様な候補を作る段階、エージェント間相互テストで比較証拠を作る段階、失敗リスクを下げる段階を分離したことが、TTS@8 の改善に寄与しています。

候補数を増やすだけでは不十分です。候補が増えるほど、良い案も悪い案も混ざります。そこで、各 run が自分で作った FAIL_TO_PASS / PASS_TO_PASS テストを持ち寄り、別 run の修正パッチにも当ててインスタンス単位の比較表を作る「エージェント間相互テスト」層を置きました。ここで使う FAIL_TO_PASS / PASS_TO_PASS も、SWE-bench データセットの同名の正解データではなく、各 run のエージェントがその場で生成した検証テストです。単純な多数決ではなく、他 run が作った実行可能な比較証拠で修正パッチを比較できるため、候補の多様性をそのまま選抜精度へ変換しやすくなります。

内部成果物名ではこの比較表を xcheck と呼んでいますが、本記事では instance_test_tables を使ったエージェント間相互テストとして説明します。直近の TTS@8 では、この比較表を再集計する形で後段の選抜規則を明示化しており、最終的に採用したのは F2P=0.3P2P=0.7 の重み付き通過率(weighted pass-rate)と shortest_patch_raw の組み合わせです。この係数 0.3 / 0.7 は SWE-bench の dev split で見積もったものです。今回の 8 runs の再集計では F2P=0.20 から 0.33 まで同じスコアでした。同点首位を修正パッチの生文字数(raw patch length)で絞るのは、同じ説明力ならより短い記述を優先する記述長最小原理に基づいています。公開に使う数値は、SWE-bench 公式クラウド環境で sb-cli により計測した 374/500 = 74.8% です。既存手法に多い LLM を使った修正パッチの採点・比較よりも、各 run で生成したテストケースを相互に適用した結果を集計するこの規則の方が、今回の設定では安定して強かったのが実務上の重要な観察です。

具体例として、解決できたインスタンスの sympy__sympy-21612 では、8 本の生成 run のうち 5 本が有効な修正パッチを出していました。size_desc 順に並べた候補順は r08r04r02r07r05 です。この 5 候補に対し、各 run 由来の FAIL_TO_PASS 5 件と PASS_TO_PASS 5 件を当て、(0.3 * F2P_pass + 0.7 * P2P_pass) / 5 で重み付きスコアを計算します。

候補 FAIL_TO_PASS PASS_TO_PASS 重み付きスコア 修正パッチ長 選抜器の判定
r08 3/5 5/5 0.88 736 不採用
r04 3/5 5/5 0.88 683 不採用
r02 3/5 5/5 0.88 664 不採用
r07 4/5 5/5 0.94 577 同点首位
r05 4/5 5/5 0.94 553 採用

このインスタンスでは、エージェント間相互テストだけでまず 0.88 群と 0.94 群に分かれ、3 候補が落ちます。残る r07r05 は実行可能な比較証拠の上では同点なので、最後に shortest_patch_raw を適用して、より短い 553 文字の r05 を採用します。同点時の規則を入れず先着だけで決めていれば r07 が残るので、「相互テストで比較証拠を作って候補を絞り、その後に単純な二次規準で 1 本へ落とす」という処理をそのまま示している例です。

4.2 工夫8: 運用安定化(シャード分割 + 再試行)

500 件評価を安定完走させるため、シャード分割実行と欠損リトライを組み合わせています。長時間ベンチマークでの部分失敗を局所再実行で対処でき、集計の再現性を確保しました。

500 件規模の評価では、個々のケースよりも実行基盤の揺らぎが結果に影響することがあります。シャーディングとリトライは派手な要素ではありませんが、比較可能な数字を出すための前提条件でした。

実行系の入口には swebench_sota.py を使い、各インスタンスの前後でクリーンアップ・補足情報の収集・/_share の共有資産書き出しをフックとして差し込みます。FAIL_TO_PASS / PASS_TO_PASS テスト・trace ログ・引き継ぎメモ・Kanban などをインスタンスごとの成果物として回収できるため、後から軌跡や選抜規則の挙動を検査しやすくなりました。

4.3 評価設定

  • TTS@8 トラック(本記事で公開する主結果)
    • データセット: SWE-bench Verified (split=test)
    • 一括実行エントリ: minisweagent.run.extra.swebench_sota
    • 候補生成: Qwen3.5-27BOrchestra 構成による 8 runs
    • 選抜基盤: エージェント間相互テスト instance_test_tables
    • 選抜規則: SWE-bench の dev split で見積もった F2P=0.3, P2P=0.7simple weighted pass-rate、同点時は shortest_patch_raw
    • 公開値: SWE-bench 公式クラウド環境で sb-cli により計測した 374/500 = 74.8%

ローカルの eval.json やエージェント間相互テストの再集計は高速な内側の反復には便利ですが、本記事で公開する TTS@8 の数値は、投稿時の計測系に合わせて sb-cli 計測値だけを採用しています。

提出時には、予測ファイルだけでなく、metadata.yamlREADME.mdtrajs/logs/ などの成果物を SWE-bench/experiments の提出形式に合わせて整理します。本記事は、特に trajs/logs/ に残る挙動や、そこで観察できる設計意図を文章で補う位置づけです。

4.4 エージェント入出力の具体例

対象軌跡: Django の実行軌跡の一例

最後に、実際の軌跡を 1 件示します。エージェントは問題文を読み、必要なツールを呼び、最小限の修正を行い、自身がその場で生成した FAIL_TO_PASS / PASS_TO_PASS テストで結果を確かめています。ここでいう FAIL_TO_PASS / PASS_TO_PASS は SWE-bench の正解テスト名と同名ですが、本節では軌跡中でエージェントが生成した検証用テストを指します。

例1: 問題文入力(抜粋)

MultiValueField ignores a required value of a sub field.

Form is valid: True
Expected is_valid=False but got True.

Number of 'required' attributes in HTML: 0
Expected 1 required attribute but got 0.

例2: エージェントの中間思考(THOUGHT, 抜粋。原文は英語で、以下は日本語訳)

THOUGHT: この問題は 1 箇所のバリデーション漏れではなく、
MultiValueField.clean() と HTML 属性付与の両方を見ないと閉じません。
まず line_trace で空入力がどの分岐を通るかを確認し、
caller_trace で required 属性の描画経路を追います。
そのうえで、局所修正を line_edit で積み上げます。

この THOUGHT に続いて、エージェントは次のようなツールコマンドを生成します。

<tool: bash>
cd /testbed && TRACE_FILE_PATH="/testbed/django/forms/fields.py" python /_tools/line_trace.py /_share/test_FAIL_TO_PASS_1.py
</tool>

例3: ツール応答(line_trace / caller_trace / line_edit の実行結果)

line_trace:

<tool: bash>
cd /testbed && TRACE_FILE_PATH="/testbed/django/forms/fields.py" python /_tools/line_trace.py /_share/test_FAIL_TO_PASS_1.py
</tool>

[trace] /testbed/django/forms/fields.py:1024 | if not value or isinstance(value, (list, tuple)):
[trace] /testbed/django/forms/fields.py:1025 | if not value or not [v for v in value if v not in self.empty_values]:
[trace] /testbed/django/forms/fields.py:1026 | if self.required:
[trace] /testbed/django/forms/fields.py:1029 | return self.compress([])

caller_trace:

<tool: bash>
cd /testbed && python /_tools/caller_trace.py --repo /testbed --target /testbed/django/forms/boundfield.py:BoundField.build_widget_attrs -- /_share/test_FAIL_TO_PASS_1.py
</tool>

=== caller_trace output ===
Target function: /testbed/django/forms/boundfield.py:BoundField.build_widget_attrs
Target hits: 1
Unique transitive callers (<full_path>:<qualname>): 5
- /testbed/django/forms/boundfield.py:BoundField.as_widget [direct]
- /testbed/django/forms/forms.py:BaseForm._html_output
- /testbed/django/forms/forms.py:BaseForm.as_table

line_edit:

<tool: bash>
python /_tools/line_edit.py --file /testbed/django/forms/fields.py --line 1029 --expected "                    return self.compress([])" --text "                    ... raise ValidationError(...) ...\n                    return self.compress([])"
</tool>

Updated /testbed/django/forms/fields.py:1029
Old: '                    return self.compress([])'
New: '                    ... raise ValidationError(...) ...\n                    return self.compress([])'

例4: 修正前後の検証ログ(エージェント生成の FAIL_TO_PASS テスト)

修正前:

=== Test 1: Both sub fields are empty ===
Form is valid: True
FAIL: Expected is_valid=False but got True.

=== Test 4: HTML required attributes ===
Number of 'required' attributes in HTML: 0
FAIL: Expected 1 required attribute but got 0.

修正後:

FAIL_TO_PASS: 9/9 passed

例5: 回帰確認(エージェント生成の PASS_TO_PASS テスト)

PASS_TO_PASS: 145/145 passed

4.5 関連研究・既存手法との比較

CORTEXAAGENTLESSR2E-Gym はいずれも、単発の 1 本の修正に賭けるのではなく、局所化・複数候補・テストによる選抜を組み合わせる方向を強く打ち出しています。今回の構成も問題意識は近いですが、重心はやや異なります。追加学習した検索器や verifier に寄らず、ハーネス側の状態管理・道具立て・選抜規則で OSS LLM の性能を引き上げることを重視しました。

手法 主眼 本構成で対応する工夫 本構成の利点
Nemotron-CORTEXA 追加学習したコード埋め込みでファイル単位の局所化を改善し、リポジトリグラフ上の局所化エージェントでシンボル単位まで絞り込む。多様な文脈とプロンプト形式で修正候補を増やし、テストと LLM judge で最終選抜する。 line_trace / caller_trace / coedit_localize、フェーズ分割、エージェント間相互テスト 追加学習した検索器や専用グラフ基盤を前提にせず、実行時の trace・呼び出し経路・共編集履歴から局所化できる。最終選抜も LLM judge ではなく相互テストの実行結果を集約する単純な規則なので、採用理由を監査しやすい。
AGENTLESS 局所化・修正・パッチ検証の 3 段で、複雑な自律ループやツール利用を抑えつつ高コスト効率を狙う。issue ごとに最終的な再現テスト 1 本と回帰テスト集合 1 組を用意し、それらを全 patch に共通適用して最終候補を選ぶ。 フェーズ + ワークフロー分割、ファイルシステム上の共有資産、特化ツール、エージェント間相互テスト AGENTLESS の単純さは強い基準だが、こちらは長い探索をフェーズ・ワークフロー・引き継ぎで明示的に管理できる。再現スクリプト・trace・テスト・handover memo をファイルシステムへ残すため、多段の調査と修正を途中で失いにくい。また各 run が自前で作ったテストを他 run の修正候補へ相互適用するため、issue ごとの共通テスト集合で patch を絞るだけでなく、run 間比較まで行える。
R2E-Gym の verifier 実行ベース verifier と実行不要 verifier の長所短所を分析し、hybrid verifier で BEST@K を伸ばす。 エージェント間相互テスト、ファイルシステム上に保存されたテスト資産、simple weighted pass-rate + shortest_patch_raw 学習済み verifier や軌跡依存の judge を追加せず、各 run が残した実行可能テストを相互適用して比較する。採点の根拠が軌跡テキストではなく run 間の FAIL_TO_PASS / PASS_TO_PASS の通過結果に置かれるため、後から追いやすく、verifier 学習や追加推論系への依存も小さい。

まとめると、CORTEXA が「学習した局所化器と多様な修正候補生成」、AGENTLESS が「単純で強い 3 段パイプライン」、R2E-Gym が「verifier と test-time scaling」を主軸に置くのに対し、今回の構成は「フェーズ / ワークフロー管理・共有作業領域・特化ツール・エージェント間相互テスト」というハーネス設計を主軸に置いています。追加学習に頼らず OSS LLM を押し上げたいという今回の立ち位置に、最も素直に対応している設計だと考えています。

AGENTLESS との差はテスト資産の持ち方に出ます。AGENTLESS では issue ごとに 1 本の再現テストと 1 組の回帰テストを選び、それを全 patch に共通適用します。一方、今回の方式では各 run がそれぞれ FAIL_TO_PASS / PASS_TO_PASS を生成し、それらを他 run の patch に相互適用します。patch の良し悪しを「1 つの共通テスト集合に通るか」だけでなく「別の run が持っている観点にもどれだけ耐えるか」で比較できる点が特徴です。

4.6 限界

今回の公開値 TTS@8 = 74.8% は、8 本の候補生成 run と後段の選抜器を含むシステム全体の値です。単発の pass@1 や 1 本の軌跡だけの強さとは切り分けて読む必要があります。候補の多様性が十分に出なければ選抜器の上限も下がりますし、逆に候補が多くても比較証拠が弱ければ最終提出の精度は伸びません。

また、SWE-bench Verified は優れた実践的ベンチマークですが、企業内のソフトウェア開発をそのまま代表しているわけではありません。実運用では、複数リポジトリの依存・アクセス権限・長時間の CI・レビュー運用・ロールバック手順・監査証跡の保存といった要件が加わります。ベンチマークで強い構成が業務でも最適とは限りません。

軌跡解析についても、公開値そのものとは役割が異なります。公開値は sb-cli による公式計測値ですが、採用軌跡のターン分布やフェーズ別滞在時間は内部 artifact を用いた補助分析です。何が効いたかを理解する材料としては有用でも、それ自体を最終的な性能指標とみなすべきではありません。

4.7 残課題

技術的にはここからさらに広げられる論点が大きく 3 つあります。

1 つ目は、ハーネス改善とモデル訓練の連動です。今回わかったのは、ベースモデルをそのまま使ってもハーネス側の設計で大きく伸ばせるという点でした。その先には、フェーズ失敗・引き継ぎ・tool-specialist の補正・相互テストの勝敗・選抜器の結果といった成果物を訓練データ側へ返し、そのモデルの癖に合わせてワークフロー・プロンプト・検証ゲートを調整していく、という双方向の最適化があります。モデル改善とハーネス改善を一体で回す余地はまだ大きいと見ています。

2 つ目は、ハーネス改善のさらなる自動化です。現在も分析や設定更新の一部は半自動化されていますが、ワークフロー定義・specialist prompt・再試行閾値・温度・選抜器の係数調整まで含めると、自動化の余地はまだ広くあります。失敗パターンと設定変更の対応を蓄積しながら、設定候補の生成・アブレーション・回帰検知・再評価までをより自動で回せるようにすると、ハーネスエンジニアリング自体の改善速度も上がります。

3 つ目は、比較証拠と選抜規則の共同最適化です。今回の選抜器は単純な規則で強く、監査もしやすい一方、その性能は各 run が生成する FAIL_TO_PASS / PASS_TO_PASS テストの質と強く結び付いています。生成テストの網羅性・冗長性・区別性をどう定量化し、それを選抜器側の重み付けや同点解消とどう結び付けるかは、今後掘り下げがいのある論点です。実行可能な比較証拠をどこまで豊かにできるかという観点で、伸ばしやすい領域だと考えています。

5. 最後に

今回の結果で強調したかったのは、「モデルをさらに大きくすること」や「追加学習を重ねること」だけが性能改善の道ではない、という点です。どのフェーズで何をさせるか、途中成果をどう残すか、複数候補をどう比較するか——こうしたハーネス設計そのものが、最終的なスコアと品質を大きく左右します。

そしてこれは、将来こうした仕組みを作りたいという話ではありません。富士通社内では、こうした開発エージェントを既に利用しています。本記事は、その中で実際に効いていた設計や運用のうち、公開可能な部分をまとめたものです。

ハーネスエンジニアリングは研究テーマであると同時に、事業やプロダクトの現場に直結する領域です。企業が実際に必要とするのは、単に 1 本の修正を返すモデルではなく、途中経過を残し、失敗時に原因を追え、既存の開発プロセスへ接続できるシステムです。フェーズ / ワークフロー管理・ファイルシステム上の共有資産・特化ツール・エージェント間相互テストといった部品は、そのまま業務導入時の要件と接続しやすい形になっています。

OSS LLM やローカル LLM を前提にすると、コスト・データ持ち出し制約・レイテンシ・モデル差し替えの自由度の面で選択肢が広がります。モデル側の進歩を取り込みつつ、ハーネス側の改善で運用性と性能を押し上げられるなら、その価値はベンチマークのスコアにとどまりません。本記事が、モデルそのものだけでなく、それを載せる実行基盤まで含めて設計する重要性を考える材料になれば幸いです。

参考文献

  1. Carlos E. Jimenez, John Yang, Alexander Wettig, Shunyu Yao, Kexin Pei, Ofir Press, Karthik R. Narasimhan, SWE-bench: Can Language Models Resolve Real-world Github Issues?, The Twelfth International Conference on Learning Representations, 2024.
  2. OpenAI, Introducing SWE-bench Verified, OpenAI, published 2024-08-13, updated 2025-02-24.
  3. SWE-bench, Overview - sb-cli, SWE-bench Documentation, accessed 2026-04-06.
  4. Qwen, Qwen3.5-27B, Hugging Face model card, accessed 2026-04-06.
  5. Mistral AI, Devstral Small 2 24B Instruct 2512, Hugging Face model card, accessed 2026-04-06.
  6. Atefeh Sohrabizadeh, Jialin Song, Mingjie Liu, Rajarshi Roy, Chankyu Lee, Jonathan Raiman, Bryan Catanzaro, Nemotron-CORTEXA: Enhancing LLM Agents for Software Engineering Tasks via Improved Localization and Solution Diversity, Proceedings of the 42nd International Conference on Machine Learning, 2025.
  7. Chunqiu Steven Xia, Yinlin Deng, Soren Dunn, Lingming Zhang, Agentless: Demystifying LLM-based Software Engineering Agents, arXiv:2407.01489, 2024.
  8. Naman Jain, Jaskirat Singh, Manish Shetty, Tianjun Zhang, Liang Zheng, Koushik Sen, Ion Stoica, R2E-Gym: Procedural Environments and Hybrid Verifiers for Scaling Open-Weights SWE Agents, DL4C @ NeurIPS 2025 Poster, 2025.

*1:2026年4月7日時点

*2:本記事の主たる執筆者

*3:バージョン1.17.2から派生

*4:Test-Time Scaling。8本の候補生成 run を走らせて、後段の選抜器で最終提出パッチを 1 本に絞る測定方式

*5:2026年4月7日時点