简介:本文面向正在上线RAG问答与生成类应用的工程师与架构师,提供一套检索池与推理池解耦的完整部署方案,覆盖资源规划、embedding预计算、异步编排、查询缓存、预取与超时降级,并配套上线验证指标与排查思路,帮助读者消除GPU空等、控制首Token延迟,让推理侧几乎感知不到检索抖动。
本文要部署的对象,是一条典型的检索增强生成(RAG)生产链路:用户请求进入后,先经过多路召回(向量检索、关键词检索、重排),把召回内容拼装成上下文,再交给大语言模型(LLM)完成流式生成。要部署的不只是“两个能跑的服务”,而是把它们组织成一条互不拖累的生产链路,具体包括:独立扩缩容的检索池、GPU推理池、异步编排层、两级缓存以及面向链路的监控告警体系。
部署完成后应达到的效果:
适合读者:正在上线或改造RAG应用的后端工程师、平台与SRE工程师、系统架构师,以及关注GPU资源成本的技术负责人。
部署前需要理解的背景是:检索和推理根本不是同一类负载。检索主要消耗CPU、内存带宽、磁盘IO以及到向量库的网络;推理主要消耗GPU算力与显存。把两者串成同步链路后,整条链的吞吐由最慢一段决定。生产环境中常见现象是:向量检索p99达三四百毫秒、偶发更高;LLM本身首token并不慢,但请求到达后必须干等检索结果;队列一积压,后续请求全部排队,表面看是“推理慢”,本质是GPU在空等。此时监控上一切“都在跑”,GPU利用率却只有五六十甚至更低,要怀疑的不是模型配置,而是链路结构本身。
这套方案主要适用于以下场景:
需要说明的是:如果业务是延迟完全不敏感的离线批处理,先用简单同步链路即可,后续流量上来再按本文方案演进,避免过度设计。
| 层次 | 组件 | 承载资源 | 职责 |
|---|---|---|---|
| 接入层 | 网关/负载均衡 | 通用计算 | 鉴权、限流、路由 |
| 编排层 | 异步编排服务 | CPU型实例或容器 | 检索任务异步调度、结果队列、超时降级、取消传播 |
| 检索池 | 向量检索、关键词检索 | 高主频CPU、大内存、低延迟磁盘、内网带宽 | 多路召回 |
| 重排模块 | 重排服务 | CPU或小规模加速资源 | 精排,建议不进主路径 |
| 推理池 | LLM推理服务 | GPU实例 | 流式生成 |
| 缓存层 | 本地LRU+远程缓存 | 实例内存+缓存服务 | query结果短缓存 |
| 数据层 | 向量库、对象存储 | 托管或自建 | 文档、embedding、索引分片 |
| 监控层 | 日志、指标、链路追踪 | 日志服务与监控告警 | 四类核心指标采集 |
三个架构要点:
1. 资源规划
2. 数据准备
3. 网络与安全
4. 配置基线
上线前必须确定的一组开关:检索超时预算与超时动作、top-k默认值与降级缩减值、缓存TTL与容量、上下文token预算、编排层最大并发、取消策略。
按“数据先行—检索池—推理池—编排层—缓存与预取—降级—监控—灰度”的顺序执行:
Step 1 数据先行:完成文档处理与embedding预计算,向量化入库并建好索引分片,预热热门分片。这一步必须最先做,否则后续所有延迟验证都会失真。
Step 2 部署检索池:在CPU型实例或容器上部署向量检索与关键词检索服务,配置健康检查,建立与向量库的内网连接,对单路召回压测,确认p99在预算内。
Step 3 部署推理池:在GPU实例上部署LLM推理服务,加载模型、开启流式输出接口,配置健康检查与显存水位保护;单池压测TTFT与吞吐基线。
Step 4 部署编排层(核心步骤):把“等检索完整返回再调模型”的同步调用,改造为异步任务提交加结果等待。要点有三:
Step 5 接入两级缓存:本地LRU承接热点query,远程缓存承接跨实例共享;query先做归一化(大小写、空白、同义改写)再作为缓存键,命中后直接跳过检索。
Step 6 配置预取:识别“用户点进某会话”“输入出现前缀”等前置信号,提前把相关文档热身到缓存或检索池。缓存命中率上来之后,GPU空等会明显下降。
Step 7 写死超时与降级:检索超时先缩减top-k;仍然超时则只用缓存命中部分;再不行先回一个“我先按已有上下文答”的流式开头,绝不让GPU干站着。注意:不要因为“不好意思降级”而不敢降,超时雪崩比优雅降级难看得多。
Step 8 配置监控与告警:强制采集四类指标(检索p99、LLM TTFT、GPU空闲率、队列深度),设置阈值告警,详见上线验证一节。
Step 9 灰度放量:小流量验证链路与降级开关,逐步放大至全量;期间保留旧版同步链路作为回滚路径。
| 配置项 | 作用 | 风险与注意事项 |
|---|---|---|
| 检索超时预算 | 限制单请求在检索上的最大等待 | 过小会频繁降级、影响答案质量;过大失去保护意义 |
| top-k动态缩减 | 超时降级的第一手段 | 缩得过小可能召回不足,需结合评测确定下限 |
| 上下文token预算 | 固定最小可答上下文,超出即截断 | 截断策略要优先保留高相关头部段落 |
| 缓存TTL | 几秒到几分钟的短缓存 | 过长会与知识库更新脱节,过短命中率上不去 |
| 编排层并发上限 | 同时保护检索池与GPU池 | 过低限吞吐,过高会引发级联雪崩 |
| 取消策略 | 用户离开即停止检索与生成 | 不做取消会形成成本黑洞 |
def handle(req):key = normalize(req.query) # query归一化后作为缓存键ctx = cache.get(key) # 两级缓存:本地LRU -> 远程缓存if ctx is not None:return stream_generate(req, ctx) # 命中即跳过检索fut = retrieval.submit_async( # 检索异步提交,不阻塞paths=["vector", "keyword"],top_k=DEFAULT_TOP_K,timeout=RETRIEVAL_BUDGET)try:ctx = fut.wait(RETRIEVAL_BUDGET)except Timeout:ctx = cache.partial_hit(key) or EMPTY_CONTEXTemit_header("先基于已有上下文作答") # 流式开头兜底ctx = truncate(ctx, MAX_CONTEXT_TOKENS) # 固定上下文预算for tok in llm.stream(req, ctx): # 流式生成emit(tok)if req.user_gone: # 取消传播fut.cancel(); release_slot(req); breakcache.put(key, ctx, TTL)
这段伪代码体现的核心逻辑是:缓存前置、检索异步、超时降级、预算截断、生成中取消。
功能验证:用真实query做端到端流式回答验证,检查上下文拼接正确性;人工触发一次降级路径,确认能返回流式开头且最终答案可接受。
指标验证:
| 指标 | 判定标准 |
|---|---|
| 检索p99 | 低于预设预算,无长尾毛刺 |
| LLM TTFT | 稳定在目标内,且波动与检索p99的相关性明显减弱 |
| GPU空闲率 | 处于低水位;GPU空闲高且队列非空,即判定为空等 |
| 队列深度 | 峰值不持续增长,可自然回落 |
| 缓存命中率 | 稳态达到预期,命中请求不再打向量库 |
压测与故障演练:压测峰值流量,观察是否出现超时雪崩;人为向向量库注入延迟,验证缩top-k、缓存兜底、流式开头三级降级是否按预期生效;模拟用户中途离开,确认GPU不再继续空转生成。
打通检索瓶颈,不是把向量库调到理论最快,而是让推理侧几乎感觉不到检索抖动。本次部署的核心可以概括为五个词:解耦(检索池与推理池分建分扩)、异步(编排层非阻塞调度与流水线重叠)、缓存(两级短缓存与预取)、降级(超时缩top-k、缓存兜底、流式开头)、观测(四类指标联动判断空等)。上线后的持续重点是两池的独立容量规划、缓存命中率运营、预取信号挖掘,以及定期的降级演练,只要这五件事站稳,GPU空等问题就能从架构层面被系统性消除。