写点什么

懒懒笔记 | 课代表带你梳理【RAG 课程 11&12:优化和加速你的 RAG】

  • 2025-06-09
    北京
  • 本文字数:6476 字

    阅读完需:约 21 分钟

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 lazyllmfrom 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.shbash 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)
# 设置固定queryquery = "证券管理的基本规范?"
# 运行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 了解!

发布于: 刚刚阅读数: 3
用户头像

用AI大模型,找商汤大装置。 2023-04-04 加入

还未添加个人简介

评论

发布
暂无评论
懒懒笔记 | 课代表带你梳理【RAG课程 11&12:优化和加速你的RAG】_AI_商汤万象开发者_InfoQ写作社区