スパイダープラス Tech Blog

建設SaaS「スパイダープラス」のエンジニアとデザイナーのブログ

マルチエージェント・オーケストレーションを SDLC で試す — 一番高いモデルが必ずしも最良ではない

こんにちは。プロダクト品質部の三好です。

この記事は、PoC の実装・検証から本稿の執筆まで、Claude と三好の協働により作成しています。要所要所の数字の確認や現象の評価は人の手で行っていますが、語り口や言い回しは Claude が大きく担っているため、表現の細部には脚色が残っているかもしれません。

ソフトウェア開発の各工程は、AI に任せられる範囲が広がってきました。 Claude の Skill を使えば、要求分析・実装・テストといった工程を個別に AI へ委ねられます。 さらにいまは、個々の工程を任せるだけでなく、複数のエージェントを束ねて自律的に動かす方向へ関心が移りつつあります。 オープンソースの OpenClaw は本稿執筆時点で GitHub の Star が 38 万を超え、それをベースにした Microsoft の Scout のような製品も登場しています。

ただし、話題の派手さと、実務で使えるかは別の問題です。 加えて、利用が広がるほど、AI のコスト最適化も事業の課題になります。

そこで、二つの問いを立てました。 第一に、要求分析からリリース判定までの各工程を、人手を介さず一本につなぐオーケストレーションが成り立つか。 第二に、工程ごとに品質とコストの釣り合うモデルをどう選ぶか。

具体的には、Microsoft の AI エージェント開発フレームワーク Microsoft Agent Framework を使い、各工程を担うエージェントを、V 字モデルの前進と手戻りを表すワークフロー・グラフに組みました。 このグラフを実行して Web 関数電卓を開発し、題材とグラフは固定したまま、各工程のモデルだけを Haiku・Sonnet・Opus に入れ替えて比較しました。

結論を先に述べます。

第一に、オーケストレーションは成立しました。手戻りも含めて、最後のリリース判定まで自動で走り切ります。ただし品質を支えたのはモデルの賢さではなく、ツールの実測で合否を判定する品質ゲートと、失敗から復帰させる設計でした。

第二に、モデル選定では、最上位の Opus が品質で中位の Sonnet に及ばず、コストで約 1.8 倍、所要時間で約 1.5 倍を要しました。性能と価格の序列が、必ずしも成果の優劣に対応しないという結果です。

このあと、まず題材を紹介します。続く本題は 2 部構成で、前半(Part 1)がオーケストレーションの設計と構築、後半(Part 2)がそのグラフ上でのモデル選定です。

1. 題材:何を作らせたのか

工程を端から端まで動かして検証するには、作り切れる程度に小さく、かつテストのしがいがある題材が必要です。選んだのは Web 関数電卓です。要件定義書も人手ではなく、要件定義を担うエージェントが生成しました。

PoC で生成された Web 関数電卓の画面

PoC が実際に生成した Web 関数電卓の画面。DEG/RAD 切替、メモリ(M+ / MR / MC)、三角関数・対数・平方根・べき乗などをボタンで操作します。

1.1 アプリの要件

機能・非機能あわせて十数件の要件に REQ-xxx という ID を付与しています。要点は次のとおりです。

  • 計算: 四則演算と優先順位・括弧、三角関数 sin/cos/tan の DEG・RAD 切替、対数 log/ln、平方根(√16=4)・べき乗。
  • 状態: メモリ M+ / MR / MC、クリア C / AC、1 文字削除。
  • UI: ボタン入力で式を組み立て、逐次表示。
  • エラー: ゼロ除算・定義域外(√ 負数・log 非正数)・構文エラーを区別して表示。
  • 非機能: 正確性(許容誤差)・応答性(1 秒以内)・堅牢性(異常入力でも落ちない)・セキュリティ・使いやすさ・主要ブラウザ互換。セキュリティは、Python の eval を使わず許可したトークンだけを評価し、任意コード実行を防ぐことを指します。

この REQ-ID が、後述する「機能テスト=要件 ID トレーサビリティ 100%」ゲートの突合キーになります。

1.2 アーキテクチャ概要

システム構成はシンプルな 2 層です。

flowchart LR
    UI["ブラウザ UI<br/>(HTML / JS ボタン)"] -->|"POST /api/calculate<br/>{expr, degrees}"| API["Flask ルート層<br/>I/O とエラーマッピングのみ"]
    API -->|"evaluate(expr)"| ENG["計算エンジン calculator.py<br/>字句解析 → 構文解析(構文木) → 評価"]
    ENG -->|"結果 / エラー型"| API
    API -->|"JSON"| UI

アプリのシステム構成。計算ロジックを Flask ルートから切り離し calculator.py に集約した 2 層。

