Skip to content

企业级 RAG 如何降低首 Token 延迟 ​

核心结论:RAG 的首 Token 延迟(TTFT)主要取决于模型开始生成前等待了多久。生产优化通常围绕四件事展开:缩短检索关键路径、减少最终上下文、复用稳定 Prompt 的计算、降低模型服务排队与网络等待。

先说结论 ​

一次 RAG 请求可以粗略表示为:

text
用户问题
  -> 鉴权与排队
  -> 查询处理
  -> Query Embedding
  -> 检索与过滤
  -> 融合与重排
  -> Context 组装
  -> LLM Prefill
  -> 首 Token

想让首 Token 更快,优先做下面四件事:

  1. 简单问题走短路径:不要每个请求都执行 Query Rewrite、HyDE、Multi-Query 和 Reranker。
  2. 检索并行执行:向量检索、关键词检索和必要的结构化过滤尽量并行。
  3. 减少最终 Prompt:召回可以多一些,但进入模型的证据应少而精,避免把大量无关 chunk 塞进上下文。
  4. 复用已有计算:缓存 Query Embedding、检索结果、稳定 Prompt 前缀和可复用的 KV Cache。

流式输出也很重要,但它主要改善用户看到内容的时间,并不自动减少后端真正完成检索和模型 Prefill 所需的时间。

1. TTFT 到底是什么 ​

1.1 TTFT、TTFB 和总延迟 ​

几个指标经常被混在一起:

指标含义主要反映什么
TTFB收到第一个响应字节的时间网关和网络是否开始响应
TTFT收到模型第一个 Token 的时间检索、Prompt 处理和模型 Prefill
首个有效内容时间收到第一段有意义答案的时间用户真正开始获得答案的时间
Total Latency完整答案生成结束的时间检索、生成和输出长度的总和

例如,系统在检索完成前先返回“正在查询”,TTFB 可能很小,但 TTFT 或首个有效内容时间并没有真正改善。另一方面,模型已经开始流式输出,也不代表最终答案会更快完成。

1.2 一个实用的延迟分解 ​

可以把 TTFT 拆成:

text
TTFT =
    网关排队
  + 鉴权与限流
  + Query Rewrite / Router
  + Query Embedding
  + 检索与过滤
  + 融合与 Rerank
  + Context 组装
  + Prompt 上传
  + 模型队列等待
  + 模型 Prefill
  + 首 Token 推理
  + 网关转发

如果多个检索分支并行,检索阶段更接近:

text
T_retrieval = max(T_dense, T_sparse, T_structured)

如果所有步骤串行执行,则会变成:

text
T_retrieval = T_dense + T_sparse + T_structured

所以“把某个组件优化了 20 毫秒”不一定能让端到端 TTFT 下降 20 毫秒。只有关键路径上的耗时才会直接影响 TTFT。

1.3 生产上应该记录什么 ​

每次请求至少记录以下阶段耗时:

text
queue_wait_ms
auth_ms
router_ms
rewrite_ms
query_embedding_ms
dense_retrieval_ms
sparse_retrieval_ms
filter_ms
fusion_ms
rerank_ms
context_build_ms
llm_queue_ms
llm_prefill_ms
first_token_ms
total_ms

同时记录:

  • P50、P95、P99,而不是只看平均值;
  • 是否命中 Query、Retrieval、Prompt 或 Answer Cache;
  • 输入 Token、缓存 Token、输出 Token;
  • 是否执行了 Rewrite、HyDE、Multi-Query 和 Reranker;
  • 召回候选数、过滤后数量和最终上下文数量;
  • 模型、embedding、reranker、索引快照和 Prompt 版本;
  • 是否发生超时、重试、降级和网关缓冲。

vLLM 的指标文档将请求延迟、队列等待、Prefill 和生成阶段分开观察,这种分解方式适合用于自建模型服务的监控设计。使用托管模型时,也应在应用侧补齐 RAG 检索和上下文构造的 span。

