Home 論文解説: MASAI — モジュラーアーキテクチャによるソフトウェアエンジニアリングAIエージェント
投稿
キャンセル

📄 論文解説: MASAI — モジュラーアーキテクチャによるソフトウェアエンジニアリングAIエージェント

論文概要(Abstract)

MASAI(Modular Architecture for Software-engineering AI agents)は、GitHub Issueの自動解決タスクを5つの専門サブエージェントに分解することで、SWE-bench Liteで28.33%(85/300件解決)を達成したシステムである。2024年6月時点でSWE-benchリーダーボードの最高性能を記録した。従来の単一ReActエージェント方式(約18%)と比較して、タスク分解と専門化による10%以上の絶対改善を実現している。各サブエージェントが独立した戦略・ツールセット・トークン予算を持ち、構造化された出力で連携する設計は、Zenn記事のGemini 3.1 Proマルチエージェント構成の理論的基盤となる。

この記事は Zenn記事: Gemini 3.1 Proで構築するマルチエージェント協調コーディングの実践手法 の深掘りです。

情報源

  • arXiv ID: 2406.14928
  • URL: https://arxiv.org/abs/2406.14928
  • 著者: Daman Arora, Atharv Sonwane, Nalin Wadhwa, Abhav Mehrotra, Saiteja Utpala, Ramakrishna Bairi, Aditya Kanade, Nagarajan Natarajan(Microsoft Research India / IIT Bombay)
  • 発表年: 2024年
  • 分野: cs.SE, cs.AI

背景と動機(Background & Motivation)

GitHub Issueの自動解決は、コードベースの理解、バグの再現、修正パッチの生成、テストによる検証という複数のステップを必要とする複雑なタスクである。SWE-bench(Jimenez et al., 2023)はこのタスクの標準ベンチマークとして、12のPythonリポジトリから2,294件の実Issue-パッチペアを収集している。

従来のアプローチは大きく2つに分類される:

  1. 単一エージェント方式(SWE-agent等): 1つのReActエージェントが全サブタスクを担当。シンプルだが、異なるサブタスクに最適な戦略を使い分けられない
  2. 固定パイプライン方式(Agentless等): エージェントを使わず、固定のパイプラインで処理。柔軟性に欠ける

MASAIの着眼点は、SEタスクのサブタスクごとに最適な戦略が異なるという観察にある。例えば、バグの再現にはコード実行が必要だが、修正箇所の特定にはAST解析が有効であり、パッチ生成には精密なコード編集能力が求められる。これらを1つのエージェントに任せるのは非効率である。

主要な貢献(Key Contributions)

  • 5サブエージェントのモジュラー設計: SEタスクを5つの明確なサブタスクに分解し、各々に専門エージェントを割り当て
  • Issue Reproducer Agentの導入: バグ再現テストを自動生成する専用エージェントにより、修正の検証品質を大幅向上
  • 階層的コードローカライゼーション: ファイル→クラス/関数→行の3段階で修正箇所を絞り込むEdit Localizer設計
  • 体系的なアブレーション研究: 各設計選択の貢献度を定量的に分析し、モジュラー設計の有効性を実証

技術的詳細(Technical Details)

5つのサブエージェントアーキテクチャ

MASAIは以下の5つの専門サブエージェントで構成される:

1. Project Setup Agent

目的: プロジェクトの実行環境を正しく構築する

戦略: ReActアプローチ(シェルアクセス付き)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ProjectSetupAgent:
    """プロジェクト環境を構築するエージェント

    README・setup.pyを読み、conda環境の作成、
    依存関係のインストールを自動実行する
    """

    TOKEN_BUDGET = 20_000

    async def setup(self, repo_path: str) -> bool:
        """環境構築を実行

        Args:
            repo_path: リポジトリのパス

        Returns:
            成功フラグ
        """
        # 1. README・setup.pyを読み込み
        setup_info = await self._read_setup_files(repo_path)

        # 2. Pythonバージョンを判定
        python_version = self._detect_python_version(setup_info)

        # 3. conda環境を作成
        await self._create_conda_env(python_version)

        # 4. 依存関係をインストール
        await self._install_deps(repo_path)

        # 5. importテストで検証
        return await self._verify_import(repo_path)

