APIが400 Bad Requestと{"error": "invalid input"}を返したとき、人間の開発者はドキュメントとペイロードを確認し、欠落フィールドを見つけて数分で修正できます。しかしエージェントは、その情報だけでは行動できません。同じリクエストを繰り返し送信し、最終的にAPIが壊れていると報告してしまいます。
エラーレスポンスは、エージェントが最も依存するAPIインターフェースの一部でありながら、最後に設計されがちです。優れたエラーは、呼び出し元に次の3点を伝えます。
- 何が問題か
- 再試行が有効か、いつ行うべきか
- 次に何を変更すべきか
エージェント側の再試行、バックオフ、サーキットブレーカーについては別途扱います。本記事では、それらのクライアントロジックが正しく動作するために、APIが返すべき情報を説明します。
エラーレスポンスは、多くのAPIで最もテストされていない部分です。Apidogなら、仕様で定義し、モックし、ハッピーパスと同じ場所でエラーをアサートできます。
エラーが答えるべき3つの質問
エージェントが受け取るすべてのエラーは、推測なしで次の質問に答えられるべきです。
これはクライアント側の問題か、サーバー側の問題か?
4xxはリクエストに問題があり、変更せずに再送しても再び失敗することを意味します。5xxはサーバー側の問題であり、同じリクエストでも後で成功する可能性があります。
この区別ができないと、エージェントはバリデーションエラーを無限に再試行したり、一時的な障害で早すぎる段階で停止したりします。
再試行すべきか、いつ再試行すべきか?
4xxにも再試行可能なものと不可能なものがあります。
-
429: 待機後に再試行可能 -
409: 現在の状態を再取得した後なら再試行可能な場合がある -
422: ペイロードを変更しなければ再試行不可
再試行可能かどうかを、ステータスコードから推測させずに明示してください。
何を修正すべきか?
「バリデーション失敗」だけでは役に立ちません。
次のように、対象フィールド、条件、修正方法を返してください。
countryがUSの場合、customer.postal_codeが必要です。
これならエージェントは次の試行で修正できます。
構造化されたエラーフォーマットを使う
独自形式を増やすのではなく、RFC 9457: HTTP APIのProblem Detailsを基盤にしてください。
{
"type": "https://api.example.com/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "The field 'customer.postal_code' is required when 'country' is 'US'.",
"instance": "/v1/orders",
"errors": [
{
"field": "customer.postal_code",
"code": "required_conditional",
"message": "Required when country is US. Provide a 5-digit or 9-digit US postal code.",
"example": "94107"
}
],
"retryable": false,
"next_action": "Add customer.postal_code to the request body and send again."
}
エージェントにとって特に重要なのは、次の4項目です。
-
detail: カテゴリ名ではなく、このリクエストが失敗した具体的な理由を完全な文で示します。 -
errors: 問題ごとの機械可読なリストです。送信ペイロードに対応するフィールドパスを含め、すべてのエラーを一度に返します。 -
retryable: ステータスコードから推測させない、明示的なブール値です。 -
next_action: 次に取るべき行動を平易な文で示します。モデルはエラーコードを推論するより、明示的な指示に従う方が安定します。
問題を一度に1件ずつ返すと、1回の修正が何度ものラウンドトリップになります。すべての失敗をまとめて返してください。
GoogleのAPIエラー設計ガイドも、エラー詳細は散文ではなく構造化されたリストとして提供すべきだという、同様の結論に達しています。
再試行する時刻を伝える
一時的な障害では、いつ再試行すべきかを数値で返してください。30秒待つべきだと分かれば、エージェントは30秒待てます。分からなければ、通常は短すぎる間隔で再試行します。
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/rate-limited",
"title": "Rate limit exceeded",
"status": 429,
"detail": "You have used 1000 of 1000 requests in the current minute window.",
"retryable": true,
"retry_after_seconds": 30,
"next_action": "Wait 30 seconds before sending this request again. Do not retry sooner."
}
Retry-Afterヘッダーは秒数またはHTTP日付を受け入れますが、クライアントにとって扱いやすいのは秒数です。
- 標準クライアント向けに
Retry-Afterヘッダーを返す - モデル向けに
retry_after_secondsをボディにも返す -
next_actionにも待機の指示を書く
この重複は安価で、各コンシューマが読みやすい形式で情報を取得できます。
同じパターンは、メンテナンス中の503やロック中リソースの409にも適用できます。待機が正しい行動であるエラーには、具体的な待機時間を含めてください。
内部情報は漏らさず、空のエラーにもさせない
避けるべき両極端があります。
スタックトレースを返すこと
フレームワークのバージョン、ファイルパス、クエリ断片などが漏洩する可能性があります。これはセキュリティ上の問題であり、モデルのコンテキストも行動不能な情報で消費します。空のエラーを返すこと
ボディなしの500や{"error": true}では、エージェントは再試行するか停止するかを判断できません。
解決策は、安定した公開エラーと相関IDです。
{
"type": "https://api.example.com/errors/internal",
"title": "Internal error",
"status": 500,
"detail": "The order could not be created due to an internal error. No order was created.",
"retryable": true,
"retry_after_seconds": 5,
"request_id": "req_01J8ZK3M2Q",
"next_action": "Retry once after 5 seconds. If it fails again, stop and report request_id req_01J8ZK3M2Q."
}
特に重要なのは、No order was created.という状態の明示です。
書き込み操作が失敗した場合、エージェントは再試行によって重複が生じるかどうかを判断しなければなりません。作成・更新が発生したかを伝えてください。保証できない場合は、操作を冪等にし、その事実をレスポンスと仕様で明示します。
request_idは、人間がログを調査するときの入口になります。IDが実際のログやトレースに解決できるよう、API監視の仕組みと連携してください。
エラーは仕様に含める
OpenAPIドキュメントにエラー形式がなければ、生成クライアント、モック、エージェントツールにとっては存在しないのと同じです。
responses:
'201':
description: Order created
content:
application/json:
schema: { $ref: '#/components/schemas/Order' }
'422':
description: >
Validation failed. Not retryable without changing the request body.
The errors array names each invalid field.
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
'429':
description: >
Rate limited. Retryable. Wait for retry_after_seconds before sending again.
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
これらの説明は装飾ではありません。OpenAPI仕様からエージェントツールを生成する場合、このテキストが失敗時にモデルが読む情報になります。
「再試行可能。まず待機」と書かれた説明は、「リクエストが多すぎます」よりも良い挙動を生みます。
成功だけでなく、エラーもテストする
エラーパスは、発生させる手間があるためテストカバレッジが落ちやすい箇所です。モックを使って、その手間を取り除いてください。
APIプロジェクトで各エラーレスポンスを定義し、モックします。Apidogでは、エンドポイントに失敗レスポンスを追加し、それらを切り替えてモックできます。実際の環境を壊さずに、エージェントを422、429、500に対して繰り返しテストできます。
保存するテストシナリオには、少なくとも次を含めてください。
- 複数フィールドのバリデーション失敗 すべての問題が1つのレスポンスで返り、エージェントが次の試行ですべて修正することを確認します。
-
待機を伴うレート制限
エージェントが連続リクエストを送らず、少なくとも
retry_after_seconds待機することを確認します。 - 書き込み時のサーバーエラー 再試行によってサイレントな重複が作られないことを確認します。
- 認証失敗 待機しても不正なトークンは修正されないため、エージェントが再試行ではなく停止することを確認します。
- 不正なエラーボディ 有効なJSONではない応答に対しても、エージェントが安全に機能低下することを確認します。実際にはアップストリームプロキシがこの問題を起こすことがあります。
これらを保存済みシナリオとしてCIで実行してください。エラー処理は、シリアライザのリファクタリングなどで静かに劣化し、ハッピーパスのテストだけでは見逃されます。
より良いエラーがもたらす価値
無駄な再試行を減らせる
{"error": "invalid input"}を受け取ったエージェントは、同じペイロードを2〜3回再試行してから終了しがちです。各試行にはモデルターンと会話コンテキストのコストがかかります。
欠落フィールドを明示するレスポンスなら、通常は1回の修正済みリクエストで完了します。ルーティンなバリデーションエラーでは、4回の呼び出しと2回の呼び出しの差になります。
人間へのエスカレーションを減らせる
回復不能なエージェントは、タスクを人間に引き渡します。修正方法を示すエラーは、処理を自動化の中で完結させます。
デバッグ時間を短縮できる
人間の対応が必要になっても、request_idと正確なdetailがあれば、ログ調査は1回の検索で始められます。
さらに、同じ改善は人間の開発者にも役立ちます。どのフィールドが間違っているかを具体的に示すエラーメッセージに、不満を持つ開発者はいません。
エスカレーションも設計する
一部のエラーはエージェントだけでは回復できません。
- 必要なスコープがない
- アカウントが閉鎖されている
- 人間の判断が必要なルールに抵触している
この場合、エラーの役割はきれいに引き継ぐことです。何が起きたか、人間が何をすべきか、調査に使える相関IDを返してください。
その情報は、人間が読む場所に届かなければなりません。たとえば、エージェントの実行結果とトレースをタスクに保持し、返信やレビューが必要な項目をInboxへルーティングする仕組みがあれば、ブロックされた実行をログ内の1行ではなく、対応すべき作業として可視化できます。
「無効な入力」だけのメッセージは、レビュー担当者にもエージェント以上の情報を与えません。エラー本文を設計することで、エスカレーションも実用的になります。
エージェントに散文を解析させない
有機的に成長したAPIでは、ステータスコードは正しくても、エラー本文が失敗ごとに異なる自然文になりがちです。
{ "message": "Sorry, that didn't work. Please check your details and try again." }
この形式では、エージェントは推測でしか対応できません。さらに、失敗を200で返すと、クライアントライブラリ、再試行ポリシー、ダッシュボード、アラートは失敗を認識できません。
次の2つを徹底してください。
- 個別の失敗に安定した機械可読コードを付ける
例: 「資金不足」という文ではなく
insufficient_fundsで分岐できるようにする。 - 成功ステータスコードで失敗を返さない
エラーを含む
200は、すべての監視・再試行ロジックから見えなくなります。
エージェントが読めるエラーのチェックリスト
- すべてのエラーでAPI全体に一貫した構造化フォーマットを使う。
-
detailにはカテゴリではなく、具体的なフィールドまたは条件を書く。 - バリデーションエラーでは、フィールドパス付きですべての問題を一度に返す。
- すべてのエラーに
retryableブール値を含める。 - 再試行可能なエラーには、ヘッダーと本文の両方に秒単位の待機時間を含める。
- 書き込み失敗では、何かが作成または変更されたかを示す。
- すべてのエラーに、ログで追跡できる相関IDを含める。
- スタックトレース、フレームワーク文字列、SQLを含めない。
- エラーレスポンスを、エージェントが読める説明とともに仕様へ記載する。
- 各エラーのモックを用意し、保存済みテストをCIで実行する。
エラーはインターフェースです。ますます増えている実際の呼び出し元、つまりレスポンス本文の指示をそのまま実行するモデルのために設計してください。エージェントが本番で初めてエラーに遭遇する前に、エラー形式を定義し、モックしましょう。
よくある質問
RFC 9457と独自のエラーフォーマットのどちらを使うべきですか?
すでに本番環境に一貫した形式がある場合を除き、RFC 9457を使ってください。一貫性は標準化より重要です。エンドポイントの半分だけを新形式にするより、全体で1つの形式を維持する方が良い選択です。
どちらを使う場合でも、retryableとnext_actionの拡張を追加してください。
next_actionをAPIレスポンスに含めても安全ですか?
固定テンプレートから生成するなら安全です。ただし、ユーザー提供コンテンツをこのフィールドへエコーしてはいけません。エージェントが指示として読むため、プロンプトインジェクションの経路になる可能性があります。
バリデーションエラーは400と422のどちらにすべきですか?
壊れたJSONなど、リクエスト自体が不正な形式なら400を使います。リクエストは解析できるものの、ビジネスルールに違反しているなら422を使います。
エージェントは修正方法が異なるため、この区別から恩恵を受けます。すでにどちらか一方を両方の用途に使っている場合は、無理に変更するより明確に文書化してください。
どの程度の詳細が多すぎますか?
呼び出し元が行動できる情報がそろった時点で止めてください。通常はフィールド名、ルール、サンプル値で十分です。内部識別子、クエリ本文、スタックフレームは多すぎます。
エラーメッセージもコンテキストウィンドウにカウントされますか?
はい。再試行のたびに冗長なエラーが蓄積されます。数百トークン未満に抑えてください。成功レスポンスだけでなく、失敗レスポンスもトリミング対象です。
再試行不可能なエラーをエージェントが再試行しないようにするには?
retryable: falseを設定し、next_actionにも停止を指示し、ツールラッパーでも強制してください。モデルの判断だけを唯一のガードにしないことが重要です。


Top comments (0)