LLMエージェントにIssue作成をリクエストしたと仮定しよう。回答には「Issueを作成しました」と表示されるが、プロジェクトには新しいIssueが存在しない。再度問い合わせると、エージェントはツールを使用したと説明する。

ここでモデルを切り替えたり、「絶対に嘘をつくな」というプロンプトを追加したりする前に確認すべきことがある。実行プロセスのどの段階まで実際に進行したのか?

エージェントの文だけではその答えは分からない。ツール呼び出しのリクエストが生成されたのか、ランタイムがリクエストを処理したのか、外部サービスに結果が残ったのかを分けて検討する必要がある。

1. 「呼び出した」という言葉には異なる事象が混在している

ツールを使用するエージェントの作業は、おおよそ次のプロセスを経る。

  1. モデルが使用するツールと引数を選択する。
  2. ランタイムが呼び出しリクエストを解析し、権限を確認する。
  3. 実行エンジンが実際の関数や外部APIを呼び出す。
  4. ツールの結果がモデルに渡される。
  5. ユーザーが要求した状態に到達したかどうかを判断する。

各段階は個別に失敗する可能性がある。

モデルが呼び出しリクエストを正しく生成しても、実行エンジンで例外が発生することがある。APIがリクエストを受信しても、非同期処理はまだ実行中の可能性がある。実行は完了したが、誤ったプロジェクトにIssueが作成された場合もあり得る。

そのため、障害調査時には「ツール呼び出し成功」という単一の状態ですべての段階をまとめない方がよい。

2. 一般テキストと呼び出しイベントを区別する

OpenAI Responses APIのカスタム関数呼び出しを例に挙げる。以下は、呼び出し項目の主要なフィールドを簡略化した例である。

json
{
  "type": "function_call",
  "call_id": "call_42",
  "name": "create_ticket",
  "arguments": "{\"title\":\"로그인 오류 점검\"}"
}

アプリケーションはこの項目を受け取り、該当する関数を実行し、同じcall_idに結果を紐付けます。

json
{
  "type": "function_call_output",
  "call_id": "call_42",
  "output": "{\"status\":\"created\",\"ticket_id\":\"T-731\"}"
}

ここでstatusおよびticket_idは、サンプルアプリケーションが定めた結果形式です。OpenAIがIssue作成の有効性を保証する標準フィールドではありません。

この構造は、通常の回答中に現れた以下の文とは明確に異なります。

create_ticketを呼び出してIssueを作成しました。

この文はモデルが生成したテキストです。コードブロック内にツール名や呼び出し形式を記載したとしても、実行エンジンがそれを呼び出しリクエストとして処理するように実装されていなければ、実行は行われません。

カスタム関数はアプリケーションが実行しますが、プロバイダーが管理するサーバーサイドのツールは、実行場所が異なる可能性があります。したがって、使用するツールがどこで実行され、どのようなイベントを残すのかを最初に確認する必要があります。OpenAIの関数呼び出しドキュメントおよびClaudeのツール使用ドキュメントは、この実行境界について説明しています。

3. 症状は同じでも原因は異なる

呼び出しリクエストなしで完了文のみを生成した場合

モデルの出力に一般的なメッセージのみがあり、呼び出しイベントがない場合、モデルが実行の根拠なしに完了を主張したかどうかを確認します。

考えられる原因には、ツールがそのリクエストに対して露出していなかった場合、曖昧なツール説明、呼び出しを選択しなかった場合などがあります。まず、実際のリクエストに含まれるツール定義と、モデルの生出力を比較する必要があります。

ただし、トレーシング画面に記録がないという理由だけで未実行を断定してはいけません。収集漏れやサンプリング、別の実行経路の有無も確認する必要があります。

呼び出しリクエストはあるが、実行エンジンが処理しなかった場合

この場合は、モデルよりもアプリケーションの制御フローを確認する必要があります。

  • レスポンスからテキストのみを読み取り、呼び出し項目を破棄していないか?
  • 複数の呼び出しのうち、最初のものだけしか処理していないか?
  • ツール名と実際のハンドラー名が一致しているか?
  • 承認待ち状態で実行ループが終了していないか?

ストリーミング処理も確認対象です。引数の断片を完了したリクエストのように処理したり、呼び出し完了イベントを見逃したりすると、実行が漏れる可能性があります。response.function_call_arguments.doneは、引数生成が完了したことを示すシグナルであり、外部関数の実行が完了したことを示すシグナルではありません。

実行失敗が成功として伝達された場合

例外処理コードが失敗を消去してしまうこともあります。

python
try:
    result = execute_tool(arguments)
except Exception:
    result = {"message": "처리했습니다"}

このコードは、原因調査のための手がかりまで削除してしまいます。モデルに伝達される結果にも、失敗とその範囲が残っていなければなりません。

MCPにおいても、レスポンスを受信したという事実とツール処理の成功は区別されます。例えば、次のレスポンスはツールエラーを示しています。

json
{
  "jsonrpc": "2.0",
  "id": 17,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "프로젝트에 이슈를 생성할 권한이 없습니다."
      }
    ],
    "isError": true
  }
}