2. 优化方向一:缩短检索关键路径 ​

2.1 不要让每个问题都走完整 RAG ​

完整链路可能是:

text
大模型改写
  -> Multi-Query
  -> 多路检索
  -> Reranker
  -> 上下文压缩
  -> 大模型回答

这条链路适合复杂问题,却不适合所有请求。企业应用通常应该先进行轻量路由:

text
简单 FAQ       -> 直接检索
多轮指代       -> standalone question
精确编号查询   -> 关键词 / BM25 优先
复杂比较问题   -> 问题分解或 Multi-Query
高风险问题     -> 保留完整证据校验

路由可以使用规则、小模型或轻量分类器。关键不是“永远不用改写”,而是避免为简单问题支付复杂策略的固定成本。

2.2 原始问题和改写问题可以并行 ​

多轮问题需要改写时,可以保留原始查询并立即开始第一路检索:

text
原始问题 ──> Query Embedding ──> 第一轮检索 ──┐
    │                                          ├─> 融合 / 重排
    └────> Standalone Rewrite ──> 第二轮检索 ──┘

这样改写模型较慢时,原始检索仍然在推进。简单问题如果原始结果已经足够,可以直接结束;只有复杂或证据不足时,才等待第二路结果。

需要注意:并行会降低关键路径上的等待,但会增加总调用量、连接数和后端负载。因此要设置最大并发、超时和候选上限。

2.3 Dense、Sparse 和结构化检索并行 ​

常见混合检索可以设计成:

text
                    ┌─> Dense Retrieval
用户问题 ────────────┼─> BM25 / Sparse Retrieval
                    └─> Metadata / SQL Filter
                               │
                               v
                         Fusion -> Rerank

不要默认采用下面这种串行链路:

text
Dense -> BM25 -> Filter -> Fusion -> Rerank

如果某个步骤确实依赖前一个步骤的结果,例如先识别实体再查图谱,就不能强行并行;但没有依赖关系的检索分支应尽量同时执行。

权限过滤不能为了性能而放到最后。无权数据不能先进入候选集、Reranker、Prompt 或日志,再由应用层隐藏。应该使用向量库的 pre-filter、分区索引、租户隔离索引或服务端 ACL 条件。

2.4 控制 Reranker 的成本 ​

Reranker 的作用是提高候选排序质量,但它会增加一次额外计算:

text
向量召回 top 100
  -> Reranker 对 100 个候选打分
  -> 选出 top 8

可以按问题类型做条件化:

text
简单 FAQ:top 20,必要时跳过 Reranker
普通问答:top 30,Rerank 后保留 5-8 条
复杂问题:top 50 或更多,保留 Reranker
高风险问题:允许更高延迟,执行证据校验

是否跳过 Reranker 不能凭感觉决定,应该通过消融实验比较:

  • Recall@k、MRR 或 nDCG;
  • 最终答案正确率和引用准确率;
  • P95 TTFT;
  • 每次请求成本;
  • 不同问题类型的退化情况。

3. 优化方向二:减少模型需要处理的上下文 ​

3.1 召回数量和最终上下文不是一回事 ​

“召回更多”与“给模型更多”是两个不同决定:

text
召回 top 50
  -> 去重
  -> Rerank
  -> 合并相邻窗口
  -> 删除明显重复内容
  -> 最终只给模型 top 5-8 个证据块

候选阶段可以适当宽一些,避免正确证据没有进入候选;生成阶段则应控制 Prompt 大小,避免无关内容增加 Prefill 时间并干扰模型。

3.2 影响 Prefill 的主要因素 ​

对长上下文 RAG 来说,模型在输出首 Token 前需要先处理输入上下文。通常需要重点关注:

  • 最终输入 Token 数;
  • chunk 是否重复;
  • 是否把整篇文档而不是相关窗口放入 Prompt;
  • 是否把所有工具定义和历史对话都重复发送;
  • 是否包含无关的检索分支结果;
  • 是否存在过长的系统指令和示例。

