月曜日にはあなたのテストはパスしました。同じ入力、同じコード、temperature=0。火曜日には失敗しましたが、何も変更していません。アサーションは厳密な文字列をチェックしていましたが、モデルは少し異なる言葉で同じ回答を返しました。テストは赤信号、エージェントは問題なし、そして今あなたは製品ではなくテストスイートのデバッグをしています。
これが、言語モデルを呼び出すものをテストする際の代償です。指示していなくても、出力は変動します。温度をゼロに設定しても、実行ごとにバイト単位で同一の応答が得られるわけではありません。ほとんどの開発者はこのことを一度、大変な方法で学び、その後テスト方法を書き直します。このガイドでは、その下のテキストが常に変化し続ける場合でも有効なアサーションの書き方を紹介します。これは、AIエージェントが本番環境で動作しなくなる理由に関するガイドの、障害モード3の詳細な解説です。
temperature=0が決定論的でない理由
温度は、モデルが次のトークンをどのようにサンプリングするかを制御します。ゼロの場合、毎回最も可能性の高いトークンを選択するため、再現性があるように感じられます。しかし、そうではなく、その理由はモデルの深層にあります。
浮動小数点演算はGPU上で結合法則が成り立ちません。同じ数値を異なる順序で加算すると、小数点以下の最後の桁でわずかに異なる結果が得られます。このごく小さな違いが、どのトークンが最も高いランクになるかを左右し、1つのトークンが異なると、その後のすべてが変わってしまいます。これらの加算の順序は、プロバイダーが他のトラフィックとリクエストをどのようにバッチ処理するか、どのハードウェアが実行するか、その日にどのカーネルバージョンがデプロイされているかによって異なります。あなたはこれらのどれも制御できません。
プロバイダーも自社側で変更を加えます。GPUを交換したり、推論ライブラリを更新したり、重みを再量子化したり、別のリージョンに呼び出しをルーティングしたりします。vLLMの長い議論では、固定シードとtemperature=0でもビット単位の再現性には十分ではない理由が説明されています。要するに、決定論はリクエストで設定するフラグではなく、サービススタック全体の特性なのです。
ですから、同一の出力を基準とすることをやめましょう。モデルは、今回の実行でどのような言葉遣いになろうとも、同じ意味を持つ回答を返します。あなたのテストはそれを受け入れる必要があります。
厳密な文字列アサーションがテストスイートを不安定にする
ここに落とし穴があります。あなたは最初に返ってきたものがそうだったからという理由でassert response == "Your order total is $42.00."と書きます。それはパスします。しかしその後、モデルが「Your total comes to $42.00」と返すと、正しい回答であるにもかかわらずテストは失敗します。
正しい回答で失敗するテストは、テストがないよりも悪いものです。チームは、このスイートが「オオカミ少年」だと学びます。人々はグリーンになるまで再実行し、その後は失敗を読まなくなり、ノイズの中に埋もれた本当のデグレードを見逃します。不安定なテストは時間を浪費するだけでなく、スイート全体の信頼を損ないます。私たちは以前、不安定なテストの原因と、それが広がる理由について書いてきました。非決定論的な出力は、それらを生み出す最も速い方法の一つです。
直感的には、出力をより厳密に固定しようとします。正確な文字列をキャプチャし、スナップショットをとり、それに対して差分をとります。しかし、それは不安定さを悪化させます。なぜなら、変化することが保証されている唯一のものにテストを結合させているからです。あなたは逆の動きをする必要があります。
厳密なテキストではなく、構造と意味に対してアサートする
出力は変化しますが、その根底にある契約は変化すべきではありません。サポートエージェントは払い戻し確認を何百通りもの言葉で表現するかもしれませんが、有効な応答にはすべて同じ事実が含まれています。払い戻し金額、注文ID、ステータスです。フレーズではなく、事実をテストしてください。
それが全体的な考え方の転換です。「モデルは正確にこれを言いましたか?」と尋ねるのをやめ、「応答は正しい形、正しいフィールド、正しい範囲の値を持っていますか?」と尋ね始めることです。これらのプロパティは言葉遣いの変更に耐えられます。真のデグレード、フィールドの欠落、範囲外の数値、不正なペイロードは、依然としてアサーションをトリガーします。これを実践するための戦略を以下に示します。
JSONスキーマに対して応答を検証する
エージェントが構造化データを返す場合、そのJSONスキーマを定義し、すべての応答をそのスキーマに対して検証します。スキーマは特定の値を気にすることなく、型、必須フィールド、許可されたenum、およびフォーマットをチェックします。statusフィールドはrefunded、pending、またはdeniedのいずれかでなければなりません。order_idはあなたのIDパターンと一致しなければなりません。amountは数値であり、文字列であってはなりません。
これは、非決定論的な応答に対して書ける最も強力な単一のアサーションです。なぜなら、モデルがフィールドを削除したり、オブジェクトを誤ってネストしたり、JSONを期待する場所で散文を返したりするなど、致命的な失敗を捕捉するからです。応答スキーマをApidogにロードし、エージェントのライブ応答をそれに対して検証してください。不一致は、400文字の文字列差分ではなく、破損した正確なフィールド名を特定します。
ツール呼び出しが正しい形とターゲットを持っていることをアサートする
エージェントがツールを呼び出すことを決定したら、それを導いた文ではなく、その呼び出し自体をテストします。3つのことをアサートします。正しいツールを選択したか、正しいターゲットを狙ったか、そしてペイロードがツールのスキーマと一致するかです。POST /reservationsを呼び出す予約エージェントは、どのような自然言語の推論がその呼び出しを生成したとしても、guestsを整数として、有効なdateを送信すべきです。
これは、応答ボディを検証するのと同じ規律が、送信リクエストに適用されたものです。必須パラメーターが存在するか、型が正しいか、そして不正なフィールドが紛れ込んでいないかを確認します。AIエージェントのAPI呼び出しをテストするエンドツーエンドの方法では、これらのツールスキーマをキャプチャし、それに対してアサートする方法を扱っています。ツール呼び出しのペイロードには、その周囲の言葉遣いが変動しても、契約が存在します。
厳密な値の代わりに数値範囲を使用する
モデルが生成または通過させるあらゆる数値について、値ではなく範囲をアサートします。ショッピングカートのエージェントは合計を計算します。すべての実行、カート、税ルールにおいて正確な数値は分かりませんが、それが負になることはなく、カートの合計額と最大送料および税金を超えることはないと分かっています。したがって、その点をアサートします。応答に0からその上限までのtotalが含まれていることを。
この単一の範囲は、負の合計、10倍大きすぎる合計、満載のカートでの合計ゼロなど、重要な失敗を捕捉し、あなたが気にしない変動は無視します。範囲は、確信度スコア、アイテム数、トークン使用量、レイテンシ予算、およびあらゆる派生数値に機能します。真のバグで依然として失敗するような、最も広い範囲を選択してください。
必須キーが存在し、禁止フィールドが存在しないことを確認する
2つの安価なアサーションが大きな意味を持ちます。1つ目は、依存するキーが存在し、nullでないことです。2つ目は、決して現れてはならないキーが存在しないことです。サポートチケットを処理するエージェントはresolutionを返すべきであり、顧客にinternal_notesやraw_promptフィールドを漏洩させてはなりません。
存在・不在チェックは、応答の内容ではなく骨格をテストするため、言葉遣いの変更に対して設計上影響を受けません。これらはまた、モデルが本来プライベートに保つべきフィールドを親切に含めてしまうといった、あらゆる種類のプライバシー漏洩に対する最も安価な保護手段でもあります。
自由テキストには意味的チェックと閾値チェックを使用する
ペイロードが散文であっても、テストする必要がある場合があります。厳密な一致は機能しないため、代わりにプロパティをチェックします。応答にはあなたが渡した注文番号が含まれていますか?長さの制限を超えていませんか?ユーザーに決して送信したくないフレーズのデニリストを避けていますか?
本当に意味をテストする必要がある場合、文字列の一致を要求するのではなく、参照回答との埋め込み類似度を比較し、スコアが閾値をクリアすることをアサートします。これらの意味的チェックは、正確なものではなく、大まかなゲートとして扱います。これらは話題から逸れた応答を捕捉します。微妙な事実の誤りは捕捉しないため、上記の構造的アサーションと組み合わせて使用してください。
厳密なスナップショットではなく、範囲のスナップショット
スナップショットテストは、安定した部分をスナップショットする限り、依然として有効です。応答の形状、キーのセット、型、enum値を固定し、自由なフィールドは許容範囲内で変動させます。実際には、スナップショットは厳密なテキストの凍結された塊ではなく、「この応答にはキーa、b、cがあり、bはこの範囲内で、cはこのセットからのものである」という情報を記録します。スナップショットが壊れた場合、それは類義語のためではなく、検討に値する構造的な変更のために壊れたことになります。
状態とメモリがこれをより困難にする
上記のすべては、1つのリクエストに対して1つの応答を前提としています。エージェントはそのようには機能しません。彼らはターンを越えてメモリを保持し、その状態が変動の原因を増やします。
ステートフルなエージェントの回答は、それが何を検索したか、以前何を保存したか、そして以前のターンがどのような順序で実行されたかに依存します。同じ会話を2回実行すると、検索ステップでドキュメントのランク付けが異なったり、ターン2で書かれた要約がターン5での推論に影響を与えたりするため、結果が異なることがあります。これで、モデル自身の非決定論と異なる開始状態という2つの複合的な理由により、出力が変動します。AIエージェントのメモリがどのように機能するかに関する私たちの解説は、その状態がどこに存在し、どのように構築されるかを詳細に説明しています。
このテスト可能性を維持するには2つの習慣があります。まず、制御できる状態を制御することです。各テストの前にエージェントのメモリを既知の開始点にシードし、2つではなく1つのものだけを変化させます。次に、パスに関わらず保持される不変条件をアサートすることです。残高がマイナスになることは決してありません。1つのフライトを予約した会話は、何ターンかかろうとも、正確に1つの予約で終了するべきです。パスに依存しないアサーションは、ステートフルで非決定論的なエージェントでも機能するものです。
テストが繰り返せるように依存関係をモックする
これらのいずれも、ライブのサードパーティAPIに対してリハーサルすることはできません。それらはレート制限を課し、データを変更し、モデルに加えて2番目のランダム性の源を追加します。再現可能なテストを得るためには、テストしている動作ではないすべてのものを固定します。
エージェントが呼び出すAPIをモックし、固定された応答をプログラムします。これで、支払いAPIは常に同じ領収書を返し、検索APIは常に同じ3つの結果を返し、残りの唯一の変動要素はエージェント自身の推論であり、それが観察したいものです。モックされた依存関係を使用すると、健全なAPIではオンデマンドで生成されないエッジケースを強制的に発生させ、エージェントがそれらを処理することを確認できます。Apidogをエージェントの依存関係に向け、安定した制御可能なボディでこれらのモックを立ち上げ、上記のスキーマアサーションと組み合わせます。これは、モックとアサーションが連携するエージェントAIテストというより広範な実践の中に位置付けられます。
Apidogが適合する場所(と適合しない場所)
ツールの役割について明確にしましょう。ApidogはAPIデザイン、テスト、モックのプラットフォームです。エージェントフレームワーク、モデルホスト、エージェントランタイム、あるいは評価および可観測性プラットフォームではありません。あなたのエージェントを構築したり、実行したり、そのステップを調整したり、推論を評価したりすることはありません。
Apidogが所有するのは、エージェントが通信するAPIレイヤーであり、これらのテストが存在する場所です。正直なところ、2つの適合点があります。非決定論的な出力に耐えうるエージェントのAPI応答に対するアサーション(スキーマ検証、応答の形状、数値範囲、必須および禁止キー、ツール呼び出しペイロードの形状)を作成します。そして、テストが2回同じように実行されるようにエージェントの依存関係をモックします。Apidogが埋めるのはこの隙間です。つまり、要求と応答に関する契約であり、それらを生成するモデルではありません。
言葉遣いではなく契約をテストする
非決定論は設定で回避できるバグではありません。それは言語モデルを実行する際の特性であり、temperature=0にしても無効にはなりません。信頼性の高いエージェントを出荷するチームは、それと戦うことをやめました。彼らは、スキーマ、形状、範囲、必須フィールドなど、常に一定であるものをテストし、言葉遣いは変動させます。そうすれば、テストスイートは良い意味で静かになります。テキストが変動してもグリーンを維持し、何かが壊れたときにのみ赤信号になります。
今週、あなたのスイートにある不安定なアサーションを1つ選び、スキーマと範囲のチェックとして書き直してください。Apidogをダウンロードして、エージェントの応答を契約に対して検証し、テストを再現可能にする依存関係をモックしてください。
