yoi/docs/plan/tool_dispatch.md
2026-06-01 18:49:23 +09:00

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) だけ、サブツール解決は外部 MCP
  • RollingDeveloperRenderer: 一定境界 (compaction 等) で developer メッセージを再描画。cache 損失は境界で吸収

Validation / Retry レイヤ

ツール呼び出しの失敗ハンドリングは ContextRenderer / Registry の上に置く独立層:

pub struct ToolDispatcher<R: ToolRegistry, C: ContextRenderer> {
    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 設計