計算ロジックは Flask のルートから切り離し、calculator.evaluate() に集約しました。エンジンは、式をトークンに分解する「字句解析」、トークンを演算の優先順位どおりの構文木に組み立てる「構文解析」、構文木をたどって計算する「評価」の 3 段で処理します。外部の数式ライブラリや eval に頼らず標準ライブラリのみで実装し、許可したトークン以外を弾くことで、任意コード実行を防ぐセキュリティ要件を構造的に満たしています。

Part 1:オーケストレーションの設計と構築

この Part では、開発から QA までの工程を 1 本のワークフロー・グラフにまとめ、品質を AI の自己申告ではなくツールで判定する仕組みを構築します。

2. ワークフロー・グラフの全体構成

Agent Framework には直列・並列・ハンドオフなどの組み込みパターンがありますが、これらは前進を前提とするため、「テストで落ちたら実装に戻す」という手戻りを表現できません。V 字モデルの手戻りは工程をまたぐ循環です。そこで今回は、ノードとエッジを自由に定義でき、前進・分岐・循環・並列をすべて 1 つのワークフローで表現できるグラフ型の WorkflowBuilder を採用しました。

構築にあたって実施したのは、以下の 6 項目です。

  1. 各 SDLC 工程を「エージェント実行ノード(Executor)」として定義する。要求分析からリリース判定までを 1 ノードずつ用意する。
  2. 工程間で受け渡す状態(State)を設計する。各工程の成果物・差し戻し理由・手戻り回数・工程別のモデルレベルを 1 つの State に保持し、ノード間を流す。
  3. ゲートを条件分岐エッジ(switch-case)で配線する。合格なら次工程へ、不合格なら根本工程へ戻すバックエッジを張る。
  4. システムテストを fan-out / fan-in で並列化する。fan-out は 1 つの工程から複数工程への分岐、fan-in は複数結果の集約であり、いずれも Agent Framework の用語(API も add_fan_out_edges / add_fan_in_edges)。これにより機能・シナリオ・セキュリティを並列実行し、集約ノードで束ねる。
  5. すべての LLM 呼び出しをラッパで包む。トークン計測・一時エラーの再試行・上限到達時の縮退を一手に引き受けさせる(詳細は次節)。
  6. 循環の暴走を防ぐ。グラフにサイクルがある以上、最大反復回数の上限を設けて無限ループを防止する。

このうち 1 つめの「各工程を Executor ノードにする」を図にすると、各ノードは次の構造になります。

flowchart LR
    IN["入力: State<br/>前工程の成果物・差し戻し理由"] --> EX
    subgraph EX["1 つの Executor ノード(= SDLC の 1 工程)"]
        direction TB
        ROLE["役割プロンプト<br/>例: 実装担当・テスト担当"] --> LLM["LLM 呼び出し<br/>工程ごとに選んだモデル"] --> ART["成果物を生成<br/>要件定義書 / コード / テスト など"]
    end
    EX --> OUT["出力: State を更新<br/>成果物・手戻り回数"]

1 つめの「各工程を Executor ノードにする」のイメージ。要求分析・要件定義・設計・実装・各種テスト・リリース判定の各工程が、それぞれこの構造のノードになる。State を入力に受け取り、役割プロンプトとモデルで成果物を生成し、State を更新して次工程へ渡す。

2 つめの State は、全工程が共有する 1 つの「箱」で、次の情報を運びます。

flowchart LR
    subgraph FLOW["工程ノードのあいだを State が流れる"]
        direction LR
        N1["工程 A"] -->|"State"| N2["工程 B"] -->|"State"| N3["工程 C"]
        N2 -. "不合格なら理由を付けて差し戻し" .-> N1
    end
    ST["1 つの State が運ぶ情報<br/>・各工程の成果物(要件定義書 / コード / テスト)<br/>・差し戻し理由(フィードバック)<br/>・手戻り回数<br/>・工程別のモデルレベル"]
    FLOW --- ST

2 つめの State のイメージ。1 つの State が工程ノードのあいだを流れ、各工程はそこへ成果物を書き込んで次工程へ渡す。不合格時は差し戻し理由を載せて前工程へ戻し、手戻り回数や工程別のモデルレベルも同じ State が保持する。

そのうえで、工程ごとに使用するモデルだけを差し替えられるようにし、後半の比較実験(Haiku / Sonnet / Opus など)に用いました。こうして構築したグラフが下図です。要点は「単につなぐ」ことではなく、V 字モデルそのものをグラフとして表現すること。品質ゲートは AI の自己申告ではなく、実ツールの数値で判定します。

  • 品質ゲートの内訳:
    • 単体テスト: pytest --cov でカバレッジ ≥ 50%(この 50% は PoC を簡単にするための暫定ラインで、実務の目標値ではありません)
    • 機能テスト: 要件 ID トレーサビリティ 100%
    • セキュリティ: bandit(静的解析)で最高深刻度(HIGH)= 0
    • シナリオ: 操作フローを pytest で実行
  • ゲートに不合格となった場合は、前工程へバックエッジで差し戻して作り直す(V 字の手戻り)。理想は不具合の根本原因にあたる工程まで戻すことです。要件定義やモジュール設計まで遡る経路もグラフ上に用意しましたが、今回の PoC は主に実装へ差し戻す挙動にとどめました(要求分析まで戻す経路は未実装)。不具合ログから戻す工程を選ぶ LLM ルータも実装しましたが、今回は各ゲートが固定の規則で必ず実装へ差し戻す配線としたため、発動していません。
