関数ツール

カスタム Rust 関数でエージェントの機能を拡張します。


関数ツールとは?

関数ツールを使用すると、エージェントに会話以外の機能(APIsの呼び出し、計算の実行、データベースへのアクセス、その他のカスタムロジック)を提供できます。LLMは、ユーザーのリクエストに基づいてツールを使用するタイミングを判断します。

主な特徴:

  • 🚀 #[tool] マクロ - ボイラープレートなしでツールを登録(推奨)
  • 🔧 FunctionTool::new() - 任意の非同期関数を手動でラップ
  • 📝 JSON パラメータ - 柔軟な入出力
  • 🎯 型安全なスキーマ - schemars を介して型から JSON Schema を自動生成
  • 🔗 コンテキストへのアクセス - セッション状態、アーティファクト、メモリ

ツール実行パイプライン

Rendering architecture…

ツールを作成する最も簡単な方法です。マクロはドキュメントコメントを説明として読み取り、引数の型から JSON スキーマを導出します。

use adk_tool::tool;
use adk_core::AdkError;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{json, Value};

#[derive(Deserialize, JsonSchema)]
struct WeatherArgs {
    /// The city to look up
    city: String,
    /// Temperature unit (celsius or fahrenheit)
    unit: Option<String>,
}

/// Get the current weather for a city.
#[tool]
async fn get_weather(args: WeatherArgs) -> Result<Value, AdkError> {
    Ok(json!({ "temp": 22, "city": args.city }))
}

// Generated: pub struct GetWeather; — implements adk_core::Tool
// Use it: agent_builder.tool(Arc::new(GetWeather))

ツールでセッションコンテキストが必要な場合は、最初のパラメータとして Arc<dyn ToolContext> を追加します。

use adk_core::ToolContext;
use std::sync::Arc;

/// Search the user's saved documents.
#[tool]
async fn search_docs(
    ctx: Arc<dyn ToolContext>,
    args: SearchArgs,
) -> Result<Value, AdkError> {
    let user_id = ctx.user_id();
    // ... use context for scoped access
}

ツールメタデータ属性

マクロ内でツールを読み取り専用、並行実行安全、または長時間実行として直接指定できます。

/// Look up cached data — no side effects, safe for parallel dispatch.
#[tool(read_only, concurrency_safe)]
async fn cache_lookup(args: LookupArgs) -> Result<Value, AdkError> {
    Ok(json!({"result": "cached"}))
}

/// Start a long-running background report.
#[tool(long_running)]
async fn generate_report(args: ReportArgs) -> Result<Value, AdkError> {
    Ok(json!({"task_id": "abc123", "status": "processing"}))
}

利用可能な属性(すべて任意で、自由に組み合わせられます):

属性効果
read_onlyis_read_only() → true — 並行 Auto ディスパッチに必要な2つのシグナルのうちの1つ
concurrency_safeis_concurrency_safe() → true — 並行 Auto ディスパッチに必要な2つのシグナルのうちの1つ
long_runningis_long_running() → true — LLM が保留中のツールを再度呼び出すのを防止

属性なしの #[tool] はデフォルト(すべての false)を維持するため、既存のコードに影響はありません。


代替方法: FunctionTool::new()

動的なツールを使用する場合、または明示的な登録を好む場合:

FunctionTool::new() でツールを作成し、LLM が渡すパラメータを把握できるよう、必ずスキーマを追加します:

use adk_rust::prelude::*;
use adk_rust::Launcher;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;

#[derive(JsonSchema, Serialize, Deserialize)]
struct WeatherParams {
    /// The city or location to get weather for
    location: String,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-3.7-flash")?;

    // Weather tool with proper schema
    let weather_tool = FunctionTool::new(
        "get_weather",
        "Get current weather for a location",
        |_ctx, args| async move {
            let location = args.get("location")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            Ok(json!({
                "location": location,
                "temperature": "22°C",
                "conditions": "sunny"
            }))
        },
    )
    .with_parameters_schema::<WeatherParams>(); // Required for LLM to call correctly!

    let agent = LlmAgentBuilder::new("weather_agent")
        .instruction("You help users check the weather. Always use the get_weather tool.")
        .model(Arc::new(model))
        .tool(Arc::new(weather_tool))
        .build()?;

    Launcher::new(Arc::new(agent)).run().await?;
    Ok(())
}

⚠️ 重要: 必ず .with_parameters_schema<T>() を使用してください。これがないと、LLM は渡すパラメータを把握できず、ツールを呼び出さない可能性があります。

