跳到正文

RAG PROJECT INTERVIEW · Q24 / 25

RAG 首字响应为什么慢?Embedding、检索和生成怎么逐段优化?

先拆 TTFT 全链路,再讨论批处理、缓存、ANN 索引、并行召回和流式输出,避免用“加机器”掩盖真正瓶颈。

作者与复核:吴师兄内容依据:公开 RAG 教程、GitHub 教学代码与工程推导查看关联教程 →查看吴师兄公开作品 →

25 道题目Q24
本页内容16 节

30 秒先说结论

先把首字前耗时拆成 Query 处理、Embedding、BM25/向量检索、Rerank、上下文构建、模型排队与首 Token 生成。文档向量应离线计算,重复 Query 可缓存 Embedding;独立的 BM25 与向量召回可并行;ANN 参数、候选数量和 Rerank 批次要一起调。最后用 SSE 流式返回改善可感知延迟。

用户点下发送以后,页面空了很久,随后答案一口气开始出现。

遇到这种现象,一种常见判断是大模型太慢,需要换一个更快的模型。

但一条 RAG 请求在模型生成第一个字之前,已经经过 Query 处理、Query Embedding、关键词与向量检索、候选融合、Rerank 和上下文构建。真正的等待可能发生在其中任何一段。

从调用依赖关系看,模型要等检索证据准备好,才能开始基于证据生成。因此,如果向量检索阶段很慢,流式输出只能改变第一个 Token 之后的展示,不能缩短前面的检索等待。这是链路顺序决定的。同样,如果 Trace 显示模型服务正在排队,继续调 HNSW 参数也无法触及这个阶段。

由此可以引出一道 RAG 工程面试题:

RAG 首字响应慢,怎样逐段定位瓶颈?Embedding、检索、Rerank、缓存和流式输出分别能解决什么问题?

只回答“加 Redis、开异步、上流式”通常不够。面试官会继续问:

  1. 首字响应和完整答案耗时有什么区别?
  2. 两路检索能不能并行,哪些步骤存在前后依赖?
  3. 缓存命中以后,权限和知识更新怎样保证?
  4. 把 ANN 参数调快了,召回质量下降怎么办?

首字响应和总耗时不是一回事

首字响应常用 TTFT 表示,关注从用户发出请求到看到第一个 Token 的时间。

总耗时关注从请求开始到完整答案生成结束。

两者会受到不同因素影响。

如果前置检索很慢、模型生成很快,TTFT 很差,总答案出来后却可能迅速结束。如果前置检索很快、答案很长,TTFT 不错,总耗时仍然很长。

要看清首字响应,可以先把链路拆成 Query Embedding、向量检索、Prompt 构建和网络、队列、I/O 等系统开销。一条完整的线上时间线还要加上路由、关键词召回、融合、Rerank、模型排队、预填充和第一个 Token。

首字响应与总耗时的区别

TTFT 只到第一个 Token,总耗时还包含后续生成。优化前要先确认用户抱怨的是哪一段等待。

如果所有阶段只记一个 response_time,你只能知道“整条请求慢”,却不知道慢在哪里。

更有用的记录方式是给每次请求分配 Trace ID,每个阶段写开始时间、结束时间、状态和关键配置。检索还要记录候选数量、是否命中缓存、使用了哪个索引和模型版本。

平均值也不够。

少量特别慢的请求可能被平均值掩盖。至少要区分中位数、P95、P99,并同时看错误率。高峰期排队、第一次加载模型、缓存未命中和日常请求也应该分组。

一条请求在首字前经过什么?

可以按下面的顺序理解。

第一步,网关和应用服务接收请求,完成身份、租户和知识库范围校验。

第二步,进行 Query 处理。如果是多轮问题,还可能做话题判断和 Query 改写。

第三步,计算 Query Embedding。文档 Embedding 应在离线入库阶段完成,在线阶段通常只计算用户问题。

第四步,执行关键词召回和向量召回。两路之间没有结果依赖时,可以并行。

第五步,融合候选并做 Rerank,然后按门槛和上下文预算截取证据。

第六步,构建 Prompt,发送到模型服务。模型可能还要排队、加载上下文并完成预填充,之后才产生第一个 Token。

第七步,通过流式协议把 Token 送到前端。

RAG 首字前的分段计时链路

只有给每个阶段独立计时,才能知道优化应该落在缓存、检索、排序还是模型服务。

这些步骤不是都能并行。

检索需要 Query 或改写后的 Query,向量检索需要 Query Embedding,模型生成又需要最终证据。它们存在明确依赖。

