👋 写在前面
大家好,我是正在独立开发 “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搜索”+“本地文档”,玩法非常多!