0Pricing
AI Engineering Academy · درس

البث في FastAPI باستخدام Server-Sent Events

ابنوا نقطة نهاية في FastAPI توجّه استجابات البث من LLM إلى عميل متصفح باستخدام StreamingResponse ونوع المحتوى text/event-stream.

البث في FastAPI باستخدام Server-Sent Events درس مجاني في AI Engineering Academy على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Engineering Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

لماذا نستخدم Server-Sent Events لبث LLM

إن Server-Sent Events (SSE) معيار من معايير W3C يتيح للخادم دفع تدفق من الأحداث النصية إلى عميل المتصفح عبر اتصال HTTP واحد طويل الأمد. وعلى خلاف WebSockets، يعمل SSE في اتجاه واحد، من الخادم إلى العميل، ويعمل عبر HTTP/1.1 القياسي، ويعيد الاتصال تلقائياً عند انقطاعه، ولا يتطلب مكتبة خاصة للمتصفح. تجعل هذه الخصائص منه وسيلة النقل المثالية لبث رموز LLM من خلفية FastAPI إلى الواجهة الأمامية للويب.

تنسيق SSE على مستوى السلك

يرسل SSE البيانات النصية بتنسيق يتكون من سلسلة من الحقول المفصولة بأسطر جديدة. يحتوي كل حدث على حقل نوع اختياري هو event، وحقل data يتضمن الحمولة، ومعرّف اختياري id لإعادة الاتصال. تُفصل الأحداث بسطر فارغ. في بث LLM، أرسلوا كل رمز كسطر data: token_text\n\n، وأرسلوا في النهاية حدثاً خاصاً هو data: [DONE]\n\n للإشارة إلى اكتمال التدفق.

# SSE wire format example
'''
data: The\n\n
data:  capital\n\n
data:  of\n\n
data:  France\n\n
data:  is\n\n
data:  Paris\n\n
data: [DONE]\n\n
'''

# Each 'data:' line is one event.
# The double newline (\n\n) terminates each event.
# The client receives these as EventSource message events.
# The content-type must be 'text/event-stream'.

StreamingResponse في FastAPI

تقبل StreamingResponse في FastAPI مولّداً غير متزامن يُرجع سلاسل نصية، وتبثها إلى العميل. وبضبط media_type على 'text/event-stream' وتنسيق كل سلسلة مُرجعة كحدث SSE، تحوّلون أي مولّد غير متزامن إلى تدفق SSE صحيح. تتولى FastAPI تلقائياً دورة حياة الاتصال والتفريغ وترويسات HTTP.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
import asyncio

app = FastAPI()
async_client = AsyncOpenAI()

async def token_generator(prompt: str):
    stream = await async_client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': prompt}],
        stream=True,
    )
    async for chunk in stream:
        delta = chunk.choices[0].delta.content
        if delta:
            yield f'data: {delta}\n\n'  # SSE format
    yield 'data: [DONE]\n\n'

@app.get('/stream')
async def stream_endpoint(prompt: str):
    return StreamingResponse(
        token_generator(prompt),
        media_type='text/event-stream',
        headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'},
    )

ترويسات HTTP مهمة لـ SSE

تُعد ثلاث من ترويسات HTTP أساسية لعمل SSE بشكل صحيح عبر الوكلاء الوسيطين وشبكات CDN. تمنع Cache-Control: no-cache الوسطاء من تخزين التدفق مؤقتاً. وتحافظ Connection: keep-alive على إبقاء اتصال TCP مفتوحاً. أما X-Accel-Buffering: no فتعطّل التخزين المؤقت لاستجابات Nginx، الذي كان سيجمع الكتل ويقضي على تأثير البث. ومن دون هذه الترويسة الأخيرة، سيخزّن Nginx كل الناتج مؤقتاً قبل تمريره إلى المتصفح.

from fastapi.responses import StreamingResponse

SSE_HEADERS = {
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no',   # disable nginx buffering
    'Access-Control-Allow-Origin': '*',  # CORS for cross-origin clients
}

@app.get('/chat')
async def chat_stream(prompt: str):
    return StreamingResponse(
        token_generator(prompt),
        media_type='text/event-stream',
        headers=SSE_HEADERS,
    )

أحداث SSE منظَّمة بحمولات JSON

بالنسبة إلى واجهات البث الأكثر غنى، رمّزوا حمولة كل حدث بتنسيق JSON بدلاً من النص الخام. يتيح لكم ذلك تضمين بيانات وصفية إلى جانب الرمز، مثل نوع الرمز، سواء كان محتوى أم استدعاء أداة، أو معرّف رسالة، أو طابع زمني لزمن الاستجابة. يحلل عميل المتصفح JSON الخاص بكل حدث ويوجّه أنواع الأحداث المختلفة إلى مكوّنات واجهة مستخدم مختلفة.

