llm_worker_rs/docs/spec/tools_design.md

5.5 KiB
Raw Blame History

Tool 設計

概要

llm-workerのツールシステムは、LLMが外部リソースにアクセスしたり計算を実行するための仕組みを提供する。 メタ情報の不変性とセッションスコープの状態管理を両立させる設計となっている。

主要な型

type ToolDefinition
  Fn() -> (ToolMeta, Arc<dyn Tool>)

worker.register_tool() で呼び出し

           ▼

- struct ToolMeta (name, desc, schema) 
    不変・登録時固定
- trait Tool (executer) 
    登録時生成・セッション中再利用

ToolMeta

ツールのメタ情報を保持する不変構造体。登録時に固定され、Worker内で変更されない。

pub struct ToolMeta {
    pub name: String,
    pub description: String,
    pub input_schema: Value,
}

目的:

  • LLM へのツール定義として送信
  • Hook からの参照(読み取り専用)
  • 登録後の不変性を保証

Tool trait

ツールの実行ロジックのみを定義するトレイト。

#[async_trait]
pub trait Tool: Send + Sync {
    async fn execute(&self, input_json: &str) -> Result<String, ToolError>;
}

設計方針:

  • メタ情報name, description, schemaは含まない
  • 状態を持つことが可能(セッション中のカウンターなど)
  • Send + Sync で並列実行に対応

インスタンスのライフサイクル:

  1. register_tool() 呼び出し時にファクトリが実行され、インスタンスが生成される
  2. LLM がツールを呼び出すと、既存インスタンスの execute() が実行される
  3. 同じセッション中は同一インスタンスが再利用される

※ 「最初に呼ばれたとき」の遅延初期化ではなく、登録時の即時初期化である。

ToolDefinition

メタ情報とツールインスタンスを生成するファクトリ。

pub type ToolDefinition = Arc<dyn Fn() -> (ToolMeta, Arc<dyn Tool>) + Send + Sync>;

なぜファクトリか:

  • Worker への登録時に一度だけ呼び出される
  • メタ情報とインスタンスを同時に生成し、整合性を保証
  • クロージャでコンテキスト(self.clone())をキャプチャ可能

Worker でのツール管理

// Worker 内部
tools: HashMap<String, (ToolMeta, Arc<dyn Tool>)>

// 登録 API
pub fn register_tool(&mut self, factory: ToolDefinition) -> Result<(), ToolRegistryError>

登録時の処理:

  1. ファクトリを呼び出し (meta, instance) を取得
  2. 同名ツールが既に登録されていればエラー
  3. HashMap に (meta, instance) を保存

マクロによる自動生成

#[tool_registry] マクロは {method}_definition() メソッドを生成する。

#[tool_registry]
impl MyApp {
    /// 検索を実行する
    #[tool]
    async fn search(&self, query: String) -> String {
        // 実装
    }
}

// 生成されるコード:
impl MyApp {
    pub fn search_definition(&self) -> ToolDefinition {
        let ctx = self.clone();
        Arc::new(move || {
            let meta = ToolMeta::new("search")
                .description("検索を実行する")
                .input_schema(/* schemars で生成 */);
            let tool = Arc::new(ToolSearch { ctx: ctx.clone() });
            (meta, tool)
        })
    }
}

Hook との連携

Hook は ToolCallContext / AfterToolCallContext を通じてメタ情報とインスタンスにアクセスできる。

pub struct ToolCallContext {
    pub call: ToolCall,      // 呼び出し情報(改変可能)
    pub meta: ToolMeta,      // メタ情報(読み取り専用)
    pub tool: Arc<dyn Tool>, // インスタンス(状態アクセス用)
}

用途:

  • meta で名前やスキーマを確認
  • tool でツールの内部状態を読み取り(ダウンキャスト必要)
  • call の引数を改変してツールに渡す

使用例

手動実装

struct Counter { count: AtomicUsize }

impl Tool for Counter {
    async fn execute(&self, _: &str) -> Result<String, ToolError> {
        let n = self.count.fetch_add(1, Ordering::SeqCst);
        Ok(format!("count: {}", n))
    }
}

let def: ToolDefinition = Arc::new(|| {
    let meta = ToolMeta::new("counter")
        .description("カウンターを増加")
        .input_schema(json!({"type": "object"}));
    (meta, Arc::new(Counter { count: AtomicUsize::new(0) }))
});

worker.register_tool(def)?;

マクロ使用(推奨)

#[tool_registry]
impl App {
    #[tool]
    async fn greet(&self, name: String) -> String {
        format!("Hello, {}!", name)
    }
}

let app = App;
worker.register_tool(app.greet_definition())?;

設計上の決定

問題 決定 理由
メタ情報の変更可能性 ToolMeta を分離・不変化 登録後の整合性を保証
状態管理 登録時にインスタンス生成 セッション中の状態保持、同一インスタンス再利用
Factory vs Instance Factory + 登録時即時呼び出し コンテキストキャプチャと登録時検証
Hook からのアクセス Context に meta と tool を含む 柔軟な介入を可能に