Echtzeitarchitektur
Diese Seite erklärt, wie eine Echtzeitsitzung in ADK-Rust tatsächlich funktioniert – die Ebenen, die Ereignisschleife, die Audio-Pipeline und der Lebenszyklus eines Turns. Wenn man dies versteht, wird der Rest dieses Abschnitts (Tools, multimodal, Speicher) offensichtlich.
Die vier Ebenen
┌──────────────────────────────────────────────────────────────┐
│ 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
Ein RealtimeModel ist eine schlanke Factory. OpenAIRealtimeModel::new(api_key, model_id)
oder GeminiRealtimeModel::new(GeminiLiveBackend::studio(api_key), model_id) erstellen
eine; BoxedModel ist einfach Arc<dyn RealtimeModel>. Der Aufruf von connect() öffnet die
WebSocket und gibt eine RealtimeSession zurück – das Objekt, das tatsächlich mit dem
Wire-Protokoll des Anbieters spricht. In der Regel greift man nicht direkt auf die Sitzung zu; der Runner verwaltet sie. Ihre Oberfläche ist klein und anbieterunabhängig:
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
}
Jeder Anbieter implementiert dies intern anders (OpenAIs
input_audio_buffer.append gegenüber Geminis realtimeInput), aber der darüberliegende
Runner muss sich darum nicht kümmern.
RealtimeRunner
Verwaltet die Sitzung und führt die Ereignisschleife aus. Seine wichtigste Aufgabe ist die Tool-Ausführung: Wenn ein
ServerEvent::FunctionCallDone eintrifft, sucht er deinen Handler, führt ihn aus und
sendet das Ergebnis zurück (siehe Tools). Er stellt die Verben der Sitzung
sowie die Tool-Registrierung bereit:
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
Die Anwendungsebene. Sie umschließt ein RealtimeRunner und bindet, während Ereignisse eintreffen, Folgendes ein:
SessionService– Abgeschlossene Turns werden an den Sitzungsverlauf angehängt.MemoryService– Wird beim Verbinden abgefragt (für den Kontext) und pro Turn geschrieben (konfigurierbar). Siehe Speicher.EnhancedPluginManager– Tool-Aufrufe laufen durch die Hooksbefore_tool_call/after_tool_call.- Die ADK-Tool-Brücke –
.adk_tool(Arc<dyn Tool>)ermöglicht es jedem normalenadk_core::Tool(z. B. den integrierten Tools vonadk-tool), in einer Echtzeitsitzung über ein synthetisiertesToolContextauszuführen, das auf die Sitzungsidentität beschränkt ist.
Du erstellst sie mit einem typisierten Builder:
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 steuert die automatischen Verhaltensweisen:
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,
}
Die serverseitige Bridge (Web-Apps)
Browser können den Provider WebSocket nicht sicher speichern — Ihr API-Schlüssel würde offengelegt, und die Audio-/Event-Verarbeitung gehört auf die Serverseite. Daher wird eine serverseitige Bridge empfohlen: Der Browser ist ein schlankes Audio-/Videogerät, und Ihr Rust-Server verwaltet die Echtzeit-Sitzung.
browser ──mic PCM16 + camera JPEG (base64 over your WS)──▶ your Axum /ws
browser ◀──agent PCM16 + transcripts + tool events────── IntegratedRealtimeRunner ──▶ provider
Der API-Schlüssel erreicht den Browser nie; Tools werden auf Ihrem Server ausgeführt. Jedes Webbeispiel in diesem Abschnitt verwendet dieses Muster – siehe Web-Apps erstellen für das vollständige Protokoll und den Web-Audio-Code.
Die Audio-Pipeline
Echtzeit-Audio ist rohes PCM16, mono, Little-Endian – ohne Container. Das Einzige, was variiert, ist die Abtastrate, und sie variiert je Provider und je Richtung:
| Anbieter | Eingabe (Mikrofon → Modell) | Ausgabe (Modell → Sie) |
|---|---|---|
OpenAI gpt-realtime-2.1 | 24 kHz | 24 kHz |
| Gemini Live | 16 kHz | 24 kHz |
Da sich die Raten unterscheiden, handelt eine Brücke sie mit dem Browser aus, bevor Audiodaten übertragen werden (die Beispiele senden eine ready-Nachricht mit input_rate/output_rate, und der Browser erstellt seine Erfassungs-/Wiedergabe-AudioContexts mit diesen Raten).
Audio durchläuft Ihre WebSocket base64-kodiert; ServerEvent::AudioDelta enthält decodierte PCM16-Bytes, die Sie für die lückenlose Wiedergabe im Browser erneut codieren.
Der Ablauf eines Durchgangs
Ein „Durchgang“ ist ein Austausch. Bei serverseitiger VAD erkennt der Anbieter Sprachgrenzen und antwortet automatisch; Sie rufen create_response() nicht für Audiodaten auf. Ein typischer Sprachdurchgang erzeugt diese Ereignisabfolge:
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
Bei Texteingaben (einem Chatfeld) gibt es keinen VAD-Auslöser, daher müssen Sie nach send_text() create_response() aufrufen, um das Modell um eine Antwort zu bitten.
Tool-Durchgänge umfassen zwei Antworten
Wenn das Modell ein Tool aufruft, ist der Durchgang länger:
(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
Deshalb sollte eine Benutzeroberfläche einen Durchgang erst dann als abgeschlossen betrachten, wenn ein ResponseDone auftritt, das keinen Tool-Aufruf enthielt. ADK-Rust gibt genau eine nachfolgende response.create pro Durchgang aus, selbst wenn mehrere Tools gleichzeitig aufgerufen werden – siehe Tools.
Serverereignisse, die Sie verarbeiten
ServerEvent ist die anbieterunabhängige Ereignisaufzählung, die der Runner liefert. Die Ereignisse, die Sie typischerweise rendern:
| Ereignis | Bedeutung |
|---|---|
AudioDelta { delta, .. } | PCM16-Bytes der Sprache des Agenten — abspielen |
TranscriptDelta { delta, .. } | Die gesprochene Antwort des Agenten als Text |
InputTranscriptDelta { delta, .. } | Live-Transkript der Sprache des Benutzers (gestreamt) |
InputTranscriptCompleted { transcript, .. } | Finales Benutzertranskript (OpenAI sendet eines) |
SpeechStarted / SpeechStopped | VAD hat erkannt, dass der Benutzer zu sprechen aufgehört hat |
FunctionCallDone { name, arguments, call_id, .. } | Das Modell möchte ein Tool |
ResponseDone { .. } | Eine Antwort wurde abgeschlossen |
TextDelta { delta, .. } | Nicht gesprochener Text (z. B. Gemini „thinking“) — normalerweise nicht angezeigt |
Error { error, .. } | Anbieterfehler |
#[non_exhaustive]: Füge beim Abgleichen vonServerEventimmer einen_ => {}-Arm ein.
Weiter: Anbieter →