import json
import time

async def json_token_generator(prompt: str, session_id: str):
    t_start = time.perf_counter()
    stream = await async_client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': prompt}],
        stream=True,
    )
    async for chunk in stream:
        delta = chunk.choices[0].delta.content
        if delta:
            payload = json.dumps({
                'type': 'token',
                'content': delta,
                'session_id': session_id,
                't_ms': round((time.perf_counter() - t_start) * 1000),
            })
            yield f'data: {payload}\n\n'
    # Send completion event
    yield f'data: {json.dumps({"type": "done", "session_id": session_id})}\n\n'

استهلاك SSE في المتصفح (JavaScript)

تتصل واجهة EventSource في المتصفح بنقطة نهاية SSE، وتطلق الأحداث عند وصولها. لبث الرموز، استمعوا إلى حدث message الافتراضي، وحلّلوا البيانات كـ JSON أو تعاملوا معها كسلسلة خام، ثم أضيفوا كل رمز إلى DOM. تعاملوا مع الرمز [DONE] بإغلاق اتصال EventSource.

// Browser-side JavaScript
const prompt = 'Explain hybrid search in one paragraph.';
const url = '/stream?prompt=' + encodeURIComponent(prompt);

const source = new EventSource(url);
const output = document.getElementById('output');

source.onmessage = (event) => {
  if (event.data === '[DONE]') {
    source.close();  // stop listening
    return;
  }
  output.textContent += event.data;  // append each token
};

source.onerror = (err) => {
  console.error('SSE error:', err);
  source.close();
};

طلبات POST باستخدام fetch للبث

لا تدعم EventSource سوى طلبات GET، مما يحدّ من استخدامها مع المطالبات المعقدة. بالنسبة إلى طلبات POST، التي ترسل نص JSON يتضمن سجل المحادثة، استخدموا واجهة fetch في المتصفح مع Streams API لقراءة نص الاستجابة تدريجياً. يُستخدم هذا النمط في واجهة الويب الخاصة بـ ChatGPT وفي معظم واجهات دردشة LLM المستخدمة في بيئات الإنتاج.

