بنية الوقت الفعلي
تشرح هذه الصفحة كيفية عمل جلسة الوقت الفعلي فعليًا في 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
}
ينفّذ كل موفر للخدمة ذلك بطريقة مختلفة في الخلفية (input_audio_buffer.append الخاص بـ OpenAI مقابل realtimeInput الخاص بـ Gemini)، لكن المشغّل أعلاه لا يهتم بهذه التفاصيل.
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) العمل في جلسة وقت فعلي عبر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 إلى المتصفح مطلقًا؛ إذ تعمل الأدوات على خادمك. يستخدم كل مثال ويب في هذا القسم هذا النمط — راجع إنشاء تطبيقات الويب للاطلاع على البروتوكول الكامل وشفرة Web Audio.
مسار الصوت
الصوت في الوقت الفعلي هو PCM16 خام، أحادي القناة، بترتيب little-endian — من دون حاويات. الشيء الوحيد الذي يختلف هو معدل أخذ العينات، وهو يختلف حسب المزوّد وحسب الاتجاه:
| المزوّد | الإدخال (من الميكروفون ← إلى النموذج) | الإخراج (من النموذج ← إليك) |
|---|---|---|
OpenAI gpt-realtime-2.1 | 24 kHz | 24 kHz |
| Gemini Live | 16 kHz | 24 kHz |
نظرًا لاختلاف معدلاتها، تقوم الجسر بالتفاوض عليها مع المتصفح قبل تدفق أي
صوت (ترسل الأمثلة رسالة ready مع input_rate/output_rate،
وينشئ المتصفح AudioContexts الخاصة بالالتقاط/التشغيل بهذه المعدلات).
يعبر الصوت 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، لذا يجب عليك استدعاء
create_response() بعد send_text() لطلب الرد من النموذج.
تمتد تناوبات الأدوات عبر استجابتين
عندما يستدعي النموذج أداة، يكون التناوب أطول:
(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
ولهذا ينبغي لواجهة المستخدم اعتبار التناوب مكتملًا فقط عند ورود
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) — لا يُعرض عادةً |
Error { error, .. } | خطأ في موفّر الخدمة |
#[non_exhaustive]: احرص دائمًا على تضمين ذراع_ => {}عند مطابقةServerEvent.
التالي: المزوّدون →