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

从零搭建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,专门用于检索功能,方便后续复用),先添加验证代码,确认第三天存入的向量数据可用:

python 复制代码
# 从零搭建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文件中,继续添加代码,实现用户问题的向量化(和第三天文档向量化用同一个模型,确保向量维度一致,才能正常匹配):

python 复制代码
# 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中检索与问题最相关的文档片段,重点控制检索数量和匹配度阈值:

python 复制代码
# 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:检索结果优化(新手必做,提升实用性)

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

python 复制代码
# 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,优化片段粒度,代码如下:
python 复制代码
# 修改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的向量已存入):

python 复制代码
# 从零搭建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系统。

相关推荐
暮霭c12 分钟前
Al 帮我写交易策略,三道关决定能不能跑
agent·ai编程·vibecoding
沉默王二1 小时前
IDEA 爽用 Claude Code 的终极方案,太丝滑。
agent·ai编程·claude
TrisighT1 小时前
DevEco Code 写鸿蒙 ArkTS 确实快,但我试了三天后把默认引擎换成了 Cursor
ai编程·harmonyos·cursor
你好潘先生2 小时前
别再记命令了,用 yeero do 说句人话就能跑脚本,而且不烧 token
服务器·python·命令行
feiyu_gao2 小时前
一个人 + AI:246 commits 做出设计系统 CLI 的故事
前端·ai编程·交互设计
吃颗糖豆搞技术2 小时前
Harness Engineering 深度解析:从"能说"到"能做"的工程跃迁
ai编程
Agent_大师2 小时前
WebSocket 行情重连成功,K线缺口不会自动消失
python
荣码2 小时前
LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑
java·python
copyer_xyf2 小时前
FastAPI 如何连接 MySQL
后端·python