先月、私のエージェントがsearch_invoicesというツールを自信満々に呼び出した。そのツールは存在しない。一度も存在しなかった。モデルが名前を発明し、引数を発明し、そのための完璧な形式のJSONブロックを出力した。信頼していて無邪気なハーネスがそれをディスパッチしようとしてクラッシュした。
エージェントを作るなら、これを見たことがあるはずだ。スキーマにないツールをモデルが呼び出す。または入力スキーマと一致しない引数で本物のツールを呼び出す。または実際のtool_useブロックを出力する代わりに平文でツール呼び出しを語る。これら全てを「ツール呼び出しをハリュシネートする」と呼ぶ。メカニズムのレベルでなぜ起きるかを説明したい。一度理解すれば、対処法が迷信のように感じなくなるからだ。
モデルはtokenを予測しており、あなたのコードを実行していない
始めた時に誰も教えてくれないことだ。モデル側から見ると、ツール呼び出しは関数呼び出しではない。テキストだ。構造化されたテキストではあるが、テキストだ。モデルは段落を出力するのと同じ方法でtool_useブロックを出力する:これまでの全てを前提に最も可能性の高い次のtokenを予測することによって。あなたのスキーマはそのコンテキストの一部だが、あなたがそうしない限り、ハード制約ではない。
ではいつ誤るのか。もっともらしい次のtokenの分布があなたのスキーマがカバーしない場所を指すときだ。会話履歴が請求書への言及で溢れていて、モデルが請求書データが必要だと判断したとき、search_invoicesはとてもあり得るtoken列だ――あなたが渡したツールが実際にはquery_billingという名前であっても。モデルは見てきた全てに対してパターンマッチングをしており、"search_" + 名詞はトレーニングデータで最も一般的なツール名の形の一つだ。
だからハリュシネートされたツール呼び出しは2つの状況でスパイクする:ツール名が珍しいとき、そしてコンテキストが長くノイジーなとき。
修正1:正しい呼び出しを最も可能性の高いものにする
モデルがツールに期待される名前でツールに名前を付けよう。get_weather、send_email、search_database。退屈が良い。退屈は予測可能で、予測可能とはモデルの次のtoken推測が発明された従兄弟ではなく実際のツールに落ちることを意味する。
次に、ツールが何をするかではなくいつ呼び出すかを言う説明を書こう。これは最近のClaudeモデルでより重要だ――Opus 4.7と4.8は4.6よりもツールへの到達が保守的なので、「ユーザーが現在の価格や最近のイベントについて尋ねるときにこれを呼び出す」のような説明は、すべき時の呼び出し率を測定可能に上げる。トリガー条件はモデルが条件付けする部分だ。説明に入れる。システムプロンプトに埋めない。
修正2:悪い呼び出しを通さない
モデルが不正な呼び出しを出力したとき、ハーネスはクラッシュするのではなく、大声で拒否すべきだ。is_error: trueのtool_resultとモデルが読めるメッセージを返そう:
{
"type": "tool_result",
"tool_use_id": "toolu_abc",
"content": "'search_invoices'という名前のツールはありません。使用可能: query_billing, get_customer.",
"is_error": true
}
モデルは次のターンでそのエラーを読んで修正する。Opus 4.8がこの方法で一つのターンでハリュシネートされたツール名から回復するのを見た――エラーを見て、使用可能なリストを確認し、正しい呼び出しを再出力する。失敗した呼び出しを静かにドロップしない。同じプロンプトで再試行して祈らない。エラーをフィードバックする。
修正3:引数問題に厳密なスキーマを
名前のハリュシネーションと引数のハリュシネーションは異なるバグだ。引数に対して――間違った型、欠けた必須フィールド、余分なフィールド――厳密なツール使用を使おう。ツール定義自体にstrict: trueを設定(tool_choiceではない、そちらは何もしない)、additionalProperties: falseとrequiredリストとともに。これでAPIはinputがあなたのスキーマに対して正確に検証されることを保証する。モデルは字義通りに合わない引数の形を出力できない。
一つ注意点:厳密モードは全てと互換性があるわけではない。プログラマティックなツール呼び出し、強制されたtool_choice、MCP toolsとは動かない。それらに対しては自分で検証に戻る。
修正4:呼び出しが依存するコンテキストを短くする
長くてノイジーなトリガーを覚えているか。長いエージェントループでは古いツール結果が積み重なる。ターン40ではモデルはほとんど無関係な39ターンの出力を条件にしており、「どのツール、どの引数」の信号が溺れる。ここでコンテキスト編集が価値を発揮する――古いtool_use結果をクリアして、モデルの次の予測が実際に関連するものを基にするようにする。これはベータ機能(clear_tool_uses_20250919)であり、要約するのでなくクリアするコンパクションとは異なる。ツール呼び出しの精度に関しては、クリアの方が要約より優れている。古いツール出力の要約は依然として分布を押すからだ。
人を驚かせるもの
モデルがツール呼び出しを出力するのではなく語るとき――「今、データベースでこれを検索します」しかし実際のtool_useブロックなし――それは本当はハリュシネーションではない。モデルがターンを早く終了しているのだ。長いClaudeの実行では時々、実際の呼び出しなしに意図の宣言を書く。修正はシステムプロンプトでのナッジだ:ターンを終了する前に、最後の段落がまだ行っていない作業についての約束なら、今すぐそのツール呼び出しでその作業を行うべきだと伝える。シンプルな「続けて」がインタラクティブに回復させる。自律パイプラインには指示を組み込む。
持ち帰ってほしいのはこれだ:これらの修正はどれもマジックではない。全部同じことをする――確率分布を欲しい呼び出しに向けて曲げるか、欲しくない呼び出しをキャッチする。search_invoicesを発明するとき、モデルは誤作動していない。間違った方向を指していたコンテキストで、まさに作られた目的のことをしている。より良く指し示そう。
