8.6 KiB
複数ツール動的読み込み機構の設計
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 インターフェース
pub trait ToolRegistry {
fn list(&self) -> Vec<ToolMeta>; // 名前+description のみ
fn schema(&self, name: &str) -> Option<&ToolSchema>;
fn dispatch(&self, name: &str, args: Value) -> Result<ToolResult, DispatchError>;
}
list()は常に全ツール返す (戦略は ContextRenderer 側の責務)schema()は ToolSearch 相当の動線で使用dispatch()は schema 照合+実行。context に schema text があるかは見ない
ContextRenderer インターフェース
pub trait ContextRenderer {
fn initial_render(&self, registry: &dyn ToolRegistry) -> InitialContext;
fn on_tool_load(&self, name: &str, registry: &dyn ToolRegistry) -> Option<ContextDelta>;
fn parse_call(&self, raw_output: &str) -> Result<ToolCall, ParseError>;
fn format_result(&self, name: &str, result: &ToolResult) -> String;
}
戦略ごとに実装を差し替える:
ClaudeDeferredRenderer: 初期 prompt に core tools のみ展開、tool_searchメタツールを常設、ロード時は tool_result として<function>{schema}</function>を流すHarmonyUpfrontRenderer: developer メッセージに namespace で全 tool 展開、ロード概念なしHarmonyDispatcherRenderer: namespace はcall_mcp(server, tool, args)だけ、サブツール解決は外部 MCPRollingDeveloperRenderer: 一定境界 (compaction 等) で developer メッセージを再描画。cache 損失は境界で吸収
Validation / Retry レイヤ
ツール呼び出しの失敗ハンドリングは ContextRenderer / Registry の上に置く独立層:
pub struct ToolDispatcher<R: ToolRegistry, C: ContextRenderer> {
registry: R,
renderer: C,
retry_policy: RetryPolicy,
}
責務:
- モデル出力をパース (
renderer.parse_call) - registry で schema 照合 → invalid なら error tool_result を返す
- dispatch → 結果を
renderer.format_resultで整形してモデルへ - 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 設計