
Keywords: LangGraph, Generator & Critic, Multi-Agent, Unit Test, Human-in-the-Loop, StateGraph, AI Agent Evaluation
こんにちは、Lena Nadir Blog 管理人です。
前回は、Microsoft Foundryを使って Parallel Fan-Out/Gather Pattern を実装し、複数の専門Agentを並列実行した後に結果を統合するMulti-Agent構成を試しました。
今回は、少し異なるパターンを扱います。テーマは Generator & Critic Pattern です。
Generator & Criticでは、1つのAI Agentが成果物を作り、別のAI Agentが内容をレビューします。問題があれば作成担当へ差戻し、修正後にもう一度レビューする流れです。
今回はこのパターンを、単体テスト仕様書の作成支援に適用しました。
単体テスト仕様書の品質は、作成者やレビューアの経験に左右されがちです。経験者へレビューが集中するとボトルネックになり、繁忙時に開発者が作成とレビューを一人で担えば、担当者による品質差も大きくなります。
例えば、次のような観点を安定して洗い出せるかどうかが課題になります。
- 設計書に記載された条件の見落とし
- コードにのみ存在する分岐のテスト漏れ
- 異常系や境界値の不足
- 実装上追加された設定や例外処理の見落とし
そこで今回は、人間が作成した主要テスト項目を起点に、AIが設計書と変更コードから不足する観点を補完する構成を試しました。レビューアを代替するのではなく、特定の人の経験だけに依存せず、一定水準の観点を確認できる仕組みを目指します。
単体テスト仕様書の品質を個人の経験だけに委ねず、人間が考えた主要項目を起点に、AIが設計書と実装コードから不足する観点を補完する
🧩 Generator & Critic Patternとは?
Generator & Critic Patternでは、成果物を作る役割と、その品質を評価する役割を分離します。
- Generator: 要求や入力情報をもとに成果物の下書きを作り、指摘を受けた場合は修正する役割
- Critic: 成果物を仕様や品質基準と照合し、承認可否、問題点、修正すべき内容を返す役割
⚠️ 用語補足: 英語のCriticは、日本語では一般に「批評家」や「評論家」と訳されます。ただし、ここでのCriticは成果物を否定的に批判する役割ではありません。あらかじめ定めた基準に沿って成果物を評価し、改善点を具体的に示す レビュー担当 に近い位置づけです。