flowchart TD
    REQ[要求分析] --> SPEC[要件定義] --> ARCH[アーキ設計] --> MOD[モジュール設計] --> IMPL[実装]
    IMPL --> UT{"単体テスト Gate<br/>カバレッジ ≥ 50%"}
    UT -- 不合格 --> IMPL
    UT -- 合格 --> IT{"結合テスト Gate"}
    IT -- 不合格 --> IMPL
    IT -- 合格 --> FAN(("fan-out<br/>並列展開"))
    FAN --> FUNC["機能テスト<br/>要件カバレッジ 100%"]
    FAN --> SCEN["シナリオテスト"]
    FAN --> SEC["セキュリティ<br/>bandit 最高深刻度 = 0"]
    FUNC --> AGG(("fan-in<br/>集約 Gate"))
    SCEN --> AGG
    SEC --> AGG
    AGG -- 不合格 --> IMPL
    AGG -- 合格 --> REL["リリース判定<br/>出荷OK / 出荷見送り"]
    UT -. "根本工程へ戻す経路<br/>(配線済・今回は未使用)" .-> MOD
    AGG -. "根本工程へ戻す経路<br/>(配線済・今回は未使用)" .-> SPEC
    style REL fill:#27ae60,color:#fff

前進=エッジ、手戻り=バックエッジ(循環)。システムテストは fan-out(並列展開)→ fan-in(集約)。ゲートは実ツール(pytest / bandit / 要件トレース)で判定する。実線の手戻りが今回の挙動(主に実装へ差し戻し)、点線はグラフ上は配線済みだが今回は未使用の「根本工程へ戻す」経路。

3. Trial 1:オーケストレーションを完走させるまで

最初に構築したのは、素朴な前進チェーンとゲートでした。これがまったく完走しません。

  • AI の無応答・接続断(socket)・一時的なエラーで停止すると、実行全体がそこでクラッシュする。長い実行(数十回の LLM 呼び出し)の途中で一度詰まるだけで、最初からやり直しになりました。
  • ゲートを 1 つ落ちただけでも、扱いを定めていなかったために実行が止まる。

そこで「呼び出し先(LLM)が失敗したら、呼び出し元(オーケストレーション)が回復するのが当然」という方針で、問題に直面するたびに 1 つずつ補強していきました。版ごとに「直面した問題 → 加えた対策 → 得られた結果」を並べると、その成長の過程は次のようになります。

直面した問題(どの工程で何が起きたか) 加えた対策 得られた結果
Ver0.1 (出発点) 素朴な前進チェーン+ゲート 途中で止まり、完走しない
Ver0.2 実装工程の LLM 呼び出しが接続断(socket)で例外 → 数十回の呼び出しの途中で実行全体がクラッシュし、最初からやり直し 一時エラーの自動リトライ 一時的な失敗では止まらない
Ver0.3 単体テスト工程で pytest が不合格 → 落ちたときの扱いを決めていなかったため、そこで実行が停止 縮退運転(結果を記録して前進)で必ず完走 リリース判定まで必ず到達
Ver0.4 全工程を Haiku 固定にすると、単体・機能テスト工程が毎回テスト落ち(量は書けるが正しく書けない) 失敗した工程だけモデルを段階昇格(Haiku → Sonnet → Opus) 弱い工程を救済
Ver0.5 システムテストの fan-out で機能・シナリオ・セキュリティの 3 エージェントを同時送信 → レート制限(429) 遅延ベースのペーシングで送信間隔を調整 429 を先回りで回避
Final 同じ方針でも、ある回は出荷OK・別の回は出荷見送りと結果が割れる n=3 の反復評価(成功率・中央値で判断) 安定して比較・評価できる

要点は 2 つです。第一に、一時エラーは「失敗」ではなく「待てば回復しうるもの」として扱い、再試行で耐えること。第二に、それでも回復しない工程は、実行を止めずに縮退させ、最後のリリース判定まで必ず到達させて結果を残すこと。落ちた事実も含めて 1 本の実行として観測できるようにしたことで、ようやく比較実験を実施できるようになりました。

なお、土台の実装が壊れたまま後工程を空転させるのは、本質的には無駄です。本来は早期中断(fail-fast:以降の工程を打ち切って記録する)か、フォールバック(別経路で実際に通す)が正しい回復であり、ここは今後の改善点として残りました。

