最近我们的产品接入了Google Gemini 1.5 API,一开始性能很差,平均响应时间要15秒,用户体验很不好。

经过一系列的优化,我们把平均响应时间降到了3秒以内,降幅达到了80%。这篇文章,我想分享一下这次性能优化的实战经验,聊聊大模型API的优化思路和具体手段。

背景和问题

先说说背景和遇到的问题。

我们的产品是一个AI文档助手,用户上传文档(PDF、Word、PPT等),然后可以和文档对话,提问、总结、翻译。我们用Gemini 1.5 Pro作为底层大模型,因为它支持1M的上下文窗口,能处理很长的文档。

刚上线的时候,性能很差。主要问题有几个:

第一个是,响应时间长。平均响应时间15秒,复杂的文档甚至要30秒以上。用户问一个问题,要等半分钟才能得到回答,体验非常差。

第二个是,首字延迟高。从用户发送问题到收到第一个字的回复,平均要5秒。用户等了半天,什么都看不到,很容易以为系统卡住了。

第三个是,长文档性能更差。文档越长,响应时间越长。超过100页的文档,有时候甚至会超时。

第四个是,并发能力弱。用户量上来之后,API的并发请求经常被限流,很多请求失败或者排队。

这些问题严重影响了用户体验,很多用户因为等待时间太长而流失。于是,我们开始了性能优化。

第一步:性能分析

优化的第一步,是搞清楚时间都花在哪里了。

我们给每次API调用加了详细的日志,记录了每个阶段的耗时:文档预处理、Prompt构建、API请求、首字返回、完整回复、后处理。

分析之后发现,时间主要花在几个地方:

第一个是,API请求和推理。这是最大的瓶颈,占了总时间的70%以上。Gemini 1.5 Pro的推理速度本身就不算快,尤其是长上下文的情况下。

第二个是,文档预处理。每次用户提问,我们都要把文档重新解析、切块、拼接进Prompt。这个过程平均要2秒,文档长的话更久。

第三个是,Prompt太长。我们把整个文档的内容都放进了Prompt,导致Prompt非常长。长Prompt不仅增加了输入token的数量,也增加了模型的处理时间。

第四个是,没有用流式输出。一开始我们用的是非流式调用,要等模型生成完所有内容,才一次性返回给用户。这样用户等待的时间就是完整的生成时间,体验很差。

找到了瓶颈之后,我们开始针对性地优化。

第二步:流式输出

我们做的第一个优化,是改成流式输出。

Gemini API支持流式调用(streaming),模型生成一个字就返回一个字,不需要等全部生成完。我们把非流式调用改成了流式调用,然后把收到的内容实时推送给用户。

这个优化的效果非常明显。虽然完整的响应时间没有变(还是要那么久才能生成完),但用户的感知等待时间大大缩短了。用户发送问题之后,1到2秒就能看到第一个字,然后内容一点点出来,体验好了很多。

而且,流式输出还有一个好处:用户可以在生成的过程中就开始阅读,如果发现回答不对,可以及时停止,不需要等全部生成完。这样又节省了一部分时间。

流式输出是大模型应用最基本的优化,也是投入产出比最高的优化。如果你还在用非流式调用,我强烈建议改成流式。

第三步:文档预处理和缓存

第二个优化,是文档预处理和缓存。

之前的做法是,用户每次提问,都重新解析文档、切块、拼接Prompt。但实际上,同一个文档的内容是不变的,不需要每次都重新处理。

我们改成了:用户上传文档的时候,就一次性把文档解析好、切块好、生成向量,缓存起来。用户提问的时候,直接用缓存好的内容,不需要重新解析。

我们还做了多级缓存:

  • 本地缓存:最近使用的文档内容缓存在内存里,访问最快。
  • Redis缓存:热点文档的内容缓存在Redis里,访问也很快。
  • 对象存储:所有文档的解析结果存在对象存储里,需要的时候再加载。

这样,大部分请求都能命中缓存,文档预处理的时间从2秒降到了几十毫秒。

另外,我们还用了Gemini API的cached content功能。Gemini 1.5支持把Prompt中不变的部分(比如文档内容)提前缓存到Google的服务器上,后续调用的时候只需要传问题,不需要重复传文档内容。这样既减少了传输时间,也降低了输入token的费用。

第四步:RAG检索优化

第三个优化,是RAG(检索增强生成)的优化。

之前我们把整个文档的内容都放进了Prompt,这不仅浪费token,也增加了模型的处理时间。实际上,用户的问题通常只和文档的某一部分相关,不需要把整个文档都给模型。

我们改成了RAG架构:

  1. 用户上传文档的时候,把文档切成小块,每块生成向量,存入向量数据库。
  2. 用户提问的时候,把问题也生成向量,在向量数据库里检索最相关的几块内容。
  3. 只把检索到的相关内容放进Prompt,传给模型。

这样,Prompt的长度大大缩短了。之前可能要几万甚至几十万token,现在只需要几千token。模型的处理时间自然就快了很多。

我们还做了几个检索优化:

  • 混合检索:同时用向量检索和关键词检索,取并集,提高召回率。
  • 重排序:检索到的内容,用一个小模型重新排序,取最相关的前几块。
  • 上下文窗口:除了相关的块,还把前后的块也加进来,保证上下文的完整性。

