面试官皱眉:"你的RAG用户要等8秒才看到第一个字?你们做过流式输出吗?"

前段时间有个朋友面腾讯算法岗,简历上写着"RAG系统平均响应时间优化至3秒以内"。

面试官看了一眼,问:

"3秒是端到端延迟还是首字延迟?"

他愣了一下:"端到端......用户拿到完整答案需要3秒。"

面试官接着问:

"那用户发出问题到看到第一个字,要等多久?你们有没有做流式输出?流式RAG和普通流式输出有什么不同,RAG场景里有哪些特有的延迟问题需要处理?检索阶段能不能流式化,还是只有生成阶段能流式?"

四个问题,他只能回答第一个------因为他的系统只做了生成阶段的流式输出,完全没有考虑检索阶段的延迟优化

面试官说了一句:"端到端3秒,但如果前2秒都在等检索,用户体验还是很差。流式输出解决的是感知延迟,不是实际延迟。"

这个场景说明一个问题:很多人做了流式输出,但没有系统性地思考过RAG场景的延迟来源和优化策略。

今天把流式RAG从头拆透。


先搞清楚两个延迟概念

做延迟优化之前,必须区分两个概念:

TTFT(Time To First Token):首字延迟

用户发出请求到看到第一个输出字符的时间。

这是用户感知最直接的指标------即使答案要10秒才生成完,只要第一个字在1秒内出现,用户的等待焦虑就会大幅降低。

端到端延迟(E2E Latency):完整响应时间

从请求发出到完整答案返回的总时间。

这是系统实际消耗的时间,决定了吞吐量和成本。

RAG场景的TTFT为什么高:

普通LLM调用:请求发出 → LLM开始生成 → 流式输出第一个token。TTFT就是LLM的推理启动时间,通常500ms~1秒。

RAG调用:请求发出 → 查询改写(可能调LLM)→ 向量检索 → Rerank → 拼装prompt → LLM开始生成 → 流式输出第一个token。在LLM开始生成之前,已经有25秒的检索链路耗时,TTFT被拉高到36秒。

用户等了3秒才看到第一个字,即使后续流式输出很流畅,体验也很差。


流式RAG的核心思路

流式RAG的目标是:尽可能早地让用户看到第一个字,同时不降低答案质量。

有两个方向可以做:

方向一:优化检索链路,缩短TTFT前的等待时间

让检索更快,LLM更早开始生成。

方向二:流式化中间过程,让用户在等待时有反馈

检索阶段无法避免,但可以把"正在检索..."这样的进度信息实时推送给用户,降低等待焦虑。

两个方向要同时做,缺一不可。


坑一:查询改写,能不能异步化

标准RAG链路里,查询改写(Query Rewriting)是第一步------把用户的问题改写成更适合检索的形式。

这一步通常需要调用LLM,耗时500ms~1秒。

问题:查询改写必须在检索之前同步完成吗?

不一定。有两种优化思路:

思路一:判断是否需要改写,不需要就跳过。

如果用户的问题是简单的单跳问题、没有指代词、语义清晰,直接用原始问题去检索,跳过改写步骤。

可以用规则快速判断(包含"它"、"这个"、"那个"等指代词才触发改写,否则直接检索)。

这样70%80%的查询可以跳过改写,直接进入检索,TTFT降低500ms1秒。

思路二:改写和检索并行。

同时发起两路:用改写后的问题检索 + 用原始问题检索,两路并行,取结果更好的那路。

改写完成之前,原始问题的检索可能已经有了结果,如果结果质量足够好,就直接用;如果改写后的结果更好,再替换。

这样TTFT不受改写延迟拖累,改写成为"可选的质量提升"而不是"必须等待的步骤"。

思路二的实际问题:怎么判断"结果更好"?

两路并行返回后,需要一个质量判断逻辑决定用哪路。通常用Top1的相似度分数做比较,改写后的Top1分数比原始高出一定阈值(比如0.05)才替换,否则直接用原始结果。

阈值怎么定?没有通用值,需要在自己的数据集上标注一批"改写有效/无效"的样本,观察分数差异的分布来确定。在我们的项目里,最终定在0.03------太小了改写几乎总会替换(失去节省延迟的意义),太大了改写的收益又体现不出来。


