yoi/crates/llm-worker/docs/spec/tools.md
2026-04-04 04:27:46 +09:00

12 KiB
Raw Blame History

Tool 設計

概要

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

ツールの管理はToolServer / ToolServerHandleに分離されており、 Workerから独立したツール管理・実行が可能。

主要な型

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

Worker.register_tool() → ToolServerHandle.register_tool() で呼び出し

           |
           v

- struct ToolMeta (name, desc, schema)
    不変・登録時固定
- trait Tool (execute)
    登録時生成・セッション中再利用
- struct ToolServer / ToolServerHandle
    ツールの管理・実行を担当

ToolMeta

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

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolMeta {
    pub name: String,
    pub description: String,
    pub input_schema: Value,
}

impl ToolMeta {
    pub fn new(name: impl Into<String>) -> Self;
    pub fn description(mut self, desc: impl Into<String>) -> Self;
    pub fn input_schema(mut self, schema: Value) -> Self;
}

目的:

  • 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())をキャプチャ可能

ToolError

#[derive(Debug, Error)]
pub enum ToolError {
    #[error("Invalid argument: {0}")]
    InvalidArgument(String),
    #[error("Execution failed: {0}")]
    ExecutionFailed(String),
    #[error("Internal error: {0}")]
    Internal(String),
}

ToolServer / ToolServerHandle

ツール管理がWorkerから分離された専用コンポーネント。 ToolServerがツールマップを所有し、ToolServerHandleがそのクローン可能な参照を提供する。

ToolServer

#[derive(Clone, Default)]
pub struct ToolServer {
    tools: Arc<Mutex<ToolMap>>,  // HashMap<String, (ToolMeta, Arc<dyn Tool>)>
}

impl ToolServer {
    pub fn new() -> Self;
    pub fn handle(&self) -> ToolServerHandle;
}

ToolServerHandle

#[derive(Clone, Default)]
pub struct ToolServerHandle {
    tools: Arc<Mutex<ToolMap>>,
}

impl ToolServerHandle {
    // 登録pub(crate) - Worker経由でのみ呼び出し可能
    pub(crate) fn register_tool(&self, factory: WorkerToolDefinition) -> Result<(), ToolServerError>;
    pub(crate) fn register_tools(&self, factories: impl IntoIterator<Item = WorkerToolDefinition>) -> Result<(), ToolServerError>;

    // 検索Hookからのアクセス用
    pub fn get_tool(&self, name: &str) -> Option<(ToolMeta, Arc<dyn Tool>)>;

    // 実行
    pub async fn call_tool(&self, name: &str, input_json: &str) -> Result<String, ToolServerError>;

    // LLMリクエスト用定義生成名前順ソート済み
    pub fn tool_definitions_sorted(&self) -> Vec<LlmToolDefinition>;
}

ToolServerError

#[derive(Debug, Error, PartialEq, Eq)]
pub enum ToolServerError {
    #[error("Tool with name '{0}' already registered")]
    DuplicateName(String),
    #[error("Tool '{0}' not found")]
    ToolNotFound(String),
    #[error("Tool execution failed: {0}")]
    ToolExecution(String),
}

設計上のポイント

  • ToolServerClone可能で、Arc<Mutex<ToolMap>>により複数箇所で共有可能
  • register_tool / register_toolspub(crate) でWorker経由のみ
  • get_tool, call_toolpub でHookやアプリケーションから直接利用可能
  • tool_definitions_sorted() は名前でソートされた決定的な定義リストを返す
  • Worker側では tool_server_handle()ToolServerHandle を取得可能

Worker でのツール管理

Worker は ToolServerHandle を内部に持ち、ツール登録のAPIを提供する。 登録は Mutable 状態でのみ可能。

// Worker API (Mutable状態のみ)
pub fn register_tool(&mut self, factory: WorkerToolDefinition) -> Result<(), ToolRegistryError>;
pub fn register_tools(&mut self, factories: impl IntoIterator<Item = WorkerToolDefinition>) -> Result<(), ToolRegistryError>;

// ハンドルの取得(全状態)
pub fn tool_server_handle(&self) -> ToolServerHandle;

登録時の処理:

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

マクロによる自動生成 (llm-worker-macros)

#[tool_registry] マクロと #[tool] マクロにより、メソッドから自動的に Tool トレイト実装と ToolDefinition ファクトリを生成する。

