# 複数ツール動的読み込み機構の設計 ## Context Yoi はエージェントが扱うツール数の増加 (built-in tools + MCP サーバ + ユーザ定義) を想定する必要がある。すべてを upfront に context へ展開すると以下が問題になる: - **入力トークン消費**: 30-50 ツールで 10-20K tokens を消費しうる (Anthropic 公式ガイド) - **ツール選択精度の低下**: 数十個を超えるとモデルの tool selection accuracy が落ちる - **KV cache 効率**: ローカル推論では prefill コストが重く、prefix が動くと再計算が走る Claude Code が採用する deferred tools 機構 (`docs/ref/claude-code-deferred-tools.md`) と OpenAI Harmony のアプローチ (`docs/ref/tool_approach_comparison.md`) を比較すると、**全モデルで同じ deferred 方式は通用しない**。モデルファミリごとに戦略を切り替えられる抽象が必要。 ## 決定事項 ### 二層分離: Registry / ContextRenderer ツール抽象を以下の二層に分ける。両者の責務を厳密に切り離す。 | 層 | 責務 | 単一の真実 | |---|---|---| | Registry | ツール実装・schema・名前解決・引数バリデーション | 全ツール常時登録 | | ContextRenderer | モデルへ渡す prompt にどの tool 定義を、どの形式で、どこに置くか | モデル戦略ごとに差し替え | **重要**: バリデーションは **Registry 側で実引数 vs 登録 schema の照合**だけで行う。「context にスキーマテキストが現れているか」は検証条件にしない (Claude Code 実演で確認済み: `claude-code-deferred-tools.md` §10)。これにより、ContextRenderer がどんな戦略で schema を見せていようと、registry の真実性が一本化される。 ### 戦略 (RenderStrategy) のモデル系統別マッピング | モデル系統 | 戦略 | 根拠 | |---|---|---| | Claude 系 (Anthropic API + ローカル Anthropic 互換) | **Deferred** + ToolSearch 相当 | XML+JSON のテキスト表現で tool_result からも schema 注入可。prefix 安定 | | OpenAI 系 (gpt-oss / Harmony / Responses) | **Upfront** + MCP-style dispatcher | namespace ブロックが構造化されており、後追い注入が訓練分布外。汎用 dispatcher で外部解決 | | Hermes / Qwen / Llama 等独自系 | **Upfront** または **Rolling Developer Message** | モデル個別 chat template に従い、必要なら境界で書き換え | ### 戦略を決める軸 `RenderStrategy` は以下の組み合わせで表現: - **配置**: `SystemPrompt` (固定) / `DeveloperMessage` (cache 境界) / `ToolResultStream` (Claude 流注入) - **発見手段**: `AlwaysVisible` / `MetaTool { search, describe }` / `Static` - **変更時の cache 影響**: `PrefixStable` / `RewindToBoundary` Claude 系 = `(ToolResultStream, MetaTool, PrefixStable)`、OpenAI 系 = `(DeveloperMessage, Static, RewindToBoundary)` または `(SystemPrompt + dispatcher, AlwaysVisible, PrefixStable)`。 ## 設計詳細 ### Registry インターフェース ```rust pub trait ToolRegistry { fn list(&self) -> Vec; // 名前+description のみ fn schema(&self, name: &str) -> Option<&ToolSchema>; fn dispatch(&self, name: &str, args: Value) -> Result; } ``` - `list()` は常に全ツール返す (戦略は ContextRenderer 側の責務) - `schema()` は ToolSearch 相当の動線で使用 - `dispatch()` は schema 照合+実行。**context に schema text があるかは見ない** ### ContextRenderer インターフェース ```rust pub trait ContextRenderer { fn initial_render(&self, registry: &dyn ToolRegistry) -> InitialContext; fn on_tool_load(&self, name: &str, registry: &dyn ToolRegistry) -> Option; fn parse_call(&self, raw_output: &str) -> Result; fn format_result(&self, name: &str, result: &ToolResult) -> String; } ``` 戦略ごとに実装を差し替える: - `ClaudeDeferredRenderer`: 初期 prompt に core tools のみ展開、`tool_search` メタツールを常設、ロード時は tool_result として `{schema}` を流す - `HarmonyUpfrontRenderer`: developer メッセージに namespace で全 tool 展開、ロード概念なし - `HarmonyDispatcherRenderer`: namespace は `call_mcp(server, tool, args)` だけ、サブツール解決は外部 MCP - `RollingDeveloperRenderer`: 一定境界 (compaction 等) で developer メッセージを再描画。cache 損失は境界で吸収 ### Validation / Retry レイヤ ツール呼び出しの失敗ハンドリングは ContextRenderer / Registry の上に置く独立層: ```rust pub struct ToolDispatcher { registry: R, renderer: C, retry_policy: RetryPolicy, } ``` 責務: 1. モデル出力をパース (`renderer.parse_call`) 2. registry で schema 照合 → invalid なら error tool_result を返す 3. dispatch → 結果を `renderer.format_result` で整形してモデルへ 4. malformed 出力時は error フィードバックして同一ターン内修正を促す これは `tool_approach_comparison.md` §4 で議論した「プロバイダ側がやっている (フォーマット規約 / バリデーション / リトライ / 訓練投資)」のうち、**ローカルモデル向けには (4) が効かないため (1)-(3) を自前で組む**ことに対応する。 ### KV cache / prompt cache 整合 戦略の選択は cache 効率に直結する: - **PrefixStable 戦略 (Claude Deferred / Dispatcher パターン)**: 初期 prefix が固定。ToolSearch 結果や dispatcher 経由の動的解決は **会話末尾の tool_result** に積まれるため、前方プレフィックスが揺らがない - **RewindToBoundary 戦略 (Rolling Developer)**: tool セット変更が cache 全消し。compaction 境界に同期させて損失を抑える Anthropic API の `prompt caching` は Explicit (cache_control) で、`llm_providers.md` §Prompt caching の `CacheStrategy::Explicit { max_breakpoints }` と整合する。ローカル推論の KV cache は基本 prefix-only のため `Auto` 相当。両方とも「安定 prefix 設計」に効く。 ## 根拠 - **Registry vs Context 分離**: Claude Code の実演で「context に schema があるかは validation に無関係」と判明 (`claude-code-deferred-tools.md` §10)。同じ抽象で Claude / OpenAI / ローカル系を統一できる - **戦略の差し替え可能性**: Harmony は構造化トークン+namespace 前提で、Claude の deferred 方式が直接通用しない (`tool_approach_comparison.md` §1, §2)。モデル系統ごとの戦略切り替えは避けられない - **MCP-style dispatcher**: OpenAI が MCP 統合で採用している方向。namespace に汎用 entry point だけ置き、サブツール解決を外部化する。upfront にせず、訓練分布も逸脱しない - **検証レイヤを別層に**: モデル側のフォーマットの強さ (Harmony の特殊トークン) と弱さ (Claude の正規表現パース) で fail mode が違うため、ContextRenderer に閉じ込めずに上位で統一的に扱う ## 実装原則 - **registry は llm-worker の上位層に置く** (低レベル基盤に留める方針: `feedback_llm_worker_scope.md`) - **MCP サーバ統合は registry のバックエンドの一つ**として扱い、Harmony 側 dispatcher と内部実装を共有 - **ContextRenderer の選択は ProviderScheme と一対多**: `scheme/anthropic` → ClaudeDeferred、`scheme/openai_responses` → HarmonyUpfront 等。`llm_providers.md` のプロバイダカタログにレンダラ指定を載せる - **ToolSearch 相当は MetaTool として実装**: registry 側に `__tool_search` / `__tool_describe` を登録し、ContextRenderer の戦略によって core tools に含めるか否かを決める - **テスト**: registry のバリデーションが context schema 有無に依存しないことを property test で保証する ## Scope 外 - 個別の MCP プロトコル実装 / サーバ連携の詳細 - 各モデル固有の chat template レンダリング (Hermes / Qwen / Llama 等の差分は別 ticket) - Tool 結果の構造化出力 (citation / file reference 等) のスキーマ - Tool 並列実行・依存解決・cancellation - ユーザ定義ツールの permission 管理 (sandbox.md と別) ## 参考 - `docs/ref/claude-code-deferred-tools.md` — Claude Code の deferred tools 機構と実演による検証 - `docs/ref/tool_approach_comparison.md` — Anthropic / OpenAI のツール呼び出しアプローチ比較 - `docs/plan/llm_providers.md` — プロバイダ抽象とスキーム / capability 設計