"""一个最小的网页聊天:后端用 FastAPI 把模型的输出流式转发给浏览器。
准备:uv add fastapi uvicorn
运行:uvicorn streaming_web:app --port 8000
然后用浏览器打开 http://127.0.0.1:8000
"""
import json
import os
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, StreamingResponse
from openai import AsyncOpenAI
client = AsyncOpenAI( # 网页服务要同时应付很多请求,用异步客户端
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ.get("LLM_BASE_URL", "https://api.deepseek.com"),
)
MODEL = os.environ.get("LLM_MODEL", "deepseek-flash")
app = FastAPI()
@app.get("/chat")
async def chat(q: str):
async def events():
stream = await client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": q}],
stream=True,
extra_body={"thinking": {"type": "disabled"}},
)
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
# SSE 的格式:每条消息以 "data: " 开头,以空行结尾
yield f"data: {json.dumps(chunk.choices[0].delta.content, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(events(), media_type="text/event-stream")
PAGE = """<!doctype html>
<meta charset="utf-8">
<input id="q" size="40" value="用三句话介绍 httpx"> <button onclick="ask()">问</button>
<pre id="out" style="white-space: pre-wrap"></pre>
<script>
function ask() {
const out = document.getElementById("out");
out.textContent = "";
const source = new EventSource("/chat?q=" + encodeURIComponent(document.getElementById("q").value));
source.onmessage = (e) => {
if (e.data === "[DONE]") { source.close(); return; }
out.textContent += JSON.parse(e.data);
};
}
</script>"""
@app.get("/")
async def index():
return HTMLResponse(PAGE)