但 BM25 与向量召回可以在准备好 Query 后并行,多个独立子查询也可以并发。并行的前提是它们互不依赖,而且下游资源能承受并发,不是把所有函数都改成 async 就会自动变快。

Embedding 阶段慢,怎么优化?

先确认在线请求有没有重复计算文档向量。

文档 Embedding 属于离线入库工作。在线问答通常只需要计算 Query 向量。如果每次问答都重新向量化整份资料,架构边界已经错了。

重复 Query 可以缓存 Embedding。这里的缓存只是一种可选工程策略:先规范化文本,再生成缓存键,命中后复用向量。是否值得采用,要看真实 Query 的重复程度和缓存维护成本。

但一个生产缓存键不能只有 Query 文本。

Embedding 模型升级后,同一句话对应的向量空间已经变化。缓存键至少要区分模型和版本。多租户系统还要考虑租户与数据边界,不能让一次缓存命中绕开访问控制。

批处理适合离线文档入库,或者多个请求积累后一起计算。它能摊薄网络与调度开销,却会引入等待批次形成的时间。吞吐量提高,不代表单个请求的 P99 一定降低。

所以批大小和等待窗口要在目标负载下一起测试吞吐、P95 和 P99,不能只看单次调用时间。

并发请求同样受服务端限流、连接池和队列影响。压测时可以从低并发逐步增加负载:并发不足时,部分资源可能没有被利用;继续提高并发,也可能只是在上游形成更长排队。通过队列长度、资源使用率、吞吐与尾延迟的同步变化,才能找到拐点。

检索阶段慢,先看索引还是先扩容?

检索优化里常见的 ANN 索引、分区、过滤、批量查询和并发连接,解决的问题并不相同。

ANN 索引用近似搜索换取速度。HNSW、IVF 等索引都有搜索宽度和构建参数,调得更激进通常会影响召回。

因此索引优化必须和固定评测集绑定。每次改变参数,都要检查正确证据是否仍进入前 K 条。查询快了但 Recall@K 下降,首字改善可能换来错误答案。

过滤是在搜索前缩小有效范围。

如果问题只允许查询某个知识库、某些文档或当前有效版本,先过滤再搜索既是权限要求,也可能减少候选范围。过滤条件应直接进入查询,让未授权和已失效内容不参与候选排序,而不是搜完全库后再二次删除。

BM25 和向量检索可以并行执行。串行写法会先等完一路再跑另一路,并行则等待较慢的一路完成后进入融合。

不过,并行会同时占用搜索引擎、连接池和 CPU。单请求变快的同时,也可能降低系统在高并发下的稳定性。要用并发压测验证,而不是只在本机发一个请求。

Rerank 为什么容易成为新瓶颈?

粗召回通常可以快速从大规模候选中找出一批可能相关的内容。Rerank 则要更细地比较 Query 和每个候选,成本会随着候选数量增长。

候选越多,召回覆盖可能越高,Rerank 工作量也越大。无限扩大 Top N 不是稳妥策略。

一条合理的排序链路会先做粗召回,再调用 rerankrerank_by_model 对有限候选做更精细的比较。昂贵排序应该作用于有限候选,而不是作用于整个知识库。候选数量由召回曲线和耗时曲线共同决定,不是越多越好。

选择候选规模时,先画出两条曲线。

一条是候选数量增加后 Recall@K 怎样变化。另一条是 Rerank 耗时和内存怎样变化。当召回增长已经很小,继续扩大候选却显著增加延迟,就应该停下来。

批量推理可以一次处理多组 Query 与文档对,减少调度开销。模型量化或更小的 Rerank 模型也可能降低耗时,但都必须重新验证排序质量,不能只看速度。

缓存能放在哪几层?

为了看清收益与失效边界,可以按缓存所处的位置把它分成三类。

第一类是 Embedding 缓存,复用重复文本的向量。

第二类是检索结果缓存,复用某个 Query 的候选文档。

第三类是完整答案缓存,对高度重复且答案稳定的问题直接返回结果。

越靠后,缓存收益可能越大,失效风险也越高。

Embedding 缓存依赖模型版本。检索缓存依赖知识库、文档版本、过滤条件、检索配置和权限。答案缓存还依赖生成模型、Prompt、引用和时效。

三层缓存的收益与失效风险矩阵

缓存位置越接近最终答案,需要纳入缓存键和失效策略的上下文越多。

