尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

面试官皱眉:“你的RAG用户要等8秒才看到第一个字?你们做过流式输出吗?“ 前段时间有个朋友面腾讯算法岗简历上写着RAG系统平均响应时间优化至3秒以内。面试官看了一眼问3秒是端到端延迟还是首字延迟他愣了一下端到端……用户拿到完整答案需要3秒。面试官接着问那用户发出问题到看到第一个字要等多久你们有没有做流式输出流式RAG和普通流式输出有什么不同RAG场景里有哪些特有的延迟问题需要处理检索阶段能不能流式化还是只有生成阶段能流式四个问题他只能回答第一个——因为他的系统只做了生成阶段的流式输出完全没有考虑检索阶段的延迟优化。面试官说了一句端到端3秒但如果前2秒都在等检索用户体验还是很差。流式输出解决的是感知延迟不是实际延迟。这个场景说明一个问题很多人做了流式输出但没有系统性地思考过RAG场景的延迟来源和优化策略。今天把流式RAG从头拆透。先搞清楚两个延迟概念做延迟优化之前必须区分两个概念TTFTTime To First Token首字延迟用户发出请求到看到第一个输出字符的时间。这是用户感知最直接的指标——即使答案要10秒才生成完只要第一个字在1秒内出现用户的等待焦虑就会大幅降低。端到端延迟E2E Latency完整响应时间从请求发出到完整答案返回的总时间。这是系统实际消耗的时间决定了吞吐量和成本。RAG场景的TTFT为什么高普通LLM调用请求发出 → LLM开始生成 → 流式输出第一个token。TTFT就是LLM的推理启动时间通常500ms~1秒。RAG调用请求发出 → 查询改写可能调LLM→ 向量检索 → Rerank → 拼装prompt → LLM开始生成 → 流式输出第一个token。在LLM开始生成之前已经有2~5秒的检索链路耗时TTFT被拉高到3~6秒。用户等了3秒才看到第一个字即使后续流式输出很流畅体验也很差。流式RAG的核心思路流式RAG的目标是尽可能早地让用户看到第一个字同时不降低答案质量。有两个方向可以做方向一优化检索链路缩短TTFT前的等待时间让检索更快LLM更早开始生成。方向二流式化中间过程让用户在等待时有反馈检索阶段无法避免但可以把正在检索…这样的进度信息实时推送给用户降低等待焦虑。两个方向要同时做缺一不可。坑一查询改写能不能异步化标准RAG链路里查询改写Query Rewriting是第一步——把用户的问题改写成更适合检索的形式。这一步通常需要调用LLM耗时500ms~1秒。问题查询改写必须在检索之前同步完成吗不一定。有两种优化思路思路一判断是否需要改写不需要就跳过。如果用户的问题是简单的单跳问题、没有指代词、语义清晰直接用原始问题去检索跳过改写步骤。可以用规则快速判断包含它、这个、那个等指代词才触发改写否则直接检索。这样70%~80%的查询可以跳过改写直接进入检索TTFT降低500ms~1秒。思路二改写和检索并行。同时发起两路用改写后的问题检索 用原始问题检索两路并行取结果更好的那路。改写完成之前原始问题的检索可能已经有了结果如果结果质量足够好就直接用如果改写后的结果更好再替换。这样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.85ada-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在生成时内联标注。请在回答中每当引用参考资料里的信息时在该句话后面立即用[来源N]标注 例如A款重疾险等待期为90天[来源1]免赔额为5000元[来源2]。LLM在生成时就内联标注流式输出时来源随着答案文字一起出现。实现稍复杂需要在prompt里维护来源编号和chunk的对应关系但用户体验更好。方案二的生产问题LLM的来源标注不可信这里有个坑在文章里没提但在生产里必须处理LLM有时会幻造来源编号。你传入了3个chunk它可能写出[来源4]、[来源5]或者把格式写成[来源 1]、[Source1]、(来源1)五花八门。流式输出的后处理需要专门处理这个问题合法性验证在流式输出结束后用正则把所有[来源N]提取出来过滤掉N超出chunk数量的幻造引用格式归一化用正则把各种变体格式统一成标准格式再渲染给用户兜底方案如果检测到来源标注格式混乱比如同一段里出现多种格式降级为方案一在末尾统一追加来源列表。这部分逻辑写起来不复杂但不写的话上线必翻车——QA测出来的问题基本就是这些格式问题。坑四流式进度反馈检索阶段的用户体验检索阶段无法真正流式化必须等检索完成才能开始生成但可以把进度信息实时推送给用户。进度反馈的实现方式SSEServer-Sent Events服务端通过SSE长连接实时向客户端推送进度事件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: 根据}用户看到的效果[正在理解您的问题... 10%] [正在检索相关资料... 40%] [正在筛选最相关内容... 70%] 根据A款重疾险产品手册等待期为...流式输出心理学研究表明有进度反馈的等待用户可接受的时间是无进度反馈的2~3倍。即使检索还是需要3秒用户看到进度条在走等待感受完全不同。SSE在生产里有几个容易踩的坑坑1Nginx默认缓冲会让SSE失效。Nginx默认会缓冲上游响应等缓冲区满了才转发给客户端。SSE需要的是逐事件实时推送被缓冲之后就变成了批量发送进度条效果完全消失。解决方法是在Nginx配置里关掉对应路由的缓冲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之后的事件开始续发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这块很多人以为加个streamTrue参数就算做了流式输出。实际上RAG场景的延迟优化是一个系统性问题——检索并行化、查询改写条件化、进度反馈、错误处理每一个环节都有优化空间也都有自己的坑。但更重要的是把这些说出来的时候要能说清楚每个决策背后的权衡软超时的阈值为什么这样定跳过Rerank的分数线怎么标定SSE为什么要关Nginx缓冲。说清楚权衡面试官才知道你真的做过而不是把文章里的结论背了一遍。
返回列表