code/03-llm-apps/streaming_web.py

60 行 · 1.9 KB
"""一个最小的网页聊天:后端用 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)