为什么你的 RAG 总是搜不到准确结果?

17 阅读5分钟

👋 写在前面

大家好,我是正在独立开发 “AI 智能就业/人岗匹配系统” 的程序员。 之前在我的 《45天实战复盘:从零开发 AI 系统的血泪史》 中,提到了ai开发的经验。今天这篇专门把其中的核心技术点,拆碎了讲讲。

如果你对 AI 落地、RAG 优化或 Agent 开发感兴趣,欢迎去主页翻阅我的实战笔记。


🚀 RAG 中的“混合检索” (Hybrid Search)

做大模型应用(RAG)的朋友们,可能都遇到过这种人工智障时刻

用户搜:“我那个报错 NullPointerException 怎么修?”

纯向量检索:给你返回了一堆关于“编程错误处理哲学”或者是“如何优雅地捕获异常”的文章,但偏偏漏掉了那篇包含具体报错代码的文档。

为什么?因为 Embedding 模型太“聪明”了,它光顾着理解语义,忽略了那个精确的字符串匹配。

今天咱们就来聊聊解决这个问题的神器——混合检索 (Hybrid Search) ,并且用 ChromaDB + 火山引擎 手撸一个 Demo。


1. 语义检索 vs 关键字检索:到底差在哪?

在 RAG 的世界里,通常有两派“搜索流派”:

🧠 左脑流派:语义检索 (Vector Search)

这就是我们常说的 Embedding。它把文字变成一串数字(向量),计算相似度。

  • 强项:懂意图。搜“苹果手机”,它能找到“iPhone”。
  • 弱项脸盲。对于专有名词、特有名词(如 RTX 5090)、报错代码、人名,它经常匹配不准。

🔍 右脑流派:关键词检索 (Keyword Search)

这就是传统的 Ctrl+F 或者搜索引擎常用的 BM25 算法。

  • 强项死磕精确匹配。你说要找 NullPointerException,我就只找包含这串字符的。
  • 弱项:不懂变通。搜“怎么去那家汉堡店”,它可能找不到“去麦当劳的路线”。

🤝 混合检索 (Hybrid Search)

混合检索 = 向量检索 (语义) + 关键词检索 (精确) + RRF 排名融合

简单来说,就是让这两人同时去干活,然后用一种叫 RRF (Reciprocal Rank Fusion) 的算法,把两边的结果“混”在一起:谁在两边排名都靠前,谁就是最终的老大。


2. 5分钟上手 Demo (Python版)

我们不讲枯燥的数学公式,直接上代码。

这个 Demo 模拟了一个**“极简简历搜索器”**。我们将使用:

  • ChromaDB: 存向量。
  • LangChain: 负责流程编排。
  • 火山引擎 (Doubao) : 提供 Embedding 能力(国产、便宜、好用)。

🛠️ 环境准备

pip install langchain langchain-community langchain-chroma rank_bm25 volcengine

💻 核心代码

import os
from langchain_community.embeddings import VolcEngineEmbeddings
from langchain_chroma import Chroma
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain_core.documents import Document

# ================= 配置区 =================
# 替换成你在火山引擎控制台获取的 Key 和 Endpoint ID
os.environ["VOLC_ACCESSKEY"] = "你的AK"
os.environ["VOLC_SECRETKEY"] = "你的SK"
VOLC_ENDPOINT_ID = "ep-2025xxxx-xxxxx" 

# ================= 1. 准备数据 =================
# 模拟几个简历片段
docs = [
    Document(page_content="候选人A:精通 Python,熟悉 Django 框架,也就是那个Web框架。"),
    Document(page_content="候选人B:张三,做过 Java 开发,熟悉 Spring Boot。"), 
    Document(page_content="候选人C:张三丰,太极宗师,不懂代码,但养生很厉害。"),
    Document(page_content="候选人D:熟悉人工智能,做过大模型 RAG 开发。"),
]

# ================= 2. 语义检索路 (Vector) =================
print(">>> 正在初始化火山引擎 Embedding...")
embeddings = VolcEngineEmbeddings(
    volc_api_key=os.environ["VOLC_ACCESSKEY"],
    volc_secret_key=os.environ["VOLC_SECRETKEY"],
    model=VOLC_ENDPOINT_ID
)

# 使用 Chroma 建立向量索引
vectorstore = Chroma.from_documents(
    docs, 
    embeddings,
    collection_name="hybrid_demo"
)
# 生成语义检索器,只取前2名
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

# ================= 3. 关键词检索路 (Keyword) =================
# 使用 BM25 建立倒排索引
# 注意:BM25 是内存级的,不需要 Embedding,直接分词统计
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 2

# ================= 4. 混合检索 (Hybrid) =================
# EnsembleRetriever 就是那个“融合怪”
# weights=[0.5, 0.5] 代表语义和关键词各占 50% 的话语权
ensemble_retriever = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.5, 0.5]
)

# ================= 5. 对比测试 =================

def run_test(query_text):
    print(f"\n====== 🔍 搜索词:【{query_text}】 ======")
    
    print("--- 1. 纯语义结果 (Vector) ---")
    vec_res = vector_retriever.invoke(query_text)
    for doc in vec_res: print(f"  - {doc.page_content}")

    print("--- 2. 纯关键词结果 (BM25) ---")
    bm_res = bm25_retriever.invoke(query_text)
    for doc in bm_res: print(f"  - {doc.page_content}")
    
    print("--- 3. 混合检索结果 (Hybrid) ---")
    hyb_res = ensemble_retriever.invoke(query_text)
    for i, doc in enumerate(hyb_res): 
        print(f"  🏆 第{i+1}名: {doc.page_content}")

# 测试场景 1:语义模糊搜索
run_test("我想找个懂 AI 开发的人")

# 测试场景 2:精确人名搜索 (这才是混合检索的高光时刻)
run_test("张三 Java")

3. 结果分析:为什么要用混合?

如果你运行上面的代码,你会看到类似这样的现象:

场景:搜 张三 Java

  • 纯语义 (Vector) 可能会把 候选人A (Python) 甚至是 候选人C (张三丰) 找出来。因为 Embedding 觉得“张三丰”和“张三”很像,或者觉得“Python”和“Java”都是编程语言,距离很近。

  • 纯关键词 (BM25) 会精准找到 候选人B (张三) 和 候选人C (张三丰),因为他们都有“张三”这个词。但它不懂“Java”和“Spring Boot”的关系。

  • 混合检索 (Hybrid) 会结合两者的优势:

    • 候选人B 既命中了关键词“张三”,又在语义上和“Java”强相关。
    • 结果:候选人B 会稳稳地排在第一名。

4. 总结

在实际开发中,如果你的 RAG 系统经常出现“答非所问”或者“搜不到具体ID/名称”的情况,不要急着换大模型。

加一个简单的 BM25 混合检索,往往比换个 GPT-4 效果还要好,而且成本几乎为零。

小贴士:LangChain 的 EnsembleRetriever 是个好东西,它可以融合任意两个 Retriever。你甚至可以融合“Google搜索”+“本地文档”,玩法非常多!