坑二:检索阶段的并行化

标准RAG的检索链路通常是串行的:

向量检索 → BM25检索RRF融合 → Rerank → 送给LLM

哪些步骤可以并行:

向量检索和BM25检索没有依赖关系,完全可以并行发起,等两路都返回结果后再做RRF融合。

如果有多个向量库分片(大知识库常见),分片查询可以并行。

并行化之后,RRF融合的等待策略怎么定?

这里有个实际的权衡:等两路都完成再融合(安全,但慢的那路拖住整体),还是设一个超时时间,超时的那路直接丢弃?

生产里通常选后者:设置一个软超时(比如800ms),在这个时间内没返回的检索路直接放弃,用已有结果做融合。这样最坏情况下检索耗时有上界,不会因为某一路抖动把整体TTFT拖垮。

软超时之外还需要关注并发压力------两路并行意味着向量库和ES同时收到请求,高峰期并发量翻倍。如果下游没有做好限流,并行化反而可能触发降级,耗时比串行更长。我们在向量库侧加了信号量控制,单个请求最多并行发起3路检索,超出的排队等待。

Rerank能不能并行化:

Rerank是对召回结果的精排,必须等向量检索和BM25都完成后才能开始,无法并行化。

但Rerank的截断位置可以动态调整------如果检索结果质量已经很高(Top1分数超过阈值),可以跳过Rerank直接用检索结果,节省Rerank的耗时(通常200~500ms)。

这里同样存在阈值标定问题。向量相似度的绝对值在不同embedding模型下含义完全不同------同样是0.85,ada-002和bge-large给出的语义距离差异很大。跳过Rerank的阈值必须在自己的embedding模型和知识库上标定,不能直接用别人项目里的数字。标定方法:在测试集上对比"跳过Rerank"和"走完Rerank"的答案质量,找到一个不影响质量的最低分数线。

实测数据:

在我们的保险知识库项目里,把向量检索和BM25并行化之后,检索阶段耗时从800ms降到了450ms,降幅约44%。加上软超时保护和并发限流之后,P99延迟从原来的2.1秒降到了1.3秒,长尾抖动明显收敛。


坑三:生成阶段的流式输出,RAG特有的问题

生成阶段的流式输出看起来简单------LLM支持流式API,直接用就行。

但RAG场景里有一个特有的问题:引用来源标注

标准RAG里,答案生成完成后,系统会在答案末尾标注引用来源("来源:A款产品手册-第3页")。

如果走流式输出,LLM一边生成一边输出token,生成过程中还不知道引用来源标注应该放在哪里、应该标注哪些来源。

解决方案一:来源标注在流式输出结束后追加。

LLM流式生成答案正文,生成结束后,系统统一追加引用来源。

实现简单,但用户要等答案完整生成后才能看到来源,不够优雅。

解决方案二:在prompt里要求LLM在生成时内联标注。

css 复制代码
请在回答中,每当引用参考资料里的信息时,在该句话后面立即用[来源N]标注,
例如:A款重疾险等待期为90天[来源1],免赔额为5000元[来源2]。

LLM在生成时就内联标注,流式输出时来源随着答案文字一起出现。

实现稍复杂(需要在prompt里维护来源编号和chunk的对应关系),但用户体验更好。

方案二的生产问题:LLM的来源标注不可信

这里有个坑在文章里没提,但在生产里必须处理:LLM有时会幻造来源编号。你传入了3个chunk,它可能写出[来源4][来源5],或者把格式写成[来源 1][Source1](来源1),五花八门。

流式输出的后处理需要专门处理这个问题:

  1. 合法性验证 :在流式输出结束后,用正则把所有[来源N]提取出来,过滤掉N超出chunk数量的幻造引用;
  2. 格式归一化 :用正则把各种变体格式统一成标准格式,再渲染给用户;
  3. 兜底方案:如果检测到来源标注格式混乱(比如同一段里出现多种格式),降级为方案一,在末尾统一追加来源列表。

