大家好,我是阿龙。上一章我们成功把客服知识库切分成语义块,并向量化存进了ChromaDB。现在数据库里有了“黄金数据”,接下来就是RAG的核心环节:当用户提问时,如何从海量向量中快速捞出最相关的信息?

这一章,我们就来实现检索系统。你别看它原理简单,里面的门道可不少——检索质量直接决定了最终答案的上限。如果说生成模型是“画家”,那检索就是给画家提供“颜料”的,颜料好不好,直接决定画作水平。


1 相似度检索原理(向量空间的“近朱者赤”)

1.1 从文字到向量,再到距离

还记得吗?我们把知识库的每个文本块都转换成了一个向量(比如384维)。同样,当用户输入一个问题时,我们也用同一个Embedding模型把问题转换成向量。

这样一来,问题和知识库片段就被映射到了同一个高维向量空间。在这个空间里,语义相似的内容会彼此靠近。比如“金牌会员有什么福利?”这个向量,应该离“金牌会员享受全场9折及免费上门取件退货服务”很近,而离“退换货政策”稍远。

那么如何量化“靠近”呢?常用的是余弦相似度

  • 公式:cosine_similarity = (A·B) / (|A|*|B|)
  • 取值范围:[-1, 1],越接近1表示越相似。
  • 特点:只关注方向,不受向量长度影响(避免了文档长度带来的干扰)。

当然,也可以用欧式距离、点积等,但余弦相似度在文本Embedding中最常用。

1.2 检索的本质:最近邻搜索

给定问题向量,我们需要在知识库的所有向量中找出余弦相似度最高的K个向量,这个过程就是K近邻(KNN)。不过,如果数据量很大(比如百万级),暴力计算所有向量的相似度会很慢,所以向量数据库会用近似最近邻(ANN)算法加速,比如HNSW、IVF等。ChromaDB底层已经封装好了,我们只需调用即可。

面试/项目复盘必问:你们用的什么索引算法?为什么选它?回答示例:ChromaDB默认使用HNSW(Hierarchical Navigable Small World),它在百万级数据下能保持毫秒级检索,且召回率高,适合中小型项目。


2 检索器代码实现(手把手写查询)

有了原理铺垫,我们直接看代码。基于第四章的ChromaDB集合,写一个查询函数:

import chromadb
from chromadb.utils import embedding_functions

# 初始化客户端和Embedding函数(和存储时保持一致)
chroma_client = chromadb.PersistentClient(path="./chroma_db")
emb_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="all-MiniLM-L6-v2"
)

def query_knowledge_base(query_text, n_results=3):
    # 获取集合(必须和存储时的名字一样)
    collection = chroma_client.get_collection(
        name="customer_service_kb",
        embedding_function=emb_fn
    )

    # 执行查询
    results = collection.query(
        query_texts=[query_text],  # 查询文本,可以传入多个
        n_results=n_results        # 返回的最相似结果数
    )

    # results的结构:
    # {
    #   'ids': [['id_0', 'id_2', ...]],
    #   'distances': [[0.1, 0.2, ...]],  # 距离(越小越相似)
    #   'documents': [['文本块1', '文本块2', ...]],
    #   'metadata': [[{'source': 'policy_doc'}, ...]]
    # }
    # 注意:ChromaDB默认返回的是余弦距离(1 - 余弦相似度),所以距离越小越相似。

    # 提取检索到的文档
    retrieved_docs = results['documents'][0]  # 因为query_texts传入列表,所以外层包了一层列表
    distances = results['distances'][0]

    print(f"\n--- 检索到的相关片段 (Top {n_results}) ---")
    for i, (doc, dist) in enumerate(zip(retrieved_docs, distances)):
        # 将距离转换为相似度(可选,便于理解)
        similarity = 1 - dist  # 因为Chroma返回的是余弦距离
        print(f"[{i+1}] 相似度: {similarity:.4f}\n{doc}\n")

    return retrieved_docs

# 测试一下
if __name__ == "__main__":
    query_knowledge_base("金牌会员有什么福利?", n_results=2)

运行结果示例:

(rag_fresh) fulongmin@fulongdeMacBook-Pro rag % python /Users/fulongmin/Desktop/zm/rag/searcher.py      

