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

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

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值