الوحدة 5 — البثّ التدريجي للردود
عرض المساعد الحواريّ في الوحدة السابقة يعمل، لكنّه يترك المستخدم أمام شاشة صامتة حتّى يكتمل الجواب. على نموذج يستغرق ثمانية أو عشر ثوانٍ لجملة طويلة، هذه الصمت الكامل يبدو عطبًا حتّى لو كان النموذج مشغولًا. الحلّ هو البثّ التدريجي: نُظهر الحرف الأوّل خلال جزء من الثانية، ونستمرّ في التحديث رمزًا رمزًا. هذه الوحدة تفكّك آليّة البثّ في Gradio باستعمال مولّدات Python.
لماذا الإحساس بالسرعة أهمّ من السرعة
نظريًّا، مولّد يعطي 50 رمزًا في الثانية يُنتج جملة من 200 رمز في 4 ثوانٍ سواء بثّاً أو دفعة واحدة. لكنّ الإحساس مختلف تمامًا:
| الطريقة | زمن أوّل حرف مرئيّ | زمن آخر حرف | إحساس المستخدم |
|---|---|---|---|
| دفعة واحدة | 4.0 ث | 4.0 ث | «العرض بطيء أو معطوب» |
| بثّ تدريجيّ | 0.1 ث | 4.0 ث | «العرض يُفكّر أمامي، الأمر يسير» |
المستخدم يرى النصّ يظهر ويقرأ ما ظهر، فيقصر الانتظار الذهنيّ. هذا هو أساس دراسات تفاعل الإنسان بالحاسوب التي تربط الإحساس بالكفاءة بـ«زمن أوّل استجابة» أكثر من ربطه بالزمن الإجماليّ.
المولّدات في Python بسرعة
مولّد هو دالّة تستعمل yield بدل return. كلّ yield يُعطي قيمة ثمّ يوقف التنفيذ حتّى الاستدعاء التالي:
def compter(n):
for i in range(n):
yield i
for v in compter(3):
print(v) # 0, 1, 2
Gradio يستوعب هذه الآليّة مباشرة. إذا كانت دالّة ChatInterface مولّدًا يعطي سلاسل نصّيّة متراكمة، فإنّ Gradio يُحدّث الواجهة عند كلّ yield. لا حاجة إلى WebSocket يدويّ، ولا إلى دفعة برمجيّة، ولا إلى JavaScript.
بثّ تدريجيّ حرفًا حرفًا (تعليميّ)
نبدأ بمثال يُوضّح المبدأ دون نموذج حقيقيّ:
import gradio as gr
import time
def repondre_bt(message, historique):
reponse_complete = f"استلمت رسالتك: «{message}». أُجيبك بثاً حرفًا حرفًا لتوضيح الآليّة."
accumule = ""
for c in reponse_complete:
accumule += c
time.sleep(0.03) # محاكاة لسرعة نموذج
yield accumule
gr.ChatInterface(
fn=repondre_bt,
title="بثّ تعليميّ",
description="كلّ حرف يظهر بعد 30 ميلي ثانية من سابقه.",
).launch()
الملاحظة المهمّة: كلّ yield يُرسل النصّ الكامل حتّى اللحظة، لا الفارق الجديد. هذا يبسّط منطق الواجهة كثيرًا: Gradio يستبدل مضمون الفقّاعة كلّ مرّة بالنصّ الجديد، فلا حاجة لفصل «الجديد» عن «القديم».
بثّ تدريجيّ من نموذج حقيقيّ
نستعمل واجهة Ollama التي رأيناها في الوحدة السابقة مع stream=True:
import gradio as gr
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
def repondre_bt(message, historique, systeme):
messages = [{"role": "system", "content": systeme}] + historique + [
{"role": "user", "content": message},
]
flux = client.chat.completions.create(
model="qwen2.5:7b-instruct-q4_K_M",
messages=messages,
temperature=0.2,
stream=True,
)
accumule = ""
for morceau in flux:
delta = morceau.choices[0].delta.content or ""
accumule += delta
yield accumule
gr.ChatInterface(
fn=repondre_bt,
additional_inputs=[
gr.Textbox(
value="أنت مساعد يجيب بإيجاز.",
label="رسالة النظام",
rtl=True,
),
],
title="مساعد ببثّ تدريجيّ",
).launch()
الفارق العمليّ عن الوحدة السابقة سطران فقط: إضافة stream=True، وحلقة تجميع مع yield. النموذج نفسه، نفس المعاملات، نفس التاريخ. لكنّ إحساس المستخدم يتحوّل من «الأداة بطيئة» إلى «الأداة تفكّر أمامي».
إتاحة الإيقاف
gr.ChatInterface يعرض تلقائيًّا زرّ «إيقاف» أثناء التوليد إذا كانت الدالّة مولّدة. النقر عليه يقطع الحلقة، ويبقى النصّ المعروض كما هو دون إكماله. لا يحتاج المطوّر إلى كتابة أيّ منطق إضافيّ لهذا؛ Gradio يتكفّل بإرسال إشارة إلغاء داخليّة تجعل المولّد يتوقّف عند yield التالي.
هذا التفصيل مهمّ عمليًّا: المستخدم يبدأ سؤالًا خاطئًا، أو يرى في الجملة الأولى أنّ الاتّجاه غير مطلوب، فيوقف بدل أن ينتظر عشر ثوانٍ إضافيّة ثمّ يعيد الكتابة. توفير عمر المستخدم = توفير طلب على النموذج = تخفيض الكلفة الحقيقيّة.
البثّ خارج المحادثة: Blocks مع gr.Textbox
المولّدات ليست حكرًا على ChatInterface. أيّ دالّة في Blocks تستطيع استعمال yield لتحديث المُخرَجات تدريجيًّا:
import gradio as gr, time
def generer(prompt):
accumule = ""
for mot in prompt.split() * 3:
accumule += mot + " "
time.sleep(0.15)
yield accumule
with gr.Blocks() as demo:
entree = gr.Textbox(label="نصّ الإدخال", rtl=True)
sortie = gr.Textbox(label="النصّ المولَّد", lines=6, rtl=True)
bouton = gr.Button("ابدأ التوليد")
bouton.click(fn=generer, inputs=entree, outputs=sortie)
demo.launch()
هذا مفيد لعروض توليد النصّ العامّ، وتلخيص وثيقة طويلة (تظهر الفقرات فقرة فقرة)، وحتّى لعرض تقدّم مهمّة طويلة عبر رسائل نصّيّة متتالية.
عند قياس أداء عرض بثّيّ، اقرأ زمن أوّل رمز (TTFT) وعدد الرموز في الثانية (throughput) بشكل منفصل. تحسين TTFT له أثر كبير على الإحساس بالسرعة حتّى لو لم يتغيّر throughput. النماذج المكمّمة تُقلّل throughput لكنّها قد تحافظ على TTFT جيّد.
نمط التاريخ المُتراكم صحيح
خطأ شائع في البثّ: إعادة إرسال الرسالة كاملة مع تاريخ في كلّ yield. هذا يستنزف الشبكة ويُبطئ التفاعل. القاعدة الصحيحة: يعطي المولّد سلسلة الجواب فقط، ويترك التاريخ لـChatInterface كي يُضيفه تلقائيًّا عند الاكتمال. هذا يعمل تلقائيًّا مع النموذج القياسي messages.
الخلاصة
- الإحساس بالسرعة يعتمد على زمن أوّل حرف مرئيّ أكثر ممّا يعتمد على الزمن الإجماليّ للجواب.
- مولّدات Python (
yield) تكفي وحدها لبناء بثّ تدريجيّ في Gradio؛ لا حاجة إلى WebSocket ولا JavaScript. - كلّ
yieldيُرسل النصّ الكامل حتّى اللحظة، لا الفارق؛ Gradio يستبدل مضمون الفقاعة تلقائيًّا. - زرّ الإيقاف يظهر تلقائيًّا مع الدوالّ المولّدة ويوفّر كلفة النداءات غير المرغوبة.
الوحدة التالية: الأمثلة المعبّأة مسبقًا التي يستعملها المستخدم بنقرة، وتخزينها مؤقّتًا عند بدء الخادم.