Better Models: Worse Tools

Armin Ronacher

より良いモデル、より悪いツール

原文は Armin Ronacher により に公開されました。 このブログを購読する

この2日間、Piの非常に奇妙なissueをきっかけに、すっかり沼にはまっていた。端的に言えば、新しいClaudeモデルがPiのeditツールを呼び出す際に、ネストされたedits[]配列の中に存在しないフィールドを勝手に付け加えてしまうのだ。しかもHaikuのような小型モデルではない。Opus 4.8だ。編集内容自体はたいてい正しいのだが、モデルがでっち上げたキーのせいで引数がスキーマに一致せず、Piはツール呼び出しを拒否して再試行を求めることになる。

モデルが不正なツール呼び出しを吐くこと自体は、とりわけ小型モデルでは珍しくないので、そこまで驚くことではない。驚いたのは、これが新しいAnthropicモデルで悪化していることだ。Opus 4.8とSonnet 5の両方で発生するのに、旧モデルでは一切起きない。言い換えれば、そのファミリーで最先端のモデルが、この特定のツールスキーマに関しては旧モデルより劣っているということだ。

ちなみにFableについて気になる人がいるかもしれないが、意図的にテストしていない。背後で動いているclassifierによって、知らないうちにOpusへダウングレードされている可能性が否定できなかったからだ。

ツール呼び出しはテキストである

LLMのツール呼び出しの内部をあまり深く追ったことがなければ、まず理解すべき重要な点がある。ツール呼び出しは魔法ではなく、かなり原始的なインバンドシグナリングで動いているということだ。モデルは会話の履歴(transcript)とシステムプロンプト、利用可能なツールのリストを受け取る。サーバーはそれらを特殊なマーカートークンを含む巨大なプロンプトにまとめ上げる。モデルはそのフォーマットの事例で学習・強化学習されているため、生成の途中で、APIやクライアントが「この引数でこのツールを呼び出せ」と解釈する何かを出力するのだ。

ファイル編集ツールの場合、想定される呼び出しペイロードは例えば次のようなものだ。

{
  "path": "some/file.py",
  "edits": [
    {
      "oldText": "text to replace",
      "newText": "replacement text"
    }
  ]
}

ハーネスは引数を検証し、編集を実行して、その結果をモデルに返す。検証に失敗すれば、モデルはエラーを目にし、たいていは再試行する。

Anthropicモデルでそのフォーマットが具体的にどう行われているかは公になっていないが、何人かが「ANTML」マーカーを引き出したことがあり、時にはそれが外部への出力に漏れ出ることもある。私の知る限り、上記の呼び出しはモデルからは次のようにシリアライズされて出てくる。

<antml:function_calls>
  <antml:invoke name="edit">
    <antml:parameter name="path">some/file.py</antml:parameter>
    <antml:parameter name="edits">
[
  {
    "oldText": "text to replace",
    "newText": "replacement text"
  }
]
    </antml:parameter>
  </antml:invoke>
</antml:function_calls>

ここで注意すべき重要な点は、これがXMLのように見えても、実際にはXMLではないということだ。単にトークン化して学習させるのに都合が良かったものに過ぎない。もう一つ注意すべきは、基本的なトップレベルの文字列パラメータはインラインで現れるのに対し、オブジェクトの配列はJSONシリアライズで実装されている点だ。これが実際にそう動いているのか、私自身完全に確信があるわけではないが、そう大きく外れてはいないことを示す兆候はある。この点は後で重要になってくる。

このような構造をモデルに生成させる方法は、大きく分けて2つある。

  1. モデルにスキーマに一致する正しいJSONを生成するよう依頼し、後から検証する。
  2. サンプラーを制約し、不正なJSON、あるいは不正なスキーマ形状そのものがそもそもサンプリングされないようにする。

