AI Engineering Academy · บทเรียน

การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์

สร้างปลายทาง FastAPI ที่ทำหน้าที่ส่งต่อการตอบกลับแบบต่อเนื่องจาก LLM ไปยังไคลเอนต์เบราว์เซอร์ โดยใช้ StreamingResponse และชนิดเนื้อหา text/event-stream

บทเรียน 3 จาก 413 ขั้นตอน

การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์ เป็นบทเรียน AI Engineering Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 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 มาตรฐาน เชื่อมต่อใหม่โดยอัตโนมัติเมื่อตัดการเชื่อมต่อ และไม่ต้องใช้ไลบรารีเบราว์เซอร์เฉพาะ คุณสมบัติเหล่านี้ทำให้ SSE เป็นช่องทางส่งข้อมูลที่เหมาะอย่างยิ่งสำหรับการสตรีมโทเค็น 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 เพื่ออ่านเนื้อความการตอบกลับทีละส่วน รูปแบบนี้ใช้ในอินเทอร์เฟซเว็บของ 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 เปิดค้างและทำให้เกิดค่าใช้จ่ายเอพีไอโดยไม่จำเป็น ให้ห่อตัวสร้างด้วย 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 ร่วมกับคีย์เอพีไอหรือการตรวจสอบส่วนหัว 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 จำเป็นต่อการทำงานที่ถูกต้องหลังพร็อกซี ต้องจัดการเมื่อไคลเอ็นต์ตัดการเชื่อมต่อ เพื่อหลีกเลี่ยงการเรียกใช้เอพีไอ LLM ที่ไม่มีผู้ดูแล บทถัดไป เราจะจัดการการตอบกลับแบบสตรีมที่มีการเรียกใช้เครื่องมือ

เริ่มต้นได้ฟรี

เรียนรู้ Python ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
30
บทเรียน
120

คำถามที่พบบ่อย

บทเรียน “การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส AI Engineering Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์”

สร้างปลายทาง FastAPI ที่ทำหน้าที่ส่งต่อการตอบกลับแบบต่อเนื่องจาก LLM ไปยังไคลเอนต์เบราว์เซอร์ โดยใช้ StreamingResponse และชนิดเนื้อหา text/event-stream คุณปฏิบัติ AI Engineering Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน AI Engineering Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน AI Engineering Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน AI Engineering Academy นี้ได้ไหม

ได้ บทเรียน AI Engineering Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. ทำความเข้าใจการส่งโทเค็นแบบต่อเนื่อง
  2. การรับข้อมูลแบบต่อเนื่องด้วย Python SDK
  3. การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์
  4. การจัดการการเรียกใช้เครื่องมือในการตอบกลับแบบต่อเนื่อง
← กลับไปที่ AI Engineering Academy