可以保留:

  • 能直接支持答案的句子;
  • 必要的标题、章节路径和版本信息;
  • 解释条件和例外所必需的相邻内容;
  • 引用所需的 document、chunk 和 locator。

可以删除:

  • 同一文档重复召回的片段;
  • 只因为相似但不能支持问题的片段;
  • 与当前问题无关的历史对话;
  • 过期版本和已经被撤回的内容。

压缩上下文不能只用“Token 越少越好”作为目标。删除一个例外条件,可能让回答更快但更错。应同时测量 Context Recall、答案忠实度和引用覆盖率。

3.3 先解决数据和分块问题 ​

如果每个 chunk 都需要拼接大量重复标题、冗余摘要和全文上下文,在线压缩只是补救。更好的做法是在离线阶段:

  • 保存章节路径和稳定标题;
  • 生成可检索的文档摘要;
  • 为表格、代码和 PDF 保存结构化定位信息;
  • 预先去除页眉、页脚和重复导航;
  • 记录 chunk 之间的父子关系;
  • 将常用的上下文窗口预先计算。

这样在线请求只需组合必要信息,不必临时做昂贵的全文处理。

4. 优化方向三:复用 Prompt 和 KV Cache ​

4.1 Prompt Cache 解决什么问题 ​

Anthropic Prompt Caching 文档说明,Prompt Cache 可以复用请求中相同的前缀,减少重复处理时间和成本;文档还明确指出,长文档场景通常可以改善 TTFT。

推荐把 Prompt 排成:

text
[稳定系统指令]
[稳定工具定义]
[稳定输出格式]
[稳定背景知识]
----------------
[本次用户问题]
[本次检索证据]

稳定部分放在前面,动态内容放在后面。这样多次请求只需要重新处理后缀。

4.2 Prompt Cache 的边界 ​

Prompt Cache 通常是精确前缀匹配,不是语义缓存。下面的内容变化都可能导致缓存失效:

  • 系统指令;
  • 工具定义和参数顺序;
  • Prompt 中的时间戳或随机 ID;
  • 模型或推理配置;
  • 缓存断点之前的检索内容;
  • 图片、文档和消息顺序。

因此应把动态内容放到缓存前缀之后,而不是把每次变化的请求内容也标记为缓存点。

企业 RAG 还必须保证缓存隔离:

text
租户、用户权限、权限版本、数据快照和安全策略
不能被缓存机制绕过

公共、稳定、非个性化知识可以共享缓存;用户私有数据、薪资、订单和刚撤回的制度需要更严格的缓存键或直接禁止共享。

4.3 Prompt Cache 不等于答案缓存 ​

两者作用不同:

类型复用什么主要收益主要风险
Prompt Cache稳定 Prompt 前缀的计算结果降低输入处理和 TTFT前缀变化导致失效
Retrieval Cache查询对应的证据 ID跳过部分检索数据更新和权限失效
Answer Cache最终答案直接返回答案过期、越权、答案不适用

高风险企业问答通常更适合缓存检索结果或 Prompt 前缀,再重新生成和验证答案,而不是直接复用最终答案。

4.4 自建模型中的 KV Cache ​

自建推理服务通常可以复用相同前缀对应的 KV Cache。SGLang 的 RadixAttention就是围绕共享前缀和 KV Cache 复用设计的代表性工作。

RAG 还有一个特殊问题:检索证据的顺序每次可能不同。如果把整段动态证据直接拼成一个完全不同的 Prompt,前缀复用效果会下降。可以考虑:

  • 保持稳定的系统指令、工具定义和输出格式;
  • 对证据做稳定排序和规范化;
  • 对高频公共文档预计算部分上下文;
  • 只缓存安全、稳定、可复用的证据单元;
  • 监控缓存命中后的答案质量,不要只看速度。

