Funktionstools
Erweitern Sie die Fähigkeiten des Agents mit benutzerdefinierten Rust-Funktionen.
Was sind Funktionstools?
Funktionstools ermöglichen es Ihnen, Agents Fähigkeiten jenseits der Unterhaltung zu geben - APIs aufzurufen, Berechnungen durchzuführen, auf Datenbanken zuzugreifen oder beliebige benutzerdefinierte Logik auszuführen. Das LLM entscheidet anhand der Anfrage des Benutzers, wann ein Tool verwendet wird.
Wichtige Punkte:
- 🚀
#[tool]-Makro - Registrierung von Tools ohne Boilerplate (empfohlen)- 🔧
FunctionTool::new()- beliebige asynchrone Funktion manuell einbinden- 📝 JSON-Parameter - flexible Ein-/Ausgabe
- 🎯 Typsichere Schemas - automatisches JSON-Schema aus Typen via schemars
- 🔗 Kontextzugriff - Session-Zustand, Artefakte, Speicher
Ausführungspipeline des Tools
Empfohlen: #[tool]-Makro
Der schnellste Weg, Tools zu erstellen. Das Makro liest Ihren Doc-Kommentar als Beschreibung und leitet das JSON-Schema aus Ihrem Args-Typ ab:
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))
Wenn Ihr Tool Session-Kontext benötigt, fügen Sie Arc<dyn ToolContext> als ersten Parameter hinzu:
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
}
Metadaten-Attribute des Tools
Markieren Sie Tools direkt im Makro als schreibgeschützt, nebenläufigkeitssicher oder langfristig laufend:
/// 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"}))
}
Verfügbare Attribute (alle optional, frei kombinierbar):
| Attribut | Effekt |
|---|---|
read_only | is_read_only() → true — eines von zwei Signalen, die für den gleichzeitigen Auto-Versand erforderlich sind |
concurrency_safe | is_concurrency_safe() → true — eines von zwei Signalen, die für den gleichzeitigen Auto-Versand erforderlich sind |
long_running | is_long_running() → true — verhindert, dass LLM ein ausstehendes Tool erneut aufruft |
Plain #[tool] ohne Attribute behält die Standardwerte bei (alle false), sodass bestehender Code nicht beeinträchtigt wird.
Alternative: FunctionTool::new()
Für dynamische Tools oder wenn Sie explizite Registrierung bevorzugen:
Erstellen Sie ein Tool mit FunctionTool::new() und fügen Sie immer ein Schema hinzu, damit der LLM weiß, welche Parameter übergeben werden sollen:
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(())
}
⚠️ Wichtig: Verwenden Sie immer
.with_parameters_schema<T>()- ohne dieses weiß der LLM nicht, welche Parameter übergeben werden sollen, und ruft das Tool möglicherweise nicht auf.
So funktioniert es:
- Der Benutzer fragt: "What's the weather in Tokyo?"
- LLM entscheidet sich,
get_weathermit{"location": "Tokyo"}aufzurufen - Das Tool gibt
{"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"}zurück - LLM formatiert die Antwort: "The weather in Tokyo is sunny at 22°C."
Schritt 2: Parameterbehandlung
Extrahieren Sie Parameter aus dem 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"
}))
},
);
Schritt 3: Typisierte Parameter mit Schema
Für komplexe Tools verwenden Sie typisierte Strukturen mit 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>();
Das Schema wird automatisch aus Rust-Typen mit schemars generiert.
Schritt 4: Multi-Tool-Agent
Fügen Sie einem Agenten mehrere Tools hinzu:
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()?;
Der LLM wählt automatisch das richtige Tool basierend auf der Anfrage des Benutzers aus.
Fehlerbehandlung
Geben Sie Fehler mit der Tool-Komponente für tool-spezifische Fehler zurück:
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 }))
},
);
Für eine schnelle Migration funktioniert auch die abwärtskompatible Kurzform:
Err(AdkError::tool("Parameter 'a' is required"))
Fehlermeldungen werden an den LLM übergeben, der erneut versuchen oder nach anderen Eingaben fragen kann.
Tool-Kontext
Greifen Sie über ToolContext auf Sitzungsinformationen zu:
#[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>();
Verfügbarer Kontext:
ctx.user_id()- Aktuelle Benutzer-IDctx.session_id()- Aktuelle Sitzungs-IDctx.agent_name()- Name des Agentenctx.artifacts()- Zugriff auf den Artefaktspeicherctx.search_memory(query)- Suchspeicherdienst
Lang laufende Tools
Für Vorgänge, die viel Zeit in Anspruch nehmen (Datenverarbeitung, externe APIs), verwenden Sie das nicht blockierende Muster:
- Tool starten gibt sofort mit einem task_id zurück
- Hintergrundarbeit läuft asynchron
- Status-Tool ermöglicht es Benutzern, den Fortschritt zu prüfen
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>();
Wichtige Punkte:
.with_long_running(true)teilt dem Agenten mit, dass dieses Tool einen ausstehenden Status zurückgibt- Das Tool startet Arbeit mit
tokio::spawn()und gibt sofort zurück - Stellen Sie ein Tool zur Statusprüfung bereit, damit Benutzer den Fortschritt abfragen können
Dies fügt einen Hinweis hinzu, um zu verhindern, dass der LLM das Tool wiederholt aufruft.
Streaming-Fortschritt aus einem Tool
Lang laufende Tools können Zwischenausgaben an die UI senden, während sie noch
ausgeführt werden, sodass der Benutzer die stdout-Ausgabe eines Shell-Befehls, die Logs eines Builds oder die Bytes eines Downloads live sieht, anstatt auf das Endergebnis zu warten. Rufen Sie
ToolContext::emit_progress auf, sobald Ausgabe eintrifft:
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 }))
}
}
Die Signatur:
async fn emit_progress(&self, stream: &str, chunk: &str)
stream— ein Label für den Chunk:"stdout","stderr"oder ein beliebiger benutzerdefinierter Kanal.chunk— der auszugebende Text (pro Zeile für terminalähnliche Ausgabe ausgeben).
Wie es die UI erreicht. Das Framework leitet jeden Chunk als partielle
Event auf dem
EventStream des Agenten weiter. Ein Consumer erkennt dies mit event.tool_progress_stream() und
rendert es live. Es gibt keinen zweiten Kanal und kein Scraping von Logs — Fortschritt,
Modelltext und das finale Tool-Ergebnis treffen alle in einem geordneten Stream ein.
Abwärtskompatibel. Die Standard-emit_progress ist ein No-Op, daher sind bestehende
Tools und Runner, die nicht streamen, nicht betroffen. Nur Tools, die sich dafür entscheiden, geben
Fortschritt aus, und nur Consumer, die tool_progress_stream() prüfen, beobachten ihn.
Fortschritt ist begrenzt und verlustbehaftet. Ein Tool kann schneller Ausgabe erzeugen, als ein Client sie konsumiert — ein Compiler-Log, ein Shell-Befehl, eine Endlosschleife — daher begrenzt das Framework, was es halten und weiterleiten wird, statt unbegrenzt zu wachsen:
| Grenze | Wert | Beim Überschreiten |
|---|---|---|
| Wartetiefe pro Tool-Batch | 256 Ereignisse | Das Tool wartet bis zu 100 ms auf freien Platz, dann wird der Chunk verworfen |
| Bytes pro Chunk | 8 KiB | Der Chunk wird an einer Zeichen-Grenze abgeschnitten |
| Bytes pro Tool-Aufruf | 1 MiB | Verbleibender Fortschritt wird nicht weitergeleitet |
Wenn Ausgaben aus einem dieser Gründe verworfen werden, wird genau ein Fortschrittsereignis mit dem Text [adk: tool progress truncated] für diesen Aufruf ausgegeben, sodass eine Lücke immer sichtbar ist und nicht stillschweigend bleibt. Ein langsamer Empfänger verlangsamt das Tool daher kurzzeitig, kann es aber niemals unbegrenzt blockieren oder den Speicher erschöpfen.
Diese Limits gelten nur für Fortschritt. Das Endergebnis eines Tools wird nicht beeinflusst, daher solltest du große Ergebnisse innerhalb des Tools abschneiden, wenn dir das wichtig ist.
Siehe das
streaming_bash-Beispiel für eine vollständige Web-UI, die Live-bash-Ausgabe und einmalige Tool-Ergebnisse (read_file,grep,glob) aus einem einzigen Ereignis-Feed rendert. Das Streaming-bash-Tool selbst befindet sich inadk-devtools.
Ausführungsbeispiele
cargo adk new tool_agent --template tools
cd tool_agent
cargo run
Beste Praktiken
- Klare Beschreibungen - Hilf dem LLM zu verstehen, wann das Tool verwendet werden soll
- Eingaben validieren - Gib hilfreiche Fehlermeldungen für fehlende Parameter zurück
- Strukturierten JSON zurückgeben - Verwende klare Feldnamen
- Tools fokussiert halten - Jedes Tool sollte eine Sache gut erledigen
- Schemas verwenden - Definiere für komplexe Tools Parameterschemas
- Sichere Nur-Lese-Tools markieren - Setze sowohl
.with_read_only(true)als auch.with_concurrency_safe(true), damitAuto-Dispatch sie in seinen gleichzeitigen Teilmengen einbeziehen kann
Tool-Metadaten: Nur-Lese und Parallelität
Markiere Tools als nur lesbar oder parallelitätssicher, um eine intelligentere Ausführung zu ermöglichen:
// 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}))
});
Wenn ToolExecutionStrategy::Auto aktiv ist, führt die Dispatch-Schleife Aufrufe zunächst parallel aus, wenn ihre ausgewählten Tools sowohl von is_read_only() als auch von is_concurrency_safe() true zurückgeben. Danach werden alle verbleibenden Aufrufe sequenziell ausgeführt. ToolExecutionStrategy::Parallel ist eine explizite Überschreibung, die diese Signale umgeht, sodass der Aufrufer die Verantwortung für die Parallelitätssicherheit trägt.
SimpleToolContext: Tools außerhalb der Agentenschleife verwenden
Wenn du ein Tool außerhalb der Agentenschleife aufrufen musst (Testen, MCP-Servermodus, Delegation an einen Unteragenten), verwende SimpleToolContext statt die vollständige ToolContext-Trait-Hierarchie zu implementieren:
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?;
Standardwerte: user_id() → "anonymous", session_id() / branch() → "", artifacts() → None, search_memory() → leeres vec. Sowohl invocation_id als auch function_call_id werden automatisch UUIDs erzeugt. Setze mit with_session_id(...) eine echte Sitzung, wenn ein Tool je Sitzung weiterleitet oder Zustand speichert.
StatefulTool: Gemeinsamer Zustand über Aufrufe hinweg
Für Tools, die Zustand zwischen Aufrufen beibehalten müssen (Zähler, Caches, Verbindungspools), verwende 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 klont den Arc<S> bei jedem Aufruf (günstige Erhöhung des Referenzzählers), sodass alle Ausführungen denselben zugrunde liegenden Zustand teilen. Es unterstützt dieselben Builder-Methoden wie FunctionTool: with_long_running, with_parameters_schema, with_response_schema, with_scopes, with_read_only und with_concurrency_safe.
Verwandt
- Integrierte Tools - Vorgefertigte Tools (GoogleSearch, ExitLoop)
- MCP Tools - Integration des Model Context Protocol
- LlmAgent - Tools zu Agenten hinzufügen
Multimodale Funktionsantworten
Gemini-3-Modelle unterstützen den Empfang von Bildern, Audio, PDFs und Dateireferenzen in Funktionsantworten — nicht nur JSON. Tools können multimodale Daten zurückgeben, indem sie inline_data- und/oder file_data-Arrays in ihren JSON-Rückgabewert aufnehmen:
/// 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
}]
}))
}
Das Framework erkennt automatisch:
inline_data/file_dataüberFunctionResponseData::from_tool_result()- kodiert Inline-Binärdaten in Base64
- verschachtelt die Teile innerhalb des
functionResponse-Wire-Objekts (entsprechend dem Gemini-3-API-Format)
Dateireferenzen
Für große Dateien, die extern gespeichert sind, verwende file_data mit einer URI statt Bytes einzubetten:
Ok(json!({
"response": { "document_id": "report-2024", "pages": 12 },
"file_data": [{
"mime_type": "application/pdf",
"file_uri": "gs://my-bucket/reports/report-2024.pdf"
}]
}))
Direkte Konstruktion
Für Code auf Framework-Ebene (benutzerdefinierte Agenten, Konvertierungsschichten) konstruiere FunctionResponseData direkt:
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);
Hinweis: Multimodale Funktionsantworten erfordern Modelle der Gemini-3-Serie (
gemini-3-flash-preview,gemini-3-pro-preview). Frühere Modelle geben einen 400-Fehler zurück.
Siehe examples/multimodal_function_response/ für ein vollständiges, funktionierendes Beispiel.
Vorherige: ← mistral.rs | Nächste: Integrierte Tools →