RAG 进阶检索学习笔记
一、函数回调规范化输出
是什么?
通过 with_structured_output() 让 LLM 的返回结果强制符合指定的 Pydantic 模型结构,而不是返回普通字符串。
有什么用?
LLM 输出默认是自由文本,格式不稳定,难以程序化处理。绑定结构化输出后:
- 返回结果直接是 Python 对象,可以直接访问字段
- 字段类型和取值范围有约束,不会出现意外格式
- 常用于:意图分类、路由分发、信息抽取等任务
示例代码
python
python
from typing import Literal
import dotenv
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
dotenv.load_dotenv()
class RouteQuery(BaseModel):
"""将用户查询映射到对应的数据源上"""
datasource: Literal["python_docs", "js_docs", "golang_docs"] = Field(
description="根据用户的问题,选择哪个数据源最相关以回答用户的问题"
)
# 1. 创建绑定结构化输出的大语言模型
llm = ChatOpenAI(model="moonshot-v1-8k", temperature=0)
structured_llm = llm.with_structured_output(RouteQuery)
# 2. 提问并获取结构化结果
question = """为什么下面的代码不工作了,请帮我检查下:
var a = "123"
"""
res: RouteQuery = structured_llm.invoke(question)
print(res) # datasource='js_docs'
print(type(res)) # <class 'RouteQuery'>
print(res.datasource) # js_docs
关键知识点
| 知识点 | 说明 |
|---|---|
BaseModel |
Pydantic 基类,定义结构化输出的字段和类型 |
Literal[...] |
限制字段只能取指定枚举值,LLM 输出会被约束在这几个选项内 |
Field(description=...) |
字段描述,LLM 会参考此描述决定填什么值 |
with_structured_output() |
LangChain 方法,让 LLM 输出直接映射为指定 Pydantic 对象 |
temperature=0 |
输出确定稳定,适合分类、路由等任务 |
二、混合检索(Ensemble Retriever)
是什么?
混合检索将两种不同类型的检索器结合,通过加权融合各自的召回结果:
- BM25:基于关键词的传统检索,擅长精确词匹配
- FAISS:基于向量的语义检索,擅长理解语义相似性
- EnsembleRetriever:集成以上两者,取长补短
有什么用?
单一检索方式各有盲区:BM25 无法理解语义,向量检索可能漏掉关键词精确匹配的文档。混合检索能同时覆盖两种场景,提升整体召回率和准确性。
示例代码
python
ini
import dotenv
from langchain_classic.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_community.embeddings import QianfanEmbeddingsEndpoint
dotenv.load_dotenv()
# 1. 创建文档列表
documents = [
Document(page_content="笨笨是一只很喜欢睡觉的猫咪", metadata={"page": 1}),
Document(page_content="我喜欢在夜晚听音乐,这让我感到放松。", metadata={"page": 2}),
Document(page_content="猫咪在窗台上打盹,看起来非常可爱。", metadata={"page": 3}),
Document(page_content="我的狗喜欢追逐球,看起来非常开心。", metadata={"page": 10}),
# ... 更多文档
]
# 2. 构建 BM25 关键词检索器
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 4 # 召回 top4
# 3. 创建 FAISS 向量检索器
faiss_db = FAISS.from_documents(documents, embedding=QianfanEmbeddingsEndpoint(model='embedding-v1'))
faiss_retriever = faiss_db.as_retriever(search_kwargs={"k": 4})
# 4. 初始化集成检索器(各占 50% 权重)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, faiss_retriever],
weights=[0.5, 0.5],
)
# 5. 执行检索
docs = ensemble_retriever.invoke("除了猫,你养了什么宠物呢?")
print(docs)
print(len(docs))
关键知识点
| 知识点 | 说明 |
|---|---|
BM25Retriever |
关键词检索,基于词频统计,不依赖向量模型 |
FAISS |
Facebook 开源的高效向量检索库,支持本地运行 |
EnsembleRetriever |
集成多个检索器,通过 RRF 算法融合排序结果 |
weights=[0.5, 0.5] |
控制两个检索器的影响权重,可按需调整 |
.k = 4 |
每个检索器各自召回的文档数量 |
💡 RRF(Reciprocal Rank Fusion) :EnsembleRetriever 内部使用的结果融合算法,根据各检索器的排名加权合并,而不是简单拼接。
三、基于逻辑和语义的路由分发
是什么?
路由分发是根据用户问题的内容,自动将其导向不同的处理链路(如不同的知识库、检索器或 Agent)。结合结构化输出 + LCEL,可以构建出智能的路由系统。
有什么用?
- 多知识库场景下,不同问题应查询不同数据源
- 避免用一个大而全的检索器处理所有问题,提升效率和准确性
- 是构建多 Agent 系统 和复杂 RAG 流程的基础能力
流程图
bash
用户问题
↓
RunnablePassthrough() # 透传问题
↓
ChatPromptTemplate # 构造路由 prompt(含系统指令)
↓
LLM + structured_output # 输出结构化路由结果 RouteQuery
↓
choose_route() # 根据 datasource 字段选择对应链路
↓
返回对应的检索器 / chain
示例代码
python
python
from typing import Literal
import dotenv
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI
dotenv.load_dotenv()
class RouteQuery(BaseModel):
"""将用户查询映射到最相关的数据源"""
datasource: Literal["python_docs", "js_docs", "golang_docs"] = Field(
description="根据给定用户问题,选择哪个数据源最相关以回答他们的问题"
)
def choose_route(result: RouteQuery) -> str:
"""根据路由结果选择不同的检索器"""
if "python_docs" in result.datasource:
return "chain in python_docs"
elif "js_docs" in result.datasource:
return "chain in js_docs"
else:
return "golang_docs"
# 1. 构建 LLM 并绑定结构化输出
llm = ChatOpenAI(model="moonshot-v1-8k", temperature=0)
structured_llm = llm.with_structured_output(RouteQuery)
# 2. 创建路由链
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个擅长将用户问题路由到适当数据源的专家。\n请根据问题涉及的编程语言,将其路由到相关数据源"),
("human", "{question}")
])
router = {"question": RunnablePassthrough()} | prompt | structured_llm | choose_route
# 3. 执行路由
question = """为什么下面的代码不工作了,请帮我检查下:
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(["human", "speak in {language}"])
prompt.invoke("中文")"""
print(router.invoke(question)) # chain in python_docs
关键知识点
| 知识点 | 说明 |
|----------------------|------------------------------------------|-----------------------------|
| RouteQuery | Pydantic 模型,约束 LLM 只能输出预定义的数据源名称 |
| choose_route() | 普通函数也可以作为 LCEL 链的最后一个节点,接收上游输出 |
| ChatPromptTemplate | 支持 system + human 多角色消息,system 用于设定角色和规则 |
| `LCEL 管道符 | ` | 将 dict、prompt、llm、函数串联成完整链路 |
| 语义路由 | LLM 理解问题语义后做分类,比规则匹配更灵活 |
总结对比
| 函数回调规范化输出 | 混合检索 | 路由分发 | |
|---|---|---|---|
| 核心目标 | LLM 输出结构化 | 提升检索召回率 | 问题智能分流 |
| 关键组件 | with_structured_output |
EnsembleRetriever |
RouteQuery + choose_route |
| 典型场景 | 意图识别、信息抽取 | 企业知识库检索 | 多知识库 / 多 Agent 系统 |
| 依赖 LLM | ✅ | ❌ | ✅ |