콜백
adk-rust의 콜백은 핵심 실행 지점에서 Agent 동작을 관찰, 사용자 지정 및 제어하기 위한 후크를 제공합니다. 이를 통해 로깅, 가드레일, 캐싱, 응답 수정 등이 가능합니다.
개요
adk-rust는 Agent 실행의 다양한 단계를 가로채는 6가지 콜백 유형을 지원합니다.
| 콜백 유형 | 실행 시점 | 사용 사례 |
|---|---|---|
before_agent | Agent가 처리를 시작하기 전 | 입력 유효성 검사, 로깅, 조기 종료 |
after_agent | Agent 완료 후 | 응답 수정, 로깅, 정리 |
before_model | LLM 호출 전 | 요청 수정, 캐싱, 속도 제한 |
after_model | LLM 응답 후 | 응답 필터링, 로깅, 캐싱 |
before_tool | 도구 실행 전 | 권한 확인, 매개변수 유효성 검사 |
after_tool | 도구 실행 후 | 결과 수정, 로깅 |
콜백 유형
Agent 콜백
Agent 콜백은 전체 Agent 실행 주기를 감쌉니다.
use adk_rust::prelude::*;
use std::sync::Arc;
// BeforeAgentCallback type signature
type BeforeAgentCallback = Box<
dyn Fn(Arc<dyn CallbackContext>)
-> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>>
+ Send + Sync
>;
// AfterAgentCallback type signature
type AfterAgentCallback = Box<
dyn Fn(Arc<dyn CallbackContext>)
-> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>>
+ Send + Sync
>;
모델 콜백
모델 콜백은 LLM 요청 및 응답을 가로챕니다.
use adk_rust::prelude::*;
use std::sync::Arc;
// BeforeModelResult - 콜백 이후 발생하는 상황을 제어합니다.
pub enum BeforeModelResult {
Continue(LlmRequest), // (수정되었을 수 있는) 요청으로 계속 진행합니다.
Skip(LlmResponse), // 모델 호출을 건너뛰고, 대신 이 응답을 사용합니다.
}
// BeforeModelCallback - 요청을 수정하거나 모델 호출을 건너뛸 수 있습니다.
type BeforeModelCallback = Box<
dyn Fn(Arc<dyn CallbackContext>, LlmRequest)
-> Pin<Box<dyn Future<Output = Result<BeforeModelResult>> + Send>>
+ Send + Sync
>;
// AfterModelCallback - 응답을 수정할 수 있습니다.
type AfterModelCallback = Box<
dyn Fn(Arc<dyn CallbackContext>, LlmResponse)
-> Pin<Box<dyn Future<Output = Result<Option<LlmResponse>>> + Send>>
+ Send + Sync
>;
도구 콜백
도구 콜백은 도구 실행을 가로챕니다.
use adk_rust::prelude::*;
use std::sync::Arc;
// BeforeToolCallback - Some(Content)를 반환하여 도구 실행을 건너뛸 수 있습니다.
type BeforeToolCallback = Box<
dyn Fn(Arc<dyn CallbackContext>)
-> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>>
+ Send + Sync
>;
// AfterToolCallback - 도구 결과를 수정할 수 있습니다.
type AfterToolCallback = Box<
dyn Fn(Arc<dyn CallbackContext>)
-> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>>
+ Send + Sync
>;
반환 값 의미론
콜백은 실행 흐름을 제어하기 위해 다양한 반환 값을 사용합니다.
Agent/Tool 콜백
| 반환 값 | 효과 |
|---|---|
Ok(None) | 정상적인 실행 계속 |
Ok(Some(content)) | 제공된 content로 재정의/건너뛰기 |
Err(e) | 오류와 함께 실행 중단 |
Model 콜백
BeforeModelCallback은 BeforeModelResult를 사용합니다:
| 반환 값 | 효과 |
|---|---|
Ok(BeforeModelResult::Continue(request)) | (수정되었을 수 있는) request로 계속 진행 |
Ok(BeforeModelResult::Skip(response)) | 모델 호출을 건너뛰고, 이 response를 대신 사용 |
Err(e) | 오류와 함께 실행 중단 |
AfterModelCallback은 Option<LlmResponse>를 사용합니다:
| 반환 값 | 효과 |
|---|---|
Ok(None) | 원본 response 유지 |
Ok(Some(response)) | 수정된 response로 대체 |
Err(e) | 오류와 함께 실행 중단 |
요약
- Agent/Tool 콜백 이전: 계속하려면
None을, 건너뛰려면Some(content)를 반환합니다. - Model 콜백 이전: 진행하려면
Continue(request)를, 모델을 우회하려면Skip(response)를 반환합니다. - 콜백 이후: 원본을 유지하려면
None을, 대체하려면Some(...)을 반환합니다.
Agent에 콜백 추가하기
콜백은 LlmAgentBuilder를 사용하여 agent에 추가됩니다:
use adk_rust::prelude::*;
use std::sync::Arc;
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
let agent = LlmAgentBuilder::new("my_agent")
.model(model)
.instruction("You are a helpful assistant.")
// before_agent 콜백 추가
.before_callback(Box::new(|ctx| {
Box::pin(async move {
println!("Agent starting: {}", ctx.agent_name());
Ok(None) // 실행 계속
})
}))
// after_agent 콜백 추가
.after_callback(Box::new(|ctx| {
Box::pin(async move {
println!("Agent completed: {}", ctx.agent_name());
Ok(None) // 원본 결과 유지
})
}))
.build()?;
Ok(())
}
CallbackContext 인터페이스
CallbackContext trait는 실행 컨텍스트에 대한 접근을 제공합니다:
use adk_rust::prelude::*;
#[async_trait]
pub trait CallbackContext: ReadonlyContext {
/// 아티팩트 저장소 접근 (구성된 경우)
fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
}
// CallbackContext는 ReadonlyContext를 확장합니다
#[async_trait]
pub trait ReadonlyContext: Send + Sync {
/// 현재 호출 ID
fn invocation_id(&self) -> &str;
/// 현재 agent의 이름
fn agent_name(&self) -> &str;
/// 세션의 사용자 ID
fn user_id(&self) -> &str;
/// 애플리케이션 이름
fn app_name(&self) -> &str;
/// 세션 ID
fn session_id(&self) -> &str;
/// 현재 브랜치 (다중 agent용)
fn branch(&self) -> &str;
/// 사용자의 입력 content
fn user_content(&self) -> &Content;
}
일반적인 패턴
로깅 콜백
모든 Agent 상호작용을 로그합니다:
use adk_rust::prelude::*;
use std::sync::Arc;
let agent = LlmAgentBuilder::new("logged_agent")
.model(model)
.before_callback(Box::new(|ctx| {
Box::pin(async move {
println!("[LOG] Agent '{}' starting", ctx.agent_name());
println!("[LOG] Session: {}", ctx.session_id());
println!("[LOG] User: {}", ctx.user_id());
Ok(None)
})
}))
.after_callback(Box::new(|ctx| {
Box::pin(async move {
println!("[LOG] Agent '{}' completed", ctx.agent_name());
Ok(None)
})
}))
.build()?;
입력 가드레일
부적절한 콘텐츠를 처리 전에 차단합니다:
use adk_rust::prelude::*;
use std::sync::Arc;
let agent = LlmAgentBuilder::new("guarded_agent")
.model(model)
.before_callback(Box::new(|ctx| {
Box::pin(async move {
// 차단된 콘텐츠에 대해 사용자 입력을 확인합니다
let user_content = ctx.user_content();
for part in &user_content.parts {
if let Part::Text { text } = part {
if text.to_lowercase().contains("blocked_word") {
// 거부 메시지와 함께 조기에 반환합니다
return Ok(Some(Content {
role: "model".to_string(),
parts: vec![Part::Text {
text: "I cannot process that request.".to_string(),
}],
}));
}
}
}
Ok(None) // 정상 실행을 계속합니다
})
}))
.build()?;
응답 캐싱 (모델 실행 전)
LLM 응답을 캐싱하여 API 호출을 줄입니다:
use adk_rust::prelude::*;
use std::sync::Arc;
use std::collections::HashMap;
use std::sync::Mutex;
// 간단한 인메모리 캐시
let cache: Arc<Mutex<HashMap<String, LlmResponse>>> = Arc::new(Mutex::new(HashMap::new()));
let cache_clone = cache.clone();
let agent = LlmAgentBuilder::new("cached_agent")
.model(model)
.before_model_callback(Box::new(move |ctx, request| {
let cache = cache_clone.clone();
Box::pin(async move {
// 요청 내용으로부터 캐시 키를 생성합니다
let key = format!("{:?}", request.contents);
// 캐시 확인
if let Some(cached) = cache.lock().unwrap().get(&key) {
println!("[CACHE] 요청에 대한 캐시 적중");
return Ok(BeforeModelResult::Skip(cached.clone()));
}
println!("[CACHE] 캐시 미스, 모델 호출");
Ok(BeforeModelResult::Continue(request)) // 모델로 계속 진행
})
}))
.build()?;
멀티모달 콘텐츠 주입 (모델 실행 전)
멀티모달 분석을 위해 이미지 또는 기타 바이너리 콘텐츠를 LLM 요청에 주입합니다:
use adk_rust::prelude::*;
use adk_rust::artifact::{ArtifactService, LoadRequest};
use std::sync::Arc;
// 미리 로드된 이미지를 포함한 Artifact Service
let artifact_service: Arc<dyn ArtifactService> = /* ... */;
let callback_service = artifact_service.clone();
let agent = LlmAgentBuilder::new("image_analyst")
.model(model)
.instruction("Describe the image provided by the user.")
.before_model_callback(Box::new(move |_ctx, mut request| {
let service = callback_service.clone();
Box::pin(async move {
// 아티팩트 저장소에서 이미지를 로드합니다
if let Ok(response) = service.load(LoadRequest {
app_name: "my_app".to_string(),
user_id: "user".to_string(),
session_id: "session".to_string(),
file_name: "user:photo.png".to_string(),
version: None,
}).await {
// 사용자 메시지에 이미지를 주입합니다
if let Some(last_content) = request.contents.last_mut() {
if last_content.role == "user" {
last_content.parts.push(response.part);
}
}
}
Ok(BeforeModelResult::Continue(request))
})
}))
.build()?;
이 패턴은 멀티모달 AI에 필수적입니다. Tool 응답은 JSON 텍스트이므로 모델이 Tool이 반환한 이미지를 "볼" 수 없기 때문입니다. 이미지를 요청에 직접 주입함으로써, 모델은 실제 이미지 데이터를 받게 됩니다.
응답 수정 (모델 실행 후)
모델 응답을 수정하거나 필터링합니다:
use adk_rust::prelude::*;
use std::sync::Arc;
let agent = LlmAgentBuilder::new("filtered_agent")
.model(model)
.after_model_callback(Box::new(|ctx, mut response| {
Box::pin(async move {
// 응답 내용을 수정합니다
if let Some(ref mut content) = response.content {
for part in &mut content.parts {
if let Part::Text { text } = part {
// 모든 응답에 면책 조항을 추가합니다
*text = format!("{}\n\n[AI-generated response]", text);
}
}
}
Ok(Some(response))
})
}))
.build()?;
Tool 권한 확인 (Tool 실행 전)
Tool 실행 권한을 검증합니다:
use adk_rust::prelude::*;
use std::sync::Arc;
let agent = LlmAgentBuilder::new("permission_agent")
.model(model)
.tool(Arc::new(GoogleSearchTool::new()))
.before_tool_callback(Box::new(|ctx| {
Box::pin(async move {
// 사용자가 Tool에 대한 권한이 있는지 확인합니다
let user_id = ctx.user_id();
// 예시: 특정 사용자가 Tool을 사용하는 것을 차단합니다
if user_id == "restricted_user" {
return Ok(Some(Content {
role: "function".to_string(),
parts: vec![Part::Text {
text: "Tool access denied for this user.".to_string(),
}],
}));
}
Ok(None) // Tool 실행 허용
})
}))
.build()?;
Tool 결과 로깅 (Tool 실행 후)
모든 Tool 실행을 로그합니다:
use adk_rust::prelude::*;
use std::sync::Arc;
let agent = LlmAgentBuilder::new("tool_logged_agent")
.model(model)
.tool(Arc::new(GoogleSearchTool::new()))
.after_tool_callback(Box::new(|ctx| {
Box::pin(async move {
println!("[TOOL LOG] Agent에 대해 Tool이 실행됨: {}", ctx.agent_name());
println!("[TOOL LOG] 세션: {}", ctx.session_id());
Ok(None) // 원본 결과 유지
})
}))
.build()?;
여러 콜백
동일한 유형의 여러 콜백을 추가할 수 있습니다. 콜백은 순서대로 실행됩니다:
use adk_rust::prelude::*;
use std::sync::Arc;
let agent = LlmAgentBuilder::new("multi_callback_agent")
.model(model)
// First before callback - logging
.before_callback(Box::new(|ctx| {
Box::pin(async move {
println!("[1] Logging callback");
Ok(None)
})
}))
// Second before callback - validation
.before_callback(Box::new(|ctx| {
Box::pin(async move {
println!("[2] Validation callback");
Ok(None)
})
}))
.build()?;
콜백이 Some(content)를 반환하면, 동일한 유형의 후속 콜백은 건너뜁니다.
오류 처리
콜백은 실행을 중단하기 위해 오류를 반환할 수 있습니다:
use adk_rust::prelude::*;
use std::sync::Arc;
let agent = LlmAgentBuilder::new("error_handling_agent")
.model(model)
.before_callback(Box::new(|ctx| {
Box::pin(async move {
// Validate something critical
if ctx.user_id().is_empty() {
return Err(AdkError::Agent("User ID is required".to_string()));
}
Ok(None)
})
}))
.build()?;
모범 사례
- 콜백을 경량으로 유지하세요: 콜백에서 무거운 계산을 피하세요
- 오류를 적절하게 처리하세요: 의미 있는 오류 메시지를 반환하세요
- 로깅을 아껴서 사용하세요: 너무 많은 로깅은 성능에 영향을 줄 수 있습니다
- 현명하게 캐싱하세요: 캐시 무효화 전략을 고려하세요
- 콜백을 독립적으로 테스트하세요: 콜백 로직을 별도로 단위 테스트하세요