การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์
สร้างปลายทาง FastAPI ที่ทำหน้าที่ส่งต่อการตอบกลับแบบต่อเนื่องจาก LLM ไปยังไคลเอนต์เบราว์เซอร์ โดยใช้ StreamingResponse และชนิดเนื้อหา text/event-stream
การส่งข้อมูลแบบต่อเนื่องใน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- ทำความเข้าใจการส่งโทเค็นแบบต่อเนื่อง
- การรับข้อมูลแบบต่อเนื่องด้วย Python SDK
- การส่งข้อมูลแบบต่อเนื่องใน FastAPI ด้วยเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์
- การจัดการการเรียกใช้เครื่องมือในการตอบกลับแบบต่อเนื่อง