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     │
└──────────────────────────────────────────────────────────────┘

RealtimeModelRealtimeSession

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 Hooks before_tool_call / after_tool_call.
  • Die ADK-Tool-Brücke.adk_tool(Arc<dyn Tool>) ermöglicht es jedem normalen adk_core::Tool (z. B. den integrierten Tools von adk-tool), in einer Echtzeitsitzung über ein synthetisiertes ToolContext auszufü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:

AnbieterEingabe (Mikrofon → Modell)Ausgabe (Modell → Sie)
OpenAI gpt-realtime-2.124 kHz24 kHz
Gemini Live16 kHz24 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:

EreignisBedeutung
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 / SpeechStoppedVAD 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 von ServerEvent immer einen _ => {}-Arm ein.

Weiter: Anbieter →