news 2026/8/15 6:07:47

RAG系统进阶:从基础检索到专家级Agent的混合检索与上下文优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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, k=60, c=60): """ 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(), key=lambda x: x[1], reverse=True) 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(model="gpt-3.5-turbo", temperature=0.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 基于语义的分块使用句子分割器(如nltksent_tokenizesentence-transformersSentenceTransformer)先将文本分成句子。然后,计算句子嵌入,根据句子间的余弦相似度进行动态合并,直到块达到预设的大小阈值。这样可以保证块内的句子在语义上是紧密相关的。

3.1.2 基于层次结构的分块如果文档有明确的结构(如Markdown的标题###),优先按标题进行分块。一个##二级标题下的所有内容作为一个块,这比固定窗口更符合人类的认知逻辑。在LlamaIndex中,可以使用SimpleNodeParser并设置chunk_size和包含MetadataMode.ALLMetadata来保留层级信息。

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_n=5): sent_encoder = SentenceTransformer(model) sentences = sent_tokenize(document) # 假设已分割 sentence_embeddings = sent_encoder.encode(sentences, convert_to_tensor=True) query_embedding = sent_encoder.encode(query, convert_to_tensor=True) # 计算余弦相似度 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应该包含以下几个明确指令:

  1. 角色与任务定义:明确告诉LLM它要扮演什么角色(如“资深技术专家”)。
  2. 上下文来源说明:明确指出提供的上下文来自可信知识库,并要求答案必须基于此。
  3. 答案格式与约束:要求答案结构化(如分点)、简洁、或包含引用。
  4. 不确定性处理:明确指示如果上下文未提供足够信息,应如实回答“不知道”,严禁编造。
  5. 引用要求:要求答案中的关键信息,尽可能指出是来自哪一段上下文(例如用【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系统,从以下维度评估:

  1. 检索阶段指标

    • 命中率(Hit Rate @ K):标准答案所在的文档,出现在检索返回的Top K个结果中的比例。这衡量检索的召回能力。
    • 平均精度均值(Mean Average Precision, MAP):不仅关心是否命中,还关心命中文档的排名位置。排名越靠前,分数越高。
  2. 生成阶段指标

    • 忠实度(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从“经常胡说”变得“言之有据”时,那种成就感是实实在在的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/15 6:07:44

LLM成本治理实战:三步构建透明可控的Token消耗管理体系

1. 项目概述:当LLM成本开始“失控” 最近和几个技术团队负责人聊天,发现一个普遍现象:年初大家还在为接入了大语言模型(LLM)而兴奋,到了年中,看着云服务商发来的账单,笑容逐渐凝固。…

作者头像 李华
网站建设 2026/8/15 6:07:11

数学建模竞赛实战:定日镜场优化设计与PSO算法应用

1. 项目概述:从“小白”到“优化”的实战路径看到“2023国赛数学建模A题第二问”这个标题,很多同学的第一反应可能是“头大”。尤其是当它和“定日镜场的优化设计”这种听起来就充满物理和工程味道的词绑在一起时,不少数学基础不错但缺乏交叉…

作者头像 李华
网站建设 2026/8/15 6:06:19

UG NX 10.0安装全攻略:从核心原理到避坑实践

1. 从零开始:为什么你的UG NX 10.0安装总出问题?UG NX 10.0,这个在工业设计、模具、数控编程领域堪称经典的版本,至今仍有庞大的用户群体。无论是资深工程师还是刚入行的学生,安装它往往是职业生涯的第一个“下马威”。…

作者头像 李华
网站建设 2026/8/15 6:05:26

从零构建面向大模型的HNSW向量检索工程框架:原理、实现与优化

1. 先搞清楚 Harness-1-E18-HNSW 到底要解决什么问题看到这个标题,很多人第一反应可能是“又一个向量检索库”。但 Harness-1-E18-HNSW 这个名字,其实把它的核心定位和关键技术路径都写出来了。它不是泛泛的向量工具,而是一个专门针对超大模型…

作者头像 李华
网站建设 2026/8/15 6:05:15

误删Windows系统Path变量后的完整恢复指南与避坑策略

1. 项目概述:一次“手滑”引发的系统危机相信不少朋友,尤其是刚接触编程、运维或者需要频繁配置各种开发环境的朋友,都曾有过这样的经历:为了给新安装的软件(比如Python、Java、Node.js)或者某个工具&#…

作者头像 李华
网站建设 2026/8/15 6:03:53

解决GitHub Desktop无法识别Unity URP项目的问题

1. 问题现象与背景解析最近在Unity项目开发中遇到一个典型问题:使用GitHub Desktop客户端时,无法正常识别包含URP(Universal Render Pipeline)渲染管线的Unity项目。具体表现为:在GitHub Desktop的仓库列表中看不到URP…

作者头像 李华