アダプターがcontentのみを伝達し、エラー表示を破棄すると、重要な情報が失われます。MCP Tools仕様では、プロトコルエラーとツール実行エラーを別々に扱っています。

リクエスト受信を作業完了と解釈した場合

デプロイ、ファイル変換、メール送信など、複数段階からなる作業では特に注意が必要です。

デプロイAPIが作業IDを返した場合、それは「デプロイ要求の受付」までが確認された状態である可能性があります。メールサービスがメッセージを受け取ったからといって、受信者の受信箱に到着したことまで確認できたわけではありません。

製品がどの状態を「完了」と呼ぶのかを、ツールごとに定義する必要があります。

4. プロンプトとスキーマが解決できる範囲を区別する

「実行結果を受け取るまでは完了したと言わないでください」という指示は有効です。しかし、実行エンジンの例外処理や途切れたワークフローを復元することはできません。

設定にも適用範囲があります。

  • tool_choice: "required"はツール呼び出しを強制します。
  • strict: trueは、サポートされているスキーマ制約に準拠した引数生成を支援します。

どちらも外部作業の成功を保証するものではありません。形式が正しいプロジェクトIDが実在するか、ユーザーがそのプロジェクトへの書き込み権限を持つかは、別途検証する必要があります。

呼び出しを強制する設定は、必要な段階に適用し、結果を説明する段階では終了できるようにすべきです。すべてのターンで無条件に呼び出しを強制すると、不必要な反復が発生する可能性があります。

5. 完了状態は実行記録から構築する

実務的には、モデルの回答とは別に作業状態を管理する方式が有用です。例えば、次の状態を区別できます。

  • requested: 呼び出しリクエストを受信した。
  • waiting_for_approval: 実行承認を待機している。
  • running: 実行を開始した。
  • succeeded: ツールが成功結果を返した。
  • verified: 定義された完了条件を検証した。
  • failed: 失敗を確認した。
  • unknown: 実行結果をまだ判断できない。

これらの名前は設計例です。重要なのは、状態をモデルに恣意的に宣言させず、実行エンジンと検証コードが実際の事象に基づいて更新することです。

Issue作成の完了条件は次のように定義できます。

  1. 作成APIがIssue IDを返した。
  2. そのIDで参照したIssueが要求されたプロジェクトに存在する。
  3. タイトルなどの必須フィールドが要求と一致する。

すべてのツールに追加の参照が必要というわけではありません。元のレスポンスが何を保証するのか、作業のリスクとコストがどの程度かによって決定すればよいのです。保存直後の参照に遅延が発生するサービスであれば、その特性も考慮する必要があります。

完了バッジなどの重要なUIは、この状態から直接レンダリングする方が安全です。モデルは検証された結果を説明しますが、完了の有無を決定する唯一の主体にならないようにします。

6. タイムアウトが発生したら、再試行の前に確認する

外部APIリクエストがタイムアウトで終了したと仮定しましょう。サーバーはすでにIssueを作成したが、レスポンスのみが失われた可能性があります。

この状態を直ちに失敗として処理し、再作成すると重複Issueが発生します。まずunknownとして記録し、参照やプロバイダーのリクエスト識別子を通じて結果を確認する経路が必要です。

サービスが冪等性キーに対応している場合、同じ論理作業の再試行では同じキーを使用します。実際の保証範囲と保持期間は、サービスのドキュメントに従う必要があります。Stripeの冪等リクエストドキュメントが具体的な例です。

モデルのcall_idと、ビジネス処理の冪等性キーも区別する必要があります。モデルが同じ作業を再度要求すると新しい呼び出しIDが付与される可能性があるため、呼び出しIDのみで重複実行を防ぐ設計では不十分な場合があります。

7. 回答評価に実行の証拠を添える

エージェントのテストでは、回答の文と実際の状態を併せて検証する必要があります。

少なくとも次のシナリオを想定できます。

  • ツールを呼び出さなかった場合、完了を主張しないか?
  • 権限拒否を成功のように説明しないか?
  • ツールエラーがモデルにそのまま伝達されるか?
  • 非同期処理を実行中として表示するか?
  • タイムアウト後に結果確認なしで重複実行しないか?
  • 複数の呼び出しの結果を正しいリクエストに紐付けるか?
  • 誤った対象に作成された結果を完了として認めないか?

トレーシングには、モデル呼び出し、ツールリクエスト、実行開始と終了、エラー、検証結果をリンクする識別子があると望ましいです。OpenAI Agents SDKのトラシングのように、これを収集する機能を活用できます。

ただし、運用ログに秘密情報や個人情報を無条件に記録してはいけません。問題追跡に必要な識別子と状態を優先的に記録し、引数と結果にはマスキングおよびアクセス制御を適用します。

まとめ

エージェントが「呼び出しました」と述べる場合、次の3点を確認しましょう。

  1. 実際の呼び出しリクエストはあったか?
  2. 実行エンジンはどのような結果を記録したか?
  3. その結果はユーザーが要求した完了条件を満たしているか?

これら3つの質問を区別することで、モデルの誤った主張とランタイムの実行失敗をより正確に診断できます。

信頼できるエージェントは、流暢な完了文だけで作られるものではありません。その文を支える実行の証拠と検証経路まで設計する必要があります。