RAGCache 论文专门讨论了面向 RAG 的多级知识缓存,说明 RAG 缓存不仅可以缓存最终答案,也可以围绕知识片段和模型计算结果设计更细粒度的复用机制。

5. 优化方向四:模型服务、网络和流式传输 ​

5.1 模型服务 ​

如果检索只需要 100 毫秒,但模型队列等待了 1 秒,继续优化向量库不会解决问题。模型服务需要重点观察:

  • 冷启动时间;
  • 请求进入模型队列后的等待时间;
  • 长上下文请求是否阻塞短请求;
  • 在线请求是否与离线评测、批处理共用资源;
  • 输入长度和输出长度是否有上限;
  • 模型实例是否提前预热;
  • 是否需要把交互请求和长任务分到不同队列。

可以使用小模型承担路由、问题独立化和格式检查,把大模型留给需要复杂回答的请求。但小模型调用本身也有成本,必须验证它是否真的比直接走大模型更快。

5.2 网络和网关 ​

企业系统中,网络开销可能被低估:

  • 应用服务、向量库、Reranker 和模型尽量部署在同一地域;
  • 复用 HTTP、TLS、数据库和 gRPC 连接;
  • 不要在多个服务之间反复传递完整原文;
  • 内部服务优先传递 chunk ID、版本和必要字段;
  • 检查代理和网关是否开启响应缓冲;
  • 确认 SSE 或其他流式协议能够逐块转发。

如果模型已经生成了首 Token,但网关等待完整 JSON 后才把响应返回给客户端,用户仍然感受不到流式输出。此时优化的是传输链路,而不是模型本身。

5.3 流式输出能做什么,不能做什么 ​

流式输出可以让用户更早看到模型已经生成的内容,但它不能替代检索。如果证据还没有回来,就不应该让模型先生成未经证据支持的事实答案。

可以先发送明确的状态事件:

text
检索中 -> 已找到证据 -> 正在生成 -> 完成

这改善的是交互体验,不应被当成真正降低 TTFT。对于复杂研究任务,可以把它设计成异步任务,先返回任务状态,再持续推送结果。

6. 按瓶颈选择方案 ​

观察到的问题优先处理方向不要先做什么
Rewrite 占用大量时间规则、小模型、按需改写、原始查询并行给所有问题增加更多改写分支
Dense 和 BM25 串行并行检索、统一超时和融合先更换整个向量数据库
Reranker 很慢减少候选、按需启用、使用更小模型无条件提高 top-k
Prompt Token 过多去重、重排、窗口化、压缩只提高模型上下文窗口
首次请求特别慢模型预热、Prompt Cache 预热、连接池把首次请求当成平均值
热点问题重复检索Query Embedding / Retrieval Cache直接缓存所有最终答案
模型队列等待明显扩容、独立队列、长短请求隔离只调向量库参数
后端已流式但前端很晚看到检查网关缓冲、SSE 转发和客户端再换一个生成模型

7. 一个简单的生产链路 ​

7.1 快速路径 ​

适用于简单 FAQ 和高频问题:

text
鉴权
  -> 轻量路由
  -> Query Embedding Cache
  -> Dense + Sparse 并行检索
  -> 小候选集重排
  -> Prompt Cache
  -> 流式生成

7.2 复杂路径 ​

适用于比较、多跳和冲突问题:

text
鉴权
  -> 问题分解
  -> 多数据源检索
  -> 证据补查
  -> 冲突处理
  -> 引用校验
  -> 生成答案

复杂路径不应该和 FAQ 使用同一个 TTFT 目标。更合理的方式是按问题类型分别设置延迟和质量目标,并允许复杂任务异步化。

8. 降级策略与安全边界 ​

当依赖变慢时,可以按顺序考虑:

text
命中高质量 Retrieval Cache
  -> 跳过非必要 Query Rewrite
  -> 减少 Multi-Query 分支
  -> 普通问题跳过 Reranker
  -> 缩小上下文
  -> 使用更小的回答模型
  -> 返回已有高置信证据或明确拒答