2つ目のアプローチは、一般に文法考慮(grammar-aware)デコーディングや制約付きデコーディングと呼ばれているものだ。サンプラーは文法に違反するトークンをマスクする。モデルが現在JSONオブジェクトの中にいて、スキーマでoldTextnewTextだけが許可されている場合、サンプラーは"in_file""type"が出力されるのを防ぐことができる。文法考慮デコーディングは、構文的に正しいJSONに制約するためにも、特定のenum値やキーを強制するためにも使える。

何の制約もなければ、モデルは単に学習した慣習に従っているに過ぎない。

失敗の内容

Piのeditツールは、1回の呼び出しで複数の完全一致文字列置換をサポートしている。そのため引数にはedits配列が含まれている。失敗するケースでは、モデルは次のようなエントリを生成する。

{
  "oldText": "...",
  "newText": "...",
  "requireUnique": true
}

あるいは次のようなものだ。

{
  "oldText": "...",
  "newText": "...",
  "oldText2": "",
  "newText2": ""
}

繰り返し試行する中で、でっち上げられた末尾のキーが動物園のように次々と現れるのを目にした。typeidkinduniquerequireUniquematchCasein_fileforceMatchCountchildrennotescostoldText2newText2oldText_2newText_2、さらにはeditオブジェクトの中にevent.0.additionalPropertiesというキーまであった。

最も腹立たしいのは、私が調べた不正な呼び出しにおいて、実際のoldTextnewTextのペイロードはバイト単位で正確だったことだ。モデルは実際には正しい呼び出しを生成していたのに、オブジェクトの末尾にナンセンスを付け加えていたのである。

この失敗は、文脈への依存性も非常に高い。「このファイルを編集して」といった真新しい単発のプロンプトでは、私の環境では全く再現しなかった。モデルがファイルを読んで問題を診断し、その上で複数行にわたる編集を組み立てるようなエージェント的な履歴があると再現できた。さらに厄介なことに、すべての履歴でこの挙動が出るわけではない。実際、私はPetr Baudis氏のトランスクリプトがなければ、そもそも再現すらできなかったのだ!そのユーザーのセッションでは、セッションを継続するとOpus 4.8が約20%の確率で失敗した。履歴からthinkingブロックを取り除くと失敗率は半分になった。strictなツール呼び出しをオンにすると、私の実行環境では失敗は完全に消えた。

なぜ悪化しているのか

私の最有力仮説は、これがランダムな劣化ではなく、学習の副産物だというものだ。

古いAnthropicモデルが学習された当時は、いくつかのツール(その一部は文書化されていた)で学習されていた。しかし、その学習にはClaude Codeのようなユーザーに配布されるハーネスが明確なターゲットとしては存在していなかった。現代のAnthropicモデルが異なるのは、そのポストトレーニングにClaude Code、あるいはそれに酷似したハーネスが含まれている可能性が高いからだ。モデルは、その環境で成功するツール呼び出しがどう見えるかを学習する。そして、その環境で許容されるミスが何かも学習する。

Claude Code自身のツールは比較的フラットだ。通常のeditツールはPiのようなネストされたedits[]の形ではなく、file_pathold_stringnew_string、そしてオプションのフラグ(replace_all)に近い形をしている。Claude Codeのクライアントを見ると非常に示唆に富む。不正なツール使用に対するリトライパス、パラメータのエイリアス、型の強制変換、Unicodeの修復、未知のキーのフィルタリングなどが含まれている。言い換えれば、Anthropic自身のクライアントは、かなりの量のゆるさ(slop)を想定して受け入れ、ほとんどは黙って修復しているように見える。

もし強化学習がそのようなハーネス、あるいはそのシミュレーションの中で行われるなら、わずかに不正なツール呼び出しでもタスクを完了して報酬を得ることができる。ハーネスがエラーを完全に吸収してしまうため、エイリアスをでっち上げたり、余計なフィールドを追加したり、近いパラメータ名を使ったりすることに対する勾配はほとんど生じない。