では、この手戻りは実際に品質を改善したのでしょうか。測定したところ、浅い不具合は手戻りで解消するが、深い不具合は解消されず堂々巡りに陥る、という結果でした。全実行を通して、手戻り 0 回で一度に合格した実行が 4 件、手戻り 1 回で救済された例が 1 件あり、手戻り上限(4 回)に達した 7 件はすべて出荷見送りです。とくに Opus は「単体を直すと結合で落ち、再び単体が悪化する」と振動しがちでした(実データの遷移は後半の付録にまとめます)。

4. 品質を支えた核心:AI の自己申告に頼らない品質ゲート

最終版で品質を担保した最大の仕掛けは、各品質ゲートが AI の言葉ではなく、実際にツールを動かした数値で合否を出すことです。

  • 「カバレッジは十分です」と言わせない。pytest --cov の実測値で判定する。
  • 「全要件をカバーしました」と言わせない。要件 ID と機能テストを機械的に突合する(100% 必須)。
  • 「安全です」と言わせない。bandit の最高深刻度 = 0 を確認する。

効果が表れた具体例があります。最も安価なモデル(Haiku)の 3 回は、カバレッジは 93〜95% に達していたにもかかわらず、単体・機能・シナリオのテストが落ち、毎回「出荷見送り」でした。「量は書けるが、正しくは書けていない」をゲートが捉えたわけです。AI の自己申告(「カバレッジ 93%、OK です」)に頼っていれば、通してしまっていたでしょう。

Part 2:どのモデルを選ぶべきか

この Part では、同じグラフで Haiku / Sonnet / Opus を n=3 で比較し、「最上位が最良とは限らない」根拠と、工程ごとのモデル選定の考え方を示します。

5. Trial 2:モデルを変えて QCD を比較する

グラフが安定して動作するようになったので、題材・グラフ・ゲートを固定したまま、モデル方針だけを差し替えて比較しました。

  • 方針1 haiku: 全工程を Haiku(最安・下限)
  • 方針2 sonnet: 全工程を Sonnet(中位)
  • 方針3 opus: 全工程を Opus(最上位・上限)
  • 方針4 recommended: ガイドライン方針(Sonnet 基準で、詰まった所だけ Opus へ昇格)
  • 方針5 adaptive: Haiku で開始し、失敗した工程だけ昇格

そして QA の観点から、1 回では判断しません。コアの 3 方針(方針1〜3)を各 n=3 回実行しました。方針4・5 は補助的な観察にとどめ、本稿では主にこの 3 方針を比較します。

5.1 結果:Sonnet が安定して最良、Opus は振るわず

モデル別の成功率・品質・コスト(n=3)

左から: 成功率/品質スコアの中央値とレンジ/トークン量。具体的な数値は下表に。

指標を 2 つ定義しておきます。まずリリース判定は、グラフの最後で「この成果物を出荷してよいか」を下す判定であり、結果は出荷OK か出荷見送りです。そのうえで、

  • 成功率: n 回試行して「出荷OK」に到達した割合(出荷OK の回数 ÷ n)。全ゲートを通過した(品質スコア 100・残課題ゼロの)ときのみ出荷OK になります。何回実行しても出荷できる品質に届くか、という信頼性の指標です。
  • 品質スコア: 4 ゲート(単体・機能・シナリオ・セキュリティ)の達成項目を加点して 0〜100 点に正規化したもの。全ゲート通過なら 100 点(出荷OK)で、不合格の工程があるほど減点されます。表の「中央値」は、出荷OK の回(100 点)と見送りの回(減点後)を合わせた中央値です。
方針 成功率 品質スコア中央値 品質レンジ トークン中央値* 所要時間(中央値)**
方針1 haiku 0/3 50 35〜55 1.21M 約29分
方針2 sonnet 3/3 100 100〜100 0.56M 約25分
方針3 opus 1/3 80 65〜100 1.00M 約40分

* トークンは cache 読み込み込み(claude -p 経由のため嵩む)。相対比較として見てください。 ** 時間は実時間(wall-clock)。claude -p のレイテンシ(1 呼び出し約 50 秒)込みなので、絶対値は遅めです。

本命の比較(Sonnet と Opus)は次のとおりです。

Sonnet Opus
成功率 3/3(毎回「出荷OK」) 1/3(2 回「出荷見送り」)
品質スコア中央値 100 80
コスト 0.56M トークン 1.00M(約 1.8 倍)
所要時間 約 25 分 約 40 分(約 1.5 倍)

最上位の Opus が、中位の Sonnet に信頼性(成功率)で及ばず、トークン量でも上回りました(このトークン差はバックエンド(claude -p)の影響を含むため、厳密なコスト比較ではなく量の目安として見てください)。上位モデルほど総合的な性能は高くても、課題次第では下位モデルのほうが安定して目的を果たす。本課題・本条件では、その傾向がはっきりと表れました。