这部分逻辑写起来不复杂,但不写的话上线必翻车------QA测出来的问题基本就是这些格式问题。


坑四:流式进度反馈,检索阶段的用户体验

检索阶段无法真正流式化(必须等检索完成才能开始生成),但可以把进度信息实时推送给用户。

进度反馈的实现方式:SSE(Server-Sent Events)

服务端通过SSE长连接,实时向客户端推送进度事件:

vbnet 复制代码
event: progress
data: {"stage": "query_rewrite", "message": "正在理解您的问题...", "progress": 10}
​
event: progress  
data: {"stage": "retrieval", "message": "正在检索相关资料...", "progress": 40}
​
event: progress
data: {"stage": "rerank", "message": "正在筛选最相关内容...", "progress": 70}
​
event: token
data: {"token": "根", "cumulative": "根"}
​
event: token
data: {"token": "据", "cumulative": "根据"}

用户看到的效果:

css 复制代码
[正在理解您的问题... 10%]
[正在检索相关资料... 40%]  
[正在筛选最相关内容... 70%]
根据A款重疾险产品手册,等待期为...(流式输出)

心理学研究表明,有进度反馈的等待,用户可接受的时间是无进度反馈的2~3倍。即使检索还是需要3秒,用户看到进度条在走,等待感受完全不同。

SSE在生产里有几个容易踩的坑:

坑1:Nginx默认缓冲会让SSE失效。

Nginx默认会缓冲上游响应,等缓冲区满了才转发给客户端。SSE需要的是逐事件实时推送,被缓冲之后就变成了批量发送,进度条效果完全消失。

解决方法是在Nginx配置里关掉对应路由的缓冲:

ini 复制代码
location /api/sse {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header X-Accel-Buffering no;
}

漏了这个配置,在本地测试没问题,一上线就发现进度条不动了------因为本地不经过Nginx。

坑2:客户端断线重连时的事件去重。

SSE协议内置了断线重连机制,客户端断线后会自动重连并携带Last-Event-ID头,告诉服务端从哪个事件开始重发。

如果服务端不处理这个头,重连后会重新从头推送,用户看到进度条归零重来,或者收到重复的token。

处理方式是给每个事件加id字段,服务端记录已发送的最大event_id,重连时从该id之后的事件开始续发:

vbnet 复制代码
id: 42
event: token
data: {"token": "据"}

坑3:负载均衡下的连接粘连。

SSE是长连接,同一个会话的所有事件必须走同一个后端实例(因为事件序列状态在内存里)。

如果负载均衡按请求轮询,断线重连后可能路由到不同实例,找不到原来的会话状态。

解决方案是配置基于会话ID的粘连路由(sticky session),或者把事件序列状态存到Redis,让任意实例都能续发。


坑五:流式输出的错误处理

流式输出开始后,如果中途出错(网络中断、LLM超时、检索结果为空),处理比普通请求复杂。

错误场景一:检索阶段出错。

还没开始流式输出,处理和普通错误一样,返回错误信息给用户。

错误场景二:生成阶段中途出错。

流式输出已经开始,用户看到了部分答案,这时LLM报错或超时。

不能直接返回错误,因为用户已经看到了部分内容,突然中断体验很差。

处理方式:在已输出内容后追加"回答生成中断,请重试",同时在前端提供"继续生成"按钮触发重试。

需要注意的是,"从中断点续写"在生产里几乎不可行------LLM生成是非确定性的,重试大概率走不回原来的路径,强行拼接会产生语义断裂。实际做法是从头重新生成,但把已输出的部分内容作为上下文传给LLM,让它尽量保持一致性,而不是真正的断点续传。

错误场景三:流式输出内容被检测到有问题。

内容安全检测通常是对完整答案做的,流式输出时答案还在生成中,如何处理?

方案一:先缓冲完整答案做安全检测,通过后再流式发送(损失了流式的TTFT优势)。

方案二:用轻量级实时内容过滤(关键词过滤),生成过程中实时检测,触发时立即中断并追加提示。