假设同一句问题在两个知识库里答案不同,缓存键却只有 Query。第一个用户的检索结果可能被第二个用户复用,这已经不是性能小问题,而是数据越界。

文档更新后,旧缓存也要失效。可以把知识库版本写入缓存键,或者在发布新版本时主动清理相关条目。

缓存命中率要按场景统计。如果大多数问题都不重复,维护复杂失效逻辑可能不划算。缓存不是必选组件,而是用真实重复度换取的工程策略。

流式输出能解决什么,不能解决什么?

流式输出让模型一产生 Token 就发送给前端,用户不用等完整答案生成完再看到内容。

它主要改善感知等待和完整答案的展示过程。

模型开始生成之前的 Query 处理、Embedding、检索、Rerank 和 Prompt 构建仍然存在。如果 TTFT 主要耗在这些前置阶段,改成流式不会让第一个字更早出现。

流式服务还需要处理断线、取消和资源释放。

用户关闭页面后,后端如果仍然生成完整答案,会继续占用模型和网络资源。超时和取消信号应该沿链路传播,让尚未开始或已经无意义的任务尽快停止。

前端也要区分“正在检索”和“正在生成”。只放一个旋转图标,用户不知道系统卡在何处;阶段状态既有助于体验,也有助于排查。

Trace 里应该记录哪些信息?

只记录每段耗时仍然不够,因为同样的慢可能由不同输入触发。

请求入口要记录 Trace ID、租户、知识库范围、请求时间和是否为多轮问题。敏感 Query 可以做受控存储或脱敏,但不能完全失去问题类型信息。

Query 处理阶段记录是否改写、改写耗时和回退状态。Embedding 阶段记录模型版本、输入长度、缓存是否命中和上游排队时间。

检索阶段记录使用的索引、过滤范围、两路候选数量、搜索参数、各自耗时和是否超时。融合与 Rerank 记录输入候选数、输出候选数、模型版本和批次等待。

Prompt 构建阶段记录最终证据数量和上下文长度。模型阶段记录请求进入队列、开始推理、第一个 Token 和结束时间,才能区分服务排队、预填充和持续生成。

客户端阶段还要记录连接建立、首个数据包和用户取消。如果服务端已经产生 Token,前端很晚才显示,问题可能在代理缓冲、网络或渲染,而不在模型。

这些字段需要使用统一时钟和统一阶段名。不同服务各写一套日志,却没有共同 Trace ID,最后仍然拼不出时间线。

监控面板可以显示分位数,但排查具体慢请求时必须能下钻到单条 Trace。看到某个阶段 P99 上升以后,继续比较输入长度、候选数量、缓存状态和队列长度,才能形成可执行结论。

出于数据安全边界的考虑,工程上还应给 Trace 设置保留周期和访问权限。它可能包含用户问题、文档标题和模型上下文,不能为了性能排查无限期保存敏感内容。保留时长、可查字段和脱敏方式都应在发布前确定。

分段计时还要避免重复计算和遗漏。

如果外层接口记录了整个检索时间,内层又把 BM25、向量和 Rerank 分别上报,面板不能把四个数字简单相加,否则并行阶段会被重复计算。应该用时间线展示重叠关系,并明确每个阶段是墙钟耗时还是 CPU 时间。

跨服务调用还要记录上游等待和实际执行。请求在队列里等了很久,模型推理本身却很快,只看模型服务函数耗时会把瓶颈藏掉。

一条 Trace 能解释单次请求,指标聚合则负责发现趋势。两者结合,才既能看到整体变慢,也能打开具体慢请求找到原因。

五个 bad case,为什么越优化越慢?

第一种,只看平均值

多数请求很快,少量请求因为冷启动或排队特别慢,平均值仍然好看。

应该按冷启动、缓存命中、缓存未命中和高峰流量分组,同时看 P95、P99 和错误率。

第二种,为了加速 ANN,召回掉了

检索时间下降,正确证据却不再进入候选,最终答案开始出错。

每次调整索引与搜索参数,都要在同一评测集上回归 Recall@K、MRR 和最终答案,不允许只过性能门槛。

第三种,Rerank 候选不断扩大

团队担心漏召回,把候选从一批增加到更大一批,Rerank 耗时持续增长,正确证据覆盖却几乎不变。

应同时观察候选规模、Recall 曲线和重排耗时,找到收益开始变小的位置。

第四种,缓存没有版本和租户

速度看起来非常快,却返回旧答案或其他知识库的结果。

先停用风险缓存,再补齐模型、知识库、文档版本、权限范围和检索配置,不能用性能收益掩盖正确性问题。

