RAG 服务化:检索管线怎样对外提供接口
FastAPI(/chat、SSE、超时、线程池)、检索与生成分开评测、Dockerfile 纪律。历史服务实现仅作可选对照。
对应课件:工程查阅(原 Job-Ready)
第 29 章 · RAG 服务化:检索管线怎样对外提供接口
把第 28 章的检索管线做成服务时,要处理 /chat 的 answer + citations、SSE 流式、超时和失败定位。这是服务化原理,不是当前主线的 Checkpoint 作业。
当前主线不是再交付一个 RAG 服务。本章保留 FastAPI / SSE / 超时 / 线程池原理,以及当时的评测数字。projects/rag-service/ 是可选历史参考,非当前作业。
历史运行:课程内容 28 篇 → 841 chunks;22 条标注查询;Qwen2.5-0.5B 生成(CPU)。
29.1 从「跑通检索」到「能对外服务」
当时那次实验覆盖过这些能力(作为阅读清单,不是现在要交的验收表):
| 能力 | 验收物 |
|---|---|
| 完整管线 | documents → chunk → BM25+向量 → hybrid → rerank → LLM → answer+citations |
| 评测 | 检索侧 4 组对照(Hit@k / Recall@k / MRR / nDCG@k,doc 级)+ 生成侧失败分类 |
| 服务 | FastAPI:/health /retrieve /chat /chat/stream(SSE) |
| 工程 | Pydantic schema、超时保护(504)、并发线程池、pytest 27 用例 |
| 部署 | Dockerfile(本机无 Docker,标注 NOT EXECUTED) |
29.2 Pipeline 架构与实测数字
ingest: 文档目录 → chunk(recursive 512/64)→ BM25 索引 + BGE 向量(FAISS)
query: BM25 top-20 ┐
向量 top-20 ┴→ RRF 融合 → cross-encoder 精排 top-5 → prompt → Qwen → answer + [n] 引用
真实评测结果(22 条标注查询,全部可复现):
| 模式 | Hit@10 | Recall@10 | MRR | nDCG@10 |
|---|---|---|---|---|
| BM25 | 100.0% | 100.0% | 0.9091 | 0.9329 |
| Dense | 86.4% | 86.4% | 0.6417 | 0.6991 |
| Hybrid(RRF) | 100.0% | 100.0% | 0.8485 | 0.8884 |
| Hybrid + Reranker | 100.0% | 100.0% | 0.9318 | 0.9497 |
调用链路的服务端耗时拆解(每个回答都返回):
"latency_ms": { "retrieve": 42, "rerank": 1850, "generate": 13000, "total": 14900 }
rerank 占大头、generate 更甚——这就是服务优化时该先动的地方(更小的 reranker / 更快的模型 / 缓存)。
29.3 FastAPI 服务化:五个工程要点
@app.post("/chat")
async def chat(req: ChatRequest) -> ChatResponse:
result = await asyncio.wait_for(
run_in_threadpool(pipeline.answer, req.question, req.mode, req.top_k, req.top_n, req.use_rerank),
timeout=pipeline.cfg.request_timeout_s,
)
return ChatResponse(answer=..., citations=..., sources=..., latency_ms=...)
| 要点 | 做法 | 为什么 |
|---|---|---|
| Pydantic schema | ChatRequest / ChatResponse / RetrieveRequest 等 |
请求校验 + 自动文档 + 类型安全 |
| 线程池 | run_in_threadpool 包装同步 CPU 任务 |
不阻塞事件循环,服务还能响应健康检查 |
| 超时 | asyncio.wait_for(..., 60s) → 504 |
模型生成慢,必须有上限,防拖垮服务 |
| SSE 流式 | /chat/stream 先发 sources 事件再发 token |
ChatGPT 式体验;引用在生成前就可见 |
| 健康检查 | /health 返回 docs/chunks/模型名 |
部署探活、容器编排必需 |
测试思路(无需真实 API key):用 FastAPI TestClient 打端点——健康检查、检索、chat、SSE 首事件、超时 504。历史参考实现把这些写在测试里;那不是当前作业。
async def chat() 里直接调用同步的 pipeline.answer()(里面是模型推理)会阻塞整个事件循环——所有请求排队,健康检查都会超时。
正确做法:把同步推理放进 run_in_threadpool。CPU 密集 + async 是 FastAPI 服务最常见的坑。
29.4 流式(SSE):为什么 ChatGPT 是"打字机"
def event_source():
for kind, payload in pipeline.answer_stream(question, ...):
if kind == "sources":
yield "data: " + json.dumps({"type": "sources", "sources": payload}) + "\n\n"
else:
yield "data: " + json.dumps({"type": "token", "text": payload}) + "\n\n"
yield "data: {\"type\": \"done\"}\n\n"
客户端按 data: 行解析事件。首字节时间(用户感知速度)从「全部生成完」降到「秒级首批 token + sources」。
29.5 Docker:提供但未实测
docker build -t rag-service .
docker run -p 8000:8000 \
-v $(pwd)/../../content:/corpus \
-e RAG_CORPUS=/corpus \
rag-service
开发机当时未装 Docker;仓库 CI 里仍保留对历史镜像的 docker build,用来检查还留在磁盘上的代码能否构建,不是要你交一份 RAG 作业。本地命令仅供对照:docker build -t rag-service .
29.6 真实运行:6 题端到端
检索命中 6/6 | 引用规范 4/6 | 耗时中位数 19.1s(CPU,0.5B 模型)
✅ PagedAttention → 回答了显存碎片机制,引用 [1]
✅ GQA vs MQA → 回答了分组折中与 KV 压缩,引用 [1]
✅ pass@k → 写出无偏估计公式,引用 [1]
✅ MinHash → 回答了签名估计与 LSH,引用 [1]
❌ online softmax → 长回答但未标注引用(uncited_answer)
❌ Chat Template → 回答了但未标注引用(uncited_answer)
样本外结论(纪律):6 题、CPU、0.5B —— 这是流程验证级实验,不是能力结论;但它完整展示了「如何用失败分类定位系统瓶颈」。
29.7 面试复盘:RAG Service
- 为什么
/chat要把同步推理放进线程池?不放会怎样? - 超时怎么设计?504 和 500 的区别是什么场景?
- SSE 相比一次性返回有什么体验差异?服务端怎么实现?
/retrieve调试端点为什么有价值?(提示:线上定位「检索还是生成」)- 响应里的
latency_ms拆解对性能优化有什么用? - Docker 镜像里模型权重怎么处理?(提示:构建时下载 vs 运行卷挂载)
- 如果并发上来了,哪些环节可以缓存?(提示:索引、embedding、rerank 结果、LLM 响应)
显示答案与解析
正确答案:B
单请求正常、并发崩——典型的资源阻塞问题。若在 async 端点里直接跑同步推理(未放线程池),或线程池/并发配置太小,请求全部排队直至超时。修法:推理放线程池 + 限制并发 + 必要时多副本扩展。| 站点 | 核心结论 |
|---|---|
| 管线 | chunk → BM25+向量 → RRF → rerank → LLM → answer+citations |
| 实测 | reranker 收益最大(MRR 0.849→0.932,超越 BM25 的 0.909);检索 6/6、生成 4/6 |
| 服务 | FastAPI + Pydantic + 线程池 + 超时 504 + SSE |
| 坑 | async 里跑同步推理 = 阻塞事件循环 |
| 部署 | Dockerfile 已提供;本机无 Docker → NOT EXECUTED |
| 定位 | /retrieve + latency 拆解 + 失败分类三件套 |