前言
书接上文(AdvancedRAG预检索-查询优化(一)),我们继续一起学习 AdvancedRAG 预检索中查询优化最后一种常用的技术手段:Multi-recall(多路召回)。
Multi-recall(多路召回) 在 Advanced RAG 中主要解决 “词汇鸿沟” 和 “语义不完整性” 的核心痛点。
- 本质逻辑: “不要把所有鸡蛋放在一个篮子里” 。
- 核心流程:
- 生成机制:通过特定的 Prompt 指令(如“请生成 3 个不同视角的查询”),LLM 会分析原始 Query 的语义,生成一系列相关的衍生问题。这些衍生问题可能包括:
-
同义词替换(如“LLM” -> “大语言模型”)
-
视角转换(如“怎么用” -> “使用教程和步骤”)
-
细节补充(如“学习 Python” -> “Python 语法、学习路线、实战项目”)
- 检索与融合:每个生成的 Query 都会独立进行向量检索,召回一批文档。最后,系统会对所有召回的文档进行 去重 和 融合,形成一个更全面的候选文档池
MultiQueryRetriever
MultiQueryRetriever 是Langchain中一个关键的查询优化组件。
- 核心:解决传统向量检索的一个痛点:用户提问的表述方式与知识库中文档的实际写法存在“词汇鸿沟”,导致单一查询容易遗漏相关信息。
- 核心流程:
-
查询扩展:接收用户的原始查询(例如“怎么使用OpenClaw”),调用一个大语言模型(LLM),让其基于原问题生成多个(通常是3-5个)语义相同但表述各异的查询变体。
-
并行检索:将这些变体查询(如“OpenClaw安装步骤”、“OpenClaw部署到飞书、QQ等及时通讯工具”、“LLM平台选择和api申请流程”)同时提交给底层的向量数据库进行检索。
-
结果融合:收集所有检索返回的文档片段,进行去重、排序和合并,最终将最相关、最全面的文档集合作为上下文提供给后续的答案生成模块。
-
核心代码
# 引入日志组件查看llm在原查询的基础上生成的多个查询
import logging
logging.basicConfig()
logging.getLogger("langchain_classic.retrievers.multi_query").setLevel(logging.INFO)
retrieval_from_llm = MultiQueryRetriever.from_llm(
retriever=retriever,
llm=llm
)
我们发现,MultiQueryRetriever 默认生成3个query相关的问题。进入其源码发现默认的 Prompt 也是定义的3个。
num_queries
那么,我们如果想生成4个相关问题时怎么办?没错,就是自定义 Prompt。
# 自定义提示词模板,用于改变query生成的数量
# 关键:必须包含 {question} 作为输入变量,并要求输出按指定格式分隔。
custom_prompt_template = """
你是一名专业的研究助手。请针对以下问题,生成 4 个不同的搜索查询,以便从向量数据库中查找相关信息。每个查询应从不同的技术角度或应用层面切入。请确保查询具体、专业且与原始问题高度相关。
原始问题:{question}
请严格按照以下格式输出,每个查询单独一行,无需编号:
查询1
查询2
...
查询N
"""
# 创建 PromptTemplate 对象
custom_prompt = PromptTemplate(
input_variables=["question", "num_queries"], # 必须包含这些变量
template=custom_prompt_template
)
retrieval_from_llm = MultiQueryRetriever.from_llm(
retriever=retriever,
llm=llm,
prompt=custom_prompt # 通过自定义prompt修改生成相关问题的数量,但是只能在custom_prompt写死,而不能当做灵活变量
)
运行后,发现也确实生效了。但是问题又来了:这里是在Prompt写死了num_queries,实际开发中往往我们需要接收一个变量。我使尽浑身解数也没直接做出来😳😳😳。
MultiQueryRetriever 内部将“生成多个查询”和“执行检索”捆绑为一个固定步骤。LLMChain的提示词模板在初始化时就被确定,而 num_queries这类变量并非其标准输入变量。这是其设计模式(追求“开箱即用”)所决定的,也造就了其局限性(牺牲了部分灵活性)。
num_queries-曲线救国
哈哈,不过最终我想到了一个思路,并验证成功了:
- num_queries 在 Prompt 中定义
- Prompt 最终用在了 MultiQueryRetriever
- 那么,我从最最外部传入一个 num_queries,然后传给 PromptTemplate 不就行了
封装 create_multi_query_retriever,返回 MultiQueryRetriever.from_llm获取的 retrieval_from_llm。
# ============ 曲线救国:通过封装来实现生成query数量的灵活控制 ============
def create_multi_query_retriever(num_queries=3):
print("============ 曲线救国:通过封装来实现生成query数量的灵活控制 ============")
custom_prompt_template = f"""
你是一名专业的研究助手。请针对以下问题,生成 {num_queries} 个不同的搜索查询,以便从向量数据库中查找相关信息。每个查询应从不同的技术角度或应用层面切入。请确保查询具体、专业且与原始问题高度相关。
原始问题:{{question}}
请严格按照以下格式输出,每个查询单独一行,无需编号:
查询1
查询2
...
查询N
"""
# 创建 PromptTemplate 对象
custom_prompt = PromptTemplate(
input_variables=["question"], # 必须包含这些变量
template=custom_prompt_template
)
retrieval_from_llm = MultiQueryRetriever.from_llm(
retriever=retriever,
llm=llm,
prompt=custom_prompt # 通过自定义prompt修改生成相关问题的数量,但是只能在custom_prompt写死,而不能当做灵活变量
)
return retrieval_from_llm
# 曲线救国:控制生成的query数目
retrieval_from_llm = create_multi_query_retriever(num_queries=2)
运行一下试试,果然也达到了我们的目的。
高阶玩法
确实以上的曲线救国解决了控制 num_queries 的目的。但是 Langchain V1.0 已经是一个成熟且🐂bi 的框架。那么在企业级生产环境中我们该怎么使用 MultiQueryRetriever 呢?
答案就是自定义实现 MultiQueryRetriever。我们知道 Langchain V1.0 后 MultiQueryRetriever 被划到了 langchain_classic 中。这又是一个兼容过度包,包括之前讲的很多函数也在这个包里。Langchain 新范式鼓励我们自己实现 MultiQueryRetriever(包括其他被划到langchain_classic的模块) 整个流程,这样才具备更高的灵活性和可控性。
# 自定义实现召回函数
async def advanced_multi_query_retrieve(question: str,
llm,
base_retriever,
num_queries: int = 3) -> List[Document]:
"""
多路召回核心函数
参数完全可控,流程清晰可见
"""
# 1. 动态生成多个查询
generation_prompt = PromptTemplate.from_template("""
请针对以下问题,生成正好{num}个不同角度的搜索查询。
问题:{question}
只返回{num}个查询,每行一个:
""")
chain = generation_prompt | llm
result = await chain.ainvoke({"question": question, "num": num_queries})
# 解析生成的查询列表
generated_queries = [q.strip() for q in result.content.split('\n') if q.strip()]
print(f"生成的查询: {generated_queries}") # 完全可观测
# 2. 并行执行所有查询(提升效率)
tasks = [base_retriever.ainvoke(q) for q in generated_queries[:num_queries]]
all_results = await asyncio.gather(*tasks)
# 3. 结果去重
seen_ids = set()
unique_docs = []
for doc_list in all_results:
for doc in doc_list:
# 基于内容哈希或ID去重
doc_id = hash(doc.page_content[:200]) # 简单示例
if doc_id not in seen_ids:
seen_ids.add(doc_id)
unique_docs.append(doc)
return unique_docs[:20] # 限制返回总数
# 同步调用封装
def multi_query_retrieve(question: str, num_queries: int = 3):
import asyncio
return asyncio.run(advanced_multi_query_retrieve(
question, llm, retriever, num_queries
))
小结
| 优化方式 | 核心逻辑 | 典型技术实现 | 典型应用场景 |
|---|---|---|---|
| Enrich Question(查询丰富化) | 交互式补全。 | 意图识别 + 槽位填充 + LLM对话补全。常构建在智能体(Agent) 框架上,管理多轮状态。 | 客服问答、任务型对话(订票、查询)、需明确多个约束条件的专业领域检索。 |
| Muti-recall(多路召回) | 并行检索与融合。 | 查询改写 + 混合检索(稠密向量、稀疏词袋、BM25等) + 后期融合策略(如RRF)。 | 开放域问答、复杂事实性问题、对查全率要求高的场景,确保不遗漏关键文档片段。 |
| Question Decomposition(问题拆解) | 分而治之。 | LLM思维链(CoT)提示 + 子查询生成 + 中间答案传递与合成。 | 复杂推理问答、多步骤问题解决、涉及比较、因果推断等需要串联多个信息点的场景。 |