《从零搭建RAG系统第4天:问题向量化+Milvus检索匹配+结果优化》

0 阅读15分钟

从零搭建RAG系统第4天:问题向量化 + Milvus检索匹配 + 结果优化【老赵全栈实战】

大家好,我是老赵,一名程序员老兵,平时主要从事企业级应用开发,最近打算从零学习、并落地一套完整的RAG检索增强生成系统。不搞虚头理论,全程边学边做,把遇到的问题、踩过的坑、能直接跑通的代码,全都真实记录下来。

前三天我们一步步完成了RAG的基础铺垫:Day1搭建Miniconda的rag-env开发环境;Day2用Docker部署Milvus向量数据库,搞定镜像拉取坑;Day3实现文档加载、文本向量化,并将向量成功存入Milvus,打通了RAG的数据输入链路。

今天,我们进入RAG最核心的环节——检索匹配。简单说,就是模拟用户提问,将用户问题向量化,然后从Milvus中检索出与问题最相关的文档片段(也就是我们第三天存入的内容),再对检索结果进行简单优化,为后续接入大模型生成回答打下基础。

全程依旧保持“实战优先”的节奏,所有代码可直接复制执行,重点记录遇到的坑(匹配度偏低),和前三天风格、环境完全衔接,新手可直接复刻,跟着一步步落地完整RAG检索流程。本系列会持续更新,最终整理成完整教程+源码+部署手册,覆盖RAG全流程。

一、今日目标

  1. 衔接第三天环境,确认Milvus中已存入文档向量(验证数据可用性);
  2. 实现用户问题向量化(复用bge-small-zh模型,和文档向量化保持一致);
  3. 基于pymilvus实现Milvus检索功能,获取与问题最相关的文档片段;
  4. 优化检索结果(排序、过滤无效片段),提升匹配准确性;
  5. 记录2个新手必踩坑(检索无结果、匹配度偏低),附可直接解决的实战方案;
  6. 完成“问题→向量化→检索→结果优化”的完整检索链路,验证RAG核心流程可用性。

二、前置准备(必做,衔接前三天环境)

  1. 环境前提:

    1. 激活Miniconda的rag-env环境(终端前缀显示(rag-env)),未激活则执行:conda activate rag-env
    2. 确保Docker的Milvus容器正常运行,执行docker ps,查看milvus-standalone容器的状态,状态为“Up”即可;若未启动,执行docker start milvus-standalone
    3. 确认第三天存入的向量数据可用:Milvus中存在rag_test_collection集合,且有文档向量(后续会先验证)。
  2. 依赖前提:

    1. 前三天安装的所有依赖均可用(langchain、pymilvus、sentence-transformers、python-dotenv、chardet);
    2. bge-small-zh模型已下载(第三天首次运行时已自动下载,无需重复操作)。
  3. 物料准备:

    1. 第三天创建的doc_load_vector.py文件(可复用部分代码,无需重新编写);
    2. 模拟用户问题(3-5个,贴合第三天存入的文档内容,示例:“RAG是什么?”、“为什么通用的基础大模型基本无法满足实际业务需求?”、“RAG解决了什么问题?”)。

三、实战步骤(可直接复制代码,全程闭环,新手零门槛)

步骤1:验证Milvus中的向量数据(必做,避免检索无数据)

首先创建一个新的Python文件(命名为question_retrieval.py,专门用于检索功能,方便后续复用),先添加验证代码,确认第三天存入的向量数据可用:

# 从零搭建RAG系统第4天:问题向量化 + Milvus检索匹配
# 作者:老赵全栈实战
# 环境:Miniconda(rag-env)+ Milvus v2.6.9

# 1. 导入所需模块(复用前三天的依赖,无需额外安装)
from pymilvus import connections, Collection, utility

# 2. 连接Milvus向量数据库(和Day2、Day3的连接参数一致)
connections.connect(
    alias="default",
    host="localhost",
    port="19530"
)

# 3. 验证Milvus中的集合和数据(确认Day3存入的数据可用)
collection_name = "rag_test_collection"  # 和Day3创建的集合名称一致

# 检查集合是否存在
if not utility.has_collection(collection_name):
    print("Error:Milvus中未找到集合!请先执行Day3的代码,确保向量存入成功。")
else:
    # 加载集合到内存(检索前必须加载)
    collection = Collection(name=collection_name)
    collection.load()

    # 查看集合中的向量数量(和Day3存入的数量一致,即为成功)
    vector_count = collection.num_entities
    print(f"Milvus集合验证成功!")
    print(f"集合名称:{collection_name}")
    print(f"存入的向量数量:{vector_count}")

执行Python文件(终端处于rag-env环境),输入命令:

python question_retrieval.py

