Yo bro!想我了吗?😏
今天懒懒课代表要带大家回顾第11讲和第12讲的内容——如何让你的RAG系统跑得更快更稳!🚀
在前面的课程中,我们学习了通过多节点组、多路召回等策略提升RAG系统的召回效果。但是!效果提升的同时也带来了新的问题:
🙋♂️“系统启动太慢了,每次都要重新加载文档...”
🙋♂️“检索响应时间太长,用户体验不好...”
🙋♂️“大模型推理速度跟不上...”
可见,打造一个高性能的RAG系统,优化是必不可少的环节!第9、10讲正是为此而来——手把手教你如何加速你的RAG~
持久化存储,告别漫长的冷启动
为什么需要持久化?
传统RAG系统将所有数据存在内存中,导致:
📌系统重启后数据丢失
📌每次启动都要重新处理文档
📌内存资源浪费严重
解决方案就是——向量数据库!LazyLLM原生支持两种存储后端:
实测对比我们对三种存储进行了性能测试(文档量1614个节点):
使用Milvus后,二次启动时间直接节省了近90%!这效率提升,爱了爱了~🔝
高效检索,向量索引的魔法
前面我们讲了持久化存储能提升系统启动速度,那检索响应速度慢怎么办?答案是:索引!
我们可以把索引想象成一本书的目录,查“retrieve”这种词,不用从 A 翻到 Z,只要按字母跳转几次,瞬间定位。没有索引时,搜索过程是线性扫描(O(n)),而有了索引之后,能将搜索时间大幅压缩到 O(log n) 或 O(m)。
以字典为例,假设有 26 个字母开头的 N 个单词,我们要查找“retrieve”:
- 线性搜索:得翻过前面 17*N 个单词,效率感人💀
- 字典树索引:按字母逐层匹配,查一次最多只需 m 步(m 为单词长度),比如 “r-e-t-r-i-e-v-e” 一共 8 步 + 每步最多 26 次比对,量化下来提升高达 95%以上!
LazyLLM里的索引系统
LazyLLM 提供了内置的 DefaultIndex 和 SmartEmbeddingIndex 两种索引方案,并支持用户自定义索引结构,只需实现 3 个核心方法:update、remove 和 query。对,就是这么简单粗暴!
与某些“拐弯抹角”的框架(比如 LlamaIndex)不同,LazyLLM 的索引就是索引,不用绕来绕去把 Index 变成什么 retriever 或 query_engine,再倒回来才能用。逻辑直白,工程师狂喜!来看个对比👇
所以在 LazyLLM 中,索引就是检索的第一步,检索器才是调度大脑。你可以组合不同的索引策略、相似度算法、节点分组逻辑,搭建属于自己的检索系统,灵活又强大。
检索速度起飞的秘密武器
说完了基础索引,我们再来看看RAG 真正跑得快的关键:向量索引。当你的检索器不是按关键词查找,而是“我和它语义像不像”的时候,背后依靠的就是向量相似度——用 embedding 向量去比“谁更接近”。
🤔问题来了:数据多了怎么办?比如百万条知识块,每条都算一次余弦相似度,谁都吃不消...这时候就要靠向量索引出马了!
为什么用向量数据库?
其实我们可以简单地理解:
向量数据库 = 向量索引 + 数据存储 + 快速搜索API
比如我们在 LazyLLM 中配置:
store_conf = {
'type': 'milvus',
'kwargs': {
'uri': "dbs/test.db",
'index_kwargs': {
'index_type': 'HNSW',
'metric_type': 'COSINE'
}
}
}
这就已经在背后创建了一个基于 HNSW 的高性能向量索引,再多数据也能“毫秒级”查出最相关的几个。
不同索引算法适合什么场景?
实际测试里,HNSW 的查询时间能比线性搜索快 95%以上,而且精度几乎不打折。
Milvus实战
向量索引很强大,但是从0开始手搓一个高性能索引成本比较高。实际研发过程中,我们只需要使用这些高性能的向量数据库,便可实现高性能的向量检索!
Milvus 是啥?为什么要用它?
Milvus 是一款高性能、分布式的向量数据库,天然支持大规模稠密向量、稀疏向量、二进制向量的索引与检索。通俗点说,它就是为语义检索而生的神器:
持久化 ✔️
秒级响应 ✔️
分布式扩展 ✔️
支持复杂过滤 ✔️
关键是:LazyLLM 已完美适配 Milvus!你不需要再去啃 Milvus 的官方文档,只要几行配置,直接用!
一键接入 Milvus 索引,只需配置store_conf
LazyLLM 的索引系统支持将 Milvus 作为后端,只需设置好 indices 字段👇
store_conf = {
'type': 'map', # 数据仍然保存在本地 mapStore 中
'indices': {
'smart_embedding_index': {
'backend': 'milvus', # 指定索引使用 milvus 后端
'kwargs': {
'uri': 'dbs/test.db',
'index_kwargs': {
'index_type': 'HNSW',
'metric_type': 'COSINE',
}
},
},
},
}
只需传入 'smart_embedding_index' 索引名,配好 index_type 和 metric_type,就能立即享受 HNSW 索引的极速体验!
实测效果:速度直接提升近 87%
我们用默认索引 vs Milvus 索引做了简单测试👇
query: "证券监管?", default time: 0.164s
query: "证券监管?", milvus time: 0.021s
是不是感受到什么叫真正的“提速”?用了 Milvus,检索像闪电一样!
更强的是:支持稠密 + 稀疏向量混合召回!
还记得前面说的多路召回吗?LazyLLM 也支持给 Milvus 配多个 embedding source,同时建立多种索引👇
import lazyllm
from lazyllm import bind, deploy
milvus_store_conf = {
'type': 'milvus',
'kwargs': {
'uri': "milvus.db",
'index_kwargs': [
{
'embed_key': 'bge_m3_dense',
'index_type': 'IVF_FLAT',
'metric_type': 'COSINE',
},
{
'embed_key': 'bge_m3_sparse',
'index_type': 'SPARSE_INVERTED_INDEX',
'metric_type': 'IP',
}
]
},
}
bge_m3_dense = lazyllm.TrainableModule('bge-m3')
bge_m3_sparse = lazyllm.TrainableModule('bge-m3').deploy_method((deploy.AutoDeploy, {'embed_type': 'sparse'}))
embeds = {'bge_m3_dense': bge_m3_dense, 'bge_m3_sparse': bge_m3_sparse}
document = lazyllm.Document(dataset_path='/path/to/your/document',
embed=embeds,
store_conf=milvus_store_conf)
document.create_node_group(name="block", transform=lambda s: s.split("\n") if s else '')
bge_rerank = lazyllm.TrainableModule("bge-reranker-large")
with lazyllm.pipeline() as ppl:
with lazyllm.parallel().sum as ppl.prl:
ppl.prl.retriever1 = lazyllm.Retriever(doc=document,
group_name="block",
embed_keys=['bge_m3_dense'],
topk=3)
ppl.prl.retriever = lazyllm.Retriever(doc=document,
group_name="block",
embed_keys=['bge_m3_sparse'],
topk=3)
ppl.reranker = lazyllm.Reranker(name='ModuleReranker',model=bge_rerank, topk=3) | bind(query=ppl.input)
ppl.formatter = (
lambda nodes, query: dict(
context_str=[node.get_content() for node in nodes],
query=query)
) | bind(query=ppl.input)
ppl.llm = lazyllm.OnlineChatModule().prompt(lazyllm.ChatPrompter(instruction=prompt, extra_keys=['context_str']))
webpage = lazyllm.WebModule(ppl, port=23492).start().wait()
- 稠密向量走 IVF + Cosine
- 稀疏向量走 倒排索引 + 内积
组合使用,召回又准又快,还能配合 reranker 进行重排,提升最终输出质量!
真的可以“0代码修改”接入Milvus存储!
如果你直接把 Milvus 作为 store_conf['type'],LazyLLM 会自动读取索引配置,不需要再写 indices 字段,简洁清爽!
store_conf = {
'type': 'milvus',
'kwargs': {
'uri': "dbs/milvus1.db",
'index_kwargs': {
'index_type': 'HNSW',
'metric_type': 'COSINE',
}
}
}
用Retriever(..., index='smart_embedding_index') 一调就能用了!
远程服务端点的接入也超简单!
通过 Docker,2行命令就能本地跑起来:
curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.sh
bash standalone_embed.sh start
默认监听 19530 端口,还可以用 user.yaml 自定义配置。
推理加速,让大模型飞起来
量化技术
通过降低模型精度来减少计算量:
- Qwen2-72B原模型:需要144GB显存
- AWQ量化后:仅需48GB显存
- 性能损失<1%,速度提升30-40%
更好的推理框架在模型不变的情况下,通过选择更好的推理框架,也可以提升大模型的推理性能。LazyLLM支持多种推理框架:
开发者可以根据自己的实际需求,灵活选择推理框架,以实现最适合自己的模型推理体验!
启动量化模型只需一行代码:
llm = TrainableModule('Qwen2-72B-Instruct-AWQ').deploy_method(deploy.vllm)
工程优化,让检索不排队
缓存机制 + 并行执行,看看如何让你的RAG系统“快得聪明,快得合理”。
记忆力上线!用K-V缓存让热门问题一键秒答
很多RAG系统都陷入一个误区:只重模型效果,不重查询效率。结果就是,每个问题系统都从头来一遍,就像每天出门都要重新背单词——太累啦!
所以我们加入了“缓存大脑”:用一个K-V缓存字典来存储检索结果。
实践:模拟KV缓存加速检索
使用简单的k-v dict模拟缓存机制,我们搭建一个从RAG系统启动至检索的过程,并设置kv字典用于保存检索过的query与节点集合,并测试有无缓存机制时系统的检索时间:
milvus_store_conf = {
'type': 'map',
'indices': {
'smart_embedding_index': {
'backend': 'milvus',
'kwargs': {
'uri': "dbs/test_cache.db",
'index_kwargs': {
'index_type': 'HNSW',
'metric_type': 'COSINE',
}
},
},
},
}
dataset_path = os.path.join(DOC_PATH, "test")
docs = lazyllm.Document(
dataset_path=dataset_path,
embed=embedding_model,
store_conf=milvus_store_conf
)
docs.create_node_group(name='sentence', parent="MediumChunk", transform=(lambda d: d.split('。')))
retriever1 = lazyllm.Retriever(docs, group_name="MediumChunk", topk=6, index='smart_embedding_index')
retriever2 = lazyllm.Retriever(docs, group_name="sentence", target="MediumChunk", topk=6, index='smart_embedding_index')
retriever1.start()
retriever2.start()
reranker = Reranker('ModuleReranker', model=rerank_model, topk=3)
# 设置固定query
query = "证券管理的基本规范?"
# 运行5次没有缓存机制的检索流程,并记录时间
time_no_cache = []
for i in range(5):
st = time.time()
nodes1 = retriever1(query=query)
nodes2 = retriever2(query=query)
rerank_nodes = reranker(nodes1 + nodes2, query)
et = time.time()
t = et - st
time_no_cache.append(t)
print(f"No cache 第 {i+1} 次查询耗时:{t}s")
# 定义dict[list],存储已检索的query和节点集合,实现简易的缓存机制
kv_cache = defaultdict(list)
for i in range(5):
st = time.time()
#如果query未在缓存中,则执行正常的检索流程,若query命中缓存,则直接取缓存中的节点集合
if query not in kv_cache:
nodes1 = retriever1(query=query)
nodes2 = retriever2(query=query)
rerank_nodes = reranker(nodes1 + nodes2, query)
# 检索完毕后,缓存query及检索节点
kv_cache[query] = rerank_nodes
else:
rerank_nodes = kv_cache[query]
et = time.time()
t = et - st
time_no_cache.append(t)
print(f"KV cache 第 {i+1} 次查询耗时:{t}s")
测试结果
✅缓存机制对“高频问题”的性能优化是碾压式的,系统从“反应慢”秒变“秒答王者”。
我全都要!用并行召回提高效率
你可能没注意:很多多路召回系统,其实是依次执行的!
比如我们定义了两个检索器 retriever1 和 retriever2,常见代码:
nodes1 = retriever1(query=query)
nodes2 = retriever2(query=query)
这样执行其实是串行的,慢!
**解决方案是什么?
使用 LazyLLM 的 parallel() 实现多路召回并行化!**
with lazyllm.parallel().sum as prl:
prl.r1 = retriever1
prl.r2 = retriever2
prl(query) # 并行执行!
实测时间对比
看起来时间差不到 0.02 秒?但别小看它:在大模型前处理、并发请求量上来时,这种优化可以显著提升吞吐效率!
集大成者!缓存 + 并行 + LLM 一条龙响应
我们最后将所有优化组合起来,构建一个响应更快、结构更优的RAG系统:
milvus_store_conf = {
'type': 'milvus',
'kwargs': {
'uri': "dbs/test_rag.db",
'index_kwargs': {
'index_type': 'HNSW',
'metric_type': 'COSINE',
}
}
}
dataset_path = os.path.join(DOC_PATH, "test")
# 定义kv缓存
kv_cache = defaultdict(list)
docs1 = lazyllm.Document(dataset_path=dataset_path, embed=embedding_model, store_conf=milvus_store_conf)
docs1.create_node_group(name='sentence', parent="MediumChunk", transform=(lambda d: d.split('。')))
prompt = '你是一个友好的 AI 问答助手,你需要根据给定的上下文和问题提供答案。\
根据以下资料回答问题:\
{context_str} \n '
with lazyllm.pipeline() as recall:
# 并行多路召回
with lazyllm.parallel().sum as recall.prl:
recall.prl.r1 = lazyllm.Retriever(docs1, group_name="MediumChunk", topk=6)
recall.prl.r2 = lazyllm.Retriever(docs1, group_name="sentence", target="MediumChunk", topk=6)
recall.reranker = lazyllm.Reranker(name='ModuleReranker',model=rerank_model, topk=3) | lazyllm.bind(query=recall.input)
recall.cache_save = (lambda nodes, query: (kv_cache.update({query: nodes}) or nodes)) | lazyllm.bind(query=recall.input)
with lazyllm.pipeline() as ppl:
# 缓存检查
ppl.cache_check = lazyllm.ifs(
cond=(lambda query: query in kv_cache),
tpath=(lambda query: kv_cache[query]),
fpath=recall
)
ppl.formatter = (
lambda nodes, query: dict(
context_str="\n".join(node.get_content() for node in nodes),
query=query)
) | lazyllm.bind(query=ppl.input)
ppl.llm = llm.prompt(lazyllm.ChatPrompter(instruction=prompt, extro_keys=['context_str']))
w = lazyllm.WebModule(ppl, port=23492, stream=True).start().wait()
通过这两讲的实战锤炼,我们不只是学会了如何“让大模型飞起来”,更明白了:一个优秀的 RAG 系统,不只要答得准,更要答得快、跑得稳、用得爽!
从持久化存储让系统告别“冷启动焦虑”,到高效索引+并发召回让检索飞奔不排队,再到缓存机制+异步流程让响应速度起飞🚀,我们一步步,把 RAG 打造成了一个既有大脑也有肌肉的超级问答机器🧠💪
这不是简单的“优化一丢丢”,而是全链路性能大提速 × 工程实战硬核进阶!
现在,轮到你上场了!快把你调教好的高性能 RAG 系统亮出来吧!评论区、B站视频弹幕区、微信交流群的debug session欢迎你分享成果、输出疑问、畅聊灵感~让我们一起把慢吞吞的RAG卷成反应超快的智能助手🔥技术星辰大海等你来闯,一起冲!未来属于又懂原理、又敢动手的你!RAG宇宙,走起! 🔥🔥🔥
更多技术讨论请移步“LazyLLM”GZH!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。