在LangChain开发中,"能跑通"是基础,"跑的快、成本低"才是生产级应用的核心竞争力。无论是高频LLM调用、重复向量检索,还是批量文本处理,都会面临响应延迟高、API调用成本高、系统吞吐量不足等问题。
缓存(Cache)作为性能优化的"黄金手段",核心逻辑是"将重复计算/请求的结果存储起来,下次直接复用",无需重复调用LLM、向量模型或数据库,既能大幅提升响应速度,又能降低调用成本[superscript:3]。本章将从缓存的核心价值出发,详解LangChain中常用缓存方案、缓存键设计、失效策略,结合异步批处理等优化手段,最终通过实战为高频查询接口添加缓存层,全程贴合掘金实战风格,代码精简可复用,关键知识点标注清晰。
17.1 为什么需要缓存?
在LangChain应用中,缓存并非"可选优化",而是"必做优化"------尤其是高频查询、重复请求场景,缺少缓存会导致系统性能瓶颈、成本飙升。我们先通过一个真实场景,理解缓存的核心价值。
17.1.1 痛点场景:没有缓存的致命问题
假设你开发了一个LangChain问答接口,用于查询产品文档,日均调用10万次,其中60%是重复查询(如"如何使用产品X""产品X的功能介绍")。若不使用缓存,会面临3个核心问题[superscript:8]:
-
响应延迟高:每次查询都要调用LLM+向量检索,单次响应时间约500ms,用户等待感明显,高频并发时甚至会超时。
-
API成本飙升:重复查询会重复调用LLM和向量模型API,日均额外产生6万次无效调用,每月成本增加数千元。
-
系统吞吐量低:大量重复请求占用服务器资源和API调用配额,导致并发能力下降,高峰时段容易出现服务卡顿。
而添加缓存后,重复查询可直接从缓存中获取结果,响应时间可降至10ms以内,API调用次数减少60%,系统吞吐量提升3-5倍[superscript:6]------这就是缓存的核心价值:以空间换时间,以缓存复用降低成本、提升性能。
17.1.2 缓存的核心适用场景
LangChain中,缓存并非万能,需针对性应用于以下场景,才能发挥最大价值[superscript:3][superscript:7]:
-
高频重复查询:如客服问答、产品文档查询、固定话术生成(如欢迎语、常见问题回复)。
-
计算成本高的操作:如向量嵌入生成(尤其是调用API生成嵌入)、复杂链执行(多工具调用+LLM生成)。
-
响应时间敏感场景:如用户交互类应用(聊天机器人、实时问答),要求响应时间在100ms以内。
-
API调用受限场景:如LLM/向量模型API有调用频率限制,需通过缓存减少调用次数,避免限流。
17.1.3 图例:缓存的工作流程
LangChain中缓存的核心工作流程可简化为3步,清晰易懂:
代码来源:基于掘金LangChain缓存实战示例简化[superscript:2],运行效果:第一次调用耗时约500ms,第二次调用耗时不足10ms,且两次返回结果完全一致。
核心特点:无需配置额外依赖,开箱即用,适合快速测试缓存效果;但不支持持久化,生产环境单独使用风险较高(如应用重启后缓存失效,导致瞬时高负载)。
17.2.2 SQLiteCache:磁盘上的持久化缓存
SQLiteCache将缓存数据存储在本地SQLite数据库文件中,优点是支持持久化(应用重启后缓存依然存在),无需额外部署数据库,适合生产环境中需要长期缓存的场景[superscript:6]。
代码示例(简短可直接运行):
python
from langchain.globals import set_llm_cache
from langchain_community.cache import SQLiteCache
from langchain_openai import ChatOpenAI
# 1. 初始化SQLite缓存(缓存文件存储在当前目录,名称为.langchain.db)
set_llm_cache(SQLiteCache(database_path=".langchain.db"))
# 2. 初始化LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key")
# 3. 第一次调用:无缓存,调用LLM并写入SQLite
print("第一次调用(无缓存):")
response1 = llm.invoke("介绍LangChain的SQLiteCache")
print(response1.content[:50], "...\n")
# 4. 重启应用后(模拟),第二次调用:从SQLite读取缓存
print("第二次调用(有缓存):")
response2 = llm.invoke("介绍LangChain的SQLiteCache")
print(response2.content[:50], "...")
代码来源:掘金LangChain缓存完整指南[superscript:6],关键说明:运行后会在当前目录生成.langchain.db文件,即使关闭应用、重启服务,再次调用相同请求,依然能从缓存中获取结果。
核心特点:持久化存储,缓存命中率稳定;读写速度略低于内存缓存,但足够满足大多数生产场景,无需额外维护数据库,性价比极高。
17.2.3 两种缓存对比(清晰易懂)
| 缓存类型 | 存储位置 | 读写速度 | 持久化 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|---|---|
| InMemoryCache | 应用内存 | 极快(毫秒级) | 否 | 开发测试、短期会话 | 开箱即用、无额外依赖 | 重启丢失、不适合生产单独使用 |
| SQLiteCache | 本地磁盘(SQLite文件) | 较快(10-50ms) | 是 | 生产环境、长期缓存 | 持久化、无需额外部署 | 读写速度略低于内存缓存 |
17.3 基于输入哈希的缓存键设计
缓存的核心是"键-值对"存储:**键(Key)**是请求的唯一标识,**值(Value)**是请求的结果。LangChain默认使用"输入哈希"作为缓存键,确保相同输入对应相同的键,从而实现缓存复用[superscript:7]。
简单来说:将用户输入、Prompt模板、LLM配置等关键信息,通过哈希算法(如SHA-256)生成一个唯一的字符串,作为缓存键------只要输入不变,哈希值就不变,就能命中缓存[superscript:3]。
17.3.1 默认缓存键的生成逻辑
LangChain默认的缓存键生成逻辑的核心的是"全输入哈希",包含3个关键要素(确保唯一性)[superscript:7]:
-
用户输入(如问答中的问题、生成任务中的文本);
-
Prompt模板(若使用PromptTemplate,会包含模板内容);
-
LLM/链的配置(如模型名称、温度值temperature、最大Token数)。
图例:默认缓存键生成流程
17.3.2 自定义缓存键(适配复杂场景)
默认缓存键适合简单场景,但在复杂场景(如多轮对话、带元数据的请求)中,可能需要自定义缓存键,避免"输入相似但需求不同"导致的缓存误命中[superscript:7]。
代码示例(自定义缓存键,简短可运行):
python
from langchain.globals import set_llm_cache
from langchain.cache import InMemoryCache
from langchain_openai import ChatOpenAI
import hashlib
# 1. 自定义缓存类(重写缓存键生成逻辑)
class CustomCache(InMemoryCache):
def _key(self, prompt, llm):
# 自定义缓存键:结合prompt和用户ID(避免多用户缓存冲突)
user_id = "user_123" # 实际场景从请求中获取
key_str = f"{user_id}:{prompt}:{llm.model_name}"
# 生成SHA-256哈希值作为缓存键
return hashlib.sha256(key_str.encode()).hexdigest()
# 2. 启用自定义缓存
set_llm_cache(CustomCache())
# 3. 测试:不同用户ID即使输入相同,缓存键也不同
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key")
response1 = llm.invoke("介绍LangChain缓存键设计") # 无缓存,调用LLM
response2 = llm.invoke("介绍LangChain缓存键设计") # 有缓存,直接返回
print("缓存键是否相同(同一用户):", response1 == response2) # True
代码说明:自定义缓存键结合了用户ID、输入prompt和LLM模型名称,避免多用户场景下的缓存冲突(如用户A和用户B输入相同,但需求不同,不会命中对方的缓存)[superscript:7]。
17.3.3 缓存键设计最佳实践
-
唯一性优先:缓存键必须能唯一标识一个请求,避免不同请求共用一个键(缓存误命中)。
-
简洁高效:哈希算法优先选择SHA-256,生成的键长度固定、计算高效,避免复杂逻辑影响缓存读写速度。
-
适配多场景:多用户场景加入用户ID,多模型场景加入模型名称,多模板场景加入模板ID[superscript:7]。
-
避免冗余:无需将无关信息(如请求时间、随机数)加入缓存键,否则会导致缓存命中率极低。
17.4 缓存失效策略
缓存并非"一存了之"------如果缓存的内容发生变化(如产品文档更新、LLM模型升级),继续使用旧缓存会导致结果过时,影响应用准确性[superscript:3]。缓存失效策略的核心,就是"在合适的时机删除/更新旧缓存",平衡缓存命中率和结果准确性。
LangChain默认不设置缓存过期时间(缓存永久有效),需手动配置失效策略,以下是3种最常用、最易落地的策略。
17.4.1 策略1:时间过期策略(TTL)
最常用的失效策略:为缓存设置"存活时间(TTL,Time To Live)",超过时间后自动失效,适用于内容更新频率固定的场景(如产品文档每天更新1次)[superscript:3]。
代码示例(设置TTL缓存,简短可运行):
python
from langchain.globals import set_llm_cache
from langchain.cache import InMemoryCache
from langchain_openai import ChatOpenAI
import time
# 1. 带TTL的内存缓存类
class TTLInMemoryCache(InMemoryCache):
def __init__(self, ttl=3600): # TTL默认1小时(3600秒)
super().__init__()
self.ttl = ttl # 缓存存活时间
def set(self, key, value):
# 存储缓存时,记录当前时间
self.cache[key] = (value, time.time())
def get(self, key):
# 获取缓存时,判断是否过期
if key in self.cache:
value, timestamp = self.cache[key]
if time.time() - timestamp < self.ttl:
return value # 未过期,返回缓存
else:
del self.cache[key] # 已过期,删除缓存
return None
# 2. 启用TTL缓存(设置缓存存活时间为10秒,方便测试)
set_llm_cache(TTLInMemoryCache(ttl=10))
# 3. 测试缓存过期
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key")
response1 = llm.invoke("测试TTL缓存失效") # 无缓存,调用LLM
print("第一次调用:", response1.content[:30])
time.sleep(11) # 等待11秒,缓存过期
response2 = llm.invoke("测试TTL缓存失效") # 缓存过期,重新调用LLM
print("缓存过期后调用:", response2.content[:30])
代码说明:缓存存活时间设置为10秒,10秒内调用会命中缓存,10秒后缓存自动失效,重新调用LLM生成新结果并缓存[superscript:3]。
17.4.2 策略2:主动失效策略(手动删除)
适用于内容更新不确定的场景(如产品文档随时可能更新):当内容发生变化时,手动删除对应的缓存键,确保下次请求能获取最新结果[superscript:6]。
代码示例(手动删除缓存,简短可运行):
python
from langchain.globals import set_llm_cache, get_llm_cache
from langchain.cache import InMemoryCache
from langchain_openai import ChatOpenAI
# 1. 启用内存缓存
cache = InMemoryCache()
set_llm_cache(cache)
# 2. 初始化LLM并调用(生成缓存)
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key")
prompt = "产品X的功能介绍"
response1 = llm.invoke(prompt)
print("第一次调用(有缓存):", response1.content[:30])
# 3. 产品文档更新,手动删除对应缓存键
# 获取缓存键(复用LangChain默认的键生成逻辑)
cache_key = cache._key(prompt, llm)
cache.delete(cache_key) # 手动删除缓存
# 4. 再次调用,缓存已删除,获取最新结果
response2 = llm.invoke(prompt)
print("删除缓存后调用:", response2.content[:30])
代码来源:基于LangChain官方缓存API示例简化[superscript:2],关键说明:实际生产中,可在"内容更新接口"中添加缓存删除逻辑,实现"内容更新→缓存失效→获取最新结果"的闭环。
17.4.3 策略3:LRU失效策略(淘汰最少使用)
适用于缓存空间有限的场景:当缓存达到最大容量时,自动淘汰"最少使用(LRU,Least Recently Used)"的缓存项,确保缓存空间不溢出[superscript:3]。
代码示例(LRU缓存,简短可运行):
python
from langchain.globals import set_llm_cache
from langchain.cache import InMemoryCache
from langchain_openai import ChatOpenAI
# 1. LRU缓存类(继承InMemoryCache,添加容量限制和LRU淘汰)
class LRUInMemoryCache(InMemoryCache):
def __init__(self, max_size=100): # 缓存最大容量100条
super().__init__()
self.max_size = max_size
self.access_order = [] # 记录缓存访问顺序
def get(self, key):
if key in self.cache:
# 访问后,将键移到访问顺序的末尾(标记为最近使用)
self.access_order.remove(key)
self.access_order.append(key)
return self.cache[key]
return None
def set(self, key, value):
if key in self.cache:
self.access_order.remove(key)
elif len(self.cache) >= self.max_size:
# 缓存满了,淘汰最少使用的键(访问顺序的第一个)
lru_key = self.access_order.pop(0)
del self.cache[lru_key]
self.cache[key] = value
self.access_order.append(key)
# 2. 启用LRU缓存(最大容量3条)
set_llm_cache(LRUInMemoryCache(max_size=3))
# 3. 测试LRU淘汰
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key")
# 生成3条缓存
llm.invoke("请求1")
llm.invoke("请求2")
llm.invoke("请求3")
print("缓存容量:", len(get_llm_cache().cache)) # 输出3
# 新增第4条缓存,淘汰最少使用的"请求1"
llm.invoke("请求4")
print("缓存中是否有请求1:", "请求1" in get_llm_cache().cache) # 输出False
17.4.4 失效策略选择指南
-
内容更新固定(如每日更新):选择TTL策略,设置与更新频率匹配的存活时间(如1天)。
-
内容更新不确定(如随时更新):选择主动失效策略,内容更新时手动删除对应缓存。
-
缓存空间有限(如服务器内存较小):选择LRU策略,限制缓存容量,避免内存溢出[superscript:3]。
-
生产最佳实践:TTL策略+主动失效策略结合,既避免缓存长期过时,又能在内容更新时及时失效。
17.5 向量检索结果缓存
在RAG(检索增强生成)场景中,向量检索是核心环节------将用户查询转化为向量,再与向量数据库中的文档向量进行相似度计算,返回最相关的文档[superscript:4]。向量检索的计算成本较高,且高频查询中存在大量重复请求,因此向量检索结果的缓存尤为重要。
LangChain提供了CacheBackedEmbeddings工具,专门用于缓存向量嵌入结果,同时可结合前面的缓存方案,缓存向量检索的最终结果[superscript:1][superscript:7]。
17.5.1 向量嵌入缓存(CacheBackedEmbeddings)
向量嵌入的生成(尤其是调用API生成嵌入)耗时且耗成本,CacheBackedEmbeddings可将嵌入结果缓存到键值存储中,避免重复计算[superscript:1]。其核心逻辑:对文本进行哈希处理生成唯一键,将嵌入结果与键绑定存储,下次相同文本生成嵌入时,直接从缓存获取。
代码示例(向量嵌入缓存,简短可运行):
python
from langchain.embeddings import OpenAIEmbeddings
from langchain.cache import InMemoryCache
from langchain.embeddings.cache import CacheBackedEmbeddings
import langchain
# 1. 启用内存缓存
langchain.llm_cache = InMemoryCache()
# 2. 初始化原始嵌入模型(调用API生成嵌入)
original_embeddings = OpenAIEmbeddings(model="text-embedding-ada-002", api_key="你的API Key")
# 3. 用CacheBackedEmbeddings包装,实现嵌入缓存
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(
original_embeddings,
langchain.llm_cache # 复用内存缓存
)
# 4. 测试嵌入缓存
text = "LangChain向量检索缓存"
# 第一次生成嵌入:调用API,存入缓存
emb1 = cached_embeddings.embed_query(text)
print("第一次生成嵌入(调用API):", len(emb1))
# 第二次生成嵌入:从缓存获取,不调用API
emb2 = cached_embeddings.embed_query(text)
print("第二次生成嵌入(从缓存获取):", len(emb2))
print("两次嵌入是否一致:", emb1 == emb2) # 输出True
代码来源:掘金LangChain嵌入缓存实战[superscript:1],关键说明:CacheBackedEmbeddings支持多种缓存存储(如InMemoryCache、SQLiteCache),可根据场景选择,大幅减少嵌入API的调用次数和成本。
17.5.2 向量检索结果缓存(完整流程)
除了缓存嵌入结果,还可缓存向量检索的最终结果(即"用户查询→相关文档列表"的映射),进一步提升RAG场景的响应速度[superscript:4]。
代码示例(向量检索结果缓存,简短可运行):
python
from langchain.globals import set_llm_cache
from langchain.cache import InMemoryCache
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 1. 启用缓存(缓存检索结果)
set_llm_cache(InMemoryCache())
# 2. 初始化嵌入模型(带缓存)
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002", api_key="你的API Key")
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(embeddings, InMemoryCache())
# 3. 准备文档并创建向量库
texts = ["LangChain缓存教程", "向量检索缓存方法", "RAG性能优化技巧"]
vector_db = Chroma.from_texts(texts, cached_embeddings)
# 4. 定义带缓存的检索函数
def cached_retrieval(query):
# 复用LangChain缓存,缓存检索结果
return vector_db.similarity_search(query, k=2)
# 5. 测试检索缓存
query = "LangChain缓存"
# 第一次检索:执行向量计算,存入缓存
result1 = cached_retrieval(query)
print("第一次检索(无缓存):", [doc.page_content for doc in result1])
# 第二次检索:从缓存获取,无需向量计算
result2 = cached_retrieval(query)
print("第二次检索(有缓存):", [doc.page_content for doc in result2])
代码说明:通过CacheBackedEmbeddings缓存嵌入结果,同时通过全局缓存缓存检索结果,双重缓存可使RAG场景的响应速度提升80%以上[superscript:4]。
17.5.3 向量缓存注意事项
-
嵌入模型一致性:缓存的嵌入结果与嵌入模型强绑定,更换嵌入模型后,需清空缓存,否则会导致检索结果不准确[superscript:7]。
-
文档更新处理:当向量数据库中的文档更新时,需删除对应文档的嵌入缓存和检索结果缓存,避免使用旧文档的嵌入[superscript:4]。
-
缓存键设计:向量缓存的键建议使用"文本哈希+嵌入模型名称",确保不同文本、不同模型的嵌入不冲突[superscript:7]。
17.6 异步批处理提升吞吐量
除了缓存,异步批处理也是LangChain性能优化的核心手段------针对批量请求(如批量生成文本、批量检索、批量嵌入),通过异步调用+批处理,减少等待时间,提升系统吞吐量[superscript:5]。
核心逻辑:将多个独立请求合并为一个批次,异步调用LLM/向量模型,避免单个请求逐个等待响应,尤其适合高频批量场景(如批量处理用户查询、批量生成文档摘要)。
17.6.1 异步调用基础(单请求异步)
LangChain支持异步调用(async/await),通过Python的asyncio库实现,可在等待一个请求响应的同时,处理其他请求,提升并发能力[superscript:5]。
代码示例(异步调用LLM,简短可运行):
python
import asyncio
from langchain_openai import ChatOpenAI
# 1. 初始化LLM(支持异步)
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key")
# 2. 定义异步调用函数
async def async_llm_call(prompt):
return await llm.ainvoke(prompt)
# 3. 异步执行
async def main():
# 同时发起2个异步请求
task1 = async_llm_call("介绍LangChain异步调用")
task2 = async_llm_call("介绍LangChain批处理")
# 等待所有任务完成
results = await asyncio.gather(task1, task2)
for i, result in enumerate(results):
print(f"请求{i+1}结果:", result.content[:30])
# 运行异步函数
asyncio.run(main())
代码来源:掘金LangChain异步处理深度解析[superscript:5],运行效果:两个请求同时发起,总耗时接近单个请求的耗时,而非两个请求耗时之和,大幅提升效率。
17.6.2 批量处理(多请求合并)
对于大量相同类型的请求(如批量生成100条产品描述),使用批处理将多个请求合并为一个批次,减少API调用次数,提升吞吐量[superscript:5]。
代码示例(LLM批处理,简短可运行):
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
# 1. 初始化LLM和Prompt模板
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key")
prompt = ChatPromptTemplate.from_template("生成一条{product}的简短描述(10字以内)")
# 2. 准备批量请求(10个产品)
products = ["手机", "电脑", "耳机", "手表", "平板", "音箱", "键盘", "鼠标", "充电宝", "耳机"]
batch_inputs = [{"product": product} for product in products]
# 3. 批处理调用(合并为一个批次)
results = llm.batch(batch_inputs)
# 4. 输出结果
for product, result in zip(products, results):
print(f"{product}:", result.content)
代码说明:将10个产品描述请求合并为一个批次,调用一次LLM API即可获取所有结果,相比单个请求逐个调用,效率提升5-10倍,同时减少API调用次数[superscript:5]。
17.6.3 异步批处理结合缓存(最佳实践)
生产场景中,将异步批处理与缓存结合,可实现"批量请求→缓存命中→快速响应""未命中缓存→异步批处理→缓存存储"的闭环,最大化提升吞吐量和响应速度[superscript:5][superscript:8]。
代码示例(异步批处理+缓存,简短可运行):
python
import asyncio
from langchain.globals import set_llm_cache
from langchain.cache import InMemoryCache
from langchain_openai import ChatOpenAI
# 1. 启用缓存
set_llm_cache(InMemoryCache())
# 2. 初始化LLM(支持异步)
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key")
# 3. 定义异步批处理函数
async def async_batch_with_cache(prompts):
# 异步批量调用,自动利用缓存
tasks = [llm.ainvoke(prompt) for prompt in prompts]
return await asyncio.gather(*tasks)
# 4. 测试(包含重复请求,触发缓存)
prompts = [
"介绍LangChain", "介绍LangChain", # 重复请求,缓存命中
"介绍缓存优化", "介绍异步批处理"
]
# 运行异步批处理
results = asyncio.run(async_batch_with_cache(prompts))
for prompt, result in zip(prompts, results):
print(f"请求:{prompt}\n结果:{result.content[:30]}\n")
代码说明:重复请求("介绍LangChain")会命中缓存,无需重新调用LLM;非重复请求通过异步批处理并行执行,兼顾缓存的高效性和批处理的高吞吐量[superscript:5]。
17.7 内存与磁盘缓存权衡
前面我们介绍了内存缓存(InMemoryCache)和磁盘缓存(SQLiteCache),实际生产中,需根据业务场景、资源限制,权衡两者的优缺点,选择合适的缓存方案------甚至可以结合使用(内存缓存作为热点缓存,磁盘缓存作为持久化备份)[superscript:2][superscript:8]。
17.7.1 核心权衡维度
选择内存缓存还是磁盘缓存,主要从以下5个维度权衡,结合业务场景做出决策[superscript:3][superscript:6]:
| 权衡维度 | 内存缓存(InMemoryCache) | 磁盘缓存(SQLiteCache) |
|---|---|---|
| 读写速度 | 极快(1-10ms),直接操作内存 | 较快(10-50ms),需读写磁盘文件 |
| 持久化能力 | 无,应用重启后缓存丢失 | 有,缓存存储在磁盘,重启不丢失 |
| 资源占用 | 占用内存,缓存量大时可能导致内存溢出 | 占用磁盘空间,对内存影响小 |
| 部署复杂度 | 无复杂度,开箱即用 | 低复杂度,仅需生成一个SQLite文件 |
| 缓存命中率 | 受内存容量限制,容量不足时命中率低 | 不受容量限制(磁盘空间足够),命中率稳定 |
17.7.2 场景化选择建议
-
开发测试场景:优先选择InMemoryCache,无需关注持久化,读写速度快,方便调试。
-
生产环境-高频热点请求:选择"内存缓存+磁盘缓存"结合------热点请求(高频重复)存在内存缓存,提升响应速度;所有请求同步到磁盘缓存,确保应用重启后缓存不丢失[superscript:8]。
-
生产环境-低频请求:优先选择SQLiteCache,无需占用大量内存,同时保证缓存持久化,减少API调用成本。
-
内存资源有限场景:选择SQLiteCache,以磁盘空间换内存,避免内存溢出。
17.7.3 内存+磁盘缓存结合(生产最佳实践)
代码示例(双重缓存,热点存内存,持久化存磁盘):
python
from langchain.globals import set_llm_cache
from langchain.cache import InMemoryCache
from langchain_community.cache import SQLiteCache
from langchain_openai import ChatOpenAI
# 1. 定义双重缓存类(内存缓存+磁盘缓存)
class DualCache(InMemoryCache):
def __init__(self, disk_cache_path=".langchain.db"):
super().__init__()
self.disk_cache = SQLiteCache(database_path=disk_cache_path)
def get(self, key):
# 先从内存缓存获取,未命中再从磁盘缓存获取
value = super().get(key)
if value is None:
value = self.disk_cache.get(key)
# 磁盘缓存命中,同步到内存缓存(下次更快)
if value is not None:
self.set(key, value)
return value
def set(self, key, value):
# 同时存入内存缓存和磁盘缓存
super().set(key, value)
self.disk_cache.set(key, value)
def delete(self, key):
# 同时删除内存和磁盘缓存
super().delete(key)
self.disk_cache.delete(key)
# 2. 启用双重缓存
set_llm_cache(DualCache())
# 3. 测试双重缓存
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key")
# 第一次调用:内存和磁盘都无缓存,调用LLM并双缓存
response1 = llm.invoke("测试双重缓存")
print("第一次调用:", response1.content[:30])
# 模拟应用重启(清空内存缓存,保留磁盘缓存)
set_llm_cache(DualCache())
# 第二次调用:内存无缓存,从磁盘缓存获取,同步到内存
response2 = llm.invoke("测试双重缓存")
print("重启后调用:", response2.content[:30])
代码说明:双重缓存结合了内存缓存的速度和磁盘缓存的持久化优势,是生产环境的最佳选择------热点请求从内存获取,响应极快;应用重启后,从磁盘缓存同步到内存,避免缓存失效[superscript:8]。
17.8 【实战】为高频查询接口添加缓存层
结合本章所学知识点,实战构建一个"高频查询接口",整合双重缓存(内存+磁盘)、TTL失效策略、异步批处理、向量检索缓存,实现高响应速度、高吞吐量、低成本的生产级接口,基于FastAPI提供API服务,可直接部署使用。
17.8.1 实战需求与技术栈
核心需求
-
接口功能:接收用户查询,返回基于产品文档的问答结果(RAG场景)。
-
性能要求:高频查询响应时间≤50ms,支持批量查询,吞吐量≥100QPS。
-
缓存要求:支持双重缓存(内存+磁盘),TTL=1小时,产品文档更新时主动失效。
-
成本要求:减少80%的LLM和向量API调用次数。
技术栈
-
核心框架:LangChain、FastAPI(提供API服务)。
-
缓存方案:双重缓存(InMemoryCache+SQLiteCache)+ TTL失效。
-
向量检索:Chroma(轻量级向量数据库)+ CacheBackedEmbeddings(嵌入缓存)。
-
LLM:ChatOpenAI(gpt-3.5-turbo),支持异步批处理。
-
依赖安装:
pip install langchain langchain\-openai langchain\-community fastapi uvicorn chromadb tiktoken。
17.8.2 完整代码实现(可直接部署)
代码分为6个模块:缓存初始化、向量检索初始化、LLM配置、核心业务逻辑、API接口、缓存失效接口,注释清晰,适配生产场景。
python
import time
import asyncio
from fastapi import FastAPI, HTTPException
from langchain.globals import set_llm_cache
from langchain.cache import InMemoryCache
from langchain_community.cache import SQLiteCache
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain.embeddings.cache import CacheBackedEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableSequence
# -------------------------- 1. 初始化双重缓存(内存+磁盘+TTL) --------------------------
class TTL DualCache(InMemoryCache):
def __init__(self, disk_cache_path=".langchain_qa.db", ttl=3600):
super().__init__()
self.disk_cache = SQLiteCache(database_path=disk_cache_path)
self.ttl = ttl # 缓存存活时间1小时(3600秒)
def get(self, key):
# 先查内存缓存
value = super().get(key)
if value is not None:
# 检查内存缓存是否过期
if time.time() - value[1] < self.ttl:
return value[0]
else:
super().delete(key) # 内存缓存过期,删除
# 内存未命中,查磁盘缓存
disk_value = self.disk_cache.get(key)
if disk_value is not None:
if time.time() - disk_value[1] < self.ttl:
# 磁盘缓存未过期,同步到内存
super().set(key, (disk_value[0], time.time()))
return disk_value[0]
else:
self.disk_cache.delete(key) # 磁盘缓存过期,删除
return None
def set(self, key, value):
# 同时存入内存(带时间戳)和磁盘(带时间戳)
super().set(key, (value, time.time()))
self.disk_cache.set(key, (value, time.time()))
def delete(self, key):
# 同时删除内存和磁盘缓存
super().delete(key)
self.disk_cache.delete(key)
# 启用双重缓存
set_llm_cache(TTL DualCache())
# -------------------------- 2. 初始化向量检索(带嵌入缓存) --------------------------
# 准备产品文档(实际场景从数据库/文件加载)
product_docs = [
"产品X:智能手表,支持心率监测、睡眠分析、蓝牙通话,续航7天",
"产品Y:无线耳机,降噪功能,续航30小时,支持快充",
"产品Z:平板电脑,10英寸屏幕,8GB内存,256GB存储,支持手写笔"
]
# 文本分块
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=10)
chunks = text_splitter.split_text("\n".join(product_docs))
# 初始化嵌入模型(带缓存)
original_embeddings = OpenAIEmbeddings(model="text-embedding-ada-002", api_key="你的API Key")
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(
original_embeddings,
TTL DualCache() # 复用双重缓存
)
# 创建向量库(Chroma)
vector_db = Chroma.from_texts(chunks, cached_embeddings, persist_directory="./chroma_db")
vector_db.persist()
# -------------------------- 3. 初始化LLM(支持异步和批处理) --------------------------
llm = ChatOpenAI(model="gpt-3.5-turbo", api_key="你的API Key", temperature=0.7)
# 构建RAG链(检索→生成)
prompt = ChatPromptTemplate.from_template(
"基于以下相关文档,简洁回答用户问题,不添加无关内容:\n{context}\n用户问题:{query}"
)
retriever = vector_db.as_retriever(k=2) # 检索前2条相关文档
rag_chain = RunnableSequence.from([
lambda x: {"context": "\n".join([doc.page_content for doc in retriever.get_relevant_documents(x)]), "query": x},
prompt,
llm
])
# -------------------------- 4. 核心业务逻辑(异步+批处理+缓存) --------------------------
# 单个查询处理(带缓存)
async def process_single_query(query):
try:
# 异步调用RAG链,自动利用缓存
response = await rag_chain.ainvoke(query)
return {"query": query, "answer": response.content}
except Exception as e:
raise HTTPException(status_code=500, detail=f"查询失败:{str(e)}")
# 批量查询处理(异步批处理+缓存)
async def process_batch_queries(queries):
tasks = [process_single_query(query) for query in queries]
return await asyncio.gather(*tasks)
# 缓存失效处理(产品文档更新时调用)
def invalidate_cache(query=None):
cache = get_llm_cache()
if query:
# 删除指定查询的缓存
cache_key = cache._key(query, llm)
cache.delete(cache_key)
else:
# 清空所有缓存(文档大规模更新时)
cache.cache.clear()
cache.disk_cache.cache.clear()
# -------------------------- 5. FastAPI API接口 --------------------------
app = FastAPI(title="LangChain高频查询接口(带缓存)")
# 单个查询接口
@app.get("/query")