실시간 아키텍처
이 페이지에서는 ADK-Rust에서 실시간 세션이 실제로 어떻게 작동하는지 — 계층, 이벤트 루프, 오디오 파이프라인, 그리고 턴 라이프사이클에 대해 설명합니다. 이를 이해하면 이 섹션의 나머지 부분(tools, multimodal, memory)이 명확해집니다.
네 가지 계층
┌──────────────────────────────────────────────────────────────┐
│ 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**이 반환됩니다 — 이는 실제로 공급자의 와이어 프로토콜을 사용하는 객체입니다. 세션을 직접 다루는 경우는 거의 없으며, runner가 이를 소유합니다. 그 표면은 작고 공급자 독립적입니다:
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), 위의 runner는 신경 쓰지 않습니다.
RealtimeRunner
세션을 소유하고 이벤트 루프를 실행합니다. 주요 작업은 tool dispatch입니다: ServerEvent::FunctionCallDone가 도착하면 핸들러를 찾아 실행하고 결과를 다시 보냅니다 (Tools 참조). 세션의 동사(verbs)와 tool 등록을 노출합니다:
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— 연결 시 (컨텍스트를 위해) 쿼리되고 턴마다 기록됩니다 (구성 가능). Memory 참조.EnhancedPluginManager— tool 호출은before_tool_call/after_tool_call훅을 통과합니다.- ADK-tool 브리지 —
.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,
}
서버 측 브리지 (웹 앱)
브라우저는 공급자 WebSocket를 안전하게 유지할 수 없습니다 — API 키가 유출될 수 있으며, 오디오/이벤트 배관은 서버 측에 속합니다. 따라서 권장되는 토폴로지는 서버 측 브리지입니다: 브라우저는 얇은 오디오/비디오 장치이며, Rust 서버가 실시간 세션을 소유합니다.
browser ──mic PCM16 + camera JPEG (base64 over your WS)──▶ your Axum /ws
browser ◀──agent PCM16 + transcripts + tool events────── IntegratedRealtimeRunner ──▶ provider
API 키는 브라우저에 도달하지 않습니다; tools는 서버에서 실행됩니다. 이 섹션의 모든 웹 예제는 이 패턴을 사용합니다 — 전체 프로토콜 및 Web Audio 코드는 Building web apps를 참조하십시오.
오디오 파이프라인
실시간 오디오는 raw PCM16, 모노, little-endian입니다 — 컨테이너가 없습니다. 유일하게 달라지는 것은 샘플 레이트이며, 이는 공급자별 및 방향별로 다릅니다:
| 제공자 | 입력 (마이크 → 모델) | 출력 (모델 → 사용자) |
|---|---|---|
OpenAI gpt-realtime | 24 kHz | 24 kHz |
| Gemini Live | 16 kHz | 24 kHz |
요율이 다르기 때문에, 브릿지는 오디오가 흐르기 전에 브라우저와 요율을 협상합니다 (예제는 ready 메시지를 input_rate/output_rate와 함께 보내고, 브라우저는 해당 요율로 캡처/재생 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는 러너가 생성하는 공급자 독립적인 이벤트 열거형입니다. 일반적으로 렌더링하는 이벤트는 다음과 같습니다.
| Event | Meaning |
|---|---|
AudioDelta { delta, .. } | PCM16 bytes의 에이전트 음성 — 재생하세요 |
TranscriptDelta { delta, .. } | 에이전트의 음성 답변, 텍스트 형식으로 |
InputTranscriptDelta { delta, .. } | 사용자 음성의 실시간 전사 (스트리밍됨) |
InputTranscriptCompleted { transcript, .. } | 최종 사용자 전사 (OpenAI가 하나를 보냅니다) |
SpeechStarted / SpeechStopped | VAD가 사용자가 시작/중지하는 것을 감지했습니다 |
FunctionCallDone { name, arguments, call_id, .. } | 모델이 도구를 원합니다 |
ResponseDone { .. } | 응답이 완료됨 |
TextDelta { delta, .. } | 음성이 아닌 텍스트 (예: Gemini "생각 중") — 일반적으로 표시되지 않음 |
Error { error, .. } | 제공자 오류 |
#[non_exhaustive]: ServerEvent를 매칭할 때 항상 _ => {} arm을 포함해야 합니다.
다음: 공급자 →