실시간 아키텍처
이 페이지에서는 ADK-Rust에서 실시간 세션이 실제로 작동하는 방식을 설명합니다. 계층, 이벤트 루프, 오디오 파이프라인, 턴 수명 주기를 다룹니다. 이를 이해하면 이 섹션의 나머지 내용(도구, 멀티모달, 메모리)도 쉽게 이해할 수 있습니다.
네 가지 계층
┌──────────────────────────────────────────────────────────────┐
│ IntegratedRealtimeRunner (feature: integration) │
│ • SessionService → persists each completed turn │
│ • MemoryService → profile-card injection + turn storage │
│ • EnhancedPluginManager → before/after-tool hooks │
│ • ADK-tool bridge → run any `adk_core::Tool` in a session │
└───────────────┬──────────────────────────────────────────────┘
│ wraps
┌───────────────▼──────────────────────────────────────────────┐
│ RealtimeRunner │
│ • pulls ServerEvents from the session │
│ • on FunctionCallDone → executes the tool handler │
│ • sends the tool result back, triggers the spoken answer │
└───────────────┬──────────────────────────────────────────────┘
│ drives
┌───────────────▼──────────────────────────────────────────────┐
│ RealtimeSession (the live transport — a WebSocket) │
│ send_audio / send_text / send_video_frame / send_tool_output │
│ next_event() → ServerEvent stream │
└───────────────┬──────────────────────────────────────────────┘
│ created by connect()
┌───────────────▼──────────────────────────────────────────────┐
│ RealtimeModel OpenAIRealtimeModel | GeminiRealtimeModel │
└──────────────────────────────────────────────────────────────┘
RealtimeModel → RealtimeSession
RealtimeModel은 얇은 팩토리입니다. OpenAIRealtimeModel::new(api_key, model_id) 또는 GeminiRealtimeModel::new(GeminiLiveBackend::studio(api_key), model_id)가 이를 빌드하고, BoxedModel은 단지 Arc<dyn RealtimeModel>입니다. connect()를 호출하면 WebSocket가 열리고 **RealtimeSession**이 반환됩니다. 이는 실제로 공급자의 와이어 프로토콜과 통신하는 객체입니다. 세션을 직접 다루는 경우는 거의 없으며, 러너가 이를 소유합니다. 세션의 인터페이스는 작고 공급자에 종속되지 않습니다.
trait RealtimeSession {
async fn send_audio_base64(&self, audio: &str) -> Result<()>;
async fn send_text(&self, text: &str) -> Result<()>;
async fn send_video_frame(&self, mime: &str, data_b64: &str) -> Result<()>;
async fn send_tool_output(&self, response: ToolResponse) -> Result<()>;
async fn create_response(&self) -> Result<()>;
async fn next_event(&self) -> Option<Result<ServerEvent>>;
async fn close(&self) -> Result<()>;
// …commit/clear audio, interrupt, mutate_context
}
각 공급자는 내부적으로 이를 서로 다르게 구현합니다(OpenAI의 input_audio_buffer.append와 Gemini의 realtimeInput). 하지만 위의 러너는 이를 신경 쓰지 않습니다.
RealtimeRunner
세션을 소유하고 이벤트 루프를 실행합니다. 핵심 역할은 도구 디스패치입니다. ServerEvent::FunctionCallDone이 도착하면 핸들러를 조회하고 실행한 다음 결과를 다시 전송합니다(도구 참조). 세션의 동작과 도구 등록 기능을 함께 제공합니다.
runner.connect().await?;
runner.send_audio(pcm16_base64).await?; // mic frames
runner.send_text("…").await?; // typed input
runner.send_video_frame("image/jpeg", b64).await?;
runner.create_response().await?; // trigger a response to text input
let ev = runner.next_event().await; // pull the next ServerEvent
runner.close().await?;
IntegratedRealtimeRunner
애플리케이션 계층입니다. RealtimeRunner을 감싸고 이벤트가 흐르는 동안 다음 기능을 연결합니다.
SessionService— 완료된 턴이 세션 기록에 추가됩니다.MemoryService— 연결 시 컨텍스트를 위해 조회되고, 각 턴마다 기록됩니다(구성 가능). 메모리를 참조하세요.EnhancedPluginManager— 도구 호출이before_tool_call/after_tool_call훅을 통과합니다.- ADK-도구 브리지 —
.adk_tool(Arc<dyn Tool>)을 사용하면 일반adk_core::Tool(예:adk-tool의 기본 제공 기능)을 세션 ID에 한정된 합성ToolContext을 통해 실시간 세션에서 실행할 수 있습니다.
타입이 지정된 빌더를 사용해 이를 구성합니다.
let runner = IntegratedRealtimeRunner::builder()
.model(model)
.config(config)
.identity("app", "user", "session-id") // required
.session_service(sessions) // optional
.memory_service(memory) // optional
.integration_config(IntegrationConfig::default())
.tool(weather_def(), weather_handler()) // native realtime ToolHandler
.adk_tool(Arc::new(remember_tool)) // bridged adk_core::Tool
.build()?;
IntegrationConfig는 자동 동작을 제어합니다.
IntegrationConfig {
persist_transcripts: true, // append turns to the session
store_to_memory: true, // memory_service.add_session per turn
inject_memory_context: true, // query memory at connect
max_memory_injection: 10,
}
서버 측 브리지(웹 앱)
브라우저는 provider WebSocket를 안전하게 보관할 수 없습니다. API 키가 유출될 수 있고, 오디오/이벤트 처리는 서버 측에 속하기 때문입니다. 따라서 권장되는 토폴로지는 서버 측 브리지입니다. 브라우저는 얇은 오디오/비디오 장치가 되고, Rust 서버가 실시간 세션을 관리합니다.
browser ──mic PCM16 + camera JPEG (base64 over your WS)──▶ your Axum /ws
browser ◀──agent PCM16 + transcripts + tool events────── IntegratedRealtimeRunner ──▶ provider
API 키는 브라우저에 절대 전달되지 않으며, 도구는 서버에서 실행됩니다. 이 섹션의 모든 웹 예제는 이 패턴을 사용합니다. 전체 프로토콜과 Web Audio 코드는 웹 앱 빌드를 참조하세요.
오디오 파이프라인
실시간 오디오는 raw PCM16, mono, little-endian이며 컨테이너를 사용하지 않습니다. 유일하게 달라지는 것은 샘플링 레이트이며, provider별 및 방향별로 달라집니다:
| 제공업체 | 입력 (마이크 → 모델) | 출력 (모델 → 사용자) |
|---|---|---|
OpenAI gpt-realtime-2.1 | 24 kHz | 24 kHz |
| Gemini Live | 16 kHz | 24 kHz |
요율이 서로 다르므로, 브리지는 오디오가 흐르기 전에 브라우저와 요율을 협상합니다 (예제에서는 input_rate/output_rate이 포함된 ready 메시지를 전송하고, 브라우저는 해당 요율로 캡처/재생 AudioContext을 생성합니다).
오디오는 WebSocket를 통해 base64로 인코딩되어 전달되며, ServerEvent::AudioDelta에는 브라우저에서 끊김 없이 재생하도록 다시 인코딩하는 디코딩된 PCM16 바이트가 포함됩니다.
턴 수명 주기
"턴"은 한 번의 교환입니다. 서버 VAD를 사용하면 제공자가 음성 경계를 감지하고 자동으로 응답하므로, 오디오에 대해 create_response()를 호출할 필요가 없습니다. 일반적인 음성 턴에서는 다음과 같은 이벤트 시퀀스가 생성됩니다.
SpeechStarted → user began talking (flush any playing audio = barge-in)
InputTranscriptDelta… → live transcript of what the user is saying
SpeechStopped → user finished
(model thinks)
TranscriptDelta… → the agent's spoken answer, as text
AudioDelta… → the agent's spoken answer, as PCM16
ResponseDone → turn complete
텍스트 입력(채팅 상자)의 경우 VAD 트리거가 없으므로, 모델에 응답을 요청하려면 send_text() 이후에 create_response()을 호출해야 합니다.
도구 턴은 두 개의 응답에 걸쳐 진행됩니다
모델이 도구를 호출하면 턴이 더 길어집니다.
(maybe a short spoken preamble) + FunctionCallDone(name, args)
ResponseDone ← the "dispatch" response ends here
→ runner executes your handler, sends the result back,
and triggers ONE follow-up response
TranscriptDelta… / AudioDelta… ← the spoken answer using the tool result
ResponseDone ← turn truly complete
따라서 UI는 도구 호출이 포함되지 않은 ResponseDone에서만 턴이 완료된 것으로 처리해야 합니다. ADK-Rust는 여러 도구가 동시에 호출되더라도 턴마다 정확히 하나의 후속 response.create을 발행합니다 — 도구를 참조하세요.
처리할 서버 이벤트
ServerEvent는 러너가 생성하는 제공자에 종속되지 않는 이벤트 열거형입니다. 일반적으로 렌더링하는 이벤트는 다음과 같습니다.
| 이벤트 | 의미 |
|---|---|
AudioDelta { delta, .. } | 에이전트 음성의 PCM16 바이트 — 재생 |
TranscriptDelta { delta, .. } | 텍스트 형식의 에이전트 음성 답변 |
InputTranscriptDelta { delta, .. } | 사용자의 음성이 실시간으로 스트리밍되는 대화 기록 |
InputTranscriptCompleted { transcript, .. } | 최종 사용자 대화 기록(OpenAI이 하나 전송함) |
SpeechStarted / SpeechStopped | VAD가 사용자의 발화 시작/중지를 감지함 |
FunctionCallDone { name, arguments, call_id, .. } | 모델이 도구를 원함 |
ResponseDone { .. } | 응답 완료 |
TextDelta { delta, .. } | 음성이 아닌 텍스트(예: Gemini의 "thinking") — 일반적으로 표시되지 않음 |
Error { error, .. } | 제공업체 오류 |
#[non_exhaustive]:ServerEvent을 일치시킬 때는 항상_ => {}암을 포함하세요.
다음: 프로바이더 →