// Browser-side: POST with fetch and ReadableStream
async function streamPost(messages) {
  const response = await fetch('/chat', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({messages}),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  const output = document.getElementById('output');

  while (true) {
    const {done, value} = await reader.read();
    if (done) break;
    const text = decoder.decode(value, {stream: true});
    // Parse SSE lines
    for (const line of text.split('\n')) {
      if (line.startsWith('data: ') && line !== 'data: [DONE]') {
        output.textContent += line.slice(6);
      }
    }
  }
}

نقطة نهاية POST في FastAPI لبث الدردشة

لبث الدردشة المعتمد على POST، عرّفوا نموذج Pydantic لنص الطلب، واقبلوا قائمة من الرسائل، ثم ابثّوا استجابة LLM. يتيح ذلك تمرير سجل المحادثة الكامل مع كل طلب، ودعم تطبيقات الدردشة متعددة الأدوار. النمط مماثل تماماً لبث GET، باستثناء استخراج المطالبة من نص الطلب.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

class ChatRequest(BaseModel):
    messages: list[dict]
    model: str = 'gpt-4o-mini'

@app.post('/chat')
async def chat_post(request: ChatRequest):
    async def generate():
        stream = await async_client.chat.completions.create(
            model=request.model,
            messages=request.messages,
            stream=True,
        )
        async for chunk in stream:
            delta = chunk.choices[0].delta.content
            if delta:
                yield f'data: {delta}\n\n'
        yield 'data: [DONE]\n\n'

    return StreamingResponse(
        generate(),
        media_type='text/event-stream',
        headers=SSE_HEADERS,
    )

التعامل مع انقطاع العميل

عندما ينتقل مستخدم المتصفح إلى صفحة أخرى أو يغلق علامة التبويب، يُغلق اتصال HTTP وترفع FastAPI الاستثناء asyncio.CancelledError في المولّد المسؤول عن البث. احرصوا دائماً على التعامل مع ذلك لتجنب إبقاء طلبات بث LLM مفتوحة وتكبّد تكاليف API غير ضرورية. غلّفوا المولّد في try/except لمعالجة CancelledError، وألغوا تدفق OpenAI عند اكتشافه.

from fastapi import Request

@app.get('/stream')
async def stream_with_disconnect(prompt: str, request: Request):
    async def generate_with_cancel():
        try:
            stream = await async_client.chat.completions.create(
                model='gpt-4o-mini',
                messages=[{'role': 'user', 'content': prompt}],
                stream=True,
            )
            async for chunk in stream:
                if await request.is_disconnected():
                    break  # client gone, stop generating
                delta = chunk.choices[0].delta.content
                if delta:
                    yield f'data: {delta}\n\n'
        except asyncio.CancelledError:
            pass  # client disconnected
        finally:
            yield 'data: [DONE]\n\n'

    return StreamingResponse(generate_with_cancel(), media_type='text/event-stream')

إضافة مصادقة الطلبات

يجب أن تصادق نقاط نهاية البث في بيئة الإنتاج على الطلبات لمنع الاستخدام غير المصرّح به لـ LLM. استخدموا Depends في FastAPI مع مفتاح API أو فحص ترويسة JWT. تحدث المصادقة قبل بدء المولّد، لذا تكون كلفتها الإضافية ضئيلة، ولا يبدأ التدفق إلا بعد التحقق من المستخدم.

from fastapi import Header, HTTPException, Depends

VALID_API_KEYS = {'sk-demo-key-1', 'sk-demo-key-2'}

async def verify_api_key(x_api_key: str = Header(None)):
    if x_api_key not in VALID_API_KEYS:
        raise HTTPException(status_code=401, detail='Invalid API key')
    return x_api_key

@app.post('/chat')
async def authenticated_chat(
    request: ChatRequest,
    api_key: str = Depends(verify_api_key),
):
    async def generate():
        stream = await async_client.chat.completions.create(
            model=request.model,
            messages=request.messages,
            stream=True,
        )
        async for chunk in stream:
            delta = chunk.choices[0].delta.content
            if delta:
                yield f'data: {delta}\n\n'
        yield 'data: [DONE]\n\n'

    return StreamingResponse(generate(), media_type='text/event-stream', headers=SSE_HEADERS)

اختبار نقاط نهاية SSE

اختبروا نقاط نهاية البث باستخدام TestClient في FastAPI بوضع البث. استخدموا with client.stream('GET', '/stream', params={...}) as r، ثم كرروا على r.iter_lines() لتلقي أحداث SSE. يتيح لكم ذلك التحقق من تنسيق الرموز بشكل صحيح، وإرسال رمز DONE، وإنتاج حالات الخطأ لأحداث SSE مناسبة.

from fastapi.testclient import TestClient

def test_sse_endpoint():
    with TestClient(app) as client:
        with client.stream('GET', '/stream', params={'prompt': 'Say hi'}) as r:
            assert r.status_code == 200
            assert 'text/event-stream' in r.headers['content-type']

            events = []
            for line in r.iter_lines():
                if line.startswith('data: '):
                    events.append(line[6:])

            assert events[-1] == '[DONE]'
            full_text = ''.join(e for e in events if e != '[DONE]')
            assert len(full_text) > 0

تحقّق سريع

اختبروا فهمكم لبث FastAPI باستخدام SSE الذي تناولتموه في هذا الدرس.

مراجعة الدرس

في هذا الدرس تعلّمتم أن Server-Sent Events وسيلة نقل HTTP القياسية لبث رموز LLM إلى عملاء المتصفح، وأن StreamingResponse مع text/event-stream يحوّل أي مولّد غير متزامن إلى تدفق SSE في FastAPI، وأن الترويسات الأساسية، بما فيها X-Accel-Buffering وCache-Control، مطلوبة لضمان السلوك الصحيح خلف الوكلاء الوسيطين. تعاملوا مع انقطاعات العميل لتجنب استدعاءات API اليتيمة إلى LLM. بعد ذلك سنتناول استجابات البث التي تتضمن استدعاءات الأدوات.

الأسئلة الشائعة

هل درس «البث في FastAPI باستخدام Server-Sent Events» مجاني؟

نعم — نص درس «البث في FastAPI باستخدام Server-Sent Events» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Engineering Academy، انتقل إلى CoddyKit PRO. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

ماذا ستتعلم في «البث في FastAPI باستخدام Server-Sent Events»؟

ابنوا نقطة نهاية في FastAPI توجّه استجابات البث من LLM إلى عميل متصفح باستخدام StreamingResponse ونوع المحتوى text/event-stream. تتمرن على AI Engineering Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ AI Engineering Academy؟

لا تُشترط خبرة سابقة. AI Engineering Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «البث في FastAPI باستخدام Server-Sent Events»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس AI Engineering Academy هذا؟

نعم. كل درس في AI Engineering Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. فهم بث الرموز
  2. استهلاك التدفقات باستخدام Python SDK
  3. البث في FastAPI باستخدام Server-Sent Events
  4. معالجة استدعاءات الأدوات في الاستجابات المتدفقة
← العودة إلى AI Engineering Academy