マクロ一覧

マクロ 種類 用途
#[tool_registry] proc_macro_attribute implブロックに適用。#[tool]メソッドを検出し、コード生成
#[tool] proc_macro_attribute メソッドに適用。マーカー属性(tool_registryが処理)
#[description] proc_macro_attribute 引数に適用。schemarsのdescriptionに変換

使用例

#[tool_registry]
impl MyApp {
    /// 検索を実行する
    /// 指定されたクエリでデータベースを検索します。
    #[tool]
    async fn search(
        &self,
        #[description = "検索クエリ文字列"] query: String,
    ) -> String {
        format!("Results for: {}", query)
    }
}

// 生成されるもの:
// - SearchArgs 構造体
// - ToolSearch 構造体 (Tool trait実装)
// - MyApp::search_definition() メソッド

生成されるコード詳細

マクロは以下を自動生成します。

1. 引数構造体({PascalCase}Args

#[derive(serde::Deserialize, schemars::JsonSchema)]
struct SearchArgs {
    #[schemars(description = "検索クエリ文字列")]
    pub query: String,
}
  • #[description = "..."] 属性は #[schemars(description = "...")] に変換される
  • 引数がない場合は空の構造体が生成される

2. ラッパー構造体(Tool{PascalCase}

#[derive(Clone)]
pub struct ToolSearch {
    ctx: MyApp,
}

#[async_trait]
impl Tool for ToolSearch {
    async fn execute(&self, input_json: &str) -> Result<String, ToolError> {
        let args: SearchArgs = serde_json::from_str(input_json)
            .map_err(|e| ToolError::InvalidArgument(e.to_string()))?;
        let result = self.ctx.search(args.query).await;
        Ok(format!("{:?}", result))
    }
}
  • 戻り値が Result の場合、ErrToolError::ExecutionFailed に変換される
  • 戻り値が Result でない場合、format!("{:?}", result) で文字列化される
  • async/非asyncの両方をサポート

3. ファクトリメソッド({method_name}_definition

impl MyApp {
    pub fn search_definition(&self) -> ToolDefinition {
        let ctx = self.clone();
        Arc::new(move || {
            let schema = schemars::schema_for!(SearchArgs);
            let meta = ToolMeta::new("search")
                .description("検索を実行する\n指定されたクエリでデータベースを検索します。")
                .input_schema(serde_json::to_value(schema).unwrap_or(json!({})));
            let tool: Arc<dyn Tool> = Arc::new(ToolSearch { ctx: ctx.clone() });
            (meta, tool)
        })
    }
}
  • ツール名はメソッド名snake_caseがそのまま使われる
  • ツールの説明はメソッドのdocコメント全体が使われるdocコメントがない場合は "Tool: {name}" がフォールバック)
  • input_schemaschemars::schema_for! で生成される

命名規則

元のメソッド名 引数構造体 ラッパー構造体 ファクトリメソッド
search SearchArgs ToolSearch search_definition()
get_user GetUserArgs ToolGetUser get_user_definition()

PascalCaseへの変換は to_pascal_case() 関数で行われるsnake_caseの各セグメントを先頭大文字に変換

Hook との連携

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

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

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

用途:

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

使用例

手動実装

struct Counter { count: AtomicUsize }

#[async_trait]
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)?;

マクロ使用(推奨)

#[derive(Clone)]
struct App;

#[tool_registry]
impl App {
    /// 挨拶する
    #[tool]
    async fn greet(&self, #[description = "名前"] name: String) -> String {
        format!("Hello, {}!", name)
    }
}

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

複数ツールの一括登録

worker.register_tools([
    app.search_definition(),
    app.get_user_definition(),
    app.greet_definition(),
])?;

設計上の決定

問題 決定 理由
メタ情報の変更可能性 ToolMeta を分離・不変化 登録後の整合性を保証
状態管理 登録時にインスタンス生成 セッション中の状態保持、同一インスタンス再利用
Factory vs Instance Factory + 登録時即時呼び出し コンテキストキャプチャと登録時検証
Hook からのアクセス Context に meta と tool を含む 柔軟な介入を可能に
ツール管理の分離 ToolServer / ToolServerHandle Worker外からのツール検索・実行を可能に
登録の可視性 register_toolpub(crate) Worker経由でのみ登録を許可し、整合性を維持
定義リストの順序 名前順ソート 決定的なリクエスト生成を保証