仕組み:

  1. ユーザーが「東京の天気は?」と尋ねる
  2. LLM が {"location": "Tokyo"} を指定して get_weather の呼び出しを決定する
  3. ツールが {"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"} を返す
  4. LLM が応答を整形する: 「東京の天気は晴れで、気温は22°Cです。」

ステップ 2: パラメータ処理

JSON args からパラメータを抽出します:

let order_tool = FunctionTool::new(
    "process_order",
    "Process an order. Parameters: product_id (required), quantity (required), priority (optional)",
    |_ctx, args| async move {
        // Required parameters - return error if missing
        let product_id = args.get("product_id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| adk_core::AdkError::tool("product_id is required"))?;
        
        let quantity = args.get("quantity")
            .and_then(|v| v.as_i64())
            .ok_or_else(|| adk_core::AdkError::tool("quantity is required"))?;
        
        // Optional parameter with default
        let priority = args.get("priority")
            .and_then(|v| v.as_str())
            .unwrap_or("normal");
        
        Ok(json!({
            "order_id": "ORD-12345",
            "product_id": product_id,
            "quantity": quantity,
            "priority": priority,
            "status": "confirmed"
        }))
    },
);

ステップ 3: スキーマによる型付きパラメータ

複雑なツールには、JSON Schema を使用した型付き構造体を使います:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(JsonSchema, Serialize, Deserialize)]
struct CalculatorParams {
    /// The arithmetic operation to perform
    operation: Operation,
    /// First operand
    a: f64,
    /// Second operand
    b: f64,
}

#[derive(JsonSchema, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Operation {
    Add,
    Subtract,
    Multiply,
    Divide,
}

let calculator = FunctionTool::new(
    "calculator",
    "Perform arithmetic operations",
    |_ctx, args| async move {
        let params: CalculatorParams = serde_json::from_value(args)?;
        let result = match params.operation {
            Operation::Add => params.a + params.b,
            Operation::Subtract => params.a - params.b,
            Operation::Multiply => params.a * params.b,
            Operation::Divide if params.b != 0.0 => params.a / params.b,
            Operation::Divide => return Err(adk_core::AdkError::tool("Cannot divide by zero")),
        };
        Ok(json!({ "result": result }))
    },
)
.with_parameters_schema::<CalculatorParams>();

スキーマは、schemars を使用して Rust の型から自動生成されます。


ステップ 4: 複数ツールのエージェント

1つのエージェントに複数のツールを追加します:

let agent = LlmAgentBuilder::new("assistant")
    .instruction("Help with calculations, conversions, and weather.")
    .model(Arc::new(model))
    .tool(Arc::new(calc_tool))
    .tool(Arc::new(convert_tool))
    .tool(Arc::new(weather_tool))
    .build()?;

LLM は、ユーザーのリクエストに基づいて適切なツールを自動的に選択します。


エラー処理

ツール固有の失敗については、Tool コンポーネントを使用してエラーを返します:

use adk_core::{AdkError, ErrorComponent, ErrorCategory};

let divide_tool = FunctionTool::new(
    "divide",
    "Divide two numbers",
    |_ctx, args| async move {
        let a = args.get("a").and_then(|v| v.as_f64())
            .ok_or_else(|| AdkError::new(
                ErrorComponent::Tool,
                ErrorCategory::InvalidInput,
                "tool.divide.missing_param",
                "Parameter 'a' is required",
            ))?;
        let b = args.get("b").and_then(|v| v.as_f64())
            .ok_or_else(|| AdkError::new(
                ErrorComponent::Tool,
                ErrorCategory::InvalidInput,
                "tool.divide.missing_param",
                "Parameter 'b' is required",
            ))?;
        
        if b == 0.0 {
            return Err(AdkError::new(
                ErrorComponent::Tool,
                ErrorCategory::InvalidInput,
                "tool.divide.division_by_zero",
                "Cannot divide by zero",
            ));
        }
        
        Ok(json!({ "result": a / b }))
    },
);

迅速な移行には、後方互換性のある短縮記法も使用できます:

Err(AdkError::tool("Parameter 'a' is required"))

エラーメッセージは LLM に渡され、再試行したり、異なる入力を求めたりできます。


ツールコンテキスト

ToolContext を介してセッション情報にアクセスします:

#[derive(JsonSchema, Serialize, Deserialize)]
struct GreetParams {
    #[serde(default)]
    message: Option<String>,
}

let greet_tool = FunctionTool::new(
    "greet",
    "Greet the user with session info",
    |ctx, _args| async move {
        let user_id = ctx.user_id();
        let session_id = ctx.session_id();
        let agent_name = ctx.agent_name();
        Ok(json!({
            "greeting": format!("Hello, user {}!", user_id),
            "session": session_id,
            "served_by": agent_name
        }))
    },
)
.with_parameters_schema::<GreetParams>();

