Echtzeit-Architektur

Diese Seite erklärt, wie eine Echtzeit-Session in ADK-Rust tatsächlich funktioniert – die Schichten, der Event-Loop, die Audio-Pipeline und der Turn-Lebenszyklus. Dies zu verstehen, macht den Rest des Abschnitts (Tools, Multimodalität, Speicher) offensichtlich.

Die vier Schichten

┌──────────────────────────────────────────────────────────────┐
│ 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 einen; BoxedModel ist einfach Arc<dyn RealtimeModel>. Der Aufruf von connect() öffnet den WebSocket und gibt eine RealtimeSession zurück – das Objekt, das tatsächlich das Wire-Protokoll des Anbieters spricht. Sie interagieren selten direkt mit der Session; der Runner besitzt 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 unter der Haube unterschiedlich (OpenAIs input_audio_buffer.append vs. Geminis realtimeInput), aber der obige Runner kümmert sich nicht darum.

RealtimeRunner

Verwaltet die Session und führt die Ereignisschleife aus. Seine Hauptaufgabe ist das Tool-Dispatching: wenn ein ServerEvent::FunctionCallDone ankommt, sucht es Ihren Handler, führt ihn aus und sendet das Ergebnis zurück (siehe Tools). Es stellt die Verben der Session 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 Anwendungsschicht. Sie umschließt einen RealtimeRunner und bindet, während Ereignisse fließen, ein:

  • SessionService — abgeschlossene Züge werden an den Sitzungsverlauf angehängt.
  • MemoryService — wird bei der Verbindung abgefragt (für den Kontext) und pro Zug geschrieben (konfigurierbar). Siehe Memory.
  • EnhancedPluginManager — Tool-Aufrufe durchlaufen before_tool_call / after_tool_call Hooks.
  • Die ADK-Tool-Brücke.adk_tool(Arc<dyn Tool>) ermöglicht es jedem normalen adk_core::Tool (z.B. den eingebauten Funktionen von adk-tool), in einer Echtzeit-Sitzung über einen synthetisierten ToolContext zu laufen, der auf die Sitzungsidentität zugeschnitten ist.

Sie bauen es 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 Brücke (Web-Apps)

Browser können den Anbieter-WebSocket nicht sicher halten — Ihr API-Schlüssel würde geleakt, und die Audio-/Ereignis-Verkabelung gehört serverseitig. Daher ist die empfohlene Topologie eine serverseitige Brücke: der Browser ist ein schlankes Audio-/Video-Gerät, und Ihr Rust Server besitzt 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 niemals den Browser; Tools laufen auf Ihrem Server. Jedes Web- Beispiel in diesem Abschnitt verwendet dieses Muster — siehe Building web apps für das vollständige Protokoll und den Web Audio Code.

Die Audio-Pipeline

Echtzeit-Audio ist rohes PCM16, mono, little-endian — keine Container. Das Einzige, was variiert, ist die Abtastrate, und sie variiert pro Anbieter und pro Richtung:

AnbieterEingabe (Mikrofon → Modell)Ausgabe (Modell → Sie)
OpenAI gpt-realtime24 kHz24 kHz
Gemini Live16 kHz24 kHz

Da die Raten unterschiedlich sind, verhandelt eine Brücke diese mit dem Browser, bevor Audio fließt (die Beispiele senden eine ready Nachricht mit input_rate/output_rate, und der Browser erstellt seine Aufnahme-/Wiedergabe-AudioContexts mit diesen Raten). Audio wird über Ihr WebSocket base64-kodiert übertragen; ServerEvent::AudioDelta enthält dekodierte PCM16-Bytes, die Sie für den Browser neu kodieren, um eine lückenlose Wiedergabe zu ermöglichen.

Der Turn-Lebenszyklus

Ein "Turn" ist ein Austausch. Mit server VAD erkennt der Anbieter Sprachgrenzen und antwortet automatisch; Sie rufen create_response() nicht für Audio auf. Ein typischer Sprach-Turn erzeugt diese Ereignissequenz:

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

Für Texteingabe (ein Chatfeld) gibt es keinen VAD-Trigger, daher müssen Sie create_response() nach send_text() aufrufen, um das Modell zur Antwort aufzufordern.

Tool-Runden umfassen zwei Antworten

Wenn das Modell ein Tool aufruft, ist die Runde 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

Aus diesem Grund sollte eine Benutzeroberfläche eine Runde erst dann als beendet betrachten, wenn eine ResponseDone die keinen Tool-Aufruf enthielt. ADK-Rust gibt genau eine Folge- response.create pro Runde aus, selbst wenn mehrere Tools gleichzeitig aufgerufen werden – siehe Tools.

Server-Ereignisse, die Sie behandeln werden

ServerEvent ist das Provider-agnostische Ereignis-Enum, das der Runner liefert. Diejenigen, die Sie typischerweise rendern:

EreignisBedeutung
AudioDelta { delta, .. }PCM16 Bytes der Agentenrede – spielen Sie diese ab
TranscriptDelta { delta, .. }Die gesprochene Antwort des Agenten, als Text
InputTranscriptDelta { delta, .. }Live-Transkript der Benutzerrede (gestreamt)
InputTranscriptCompleted { transcript, .. }Endgültiges Benutzertranskript (OpenAI sendet eines)
SpeechStarted / SpeechStoppedVAD hat erkannt, dass der Benutzer anfängt/aufhört zu sprechen
FunctionCallDone { name, arguments, call_id, .. }Das Modell möchte ein Tool verwenden
ResponseDone { .. }Eine Antwort wurde beendet
TextDelta { delta, .. }Nicht gesprochener Text (z.B. Gemini „denkt“) – wird normalerweise nicht angezeigt
Error { error, .. }Provider-Fehler

#[non_exhaustive]: Fügen Sie immer einen _ => {} Arm ein, wenn Sie ServerEvent abgleichen.

Weiter: Provider →