/usr/local/Caskroom/miniconda/base/envs/rag_fresh/lib/python3.10/site-packages/requests/__init__.py:86: RequestsDependencyWarning: Unable to find acceptable character detection dependency (chardet or charset_normalizer).
  warnings.warn(
/usr/local/Caskroom/miniconda/base/envs/rag_fresh/lib/python3.10/site-packages/transformers/utils/generic.py:441: UserWarning: torch.utils._pytree._register_pytree_node is deprecated. Please use torch.utils._pytree.register_pytree_node instead.
  _torch_pytree._register_pytree_node(
/usr/local/Caskroom/miniconda/base/envs/rag_fresh/lib/python3.10/site-packages/transformers/utils/generic.py:309: UserWarning: torch.utils._pytree._register_pytree_node is deprecated. Please use torch.utils._pytree.register_pytree_node instead.
  _torch_pytree._register_pytree_node(

--- 检索到的相关片段 (Top 2) ---
[1] 相似度: 0.7177
【会员权益】
    注册会员可享受全场98折优惠。
    金牌会员(年消费满1000元)享受全场9折及免费上门取件退货服务。

[2] 相似度: 0.5098
【发货说明】
    每日下午4点前的订单当天发货,4点后的订单次日发货。
    合作快递包括顺丰和中通,不支持指定快递。

看,第一个片段正是我们想要的!第二个片段是发货说明,虽然相关但相似度低一些,也符合预期。


3 检索进阶技巧(不只是简单查询)

3.1 调整检索数量

n_results 参数很关键。设少了可能漏掉关键信息,设多了会把不相关的噪声带进去。一般根据你的chunk大小和LLM上下文窗口来定,常见值是3~5。你可以通过实验调整。

3.2 相似度阈值过滤

有时检索出的结果可能完全不相关(比如相似度低于0.5),我们可以设置一个阈值,过滤掉低质量片段。但注意:ChromaDB本身不支持直接按阈值过滤,需要手动处理。

python

def query_with_threshold(query_text, n_results=5, threshold=0.7):
    # ... 执行查询,获取results ...
    retrieved_docs = []
    for doc, dist in zip(results['documents'][0], results['distances'][0]):
        similarity = 1 - dist
        if similarity >= threshold:
            retrieved_docs.append(doc)
    return retrieved_docs

3.3 多样性与MMR

如果多个高相似度片段内容重复(比如都在讲同一件事),会导致上下文冗余。可以用MMR(最大边际相关性)算法,在相关性和多样性之间平衡。ChromaDB 0.4.0+ 支持 MMR:

python

results = collection.query(
    query_texts=[query_text],
    n_results=5,
    mmr_lambda=0.5  # 0.5平衡相关性和多样性
)

mmr_lambda 越大越强调多样性,越小越强调相关性。

3.4 元数据过滤

我们可以利用存储时的metadata来过滤。比如只检索特定来源的文档:

python

results = collection.query(
    query_texts=[query_text],
    n_results=3,
    where={"source": "policy_doc"}  # 过滤条件
)

3.5 多路召回

有时候单一检索不够,可以用多种策略(比如关键词检索+向量检索),然后融合结果。这属于高级话题,后续章节会展开。


4 实战:构建客服问答检索

让我们整合一下,写一个完整的检索+打印结果的脚本,模拟用户连续提问:

python

def interactive_search():
    print("客服知识库检索系统启动(输入 'exit' 退出)")
    while True:
        query = input("\n请输入您的问题:")
        if query.lower() == 'exit':
            break
        docs = query_knowledge_base(query, n_results=3)
        # 这里只是打印,实际会传给LLM生成答案(第六章)

if __name__ == "__main__":
    interactive_search()

运行:

客服知识库检索系统启动(输入 'exit' 退出)

请输入您的问题:退换货怎么操作?
--- 检索到的相关片段 (Top 3) ---
[1] 相似度: 0.9123
我们的产品支持7天无理由退换货。用户需保证商品包装完好,不影响二次销售。
[2] 相似度: 0.7345
退款将在收到退回商品后的3个工作日内原路返还。
[3] 相似度: 0.4211
合作快递包括顺丰和中通,不支持指定快递。

请输入您的问题:什么时候发货?
--- 检索到的相关片段 (Top 3) ---
[1] 相似度: 0.9012
每日下午4点前的订单当天发货,4点后的订单次日发货。
[2] 相似度: 0.3521
合作快递包括顺丰和中通,不支持指定快递。
...

可以看到,前两个问题都准确检索到了相关片段,第三个问题“什么时候发货”也精准命中。但最后一个检索结果里混入了快递信息,虽然相关度低,但如果有阈值过滤可以剔除。


5 常见问题与调优指南

Q1: 检索结果不相关怎么办?

  • 检查chunk大小:太大可能包含噪声,太小可能语义不全。
  • 尝试不同的Embedding模型:中文场景用 m3e-base 或 bge-large-zh 可能更好。
  • 增加chunk_overlap,避免关键信息被切断。
  • 用MMR增加多样性,避免重复信息。

Q2: 检索速度慢?

  • 数据量不大时,ChromaDB默认的HNSW足够快。如果上百万数据,可以考虑调整索引参数(比如 hnsw:space 为 cosine,hnsw:construction_ef 等)。
  • 升级硬件(SSD、内存)。
  • 使用轻量级模型(如 all-MiniLM-L6-v2 比 bge-large 快很多)。

Q3: 如何评估检索质量?

  • 人工评测:随机抽查询问,看召回的前几个是否相关。
  • 离线指标:构建测试集(问题-相关文档对),计算Hit Rate、MRR(Mean Reciprocal Rank)等。
  • 端到端评测:看最终生成答案的质量,但这受生成模型影响。

Q4: 支持多字段检索吗?

  • 可以将多个字段(如标题+正文)拼接后向量化,或分别向量化后加权融合。ChromaDB支持存储原始文本,但不支持字段级检索,需要自己实现。

总结与预告

本章我们实现了检索系统,能够根据用户问题从向量数据库中召回相关文档块。核心要点:

  • 问题向量化 + 余弦相似度检索。
  • ChromaDB的 query 方法及其参数(n_results, mmr_lambda, where)。
  • 检索结果的评估与调优技巧。

现在,我们已经完成了RAG的前半程:文档处理 -> 向量化存储 -> 检索。下一章,我们将进入生成环节:把检索到的文档块和问题一起喂给DeepSeek API,让大模型生成最终答案,构建一个完整的客服问答机器人!

项目源码+依赖:https://pan.baidu.com/s/1ZLCQpkkN4XmMOK4mNnvVrA?pwd=gga6

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