重要性: 環境構築の失敗は後続の全エージェントに波及するカスケード障害を引き起こすため、最優先で実行される。

2. Issue Reproducer Agent

目的: Issueで報告されたバグを再現するテストスクリプトを生成する

戦略: ReActアプローチ + コード生成 + 反復実行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class IssueReproducerAgent:
    """バグ再現テストを生成するエージェント

    Issue記述から再現スクリプトを生成し、
    実行によって再現を確認する
    """

    TOKEN_BUDGET = 40_000
    SUCCESS_RATE = 0.60  # 約60%のIssueで再現に成功

    async def reproduce(
        self,
        issue_text: str,
        repo_path: str,
    ) -> tuple[bool, str]:
        """バグ再現テストを生成・実行

        Args:
            issue_text: Issueの本文
            repo_path: リポジトリのパス

        Returns:
            (再現成功フラグ, テストスクリプトのパス)
        """
        for attempt in range(5):
            # テストスクリプトを生成
            script = await self._generate_test_script(
                issue_text, repo_path
            )

            # 実行して再現を確認
            result = await self._run_script(script, repo_path)

            if result.exit_code != 0:  # 失敗=バグ再現成功
                return True, script.path

        return False, ""

MASAIの最大の差別化要素: Issue Reproducer Agentの存在により、Fixer Agentは「再現テストが通るかどうか」という明確な成功基準でパッチの正しさを検証できる。アブレーション研究では、Issue Reproducer Agentなしの場合24.67%(-3.66%)に低下することが確認されている。

3. Edit Localizer Agent

目的: 修正が必要なファイルと行範囲を特定する

戦略: 3段階の階層的コード検索

