RAG系统进阶:从基础检索到专家级Agent的混合检索与上下文优化
1. 项目概述从“能聊”到“懂行”的Agent进阶之路上次我们聊了RAG检索增强生成的基础搭建让Agent能“开口说话”从自己的知识库里找答案。但很多朋友在实际操作后反馈效果远不如预期要么检索出来的文档驴唇不对马嘴要么生成的回答依然在胡编乱造知识库仿佛成了摆设。这太正常了因为一个能用的RAG系统和一个好用的RAG系统中间隔着一道巨大的鸿沟。今天我们就来填这道鸿沟聚焦于RAG系统的“进阶能力”建设。简单说基础RAG解决了“有没有”的问题而进阶RAG要解决“准不准”和“好不好”的问题。这就像给你的Agent从“实习生”升级为“资深专家”。实习生只能照本宣科而专家懂得在浩如烟海的资料中快速找到最相关的那几页并且结合自己的经验和上下文给出精准、可靠、有深度的解答。我们这次的目标就是通过一系列核心组件的优化与组合打造这样一个“专家级”的Agent。整个过程会涉及更精细的检索策略、更智能的上下文处理以及如何让大模型LLM更好地“消化”我们喂给它的信息。我会基于Python生态下的常见工具链如LangChain、LlamaIndex等结合我踩过的无数个坑把每一步的原理、选择和实操细节掰开揉碎讲清楚。2. 核心架构深化超越简单的“检索-拼接-生成”在基础版本中我们的流程通常是用户提问 - 将问题转换为向量 - 去向量数据库做相似度搜索 - 把Top K个结果拼接到Prompt里 - 交给LLM生成答案。这个流程的瓶颈非常明显检索精度和上下文利用率。2.1 检索策略的多元化与混合检索单一向量检索Embedding Search严重依赖于文本嵌入模型的质量和文本分块的合理性。对于专业术语、缩写、数字等向量检索可能失效。因此引入混合检索Hybrid Search是进阶的第一步。2.1.1 关键词检索的王者回归BM25别觉得传统方法过时了。BM25是一种基于词频和文档长度的概率检索模型对于精确匹配关键词、术语、实体名如“Spring Boot 2.7.0”、“Transformer架构”的效果往往比语义向量更直接、更稳定。它的原理是计算查询词与文档的统计相关性分数不依赖语义理解因此不存在“语义漂移”问题。在Python中我们可以使用rank_bm25库。它的集成并不复杂核心是构建一个所有文档分块的词条Token列表并为每个查询计算BM25分数。from rank_bm25 import BM25Okapi import jieba # 中文分词示例英文可用nltk # 假设 text_chunks 是你的文档分块列表 tokenized_corpus [list(jieba.cut(chunk)) for chunk in text_chunks] bm25 BM25Okapi(tokenized_corpus) # 对用户查询进行检索 query 如何配置Spring Boot的数据源 tokenized_query list(jieba.cut(query)) bm25_scores bm25.get_scores(tokenized_query) # 获取Top K个结果的索引 top_k_indices np.argsort(bm25_scores)[::-1][:k]注意BM25对分词质量非常敏感。中文必须使用可靠的分词工具如jieba、HanLP并考虑是否加入自定义词典专业术语。英文则需要注意词干还原Stemming和停用词过滤。2.1.2 混合检索的艺术分数融合拿到向量检索的相似度分数假设是余弦相似度范围[-1,1]或[0,1]和BM25的分数理论上无上限后不能直接相加。我们需要对它们进行标准化Normalization和加权融合。一种常见且有效的方法是使用倒数排名融合Reciprocal Rank Fusion, RRF。它不关心分数的绝对数值只关心排名避免了分数标准化带来的偏差。def reciprocal_rank_fusion(vector_results, bm25_results, k60, c60): vector_results: list of (doc_id, score) from vector search bm25_results: list of (doc_id, score) from BM25 search k: 最终返回的文档数量 c: 常数通常设为60用于平滑排名 scores {} # 处理向量检索结果 for rank, (doc_id, _) in enumerate(vector_results): scores[doc_id] scores.get(doc_id, 0) 1 / (c rank 1) # 处理BM25检索结果 for rank, (doc_id, _) in enumerate(bm25_results): scores[doc_id] scores.get(doc_id, 0) 1 / (c rank 1) # 按融合分数排序 fused_results sorted(scores.items(), keylambda x: x[1], reverseTrue) return fused_results[:k]在实际操作中我通常会先分别进行向量检索和BM25检索各取Top 20-30个候选文档然后通过RRF融合得到最终的Top 10。这样可以确保既捕捉到语义相似性又不漏掉关键词精确匹配的文档。2.2 查询理解与改写让Agent“听懂”问题用户的问题往往是模糊、简短或包含指代的。直接用于检索效果很差。我们需要一个“查询理解”层。2.2.1 查询扩展Query Expansion利用LLM本身的能力对原始查询进行扩展或生成多个相关的查询变体。例如用户问“电脑卡怎么办”可以扩展为“计算机运行速度缓慢的解决方案”、“提升PC性能的方法”、“清理系统垃圾以加速电脑”。from langchain.prompts import ChatPromptTemplate from langchain.chat_models import ChatOpenAI prompt_template ChatPromptTemplate.from_messages([ (system, 你是一个专业的查询优化助手。请根据用户原始问题生成3个不同角度但语义相同的搜索查询用于文档检索。直接输出查询用分号隔开。), (human, {original_query}) ]) llm ChatOpenAI(modelgpt-3.5-turbo, temperature0.3) query_expansion_chain prompt_template | llm original_query Python安装失败 expanded_queries_str query_expansion_chain.invoke({original_query: original_query}).content # 输出可能为“Python安装过程中报错安装Python时遇到问题如何解决Python installer failed” expanded_queries [q.strip() for q in expanded_queries_str.split(;)]然后我们可以用这多个查询分别进行检索最后合并去重。这显著提高了召回率。2.2.2 查询重写Query Rewriting在多轮对话中用户的问题可能是省略的。例如之前讨论了“Spring Boot”用户接着问“那数据源配置呢”。我们需要将当前查询与对话历史结合重写为一个完整的、可独立检索的查询。rewrite_prompt ChatPromptTemplate.from_messages([ (system, 请根据以下对话历史和最新的人类问题生成一个完整的、独立的搜索查询。这个查询将用于在一个知识库中检索相关文档。不要回答问题本身只输出查询语句。), (human, 对话历史{chat_history}\n\n人类最新问题{question}) ]) rewrite_chain rewrite_prompt | llm chat_history 人类如何创建一个Spring Boot项目\n助手你可以使用Spring Initializr网站或IDE的插件。 current_question 数据源怎么配 standalone_query rewrite_chain.invoke({chat_history: chat_history, question: current_question}).content # 输出可能为“Spring Boot项目中配置数据源的方法”这个“完整查询”再送入检索器效果远好于直接检索“数据源怎么配”。3. 上下文构建与优化给LLM喂“精饲料”检索到相关文档后如何把它们组织成有效的Prompt上下文是决定生成质量的关键。不是简单拼接就完事了。3.1 智能分块Chunking策略回顾与优化基础分块通常用固定大小的字符窗口如500字滑动。但这会切断完整的句子或段落破坏语义。3.1.1 基于语义的分块使用句子分割器如nltk的sent_tokenize或sentence-transformers的SentenceTransformer先将文本分成句子。然后计算句子嵌入根据句子间的余弦相似度进行动态合并直到块达到预设的大小阈值。这样可以保证块内的句子在语义上是紧密相关的。3.1.2 基于层次结构的分块如果文档有明确的结构如Markdown的标题###优先按标题进行分块。一个##二级标题下的所有内容作为一个块这比固定窗口更符合人类的认知逻辑。在LlamaIndex中可以使用SimpleNodeParser并设置chunk_size和包含MetadataMode.ALL的Metadata来保留层级信息。3.2 上下文压缩与选择性注入当检索到的文档总长度超过LLM的上下文窗口限制时我们必须做出选择。全部截断是下策。3.2.1 最大边际相关性MMR去重检索到的文档之间可能存在大量重复内容。MMR算法可以在保证相关性的同时增加结果的多样性。LangChain内置了MMRRetriever。其核心思想是在选择下一个文档时不仅考虑它与查询的相关性还考虑它与已选文档集合的相似度优先选择相关性高且与已选文档区别大的。3.2.2 提取式摘要Extractive Summarization对于较长的相关文档块在注入Prompt前先对其进行摘要压缩。这里不是用LLM生成摘要那太慢而是使用更快的提取式方法例如Lead-3直接取文档的前三句话。对于新闻、报告类文档往往有效。基于嵌入的句子选择计算文档中每个句子的嵌入同时计算查询的嵌入。选择与查询嵌入最相似的Top N个句子按原顺序拼接。这能直接从长文档中“抽”出最相关的部分。from sentence_transformers import SentenceTransformer, util import numpy as np def extract_relevant_sentences(document, query, model, top_n5): sent_encoder SentenceTransformer(model) sentences sent_tokenize(document) # 假设已分割 sentence_embeddings sent_encoder.encode(sentences, convert_to_tensorTrue) query_embedding sent_encoder.encode(query, convert_to_tensorTrue) # 计算余弦相似度 cos_scores util.cos_sim(query_embedding, sentence_embeddings)[0] # 按分数排序并取Top N top_results np.argsort(-cos_scores.cpu().numpy())[:top_n] # 按原文顺序排序后返回 top_results_sorted sorted(top_results) return .join([sentences[i] for i in top_results_sorted])3.2.3 元数据过滤与排序在检索时除了内容我们还可以利用元数据如文档类型、创建日期、作者、章节标题进行过滤和排序。例如在技术文档中优先返回“故障排除”章节的内容在知识库中优先返回最近更新的文档。这需要在构建索引时就将这些元数据存储起来并在检索查询中指定过滤器。4. Prompt工程的精细化设计上下文准备好了如何“问”LLM同样至关重要。一个糟糕的Prompt会让前面的努力付诸东流。4.1 RAG专用Prompt模板不要用通用的“请根据以下上下文回答问题”这种Prompt。一个强大的RAG Prompt应该包含以下几个明确指令角色与任务定义明确告诉LLM它要扮演什么角色如“资深技术专家”。上下文来源说明明确指出提供的上下文来自可信知识库并要求答案必须基于此。答案格式与约束要求答案结构化如分点、简洁、或包含引用。不确定性处理明确指示如果上下文未提供足够信息应如实回答“不知道”严禁编造。引用要求要求答案中的关键信息尽可能指出是来自哪一段上下文例如用【1】、【2】标注。一个示例模板如下你是一个严谨的技术支持专家。请严格根据提供的“参考上下文”来回答用户问题。 参考上下文{context}用户问题{question} 请遵循以下规则生成答案 1. 答案必须完全基于上述参考上下文。如果上下文没有提供足够信息请直接说“根据现有资料无法确定该问题的答案”。 2. 答案应清晰、有条理优先使用分点列表。 3. 如果答案中的关键信息对应上下文中的特定部分请在信息后标注出处例如【上下文1】。 4. 不要提及“根据上下文”这类字眼直接将信息融入答案。4.2 少样本示例Few-Shot注入对于复杂或格式要求严格的问答在Prompt中提供1-2个输入上下文问题和输出理想答案的示例能极大地引导LLM的输出格式和质量。这被称为“少样本学习”。在你的Prompt模板中可以在系统指令后加入“示例”部分。例如如果你希望答案总是以“核心原因是”开头那么就展示一个这样做的例子。4.3 后处理与验证生成答案后工作还没完。增加一个答案验证步骤可以大幅提升可靠性。引用校验检查答案中声称引用的【上下文X】是否真实存在于提供的上下文中并且引用的信息是否准确。可以写一个简单的正则表达式匹配和交叉验证程序。幻觉检测让另一个LLM或同一个LLM的不同调用扮演“验证者”判断生成的答案是否严格基于提供的上下文并标记出任何可能编造的部分。这虽然增加了成本但对高可靠性场景是值得的。自我一致性Self-Consistency用相同的Prompt和上下文让LLM生成多个答案通过调整temperature参数然后比较这些答案的核心事实是否一致。如果不一致可能意味着上下文不充分或问题模糊需要触发重新检索或向用户澄清。5. 系统评估与迭代优化搭建完系统不是终点必须建立评估体系持续迭代。没有度量就没有优化。5.1 构建评估数据集不要用感觉评判。你需要一个测试集QA对来源从你的知识库中人工构造一批“问题”和对应的“标准答案”或至少是答案所在的文档片段。类型应覆盖简单事实型、复杂推理型、多跳问答型需要结合多个文档等不同问题类型。5.2 核心评估指标对于每个测试问题运行你的RAG系统从以下维度评估检索阶段指标命中率Hit Rate K标准答案所在的文档出现在检索返回的Top K个结果中的比例。这衡量检索的召回能力。平均精度均值Mean Average Precision, MAP不仅关心是否命中还关心命中文档的排名位置。排名越靠前分数越高。生成阶段指标忠实度Faithfulness生成的答案在事实上是否与提供的上下文一致是否存在幻觉。可以用LLM作为评判员LLM-as-a-Judge或者用基于NLI自然语言推理的模型自动判断。答案相关性Answer Relevance生成的答案是否直接、完整地解决了用户的问题是否答非所问。引用精度Citation Precision/Recall答案中的引用是否准确指向了上下文中支持该信息的具体位置。5.3 端到端评估与A/B测试使用LLM如GPT-4作为裁判让它对比系统生成的答案和标准答案或根据上下文判断在“正确性”、“完整性”、“清晰度”等维度上进行打分例如1-5分。虽然主观但GPT-4的评判通常与人类有较高一致性。在线上环境可以进行A/B测试。将用户流量随机分到新旧两个RAG策略版本核心观测指标可以是问题解决率用户得到满意答案后是否结束了会话。人工转接率用户是否要求转接人工客服。平均对话轮次解决一个问题需要的交互次数。通过分析评估结果你可以定位瓶颈是检索不准还是上下文组织不好或者是Prompt指令不清然后有针对性地进行下一轮优化。例如发现“忠实度”低可能是上下文过长导致LLM注意力分散需要加强上下文压缩发现“命中率”低则需要调整检索策略如引入混合检索或优化文本分块方式。整个RAG系统的进阶就是一个“检索 - 构建上下文 - 生成 - 评估 - 优化”的持续闭环。没有一劳永逸的银弹只有基于数据和实验的持续打磨。这个过程虽然繁琐但当你看到你的Agent从“经常胡说”变得“言之有据”时那种成就感是实实在在的。