ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG又慢又飘?从检索到生成的全链路性能调优指南

RAG又慢又飘?从检索到生成的全链路性能调优指南

很多团队第一次把 RAG 做出来用时,都会经历一段“能用,但很难用”的尴尬期。接口响应慢吞吞,答案时好时坏,问深一点就开始胡编。如果最近你也在折腾检索增强生成系统,被延迟高、回答飘这两个问题搞得头疼,这篇文章应该能帮你少走不少弯路。我会从一次真实的性能调优过程出发,把 RAG 从检索到生成的完整链路拆开来讲,细说每一步为什么会慢、为什么会答不准,以及我实际验证过有效的优化手段。

这个内容适合刚把 RAG 跑通、正准备做性能优化的开发者,也适合已经上线但被吐槽体验太差的团队参考。我不会堆概念,所有方案都会给出参数、步骤和避坑经验,你可以直接照着动手调。

1. 先说清楚:RAG 的延迟和飘忽来自哪里

1.1 一次请求背后到底发生了什么

很多人的第一反应是“换个更快的模型”。但真实情况里,大模型推理往往只占整个响应时间的一部分。一次完整的 RAG 链路大致是:收到用户问题 → 处理问题文本 → 向量化编码 → 到向量库做相似度检索 → 拼接检索结果与原始问题 → 发给大模型生成回答 → 流式返回。每一步都有延迟,而且像网络传输、文本预处理这些容易被忽略的环节,在极端情况下也会吃掉几百毫秒。

回答飘就更复杂了。它往往是三个因素叠加的结果:检索到的内容本身不对,检索内容对但被截断或切碎,模型被无关片段干扰。很多人只盯着大模型换参数,实际上答案不靠谱时,80% 的时间都应该去查检索环节。

我建议先把自己系统的全链路耗时打点打出来,让数据说话。不要凭感觉判断慢在哪里。我曾经接手的某内部文档问答项目,表面看是整体响应 6 秒太慢,打点之后发现检索阶段占了 2.8 秒,文本后处理加拼接占了 1.2 秒,真正送给模型的只有 1.8 秒的推理时间。如果当时盲目换更大的模型,方向就完全走错了。

1.2 延迟高的三个大头

第一是向量检索延迟。向量库的索引类型、数据量大小、查询并发数直接决定这一步耗时。数据量十万级和千万级,量级不同,需要选择的索引完全不一样。第二是 Embedding 环节。不管是用开源模型还是调用在线接口,把一段文本转成向量都要花时间,尤其当输入文本很长、或者单次请求需要批量编码时,这个耗时会被放大。第三是 LLM 推理本身的延迟。生成 token 是逐个出的,输出越长耗时越线性增长。

除此之外,还有两个容易被忽略的隐藏延迟源:一个是网络往返,尤其是调用外部模型接口时,请求体较大、连接未复用都会增加额外延迟;另一个是向量库连接池配置不对,高并发时线程排队,导致检索耗时倍增。

1.3 回答飘的核心原因

回答飘,我自己的排查经验是优先看召回。召回不准通常有几种来源:一是嵌入模型能力不足,语义相近但表述不同的内容召回效果差;二是 TopK 设置不合理,要么召回太少导致信息不够,要么召回太多把噪声也带进去了;三是相似度阈值没有控制,低质量片段也被拼进上下文;四是文本被切得太碎,把一个完整知识点横切成了好几段,导致每段信息都不完整。

另外还要看提示词的结构。如果提示词里只说“根据资料回答”,没有明确“如果资料中没有答案,请直接说不知道”,模型就会倾向于强行编造。把这块规则写清楚,回答飘的问题能改善不少。

2. 检索侧调优:把速度和回头率一起抓

2.1 向量检索参数怎么调才合理

TopK 是我建议第一个动手调的参数。很多人习惯设一个固定值就不动了,但实际项目里,不同场景对 TopK 的需求差异很大。我做过的某电商客服知识库,FAQ 型问答 TopK 取 3 效果就很好;但换到文档分析场景,同一个问题往往横跨多个章节,TopK 设成 8 甚至 10,召回才够完整。

这里有一个实用技巧:不要把 TopK 当成一个参数,而是配合相似度分数阈值一起看。先取一个较大的 TopK(比如 20),然后根据分数分布动态截断。例如只保留相似度超过 0.72 的片段,如果不足 3 段就再放宽阈值。这种动态策略比固定值靠谱,能显著减少噪声片段进入上下文。

