先理解整个链路
前端通过 HTTP POST 提交 chat 或 resume;
FastAPI 处理会话鉴权、参数校验与请求可靠性;
业务层执行路由、Agent、Tool、RAG、状态持久化及输出保护;
流式端点使用 SSE 返回进度事件;
最终内容在完整生成并校验后拆分为片段推送。
一句话:FastAPI 负责提供 HTTP 接口,async/await 负责高效等待 I/O,SSE 负责服务器到浏览器的单向持续推送。三者不是同一个概念。
FastAPI:它解决什么问题
从一次 HTTP 请求理解后端
浏览器发起 POST /chat,携带 JSON;
FastAPI 用 Pydantic 对请求结构做校验;
依赖注入提供服务;
业务函数处理请求;
返回结构化 ChatResponse。
失败时按统一异常处理转换为 HTTP 状态码和错误体。
对比:路由负责入口,Service 负责业务编排,Repository/Store 负责持久化。不要把所有业务代码堆在路由函数里。
Pydantic 与 HTTP 错误码
Pydantic 不是用来保证模型说真话的,它验证输入/输出是否符合结构约束。
例如 session_id 非空、action 只能是 confirm/modify/cancel,modify 必须有 modification。
业务合法性仍需状态机、数据库和 Service 校验。
状态码速查表
| 状态码 | 含义 | 观尘关联 |
|---|---|---|
| 400/422 | 请求不符合要求 | 输入验证或业务字段非法 |
| 401 | 未授权 | 会话鉴权 |
| 409 | 当前状态冲突 | STALE_INTERRUPT、IDEMPOTENCY_CONFLICT、REQUEST_PENDING |
| 429 | 限流或繁忙 | 并发容量/访问频率控制 |
| 503 | 依赖服务不可用 | Redis 故障等 |
| 504 | 执行超时 | 请求超时边界 |
async/await:为什么 Agent 后端常用异步
I/O 密集 vs CPU 密集
LLM API、PostgreSQL 查询、Redis 命令、等待网络响应都属于 I/O 等待。
async 函数在 await 一个真正可异步等待的操作时,可以把控制权交还事件循环,让同一进程调度其他请求。
它不会让单次模型推理、SQL 执行或 CPU 计算本身更快。若在 async def 中直接执行长时间同步 CPU 计算或阻塞式网络调用,仍可能卡住事件循环。
xml
async def get_answer(llm, query: str) -> str:
# 网络等待期间,事件循环可以运行其他协程
response = await llm.generate(query)
return response.text
# 错误示例:在 async 函数中使用 time.sleep(5)
# 正确的异步等待:await asyncio.sleep(5)
并发不是并行
并发:多个任务在等待与运行之间交替推进。
并行:多个任务在同一时刻真正执行计算。
单线程事件循环可以有高并发 I/O,但并不自动获得多核 CPU 并行。
await、Task、Semaphore、Timeout
await:等待可等待对象,同时允许事件循环调度其他任务。
create_task:创建可并发推进的任务,但必须管理生命周期和异常。
Semaphore:限制同一时刻进入关键路径的请求数量。
timeout:为请求设置时间预算,避免无限等待。
xml
import asyncio
sem = asyncio.Semaphore(4)
async def guarded_call(coro):
async with sem:
async with asyncio.timeout(30):
return await coro
SSE:什么是服务器发送事件
它的通信方向
SSE(Server-Sent Events)在一个 HTTP 响应中持续输出事件,方向主要是服务端 → 客户端。用户发新消息或点击确认仍使用普通 POST。SSE 不等于 WebSocket,也不等于模型 token streaming。
标准响应 Content-Type 为 text/event-stream;事件可有 event、id、data 字段,事件之间用空行分隔。客户端可以逐条解析并更新界面。