第五种,批处理提高吞吐,尾延迟反而上升

请求需要等待同批数据凑齐,低流量或突发流量下,某些请求等待更久。

要同时报告吞吐与单请求延迟,并设置最大等待时间。批处理是资源利用策略,不是无条件的低延迟策略。

怎么验证一次性能优化?

首先建立端到端 Trace,固定知识库、问题集和检索配置,采集每个阶段的耗时、错误、候选和模型版本。

然后准备多种负载:单请求、稳定并发、突发流量、冷启动、缓存命中和缓存未命中。不同场景使用同一套统计口径。

性能指标至少包括各阶段耗时、TTFT、总耗时、P95、P99、吞吐、超时和错误率。

质量指标至少包括 Recall@K、MRR、无答案错误和最终答案的证据一致性。

每次只改变一个主要因素,比如索引参数、候选数量、缓存层或模型版本。记录改动前后的性能和质量,不用一个模糊的“明显变快”替代数据。

RAG 性能优化的联合验收清单

性能、质量、稳定性和数据边界必须一起通过,才算完成一次优化。

线上还要持续监控队列长度、连接池、模型加载、缓存命中和各阶段分位数。测试环境里没有排队,线上慢的原因可能完全不同。

把性能追问分成四类

下面四个问题是用于检查机制理解的教学设计,不对应某次真实面试的逐字记录。

为什么不先换更快的大模型?

先看 Trace。模型排队和首 Token 如果不是最长阶段,换模型不会解决主要等待,还可能改变答案质量和成本。

BM25 和向量检索一定能并行吗?

当两路使用同一个已准备好的 Query 且没有结果依赖时可以并行。如果向量路线依赖额外改写或意图路由,要先完成对应前置步骤。

缓存 Query Embedding 会不会串数据?

向量本身不直接包含检索结果,但缓存仍要区分模型版本和必要边界。检索结果与答案缓存必须额外包含租户、知识库、版本和配置。

流式输出是不是能降低 TTFT?

它能避免等待完整答案,但无法跳过检索前置链路。只有模型已经开始生成却被服务端缓冲时,正确流式传输才会直接改善首字。

只看 P99 会不会过度优化极端请求?

需要结合请求量、错误类型和业务影响。重点是知道慢请求属于冷启动、超时重试、特定文档还是资源饱和,再决定是否值得优化,而不是盲目追一个分位数。

现场回答:先定位最长阶段,再做联合验收

这道题最适合用“先定位,再优化,最后联合验收”三步作答:

RAG 首字响应慢时,我会先用 Trace 拆出网关排队、Query 处理、Embedding、BM25 和向量召回、融合、Rerank、上下文构建、模型排队与首 Token 的耗时,并区分冷启动、缓存命中和高峰请求。TTFT 和完整答案耗时分开统计,同时看 P95、P99 与错误率。

定位后再按阶段优化。文档向量放在离线阶段,重复 Query 可以做带模型版本的 Embedding 缓存;BM25 与向量召回在没有依赖时并行;向量索引和过滤缩小搜索范围;Rerank 用有限候选并批量处理;生成端用流式返回并支持取消。检索结果和答案缓存还必须包含租户、知识库和文档版本,更新后及时失效。

每次加速都会和质量一起验收。改变索引参数、候选数量或模型后,用同一评测集回归 Recall@K、MRR、无答案控制和答案证据,同时做并发、突发、冷启动和缓存分组压测。这样才能证明是缩短了等待,而不是把正确性和数据边界换掉了。

面试官最终会沿三件事判断这次性能优化是否成立:有没有找到真正的最长阶段,能不能解释每项优化的代价,以及速度改善后,召回、权限和答案质量是否仍然守住。列出多少性能组件并不重要,定位与验证证据才重要。

INTERVIEW FOLLOW-UPS

面试官会怎样继续追问?

这些追问会继续检查方案取舍、验证方法和项目事实。
  1. Q01

    TTFT 和完整回答耗时有什么区别?

  2. Q02

    哪些步骤可以并行,哪些存在严格依赖?

  3. Q03

    Query 缓存怎样处理知识库更新与多租户隔离?

  4. Q04

    ANN 参数调快后,怎样确认召回没有明显下降?

PROJECT CHECK

换成你的项目,这几件事能否说清楚?

  • 记录首字前每个阶段的耗时
  • 区分冷启动、缓存命中与正常请求
  • 同时记录性能与召回质量
  • 验证缓存键包含版本和租户边界
用自己的项目经历练一次