两个方案本质是安全和体验的权衡,没有通用答案。风险敏感型业务(金融、医疗)通常选方案一,宁可损失一点TTFT,也不能让问题内容在检测完成前就到达用户;内容风险较低的场景可以选方案二,用轻量过滤兜底,全量安全检测异步进行,发现问题后在后续会话里介入。


端到端延迟优化的优先级排序

结合前面讲的所有优化点,按收益/成本比排序:

第一优先:生成阶段流式输出

实现成本低,TTFT收益极大,几乎所有系统都应该做。

第二优先:检索并行化(向量+BM25并行)

实现成本中等,检索阶段耗时降低40%+,收益明显。注意同步加软超时保护和并发限流,否则高峰期可能适得其反。

第三优先:流式进度反馈

实现成本中等,不改变实际延迟,但大幅改善用户体验感知,值得做。记得处理Nginx缓冲、断线重连、负载均衡这三个坑,否则测试环境没问题、生产环境翻车。

第四优先:查询改写条件触发

实现成本低,对简单查询节省500ms~1秒,对复杂查询无影响。

第五优先:动态跳过Rerank

实现成本低,对高质量检索结果场景节省200~500ms,但阈值必须在自己的数据集上标定,不能套用别人的数字。


面试被问到,这样答

面试官问"流式输出是怎么做的"或者"怎么优化用户的等待体验",这样展开:

先区分TTFT和端到端延迟。说清楚两者的区别,RAG场景里TTFT高的原因是检索链路在LLM生成之前,体现你对延迟问题有系统认知。

再说检索阶段的优化。向量检索和BM25并行化,并行的等待策略(软超时+并发限流),查询改写条件触发,动态跳过Rerank,说具体的收益数据。

然后说生成阶段的流式化。RAG场景特有的问题------引用来源标注怎么处理,两种方案各自的优缺点,以及方案二的生产坑(来源幻造、格式不稳定的后处理)。

再说进度反馈。SSE的实现方式,重点说Nginx缓冲、断线重连、负载均衡三个生产细节,这些说出来加分明显。

最后说错误处理。流式断点续传为什么在生产里行不通,内容安全检测的两种方案和各自适用场景,体现你考虑过真实的生产约束。


最后说一句

流式RAG这块,很多人以为"加个stream=True参数"就算做了流式输出。

实际上,RAG场景的延迟优化是一个系统性问题------检索并行化、查询改写条件化、进度反馈、错误处理,每一个环节都有优化空间,也都有自己的坑。

但更重要的是,把这些说出来的时候,要能说清楚每个决策背后的权衡:软超时的阈值为什么这样定,跳过Rerank的分数线怎么标定,SSE为什么要关Nginx缓冲。说清楚权衡,面试官才知道你真的做过,而不是把文章里的结论背了一遍。

相关推荐
weixin_727535621 小时前
AI 应用开发学习指南:从零到生产的分阶段实践路线
人工智能
手写码匠1 小时前
华为云征文|DeepSeek-R1 智能问数 Agent 实战:Flexus X 实例 + Dify 构建企业级 Text-to-SQL 查询助手
人工智能·深度学习·算法·aigc
梅孔立1 小时前
推荐一个 Python 开源项目:AI 模板填充 + Markdown 转 Word,面向 Aspose 模板引擎的效率神器
人工智能·python·开源
CodeLinghu1 小时前
LangSmith Evaluate实战评估Agent
人工智能·python·语言模型·llm
weixin_468466852 小时前
DeepSeek-V4-Flash正式版深度解析:当“轻量模型”用后训练撬动Agent能力革命
人工智能·大模型·agent·deepseek·deepseek-v4
donoot2 小时前
《大话文渊慧典》:六
人工智能·深度学习·aigc·ppstructure·文渊慧典·大话系列
阿部多瑞 ABU2 小时前
正反馈的死亡螺旋:数字资本时代泛二次元文化-情感-金融复合体的系统性分析
大数据·人工智能·金融
过期的秋刀鱼!2 小时前
学习曲线-过拟合和欠拟合要做什么以及原因
人工智能·python·深度学习·算法·机器学习·模型评估
haerapi2 小时前
别把页面塞进三个枚举:用“数据、过程、消息”重建 UI 状态
人工智能