この「及ばなかった」結果は実力なのか、回線によるものなのか。公平を期すため、「出荷見送り」の原因を切り分けてあります。今回の「出荷見送り」はすべて品質ゲート起因(テストが通らない)であり、通信エラー・API の一時エラー・レート制限・トークン上限といったインフラ起因の失敗はゼロでした。こうした一時エラーは再試行で吸収する設計で、再試行が尽きた工程のみを「失敗」として記録しますが、全 12 実行でその記録は 0 件です。自分側のネットワーク切断(Wi-Fi や PC の移動)のような外的要因も、判定には混入していません。Opus の「出荷見送り」は、純粋に「品質ゲートを通せなかった」結果です。

5.2 なぜ Opus は及ばなかったのか:生成物の中身を見る

勝敗の数字よりも、及ばなかった Opus が何を生成していたかを見ます。

その前に「落ちた」の意味を明確にしておきます。ここでの「出荷見送り」は、あるゲートを通せないまま、作り直し(手戻り)を上限の 4 回くり返しても通らなかった状態を指します。Opus の「出荷見送り」は 2 回とも、単体テストと機能テストを往復しながら手戻り上限に達したもので、いずれも「カバレッジ 94%・要件カバー 100% という数値は満たすのに、テスト自体が pass しない」状態でした。量は書けるが正しく書けていない、ということです。ここでは自己矛盾が最も鮮明な回を取り上げます。

まず過剰実装です。同じ関数電卓でありながら、この回の Opus の calculator.py は 492 行にまで肥大化していました(Sonnet は 287〜339 行)。

そして自己矛盾です。Opus 自身が書いたテストは、2++3 のような不正な式を構文エラー(SYNTAX)として弾くことを要求していました。

@pytest.mark.parametrize(
    "expression",
    ["2++3", "(1+2", "sin)", "1+", "@#$", "3..14"],
)
def test_syntax_errors(client, expression):
    status, body = post(client, {"expression": expression})
    assert body["ok"] is False
    assert body["errorType"] == "SYNTAX"

ところが、その Opus 自身のパーサは 2++3 を受理してしまいます。++3 を単項プラスと解釈し、2+3 = 5 と計算してしまうのです。実際に評価すると calculator.evaluate("2++3")5.0 を返し、構文エラーになりません。自分で課した厳しい仕様に、自分の実装が追いつかないわけです。このわずか 1 ケースのずれから手戻りが始まり、さらに実装を膨らませる空転に陥って、手戻り上限に達して「出荷見送り」となりました。

同じ傾向は Opus の中だけを見ても確認できます。3 回のうち出荷できた回のコードが最も小さく、落ちた回ほど膨らんでいました(小さい順に、出荷OK の回が 404 行、出荷見送りが 418 行、出荷見送りが 492 行)。実装を膨らませるほど、自己矛盾で落ちるのです。

要点を表で裏づけると、次のようになります。

観点 Sonnet(毎回「出荷OK」) Opus(「出荷見送り」の回)
コード規模 287〜339 行 492 行
自作テストの結果 全件 pass 1 件失敗(自作テストを自作実装が満たせない)
品質スコア 100・100・100(安定) 100・80・65(大きく振れる)
成功率 3/3 1/3

賢いモデルほど作り込みすぎて自滅することがあります。自らに厳しい仕様を課し、その仕様に自分の実装が追いつかない、という形です。

6. Trial 3:1 回では見極められないので反復する

n=1(1 回ずつ)の結果だけを見ると、Opus は Sonnet に明らかに劣る、と結論づけたくなります。しかし、それは早計です。

決定的だったのは、同じ方針でも回によって結果が割れたことです。方針4 recommended を、設定差をペーシング(速度調整)の有無だけにして 2 回実行したところ、片方は「出荷見送り」、片方は「出荷OK」になりました。原因は実行ごとのばらつきです。片方は途中で手戻りが発生して実装が Opus へ昇格し、もう片方は手戻りゼロで一度に「出荷OK」となりました。同じ方針でも、その回の偶然で最終状態がここまで割れるのです。

実際、Opus も 1 回だけ見れば満点で「出荷OK」となる回もあれば、「出荷見送り」の回もありました。1 回の結果だけでは、方針の優劣を見極められません。

そこで n=3 に増やし、成功率(信頼性)と品質スコアの中央値で評価することにしました。これは AI 固有の話ではなく、QA の基本そのものです。「1 回通った=大丈夫」とは言えず、判断には反復が要る、ということです。

7. 得られた指針:モデル選定の考え方