利用可能なコンテキスト:

  • ctx.user_id() - 現在のユーザー ID
  • ctx.session_id() - 現在のセッション ID
  • ctx.agent_name() - エージェントの名前
  • ctx.artifacts() - アーティファクトストレージへのアクセス
  • ctx.search_memory(query) - メモリ検索サービス

長時間実行ツール

かなりの時間がかかる操作(データ処理、外部 APIs)には、ノンブロッキングパターンを使用します。

  1. 開始ツールは task_id を直ちに返す
  2. バックグラウンド処理は非同期で実行される
  3. ステータスツールでユーザーが進捗を確認できる
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(JsonSchema, Serialize, Deserialize)]
struct ReportParams {
    topic: String,
}

#[derive(JsonSchema, Serialize, Deserialize)]
struct StatusParams {
    task_id: String,
}

// Shared task store
let tasks: Arc<RwLock<HashMap<String, TaskState>>> = Arc::new(RwLock::new(HashMap::new()));
let tasks1 = tasks.clone();
let tasks2 = tasks.clone();

// Tool 1: Start (returns immediately)
let start_tool = FunctionTool::new(
    "generate_report",
    "Start generating a report. Returns task_id immediately.",
    move |_ctx, args| {
        let tasks = tasks1.clone();
        async move {
            let topic = args.get("topic").and_then(|v| v.as_str()).unwrap_or("general").to_string();
            let task_id = format!("task_{}", rand::random::<u32>());
            
            // Store initial state
            tasks.write().await.insert(task_id.clone(), TaskState {
                status: "processing".to_string(),
                progress: 0,
                result: None,
            });

            // Spawn background work (non-blocking!)
            let tasks_bg = tasks.clone();
            let tid = task_id.clone();
            tokio::spawn(async move {
                // Simulate work...
                tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
                if let Some(t) = tasks_bg.write().await.get_mut(&tid) {
                    t.status = "completed".to_string();
                    t.result = Some("Report complete".to_string());
                }
            });

            // Return immediately with task_id
            Ok(json!({"task_id": task_id, "status": "processing"}))
        }
    },
)
.with_parameters_schema::<ReportParams>()
.with_long_running(true);  // Mark as long-running

// Tool 2: Check status
let status_tool = FunctionTool::new(
    "check_report_status",
    "Check report generation status",
    move |_ctx, args| {
        let tasks = tasks2.clone();
        async move {
            let task_id = args.get("task_id").and_then(|v| v.as_str()).unwrap_or("");
            if let Some(t) = tasks.read().await.get(task_id) {
                Ok(json!({"status": t.status, "result": t.result}))
            } else {
                Ok(json!({"error": "Task not found"}))
            }
        }
    },
)
.with_parameters_schema::<StatusParams>();

主なポイント:

  • .with_long_running(true) により、このツールが保留中のステータスを返すことをエージェントに伝える
  • このツールは tokio::spawn() で処理を開始し、直ちに戻る
  • ユーザーが進捗をポーリングできるよう、ステータス確認ツールを提供する

これにより、LLM がツールを繰り返し呼び出すのを防ぐ注記が追加されます。


ツールからのストリーミング進捗

時間のかかるツールは、実行中でも中間出力を UI に送信できるため、ユーザーは最終結果を待つことなく、シェルコマンドの標準出力、ビルドのログ、ダウンロードのバイト列をリアルタイムで確認できます。出力が到着するたびに ToolContext::emit_progress を呼び出します。

use adk_core::{Result, Tool, ToolContext};
use std::sync::Arc;

#[async_trait::async_trait]
impl Tool for BuildTool {
    // ... name(), description(), parameters_schema() ...

    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: serde_json::Value) -> Result<serde_json::Value> {
        // Emit chunks as they arrive — each becomes a partial Event on the
        // agent's EventStream, the SAME stream the model's reply travels on.
        ctx.emit_progress("stdout", "Compiling project...\n").await;
        ctx.emit_progress("stdout", "Build finished in 4.2s\n").await;
        ctx.emit_progress("stderr", "warning: unused variable `x`\n").await;

        // The final return value is still the complete result the model consumes.
        Ok(serde_json::json!({ "status": "ok", "warnings": 1 }))
    }
}

シグネチャ:

async fn emit_progress(&self, stream: &str, chunk: &str)
  • stream — チャンクのラベル: "stdout"、"stderr"、または任意のカスタムチャネル。
  • chunk — 出力するテキスト(ターミナル形式の出力では、1 行ごとに出力します)。

