缓存机制,同样的问题不要让大模型回答两次
上一篇聊异步,把执行速度提上去了。速度提上去,账单也跟着涨。每个请求都打到大模型,哪怕问的是一模一样的问题,模型还是老老实实算一遍,token照烧。这篇就聊缓存,把算过的结果存下来,下次再问直接取,能省一截是一截。
为什么Agent需要缓存
Agent跑起来,重复调用比你想的频繁。客服场景里,用户问"怎么退货",今天问明天问,十个人里八个问的差不多。每次都让模型重新组织一遍答案,答案大同小异,token花得冤枉。
Agent内部也有重复。一个任务跑下来,中间可能调好几次模型,有些中间步骤的输入高度相似。比如每次都让模型先判断意图,输入就是用户那句话,翻来覆去就那些。这些重复调用全压在模型上,成本和延迟都白搭。
我自己踩过这个坑。做一个文档问答Agent,测试阶段同一个问题反复调,跑一下午账单几十刀。当时没接缓存,每个问题都实打实打模型。后来加了缓存,同样的测试再跑一遍,账单掉到几刀以内。那会儿才体会到,缓存这东西在AI应用里算省钱的命门。
原理也简单。把prompt和模型参数算个哈希当key,结果当value存起来。下次同样的输入进来,先查缓存,命中直接返回,没命中再打模型。省的是模型那一段的计算和等待。
InMemoryCache
最简单的缓存是InMemoryCache,存进程内存里,进程一停就没了。
fromlangchain_core.globalsimportset_llm_cachefromlangchain_core.cachesimportInMemoryCachefromlangchain_openaiimportChatOpenAI set_llm_cache(InMemoryCache())model=ChatOpenAI(model="gpt-4o-mini")print(model.invoke("什么是向量数据库"))print(model.invoke("什么是向量数据库"))两次invoke同一句话,第二次几乎瞬间返回,因为命中缓存没打模型。set_llm_cache是全局设置,设一次所有模型实例都走这个缓存。
InMemoryCache图快,零配置,调试和原型阶段最合适。坏处也明显,进程重启缓存就没了,多进程或者多台机器之间不共享。上了gunicorn起四个worker,每个worker各存各的,命中率惨不忍睹。
SQLiteCache
想跨重启保留缓存,SQLiteCache把结果写到本地sqlite文件里。
fromlangchain_core.globalsimportset_llm_cachefromlangchain_community.cacheimportSQLiteCache set_llm_cache(SQLiteCache(database_path=".langchain.db"))设完之后,所有缓存结果写进.langchain.db这个文件。进程重启,缓存还在,下次同样的输入照样命中。适合个人项目或者单机部署的小服务。
SQLiteCache有个坑我得提一句。它默认建表没怎么管并发,多进程同时写同一个db文件,偶尔会锁库报错。跑批处理脚本一个进程没事,上了多worker的Web服务就可能撞。碰到这种情况,要么换Redis,要么把缓存挪到独立的单进程里。
RedisCache
真上量,多机器多进程,缓存得放共享存储里,Redis是标配。RedisCache把缓存放到Redis,所有进程所有机器读同一份,命中率高,还自带过期机制。
fromlangchain_core.globalsimportset_llm_cachefromlangchain_community.cacheimportRedisCachefromredisimportRedis set_llm_cache(RedisCache(redis_=Redis(host="localhost",port=6379)))RedisCache构造函数传一个redis客户端实例进去,LangChain拿它读写缓存。服务重启缓存还在,加机器命中率照样高,生产环境基本都走这套。
有个细节得注意。Redis里缓存的value是序列化之后的完整响应,占内存。问题量大的时候留意Redis的内存上限,该设过期时间就设,别让缓存把Redis撑爆。RedisCache支持传ttl参数控制过期,我的习惯是给个几小时到一天,看问题更新频率。
语义缓存,相似问题也命中
前面几种都是精确匹配,输入一字不差才命中。但用户问"怎么退货"和"退货流程是什么",两句话字面不同意思一样,精确匹配抓不到,还得打模型。
语义缓存干的就是这个。它把每个问题算个embedding向量存下来,新问题来了先算向量,去库里找最相似的,相似度超过阈值就认为命中,直接返回那条答案。RedisSemanticCache是LangChain现成的实现。
fromlangchain_core.globalsimportset_llm_cachefromlangchain_community.cacheimportRedisSemanticCachefromlangchain_openaiimportOpenAIEmbeddingsfromredisimportRedis set_llm_cache(RedisSemanticCache(redis_=Redis(host="localhost",port=6379),embedding=OpenAIEmbeddings(),distance_threshold=0.2,))distance_threshold控制相似度门槛,越小越严格。设得太大,不相关的问题也命中,答非所问。设得太小,跟精确匹配差不多,优势没了。这个值得花时间调,拿一批真实问题挨个试,看哪些该命中没命中、哪些不该命中却命中了。
语义缓存我踩过一个坑。threshold调得太松,用户问"怎么退货"命中了"怎么退款"的缓存,退货和退款在电商里是两套流程,答案给错了用户直接投诉。语义缓存对问题相近但答案必须区分的场景要格外小心,客服、医疗、法律这类,宁可严一点别图省事。
缓存策略怎么选
几种缓存怎么挑,看部署形态和问题特征。
本地开发、原型验证,InMemoryCache一行代码搞定,图省事。单机部署的小服务、脚本批处理,SQLiteCache结果落盘不怕重启。多进程或多机器的生产服务,RedisCache共享缓存命中率高。问题表述发散、重复语义多的场景,再叠加语义缓存。
几个通用建议。缓存命中得可观测,打日志记录命中没命中,不然哪天缓存没生效你都不知道,钱照花。模型升级或者提示词改了,旧缓存全失效,得有办法清。语义缓存上线前拿真实问题集做一轮评测,threshold调到误命中率能接受再放出去。
还有一点,工具调用的结果别一股脑进缓存。Agent里模型决定调哪个工具,这个决策过程缓存了,工具本身的数据变了,缓存还是老结果,容易出脏数据。我一般只缓存纯文本问答那一段,涉及工具和检索的步骤留着实时跑。
小结
这篇把LangChain的缓存机制过了一遍。为什么需要缓存,重复调用白烧token。InMemoryCache存内存图快,SQLiteCache落盘抗重启,RedisCache共享适合多机,语义缓存连相似问题都能命中。每种各有各的适用场景,选对了省token省延迟,选错了要么没用要么答错。
缓存是省钱省时间的一招,光有缓存还不够。代码写得乱、链搭得歪、Prompt拼得随意,这些毛病缓存救不了。下一篇就聊LangChain最佳实践,看看代码组织和工程上有哪些该守的规矩。