さらに悪いことに、モデルはClaude Codeの正規のeditツールの形状に非常に強く適応してしまう可能性がある。別のハーネスが、意味的には同じ意図を持ちつつスキーマだけが異なるツールを提示することもある。そうしたツールは、分布からどんどん外れていくことになる。よりよく学習されたモデルほど、事前分布が強い分、かえって強く抵抗してくるかもしれない。

これはそこまで驚くことではないが、数ヶ月前からの変化ではある。Opus 4.5が登場したときは、他のeditツールにも非常によく適応していた。実際、指示さえ良ければ、モデルはどんなツール形状にも適応していく良い道筋にいるのだと、私はかなり確信していた。

今は、この先の行く末についてやや心配している。代替のツールスキーマは、単に馴染みがないというだけではなく、一つの寛容なツール生態系に最適化されたポストトレーニングによって、暗黙のうちに罰せられている可能性がある。そしてその生態系は文書化されていない。text editor toolという文書化されたものは存在するものの、実際にはClaude Codeはそのフォーマットに従っていないことがわかるだろう。Claude Codeが内部で何をしているか(それはクローズドソースのハーネスだ)は、外部からは見えないのだ。

ゆるいハーネス

Claude Codeはもちろんクローズドソースだが、minifyされたコードを見ることで、何をしているのかある程度推測できる。そして正直なところ、入力データに対して非常に寛容だ。

まず、Claude Codeはモデルの可視テキストの中に漏れ出した<invokeマークアップがないかチェックする。それが発生した際にはテレメトリも送信し、独自のステートマシンでモデルに押し戻してそのような不正な呼び出しをリトライさせる。

文字列値の中の壊れた\uXXXXシーケンスや単独のサロゲートを修正する、明示的なUnicodeエスケープ修復機能も備えている。ツールごとにパラメータのエイリアスも持っている。例えばEditは、old_str(おそらくモデルが公式に文書化されたtext editor toolで学習されていた頃の名残)、スキーマ上の新しいold_stringnew_str/new_stringfile_pathのエイリアスとしてのpathなどを受け付ける。

さらに、予期しないキーを黙ってフィルタリングするし、strictモードも使っていない。strictモードの問題は、Anthropicがツール定義に複雑さの制限を課しており、それがAPIリクエストを失敗させることにある。おそらくそれが、Claude Codeがstrictを使おうとしない理由だろう。

Strictness

この問題は他のハーネスでも付きまとうのだろうか。Anthropicの大きな問題の一つは、モデルが完全にクローズドであることに加え、ハーネスもクローズドだという点だ。Codexモデルもクローズドだが、少なくともハーネスはそうではない。さらに、少なくとも多少興味深いgpt-ossもある。モデルはOpenAIのharmonyレスポンスフォーマットを使うように明示的に学習されており、OpenAIの人々がこれについてどう考えているかを少なくとも教えてくれるドキュメントも豊富にある。

Harmonyでは、チャネルやツール呼び出しのコンテンツタイプがプロンプトフォーマットの一部となっている。関数呼び出しは例えば次のようになる。

<|start|>assistant<|channel|>commentary to=functions.get_weather
<|constrain|>json<|message|>{"location":"San Francisco"}<|call|>

重要なのは<|constrain|>jsonの部分だ。モデルはこのメッセージボディがJSONであることをインバンドで表現でき、推論スタックはその境界を利用して、ツール呼び出しのボディに対してJSON制約付きサンプリングに切り替えることができる。おそらくAnthropicのモデルでも、少なくともstrictモードでは、同様のことが多少は行われているのだろう。

harmonyにおけるこのマーカーは、サンプラーが特定の文法でサンプリングすべきタイミングを検出するのに役立ち、トランスクリプトの一部であるため、それを非常に容易にしている。ホストされたGPTモデルでは、このようなものに準拠する必要があるカスタムツール向けに、LARK文法を提供するオプションもある。

