透過 SSE 串流回應¶
本導覽會追蹤聊天回應從 FastAPI 路由器傳至瀏覽器用戶端的完整流程。您將了解確切的 SSE 傳輸格式、如何使用 StreamingResponse,以及 Python 技術堆疊為何刻意維持與 .NET API 相同的合約。
API 進入點¶
app/routers/chat.py 會處理 POST /api/chat/stream。它接受 ChatRequest,選擇要求指定的模型;若未指定,則使用 claude-haiku-4.5 預設值,並傳回 FastAPI StreamingResponse:
@router.post("/stream")
async def stream_chat(request: Request, body: ChatRequest) -> StreamingResponse:
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
)
這些標頭會告知中介服務與瀏覽器:這是一個長效串流,而不是應緩衝至完成才傳送的一般 JSON 回應。
傳輸格式¶
對於來自 CopilotChatService.chat_stream的每個資料區塊,路由器會將小型 JSON 物件序列化,並寫入一則 SSE 訊息:
串流正常結束時,端點會寫入哨兵值:
如果串流啟動後發生例外狀況,路由器會在同一個 SSE 資料通道中寫入錯誤事件:
此時已無法可靠地切換為 HTTP 錯誤狀態。狀態碼和回應標頭都已送出,因此若要回報較晚發生的失敗,唯一實用的方法就是將錯誤放入串流承載資料中。
刻意維持 .NET 相容性¶
傳輸合約與 .NET API 相符:使用 data: {...}\n\n 訊框,並以 data: [DONE]\n\n 終止。Python 的 json.dumps 與 .NET 的 JsonSerializer所產生的 JSON 空白可能不同,但 content/error 承載資料結構與哨兵值皆相同。Python 頁面將連接埠改為 5070,因為 FastAPI 會由同一個處理程序同時提供 API 與使用者介面。
要求本文別名¶
ChatRequest 使用 Pydantic 的 camelCase 別名產生器:
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
prompt: str | None = None
model: str | None = None
system_message: str | None = None
這能讓 Python 程式碼維持慣用寫法(system_message),同時保留 systemMessage 這個 .NET 用戶端與文件所使用的 HTTP 合約。
中斷連線處理¶
在產生器內,路由器會先檢查瀏覽器是否已離線,再寫入下一個訊框:
async for chunk in service.chat_stream(prompt, model, body.system_message):
if await request.is_disconnected():
break
yield f"data: {json.dumps({'content': chunk})}\n\n"
如果分頁已關閉或要求已放棄,API 就能停止寫入並結束串流工作。
緩衝式聊天與健康狀態¶
同一個路由器也公開 POST /api/chat 以提供緩衝式回應,以及 GET /api/chat/health 供不需 SDK 傳輸連線的探查使用:
response = await _service(request).chat(body.prompt or "", model, body.system_message)
return ChatResponse(content=response, model=model)
實際健康狀態輸出:
{"status":"healthy","service":"CopilotChat","availableModels":["claude-haiku-4.5","gpt-4.1","gpt-5","claude-sonnet-4.5","claude-opus-4.5","gemini-2.5-pro"]}
使用 curl 試用¶
將要求傳送至連接埠 5070。使用 curl -sN,讓 curl 不會緩衝回應:
$ curl -sN -X POST http://localhost:5070/api/chat/stream \
-H 'Content-Type: application/json' \
-d '{"prompt":"Reply with exactly: streaming works","model":"claude-haiku-4.5"}'
data: {"content": "streaming works"}
data: [DONE]
⚠️ 欄位名稱是 prompt,不是 message。無法辨識的索引鍵會遭忽略,因此這裡若有拼字錯誤,就會在沒有任何提示的情況下送出空白提示,模型會回覆一般問候語,而不是傳回錯誤。
助理實際傳回的文字取決於模型與帳戶狀態,但重點在於訊框結構。較長的回答只會以更多 data:
訊框陸續抵達,最後才是哨兵值。
瀏覽器用戶端¶
靜態使用者介面會讀取相同的 data: 訊框,相關程式碼位於 app/static/app.js。它使用 fetch()、 res.body.getReader(),以及 TextDecoder;完整的呈現路徑請參閱 網頁使用者介面。