✅ 验证成功标志:终端打印出集合名称和向量数量,数量和第三天存入的文档片段数一致(无报错);若报错“未找到集合”,请重新执行第三天的doc_load_vector.py文件,确保向量存入成功。

步骤2:实现用户问题向量化(复用bge-small-zh模型)

question_retrieval.py文件中,继续添加代码,实现用户问题的向量化(和第三天文档向量化用同一个模型,确保向量维度一致,才能正常匹配):

# 4. 初始化bge-small-zh模型(复用Day3的模型,无需重新下载)
from sentence_transformers import SentenceTransformer

embedding_model = SentenceTransformer('BAAI/bge-small-zh')

# 5. 模拟用户问题(可自定义3-5个,贴合Day3存入的文档内容)
user_questions = [
    "RAG是什么?",
    "为什么通用的基础大模型基本无法满足实际业务需求?",
    "RAG解决了什么问题?",
]

# 6. 选择一个问题进行测试(先测试1个,后续可批量处理)
test_question = user_questions[0]
print(f"\n测试用户问题:{test_question}")

# 7. 问题向量化(和文档向量化逻辑一致,维度512)
question_embedding = embedding_model.encode(test_question)

print(f"问题向量化成功!向量维度:{len(question_embedding)}")  # 输出512,即为成功
print(f"问题向量(前10位):{question_embedding[:10]}")

再次执行question_retrieval.py文件,若打印出问题向量维度为512,说明问题向量化成功。核心注意:问题向量化必须和文档向量化用同一个模型,否则维度不一致,无法在Milvus中检索。

步骤3:基于Milvus实现检索匹配(核心步骤)

继续在question_retrieval.py文件中添加代码,配置检索参数,从Milvus中检索与问题最相关的文档片段,重点控制检索数量和匹配度阈值:

# 8. 配置Milvus检索参数(新手可直接复用,无需修改)
search_params = {
    "metric_type": "L2",  # 距离度量方式,和Day3创建索引时一致(L2欧氏距离)
    "params": {"nprobe": 10}  # 检索参数,nprobe越小,检索越快;越大,匹配越精准(新手设10即可)
}

# 9. 执行检索(核心代码)
# 参数说明:
# - data:问题向量(需用列表包裹,Milvus要求格式)
# - anns_field:检索的向量字段(和Day3集合定义的向量字段一致)
# - limit:检索返回的最相关片段数量(新手设3-5个即可,太多易冗余)
# - param:检索参数(上面配置的search_params)
# - output_fields:检索返回的字段(需包含doc_text,用于查看匹配的文档内容)
results = collection.search(
    data=[question_embedding],
    anns_field="doc_embedding",
    limit=3,
    param=search_params,
    output_fields=["doc_text"]
)

# 10. 解析检索结果(提取匹配的文档片段和匹配度)
print(f"\n检索到与问题最相关的{len(results[0])}个文档片段:")
for i, result in enumerate(results[0]):
    # 匹配度:L2距离越小,匹配度越高(距离为0时完全匹配)
    distance = result.distance
    # 匹配的文档文本
    doc_text = result.entity.get("doc_text")
    print(f"\n第{i+1}个匹配片段(距离:{distance:.4f}):")
    print(f"文档内容:{doc_text}")

再次执行question_retrieval.py文件,若能打印出3个匹配的文档片段和对应的匹配度,说明检索功能实现成功。补充说明:L2距离越小,代表文档片段和用户问题越相关。

步骤4:检索结果优化(新手必做,提升实用性)

检索出结果后,需要过滤无效片段、按匹配度排序(默认已按距离升序排序),同时添加匹配度阈值,只保留匹配度高的片段,避免无效结果干扰后续大模型生成。继续在文件中添加代码:

# 11. 结果优化:过滤匹配度过低的片段,只保留有效结果
# 设定匹配度阈值(L2距离≤0.5,可根据实际情况调整)
threshold = 0.6
optimized_results = []

for result in results[0]:
    distance = result.distance
    doc_text = result.entity.get("doc_text")
    # 过滤距离大于阈值的片段(匹配度太低,无效)
    if distance <= threshold:
        optimized_results.append({
            "匹配度": round(distance, 4),
            "文档片段": doc_text
        })

# 12. 打印优化后的结果(后续接入大模型,直接使用这个优化后的列表即可)
print(f"\n优化后的检索结果(匹配度阈值≤{threshold}):")
if optimized_results:
    for i, res in enumerate(optimized_results, 1):
        print(f"\n第{i}个有效片段:")
        print(f"匹配度:{res['匹配度']}")
        print(f"文档片段:{res['文档片段']}")
else:
    print(f"未检索到有效结果,请调整匹配度阈值或修改用户问题(贴合文档内容)。")