Anthropicはそれとは異なるように見えるが、完全にそうとも限らない。オブジェクトの配列がJSONとして表現されているとすれば――実際そう見えるのだが――モデルはツールパラメータの中でJSONを書かなければならない。おそらく基本的な文法制約付きサンプリングが行われており、それが余計なキーの一因を部分的に説明しているのかもしれない。ネストされた配列パラメータの場合、そのJSONには1つのタグの中に、文字列リテラル内のエスケープされた複数行のファイルコンテンツが含まれる。予期しないでっち上げのキーは、まさにそのタスクで最もエントロピーが高い地点、すなわち数百トークンに及ぶエスケープされたnewText文字列を閉じた直後、モデルが}, "..."かを判断しなければならない箇所で現れる。

Opus 4.8とSonnet 5は、editツール呼び出しがどうあるべきかについて、はるかに強い事前分布を持っているように見える。そしてその事前分布はClaude Codeのeditスキーマ、すなわちフラットなold/newの文字列ペアにオプションのreplace_allフラグが付く形であるように見える。私の推測では、Opusは編集操作には追加のオプションフィールドが1つありうることを学習しているが、PiのネストされたoldText/newText形状の下では、そのフィールドに対して学習された名前を持っていない。だから毎回もっともらしい名前を新たにサンプリングし、その結果、失敗時には1つの安定したエイリアスではなく、数十種類のランダムなキーが生成されるのだ。

strictモードがこれを修正するように見えるので、サーバー側ではJSONスキーマ構造で許可されていないキーのサンプリングを拒否しているのだと推測している。それは、strictモードが有効なときにツール定義の複雑さに制限がある理由も説明するだろう。

これまでのところ、私がテストしたCodexモデルではこの種の退行は見られなかった。まだアクセスできない5.6を除く、利用可能なすべてのモデルをテストした。

ハーネスにとっての意味

厄介な教訓は、ツールスキーマは中立ではないということだ。少なくともAnthropicモデルにおいては。私たちは、スキーマは抽象的な契約であり、モデルはそれに従う汎用的な推論者であると装いがちだが、一部のツールに関しては、もはやそうではないのかもしれない。

ツールスキーマは分布の中のどこかに位置しており、ポストトレーニングでモデルが見たものに近い形状もあれば、遠い形状もある。プロバイダーの隠されたエンコーディングにとって容易なもの(例えばANTMLにおけるトップレベルの属性)もあれば、長い複数行文字列の後にネストされた配列の中で大きなエスケープされたJSONオブジェクトを書くことをモデルに要求するものもある。モデルはスキーマを理解するほど賢くても、プレッシャーの下で正確な形状をサンプリングするのは下手かもしれない。

この種のモデル挙動が続くなら、ハーネスにとってどのような示唆があるのだろうか。もちろんAnthropicでstrictサンプリングをオンにすれば問題は解消するはずだ。一方で、モデルがこのような挙動を示すこと自体が、強化学習がモデルに与える影響の大きさを示している。その事前分布に逆らうのは、最高のモデル性能を引き出したいのであれば、おそらく無駄なことだろう。

現状、Claude Codeはオープンソースではなく、彼らがRL環境で何をやっているのかも実際には知ることができない。ツールが近い形で一致しない限り、Claude Codeで学習された挙動が自分のツールにきれいに転移するとは想定できない。支配的な一つのハーネスの中でポストトレーニングが進めば進むほど、他のすべてのハーネスはその癖を継承せざるを得なくなる。

私は以前、制約付きデコーディングには品質面のトレードオフがあるため、厳格な文法制約付きツール呼び出しに対してより懐疑的だった。一般的には今でもそれが当てはまる可能性はあると思うが、このバグによって私の考えは大きく変わった。最新のモデルがタスク解決は上手くなりつつ、代替のツールスキーマを忠実に出力する点では下手になっているのであれば、ハーネスはどこかでより強い保証を必要とすることになる。

さらに詳しく知りたい、あるいは議論したい場合は、Piトラッカーのissueを読んでみてほしい。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント