エージェントに、すべてのツールをコンテキストへ載せる必要はない
DynoTable の AI エージェントは 38 のツールに到達できます。しかし、そのすべてを一度に見ることはめったにありません。モデルが即座に手にするものと、自分で探しに行かなければならないものとの間にどう線を引くか — それがツールキット全体で最も影響の大きい決定であり、そしてその線は、最初に引いた場所とはまるで違うところに落ち着きました。
私たちは、モデルが小さなセットから始めて残りを検索するように、ディスカバリの仕組みを作りました。そして安価なモデルがそれを使う様子を観察し、38 のうち 27 のツールを常に見えるコアへ戻しました。仕組みそのものは生き残りました。誰がそれを必要とするかについての私たちの理屈は、生き残りませんでした。
以下は、探しに行くことを信頼できないモデルのためにツールの表面を設計して学んだことです。
モデルが一度も呼ばないツールにも、コストはかかる
公開するすべてのツールは、その名前、説明、そして完全な入力スキーマであり、ユーザーが何かを入力する前にリクエストへシリアライズされます。それが 38 個あるのは、ただではありません。
トークンは請求書の小さいほうの半分です。本当のコストは選択の正確さです: モデルが走査するほぼ同一の選択肢が増えるほど、間違ったものを選ぶ頻度が上がります。そして私たちのカタログは、ほぼ同一の選択肢で意図的に埋まっています。5 つのツールは二重に存在します — openTable と proposeOpenTable、openWorkbench と proposeOpenWorkbench、といった具合に。各ペアは同じことをしますが、一方は即座に実行し、もう一方はユーザーが先にクリックするチップを出します。この区別は の安全性にとって決定的でありながら、名前のフラットな一覧の中ではほとんど見えません。
自身のクライアント向けガイダンスも、まさにこの点から始まります: すべてのツール定義を前もって読み込むのはトークンの無駄で、レイテンシを増やし、モデルの性能を劣化させる、と。それに同意するのは簡単です。どのツールが席を失うのかを決めるところから、面白くなります。
検索ツールを作った。下限のモデルは、それを呼ばなかった。
仕組みは 2 層です。あるツールの集合は最初のステップから有効です。残りは、モデルが searchTools(query) を呼ぶまで不可視で、この呼び出しは名前・説明・キーワードでカタログをスコアリングし、一致したものを返し、以降のステップでモデルが呼び出せるツールの集合へそれらを追加します。
そして、これを私たちの下限モデルに対して走らせました。このエージェントをフロンティアモデルに合わせてチューニングすることはありません — これはあなた自身の Bedrock 認証情報の上で動くので、人々は安価なモデルを選びます。だから私たちは最も安いものに向けて最適化します。添付ファイルについて尋ねたところ、そのモデルは代わりに開いているタブの一覧を漁りに行きました。検索ツールをそもそもめったに呼びませんでした。直接見えないものは、そのモデルにとって存在しないのです。
この結果は、当たり前の設計を殺します。ディスカバリがツールへの唯一の経路なら、そのツールを必要とするすべてのリクエストは、モデルが探しに行くことを選ぶかどうかに依存します — そして助けを最も必要とするモデルこそが、最も助けを求めないのです。
そこで分割は「小さなコアと大きなテール」であることをやめ、ツールではなくリクエストについての問いになりました: ユーザーの言い回しは、そのツールを名指ししているか? ディスカバリ側に残した 11 のツールは、その答えが「はい」であるものです。「これを CSV にエクスポートして」は、モデルに export を検索させます。「先月の注文を見せて」は、フィルタ設定のツールを検索させたりはしないので、それはインラインに留まります。インデックスの統計、保存されたスペック、リレーションシップのイントロスペクション、そしてステージングされた変更の各表面は、いずれもユーザーが必要なときに名指しで求めるものであり、暗黙に呼ばれることは決してありません。
インライン 27 個は、事前に擁護できたはずの数字ではありません。実際に出荷相手にしているモデルとの接触を生き延びた数字です。
ディスカバリを静かに無意味にしていたはずの競合
ディスカバリには、間違えやすく、気づきにくいタイミングの制約があります。
検索ツールが一致を返したとき、その名前はモデルの次のステップが準備される 前に 許可セットへ加わらなければなりません。それを行う場所として当たり前に思えるのは、ステップ完了時に発火するフックです。ところがそのフックは、一部の SDK バージョンでは次のステップの準備の後に発火すると文書化されています — つまり変更が 1 ステップ遅れて着地するということです。
その失敗の仕方が厄介です。モデルは検索します。必要なツールを名指しした正しい結果を得ます。まさに次のステップでそのツールを呼び、そんなツールは存在しないと言われます。断続的で、解決された SDK のバージョンに依存し、そして壊れたハーネスではなく愚かなモデルのように見えるのです。
修正は、許可セットの変更を検索ツール自身の実行の内側で行うことです。そこはループが進む前に完了することが保証されています。文が置かれる場所の 1 行分の違いでしかありませんが、それが、動くディスカバリの仕組みと、誰も正しく原因を突き止められない理由で一定の割合で失敗する仕組みとの違いです。
検索は 3 回まで、そこで止める
検索はエージェントの 1 ターンあたり 3 回に制限しています。4 回目は実行される代わりに、これを返します:
{"error": "search-budget-exhausted", "budgetCap": 3}この上限が存在するのは、特定のループのためです: モデルが検索し、想像していたものが見つからず、同義語でもう一度検索し、それも見つからず、データベースに一度も触れないまま検索ツールの内側でステップ予算を使い切る。上限を設けることで、これ以上の検索が割に合わなくなった時点で決断を強制します — すでに見つかったツールのどれかに踏み切るか、ユーザーに尋ねるか。
まだディスカバリしていないツールをモデルが呼んだときのエラーメッセージは、エージェント内のすべてのバリデータに用いているのと同じ原則に従います:
Tool 'startExport' not in active set. Call searchTools(query='startExport')
to discover it, or use one of: <inline tool names>回復の手段を名指しする拒否は、余分に 1 ステップかかります。ただ「だめ」と言うだけの拒否は、ターンそのものを失わせます。
ツールごとに 1 行、あとはすべて導出する
各ツールは 1 つのフラットなリストに一度だけ宣言され、その行がツールのアイデンティティ全体を担います: 名前と説明、検索が一致するキーワード、インラインで始まるかディスカバリ側か、どのティアで動くか、そして MCP 上でどう公開されるか。
このティアは、可視性の分割と同じくらい重要です。21 のツールはサイレント — 誰も邪魔せずに走る読み取りです。16 は認可のはしごの背後でゲートされています。ちょうど 1 つはそのどちらにも属しません。検索ツールは、エージェントがあなたのデータに対して使うケイパビリティではなく、ループそのものの一部だからです。MCP の公開は同じ行の第 3 の軸です: read-only、staging、full、あるいは完全に除外 — 3 つのツールがそれに当たります。
これを誠実に保っているルールは、システム内の他のすべてのリストがそれらの行から導出されることです — サイレントティアの集合、MCP のスコープティア、書き込みスコープの集合 — そしてそのどれも手で保守されていません。手書きのサイレントリストの隣に手書きの MCP リストがある、というのはまさに、チャットでは正しくゲートされているツールが外部クライアントには静かに開放されてしまう経路です。
予想していなかった制約は、この宣言リストがランタイムの import をゼロにしなければならないことです。これはデスクトップ UI とバックエンドで共有されており、たった 1 つの import が、あるツールの実装を経由して推移的に Node 専用の暗号依存へ到達します。それをブラウザのバンドルへ引き込むと、アプリはモジュールのロード時に落ちます。型チェッカーもユニットテストもこれを捕まえません — どちらも import を平然と解決します。捕まえるのは、そのファイルをテキストとして読み、import 文が 1 つでもあれば失敗するテストです。粗雑に感じられますが、それは最初に救われるまでの話です。
あなたが同じものを作るなら、何が持ち帰れるか
- アーキテクチャを擁護する前に、ツールの数を数えること。正しい分割は原則ではなく計測結果です。
- ディスカバリは最も弱いモデルに対してテストすること。フロンティアモデルは検索すべきときに検索します。それはユーザーが選ぶモデルについて何も教えてくれません。
- 可視性は、ユーザー自身の言い回しがそのツールを名指しするかどうかで決めること。暗黙に呼ばれるツールはインラインに属し、人々が名指しで求めるツールは見つけてもらえます。
- 順序に敏感なものをステップのフックに入れる前に、フレームワークのフックが実際にいつ発火するかを確認すること。
- メタツールには上限を設けること。実際の状態に触れずに何度でも呼べるものは必ず何度も呼ばれ、検索に費やされたステップ予算はターンの浪費です。
- 未ディスカバリのツールのエラーには、他のバリデータのエラーと同じく、回復のための呼び出しを名指しさせること。
- 各ツールを一度だけ宣言し、他のすべてのリストはそこから導出すること。同じツールについて手で保守されたリストが 2 つあれば、いずれ食い違い、その食い違いはセキュリティ境界で表に出ます。
- コンパイラが表現できない決定的な制約をモジュールが抱えているなら、それをテキストとして強制する粗雑なテストを書くこと。
これが動いている場所
以上はすべて DynoTable のツールカタログの中で出荷されています — あなた自身の 認証情報の上で動くスキーマ対応のクエリ、そして書き込みは常にレビュー可能なステージングエリアにしか着地しません。同じ宣言が、外部エージェントの接続先となる MCP サーバーも駆動しており、そこでは各行の公開ティアが、外部クライアントに与えられるスコープになります。それをどう安全にしたか — OAuth、同意、認証情報の分離 — は別の話です。
そしてそのすべての下の層、これらのツール一つひとつを安価なモデルでも扱えるものにしているバリデータについては、それ自体が 1 本の記事になっています。