LlmAgent
ADK-Rust에서 추론과 의사 결정을 위해 대규모 언어 모델을 사용하는 핵심 에이전트 유형이 바로 LlmAgent입니다.
빠른 시작
새 프로젝트를 생성합니다:
cargo new llm_agent
cd llm_agent
Cargo.toml에 종속성을 추가합니다:
[dependencies]
adk-rust = "2.0.0"
tokio = { version = "1.40", features = ["full"] }
dotenvy = "0.15"
serde_json = "1.0"
API 키로 .env를 생성합니다:
echo 'GOOGLE_API_KEY=your-api-key' > .env
src/main.rs를 교체합니다:
use adk_rust::prelude::*;
use adk_rust::{SessionId, UserId};
use adk_rust::Launcher;
use std::sync::Arc;
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = GeminiModel::new(&api_key, "gemini-2.5-flash")?;
let agent = LlmAgentBuilder::new("my_agent")
.instruction("You are a helpful assistant.")
.model(Arc::new(model))
.build()?;
Launcher::new(Arc::new(agent)).run().await?;
Ok(())
}
실행합니다:
cargo run
에이전트와 상호작용하기
대화형 프롬프트가 표시됩니다:
🤖 Agent ready! Type your questions (or 'exit' to quit).
You: Hello! What can you help me with?
Assistant: Hello! I'm a helpful assistant. I can help you with:
- Answering questions on various topics
- Explaining concepts clearly
- Having a conversation
What would you like to know?
You: exit
👋 Goodbye!
지시사항으로 에이전트 동작 구성하기
instruction() 메서드는 에이전트의 성격과 동작을 정의합니다. 이는 모든 응답을 안내하는 시스템 프롬프트입니다:
// A formal business assistant
let formal_agent = LlmAgentBuilder::new("formal_assistant")
.instruction("You are a professional business consultant. \
Use formal language. Be concise and data-driven.")
.model(Arc::new(model))
.build()?;
// A friendly coding tutor
let tutor_agent = LlmAgentBuilder::new("code_tutor")
.instruction("You are a friendly coding tutor for beginners. \
Explain concepts simply. Use examples. \
Encourage questions. Never make the user feel bad for not knowing.")
.model(Arc::new(model))
.build()?;
// A creative storyteller
let story_agent = LlmAgentBuilder::new("storyteller")
.instruction("You are a creative storyteller. \
Craft engaging narratives with vivid descriptions. \
Use plot twists and memorable characters.")
.model(Arc::new(model))
.build()?;
예시 출력
사용자 프롬프트: "Rust가 무엇인가요?"
공식적인 비즈니스 어시스턴트:
Rust is a systems programming language focused on performance, memory safety,
and concurrency. Key metrics: 0 runtime overhead, 0 garbage collection.
Adoption growing 30% YoY in enterprise. Recommended for performance-critical
applications where reliability is paramount.
친근한 코딩 튜터:
Great question! 🎉 Rust is a programming language that helps you write fast
and safe code. Think of it like a helpful friend who stops you from making
mistakes before they happen!
Here's a simple example:
let name = "Alice"; // This creates a variable
What makes Rust special is that it catches bugs at compile time, not when
your program is running. Pretty cool, right? Want me to explain more?
창의적인 스토리텔러:
In the twilight of 2010, in a secret Mozilla laboratory, a lone developer
named Graydon Hoare breathed life into something extraordinary—a language
that would challenge the old gods of C and C++.
They called it Rust, named for a fungus that survives against all odds.
Like a phoenix rising from the ashes of segmentation faults and memory
leaks, Rust promised a new era: speed without sacrifice, power without peril.
And so, the revolution began...
지시사항 템플릿화
지시사항은 {var} 구문을 사용한 변수 주입을 지원합니다. 변수는 런타임에 세션 상태에서 해석됩니다:
let agent = LlmAgentBuilder::new("personalized")
.instruction("You are helping {user_name}. Their role is {user_role}. \
Tailor your responses to their expertise level.")
.model(Arc::new(model))
.build()?;
템플릿 사용 단계별 가이드:
- 에이전트를 생성할 때 지시사항에 템플릿 변수를 포함합니다
- 상태를 관리하기 위해 Runner와 SessionService를 설정합니다
- 템플릿과 일치하는 상태 변수를 사용해 세션을 생성합니다
- 에이전트를 실행합니다 - 템플릿이 자동으로 치환됩니다
다음은 완전한 동작 예제입니다:
use adk_rust::prelude::*;
use adk_rust::{SessionId, UserId};
use adk_rust::runner::{Runner, RunnerConfig};
use adk_rust::session::{CreateRequest, InMemorySessionService, SessionService};
use adk_rust::futures::StreamExt;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = GeminiModel::new(&api_key, "gemini-2.5-flash")?;
// 1. Agent with templated instruction
let agent = LlmAgentBuilder::new("personalized")
.instruction("You are helping {user_name}. Their role is {user_role}. \
Tailor your responses to their expertise level.")
.model(Arc::new(model))
.build()?;
// 2. Create session service and runner
let session_service = Arc::new(InMemorySessionService::new());
let runner = Runner::new(RunnerConfig {
app_name: "templating_demo".to_string(),
agent: Arc::new(agent),
session_service: session_service.clone(),
artifact_service: None,
memory_service: None,
run_config: None,
})?;
// 3. Create session with state variables
let mut state = HashMap::new();
state.insert("user_name".to_string(), json!("Alice"));
state.insert("user_role".to_string(), json!("Senior Developer"));
let session = session_service.create(CreateRequest {
app_name: "templating_demo".to_string(),
user_id: "user123".to_string(),
session_id: None,
state,
}).await?;
// 4. Run the agent - instruction becomes:
// "You are helping Alice. Their role is Senior Developer..."
let mut response_stream = runner.run(
UserId::new("user123")?,
SessionId::new(session.id())?,
Content::new("user").with_text("Explain async/await in Rust"),
).await?;
// Print the response
while let Some(event) = response_stream.next().await {
let event = event?;
if let Some(content) = event.content() {
for part in &content.parts {
if let Part::Text { text } = part {
print!("{}", text);
}
}
}
}
Ok(())
}
템플릿 변수 유형:
| 패턴 | 예시 | 소스 |
|---|---|---|
{var} | {user_name} | 세션 상태 |
{prefix:var} | {user:name}, {app:config} | 접두사 상태 |
{var?} | {user_name?} | 선택 사항(누락된 경우 비어 있음) |
{artifact.file} | {artifact.resume.pdf} | 아티팩트 내용 |
출력 예시:
템플릿: "You are helping {user_name}. Their role is {user_role}."
변환 후: "You are helping Alice. Their role is Senior Developer."
이제 에이전트는 사용자의 이름과 전문성 수준에 따라 개인화된 콘텐츠로 응답합니다!
도구 추가하기
도구는 에이전트에 대화 이상의 기능을 제공합니다. 데이터 가져오기, 계산 수행, 웹 검색, 또는 외부 APIs 호출이 가능합니다. LLM는 사용자의 요청에 따라 언제 도구를 사용할지 결정합니다.
도구가 작동하는 방식
- 에이전트가 사용자 메시지를 받음 → "What's the weather in Tokyo?"
- LLM가 도구 호출을 결정함 →
get_weather을{"city": "Tokyo"}과 함께 선택 - 도구 실행 →
{"temperature": "22°C", "condition": "sunny"}반환 - LLM가 응답을 형식화함 → "The weather in Tokyo is sunny at 22°C."
FunctionTool로 도구 만들기
FunctionTool는 도구를 만드는 가장 간단한 방법입니다. 모든 async Rust 함수를 감싸면 LLM가 이를 호출할 수 있습니다. 이름, 설명, 그리고 JSON 인수를 받아 JSON 결과를 반환하는 핸들러 함수를 제공합니다.
let weather_tool = FunctionTool::new(
"get_weather", // Tool name (used by LLM)
"Get the current weather for a city", // Description (helps LLM decide when to use it)
|_ctx, args| async move { // Handler function
let city = args.get("city") // Extract arguments from JSON
.and_then(|v| v.as_str())
.unwrap_or("unknown");
Ok(json!({ "city": city, "temperature": "22°C" })) // Return JSON result
},
);
내장 provider-native 도구는 이제 같은 에이전트 안에서 FunctionTool 인스턴스와 함께 혼합할 수 있습니다. ADK는 일반 함수 도구는 로컬에서 계속 실행하면서, native 도구 선언은 provider로 전달합니다.
멀티 도구 에이전트 만들기
새 프로젝트를 생성합니다:
cargo new tool_agent
cd tool_agent
Cargo.toml에 의존성을 추가합니다:
[dependencies]
adk-rust = { version = "2.0.0", features = ["tools"] }
tokio = { version = "1.40", features = ["full"] }
dotenvy = "0.15"
serde_json = "1.0"
.env을 생성합니다:
echo 'GOOGLE_API_KEY=your-api-key' > .env
src/main.rs를 세 개의 도구를 가진 에이전트로 교체합니다:
use adk_rust::prelude::*;
use adk_rust::Launcher;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = GeminiModel::new(&api_key, "gemini-2.5-flash")?;
// Tool 1: Weather lookup
let weather_tool = FunctionTool::new(
"get_weather",
"Get the current weather for a city. Parameters: city (string)",
|_ctx, args| async move {
let city = args.get("city").and_then(|v| v.as_str()).unwrap_or("unknown");
Ok(json!({ "city": city, "temperature": "22°C", "condition": "sunny" }))
},
);
// Tool 2: Calculator
let calculator = FunctionTool::new(
"calculate",
"Perform arithmetic. Parameters: a (number), b (number), operation (add/subtract/multiply/divide)",
|_ctx, args| async move {
let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
let op = args.get("operation").and_then(|v| v.as_str()).unwrap_or("add");
let result = match op {
"add" => a + b,
"subtract" => a - b,
"multiply" => a * b,
"divide" => if b != 0.0 { a / b } else { 0.0 },
_ => 0.0,
};
Ok(json!({ "result": result }))
},
);
// Tool 3: Built-in Google Search (Note: Currently unsupported in ADK-Rust)
// let search_tool = GoogleSearchTool::new();
// Build agent with weather and calculator tools
let agent = LlmAgentBuilder::new("multi_tool_agent")
.instruction("You are a helpful assistant. Use tools when needed: \
- get_weather for weather questions \
- calculate for math")
.model(Arc::new(model))
.tool(Arc::new(weather_tool))
.tool(Arc::new(calculator))
// .tool(Arc::new(search_tool)) // Currently unsupported
.build()?;
Launcher::new(Arc::new(agent)).run().await?;
Ok(())
}
에이전트를 실행합니다:
cargo run
예시 상호작용
You: What's 15% of 250?
Assistant: [Using calculate tool with a=250, b=0.15, operation=multiply]
15% of 250 is 37.5.
You: What's the weather in Tokyo?
Assistant: [Using get_weather tool with city=Tokyo]
The weather in Tokyo is sunny with a temperature of 22°C.
You: Search for latest Rust features
Assistant: I don't have access to search functionality at the moment, but I can help with other questions about Rust or perform calculations!
JSON 스키마를 사용한 구조화된 출력
구조화된 데이터가 필요한 애플리케이션에서는 output_schema()를 사용합니다:
use adk_rust::prelude::*;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = GeminiModel::new(&api_key, "gemini-2.5-flash")?;
let extractor = LlmAgentBuilder::new("entity_extractor")
.instruction("Extract entities from the given text.")
.model(Arc::new(model))
.output_schema(json!({
"type": "object",
"properties": {
"people": {
"type": "array",
"items": { "type": "string" }
},
"locations": {
"type": "array",
"items": { "type": "string" }
},
"dates": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["people", "locations", "dates"]
}))
.build()?;
println!("Entity extractor ready!");
Ok(())
}
provider가 스키마를 강제하는 방식
output_schema는 GenerateContentConfig::response_schema로 provider에 전달됩니다.
provider가 이를 어떻게 처리하는지는 다르지만, 에이전트는 어느 경우든 결과를 검증합니다:
| 공급자 | 네이티브 강제 적용 |
|---|---|
| Gemini | 전체 스키마, response schema로 전송됨 |
| OpenAI and OpenAI-compatible | 전체 스키마, strict json_schema response format으로 전송됨 |
| OpenRouter | 전체 스키마 |
| DeepSeek | JSON 구문만 — DeepSeek의 JSON Output mode에는 json_schema 변형이 없으므로, 스키마는 agent의 검증에 의해 강제됩니다 |
공급자가 구문만 강제하거나 아무것도 강제하지 않는 경우에도, 에이전트는 여전히 스키마를 지시로 주입하고 응답을 검증하므로, 규격에 맞지 않는 답변은 잘못된 데이터를 반환하는 대신 재시도를 발생시킵니다.
참고: DeepSeek는 JSON Output이 켜져 있을 때 프롬프트에 "json"이라는 단어가 포함되어 있어야 하며, 그렇지 않으면 API가 빈 내용을 반환할 수 있습니다. 어댑터는 프롬프트에 이미 그 언급이 없는 경우 그 언급을 자체적으로 추가합니다.
JSON 출력 예시
입력: "John met Sarah in Paris on December 25th"
출력:
{
"people": ["John", "Sarah"],
"locations": ["Paris"],
"dates": ["December 25th"]
}
고급 기능
내용 포함
대화 기록 표시 여부를 제어합니다:
// Full history (default)
.include_contents(IncludeContents::Default)
// Stateless - sees only injected instructions plus the current user turn
.include_contents(IncludeContents::None)
출력 키
에이전트 응답을 세션 상태에 저장합니다:
.output_key("summary") // Response saved to state["summary"]
동적 지시사항
런타임에 지시사항을 계산합니다:
.instruction_provider(|ctx| {
Box::pin(async move {
let user_id = ctx.user_id();
Ok(format!("You are assisting user {}.", user_id))
})
})
콜백
에이전트 동작을 가로챕니다:
.before_model_callback(|ctx, request| {
Box::pin(async move {
println!("About to call LLM with {} messages", request.contents.len());
Ok(BeforeModelResult::Continue)
})
})
빌더 참조
| 메서드 | 설명 |
|---|---|
new(name) | 에이전트 이름으로 빌더를 생성합니다 |
model(Arc<dyn Llm>) | LLM(필수)를 설정합니다 |
description(text) | 에이전트 설명 |
instruction(text) | 시스템 프롬프트 |
tool(Arc<dyn Tool>) | 정적 도구를 추가합니다 |
toolset(Arc<dyn Toolset>) | 호출마다 해결되는 동적 도구셋을 추가합니다 |
output_schema(json) | JSON 구조화된 출력용 스키마 |
output_key(key) | 응답을 상태에 저장 |
include_contents(mode) | 히스토리 표시 여부 |
max_iterations(n) | 최대 LLM 왕복 횟수 (기본값: 100) |
tool_execution_strategy(strategy) | 도구 디스패치 모드: Sequential, Parallel, 또는 Auto |
default_retry_budget(RetryBudget) | 실패한 도구를 지연을 두고 최대 N번까지 재시도 |
tool_retry_budget(name, RetryBudget) | 도구별 재시도 재정의 |
circuit_breaker_threshold(u32) | N번 연속 실패 후 도구 비활성화 |
on_tool_error(callback) | 도구 실패를 위한 폴백 핸들러 등록 |
after_tool_callback_full(callback) | 도구, 인자, 응답을 포함하는 V2 rich after-tool 콜백 |
build() | 에이전트를 생성합니다 |
반복 제어
max_iterations() 메서드는 에이전트가 중지되기 전에 수행할 수 있는 LLM 왕복 횟수를 제한합니다. 이는 다음에 유용합니다:
- 무한 루프에 빠진 도구 호출 방지
- 프로덕션에서 비용 제어
- 복잡한 작업에 대해 합리적인 상한 설정
let agent = LlmAgentBuilder::new("bounded_agent")
.model(Arc::new(model))
.instruction("You are a helpful assistant.")
.tool(Arc::new(my_tool))
.max_iterations(10) // Stop after 10 LLM calls
.build()?;
기본값은 100회 반복이며, 대부분의 사용 사례에 충분합니다. 단순한 Q&A 에이전트에는 더 낮은 값(5-20)을 권장하며, 복잡한 다단계 추론 작업에는 더 높은 값이 필요할 수 있습니다.
동적 도구셋
호출 컨텍스트에 따라 달라지는 도구(예: 사용자별 브라우저 세션)의 경우, .tool() 대신 .toolset()를 사용하세요. 도구셋은 각 run() 호출이 시작될 때 해석됩니다:
use adk_browser::{BrowserSessionPool, BrowserToolset, BrowserConfig};
use adk_agent::LlmAgentBuilder;
use std::sync::Arc;
let pool = Arc::new(BrowserSessionPool::new(BrowserConfig::new(), 10));
let toolset = Arc::new(BrowserToolset::with_pool(pool));
let agent = LlmAgentBuilder::new("web_agent")
.model(model)
.instruction("You are a web automation assistant.")
.toolset(toolset) // Resolved per-user at runtime
.build()?;
같은 에이전트에서 정적 .tool()과 동적 .toolset()를 함께 사용할 수 있습니다. 정적 도구와 도구셋 간에 도구 이름이 중복되면 결정적 오류가 발생합니다.
RealtimeAgentBuilder도 동일한 의미 체계로 .toolset()를 지원하므로, 실시간 음성 에이전트도 동적 도구 해석을 사용합니다.
도구셋 구성
FilteredToolset, MergedToolset, PrefixedToolset를 adk-tool에서 사용하여 복잡한 도구셋 구성을 작성할 수 있습니다:
use adk_tool::{BasicToolset, FilteredToolset, MergedToolset, PrefixedToolset, string_predicate};
// Prefix weather tools to avoid name collisions
let weather = Arc::new(PrefixedToolset::new(weather_toolset, "wx"));
// Filter utility tools to only expose search and calculate
let utils = Arc::new(FilteredToolset::new(
utility_toolset,
string_predicate(vec!["search".into(), "calculate".into()]),
));
// Merge into a single toolset
let composed = MergedToolset::new("all", vec![weather, utils]);
let agent = LlmAgentBuilder::new("agent")
.model(model)
.toolset(Arc::new(composed))
.build()?;
모든 구성 유틸리티는 Toolset 구현체와 함께 동작하며, 여기에는 McpToolset와 BrowserToolset도 포함됩니다.
병렬 도구 실행
LLM가 하나의 응답에서 여러 도구 호출을 반환할 때, 이를 어떤 방식으로 실행할지 제어할 수 있습니다:
use adk_core::ToolExecutionStrategy;
let agent = LlmAgentBuilder::new("fast_agent")
.model(Arc::new(model))
.instruction("You are a research assistant. Use multiple tools in parallel.")
// Auto requires both safety signals for concurrent inclusion
.tool_execution_strategy(ToolExecutionStrategy::Auto)
.tool(Arc::new(
search_tool
.with_read_only(true)
.with_concurrency_safe(true),
))
.tool(Arc::new(
lookup_tool
.with_read_only(true)
.with_concurrency_safe(true),
))
.tool(Arc::new(save_tool)) // runs after the concurrent safe subset
.build()?;
세 가지 전략을 사용할 수 있습니다:
Sequential(기본값) — 도구가 LLM 순서대로 하나씩 실행됩니다Parallel— 모든 도구가 동시에 실행됩니다. 이 명시적 재정의는 안전 메타데이터를 우회하므로, 안전성은 호출자가 책임집니다Auto— 도구가 읽기 전용이면서 동시성 안전한 호출은 먼저 동시에 실행되고, 나머지 호출은 이후 순차적으로 실행됩니다
결과는 전략과 관계없이 항상 원래 LLM 순서대로 반환됩니다. 실패한 도구는 배치를 중단하지 않고 JSON 오류 응답을 생성합니다.
전략은 LlmAgentBuilder::tool_execution_strategy()를 통해 에이전트별로 설정합니다. 설정하지 않으면 기본값은 Sequential입니다.
도구 복원력
프로덕션 에이전트를 위해 재시도 예산과 서킷 브레이커를 구성하세요:
use adk_core::RetryBudget;
use std::time::Duration;
let agent = LlmAgentBuilder::new("resilient_agent")
.model(model)
.tool(Arc::new(my_tool))
// Retry all tools up to 2 times with 500ms delay
.default_retry_budget(RetryBudget::new(2, Duration::from_millis(500)))
// Override for a specific tool
.tool_retry_budget("flaky_api", RetryBudget::new(4, Duration::from_secs(1)))
// Disable a tool after 3 consecutive failures in one invocation
.circuit_breaker_threshold(3)
// Provide a fallback when a tool fails
.on_tool_error(Box::new(|_ctx, tool, _args, error| {
Box::pin(async move {
tracing::warn!(tool = tool.name(), %error, "tool failed");
Ok(None) // None = propagate error; Some(value) = use as fallback
})
}))
.build()?;
도구 후 콜백은 CallbackContext::tool_outcome()를 통해 구조화된 ToolOutcome 메타데이터를 검사할 수 있습니다:
.after_tool_callback(Box::new(|ctx| {
Box::pin(async move {
if let Some(outcome) = ctx.tool_outcome() {
println!(
"Tool '{}' {} in {:?} (attempt {})",
outcome.tool_name,
if outcome.success { "succeeded" } else { "failed" },
outcome.duration,
outcome.attempt,
);
}
Ok(None)
})
}))
전체 예제
여러 도구(날씨, 계산기, 검색)를 사용하고 출력이 세션 상태에 저장되는 프로덕션용 에이전트:
use adk_rust::prelude::*;
use adk_rust::Launcher;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = GeminiModel::new(&api_key, "gemini-2.5-flash")?;
// Weather tool
let weather = FunctionTool::new(
"get_weather",
"Get weather for a city. Parameters: city (string)",
|_ctx, args| async move {
let city = args.get("city").and_then(|v| v.as_str()).unwrap_or("unknown");
Ok(json!({
"city": city,
"temperature": "22°C",
"humidity": "65%",
"condition": "partly cloudy"
}))
},
);
// Calculator tool
let calc = FunctionTool::new(
"calculate",
"Math operations. Parameters: expression (string like '2 + 2')",
|_ctx, args| async move {
let expr = args.get("expression").and_then(|v| v.as_str()).unwrap_or("0");
Ok(json!({ "expression": expr, "result": "computed" }))
},
);
// Build the full agent
let agent = LlmAgentBuilder::new("assistant")
.description("A helpful assistant with weather and calculation abilities")
.instruction("You are a helpful assistant. \
Use the weather tool for weather questions. \
Use the calculator for math. \
Be concise and friendly.")
.model(Arc::new(model))
.tool(Arc::new(weather))
.tool(Arc::new(calc))
// .tool(Arc::new(GoogleSearchTool::new())) // Provider-native tools can be mixed with FunctionTool
.output_key("last_response")
.build()?;
println!("✅ Agent '{}' ready!", agent.name());
Launcher::new(Arc::new(agent)).run().await?;
Ok(())
}
다음 프롬프트를 시도해 보세요:
You: What's 25 times 4?
Assistant: It's 100.
You: How's the weather in New York?
Assistant: The weather in New York is partly cloudy with a temperature of 22°C and 65% humidity.
You: Calculate 15% tip on $85
Assistant: A 15% tip on $85 is $12.75, making the total $97.75.
관련 항목
- 워크플로 에이전트 - 순차, 병렬, 루프 에이전트
- 멀티 에이전트 시스템 - 에이전트 계층 구조 구축
- 함수 도구 - 사용자 정의 도구 생성
- 콜백 - 에이전트 동작 가로채기
이전: 빠른 시작 | 다음: 워크플로 에이전트 →