オートコンプリートからエージェントへ:LangGraphとPytestによるユニットテストの自動化

AI tutorial - IT technology blog
AI tutorial - IT technology blog

エージェントによるテスト導入の6ヶ月:本番環境レポート

私のチームにおける最大のボトルネックは、新機能のリリースではなく、ユニットテストの作成と回帰バグの追跡という、精神を削られるようなサイクルでした。6ヶ月前、私たちはAIを単なる高度なオートコンプリートとして扱うのをやめ、LangGraphとPytestを使用した動的なエージェント型ワークフローの構築を開始しました。その結果は非常に堅実なものでした。定型的なテスト作成に費やす時間を40%削減し、assert文の作成よりも高レベルなアーキテクチャに集中できるようになりました。

多くの開発者はAIを静的な神託のように扱います。コードを入力し、AIがテストを出力し、ハルシネーション(もっともらしい嘘)によるエラーが発生すると手動で修正します。エージェント型ワークフローはこのループを完結させます。テストを書き、実行し、トレースバックを分析し、コードがパスするまで繰り返します。手動の介入は必要ありません。

5分で完了するセットアップ:AIテスターの構築

開始するのに必要なのは、Pythonといくつかのライブラリだけです。状態のオーケストレーションにはLangGraphを使用し、実行の管理にはPytestを使用します。頭脳としては、GPT-4oやClaude 3.5 Sonnetのような高度な推論モデルが最適です。

pip install langgraph langchain-openai pytest pytest-json-report

この最小限のスクリプトは、ソースコード、生成されたテスト、および失敗ログを追跡する状態(state)を定義します。

from typing import TypedDict, List
from langgraph.graph import StateGraph, END

class AgentState(TypedDict):
    code: str
    tests: str
    test_results: str
    iterations: int

# Writer: 初期のテストスイートを生成する
def coder_node(state: AgentState):
    # ソースコードに基づいてLLMにテストを書くようプロンプトを出す
    return {"tests": "def test_logic(): ...", "iterations": state['iterations'] + 1}

# Executor: テストを実行し、エラーをキャプチャする
def executor_node(state: AgentState):
    # pytest.main()をトリガーし、出力をキャプチャする
    return {"test_results": "失敗: AssertionError..."}

# フローを定義する
workflow = StateGraph(AgentState)
workflow.add_node("writer", coder_node)
workflow.add_node("tester", executor_node)
workflow.set_entry_point("writer")
workflow.add_edge("writer", "tester")
workflow.add_edge("tester", END)

app = workflow.compile()

自己修復ループ:どのように修復されるか

このワークフローが真価を発揮するのは、条件付きルーティングを導入したときです。本番環境ではテストを書くだけでは不十分で、エージェントはテストが有効であることを証明しなければなりません。私はシステムを3つの明確な役割に分割しました。

  • アーキテクト: クリティカルなエッジケースを特定するためにソースコードをスキャンします。
  • デベロッパー: Pytestファイルと必要なモックを生成します。
  • デバッガー: テストロジックまたはソースコード自体のいずれかを修正するためにスタックトレースを解釈します。

add_conditional_edgesを使用することで、グラフはPytestの終了コードを検査します。「FAIL(失敗)」であれば、状態をデバッガーに戻します。「PASS(合格)」であれば、プロセスは終了します。これは単なるスクリプトではなく、毎週約15時間の開発時間を節約する自己修復ループです。

セキュリティのヒント:エージェントのサンドボックス化

早い段階で、AIにローカルでpytestを実行する権限を与えることは、セキュリティ上の悪夢であることに気づきました。誤って(あるいは論理的な帰結として)ファイルを削除してしまう可能性があるからです。これを解決するため、実行環境をDockerコンテナに移動しました。LangGraphノードは一時的なコンテナにコードを送り、テストを実行し、JSON形式のレポートを返します。クリーンで隔離されており、安全です。

基本を超えて:コンテキストとモッキング

実際のコードは煩雑です。Postgresへの接続、Redisのキャッシュ、サードパーティのAPIなどに依存しています。エージェントを効果的に機能させるために、「コンテキスト注入(Context Injection)」を実装しました。

エージェントがコードを書き始める前に、前処理ノードが既存のconftest.pyのフィクスチャと関数シグネチャを抽出します。これらをシステムプロンプトに入力することで、AIはどのモックが利用可能かを正確に把握できます。例えば、db_sessionフィクスチャがある場合、エージェントは新しいデータベースドライバをインスタンス化しようとするのではなく、それを使用すべきだと判断します。

# エージェントにコンテキストを提供する
system_msg = f"""
あなたはリードQAです。
利用可能なフィクスチャ: {active_fixtures}
制約事項: すべての外部呼び出しには 'unittest.mock' を使用してください。
"""

このコンテキストを追加したことで、存在しないインポート(ハルシネーション)が80%減少し、初回試行での成功率が大幅に向上しました。

実戦から学んだ教訓

これをCI/CDパイプラインに統合する場合は、以下の4つのルールを念頭に置いてください。

  1. 繰り返しの制限 (Cap the Iterations): エージェントは「修正→破損→修正」のループに陥ることがあります。私たちはAgentStateの繰り返しを5回までに制限しています。それまでに失敗し続けている場合は、人間が介入できるようにPR(プルリクエスト)にフラグを立てます。
  2. ログの構造化 (Structure Your Logs): LLMにターミナルの生テキストを解析させてはいけません。pytest-json-reportプラグインを使用して構造化データを提供しましょう。モデルが処理する際、その方がはるかにコストが安く、精度も高くなります。
  3. ペイロードの最小化 (Minimize the Payload): 小さなテスト修正のたびに2,000行のファイルを送信するのはトークンの無駄です。私たちは「スニペット抽出器」を使用し、関連する関数とトレースバックで強調された特定の行のみを送信するようにしています。
  4. 人間による介在を維持する (Keep a Human in the Loop): LangGraphのinterrupt_before機能を使用しています。AIが修正案を提示しますが、マージする前に人間が「承認 (Approve)」をクリックする必要があります。

エージェント型ワークフローは、私たちを置き換えるためのものではありません。燃え尽き症候群の原因となる単純作業をなくすためのものです。私の現在のセットアップは、私が眠っている間に定型業務をこなし、ユニットテストを作業からバックグラウンドプロセスへと変えてくれました。

Share: