エージェントは、読める道具に手を伸ばす
道具は、何ができるかを基準に最適化されます。ところがエージェントは、結果を予測できるか、そして答えが空で返ってきたときにそれを読めるかを基準に道具を選びます。この二つは別の性質であり、その隔たりこそが、より高機能な道具ほど手つかずのまま残りがちな理由です。
動かしているコーディングエージェントには、言語サーバーを与えています。定義へ飛び、シンボルの参照を見つけ、コールグラフをたどれます。それでもたいていの場合、エージェントはそのすべてを無視して grep を走らせます。
しばらくは、それをエージェントが妥協しているのだと受け取っていて、もっと良い道具のほうへ促そうとずっと思っていました。そこで、grep が実際に何を返しているのかを見てみました。grep はより小さな問いに答えますが、エージェントが使える形で答えます。そしてどうやら、それこそがどの道具を呼ぶかを決める性質だったのです。誇りに思っていた能力は、そもそも天秤にかけられてすらいませんでした。
エージェントのために作る道具と、エージェントが手を伸ばす道具は、二つの別ものです。前者は作り手が能力を基準に最適化します。できることが多いほど、見に行ける場所が多いほど、答えが豊かなほど良い、と。後者をエージェントは、作り手が設計していなかった軸で選びます。呼ぶ前に結果がどう見えるか予測できるか、そして結果が空で返ってきたときにそれを読めるか。これは道具選びのすべてではなく、ほかに何が絡むかはあとで戻ってきますが、作り手が能力に一切を注ぎ込みながら無視し続けるのは、こちらの半分です。
このパターン全体には、もっと単純な説明があり、自分の説に寄りかかる前にそれを聞くに値します。grep はモデルの訓練データに、作れるどんなものよりも多く現れ、あつらえの道具はほぼ現れません。エージェントが grep に手を伸ばすのは、読めるからではなく馴染みがあるからで、人気投票を原理のように装っているだけかもしれません。正直に言えば、この二つは絡み合っていて、一度の観察では引き剥がせません。それでも一つの場合には切り離せて、それが見ておくに値する場合です。エージェントが一度も見たことのない、結果が硬く自己記述的な形をした真新しい道具を、出力がどろどろの使い古された道具と突き合わせるのです。馴染みがすべてなら、新しい道具は有名になるまで手つかずのままでしょう。読みやすさが実際に効いているなら、新しい道具はその結果が読めると証明された最初の一度で、手を伸ばしてもらう資格を得ます。この実験を今いる場所からきれいに走らせることはできないので、因果の主張はゆるく握っておきます。ゆるく握らなくていいのは、どちらの半分を自分が握っているかです。すでに訓練されたモデルに、自分の道具をより馴染みあるものにすることはできません。その結果を読めるようにすることはできます。作り手が実際に握っているてこは、まさにこの議論が扱っているものです。
空の grep は一つのことを意味する
grep は何も見つからないと、ステータス 1 で終了し、何も出力しません。それが出来事のすべてです。空の出力には読み方がちょうど一つしかありません。パターンは、検索したファイルの中にない、ということです。一方 grep が見に行けないとき、たとえばパスがない、正規表現が壊れているといったときは、それを stderr に告げて 2 で終了します。つまり空で返ってくる道は二つあり、それらは呼び出し側がすでに確認している同じ小さな場所に、二つの異なる数として届きます。沈黙は不在のために取ってあります。理由は結果と並んで家まで帰ってきます。それこそが、より豊かな道具にはできないことです。
この読みやすさが成り立つのは、終了ステータスを読む呼び出し側に対してだけです。stdout は保持しながらステータスを捨てるハーネスは、grep が避けたはずのあいまいさを自ら抱え直します。情報はそこにあり、それだけでもこの先の道具が約束できる以上ですが、それが数に入るのは、決定の瞬間まで生き延びたときだけです。
より豊かな道具の空の答えは、そうならない
言語サーバーに定義へ飛ぶよう頼むと、答えは一つの場所か、複数か、null です。仕様はこう述べています。「null という戻り値は、結果がないことを示す」。型を読めば、その隙間が見えます。null は、なぜという理由の入る余地のない形です。理由を剥ぎ取られた不在なのです。
仕様はこの点で素朴ではありません。きれいに名づけられる状態のためのエラーコードを持っています。処理中の要求の下で文書がずれたときの ContentModified、起動ハンドシェイクの前に届いた要求のための ServerNotInitialized。仕様がしないのは、コーディングエージェントが実際にぶつかる状態を、そのどれかに押し込むことです。立ち上がっていて、ハンドシェイクは済み、それでもまだインデックスを作成中のサーバー、という状態です。準備のできていないサーバーは、定義の問い合わせに素の null で答えてかまわず、プロトコルはそれを止めません。だから、エージェントが「定義はない」と「見に行く準備ができていなかった」を区別できるはずの事実は、どこかに記録されている保証がありません。エラーオブジェクトを確認しても救いにはなりません。準備のできていないサーバーは、そもそもエラーを上げていないからです。中間の厄介な状態は、null を読む者に降りかかり、そして読まれるその当のものは、自分の原因を運べないのです。
そう見れば、言語サーバーを飛び越して手を伸ばすエージェントは、怠けているのではありません。読めない信号の上に築くのを拒み、読める信号に手を伸ばしているのであって、それは信号についての正しい勘です。ただし仕事についていつも正しいわけではありません。grep はシンボルを、コメント内の同じ名前の文字列と区別できず、スコープを考慮できず、言語サーバーのように安全な名前変更を進めることもできません。そして間違った問いへの読める答えは、自信たっぷりの誤りにすぎません。この手の伸ばし方は、何を信頼できるかについては正しく、仕事が何を必要とするかについては正しくありません。良い作り手は、エージェントにその両方を負っています。
エージェントのために作る道具にも、同じ穴がある
この形は一つ外の層、つまりエージェントのためにわざわざ立てるサーバーにも現れます。Model Context Protocol のツールはコンテンツブロックのリストを返し、既定ではそれらのブロックは構造化されておらず、たいていはひとかたまりのテキストです。仕様は、ツールが形の保証されたものを返せるように、任意の出力スキーマと構造化コンテンツのフィールドを加えましたが、その形はオプトインなので、ほとんどのツールは、モデルが呼ぶたびに一から解析しなければならないテキストを返します。読めないほうは自分で作ったことがあります。毎回正しく、しかも毎回違う並びの段落で答える小さなサーバーで、その答えを読み返すこと自体が、ちょっとした解析仕事になっていました。
失敗については、奇妙なことに、MCP のほうが言語サーバーより正解に近いのです。プロトコルエラーは未知のツールや不正な要求をカバーし、ツール内部の失敗は isError: true として、普通の結果の中に乗って、エージェントがすでに見ている当のその場所に戻ってきます。MCP が開いたままにしているのは、もう半分、つまり形です。フラグは呼び出しが失敗したと告げます。失敗の理由は、ほかのすべてと同じ自由記述のテキストとして届きます。読める信号が、読めない原因を包んでいるのです。能力と読みやすさは別々の投資であり、二つ目が一つ目についてくると当てにしたまま、一つ目に払い続けています。
遅延読み込みでも消えないコスト
これをトークン税の隣に分類したくなりますが、これは別の請求です。あちらは呼び出しの前に、読み込まれた定義のスキーマがひとつ残らずコンテキストウィンドウに居座ることに対して払われます。こちらは呼び出しの後に、結果に対して払われ、あちらへのどんな対処も生き延びます。言語サーバーの定義を遅延させ、エージェントが求めるまで一切コストがかからないようにしても、エージェントはやはりそれを飛ばします。飛ばす理由は、そもそもプロンプト内のスキーマではなかったからです。それは、返ってくるであろう答えでした。
トークン税の記事にも、それ自身の選択の議論があります。十分に大きなカタログは、多くの道具にまたがってモデルの狙いを鈍らせる、というものです。あちらは量の話、込み合った選択肢のあいだの見分けの話です。こちらは単一の道具の結果契約の話で、その道具は、ベンチに並ぶ唯一の道具であってさえ負けます。二つは重なりません。読める形をした道具は、連れがいてもいなくても勝ちます。
二つの読み方を持つ不在に出くわすのは、これで三度目です。ページャーは、失敗したジョブを、わざと止められたジョブと区別できません。ウォッチドッグは、働いているエージェントを、行き詰まったエージェントと区別できません。null は、存在しない定義を、サーバーがまだ見つける準備のできていなかった定義と区別できません。場面は違い、直し方も違いますが、形は一つの形です。不在は自分の原因を運べないので、読み手が見に行く場所にその原因を書き留めておくものが何もないかぎり、読み手には推し量ることしか残らず、二つの原因は同じように推し量られてしまいます。
空の答えに理由を持たせる
だから、エージェントが無視する道具への対処は、最初に提案されるものであることはまずありません。ありがちな勘は、もっと高機能にするか、モデルがようやく用途を理解できるように、より良い説明を書いてやることです。どちらも、それが使われない理由には触れていません。道具が手を伸ばしてもらえるのは、その結果がエージェントに予測できる形を持ち、空の答えが自分の原因を運ぶときです。ただの「何もない」ではなく、どの何もないか、まで。それが、素の null と、自分の空を名指す結果との違いです。
{ "definitions": [], "reason": "not_indexed" }no_definition、not_indexed、out_of_scope を、エージェントがすでに読んでいる当のその場所で読める、それぞれ別個の値として持たせるのです。そうすれば「ひとつもない」と「見に行けなかった」が同じ顔をまとうことは、けっしてありません。理由を、結果を運ぶ通路の中へ畳み込むのです。終了コードが stdout の隣で家まで帰ってくるように。そして、最初のものが言ったとおりの意味だったのかを知るために、エージェントに二つ目のオブジェクトを突き合わせさせたり、推測させたりするのを、やめるのです。それは機能を出荷するより地味な仕事で、しかも道具を呼んでもらえるようにする仕事です。
作った道具は、何ができるかで測られます。エージェントが手を伸ばす道具は、呼んだときに今何が起きたのかをエージェントが見分けられるかで測られます。この二つは同じ道具ではなく、二つ目を作るまで、一つ目はエージェントの読めない形で答え続け、その間エージェントは grep を走らせます。