この実測から得た、AI 駆動 SDLC のモデル選定の指針です。

  • Sonnet を一律のデフォルトに。本程度の複雑度では十分で、最も安定し、最も安価です。
  • Opus も、凝った工程別の振り分けも、まずは導入しない。賢さや複雑さは、かえって不安定さや自己矛盾を生みます。Opus は、Sonnet が反復しても安定して落ちる工程に限った予備手段にとどめます。
  • 検証器(テスト生成)は安価にしない。テストの質がそのまま判定の質になります。安価なモデルが脆いテストを書くと、実装が正しくても落ちます。
  • 採用判断は n≥3 の成功率と中央値で。1 回での判断は禁物です。
  • 規模が大きくなれば、従量課金の API キーと SDK へ移行する。ただし今回のように、claude -p(CLI)でも全ツールを禁止し再試行で耐えれば、PoC 規模なら十分に実行できます。サブスクや CLI 認証はレート制限や接続の不安定さが出やすいため、安定性とスケールが必要になるほど API キーが適する、という使い分けです。

8. まとめ

主目的のオーケストレーションについては、分かったことが 2 つあります。1 つは、V 字 SDLC を、前進エッジ・手戻りバックエッジ・fan-out / fan-in(分岐と集約)のグラフとして無理なく構築できること。もう 1 つは、品質を担保するのはモデルの賢さではなく、ツールで判定する品質ゲートと、失敗からの回復設計だということです。素朴な初期版が完走すらできず、再試行と縮退完走を加えてようやく比較実験を実施できた経緯が、それを物語っています。

あわせて分かったのは、モデルに得手不得手があること自体は知られていますが、ツールで測定して複数回実行すると、本課題では中位の Sonnet のバランスの良さがはっきり確認できた、という点です。「高いモデルほど安心」とは限らず、課題に応じた選定が必要だということです。オーケストレーションの価値は「賢い AI を呼ぶこと」ではなく、凡庸なモデルでも品質を担保する構造(ゲート・手戻り・回復)をどう設計するかにあります。

なお、本実験のハーネスは、ワークフロー・グラフ、品質ゲート(pytest-cov・要件トレース・bandit)、工程別エージェント、実験ランナーの 4 部品で構成しました。方針(haiku / sonnet / opus / recommended / adaptive)を切り替えて 1 本実行し、n 回分を中央値と成功率で集計しています。

9. 次の一手

プロダクト品質部は、テストや品質保証の工程を AI ファーストで内製化・移管していく方針を進めています。その一環として、今回の検証は次のように広げていきます。

  • 業務レベルのエージェントワークフローへ。関数電卓のような題材ではなく、QA の実工程(要求レビュー・テスト設計・回帰判定など)を実際に連携させ、現場で使えるワークフローへ育てる。
  • Claude だけに依存しない構成へ。Agent Framework は複数プロバイダを混在できるため、Codex(OpenAI 系)やローカル LLM も同じハーネスに載せ、工程ごとに最適なモデルを横断的に比較・併用する。
  • 実際の成果物で QCD を測る。開発や OA(業務自動化)で実際に作成した成果物を題材に、別モデルで再現させて QCD を計測する。今回の「成功率・品質スコア・トークン・時間」を QCD へ発展させ、どの工程にどのモデルが効くかを詰める。
  • 手戻りを「賢く」する。今回最大の課題で、付録B で分析ノードとして試作しました(検証器の凍結・実装のパッチ化・停滞時の診断 PATCH / REPLAN / HALT まで動作)。ここは単体で掘り下げる価値があるため、契約の明示的な固定や診断精度・適用範囲の向上を含め、別記事で改めて深く扱う予定です。
  • 比較の確度を高める。従量課金 API キーを前提に、対象方針と n を増やし、確定的なランキングに近づける。

付録A:環境セットアップと再現手順

本稿だけで一通り再現できるよう、骨子を残します。

  1. 環境(agent-framework の要件は Python 3.10 以上。3.9 では動きません。今回は 3.13 で実行)
python3.13 -m venv .venv && . .venv/bin/activate
pip install --pre agent-framework agent-framework-anthropic \
    flask pytest pytest-cov bandit radon scikit-learn
  1. ハーネスの構成(4 部品)

  2. ワークフロー・グラフ: WorkflowBuilder で各工程の Executor をノード化し、add_edge(前進)/ add_switch_case_edge_group(合否分岐+バックエッジ)/ add_fan_out_edgesadd_fan_in_edges(システムテストの並列)で配線。max_iterations で循環の上限を設定。

  3. 品質ゲート: pytest --cov(カバレッジ)/ 要件 ID トレーサビリティの突合 / bandit(HIGH=0)を関数化し、各ゲートの Executor から呼ぶ。
  4. 工程別エージェント: 「工程 × モデル方針」でエージェントを生成し、環境変数 SDLC_POLICY で方針を切り替える。
  5. 実験ランナー: 1 方針=1 プロセスで実行し、結果(outcome・品質スコア・トークン・時間・各ゲート値)を JSON 保存 → 集計する。

  6. 実行と集計

# 1 本(方針を環境変数で指定: haiku / sonnet / opus / recommended / adaptive)
SDLC_POLICY=sonnet python -m sdlc.experiments

# n=3 反復(rep を変えて 3 回)
for r in 1 2 3; do SDLC_REP=$r SDLC_POLICY=opus python -m sdlc.experiments; done