# 13. 批量检索(可选,测试多个用户问题)
print(f"\n=== 批量检索测试 ===")
for question in user_questions:
    q_embedding = embedding_model.encode(question)
    q_results = collection.search(
        data=[q_embedding],
        anns_field="doc_embedding",
        limit=2,
        param=search_params,
        output_fields=["doc_text"]
    )
    # 过滤无效结果
    q_optimized = [{"匹配度": round(r.distance,4), "文档片段": r.entity.get("doc_text")}
                   for r in q_results[0] if r.distance <= threshold]
    print(f"\n用户问题:{question}")
    if q_optimized:
        for i, res in enumerate(q_optimized, 1):
            print(f"  第{i}个有效片段:匹配度{res['匹配度']},内容:{res['文档片段'][:50]}...")
    else:
        print(f"  未检索到有效结果")

# 14. 关闭Milvus连接(可选,后续开发可保持连接)
connections.disconnect("default")

执行文件后,会打印出优化后的检索结果和批量检索测试结果,无效片段已被过滤,后续接入大模型时,直接将优化后的“文档片段+用户问题”传入即可,提升回答的准确性。

四、今天踩的坑

检索结果匹配度偏低(距离过大,内容不相关)

问题触发操作

检索能获取到结果,但匹配度距离过大,检索出的文档片段和用户问题无关,比如问“RAG的核心目标”,检索出的是“Milvus的部署方法”。

问题现象
  1. 检索结果的L2距离均>0.6,匹配度极低;
  2. 文档片段和用户问题无关联,无法用于后续大模型生成回答;
  3. 批量检索时,所有问题的匹配结果都不准确。
原因
  1. 文档拆分不合理:第三天拆分文档时,chunk_size设置过大或过小,导致片段上下文不连贯,无法和问题匹配;
  2. 检索参数nprobe过大:导致检索范围过宽,引入无关向量;
  3. 文档内容过少或过于笼统:第三天存入的文档文本过短,没有覆盖用户问题的关键词;
  4. 模型选择不当:bge-small-zh是轻量模型,匹配精度有限(后续可替换为更精准的模型,新手先解决基础问题)。
最终可用解决方案(按优先级执行)
  1. 重新拆分文档(修改第三天的代码):调整chunk_size和chunk_overlap,优化片段粒度,代码如下:
# 修改Day3 doc_load_vector.py中的拆分参数 
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=150, # 适当缩小,让片段更聚焦 
chunk_overlap=30, # 增加重叠长度,保证上下文连贯
length_function=len)

修改后,重新执行Day3的doc_load_vector.py文件,覆盖Milvus中的数据,再重新检索。

  1. 调整检索参数nprobe:将nprobe改为5-10,缩小检索范围,提升匹配精度;
  2. 补充文档内容:修改第三天的test_rag.txt,增加和用户问题相关的关键词(比如增加“RAG的核心目标是解决大模型幻觉”),重新执行Day3代码,更新Milvus数据;

五、完整核心代码汇总(新手可直接复制保存)

将上述所有代码整合,完整的question_retrieval.py文件代码(无需修改,直接执行即可,前提是Day3的向量已存入):

# 从零搭建RAG系统第4天:问题向量化 + Milvus检索匹配
# 作者:老赵全栈实战
# 环境:Miniconda(rag-env)+ Milvus v2.6.9

# 1. 导入所需模块(复用前三天的依赖,无需额外安装)
from pymilvus import connections, Collection, utility

# 2. 连接Milvus向量数据库(和Day2、Day3的连接参数一致)
connections.connect(
    alias="default",
    host="localhost",
    port="19530"
)

# 3. 验证Milvus中的集合和数据(确认Day3存入的数据可用)
collection_name = "rag_test_collection"  # 和Day3创建的集合名称一致

# 检查集合是否存在
if not utility.has_collection(collection_name):
    print("Error:Milvus中未找到集合!请先执行Day3的代码,确保向量存入成功。")
else:
    # 加载集合到内存(检索前必须加载)
    collection = Collection(name=collection_name)
    collection.load()

    # 查看集合中的向量数量(和Day3存入的数量一致,即为成功)
    vector_count = collection.num_entities
    print(f"Milvus集合验证成功!")
    print(f"集合名称:{collection_name}")
    print(f"存入的向量数量:{vector_count}")

# 4. 初始化bge-small-zh模型(复用Day3的模型,无需重新下载)
from sentence_transformers import SentenceTransformer

embedding_model = SentenceTransformer('BAAI/bge-small-zh')

# 5. 模拟用户问题(可自定义3-5个,贴合Day3存入的文档内容)
user_questions = [
    "RAG是什么?",
    "为什么通用的基础大模型基本无法满足实际业务需求?",
    "RAG解决了什么问题?",
]

