함수 도구

사용자 정의 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 디스패치에 필요한 두 신호 중 하나
concurrency_safeis_concurrency_safe() → true — 동시 Auto 디스패치에 필요한 두 신호 중 하나
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-2.5-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가 get_weather{"location": "Tokyo"}와 함께 호출하기로 결정합니다
  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단계: 멀티 도구 에이전트

하나의 에이전트에 여러 도구를 추가합니다:

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로 푸시할 수 있으므로, 사용자는 최종 결과를 기다리지 않고 셸 명령의 stdout, 빌드 로그, 또는 다운로드 바이트를 실시간으로 볼 수 있습니다. 출력이 도착할 때마다 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 — 출력할 텍스트(터미널 스타일 출력은 줄 단위로 emit).

UI에 도달하는 방식. 프레임워크는 각 청크를 에이전트의 EventStream에서 부분적인 Event로 전달합니다. 소비자는 event.tool_progress_stream()로 이를 감지하고 실시간으로 렌더링합니다. 두 번째 채널도 없고 로그 스크래핑도 없습니다 — 진행 상황, 모델 텍스트, 최종 도구 결과가 모두 하나의 정렬된 스트림으로 도착합니다.

하위 호환성. 기본 emit_progress는 no-op이므로, 스트리밍하지 않는 기존 도구와 러너는 영향을 받지 않습니다. 진행 상황을 내보내는 것은 opt-in한 도구뿐이며, tool_progress_stream()를 확인하는 소비자만 이를 관찰합니다.

진행 상황은 제한되며 손실될 수 있습니다. 도구는 클라이언트가 소비하는 속도보다 더 빠르게 출력을 생성할 수 있습니다 — 컴파일러 로그, 셸 명령, 무한 루프 등 — 따라서 프레임워크는 무한히 늘리지 않고 보유하고 전달할 수 있는 양을 제한합니다:

제한초과 시
도구 배치당 큐 깊이256 이벤트도구는 최대 100 ms 동안 공간을 기다린 다음, 청크가 삭제됩니다
청크당 바이트 수8 KiB청크는 문자 경계에서 잘립니다
도구 호출당 바이트1 MiB남은 진행률은 전달되지 않습니다

출력이 이러한 이유 중 하나로 떨어지면, 그 호출에 대해 텍스트 [adk: tool progress truncated]를 담은 진행 이벤트가 정확히 하나 emitted되므로, 간격이 항상 조용히 지나가는 대신 눈에 보입니다. 따라서 느린 소비자는 도구를 잠시 느리게 만들 뿐, 무기한으로 멈추게 하거나 메모리를 고갈시킬 수는 없습니다.

이 제한은 진행에만 적용됩니다. 도구의 최종 결과에는 영향이 없으므로, 중요하다면 도구 내부에서 큰 결과를 잘라내세요.

라이브 bash 출력과 단일 이벤트 피드에서의 일회성 도구 결과(read_file, grep, glob)를 렌더링하는 완전한 웹 UI에 대해서는 streaming_bash 예제를 참조하세요. 스트리밍 bash 도구 자체는 adk-devtools에 있습니다.


실행 예제

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

모범 사례

  1. 명확한 설명 - 도구를 언제 사용해야 하는지 LLM가 이해하도록 돕기
  2. 입력 검증 - 누락된 매개변수에 대해 유용한 오류 메시지 반환
  3. 구조화된 JSON 반환 - 명확한 필드 이름 사용
  4. 도구를 집중적으로 유지 - 각 도구는 한 가지 일을 잘해야 함
  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_idfunction_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 모델은 함수 응답에서 이미지, 오디오, PDFs, 그리고 파일 참조를 수신하는 것을 지원합니다. JSON만이 아닙니다. 도구는 inline_data 및/또는 file_data 배열을 JSON 반환 값에 포함하여 멀티모달 데이터를 반환할 수 있습니다:

/// 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. inline_data/file_dataFunctionResponseData::from_tool_result()를 통해 감지
  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 시리즈 모델(gemini-3-flash-preview, gemini-3-pro-preview)이 필요합니다. 이전 모델은 400 오류를 반환합니다.

완전한 동작 예제는 examples/multimodal_function_response/를 참조하세요.


이전: ← mistral.rs | 다음: 내장 도구 →

함수 도구 - ADK-Rust 문서 | ADK-Rust