\[\text{Search}_{hierarchical} = \text{File} \xrightarrow{\text{AST}} \text{Class/Function} \xrightarrow{\text{Line}} \text{Edit Location}\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class EditLocalizerAgent:
    """修正箇所を特定するエージェント

    3段階の階層的検索で、大規模コードベースから
    修正が必要な具体的な行範囲を特定する
    """

    TOKEN_BUDGET = 60_000  # 最大(コード探索が最も情報量を要する)

    async def localize(
        self,
        issue_text: str,
        reproduction_output: str,
        repo_path: str,
    ) -> list[EditLocation]:
        """修正箇所を特定

        Args:
            issue_text: Issueの本文
            reproduction_output: 再現テストの出力
            repo_path: リポジトリのパス

        Returns:
            修正箇所のリスト
        """
        # Level 1: ファイルレベル検索
        candidate_files = await self._file_search(
            issue_text, repo_path
        )

        # Level 2: クラス/関数レベル検索(AST)
        candidate_functions = await self._ast_search(
            candidate_files, repo_path
        )

        # Level 3: 行レベル特定
        edit_locations = await self._line_search(
            candidate_functions, reproduction_output
        )

        return edit_locations

AST(Abstract Syntax Tree)ツール:

Edit Localizer Agentは以下のAST系ツールを使用する:

ツール機能
get_class_body(file, class_name)クラスの本体コードを取得
get_function_body(file, func_name)関数の本体コードを取得
list_classes(file)ファイル内のクラス一覧を取得
list_functions(file)ファイル内の関数一覧を取得

これらのツールにより、LLMが大量のコードを読む必要がなく、セマンティックな単位でコードを把握できる。

4. Fixer Agent

目的: バグを修正するパッチを生成する

戦略: ローカライズ情報と再現テストを活用した反復修正

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class FixerAgent:
    """パッチを生成するエージェント

    Edit Localizerが特定した箇所を修正し、
    Issue Reproducerのテストで検証する
    """

    TOKEN_BUDGET = 40_000

    async def fix(
        self,
        issue_text: str,
        edit_locations: list[EditLocation],
        reproduction_script: str | None,
        repo_path: str,
    ) -> str:
        """修正パッチを生成

        Args:
            issue_text: Issueの本文
            edit_locations: 修正箇所リスト
            reproduction_script: 再現テスト(あれば)
            repo_path: リポジトリのパス

        Returns:
            unified diff形式のパッチ
        """
        for attempt in range(3):
            # パッチを生成
            patch = await self._generate_patch(
                issue_text, edit_locations, repo_path
            )

            # パッチを適用
            await self._apply_patch(patch, repo_path)

            # 再現テストで検証
            if reproduction_script:
                result = await self._run_test(
                    reproduction_script, repo_path
                )
                if result.exit_code == 0:  # 成功=バグ修正完了
                    return patch

            else:
                # 再現テストがない場合はパッチを返す
                return patch

        return ""  # 修正失敗

5. Test Isolator Agent

目的: 修正に関連する既存テストを特定し、バリデーションに使用する

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class TestIsolatorAgent:
    """関連テストを特定するエージェント

    パッチの変更内容を解析し、関連するテスト関数を
    特定して実行する
    """

    TOKEN_BUDGET = 20_000

    async def isolate_tests(
        self,
        patch: str,
        repo_path: str,
    ) -> list[str]:
        """関連テストを特定

        Args:
            patch: unified diff形式のパッチ
            repo_path: リポジトリのパス

        Returns:
            テスト関数のリスト
        """
        # パッチから変更モジュールを抽出
        changed_modules = self._extract_modules(patch)

        # テストファイルで変更モジュールをimportしているものを検索
        test_files = await self._find_test_files(
            changed_modules, repo_path
        )

        # 関連テスト関数を特定
        test_functions = await self._find_test_functions(
            test_files, repo_path
        )

        return test_functions

実行パイプライン

5つのサブエージェントは以下の順序で実行される:

1
2
3
4
5
6
7
8
9
10
11
Project Setup Agent ─────────────────────────────────> 完了
        │
        ├── Issue Reproducer Agent ──> 再現テスト
        │                              │
        ├── Edit Localizer Agent ──> 修正箇所リスト
        │                              │
        │         ┌────────────────────┘
        │         │
        └── Fixer Agent ──> パッチ
                    │
                    └── Test Isolator Agent ──> テスト結果

Issue Reproducer AgentとEdit Localizer Agentは並列実行が可能である。両者の出力がFixer Agentの入力となる。

トークン予算管理

各サブエージェントに独立したトークン予算を設定:

サブエージェントトークン予算理由
Project Setup20,000環境構築は定型的
Issue Reproducer40,000コード生成+反復実行
Edit Localizer60,000コードベース探索が最も情報量を要する
Fixer40,000パッチ生成+検証
Test Isolator20,000テスト検索は限定的

合計: 約180,000トークン/Issue(実測では約200,000トークン)

\[\text{Cost}_{per\_issue} = \frac{200{,}000 \times \text{Price}_{GPT4o}}{1{,}000{,}000} \approx \$2\text{-}\$3\]

実装のポイント(Implementation)

構造化出力によるエージェント間通信

各サブエージェントの出力はJSONまたはテンプレート形式で構造化されている:

1
2
3
4
5
6
7
8
9
# Edit Localizer → Fixer への出力例
@dataclass
class EditLocation:
    """修正箇所の構造化表現"""
    file_path: str       # 修正対象ファイル
    start_line: int      # 開始行(1-indexed)
    end_line: int        # 終了行(1-indexed、inclusive)
    description: str     # 修正内容の説明
    code_context: str    # 周辺コード(参考用)

この構造化により、下流エージェントが上流の出力を確実にパースでき、情報の欠落を防いでいる。

ファイル読み込みツールの行番号表示

Edit Localizer Agentのファイル読み込みツールは必ず行番号を表示する:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def read_file_with_line_numbers(
    file_path: str,
    start: int = 1,
    end: int | None = None,
) -> str:
    """行番号付きでファイルを読み込む

    Args:
        file_path: ファイルパス
        start: 開始行
        end: 終了行(Noneで末尾まで)

    Returns:
        行番号付きのファイル内容
    """
    with open(file_path) as f:
        lines = f.readlines()

    if end is None:
        end = len(lines)

    result = []
    for i, line in enumerate(lines[start-1:end], start=start):
        result.append(f"{i:4d} | {line.rstrip()}")

    return "\n".join(result)

行番号表示により、LLMが「142行目から155行目を修正する」という精密な指示を生成でき、ローカライゼーションの精度が向上する。

Gemini 3.1 Proとの対応

MASAIの設計はZenn記事の構成と以下のように対応する:

MASAIのサブエージェントZenn記事の対応thinking_level
Project Setup Agent— (環境構築はADK外)
Issue Reproducer AgentPlanner層の一部high
Edit Localizer AgentPlanner層の一部high
Fixer AgentCoder層medium
Test Isolator AgentReviewer層の一部medium

MASAIの5エージェント構成をADKで実装する場合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from google.adk.agents import SequentialAgent, ParallelAgent, LlmAgent

# MASAI on ADK
masai_pipeline = SequentialAgent(
    name="masai",
    sub_agents=[
        LlmAgent(name="setup", model="gemini-3.1-pro-preview",
                 instruction="環境を構築..."),
        ParallelAgent(
            name="analysis",
            sub_agents=[
                LlmAgent(name="reproducer",
                         model="gemini-3.1-pro-preview",
                         instruction="バグを再現..."),
                LlmAgent(name="localizer",
                         model="gemini-3.1-pro-preview",
                         instruction="修正箇所を特定..."),
            ],
        ),
        LlmAgent(name="fixer", model="gemini-3.1-pro-preview",
                 instruction="パッチを生成..."),
        LlmAgent(name="test_isolator",
                 model="gemini-3.1-pro-preview",
                 instruction="関連テストを実行..."),
    ],
)

Production Deployment Guide

AWS実装パターン(コスト最適化重視)

規模月間リクエスト推奨構成月額コスト主要サービス
Small~3,000 (100/日)Serverless$100-300Step Functions + Lambda + Bedrock
Medium~30,000 (1,000/日)Hybrid$600-1,500ECS Fargate + Step Functions + Bedrock
Large300,000+ (10,000/日)Container$4,000-10,000EKS + Step Functions + Bedrock

Small構成の詳細 (月額$100-300):

  • Step Functions: 5エージェントのオーケストレーション(並列分岐含む) ($20/月)
  • Lambda × 5: 各サブエージェント用 ($30/月)
  • Bedrock: Claude 3.5 Sonnet (Localizer/Fixer) + Haiku (Setup/Isolator) ($200/月)
  • S3: パッチ・テスト結果保存 ($5/月)
  • CloudWatch: 監視 ($5/月)

コスト削減テクニック:

  • サブエージェント別モデル選択: Setup/Isolator=Haiku($0.25/MTok), Reproducer/Fixer=Sonnet($3/MTok), Localizer=Sonnet
  • トークン予算の厳守: 各サブエージェントのmax_tokensを設定し過剰消費防止
  • 並列実行: Issue ReproducerとEdit Localizerの並列実行で壁時計時間50%削減
  • Bedrock Batch API: 非リアルタイムのバグ修正で50%割引

コスト試算の注意事項:

  • 上記は2026年2月時点のAWS ap-northeast-1(東京)リージョン料金に基づく概算値です
  • Issue1件あたりの実コストは約$2-3(GPT-4o相当モデル使用時)
  • 最新料金は AWS料金計算ツール で確認してください

Terraformインフラコード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# --- Step Functions: MASAIパイプライン ---
resource "aws_sfn_state_machine" "masai" {
  name     = "masai-pipeline"
  role_arn = aws_iam_role.sfn_role.arn

  definition = jsonencode({
    StartAt = "ProjectSetup"
    States = {
      ProjectSetup = {
        Type     = "Task"
        Resource = aws_lambda_function.setup_agent.arn
        Next     = "ParallelAnalysis"
      }
      ParallelAnalysis = {
        Type = "Parallel"
        Branches = [
          {
            StartAt = "IssueReproducer"
            States = {
              IssueReproducer = {
                Type     = "Task"
                Resource = aws_lambda_function.reproducer_agent.arn
                End      = true
              }
            }
          },
          {
            StartAt = "EditLocalizer"
            States = {
              EditLocalizer = {
                Type     = "Task"
                Resource = aws_lambda_function.localizer_agent.arn
                End      = true
              }
            }
          }
        ]
        Next = "Fixer"
      }
      Fixer = {
        Type     = "Task"
        Resource = aws_lambda_function.fixer_agent.arn
        Next     = "TestIsolator"
      }
      TestIsolator = {
        Type     = "Task"
        Resource = aws_lambda_function.test_isolator_agent.arn
        End      = true
      }
    }
  })
}

# --- Lambda: 各サブエージェント ---
locals {
  agents = {
    setup      = { memory = 512,  timeout = 120, model = "haiku" }
    reproducer = { memory = 1024, timeout = 300, model = "sonnet" }
    localizer  = { memory = 1024, timeout = 300, model = "sonnet" }
    fixer      = { memory = 1024, timeout = 300, model = "sonnet" }
    test_isolator = { memory = 512, timeout = 120, model = "haiku" }
  }
}

resource "aws_lambda_function" "agents" {
  for_each      = local.agents
  filename      = "${each.key}_agent.zip"
  function_name = "masai-${each.key}"
  role          = aws_iam_role.lambda_role.arn
  handler       = "main.handler"
  runtime       = "python3.12"
  timeout       = each.value.timeout
  memory_size   = each.value.memory

  environment {
    variables = {
      BEDROCK_MODEL = each.value.model == "sonnet" ? "anthropic.claude-3-5-sonnet-20241022-v2:0" : "anthropic.claude-3-5-haiku-20241022-v1:0"
      TOKEN_BUDGET  = each.key == "localizer" ? "60000" : each.key == "setup" || each.key == "test_isolator" ? "20000" : "40000"
    }
  }
}

運用・監視設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import boto3

cloudwatch = boto3.client('cloudwatch')

# サブエージェント別の成功率監視
for agent_name in ['setup', 'reproducer', 'localizer', 'fixer', 'test_isolator']:
    cloudwatch.put_metric_alarm(
        AlarmName=f'masai-{agent_name}-failure-rate',
        ComparisonOperator='GreaterThanThreshold',
        EvaluationPeriods=1,
        MetricName=f'{agent_name}_failure_rate',
        Namespace='Custom/MASAI',
        Period=3600,
        Statistic='Average',
        Threshold=0.5,
        AlarmDescription=f'MASAI {agent_name}の失敗率が50%を超過'
    )

コスト最適化チェックリスト

  • サブエージェント別モデル選択: Setup/Isolator=Haiku, Others=Sonnet
  • トークン予算: 各エージェントにmax_tokens設定
  • 並列実行: Reproducer + Localizer の並列化
  • Bedrock Batch API: 非リアルタイム処理で50%割引
  • Step Functions Express: 短時間ワークフロー最適化
  • Lambda メモリ最適化: Setup/Isolator=512MB, Others=1024MB
  • S3 ライフサイクル: パッチ・テスト結果の自動削除(7日)
  • CloudWatch Logs: 保持期間14日
  • AWS Budgets: 月額予算アラート
  • Issue Reproducer成功率の監視: 60%未満で調査

実験結果(Results)

SWE-bench Lite(300タスク)メイン結果

システム解決率差分 vs MASAI
MASAI28.33% (85/300)ベースライン
Agentless~24%-4.33%
AutoCodeRover~19%-9.33%
SWE-agent12.47%-15.86%
単一ReActエージェント~18%-10.33%

アブレーション研究

設定解決率差分
Full MASAI28.33%ベースライン
Issue Reproducer Agentなし24.67%-3.66%
Test Isolator Agentなし27.33%-1.00%
単一ReActエージェント~18%-10.33%

Issue Reproducer Agentの効果が最も大きい。再現テストの存在により、Fixer Agentの修正精度が大幅に向上する。

リポジトリ別成績

リポジトリ解決率特徴
sympy41.5% (27/65)数学ライブラリ、テストが明確
scikit-learn35.3% (12/34)ML系、API変更が多い
astropy27.3% (6/22)天文学系、複雑な依存関係
django12.3% (14/114)大規模フレームワーク、最も困難

Djangoの解決率が最も低い(12.3%)ことは、大規模・複雑なフレームワークにおけるマルチエージェントの限界を示唆している。

実運用への応用(Practical Applications)

MASAIの設計原則は、Zenn記事のGemini 3.1 Pro構成に以下のように応用できる:

  1. サブエージェント分業の粒度: MASAIの5エージェント構成は、ADKのSequentialAgent + ParallelAgentで直接実装可能。thinking_level制御により、探索的タスク(Localizer)にはhigh、定型的タスク(Setup, Isolator)にはlowを割り当て

  2. Issue Reproducer パターンの汎用化: バグ修正に限らず、「成功基準を自動生成するエージェント」として汎用化できる。コード生成タスクでも、まずテストを生成してからコードを生成するTDD的アプローチが有効

  3. トークン予算管理: MASAIの予算配分(Localizer=60K, 他=20-40K)は、Gemini 3.1 Proのthinking_levelとの組み合わせで実装可能。thinking_level=highは多くのthinkingトークンを消費するため、Localizerのみhigh、他はmedium/lowとすることでコスト最適化

関連研究(Related Work)

  • SWE-agent (Yang et al., 2024): 単一エージェント+ACI設計。MASAIはACIの概念を各サブエージェントのツールセットとして取り入れつつ、モジュラー設計で性能を上回った
  • Agentless (Xia et al., 2024): エージェントなしの固定パイプライン。MASAIは各ステップをエージェント化することで、動的な探索と反復修正を実現
  • MetaGPT (Hong et al., 2023): SOP駆動のマルチエージェント。MASAIはSE特化のモジュラー設計で、MetaGPTの汎用設計とは異なるアプローチ
  • OpenHands (Wang et al., 2024): CodeAct設計のオープンプラットフォーム。MASAIの設計原則をOpenHandsに統合する研究が進行中

まとめと今後の展望

MASAIの最大の貢献は、SEタスクの分解粒度と各サブエージェントの専門化が性能に大きな影響を与えることを実証した点にある。単一エージェント(~18%)からモジュラー5エージェント(28.33%)への改善は、タスク分解の重要性を示している。

今後の方向性:

  • マルチLLMアーキテクチャ: 各サブエージェントに最適なLLMを選択(例:LocalizerにはGemini 3.1 Pro、FixerにはClaude 3.5 Sonnet)
  • バグ再現テスト生成の改善: 現在60%の成功率を80%以上に向上
  • 多言語対応: 現在Pythonのみ。TypeScript、Java、Go等への拡張
  • 複数ファイル変更の改善: 大規模リファクタリングへの対応

参考文献

この投稿は CC BY 4.0 でライセンスされています。

論文解説: RouteLLM — 人間選好データによるLLMコスト最適化ルーティング

論文解説: SRAG — マルチホップ質問応答のための構造化RAG