# 成功率・中央値で集計
python -m sdlc.experiments aggregate-reps
  1. バックエンド(モデルの呼び出し方)

  2. 本命: 従量課金の API キー。export ANTHROPIC_API_KEY=...(安定・スケール・レートが透明)。

  3. 今回の代替: 本来は API キーを使うところを、Claude Code の CLI(claude -p)をモデルの呼び出し口として使いました。工程の制御は Agent Framework のグラフが担うため、Claude Code が標準で持つツール(ファイル編集やコマンド実行など)はすべて無効化し、プロンプトにテキストを返すだけの「生成器」として動かしています(SDLC_BACKEND=cli)。サブスク認証で動きますが遅く、接続も不安定なため、規模が大きくなれば API キーが適します(前述)。

付録B:手戻りはどこまで効いたか

手戻り(しかるべき工程に差し戻して作り直す仕組み)が実際に品質を改善したかを、全 12 実行で見ると次のようになりました。

手戻り回数 実行数 結果
0 回(一度で合格) 4 すべて出荷OK
1 回(差し戻して修正) 1 出荷OK(救済成功)
4 回(上限到達) 7 すべて出荷見送り(救済できず)

浅いミスは 1 回の手戻りで解消するが、上限まで反復しても解消しない深い不具合がある、という非対称が見られます。

さらに Opus を 1 本、手戻りごとの件数まで記録して最後まで実行した遷移が次です(リリース判定まで到達。最終は出荷見送り・品質スコア 50)。

試行 詰まったゲート カバレッジ OK 数 NG 数
初回 単体 96% 61 1
rework1 後 単体 96% 62 0(通過)
(機能・シナリオで落ち→実装へ戻し) 単体 96% 49 1
rework3 後 単体 96% 36 0(通過)
(また機能・シナリオで落ち→戻し) 単体 95% 52 2(上限到達)

セキュリティ(Bandit)は毎回 HIGH=0 ですが、収束しません。単体を 2 度直して NG 0 にしても、システム機能テストが毎回落ち、実装に戻すと単体がまた壊れます。修正と破壊が交互に起きるのです。OK 数が 61→62→49→36→52 と乱高下するのは、Opus が毎回テストスイートごと書き直すため。自己生成テストを検証器に使うと、テストの量も内容も毎回変わるという難しさが出ています。

B.1 NG は本物の不具合か、テストの誤りか

NG が出たとき、それが実装のバグなのか、テスト自体の誤りなのかは切り分けが必要です。今回、テストが用いる期待値は SRS の要件から取っています。たとえば平方根は REQ-003 に明記され、記号のみで括弧を省いた √16 = 4 もテストケースに含まれます。つまりテスト側は要件どおりで、こうしたテストが落ちるなら原因は実装側にあると判断できます。

ただし、付録 A の再現手順のとおり、実行のたびに生成物を消去して作り直すため、どの回でどのテストがなぜ落ちたかという個別の記録は残っていません。本稿で確実に追えるのは、B.2 に示す手戻りごとの合否件数の遷移までです。

B.2 手戻りが収束しないのは設計の問題

「手戻り回数を増やせば解消するのか」を確かめようと上限を 12 に上げて実行したところ、収束しませんでした。しかも、回を追うごとに落ちるテストが入れ替わります。

掘り下げると、後半の失敗は計算ミスではなく、実装とテストが期待する API 応答の「契約」(成功・エラーをどのキーで、どの形で返すか)の食い違いでした。実装エージェントとテストエージェントが手戻りのたびに別々の契約を作り、噛み合わなくなっていたのです。原因は明確でした。

  • 手戻りのたびに実装もテストも丸ごと再生成するため、両者が API 契約を非互換に作り直す(固定された唯一の契約がない)。
  • 手戻り先を常に「実装」に固定していた。本来は上流(モジュール設計・要件)へ戻して契約から見直すべきところ、グラフ上に用意した「根本工程へ戻す経路」を一度も使っていなかった。
  • 停滞検知がないため、同じ箇所で足踏みしても、ただ実装の再生成を繰り返すだけだった。

これは AI 固有の問題ではありません。毎回違う問題が芋づる式に出るのは、個別のバグではなく設計の欠陥を示すサインです。実装をいじり続けるのではなく、一段上げて計画を見直すべきでした。具体的には、API 契約を設計で確定・凍結し、実装とテストの双方に従わせることです。直らなければ手戻り先を上流へ、それでも駄目なら止めて人へ渡す。この切り替えが、今回のワークフローには欠けていました。そこで作り直しました。

B.3 Trial 4:手戻りを「賢く」する分析ノード

