基于大语言模型的查询扩展技术:Query2doc原理与实践

📅 2026/8/12 15:47:03
基于大语言模型的查询扩展技术:Query2doc原理与实践
1. 项目概述当传统检索遇上大语言模型信息检索IR这个领域听起来有点老派但它的核心问题一直没变用户输入一个简短的查询Query系统如何从海量文档中精准地找到最相关的内容我们常用的搜索引擎、企业内部知识库、乃至电商的商品搜索背后都是信息检索技术在支撑。传统的检索模型比如经典的BM25其工作原理可以简单理解为“词频统计文档长度惩罚”它非常高效但也非常“机械”——它只认识你输入的那几个关键词对于同义词、上下文隐含的意图、或者表述不完整的查询往往就力不从心了。举个例子用户搜索“苹果发布会新品”BM25会拼命去找同时包含“苹果”、“发布会”、“新品”这三个词的文档。但如果有一篇优质报道的标题是“库克秋季特别活动揭晓iPhone 15系列”尽管内容高度相关但因为字面匹配度低排名可能很靠后。这就是所谓的“词汇鸿沟”问题。最近几年大语言模型LLM的爆发让我们看到了新的可能性。LLM拥有强大的语言理解和生成能力它不仅能“读懂”查询还能“脑补”出更丰富、更准确的上下文。于是一个很自然的想法出现了能不能用LLM来“改造”一下用户原始的查询生成一个更详细、更规范的“伪文档”Pseudo-document再用这个“伪文档”去进行检索呢这就是“Query2doc”这个想法的核心。它本质上是一种高级的查询扩展Query Expansion技术只不过这次的“扩展”不是简单地加几个同义词而是让LLM写一小段话来描述用户的查询意图。我最近深入研读并复现了相关论文发现这个思路在实践中效果显著尤其能提升像BM25这类传统稀疏检索器的性能成本却远低于全面转向向量检索或稠密检索。对于很多既有系统来说这几乎是一个“即插即用”的升级方案。接下来我就结合自己的实操拆解一下Query2doc的完整实现思路、技术细节以及那些容易踩坑的地方。2. Query2doc的核心原理与方案设计2.1 从关键词到“小作文”思路演进传统的查询扩展方法比如基于全局或局部反馈的伪相关反馈PRF或者使用同义词词典其扩展是“离散”和“局部”的。它们只是在原始查询词的基础上增加一些额外的关键词。而Query2doc的思路是“生成式”和“整体性”的。它要求LLM根据原始查询生成一段连贯的、描述性的文本。为什么生成一段文本比单纯加词更有效这涉及到检索模型对文本的表示方式。像BM25这样的模型虽然基于词袋但它会对文档中词的分布进行建模。一段由LLM生成的、语义连贯的文本其中包含的词汇不再是孤立的而是按照自然语言逻辑组织起来的。这些词汇共同构成了对原始查询意图的一个“分布式”描述。当用这段生成的文本作为新的查询时BM25计算相关性时实际上是在匹配一个更完整、更丰富的语义“轮廓”而不仅仅是几个孤立的关键词点。这在一定程度上模拟了语义匹配但又完全兼容现有的、高效的倒排索引基础设施。2.2 系统架构与工作流程一个完整的Query2doc增强检索系统其工作流程可以清晰地分为离线和在线两个阶段下图展示了其核心架构与数据流向flowchart TD subgraph A[离线阶段] direction LR A1[文档库] -- A2[构建倒排索引] end subgraph B[在线检索阶段] direction TB B1[用户输入原始查询] -- B2[LLM Query2doc 转换器] B2 -- “生成伪文档” -- B3[新查询br生成的伪文档] B3 -- B4[检索器br如BM25] A2 -.- B4 B4 -- B5[返回排序后的相关文档] end A -- B离线阶段相对传统即对目标文档库进行预处理并构建倒排索引为高效检索做好准备。在线阶段则是Query2doc发挥作用的环节查询转换当用户提交一个原始查询如“缓解头痛的方法”时系统首先将其发送给LLM Query2doc转换器。生成伪文档LLM根据预设的指令生成一段相关的文本。例如可能生成“头痛是一种常见症状可能由压力、脱水或偏头痛引起。常见的缓解方法包括休息、补充水分、服用非处方止痛药如布洛芬或对乙酰氨基酚以及在黑暗安静的环境中放松。长期或剧烈头痛应咨询医生。”执行检索将生成的这段“伪文档”作为新的查询输入到BM25等检索器中。返回结果检索器从索引中找出与“伪文档”最相关的文档排序后返回给用户。这个流程的关键在于检索器如BM25本身没有任何改变。我们只是巧妙地改变了它的“输入”。这使得该方案具备极强的可移植性和低侵入性。2.3 方案选型背后的考量为什么选择“LLM生成 BM25检索”这个组合这里有几个关键的工程和效率考量成本与效率的平衡纯向量检索稠密检索虽然语义匹配能力更强但需要将所有文档编码为向量并建立向量索引计算和存储成本高检索速度也可能慢于倒排索引。Query2doc方案仅对每次查询调用一次LLM生成文本后续检索完全沿用高效的BM25总体延迟和成本可控。利用现有基础设施很多生产系统已经建立了庞大而稳定的倒排索引集群。Query2doc允许我们在不推翻重来的前提下显著提升检索质量技术风险低。LLM的强项与弱项LLM擅长理解和生成语言但并不擅长直接从海量数据中精确查找信息即“检索”本身。让LLM做它擅长的事文本生成让专业的检索器做它擅长的事快速匹配这是一种合理的分工。可解释性生成的“伪文档”本身可以作为中间结果帮助开发者分析和理解系统是如何“理解”用户查询的这比黑盒的向量匹配更具可解释性。3. 核心细节解析与实操要点3.1 提示词工程如何与LLM有效沟通整个方案的效果一半取决于LLM生成文本的质量而质量的核心在于提示词Prompt的设计。这里的提示词不是简单的问答而是给LLM一个明确的角色和任务指令。一个经过多次调试后效果相对稳定的提示词模板如下你是一个专业的信息检索查询扩展助手。你的任务是根据用户简短、模糊或不完整的查询生成一段简明、准确、信息丰富的描述性文本。这段文本将用于检索相关文档。 请遵循以下规则 1. 严格基于用户查询的主题和意图进行生成不要引入无关信息。 2. 生成的文本应像一段百科摘要或问题解答包含核心概念、相关实体、常见方法或属性。 3. 语言平实、客观避免主观评价和第一人称。 4. 长度控制在80-150字之间。 用户查询{user_query} 请生成描述性文本提示词设计要点解析角色设定“信息检索查询扩展助手”明确了任务边界防止LLM天马行空。规则具体化四条规则分别约束了相关性、文体、语气和长度这是产出可用文本的关键。文体要求“百科摘要或问题解答”这个类比非常有效它能引导LLM生成结构化的说明文而不是对话或故事。长度控制80-150字是一个经验值。太短可能扩展不充分太长则会引入噪声且增加后续BM25计算的无用负担。注意不同的LLM如GPT-4、Claude、国产大模型对提示词的敏感度不同。在实际应用中需要准备一个包含各种类型查询短句、关键词、问题句的测试集针对选定的LLM进行多轮提示词微调才能达到最佳效果。3.2 检索器配置BM25的参数调优虽然我们使用的是标准BM25但用生成的“伪文档”作为查询后原有的参数可能不是最优的。BM25的核心公式涉及两个主要参数k1和b。k1控制词频饱和度的参数。k1越大词频的影响越大。b控制文档长度归一化的参数。b在0到1之间b1表示完全进行长度归一化b0表示不做归一化。当查询变成长文本后词频分布变化原始查询可能只有几个词而生成的伪文档有上百个词。一些核心概念可能在伪文档中多次出现例如在关于“头痛”的生成文本中“头痛”、“缓解”、“药物”等词可能重复。这时适当调低k1值例如从默认的1.2调到0.9可以防止单个词因在生成文本中重复而获得过高的权重避免检索结果过度偏向那些恰好频繁出现生成词但实际主题不相关的文档。文档长度归一化生成的伪文档本身就是一个“文档”但其长度是固定的。参数b的调整影响的是对被检索文档长度的敏感度。如果我们的文档库中文档长度差异巨大比如既有短新闻又有长报告通常保持b在0.5-0.75之间是一个合理的起点。对于Query2doc一般不需要专门调整b除非你发现结果系统性地偏向过长或过短的文档。实操建议在实施Query2doc后建议在一个有标注的数据集如MS MARCO上对k1参数进行网格搜索例如在[0.5, 1.5]区间内以0.1为步长使用NDCG10或MAP等指标来重新确定最优的k1值。这是一个简单但有效的优化步骤。3.3 生成文本的后处理LLM生成的文本并非完美直接使用可能带来问题需要进行后处理去除无关表述LLM可能会生成“根据您的问题…”“综上所述…”等元话语。这些内容与检索主题无关必须过滤掉。可以用简单的规则如删除以“根据”、“总之”开头的句子或一个小的分类器来清洗。处理不确定性对于模糊查询LLM有时会生成包含“可能”、“或许”、“有多种原因”等不确定性词汇的文本。在大多数检索场景下保留这些内容是利大于弊的因为它们反映了查询的模糊性可能匹配更广泛的文档。除非极端情况一般不需处理。截断与分段如果生成文本超长可以按句子或标点进行截断。更高级的做法是将生成的文本分成几个语义完整的片段分别用BM25检索后再合并结果但这会显著增加计算复杂度需权衡利弊。4. 实操过程与核心环节实现4.1 环境搭建与工具选型要实现一个Query2doc的测试管道你需要以下组件LLM API/本地模型这是核心。对于快速验证推荐使用OpenAI的GPT-3.5-Turbo API成本低、响应快、质量稳定。如果对数据隐私有要求或需要离线使用可以考虑在本地部署类似Llama 3、Qwen等开源模型但需要相应的GPU资源。检索库与索引工具Elasticsearch或Apache Solr是生产级的选择它们内置了BM25实现。对于研究和快速实验Python库如rank_bm25或pyserini基于Lucene更加轻量便捷。编程语言Python是不二之选有丰富的NLP和机器学习库支持。一个最小化的依赖列表requirements.txt可能如下openai1.0.0 # 调用GPT API rank-bm250.2.2 # 纯Python的BM25实现适合实验 pydantic2.0 # 用于数据验证和设置管理 requests2.31.0 # 用于HTTP请求 tenacity8.2.0 # 用于API调用的重试装饰器4.2 端到端实现代码拆解下面我以一个使用rank_bm25和OpenAI API的简化示例展示核心代码逻辑。import os from typing import List import openai from rank_bm25 import BM25Okapi from tenacity import retry, stop_after_attempt, wait_exponential # 1. 配置LLM客户端 openai.api_key os.getenv(OPENAI_API_KEY) MODEL_NAME gpt-3.5-turbo # 2. 定义提示词模板 QUERY2DOC_PROMPT_TEMPLATE 你是一个专业的信息检索查询扩展助手。你的任务是根据用户简短、模糊或不完整的查询生成一段简明、准确、信息丰富的描述性文本。这段文本将用于检索相关文档。 请遵循以下规则 1. 严格基于用户查询的主题和意图进行生成不要引入无关信息。 2. 生成的文本应像一段百科摘要或问题解答包含核心概念、相关实体、常见方法或属性。 3. 语言平实、客观避免主观评价和第一人称。 4. 长度控制在80-150字之间。 用户查询{query} 请生成描述性文本 # 3. 带重试机制的LLM调用函数 retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min4, max10)) def generate_query_doc(query: str) - str: 调用LLM将原始查询转换为描述性文档 prompt QUERY2DOC_PROMPT_TEMPLATE.format(queryquery) try: response openai.chat.completions.create( modelMODEL_NAME, messages[{role: user, content: prompt}], temperature0.3, # 较低的温度确保生成稳定、确定性高 max_tokens200, ) generated_text response.choices[0].message.content.strip() # 简单的后处理移除可能的引导性短语 for prefix in [生成的描述性文本, 描述性文本, 根据您的查询]: if generated_text.startswith(prefix): generated_text generated_text[len(prefix):].strip() return generated_text except Exception as e: print(f生成查询文档失败查询: {query}, 错误: {e}) # 失败时降级策略返回原始查询避免整个检索失败 return query # 4. 构建检索系统类 class Query2DocRetrievalSystem: def __init__(self, documents: List[str]): 初始化检索系统。 :param documents: 文档列表每个元素是一个文档的文本内容。 self.original_docs documents # 对文档进行分词这里使用简单的空格分割生产环境应用更复杂的分词器 self.tokenized_docs [doc.split() for doc in documents] # 初始化BM25模型注意这里调整了k1参数 self.bm25 BM25Okapi(self.tokenized_docs, k10.9, b0.75) def retrieve(self, original_query: str, top_k: int 10) - List[tuple]: 执行Query2doc检索流程。 :param original_query: 用户原始查询 :param top_k: 返回最相关的K个结果 :return: 列表元素为(文档索引, BM25分数, 文档内容) # 步骤1: Query Expansion via LLM expanded_query_doc generate_query_doc(original_query) print(f原始查询: {original_query}) print(f生成文档: {expanded_query_doc[:100]}...) # 打印前100字符 # 步骤2: 对生成的文档进行分词作为新查询 tokenized_query expanded_query_doc.split() # 步骤3: 使用BM25计算相关性得分 doc_scores self.bm25.get_scores(tokenized_query) # 步骤4: 获取Top-K个结果的索引 top_indices doc_scores.argsort()[-top_k:][::-1] # 步骤5: 组装返回结果 results [] for idx in top_indices: results.append((idx, doc_scores[idx], self.original_docs[idx])) return results # 5. 使用示例 if __name__ __main__: # 模拟一个文档库 corpus [ 苹果公司于2023年9月发布了iPhone 15系列手机搭载A16仿生芯片。, 库克在秋季特别活动中介绍了新款Apple Watch和iPad。, 头痛的常见原因包括压力、睡眠不足和脱水布洛芬可以缓解症状。, 如何有效缓解偏头痛医生建议记录头痛日记并避免已知诱因。, 特斯拉发布了最新款电动汽车Model 3焕新版续航里程提升。 ] # 初始化系统 retrieval_sys Query2DocRetrievalSystem(corpus) # 测试查询 test_query 苹果发布会新品 results retrieval_sys.retrieve(test_query, top_k3) print(f\n查询 {test_query} 的检索结果:) for i, (idx, score, doc) in enumerate(results): print(f{i1}. [得分: {score:.4f}] {doc})代码关键点解读降级策略在generate_query_doc函数中如果LLM调用失败我们选择返回原始查询。这保证了系统的鲁棒性即使LLM服务暂时不可用检索功能依然能以基线水平工作。参数注入在BM25Okapi初始化时我们传入了调整后的参数k10.9。这是基于前文分析进行的优化。流程透明打印出原始查询和生成文档的前缀便于调试和观察LLM的工作效果。分词简化示例中使用了简单的空格分词split()。在实际应用中必须使用与文档索引时相同的分词器如Elasticsearch的standard analyzer或中文的jieba、HanLP等否则词项无法匹配检索会完全失效。这是实现中最容易出错的地方之一。4.3 效果评估与对比实现之后如何证明它有效我们需要一个量化的评估。通常使用信息检索的标准评测集如MS MARCO Passage Ranking或TREC Robust04。评估步骤准备数据获取评测集的文档集合、查询集合以及对应的相关性标注qrels。建立基线使用原始查询BM25默认参数进行检索计算评价指标如MRR10, NDCG10。运行Query2doc对每个查询用LLM生成伪文档再用BM25调优后参数检索计算相同指标。对比分析比较指标提升幅度。在论文报告和我的复现中在多个数据集上Query2doc能稳定带来MRR或NDCG指标上5%-15%的相对提升。除了定量指标定性分析同样重要。人工检查一些查询的检索结果前后变化你会发现对于短查询“AI发展” - 生成文本可能包含“人工智能”、“机器学习”、“深度学习”、“技术进步”、“伦理挑战”等能检索出更全面的背景性文档。对于模糊查询“苹果好吃吗” - 生成文本可能区分“作为水果的苹果的营养价值”和“苹果公司的产品评价”减少了关于科技公司的文档被误检的概率。对于包含实体关系的查询“马斯克的公司” - 生成文本会列举“特斯拉、SpaceX、Neuralink、X原推特”能一次性检索出与这些实体相关的所有文档。5. 常见问题与排查技巧实录在实际部署和调试Query2doc方案时我遇到了不少坑。这里总结一份“避坑指南”。5.1 生成质量不稳定问题现象LLM生成的文本有时偏离主题或包含事实性错误“幻觉”导致检索结果变差。排查与解决检查提示词这是首要原因。确保提示词指令清晰、约束性强。可以尝试加入“如果查询意图不明确请主要围绕[某个核心解释]进行生成”这样的引导或者提供少量示例Few-shot Prompting。调整温度参数temperature参数控制生成随机性。对于检索任务需要确定性高的输出通常设置在0.1到0.3之间。过高的温度会导致每次生成差异大影响检索一致性。设置最大生成长度通过max_tokens限制生成文本的长度避免LLM滔滔不绝产生大量无关细节。后处理过滤建立一套规则或关键词黑名单过滤掉生成文本中明显无关的短语或句子。模型选择如果使用开源模型不同模型的能力差异很大。7B参数以下的模型可能难以稳定完成此任务。可以考虑使用更大的模型如13B、70B或使用经过指令微调Instruction-tuning的专用模型。5.2 检索性能下降问题现象引入Query2doc后某些查询的检索结果反而比原始查询更差了。排查与解决确认分词一致性这是最高频的错误来源确保生成文本的分词方式与构建文档索引时的分词方式完全一致。如果你用Elasticsearch的standard分析器索引文档那么在Python端处理生成文本时也必须使用相同的逻辑进行分词可以调用Elasticsearch的_analyze接口或使用elasticsearch库的相同分析器。检查BM25参数如前所述生成长文本查询后BM25的k1参数可能需要调低。在测试集上做参数扫描是必要的。分析坏案例找出那些效果变差的查询人工分析其生成文本。常见问题有过度泛化查询“Python lambda”生成文本大谈“函数式编程”、“匿名函数”但用户可能只想找具体语法示例。解决方法是在提示词中强调“聚焦于查询本身的核心概念”。引入歧义查询“Java”生成文本可能同时涉及“编程语言”和“印尼岛屿”造成混淆。对于高度歧义的查询一个策略是同时用原始查询和生成文本分别检索然后融合结果如取分数加权平均。考虑查询分类并非所有查询都适合扩展。对于非常具体、已经是长句的查询如“如何在MacBook上安装Windows 11双系统”直接使用原始查询可能更好。可以训练一个简单的分类器或基于规则如查询长度、是否包含疑问词来决定是否启用Query2doc。5.3 延迟与成本开销问题现象LLM API调用显著增加了检索的响应时间可能增加数百毫秒到数秒和运营成本。优化策略缓存机制这是最有效的优化。对相同的原始查询其生成文本是确定的在temperature很低时。可以建立一个查询到生成文本的缓存如使用Redis。对于热门查询命中缓存能极大降低延迟和API调用次数。异步处理与预热对于搜索量大的应用可以考虑异步生成。用户首次发起一个罕见查询时可能略有延迟但生成文本会存入缓存供后续使用。也可以定期预热高频查询的缓存。模型轻量化评估是否能用更小、更快的模型如经过蒸馏的模型来承担Query2doc任务虽然生成质量可能略有下降但在延迟和成本上收益巨大。降级与熔断设置监控当LLM服务响应时间过长或失败率升高时自动降级为直接使用原始查询进行检索保障核心服务可用性。5.4 领域适配问题问题现象在通用领域效果不错的方案迁移到医疗、法律等专业领域时生成文本不专业检索效果不佳。解决方案领域微调如果条件允许收集一批领域内的查询-相关文档对用这些数据对选定的开源LLM进行轻量级的指令微调Instruction Tuning让模型学会生成更符合该领域语体和术语的文本。提示词专业化在提示词中明确领域和风格要求。例如在医疗领域提示词中加入“你是一个医学信息检索助手请使用规范的医学术语...”。知识增强在生成前可以先用一个快速的检索步骤如用原始查询做一次快速BM25取回前几名文档的片段将这些片段作为上下文连同查询一起喂给LLM。这相当于给LLM提供了领域知识的“提示”能显著提升生成文本的专业性和准确性。这其实就是一种轻量级的“检索增强生成”RAG思路。6. 进阶思考与扩展方向Query2doc为我们打开了一扇窗展示了LLM如何以“顾问”而非“执行者”的角色赋能传统系统。沿着这个思路还可以做更多探索迭代式Query2doc不要只生成一次。可以用第一次检索到的Top文档作为反馈让LLM再次精炼或重写查询文档进行第二轮检索。这模拟了人类“先粗略找再根据看到的信息调整搜索词”的过程。混合检索策略不抛弃原始查询。将原始查询和生成的伪文档分别进行BM25检索然后将两份结果列表通过分数融合如加权求和、RRF的方式合并。这样既能利用LLM的扩展能力又保留了原始查询的精确性鲁棒性更强。面向稠密检索器的扩展Query2doc的思想同样适用于稠密检索Dense Retrieval。可以用LLM生成伪文档后再用嵌入模型Embedding Model将整段伪文档编码为查询向量去向量数据库中检索。这能缓解稠密检索模型对查询长度敏感、对表述变化脆弱的问题。个性化扩展如果系统有用户历史行为数据可以将用户画像或历史兴趣关键词融入到给LLM的提示词中生成更符合用户个人需求的查询扩展文本实现个性化检索。从我实际的测试和部署经验来看Query2doc是一项性价比极高的技术。它不需要改动底层索引和检索架构仅通过改造查询入口就能用相对较小的成本一次LLM API调用换取检索质量的显著提升。尤其是在那些检索精度要求高、但暂时无法全面转向下一代神经检索系统的场景里它是一个非常值得尝试的“升级补丁”。当然它也不是银弹LLM的生成质量、延迟和成本是需要持续权衡和优化的关键点。我的建议是从一个具体的、有评测集的子场景开始实验用数据说话逐步将其集成到你的检索流水线中。