向量库索引类型的选择也很关键。数据量小于 50 万时,暴力检索(Flat)性能足够,毫秒级响应且召回最准,没必要上复杂索引。数据量上百万后,再考虑 HNSW 这类 ANN 索引。HNSW 有两个关键参数需要调:M(控制每个节点的最大连接数)和 efSearch(控制查询时的探索范围)。简单来说,M 越大召回越准但内存占用越高,efSearch 越大查询越慢但准确率越高。我自己的经验是 M 取 16 到 32,efSearch 先设 64,如果延迟达标但召回不足,再往上加到 128、256,一个一个试。

2.2 混合检索是“回答飘”的一剂猛药

向量检索擅长语义匹配,但对精确关键词非常不敏感。比如用户搜“RPA 机器人流程自动化”,向量可能把它理解成“机器人技术”,召回结果完全跑偏。解决这个问题的方法就是混合检索:把 BM25 稀疏检索和向量稠密检索的结果做融合。

我在某内部知识库项目中实测过,只做向量检索时,命中率大概在 71%;加上 BM25 后,命中率提升到 84% 左右。融合方式我推荐 RRF(Reciprocal Rank Fusion),原理很简单:对多路检索结果,按排名取倒数分再求和,最后按总分排序。它不需要调权重,鲁棒性比加权求和好很多。

需要留意的是,BM25 对中文场景要做好的分词。直接用空格分词效果很差,建议接入 jieba 或按字粒度索引,否则关键词检索的收益会大打折扣。某些向量库虽然内置了 BM25,但分词器对中文支持一般,采样测试几轮就能看出差距。

2.3 分块策略直接影响检索质量

这块是很多人容易忽略的重灾区。分块太大,单段包含多个主题,向量化后语义不够聚焦,检索命中率下降;分块太小,语义被切碎,信息不完整,就算检索到了,给到模型也没办法生成完整答案。

我的经验是分块策略和文本本身的结构强相关。具体来说:对 Markdown 或标题层级文本,优先按标题语义分段,而不是机械按字符数切。先按章节切开,再对过长的章节按段落二次切分,最后如果单段还是太长,设定一个最大长度做重叠切割,重叠量控制在块长度的 10% 到 20%。

重叠切割有一个反直觉的地方:重叠太多会导致检索结果冗余,拼接后上下文里大量相同信息,反而挤压了其他有效信息的位置。我曾经把重叠设成 30%,结果回答质量反而下降。后来改为按句子边界加少量重叠,效果比较理想。

3. 生成侧调优:让大模型把答案“答稳”

3.1 提示词模板里的约束条件

同样的检索结果,给到大模型后能不能生成靠谱回答,提示词的约束强度差别很大。我见过太多提示词只写“根据以下资料回答问题”,其余全靠模型自由发挥。这种做法一旦检索结果里混入无关内容,模型很容易被带偏。

我实践下来比较稳定的模板包含这几个部分:角色定义、任务说明、引用资料区、约束条件、输出格式。其中约束条件要明确写:只能基于资料内容回答、资料不足时直接回答“未找到相关信息”、不要推断或编造。这些约束放在提示词后部、靠近资料区的位置,模型执行得更好。

另外,如果检索到的片段有来源和标题信息,拼进提示词时保留它们,能让模型在处理多段内容时更好地区分信息来源。一个小细节是片段之间用明确的 XML 标签或 Markdown 引用块分隔,比连续堆文本的效果好得多。

3.2 生成参数怎么配合调整

Temperature、TopP 这类参数不是只影响创造性,它们直接影响回答的“飘”的程度。对知识问答类场景,Temperature 我建议压在 0.1 到 0.3 之间,TopP 控制在 0.8 左右。需要明确的是,Temperature 调低只是减小随机采样概率,并不能彻底消除幻觉。真正要消除幻觉,还得靠检索质量和提示词约束。

max_tokens 也要合理设置。设得太短,答案容易被截断;设得太长,用户等得越久。比较好的做法是结合问题类型动态判断,但实现复杂度高。折中方案是设一个偏大的值,配合流式输出,用户感知不到生成时间和完整生成的差别。

还有一个容易被忽略的参数是频率惩罚和存在惩罚。当检索片段较多、上下文较长时,适当调高存在惩罚可以减少模型重复说过的内容,但调过头会导致回答生硬、信息密度降低。我的建议是这两个参数默认不动,只有在输出明显重复时才微调。

3.3 Rerank 重排序:用一点延迟换大幅准确率

很多人没有在 RAG 链路里加重排序,其实这一步对回答飘的改善非常明显。向量召回的时候,头部结果可能还行,但第 5 到第 20 位的结果相关性会明显变差。如果直接把 TopK=8 的结果全送给模型,很有可能一半都是噪声。Rerank 模型会把召回的候选重新打一遍分,把最相关的排到最前面。