RAG优化之后,不仅响应时间快了,回答的质量也提高了。因为模型只看到相关的内容,不容易被无关内容干扰。

第五步:Prompt优化

第四个优化,是Prompt的优化。

Prompt的写法,对模型的响应时间和输出质量都有很大影响。我们做了几个优化:

第一个是,精简Prompt。去掉了不必要的说明和示例,只保留最核心的指令。Prompt越短,模型处理越快。

第二个是,明确输出格式。在Prompt里明确要求模型用特定的格式输出(比如Markdown、JSON),减少模型生成无关内容的概率,也减少了输出token的数量。

第三个是,限制输出长度。在API参数里设置maxoutputtokens,限制模型的最大输出长度。不需要长回答的场景,把maxoutputtokens设小一点,生成时间自然就短了。

第四个是,用系统提示(system instruction)。Gemini API支持系统提示,可以把角色设定、规则等放在系统提示里。系统提示的优先级更高,模型更容易遵循,而且可以和用户输入分开管理。

Prompt优化之后,输出的token数量减少了约30%,响应时间也相应缩短了。

第六步:模型选择和降级

第五个优化,是模型选择和降级策略。

Gemini有多个模型:Gemini 1.5 Pro(能力强,速度慢,贵)、Gemini 1.5 Flash(能力稍弱,速度快,便宜)、Gemini 1.5 Flash-Lite(能力更弱,速度更快,更便宜)。

之前我们所有请求都用Pro模型,但实际上,很多简单的问题(比如总结、翻译、简单问答),用Flash模型就足够了,速度快很多,价格也便宜很多。

我们做了一个智能路由:

  • 简单的问题(总结、翻译、短文档问答):用Flash模型。
  • 复杂的问题(长文档深度分析、多步推理):用Pro模型。
  • 高并发的时候:Pro模型限流了,就降级到Flash模型,保证服务可用。

这样,大部分请求都用Flash模型,平均响应时间大大缩短。只有少数复杂的请求才用Pro模型,保证了回答质量。

我们还做了降级策略:如果Pro模型超时或者限流,自动降级到Flash模型重试。虽然回答质量可能稍差,但至少能保证用户得到回复,比超时失败好很多。

第七步:并发和限流优化

第六个优化,是并发和限流的优化。

Gemini API有速率限制(RPM和TPM),超过限制就会被限流。之前我们没有做好限流控制,经常触发限流,导致请求失败或者排队。

我们做了几个优化:

第一个是,客户端限流。在客户端做令牌桶限流,控制发送给API的请求速率,不超过API的限制。这样可以避免触发服务端的限流,减少429错误。

第二个是,请求队列。超过限流的请求,放进队列里排队,等有令牌了再发送。这样用户的请求不会直接失败,只是稍微等一会儿。

第三个是,并发连接复用。用HTTP/2或者连接池,复用TCP连接,减少连接建立的开销。Gemini API支持HTTP/2,我们用了gRPC的流式调用,性能更好。

第四个是,批量处理。对于不需要实时响应的任务(比如文档总结),我们攒一批一起处理,提高并发利用率。

这些优化之后,API的限流错误基本消失了,系统的吞吐量也提高了。

优化效果

经过以上这些优化,我们的系统性能有了质的飞跃。

响应时间方面:平均响应时间从15秒降到了3秒以内。其中,首字延迟从5秒降到了1秒以内。用户体验提升了很多。

成本方面:因为用了RAG减少了输入token,用了Flash模型处理大部分请求,用了cached content,API的费用降低了约60%。

稳定性方面:限流错误从5%降到了0.1%以下,系统的可用性达到了99.9%。

回答质量方面:虽然大部分请求用了Flash模型,但因为RAG检索更精准了,回答的质量反而比之前用Pro模型加全文档Prompt更好。

总的来说,这次优化非常成功,性能、成本、质量三个方面都有提升。

一些经验总结

这次Gemini 1.5的性能优化,我总结了一些经验。

第一,大模型应用的优化,要从全链路入手。不要只盯着模型推理的速度,要看看文档预处理、Prompt构建、网络传输、后处理等各个环节,每个环节都可能有优化空间。

第二,流式输出是必须的。不管你的模型有多快,流式输出都能大大提升用户的感知体验。这是投入产出比最高的优化。

第三,RAG是长文档处理的标配。不要把整个文档都塞进Prompt,既慢又贵,效果还不好。用RAG检索相关内容,又快又好又便宜。

第四,合理选择模型。不是所有请求都需要最强的模型。大部分简单的请求,用小模型就够了,又快又便宜。把强模型留给真正复杂的任务。

第五,做好缓存。能缓存的都缓存:文档解析结果、向量、Prompt、模型的回答。缓存是提升性能、降低成本最有效的手段之一。

第六,监控和分析很重要。优化之前,先做详细的性能分析,找到真正的瓶颈。不要盲目优化,用数据说话。优化之后,也要持续监控,看效果如何,有没有新的瓶颈。

写在最后

大模型应用的性能优化,是一个持续的过程。模型在更新,工具在进步,用户的需求也在变化。没有一劳永逸的优化方案,需要持续监控、持续优化。

但只要掌握了正确的思路——分析瓶颈、针对性优化、持续监控,就能把大模型应用的性能做到用户满意的程度。

希望这篇文章能给做AI应用开发的朋友一些参考。如果你也有大模型性能优化的经验,欢迎在评论区交流。