この記事は Products not Projects の観点からエージェントのテスト戦略を掘り下げます。「作ったら終わり」ではなくサービスを継続的に運用・改善するには、エージェントの振る舞いを再現可能な形で検証できなければなりません。

非決定性というテストの難しさ

エージェントのテストで最初に直面するのは非決定性の問題です。同じ入力を LLM に渡しても、出力は毎回変わります。温度パラメータを 0 にしても、モデルのバージョンが変われば変化します。自然言語の出力を assertThat(response).contains("おすすめ") で検証しても、フレーズが変わっただけで落ちます。

かといって「LLM を呼ばないから Agent のテストはしない」という判断は問題を先送りにするだけです。ツール呼び出しの順序が正しいか、structured output のフィールドが期待通りか、skill 発動のロジックが機能しているか、これらはコードの振る舞いであり、テスト対象です。

テスト可能なモデル境界を設ける

food-delivery-demo が採用したのは、LLM を差し替え可能な境界として設計するというパターンです。本物の LLM プロバイダーと同じ Model インターフェースを実装した決定論的なモデルをテスト用に用意し、プロパティの切り替えで DI コンテナが差し替えます。

この決定論的モデル(DeterministicModel)は、渡ってきたメッセージの内容を見て固定シーケンスを返します。実際の LLM は一切呼ばれません。LLM に任せている「自然言語の解釈と判断」の代わりに、テストシナリオとして意図した一連のツール呼び出しと構造化出力を返す、という役割です。エージェントを持つ全サービスが対応する DeterministicModel を持ち、CI はすべてこのモデルで実行します。

Agent は Model インターフェース越しに LLM を呼ぶ。本番は BedrockModel、テストは DeterministicModel に DI で切り替えます。

graph LR
  Agent["Agent\n(EventLoop)"] -->|uses| Interface["<<interface>>\nModel"]
  Interface -->|本番| Bedrock["BedrockModel\nAWS Bedrock を呼ぶ"]
  Interface -->|テスト| Det["DeterministicModel\n固定シーケンス · LLM なし"]
  Props["application.properties\nmodel.mode=deterministic"] -.->|DI で切り替え| Interface

ツール呼び出しシーケンスを固定する

DeterministicModel は、渡ってきた会話履歴を見て「次に何を呼ぶか」を決めます。スキル発動の確認→カタログ検索→合計計算→構造化出力という順序を、メッセージ状態を確認しながら一ステップずつ返します。

この「次に何を返すか」の判断は、どのツール結果がすでにメッセージ中にあるか、スキルが発動済みかといった状態で決まります。実際の LLM の判断を、固定的な条件分岐で模倣するアプローチです。

テストが検証するのは「何を呼んだか」と「何を返したか」

各サービスのテストは、実際に HTTP リクエストを送る形で動作します。確認する点は次のとおりです。

  • 正しいエージェント名でレスポンスが返ること
  • 下流サービスへのリクエストが正しいエンドポイントに飛んでいること(capability クエリ経由の discover を含む)
  • structured output が期待した型・フィールドで返ってくること
  • メトリクスが正しく記録されていること

自然言語のフレーズは一切検証しません。「おすすめ理由が自然な日本語かどうか」はテストしません。エージェントがツールを正しい順序で使い、型として定義された出力を正しいフィールドで返したか、を確認します。

スキル発動条件のテストと、契約ドリフトの検出

サービスは system prompt を動的に組み立てます。スキルドキュメントに書かれた発動条件をパースして、システムプロンプトのスキル発動セクションを生成する処理です。この動的な組み立て結果が DeterministicModel に渡るため、スキルの発動条件を変えたときにエージェントの振る舞いが変わることをテストとして表現できます。

また、ツールの入力スキーマ(フィールド名と型)と、DeterministicModel が生成する入力マップのずれも検出できます。フィールド名を変更したとき、契約(ToolSpec の inputSchema)と実装の間のドリフトをこのテストが検出します。型として定義された境界を DeterministicModel が通過することで、ツール契約の回帰テストになる、という構造です。

非決定性とどう向き合うか

DeterministicModel パターンが解くのは「ツール呼び出し構造のテスト」です。LLM の判断そのものは対象外です。

この分担は意図的です。エージェントに期待するのは「曖昧な自然言語を解釈して適切なツールを選ぶ」という振る舞いです。しかし、どのフレーズを使うかは LLM に委ねます。テストが守るべきなのは、ツールの呼び出し契約structured output の型とフィールドです。

実際の LLM との接続は、Bedrock を使ったインテグレーションテスト(*IntegrationTest)に分離します。こちらは CI では常時実行せず、AWS 環境があるときだけ走らせます。非決定性を含む検証はそこに委ねることで、ユニットテストとインテグレーションテストの役割を分けられます。

この設計はつまり「LLM の判断を信頼する範囲を限定し、コードで制御できる部分はコードでテストする」という考え方です。エージェントを業務システムに組み込むとき、テストの信頼性を確保するためにこの分担線は不可欠です。

この断面を含む探索全体の評決は Conclusions にまとめています。