不能为了 TTFT 而跳过:

  • 租户和 ACL 过滤;
  • 删除、撤权和版本检查;
  • 高风险问题的证据校验;
  • 引用必须对应真实来源的约束;
  • 外部网页内容的不可信数据边界。

速度优化的正确目标是:

text
在不突破安全和质量门禁的前提下,缩短关键路径

9. 如何证明优化真的有效 ​

建议冻结一份带问题类型标签的评测集,至少包含:

  • 简单单事实问题;
  • 多轮指代问题;
  • 精确编号、错误码和专有名词查询;
  • 多条件和多跳问题;
  • 无答案、过期版本和权限边界问题。

然后逐项做消融实验:

text
A:基础 RAG
B:A + Query Rewrite
C:B + Reranker
D:C + Context Compression
E:D + Prompt Cache / KV Cache

每组都记录:

  • TTFT P50/P95/P99;
  • 完整答案延迟;
  • Recall@k、MRR 或 nDCG;
  • Context Precision 和 Context Recall;
  • Answer Faithfulness;
  • Citation Precision;
  • 拒答和权限错误率;
  • 每次请求成本;
  • 缓存命中率和陈旧命中数。

如果跳过 Reranker 后 TTFT 下降,但引用正确率和多条件问题准确率明显下降,这不能算成功。延迟优化必须和质量、安全、成本一起验收。

10. 常见误区 ​

误区一:打开流式输出,TTFT 就降低了 ​

流式输出只改变响应传输方式。它可以让客户端更早收到已经生成的内容,但不能自动减少 Query Rewrite、检索、Rerank、Prompt Prefill 或模型排队时间。

误区二:把 top-k 调小就一定更快更好 ​

top-k 过小可能直接漏掉正确证据;top-k 过大又会增加 Rerank 和 Prompt 成本。应区分候选召回数量与最终上下文数量,并按问题类型评估。

误区三:缓存命中率高就代表系统优化成功 ​

缓存可能命中旧版本、错误租户或过期权限。必须同时记录缓存对应的索引快照、权限版本、文档版本和陈旧命中数。

误区四:Prompt Cache 可以缓存“相似问题” ​

Prompt Cache 通常依赖精确一致的前缀,不是语义相似缓存。相似问题的答案缓存还要额外处理证据、权限、时间和问题约束,不能混为一谈。

误区五:TTFT 越低越好 ​

高风险合同、财务、医疗和权限问题不能为了几十毫秒而跳过证据检查。应按业务风险设置不同 SLO,复杂问题允许更高延迟,简单问题走快速路径。

11. 面试回答模板 ​

面试中可以这样回答:

我会先把 TTFT 拆成网关排队、查询处理、embedding、检索、重排、上下文组装、模型队列、Prefill 和首 Token 推理,而不是直接换模型。然后根据 trace 找关键路径:查询改写慢就按问题复杂度路由,检索分支独立时就并行,Reranker 候选过多就控制 top-k,Prompt 过长就去重和压缩。对于稳定系统 Prompt 和长文档,会考虑 Prompt Cache 或 KV Cache;对于模型队列和网络问题,则做预热、连接复用、长短请求隔离和真正的流式转发。最后用 TTFT、完整延迟、召回、答案忠实度、引用准确率、权限错误率和成本做联合验收,不能只看首 Token 变快。

先看一个小场景 ​

“公司地址是什么”只需一次过滤和检索,却和“比较三份制度并给出例外”共用同一条长链路,首 Token 自然会被慢请求拖住。可以让简单问题走短路径,复杂问题再启用改写、并行召回和 reranker;同时缩短送入模型的上下文,TTFT 往往比单纯把输出流打开更容易改善。

读完后可以分别测一条简单地址查询和一条多制度比较,观察短路径、上下文长度和模型排队对 TTFT 的影响。

持续学习,持续实践。