
做AI应用最容易被忽视、又最让人头疼的事就是性能。我做过几个智能问答和RAG检索类的项目前期功能都跑通了一上量就开始卡请求排队、Token烧钱、首字返回慢到用户以为服务挂了。后来把缓存、提示压缩、检索加速这三板斧全用上同样的服务延迟直接砍掉一半多成本也降了不少。这篇就把我这套性能优化思路完整整理出来。文章会围绕三个核心手段展开缓存机制解决重复计算提示压缩解决Token浪费检索加速解决向量召回慢。适合正在做LLM应用、RAG系统、智能客服、知识库问答的同学参考如果你刚入门但已经能跑通一个基础Demo这篇的实操方案也能直接照抄。1. 性能检查先搞清楚系统到底慢在哪性能优化最忌讳一上来就动手结果改了一堆配置实际收益却很小。我习惯先做一轮瓶颈画像把请求链路从头到尾拆开测量一遍再决定动哪个环节。1.1 完整的请求链路有哪些耗时环节一个典型的RAG问答请求从用户输入到返回答案大体经过这几个环节输入预处理检查缓存、意图判断、问题改写耗时大概几毫秒到几十毫秒。检索阶段Embedding向量化、向量库查询、可选的关键词召回、排序融合这里往往是最大的延迟来源。上下文组装把检索到的文档拼进Prompt做截断或压缩耗时很少但直接影响后面生成的质量。LLM推理调用大模型生成答案依赖输入长度、输出长度和模型大小通常要几百毫秒到数秒。拿我之前的日志数据举例一次请求总耗时2.8秒其中向量检索占了1.2秒LLM推理占1.1秒剩下的是其他开销。所以优化重点就很明确先把检索压下来再压缩Prompt减少模型计算量。1.2 量化指标别凭感觉决策性能优化之前必须先建立基线。我一般用三个指标来衡量P95首字节延迟用户感知最明显的是首Token时间目标是大型对话场景控制在1.5秒以内。总请求耗时从请求发出到完整流式返回结束适合判断一次交互的整体体验。每请求Token消耗包括输入Token和输出Token直接和成本挂钩。建议在日志里打上阶段耗时埋点比如cache_hit_time、retrieve_time、llm_first_token_time跑上一两天真实流量拿数据说话。我见过不少项目优化了半天回头看日志才发现瓶颈根本不在自己猜的地方。1.3 缓存、压缩、检索各自解决的痛点这三个技术手段不是孤立的它们对应三个不同的瓶颈缓存解决“重复计算”问题同样的诉求别算两遍。提示压缩解决“Token浪费”问题Prompt里塞满废话每一句都在烧钱和增加首字延迟。检索加速解决“召回慢”问题让向量查询从秒级降到百毫秒级。把它们组合起来的效果是缓存兜底高频请求压缩降低生成成本检索加速降低召回时间。三者合在一起收益往往是相乘而不是相加。2. 缓存机制从KV缓存到语义缓存一层一层把重复计算干掉缓存是性能优化里见效最快的手段但很多人只盯着“KV Cache”忽略了应用层的语义缓存结果优化空间白白浪费了。2.1 应用层缓存命中一次省一次Embedding和生成我在项目里最先实现的是一个简单的“精确查询缓存”把用户输入做归一化去空格、转小写、同义词归一然后作为Key把答案和对应的检索结果存进Redis。命中后直接返回省去了检索、组装、生成整个链路。但精确匹配的覆盖率很低因为用户问同一件事经常会换不同的说法。比如“怎么设置支付密码”和“支付密码在哪里改”本质上是一回事但字符串完全不一样。所以我又加了语义缓存把用户查询向量化。在向量库里查找历史问题的相似度取Top1。如果相似度超过阈值我常用0.92直接复用缓存答案。语义缓存的核心是把Map从“字符串等于”升级成“语义相似”命中率能提升一大截。不过要小心一个问题相似并不等于相同阈值设太高会漏掉真正的重复设太低会把不同意图的错误答案返回给用户。我的经验是知识问答类场景0.90-0.94比较稳意图越刚性比如查询订单状态越可以往下调到0.88。2.2 大模型推理层的Cache不只是用现成的模型侧其实也有两层缓存值得关注。第一层是HuggingFace生态里的KV Cache这是Transformer推理内置的缓存解码时把前面Token的Key和Value缓存下来避免重复计算。这个一般框架默认就开着不需要手动动它但要注意显存占用长上下文场景下KV Cache会吃掉大量显存如果爆显存反而更慢。此时可以考虑对整条链路做流式输出降低峰值压力。第二层是“Prompt Cache”。很多推理框架比如常见的推理服务会缓存前缀的KV状态也就是把系统提示词和固定的工具说明部分缓存起来只要输入前缀一致就能复用计算结果。这就要求在工程上把Prompt模板固定下来不要在系统提示词里动态插入用户信息、时间戳之类的内容否则缓存永远打不中。2.3 缓存淘汰策略与一致性很容易踩坑缓存写起来简单淘汰才考验功力。我之前犯过的错包括缓存永不过期导致数据陈旧缓存全部设置了5分钟过期导致雪崩。后来统一用以下的策略组合热点数据设置TTL知识库更新时主动删除对应Key。使用LRU淘汰策略保证内存里只留热数据。大促或运营活动后手动清理历史语义缓存的向量索引防止垃圾相似结果污染。另外强烈建议做“缓存填充”而不是“缓存穿透”。流量一来如果缓存没命中所有人都去打后端的向量库和模型接口系统照样崩溃。我一般会在缓存未命中时加一把分布式锁让同一个Key只有一个请求去查询后端其余请求等结果直接复用。3. 提示压缩把Prompt里的每一克Token都榨干提示压缩这门技术本质是在保证生成质量的前提下把输入给大模型的Token数量减下来。输入Token少了首字延迟下降、计费下降、服务端的资源占用也下降。听起来很直接但具体怎么做大多数人只想到了“让模型自己概括”这块坑很多。3.1 为什么提示压缩能带来实打实的加速大模型生成时的计算量和输入长度正相关尤其处理长上下文时KV Cache和注意力计算都随序列长度上升。把5000字压缩到2000字等于直接让这部分的计算量和内存占用减半以上。但有个原则务必记住压缩不能破坏关键信息。做检索场景时如果压缩太狠把答案所在的原文细节弄丢了生成的回答就是一本正经的胡说八道。所以我在做提示压缩时追求的是“减掉无关保住核心”。3.2 我实测有效的三档压缩策略第一档结构化截断。这是成本最低、风险也最低的方法。先按文档标题和段落结构拆分内容只保留与用户问题语义相关的段落把无关背景、版权信息、落款直接去掉。用这种方式检索返回的Top5文档经常能压缩掉50%-60%的Token而且几乎不损失答案质量。第二档摘要压缩。让模型把较长段落提炼成要点适合知识库中背景介绍类的内容。但实际操作里我不建议每次请求都用大模型做摘要因为那也是一次模型调用成本反而拉高了。更靠谱的做法是离线预处理好在文档入库时就把长文切成结构化的“摘要块”和“关键数据块”在线推理时直接取现成的。第三档插入压缩标记。由于现在很多大模型对中英文内容都比较敏感我会尝试“只提取与问题直接相关的句子”相当于用规则先筛一遍再拼进Prompt。如果检索到的段落是“产品的保修政策是……具体条款包括……”我会只提取“保修政策为一年免费保换非人为损坏不修”这类关键句。3.3 压多少合适一个可量化的经验区间我一般按输入Token预算来控制压缩强度。比如设定“最终Prompt输入不超过2500 Token”如果检索回来的原始材料有7000 Token就按递归压缩法逐层压缩直到到达预算。压得越狠回复越容易偏。我自己测试下来压缩率控制在60%-70%之间质量下降很小超过80%就要小心了特别是指令中包含复杂推理步骤的场景。比如“先判断提问意图再调用工具最后总结”这类少样本逻辑压缩掉示例会导致模型行为漂移。还有一个小技巧如果系统提示词和示例占了大量Token优先压缩示例。把多轮示例从“三个完整对话”剪成“一段最短可行示例”既保住了格式约束又大幅缩减输入长度。少样本示例的收益是边际递减的塞得再多效果提升也很有限Token却照常烧。4. 检索加速向量索引调优、量化、混合检索策略检索慢主要是向量库的ANN检索慢以及海量文档下候选集太宽。这里我从三个层面说明我自己的提速方案。4.1 向量索引类型与参数HNSW和IVF怎么选当数据量在百万级以下我默认用HNSW索引搜得准速度也能到百毫秒级。关键在于参数调整M每个节点的最大连接数我设32连接数大一点召回率高但内存占用上升。efConstruction建索引时的搜索范围设200左右建索引慢一点但质量好。efSearch每次查询时扩展的候选集设64延迟一般在40-80ms。如果追求更快可以降到32但召回率会轻微下降。如果数据量到了千万级以上内存快撑不住就切到IVF索引。IVF的核心是“聚类后再搜索”先通过聚类把向量空间划成N个桶查询时只搜邻近的桶。关键参数是nlist和nprobenprobe表示查询时搜索多少个桶。nprobe越少速度越快但精度越低。我的调参路径是先固定nlist4096然后从nprobe8往上加直到召回率达到需要的95%同时保证延迟不超过150ms。4.2 向量量化内存降一半延迟也能降向量维度越高检索耗时越长。有两个方案方案一是标量量化SQ把浮点型向量压成int8。精度损失很小内存直接砍到四分之一。我实测下来在百万级数据集上SQ8压完检索延迟大概能降20%-30%召回率下降不到两个点对绝大多数问答场景完全够用。方案二是乘积量化PQ把向量切分成子空间每个子空间单独聚类并编码适合超大规模数据。但PQ的精度损失比SQ大一点如果检索结果频繁不理想我会退回到SQHNSW的组合。4.3 混合检索关键词兜底语义召回加速单用向量检索有一个经典毛病遇到专有名词、缩写、产品型号向量召回容易失灵。比如搜“iPhone 15 Pro 掉电快”向量可能找到一堆“手机电池健康度”的泛泛文章却漏掉精确配型的内容。我的解决方案是“向量检索 BM25关键词检索 混合”召回后做RRFReciprocal Rank Fusion融合排序。具体做法是向量检索返回Top20。BM25返回Top20权重略偏向精确关键词匹配。用RRF公式合并排序公式简化后就是“排名倒数的加权和”让两个来源的结果排队不再打架。混合检索看似增加了检索环节实际上反而降低了无效Token进入Prompt的概率因为召回更精准后续提示压缩的压力也小很多。4.4 检索侧缓存还记得前面的语义缓存吗把语义缓存放在检索链路之前是最划算的一笔投资。用户问“打印机连接不上怎么办”第一次可能跑完整个检索流程第二次来一个相似问法直接命中缓存答案返回检索环节一次都不用走。我在实际项目里测过启用语义缓存之后整体平均延迟从1.8秒降到0.7秒主要收益就来自免检索和免生成。注意语义缓存一定要绑定“知识库版本号”知识库内容变更后历史缓存要清理掉否则答案会越来越陈旧。5. 常见问题与排查技巧这部分是纯踩坑经验。很多性能问题不是优化方案不对而是细节没处理到位。5.1 缓存总是不命中怎么办先看键设计。如果缓存Key包含时间戳、用户ID这些一秒钟变好几次的内容那命中率必然很低。我的做法是把Key拆成“意图ID参数摘要知识库版本号”固定部分和变化部分分离。再看阈值。语义缓存的相似度阈值调太高比如0.99那几乎只能命中完全没有改写的句子调太低又容易误命中。建议先用一批真实历史查询统计相似度分布再把阈值打到分布图的拐点位置。5.2 压缩提示后模型输出变差怎么办记住一句话压缩的本质是信息筛选不是泛泛地“让模型浓缩”。如果压缩后答案变差我建议拆开检查是不是连关键数据数字、型号、专有名词都被压掉了解决办法是加一条规则压缩时保留所有数字后跟单位的内容。是不是上下文逻辑断了比如把“如果A那么B”的转折条件压没了。解决办法是把每段保留其首句和最末句保住局部逻辑结构。是不是角色指令被压掉了一条系统提示词里的“仅基于资料回答”这种护栏语句永远不要出现在可压缩的文本里。5.3 向量检索延迟突然飙升我遇到过一种情况数据集不变延迟却从80ms涨到300ms。排查后才发现是批量写入把索引的实时性拖住了。如果你把新文档实时入库并且要求立即能被检索到那每次插入都会触发索引更新。数据写入频繁时HNSW索引会一直处于“重建状态”查询排队就很厉害。解决办法是把写入做成批量模式每5秒或每100条批量刷一次索引同时让查询走旧索引写完再切换。另外一个常见点是网络带宽和磁盘IO向量文件往往几十GB如果存储盘是慢速机械盘加载和查询都会受影响最好把向量库建立在SSD实例或纯内存模式下。5.4 压测时最容易忽视的隐性问题Embedding接口也会成为瓶颈尤其向量化查询并发高的时候建议给Embedding模型也加缓存。LLM流式输出时机要提前首字返回速度直接影响用户体验建议在模型输出第一个字节后就启动流式返回而不是等完整生成完再回。批处理大小如果你针对离线批量任务优化Batch Size适当调大能明显提高吞吐但在线请求千万别用大Batch尤其不要用“边生成边等待所有请求完成”的逻辑那样单请求延迟会急剧上升。6. 经验总结和个人会坚持的做法性能优化不是一个一劳永逸的改动而是一套持续按数据调整的方法论。我自己现在做新项目第一件事就是先把缓存和日志埋点搭好。哪怕最初缓存逻辑很简单只有精确匹配和短TTL也能在早期挡住大部分重复流量留出充裕时间来优化检索和压缩。提示压缩这步我强烈建议做成“压缩规则可配置”不要硬编码在代码里。每次模型版本升级后同样的压缩策略效果可能不一样留一个配置开关方便线上对比和回滚。检索加速的收益上限取决于数据质量索引参数调了半天也救不了脏数据。所以项目早期宁可多花时间在切分和清洗也别急着上HNSW调参。最后分享一个小技巧性能优化的优先级永远跟着真实流量走。先看日志统计哪个环节耗时最长、哪个环节重复率最高再动手改。如果需要准备一个1000条左右的历史问答数据集做回归测试每次调完缓存或压缩策略跑一遍看答案相似度和延迟变化。有了这套基线后面再怎么改都不怕翻车。