UI に届く仕組み。 フレームワークは各チャンクを、エージェントの EventStream 上にある部分的な Event として転送します。コンシューマーは event.tool_progress_stream() でそれを検出し、リアルタイムで描画します。別のチャネルやログのスクレイピングはありません。進捗、モデルのテキスト、最終的なツール結果はすべて、1 つの順序付きストリームで到着します。

後方互換性。 デフォルトの emit_progress は何もしないため、ストリーミングを行わない既存のツールやランナーに影響はありません。進捗を出力するのはオプトインしたツールだけであり、tool_progress_stream() を確認するコンシューマーだけがそれを認識します。

進行は制限され、情報が失われます。 ツールはクライアントが消費するよりも速く出力を生成することがあります。コンパイラログ、シェルコマンド、暴走したループなどがその例です。そのため、フレームワークは保持して転送するデータ量に上限を設け、無制限に増加しないようにします。

制限値超過した場合
ツールバッチあたりのキューの深さ256 イベントツールは空きができるまで最大 100 ms 待機し、その後チャンクが破棄されます
チャンクあたりのバイト数8 KiBチャンクは文字境界で切り詰められます
ツール呼び出しあたりのバイト数1 MiB残りの進行状況は転送されません

いずれかの理由で出力が破棄された場合、その呼び出しではテキスト [adk: tool progress truncated] を含む進行状況イベントが必ず1つだけ発行されるため、無音のまま欠落することはありません。そのため、低速なコンシューマーはツールを一時的に遅くしますが、無期限に停止させたり、メモリを使い果たしたりすることはありません。

これらの制限は 進行状況 にのみ適用されます。ツールの最終結果には影響しないため、必要であればツール内で大きな結果を切り詰めてください。

単一のイベントフィードからライブの bash 出力とワンショットのツール結果(read_file、grep、glob)をレンダリングする完全な Web UI については、streaming_bash の例を参照してください。ストリーミング bash ツール自体は adk-devtools にあります。


実行例

cargo adk new tool_agent --template tools
cd tool_agent
cargo run

ベストプラクティス

  1. 明確な説明 - LLM がいつツールを使用すべきか理解できるようにする
  2. 入力を検証する - パラメーターが不足している場合は、役立つエラーメッセージを返す
  3. 構造化された JSON を返す - 明確なフィールド名を使用する
  4. ツールの責務を絞る - 各ツールは1つのことを適切に実行するようにする
  5. スキーマを使用する - 複雑なツールでは、パラメータースキーマを定義する
  6. 安全な読み取り専用ツールを指定する - .with_read_only(true) と .with_concurrency_safe(true) の両方を設定し、Auto のディスパッチで同時実行可能なサブセットに含められるようにする

ツールメタデータ: 読み取り専用と同時実行

よりスマートなディスパッチを有効にするには、ツールを読み取り専用または同時実行可能として指定します。

// A lookup tool that performs no side effects
let lookup = FunctionTool::new("lookup", "Look up data", |_ctx, args| async move {
    Ok(json!({"result": "cached data"}))
})
.with_read_only(true)
.with_concurrency_safe(true); // Auto mode requires both signals

// A mutation tool (defaults: read_only=false, concurrency_safe=false)
let update = FunctionTool::new("update", "Update record", |_ctx, args| async move {
    Ok(json!({"updated": true}))
});

ToolExecutionStrategy::Auto が有効な場合、ディスパッチループはまず、選択されたツールが is_read_only() と is_concurrency_safe() の両方から true を返す呼び出しを同時実行します。その後、残りのすべての呼び出しを順番に実行します。ToolExecutionStrategy::Parallel はこれらのシグナルを無視する明示的なオーバーライドであるため、同時実行の安全性は呼び出し元が担います。


SimpleToolContext: エージェントループ外でのツールの使用

エージェントループの外部でツールを呼び出す必要がある場合(テスト、MCPサーバーモード、サブエージェントへの委譲)は、完全なToolContextトレイト階層を実装する代わりにSimpleToolContextを使用します。

use adk_tool::SimpleToolContext;
use adk_core::ToolContext;
use std::sync::Arc;

// Construct with just a caller name — all other fields get sensible defaults
let ctx = SimpleToolContext::new("my-test-harness");

// Optionally override the function call ID
let ctx = SimpleToolContext::new("my-mcp-server")
    .with_function_call_id("custom-call-id");