まず素朴に直そうとしました。「テストを初回で凍結(契約固定)」と「停滞したら上流へ差し戻し」です。これは逆効果でした。凍結(契約を固定する前提)と上流再計画(契約を作り直す)が矛盾し、再計画した瞬間に、新しい実装が古い凍結テストと総崩れになりました(NG が 0 → 41 → 58 と急増)。仕掛けを無計画に積むと、仕掛けどうしが衝突します。

そこで、停滞時に呼ばれる分析ノード(自らの失敗を読み返して次手を決める、reflexion 的な役割)を追加しました。これは実際の失敗ログを読み、現象から根本原因を診断して、次の一手を 1 つ選びます。

  • PATCH: 実装の単純なバグ。実装だけを最小修正する(テストは凍結のまま)。
  • REPLAN: 実装とテストの契約不一致。上流で契約を確定し、テストを凍結解除して作り直す(凍結と再計画を協調させ、先の逆効果を解消する)。
  • HALT: 自力では解消できない。根本原因レポートを付けて人へエスカレーションする。

要点は、この診断と差し戻し先の選択を、Agent Framework のグラフのノードとして表現したことです。停滞の検知自体はコード側のしきい値(実装に数回戻して解消しなければ、というガード)に残していますが、その先の「どう直すか」をグラフが分岐します。

flowchart TD
    GATE{"ゲート不合格"} -- 数回で直る --> IMPL[実装を修正]
    GATE -- "停滞(コード側しきい値で検知)" --> ANALYST(["分析ノード<br/>失敗ログから診断"])
    ANALYST -- PATCH --> IMPL
    ANALYST -- REPLAN --> PLAN[上流で契約を確定<br/>+テスト作り直し]
    ANALYST -- HALT --> HUMAN["中断 → 人へ<br/>(根本原因レポート付き)"]
    style ANALYST fill:#2980b9,color:#fff
    style HUMAN fill:#c0392b,color:#fff

結果は明快でした。この分析ノードは Opus を無理に GO させません。たとえば次のように振る舞います。「REPLAN で要件を足したところ新たな RecursionError が発生し、カバレッジも悪化した。自分の修正がむしろ事態を悪化させている。これ以上は無駄なので HALT」。停滞を検知すると分析ノードが REPLAN から HALT へと判断し、手戻り 3 回で原因を特定して人へ渡します。やみくもに 8〜12 回反復して不透明な不合格を出すのとは、挙動が大きく異なります。

オーケストレーションの価値は、凡庸なモデルを無理に成功させることではなく、失敗を速く、診断付きで確定して人に渡すことにあります。その判断は、Python の制御フローに埋め込むのではなく、グラフのノードとして表現できます。

B.4 AI が書いた検証器をどう固定するか

品質ゲートは「AI の自己申告ではなくツールの実測で判定する」と書きました。ただ、ここには一段深い問いがあります。判定の基準であるテストコード自体も、今回は AI が生成しています。テストは実装とは別のテスト設計エージェント(単体・機能それぞれ)が書きますが、書き手が AI であることに変わりはありません。実装と同じ作り手がテストを書き、しかも手戻りのたびに書き直すと、判定の基準が毎回変わり、何を測っているのか分からなくなります。実際、テストを毎回作り直していた Trial 1〜3 では OK 数が 61→62→49→36→52 と乱高下しました(B.2)。実装の良し悪し以前に、テストが毎回別物だったからです。

「ツールで判定するから決定論的(同じ成果物なら、いつ測っても同じ合否になること)」を本当に成り立たせるには、判定の基準を動かないものにする必要があります。そこで Trial 4 では、テストを初回生成で凍結し、手戻り中は再生成せず再利用しました。テストを作り直すのは、最初の 1 回と、契約そのものを見直す REPLAN のときだけです。実装は、この固定された基準に対して直していきます。

# pipeline.py — 手戻り中(reworks>0)は既存テストを再生成せず再利用=検証器を固定する
async def _gen_or_reuse_test(stage_id, test_file, agent, s):
    if FREEZE_VERIFIER and s.reworks > 0 and (ARTIFACTS / test_file).exists():
        _say(f"(検証器凍結: {test_file} を再生成せず再利用)")
        return s.art.get(stage_id) or _read(test_file)   # ループ中は前回のテストをそのまま使う
    res = await agent.run(build_prompt(stage_id, s))       # 初回(または REPLAN 後)だけ生成
    gates.write_artifact(ARTIFACTS, test_file, gates.extract_code(res.text))
    return res.text

こうすると、ループの中で動くのは実装だけになり、テスト(判定の基準)は固定されます。「決定論的」という言葉が、テストの出所まで含めて成り立ちます。逆に、検証器を凍結しないままツールで測っても、測るたびに基準が変わるため、決定論的とは呼べません。AI に検証器を書かせるなら、その凍結もセットで設計する必要があります。

終わりに

スパイダープラスでは仲間を募集中です。

スパイダープラスにちょっと興味が出てきたなという方がいらっしゃったらお気軽にご連絡ください。

https://hrmos.co/pages/spiderplus/jobs