콜백

adk-rust의 콜백은 핵심 실행 지점에서 Agent 동작을 관찰, 사용자 지정 및 제어하기 위한 후크를 제공합니다. 이를 통해 로깅, 가드레일, 캐싱, 응답 수정 등이 가능합니다.

개요

adk-rust는 Agent 실행의 다양한 단계를 가로채는 6가지 콜백 유형을 지원합니다.

콜백 유형실행 시점사용 사례
before_agentAgent가 처리를 시작하기 전입력 유효성 검사, 로깅, 조기 종료
after_agentAgent 완료 후응답 수정, 로깅, 정리
before_modelLLM 호출 전요청 수정, 캐싱, 속도 제한
after_modelLLM 응답 후응답 필터링, 로깅, 캐싱
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 콜백

BeforeModelCallbackBeforeModelResult를 사용합니다:

반환 값효과
Ok(BeforeModelResult::Continue(request))(수정되었을 수 있는) request로 계속 진행
Ok(BeforeModelResult::Skip(response))모델 호출을 건너뛰고, 이 response를 대신 사용
Err(e)오류와 함께 실행 중단

AfterModelCallbackOption<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()?;

모범 사례

  1. 콜백을 경량으로 유지하세요: 콜백에서 무거운 계산을 피하세요
  2. 오류를 적절하게 처리하세요: 의미 있는 오류 메시지를 반환하세요
  3. 로깅을 아껴서 사용하세요: 너무 많은 로깅은 성능에 영향을 줄 수 있습니다
  4. 현명하게 캐싱하세요: 캐시 무효화 전략을 고려하세요
  5. 콜백을 독립적으로 테스트하세요: 콜백 로직을 별도로 단위 테스트하세요

관련 항목


이전: ← 상태 관리 | 다음: 아티팩트 →