// Bind a session ID so session-aware tools (and MCP servers that key state by
// session) see a stable identifier instead of the empty default.
let ctx = SimpleToolContext::new("my-mcp-server")
    .with_session_id("session-42");

// Use it to execute any tool
let tool_ctx: Arc<dyn ToolContext> = Arc::new(ctx);
let result = my_tool.execute(tool_ctx, json!({"key": "value"})).await?;

デフォルト値: user_id() → "anonymous"、session_id() / branch() → ""、artifacts() → None、search_memory() → 空の vec。invocation_idとfunction_call_idはいずれも自動生成されるUUIDsです。ツールがセッションごとに状態をルーティングまたは永続化する場合は、with_session_id(...)で実際のセッションを設定します。


StatefulTool: 呼び出し間で共有される状態

呼び出し間で状態(カウンター、キャッシュ、接続プールなど)を維持する必要があるツールには、StatefulTool<S>を使用します。

use adk_tool::StatefulTool;
use adk_core::ToolContext;
use std::sync::Arc;
use tokio::sync::RwLock;

struct AppCache {
    entries: RwLock<HashMap<String, String>>,
}

let cache = Arc::new(AppCache {
    entries: RwLock::new(HashMap::new()),
});

let cache_tool = StatefulTool::new(
    "cache_lookup",
    "Look up a value in the application cache",
    cache.clone(),
    |state, _ctx, args| async move {
        let key = args["key"].as_str().unwrap_or("");
        let entries = state.entries.read().await;
        let value = entries.get(key).cloned().unwrap_or_default();
        Ok(json!({"key": key, "value": value}))
    },
)
.with_read_only(true)
.with_concurrency_safe(true);

StatefulToolは呼び出しごとにArc<S>をクローンするため(低コストな参照カウントの増加)、すべての実行で同じ基盤状態が共有されます。FunctionToolと同じビルダーメソッド、with_long_running、with_parameters_schema、with_response_schema、with_scopes、with_read_only、with_concurrency_safeをサポートします。



マルチモーダルな関数レスポンス

Gemini 3 モデルは、関数レスポンスでJSONだけでなく、画像、音声、PDFs、ファイル参照も受け取れます。ツールは、JSONの戻り値にinline_data配列やfile_data配列(またはその両方)を含めることで、マルチモーダルデータを返せます。

/// Tool that returns a chart image alongside JSON metadata.
async fn generate_chart(
    _ctx: Arc<dyn ToolContext>,
    args: serde_json::Value,
) -> Result<serde_json::Value> {
    let png_bytes: Vec<u8> = render_chart(&args);

    // Include inline_data in the return value — the framework extracts it automatically
    Ok(json!({
        "response": {
            "title": "Q4 Sales",
            "chart_type": "bar"
        },
        "inline_data": [{
            "mime_type": "image/png",
            "data": png_bytes
        }]
    }))
}

フレームワークは自動的に次の処理を行います。

  1. FunctionResponseData::from_tool_result()を介してinline_data/file_dataを検出する
  2. インラインのバイナリデータを Base64 エンコードする
  3. パーツをfunctionResponseワイヤーオブジェクト内にネストする(Gemini 3 API形式に一致)

ファイル参照

外部に保存された大きなファイルの場合は、バイト列を埋め込む代わりに、URIを指定してfile_dataを使用します。

Ok(json!({
    "response": { "document_id": "report-2024", "pages": 12 },
    "file_data": [{
        "mime_type": "application/pdf",
        "file_uri": "gs://my-bucket/reports/report-2024.pdf"
    }]
}))

直接構築

フレームワークレベルのコード(カスタムエージェント、変換レイヤー)では、FunctionResponseDataを直接構築します。

use adk_core::{FunctionResponseData, InlineDataPart, FileDataPart};

// JSON + inline image
let frd = FunctionResponseData::with_inline_data(
    "chart_tool",
    json!({"title": "Q4 Chart"}),
    vec![InlineDataPart { mime_type: "image/png".into(), data: png_bytes }],
);

// JSON + file reference
let frd = FunctionResponseData::with_file_data(
    "doc_tool",
    json!({"status": "ok"}),
    vec![FileDataPart { mime_type: "application/pdf".into(), file_uri: "gs://bucket/file.pdf".into() }],
);

// JSON + both
let frd = FunctionResponseData::with_multimodal("tool", json, inline_parts, file_parts);

注: マルチモーダル関数レスポンスには、gemini-3.7-flashなどの Gemini 3 シリーズモデルが必要です。以前のモデルでは 400 エラーが返されます。

完全に動作する例については、examples/multimodal_function_response/を参照してください。


前へ: ← mistral.rs | 次へ: 組み込みツール →