# 6. 选择一个问题进行测试(先测试1个,后续可批量处理)
test_question = user_questions[0]
print(f"\n测试用户问题:{test_question}")

# 7. 问题向量化(和文档向量化逻辑一致,维度512)
question_embedding = embedding_model.encode(test_question)

print(f"问题向量化成功!向量维度:{len(question_embedding)}")  # 输出512,即为成功
print(f"问题向量(前10位):{question_embedding[:10]}")

# 8. 配置Milvus检索参数(新手可直接复用,无需修改)
search_params = {
    "metric_type": "L2",  # 距离度量方式,和Day3创建索引时一致(L2欧氏距离)
    "params": {"nprobe": 10}  # 检索参数,nprobe越小,检索越快;越大,匹配越精准(新手设10即可)
}

# 9. 执行检索(核心代码)
# 参数说明:
# - data:问题向量(需用列表包裹,Milvus要求格式)
# - anns_field:检索的向量字段(和Day3集合定义的向量字段一致)
# - limit:检索返回的最相关片段数量(新手设3-5个即可,太多易冗余)
# - param:检索参数(上面配置的search_params)
# - output_fields:检索返回的字段(需包含doc_text,用于查看匹配的文档内容)
results = collection.search(
    data=[question_embedding],
    anns_field="doc_embedding",
    limit=3,
    param=search_params,
    output_fields=["doc_text"]
)

# 10. 解析检索结果(提取匹配的文档片段和匹配度)
print(f"\n检索到与问题最相关的{len(results[0])}个文档片段:")
for i, result in enumerate(results[0]):
    # 匹配度:L2距离越小,匹配度越高(距离为0时完全匹配)
    distance = result.distance
    # 匹配的文档文本
    doc_text = result.entity.get("doc_text")
    print(f"\n第{i+1}个匹配片段(距离:{distance:.4f}):")
    print(f"文档内容:{doc_text}")


# 11. 结果优化:过滤匹配度过低的片段,只保留有效结果
# 设定匹配度阈值(L2距离≤0.5,可根据实际情况调整)
threshold = 0.6
optimized_results = []

for result in results[0]:
    distance = result.distance
    doc_text = result.entity.get("doc_text")
    # 过滤距离大于阈值的片段(匹配度太低,无效)
    if distance <= threshold:
        optimized_results.append({
            "匹配度": round(distance, 4),
            "文档片段": doc_text
        })

# 12. 打印优化后的结果(后续接入大模型,直接使用这个优化后的列表即可)
print(f"\n优化后的检索结果(匹配度阈值≤{threshold}):")
if optimized_results:
    for i, res in enumerate(optimized_results, 1):
        print(f"\n第{i}个有效片段:")
        print(f"匹配度:{res['匹配度']}")
        print(f"文档片段:{res['文档片段']}")
else:
    print(f"未检索到有效结果,请调整匹配度阈值或修改用户问题(贴合文档内容)。")

# 13. 批量检索(可选,测试多个用户问题)
print(f"\n=== 批量检索测试 ===")
for question in user_questions:
    q_embedding = embedding_model.encode(question)
    q_results = collection.search(
        data=[q_embedding],
        anns_field="doc_embedding",
        limit=2,
        param=search_params,
        output_fields=["doc_text"]
    )
    # 过滤无效结果
    q_optimized = [{"匹配度": round(r.distance,4), "文档片段": r.entity.get("doc_text")}
                   for r in q_results[0] if r.distance <= threshold]
    print(f"\n用户问题:{question}")
    if q_optimized:
        for i, res in enumerate(q_optimized, 1):
            print(f"  第{i}个有效片段:匹配度{res['匹配度']},内容:{res['文档片段'][:50]}...")
    else:
        print(f"  未检索到有效结果")

# 14. 关闭Milvus连接(可选,后续开发可保持连接)
connections.disconnect("default")

六、今日核心总结

  1. 检索匹配的核心前提:问题向量化和文档向量化必须用同一个模型,否则维度不一致,无法检索;
  2. Milvus检索的关键参数:nprobe(检索范围)和threshold(匹配度阈值),新手可按本文默认值配置,后续逐步优化;
  3. 检索结果优化是刚需:过滤低匹配度片段,能大幅提升后续大模型生成回答的准确性,避免无效信息干扰;
  4. 今日已打通RAG的检索链路:问题→向量化→Milvus检索→结果优化,后续只需接入大模型,即可完成完整RAG问答。

七、明日计划

在Windows环境下安装Ollama,并在本地运行LLM模型接入,为后续问题答案生成提供服务。

我是老赵,一名程序员老兵,全程真实记录RAG从零搭建全过程。本系列会持续更新,最后整理成完整视频教程+源码+部署手册。

关注 老赵全栈实战,不迷路,一起从0落地RAG系统。