我实测的某项目,加入 Rerank 后,精确匹配率提升明显,代价是每次查询增加了 50 到 120 毫秒的延迟。这个延迟换来的准确率提升非常值得。需要注意的是,Rerank 的输入长度有限制,不要直接把超长文本整个丢进去。配合前面“先取较大 TopK,再 Rerank 后取前 N”的策略,效果最好。

选 Rerank 模型时,优先考虑专用模型而非通用向量模型。因为 Rerank 任务和语义匹配任务不完全一样,专用模型对相关性的判断更准确。如果条件有限,至少也要选择在配对任务上训练过的模型,不要直接拿向量模型充当 Rerank。

4. 工程链路优化:从 6 秒压到 2 秒的真实手段

4.1 缓存带来的收益超出想象

RAG 场景中,用户问题往往有大量重复或近似。同一个问题反复查库、反复调用模型,纯属浪费。语义缓存可以把常见问题直接截住,大幅降低平均响应延迟。

我做的某知识问答系统,上线语义缓存后,整体响应时间平均值从 5.8 秒降到了 3.2 秒。缓存命中时直接返回生成好的答案,响应时间能压到 300 毫秒以内。实现思路是:对用户问题做向量化,在缓存中做相似度检索,如果相似度超过预设阈值(比如 0.92),就直接返回缓存中的答案。

这里有一个细节要提醒:缓存必须有失效策略,否则资料库更新后,旧答案还会继续被重复返回。我在项目里是结合文档版本的变更来清缓存,简单地说,每当知识库文档重新导入或更新,就对受影响的缓存做失效处理。还有一种做法是给缓存结果打上文档版本号,查询时对比版本号,版本不一致就直接穿透到完整链路。

4.2 连接池与并发配置:看不见的延迟杀手

很多团队在压测时发现,单请求延迟已经优化得不错,但并发一上来就崩。问题往往出在连接池配置上。向量库客户端、模型服务 SDK 默认连接池非常保守,不调配置就等于把并发请求排队处理。

以向量库客户端为例,默认连接数可能只有 10 到 20,当查询并发到达 50 时,大部分请求都在等待连接释放。我把连接池最大连接数调到和线上服务实例副本数匹配,同时设置合理的超时时间,单查询耗时和 P95 都大幅改善。模型推理服务也是一样,如果用的是自建的推理网关,务必检查它的最大并发数和队列长度,不要让请求在队列里空等。

另一个细节是连接复用。调用外部模型 API 时,如果不开启 HTTP Keep-Alive,每次请求都要重新建立 TCP 连接,TLS 握手开销会占掉不少时间。开启连接复用之后,延迟能降低 10% 到 15%。这个收益在长文本、高并发场景特别明显。

4.3 模型量化和服务部署层面的取舍

模型推理是 RAG 链路里耗时的大头之一。把 LLM 做量化是最直接的加速手段。从实际效果看,4-bit 量化一般能带来 30% 到 50% 的推理加速,模型精度损失在可接受范围,对知识问答场景影响很小。如果你用的是 7B 到 13B 级别的模型,量化前后的差异用户基本感知不到。

但量化也有代价。显存占用降低后,你可以把更大的模型塞进同一张卡,但吞吐量不一定线性提升。这里建议做一次完整的压测,对比量化前后在不同并发下的吞吐表现。我自己的经验是,量化后单卡吞吐确实提升明显,但并发到一定程度后,瓶颈会转移到显存带宽上,继续提并发的收益开始递减。

服务部署层面,一个容易被忽视的点是动态批处理。推理服务端开启动态批处理,可以把多个请求合并成一个大 batch,提升 GPU 利用率。这需要推理框架支持,并配合合理的等待时间设置。等待时间设 20 到 50 毫秒比较合适,太长会牺牲单请求延迟,太短又来不及攒批。这个参数需要根据实际流量节奏反复试。

5. 一个完整调优案例的复盘与避坑记录

5.1 项目初始状态与无法忍受的问题

这里分享一个我用上述思路完整调优的模拟项目。某团队做了一个内部文档智能问答系统,知识库大概 30 万段文档切片,部署在单台 GPU 服务器上,模型是 7B 量化版本。上线后的表现是:平均响应时间 8 秒,遇到长文档问题甚至会到 15 秒;答案质量也不稳定,用户经常反馈答非所问,有些问题来回问两次给出矛盾答案。

这个项目最让人抓狂的不是慢,而是无法定位瓶颈。于是第一步就是做链路打点,把从请求进入网关到响应返回的每一步耗时都记录下来。打点结果非常直观:文本预处理和 Embedding 耗时占 25%,向量检索占 35%,Rerank 占 5%,LLM 推理占 30%,其他开销占 5%。检索相关的时间加起来超过了一半。