入力の読み込みからテスト仕様書の作成、レビュー、人間への確認、成果物の出力までを含む全体像。
前回のParallel Fan-Out/Gatherが、独立した複数の処理を並列実行して最後に統合するパターンだったのに対し、Generator & Criticでは、1つの成果物を生成し、レビューと修正を繰り返しながら品質を上げることが中心になります。
そのため今回は、処理の状態と条件分岐を管理し、レビューと修正の繰り返しを表現しやすい LangGraph を利用しました。
今回の試作版では、GitHub Pull RequestやCI/CDとの連携は対象外とし、3種類の入力をローカルファイルから読み込みます。個別の開発案件に依存する要素を切り離し、レビューと修正の流れ、および出力品質の検証に範囲を絞りました。
📥 3種類の入力をどう扱うか
1. 設計書(Design Documents)
基本設計書、システム設計書、詳細設計書などを想定しています。設計書からは、システムとして何を実現したいのか、どのような処理や制約が存在するのかを読み取ります。
ただし、実際の設計書はコードレベルの詳細まで書かれていないこともあります。設計書には「注文登録処理を追加する」とだけ書かれている一方、実装コードには入力チェック、重複チェック、Database更新、条件を満たさない場合の途中終了、エラー処理まで存在する、といったケースです。
そのため、設計書だけを正解としてテスト仕様書を生成しないことを重要な方針にしました。
2. 変更したソースコード(Source Codes)
2つ目は、今回開発・変更したソースコードです。Python、Java、C#、JavaScript / TypeScript、SQL、YAML / JSONなど、特定の言語には限定しません。
ソースコードを参照することで、設計書には書かれていない次のような実装上の条件を拾います。
- 条件分岐と、条件を満たさない場合に処理を途中で終了する箇所
- 上限値、下限値、空値などの境界条件
- 例外処理
- Database処理
- 設定値
- 認証処理
- タイムアウトと再試行
ただし、コードに書かれているから、それが正しい仕様であるとは扱いません。設計書とコードが矛盾していれば、その差分自体を確認対象にします。
3. 開発者が作成した主要テスト項目(Key Test Items)
3つ目は、開発者が事前に考えた主要テスト項目です。
## Key Test Items
- 正常系で登録できること
- Email未入力時にエラーになること
- 重複Emailを登録できないこと
この入力を完成したテスト仕様書とはみなしません。開発者が主要なケースは考えているものの、抜け漏れが存在する可能性がある、という前提で扱います。
テスト仕様書を作るAI Agentは、開発者の主要テスト項目を起点に設計書と変更コードを読み、Email形式の確認、Database登録の失敗、処理を途中で終了する条件など、記載されていなかった観点を補完します。
開発者の主要テスト項目を起点にはするが、それだけを正解とはみなさない
🤖 Test DesignerとCriticの責務
Test Designerは3つの入力を読み、テスト仕様書の下書きを作成します。各テストケースには、どの情報を根拠に追加したのかを可能な限り残します。
| テストID | シナリオ | 期待結果 | 根拠 |
|---|---|---|---|
| UT-001 | 正常登録 | 正常に登録される | 設計書 + 開発者 |
| UT-002 | 重複登録 | 重複エラーになる | 変更コード |
| UT-003 | 入力上限値 | 上限値を受理する | 変更コード |
表の「根拠」列は、各テストケースが設計書、変更コード、開発者の主要テスト項目のどれをもとにしているかを示します。このように、テストケースから根拠となる情報をさかのぼれる状態を Traceability(追跡可能性) と呼びます。AIが追加したテストケースについて「なぜ必要なのか」を確認できなければ、人間側のレビュー負担が逆に増えるためです。
一方、Criticはテスト仕様書の下書きを次の観点でレビューします。
- 設計書の内容が反映されているか
- 変更コードの主要な条件分岐が網羅されているか
- 開発者の主要テスト項目だけに引っ張られていないか
- 正常系だけに偏っていないか
- 関連する異常系や境界値が不足していないか
- 期待結果が曖昧でないか
- 設計書とコードの不整合を見落としていないか
- 関係のない一般論に基づくテストケースを追加していないか
レビュー結果は、次の3つに分類します。
- 承認(
APPROVED): 仕様書を確定する - 要修正(
NEEDS_REVISION): Test Designerへ差し戻す - 人間の確認が必要(
HUMAN_CLARIFICATION_REQUIRED): AIだけでは判断せず、人間への確認事項を整理する
🔁 LangGraphでレビューと修正の流れを実装する
今回の実装ではLangGraphのStateGraphを使っています。Criticの判定結果と修正回数に応じて、Test Designerへ差し戻す、仕様書を確定する、人間へ確認を求めるという3つの処理を切り替えます。
次に行う処理は、decide_next_step()で決定します。
def decide_next_step(state, default_max_revisions):
# Criticの判定結果、修正回数の上限、現在の修正回数を取得する
status = state["review_result"].status
max_revisions = state.get(
"max_revisions",
default_max_revisions,
)
revision_count = state.get("revision_count", 0)
# Criticが承認した場合は成果物を確定する
if status == "APPROVED":
return "finalize"
# AIだけでは判断できない場合は、人間への確認内容を整理する
if status == "HUMAN_CLARIFICATION_REQUIRED":
return "human_clarification_required"
# 修正回数が上限に達した場合も、人間へ判断を委ねる
if revision_count >= max_revisions:
return "human_clarification_required"
# 上記以外は「要修正」のため、Test Designerへ戻す
return "revise_test_spec"
decide_next_step()は、Criticの判定結果と修正回数だけを見て、次の処理を選ぶ独立した関数です。そのため、AIや処理全体を起動せず、入力と戻り値だけで単体テストできます。また、「要修正」が続いても終わらなくならないよう、修正は最大2回に制限しました。
# Criticの判定に応じて、次に実行する処理を切り替える
graph.add_conditional_edges(
"review_test_spec",
route_after_review,
{
"finalize": "finalize",
"human_clarification_required": "human_clarification_required",
"revise_test_spec": "revise_test_spec",
},
)
# 修正後はCriticへ戻し、再度Reviewする
graph.add_edge("revise_test_spec", "review_test_spec")
# 人間への確認事項を出力へ反映した後、成果物を確定する
graph.add_edge("human_clarification_required", "finalize")
LangGraphを採用した価値は、単に2つのAIを順番に呼び出せることではありません。どの条件で修正へ戻し、いつ終了し、いつ人間へ判断を戻すかを、処理の流れとして明示できることにあります。
🛡️ 処理のガードレールを設計する
今回の処理には、AIの判断をそのまま通さず、誤った出力や終わらない修正を防ぐための 処理のガードレール(Workflow Guardrail) を組み込んでいます。禁止語や有害な出力を検出する仕組みではなく、レビュー結果、処理の分岐条件、修正回数によって進め方を制御します。
| ガードレール | 制御内容 | 防ぎたいこと |
|---|---|---|
| レビュー結果を3種類に制限する | 承認、要修正、人間の確認が必要、のいずれかに限定する | 想定外の判定によって処理が誤った方向へ進むこと |
| 「分からない」を推測しない | 判断材料が不足した場合は、人間への確認事項を整理する処理へ進む | 根拠のない推測によるテストケース生成 |
| 修正回数に上限を設ける | 修正が2回に達したら繰り返しを終了する | 終わらない修正、利用量、実行時間の増大 |
| レビューを収束させる | 差戻しが必要な指摘と改善提案を分け、過去のレビュー履歴を参照する | 軽微な指摘による不要な差戻しと、レビューごとの評価の揺れ |
以降では、表の4つのガードレールを順に説明します。
レビュー結果を3種類に制限する
Criticの回答形式をあらかじめ定め、レビュー結果には「承認」「要修正」「人間の確認が必要」の3種類だけを許可しています。想定外の回答は処理を分岐する前にエラーとして検出するため、意味の曖昧な判定によって意図しない処理へ進むことを防げます。
「分からないこと」を推測させない
例えばSQLに、次の条件が書かれていたとします。
WHERE status IN (3, 5)
設計書やパラメータ定義に説明がなければ、3と5が表す業務パラメータはコードだけでは判断できません。ここでAIが意味を推測すると、尤もらしい推測で誤ったテストケースが生成されます。
そこで、Criticが判断材料の不足を検出した場合は、「人間の確認が必要」と判定します。その後、AIが判断できなかった内容と確認したい事項を整理し、人間へ判断を戻します。
修正回数に上限を設ける
Criticが「要修正」を返し続けた場合も、無制限にはやり直しません。修正が上限の2回に達した時点で繰り返しを終了し、人間へ判断を戻します。これは終わらない処理を防ぐだけでなく、改善が収束しない処理へAIの利用量と時間を使い続けないための制御でもあります。
レビューを収束させる
すべての指摘を同じ重要度で扱うと、表現や記載の細かさを整えるだけでも差戻しが続きます。そこで、テスト観点の不足や根拠のないテストケースを 差戻しが必要な指摘(Blocking)、重複や軽微な表現の改善を 改善提案(Advisory) としました。未解決の「差戻しが必要な指摘」がある場合だけTest Designerへ戻します。
さらに、2回目以降は過去のレビュー履歴をCriticへ渡します。前回の指摘が解消されたかを優先して確認し、直前の修正で追加したテストケースを新たな軽微な指摘の対象にしたり、根拠なく前回と逆の要求を出したりすることを抑えます。
Agentに、「想定外の判定を通さない」「分からない内容は推測させない」「レビューを収束させる」「評価基準を揺らさない」というガードレールを設計する
⚙️ config.yamlで入出力ファイルを管理する
今回の試作版では、入力ファイルと出力先をconfig.yamlで管理しています。
input:
design_paths:
- "input/design/**/*.md"
source_paths:
- "input/source/**/*"
key_test_items:
- "input/key-test-items.md"
output:
test_specification:
- "artifacts/unit-test-specification.md"
trace:
- "artifacts/execution-trace.md"
🔎 実行履歴を残す
生成された単体テスト仕様書だけでは、どの処理を経てその結果になったのか分かりません。そこで、入力の読み込み、仕様書の作成、レビュー、修正、確定という実行順序を記録します。
START
→ load_context
→ generate_test_spec
→ review_test_spec
→ revise_test_spec
→ review_test_spec
→ finalize
→ END
さらに、Criticの判定結果、判定理由、指摘事項、人間への確認事項をレビュー履歴へ保存し、execution-trace.mdとして出力します。
この履歴は、処理の状況を把握するためだけのものではありません。修正によって品質が本当に改善したのか、同じ指摘を繰り返していないか、人間へ判断を戻した理由が妥当かを後から確認する材料になります。
📊 実行結果について
実行の入出力ファイルは以下を使用しました。
| 種別 | ファイル | 内容 |
|---|---|---|
| 入力 | email.py | Azure Communication Emailを使ったメール生成・送信処理 |
| 入力 | SendMailLogic-design.md | Plain Text / HTML形式と送信元・送信先の設定を記載した簡易設計書 |
| 入力 | key-test-items.md | 開発者が作成した3件の主要テスト項目 |
| 出力 | unit-test-specification.md | 生成された単体テスト仕様書 |
| 出力 | execution-trace.md | 実行した処理の順序とCriticの判定履歴 |
入出力ファイルの内容
各ファイルは、ファイル名を選択すると内容を確認できます。長いOutputはプレビュー領域内でスクロールできます。
入力ファイル
email.py
import logging
from azure.communication.email import EmailClient
from shared.constants import Email
# メールコンテンツの作成
def _make_send_massage(subject: str, plain_text: str, html: str):
"""
メール送信情報の作成
Args:
subject (str): メール件名
plain_text (str): プレーンテキスト形式のメール本文
html (str): HTML形式のメール本文
Returns:
dict: メール送信情報
"""
message = {
"senderAddress": Email.FROM_ADDRESS.value,
"recipients": {
"to": [{"address": Email.TO_ADDRESS.value}],
},
"content": {"subject": subject, "plainText": plain_text, "html": html},
}
return message
# メール送信用関数
def send_htmlmail(subject: str, plain_text: str, html: str, email_conn_string: str):
try:
# EmailClientサービスに接続
client = EmailClient.from_connection_string(email_conn_string)
# 送信メール
message = _make_send_massage(subject=subject, plain_text=plain_text, html=html)
poller = client.begin_send(message)
poller.result()
logging.info("Email sent successfully.")
except Exception as ex:
print(ex)
SendMailLogic-design.md
Design Document for SendMail Function
Overview
Azureのメールサービスを用いて、emailを送信する関数を作成
- Library: azure.communication.email
- Library: azure.communication.email
- Reference: https://learn.microsoft.com/en-us/python/api/overview/azure/communication-email-readme?view=azure-python
- プレーンテキスト形式、HTML形式の2つを実装
- 自作の定数関数で,送信先/送信元メールアドレスをBind
- from shared.constants import Email
key-test-items.md
Key Test Items
- 正常系で登録できること
- Email未入力時にエラーになること
- 重複Emailを登録できないこと
出力ファイル
unit-test-specification.md
ユニットテスト仕様書
対象の変更内容
- ソースファイル:
input\source\email.py - 概要: Azure Communication Email を利用してメール送信を行う関数が追加された。送信メッセージは定数の送信元・送信先アドレスを用いて組み立てられ、件名・プレーンテキスト本文・HTML本文を含む構造で送信され、送信処理全体は例外を捕捉する実装になっている。
開発者提供の Key Test Items
- 正常系で登録できること
- Email未入力時にエラーになること
- 重複Emailを登録できないこと
テストケース
| ID | カテゴリ | シナリオ | 前提条件 | 入力 | 期待結果 | Evidence |
|---|---|---|---|---|---|---|
| UT-001 | 正常系 | 件名・プレーンテキスト本文・HTML本文を指定したとき、メール送信用メッセージが定数の送信元・送信先アドレスを用いて正しく生成されることを確認する。 | Email.FROM_ADDRESS.value および Email.TO_ADDRESS.value が取得可能であること。 | _make_send_massage(subject=‘件名’, plain_text=‘本文テキスト’, html=‘ 本文HTML ’) を呼び出す。 | 戻り値が dict であり、senderAddress に Email.FROM_ADDRESS.value、recipients.to[0].address に Email.TO_ADDRESS.value、content.subject に ‘件名’、content.plainText に ‘本文テキスト’、content.html に ‘ 本文HTML ’ が設定されていること。 | Design + Code |
| UT-002 | 正常系 | 有効な接続文字列とメール内容を指定したとき、EmailClient を生成してメール送信処理が最後まで実行されることを確認する。 | EmailClient.from_connection_string、begin_send、poller.result をモック可能であること。 | send_htmlmail(subject=‘件名’, plain_text=‘本文テキスト’, html=‘ 本文HTML ’, email_conn_string=‘valid-connection-string’) を呼び出す。 | EmailClient.from_connection_string が指定した接続文字列で1回呼ばれること。begin_send が _make_send_massage で生成されるメッセージを引数に1回呼ばれること。poller.result が1回呼ばれること。例外が送出されないこと。 | Design + Code + Human Key Test Item |
| UT-003 | 正常系 | メール送信成功時に成功ログが出力されることを確認する。 | logging.info をモック可能であり、送信処理が成功するように EmailClient 系をモックすること。 | send_htmlmail(subject=‘件名’, plain_text=‘本文テキスト’, html=‘ 本文HTML ’, email_conn_string=‘valid-connection-string’) を呼び出す。 | logging.info が ‘Email sent successfully.’ を引数に1回呼ばれること。 (Note: 成功ログの出力は設計書に明記されておらず、実装に存在する振る舞いである。) | Code |
| UT-004 | 例外系 | 接続文字列が未入力で EmailClient 生成時に例外が発生した場合、例外が外部へ再送出されず処理内で捕捉されることを確認する。 | EmailClient.from_connection_string が例外を送出するようにモック可能であること。print をモック可能であること。 | send_htmlmail(subject=‘件名’, plain_text=‘本文テキスト’, html=‘ 本文HTML ’, email_conn_string=”) を呼び出す。 | 関数呼び出し元へ例外が送出されないこと。発生した例外オブジェクトまたはその内容が print に渡されること。 (Note: 人手テスト項目の『Email未入力時にエラーになること』は本変更のコード上では接続文字列未入力に近い観点として解釈できるが、設計書には未入力時の仕様が記載されていない。) | Code + Human Key Test Item |
| UT-005 | 例外系 | begin_send 実行時に例外が発生した場合、例外が外部へ再送出されず処理内で捕捉されることを確認する。 | EmailClient.from_connection_string は正常にクライアントを返し、client.begin_send が例外を送出するようにモック可能であること。print をモック可能であること。 | send_htmlmail(subject=‘件名’, plain_text=‘本文テキスト’, html=‘ 本文HTML ’, email_conn_string=‘valid-connection-string’) を呼び出す。 | 関数呼び出し元へ例外が送出されないこと。client.begin_send が1回呼ばれ、発生した例外オブジェクトまたはその内容が print に渡されること。 | Code |
| UT-006 | 例外系 | poller.result 実行時に例外が発生した場合、例外が外部へ再送出されず処理内で捕捉されることを確認する。 | EmailClient.from_connection_string と client.begin_send は正常動作し、poller.result が例外を送出するようにモック可能であること。print をモック可能であること。 | send_htmlmail(subject=‘件名’, plain_text=‘本文テキスト’, html=‘ 本文HTML ’, email_conn_string=‘valid-connection-string’) を呼び出す。 | 関数呼び出し元へ例外が送出されないこと。poller.result が1回呼ばれ、発生した例外オブジェクトまたはその内容が print に渡されること。 | Code |
| UT-007 | 境界値 | 件名・プレーンテキスト本文・HTML本文に空文字を指定した場合でも、メッセージ生成関数がその値をそのまま保持した構造を返すことを確認する。 | Email.FROM_ADDRESS.value および Email.TO_ADDRESS.value が取得可能であること。 | _make_send_massage(subject=”, plain_text=”, html=”) を呼び出す。 | 戻り値の content.subject、content.plainText、content.html がいずれも空文字であり、senderAddress と recipients.to[0].address は定数値のままであること。 (Note: 入力値の必須チェックは実装されていないため、現実装の保持動作を確認するテストである。) | Code |
実装上の注意点
- 送信先メールアドレスと送信元メールアドレスは関数引数ではなく Email 定数に固定されているため、環境ごとの差し替え方法やテスト時の扱いに注意が必要である。
- send_htmlmail は広い Exception を捕捉し、logging ではなく print で例外を出力して終了するため、呼び出し元で失敗を検知しにくい実装である。
- Azure Communication Email への実通信は EmailClient.from_connection_string、begin_send、poller.result に依存するため、単体テストではモック化が前提となる。
Design / Code の差異
- 設計書では『プレーンテキスト形式、HTML形式の2つを実装』とあるが、入力値が未入力・空文字の場合の扱いは記載されていない一方、実装ではバリデーションを行わずそのままメッセージに設定している。
- 設計書ではメール送信失敗時のエラーハンドリング方針が記載されていない一方、実装ではすべての例外を捕捉して print 出力のみを行い、例外を再送出しない。
- 人手テスト項目にある『重複Emailを登録できないこと』は、本変更のコードおよび設計書がメール送信機能であり登録処理や重複判定を持たないため、変更内容と整合しない。
- 人手テスト項目にある『Email未入力時にエラーになること』は、送信先・送信元アドレスが定数から設定される実装と一致しておらず、どの Email 入力を指すのか設計書上で明確でない。
人による確認が必要な事項
なし。
execution-trace.md
実行トレース
本実行の診断記録です。LangGraphが実際にたどったノードの順序と、各段階でのCriticの判定根拠をすべて記録しています。成果物本体は、別途出力されるユニットテスト仕様書ファイルを参照してください。
実行ルート
ルート: START -> load_context -> generate_test_spec -> review_test_spec -> finalize -> END
Criticレビューサイクル
レビュー回数 1
- 判定: APPROVED
- 判定根拠(Criticの要約): 設計書と変更コードに対する主要な振る舞いは概ね網羅されており、未解決の重大なカバレッジ欠落は見当たりません。人手テスト項目との不整合もドラフト内で明示的に扱えているため、承認可能です。
- 指摘事項:
- [redundant_test] (対象テストケース: UT-004) UT-004 は人手テスト項目の『Email未入力時にエラーになること』を接続文字列未入力に読み替えた補助的なテストですが、設計書・実装上の直接的な仕様ではなく、例外捕捉の観点自体は UT-005/UT-006 と重複しています。残してもよいものの、トレーサビリティ上はコード根拠のある例外発生箇所別テストに整理した方が明確です。 [根拠: input/source/email.py, input/design/SendMailLogic-design.md]
- 確認質問: (なし)
Human-in-the-Loop(HITL)へのエスカレーション
この実行ではHuman-in-the-Loop(HITL)へのエスカレーションは発生しませんでした。
初版では、簡易なメール送信プログラムを対象としたにもかかわらず、修正回数が上限の2回に達しました。レビューと修正が想定どおりに終了しなかったため、実行履歴とCriticの判定内容を分析しました。その結果を踏まえ、Criticの指摘を「差戻しが必要な問題」と「改善提案」に分け、過去のレビュー履歴も参照するように変更しました。以下は、同じ入力を使った初回実行と再実行の比較です。
| 指標 | 初回実行 | 再実行 |
|---|---|---|
| Criticによるレビュー回数 | 3回 | 1回 |
| Test Designerによる修正回数 | 2回 | 0回 |
| 人間への確認 | あり | なし |
| 最終判定 | 修正回数の上限に到達 | 承認(APPROVED) |
| 生成されたテストケース数 | 8件 | 7件 |
| 推定AI呼び出し回数(※) | 6回 | 2回 |
⚠️ 推定AI呼び出し回数: 通信エラーなどによる再試行がなく、各処理でAIを1回呼び出した場合の概算を示す
🔎 結果の考察と試行錯誤
結論からいえば、テスト観点の洗い出しには利用できましたが、単体テスト仕様書をAIだけで完成させる仕組みとして、そのまま実務へ適用できる段階ではありません。
初回のレビューが終了しなかった理由
初回実行では、Criticが3回とも「要修正」と判定しました。Test Designerが指摘へ対応しても、次のレビューで評価基準が変わり、新しい指摘が発生していたためです。
- 修正で追加したテストケースが、次のレビューでは重複と判定される
- 利用者が直接呼び出す処理と内部処理の連携を、どこまで個別のテストケースにするかが揺れる
- エラー処理を発生箇所ごとに分けるか、代表ケースへまとめるかが定まらない
この結果から、Generator & Critic Patternを構成するだけでは、品質が自動的に一定水準へ高まるとは限らないことが分かりました。
Criticの差戻し条件を見直した
軽微な改善提案まで 差戻しの対象 になっていたため、テスト観点の不足や根拠のないテストケースを「差戻しが必要な指摘」、重複や表現改善を「改善提案」に分けました。さらに、前回と矛盾する指摘を抑えるため、Criticが過去のレビュー履歴も参照するようにしました。
再実行では修正回数が2回から0回、推定AI呼び出し回数が6回から2回へ減り、最初のレビューで終了しました。ただし、最初から「承認」と判定されたため、過去のレビュー履歴を使った一貫性の確認は実際には動いていません。今回確認できたのは主に、差戻しが必要な指摘と改善提案を分けた効果です。
承認されても品質課題は残った
再実行時の仕様書には、次の問題が残りました。
- メール送信処理と整合しない主要テスト項目を根拠に含めている
- 「Email未入力」を接続文字列未入力へ読み替えている
- エラー発生時の期待結果が曖昧で、具体的な合否判定の条件になっていない
根拠の弱い読み替えは、「裏付けのない推測」として差戻し対象にする余地があります。「承認」は設定した判定条件を通過したことを示すだけで、仕様書の正しさを保証するものではありません。
費用対効果は未評価
AIの利用量、実行時間、料金は計測していません。AIの呼び出し回数は3分の1になりましたが、待ち時間や料金がどれだけ減ったかは判断できません。処理効率と出力品質を同じ条件で計測し、人間のレビュー工数と比較する必要があります。
🧪 Multi-Agent処理のテスト方法
Multi-Agent処理を実務で利用するには、処理の分岐が設計どおりに動くか、各AI Agentが期待する役割を果たすか、入力から最終出力まで正しく動くかを分けてテストする必要があります。今回はGenerator & Critic Patternの実装と検証結果に範囲を絞ります。
Multi-Agent処理のテスト設計は、次回以降のブログで独立したテーマとして取り上げます。
⚠️ Generator & Criticで気をつけること
Criticは自動的に正しいわけではない
Criticを置くだけで品質が保証されるわけではありません。GeneratorとCriticが同じ前提を共有し、同じ見落としをする可能性があります。Critic自体にも、問題のある下書きと十分な下書きの両方を与え、正しく判定できるか評価する必要があります。
修正回数を増やせばよいわけではない
修正を増やすほど、AIの利用量、料金、待ち時間は増えます。また、判断材料の不足が原因なら、何度繰り返しても解決しません。一定回数で打ち切り、人間へ判断を委ねる方が合理的です。
AIの出力を最終成果物にしない
今回の仕組みは、単体テスト仕様書を自動承認するものではありません。AIは抜け漏れの探索と下書きの改善を支援し、最終判断は人間が行います。
まとめ
今回の試作では、テスト観点と設計書・コード間の差異を抽出できました。Criticの差戻し条件を見直すとレビューは早く終了しましたが、承認後の仕様書にも根拠の弱いテストケースが残っています。改善できたのは主に処理の収束性であり、品質が保証されたわけではありません。
今回の検証は、模範例を与えずに設計書とコードから判断させる、いわば Zero-shot に近い構成です。次の有力な改善策は、過去の承認済みテスト仕様書、レビュー記録、不具合事例をHybrid Searchで類似検索し、 Few-shotのサンプル としてTest DesignerとCriticへ渡すことです。これにより、組織やチームに蓄積したテストノウハウと評価基準を、双方のAgentへ反映できます。
ただし、検索結果も正解とは限りません。対象システム、バージョン、機能で絞り込み、承認済みで新しい文書を優先し、参照元を仕様書へ残す必要があります。そのうえで、出力品質、AIの利用量、人間のレビュー工数を比較して費用対効果を評価します。
Zero-shotでAIを調整し続けるのではなく、Hybrid Searchで検索した模範事例をFew-shotとして与え、蓄積したテストノウハウによる品質向上を狙う
コード一式
この記事で使用したコードは、GitHub Repositoryのmulti-agent/langgraph-test-spec-generator/に公開しています。