5.2 逐步优化的完整过程

第一步优化的是向量检索。原来用的索引参数是无脑默认,efSearch 只有 40,数据量上来后召回质量明显下降。我把 efSearch 调到 128,同时把 TopK 从 5 调整到动态策略:先召回 20 条,再用 Rerank 精排取前 5。这一步之后,回答飘的问题明显好转,但查询延迟涨了一些,还在可接受范围。

第二步是引入 BM25 混合检索。原来只做向量检索,用户输入含专业缩写或精确型号时经常召回失败。接入 BM25 并用 RRF 做融合后,召回率明显提升。这一步查询耗时增加了大约 30 毫秒,但答案准确率的提升非常可观。同时重新设计了分块策略,从原来固定 300 字切块改为按标题语义切分+重叠切片,进一步改善了长文档的召回连贯性。

第三步是处理 Embedding 瓶颈。原来每次请求都现算用户问题的向量,虽然单次只有几十毫秒,但并发一高就很伤。我先缓存了知识库侧的所有向量,用户侧的查询向量做了语义缓存。结果就是高频重复问题的部分请求可以直接命中缓存,不需要走完整链路。这一步让平均响应延迟又降了一截。

第四步才是优化模型推理。把生成参数统一调整为低 Temperature,配合 Prompt 强调“资料不足时直接拒绝回答”,明显降低了幻觉率。然后把模型从通用对话向调优为更偏知识问答的方向,同参数量下回答稳定性提升了一个台阶。

5.3 优化效果与关键数字变化

优化完成后的结果:平均响应时间从 8 秒降到 2.1 秒,P95 从 12 秒降到 3.8 秒。回答质量方面,人工评测的相关性准确率从 62% 提升到 84%,幻觉比例从明显偏高降到可接受区间。

这里有一个很有参考价值的对比:单看 LLM 推理耗时,我们其实只优化了大约 20% 的量。更多的耗时节省来自缓存命中、检索优化、并发配置这些外围环节。这说明 RAG 性能调优绝对不能只盯着模型本身,整个链路上的每个环节都值得打磨。

5.4 常见问题速查表

现象可能原因排查方向与解决建议
TopK 增大后延迟飙升向量索引 efSearch 过低或未用 ANN 索引调整 efSearch,检查索引类型是否适应当前数据量
答案经常带着无关内容相似度阈值过低或 Rerank 缺失调高阈值,加入 Rerank 并设置合理截断
同一问题过段时间答案变差资料更新后缓存未失效给缓存打版本号,文档更新后主动失效对应缓存
并发一高就大量超时连接池配置过小或 HTTP 未复用调整连接池上限,开启 Keep-Alive 和动态批处理
专业术语、型号检索不到只有向量检索、分词不合适引入 BM25 混合检索,优化中文分词
回答空洞但检索看起来有结果分块过大导致语义不聚焦按标题语义切分,控制单块长度,加少量重叠
输出总是重复几句话频率惩罚或存在惩罚未设置适当调高存在惩罚,观察输出变化

6. 调优过程中的几条实用经验与心得

最后再分享几个我在多次调优中沉淀下来的经验。

第一,链路打点必须放在第一位。没有完整耗时分布之前,任何盲目优化都可能白费功夫。建议从最开始就把每一步的耗时、输入输出大小、调用成功失败状态记录清楚。而且要量化到每一个环节,包括文本预处理、Embedding 调用、向量库查询、Rerank、Prompt 拼接、LLM 流式首个 token 延迟和总生成耗时。有了这些数据,你才知道该往哪个方向使劲。

第二,不要一上来就追求最新的模型或最贵的方案。很多性能问题通过调整现有组件的配置就能解决大半。先把手上的每个环节的参数吃透,再考虑上重武器。比如 efSearch、TopK、连接池、缓存这几个点,调好之后往往能有质的飞跃。

第三,任何优化都要以线上真实流量来验证。离线测试时我用的是理想环境,数据分布和线上差异很大。只有在灰度环境压测并对比真实用户反馈之后,才能确定优化没有引入新的问题。

第四,流式输出是一种性价比极高的体验优化手段。即使做完所有优化后总耗时有 3 秒,只要用户第一 token 在 500 毫秒内出现,主观体验就完全不像 3 秒。这个技巧在很多场景下比硬调性能更有效果,而且实现成本很低。

RAG 性能调优不是一锤子买卖,它是一个不断测量、调整、验证的循环过程。希望这篇内容能帮你在自己的系统上把延迟降下来、答案质量提上去,少踩几个我踩过的坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进