尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

RAG高级检索实战:切块、混合检索与重排序

RAG高级检索实战:切块、混合检索与重排序 在AI应用开发中RAGRetrieval-Augmented Generation检索增强生成已经成为构建知识库问答、企业文档助手、智能客服等场景的主流方案之一。很多初学者拿到 RAG 相关资料后第一反应是“这不就是把文档切成块存进向量数据库再让大模型回答吗”。方向没错但真实项目跑起来会发现问题往往不出在大模型而出在检索环节文档切得不合适、向量召回不精准、TopK 结果里正确答案排不到前面。这篇文章从 0 到 1 梳理 RAG 的原理重点围绕高级检索实战讲清楚文本切块、向量检索、混合检索和重排序到底怎么配合同时给出可运行的代码、参数建议和排查链路。文章面向对 AI 应用开发有一定基础、希望深入理解 RAG 检索质量的开发者。学完后你能搭建一个最小可运行的 RAG 系统知道如何用高级检索手段提升答案准确率也能在生成效果变差时快速定位问题出在检索、切块还是生成环节。1. RAG 到底解决什么问题为什么现在应用这么广1.1 大模型的知识局限大语言模型虽然能够生成相当流畅的文本但它有一个天然限制知识来自训练数据并且训练数据有截止时间。当用户问一个企业内部 SOP、一套私有接口文档或者最新产品说明时模型很可能回答不出来甚至一本正经地给出错误答案。要解决这个问题主流思路有两种继续微调模型让模型记住新知识。在问答时临时把相关资料检索出来放进提示词让模型基于资料回答。微调成本高、周期长而且每次知识更新都要重新训练不适合需要频繁变更内容的场景。RAG 选择的是第二种思路不改变模型权重只改变模型回答问题时“看到”的上下文。所以 RAG 的核心定位是给大模型接上外挂知识库让它能依据检索到的内容生成答案。这也解释了为什么 RAG 和向量数据库、知识库这几个概念经常一起出现。1.2 RAG 的核心概念RAG 可以拆成三个字母来理解Retrieval检索。从知识库中找到和用户问题最相关的若干文本片段。Augmented增强。把检索到的片段作为额外上下文拼进提示词。Generation生成。大模型基于“原始问题 检索片段”生成回答。一个最小 RAG 流程如下用户问题 - 查询向量化 - 检索相似片段 - 组装提示词 - 大模型生成回答这里的“检索”是 RAG 的灵魂。如果检索回来的片段本身不相关大模型再强也无法给出正确答案。很多 RAG 项目效果差恰恰是因为把太多精力放在模型选择上忽略了检索质量。1.3 RAG、知识库与向量数据库的关系项目里经常听到“RAG 知识库”“向量数据库”这些说法需要理清它们的关系概念作用举例知识库原始文档的组织形式包含切分后的文本片段、元数据、权限信息企业文档库、产品手册、运维工单向量数据库存储文本向量并支持相似度检索FAISS、Milvus、pgvector、QdrantRAG一套应用框架把知识库、向量检索和大模型串起来LangChain、LlamaIndex、自研流程简单理解知识库是“存什么”向量数据库是“怎么存和怎么查”RAG 是“查完之后怎么让模型用起来”。做高级检索实战时这三个层面都要考虑。2. RAG 的完整链路每个环节都要先想清楚RAG 不是“把所有文档塞进向量库”这么简单。一个完整的 RAG 系统由离线和在线两部分组成。2.1 数据接入与准备离线阶段的核心任务是把原始数据变成可检索的文本片段。原始数据可能是 PDF、Word、Markdown、HTML也可能来自数据库表格。这一步通常包括格式解析把 PDF、Word 等内容抽取成纯文本。清洗去掉页眉页脚、重复内容、无关水印、乱码字符。结构识别尽量保留标题、段落、表格、列表等结构信息。清洗后的文本质量直接决定切块效果。一个常见错误是解析 PDF 时带了大量换行和页码导致切出来的块有一半是空白字符向量化之后语义被稀释。2.2 索引构建切分、向量化、存储索引构建是离线阶段最核心的步骤包含三个动作切分把长文档按策略切成长度合适的块chunk。向量化用 Embedding 模型把每个块转换成向量。存储把向量和原始文本、元数据一起写入向量数据库。切分是 RAG 里最容易被忽略但影响最大的环节。后面第 4 章会详细讲不同切块策略的效果。向量化时要选择与查询语言和场景匹配的 Embedding 模型。中文场景下可以优先考虑支持中文的模型并在出现术语、行业缩写时注意模型是否认识。索引存储阶段除了向量本身还要保存原始文本片段、文档来源、章节路径、更新时间等元数据。这些元数据在后续过滤、展示引用来源、排错时非常有用。2.3 查询阶段检索、重排序、生成在线阶段是用户每次提问都会执行的链路将用户问题向量化。从向量索引中召回 TopK 个相似片段。可选地加入关键词检索合并结果。可选地用重排序模型对合并结果进行精排。将精排后的片段按顺序拼接到提示词中。调用大模型生成答案。第 2 到第 4 步属于“高级检索”范畴。基础 RAG 通常只做第 2 步高级 RAG 则会加入混合检索和重排序。理解这条链路后我们再从环境搭建开始先跑通一个最小系统再逐步优化检索质量。3. 从0到1搭建一个最小RAG系统3.1 环境准备与依赖安装为了减少配置负担示例采用 Python 3.9 以上环境核心依赖包括sentence-transformers负责文本向量化。faiss-cpu负责向量索引和相似度检索。rank-bm25负责关键词检索用于混合检索对比。openai 或其他 OpenAI 兼容客户端负责调用大模型接口。python-dotenv读取环境变量保存 API Key。安装命令如下pip install sentence-transformers faiss-cpu rank-bm25 openai python-dotenv如果使用本地 GPU可以安装faiss-gpu如果只是学习CPU 版本足够。注意faiss-gpu和faiss-cpu不要同时安装否则会出现底层库冲突。注意实际项目落地前需要先确认依赖版本与 Python 版本、操作系统的兼容性。这里给出的命令用于演示思路具体版本要以自己的环境为准。3.2 准备测试文档先用一段模拟产品说明作为测试文档内容包含可被检索的关键信息。raw_text 多功能巡检机器人系统说明 一、设备供电 机器人本体采用 48V 直流电源供电充电桩输出功率为 500W。 二、通信方式 机器人支持 Wi-Fi 6 和 5G 两种通信方式在断网情况下可自动切换为本地规划模式。 三、安全机制 当激光雷达检测到前方 30cm 内有障碍物时机器人会紧急制动。 四、任务调度 机器人可通过管理后台下发巡检任务任务类型包括日常巡检、指定点位抽查和环境监测。 这段文本结构简单包含编号和标题。切分时如果直接按固定长度切很容易把“48V 供电”和“通信方式”混在一个块里导致检索精度下降。后面会看到如何通过结构化切分改善这个问题。3.3 编写文本切分与向量化代码先实现一个简单的切分函数。为了演示原理这里使用按字符固定长度切分并允许重叠。def split_text(text, chunk_size100, chunk_overlap20): chunks [] start 0 text_len len(text) while start text_len: end min(start chunk_size, text_len) chunks.append(text[start:end]) if end text_len: break start end - chunk_overlap return chunks chunks split_text(raw_text, chunk_size80, chunk_overlap15) for i, c in enumerate(chunks): print(fchunk {i}: {c})这段代码展示的是最基础的字符切分。chunk_size决定每个块的长度chunk_overlap让相邻块保留重复信息避免关键句子被切断。实际项目中不建议按字符切分更好的是按语义或结构切分后面会讲。接下来做向量化并建索引。这里使用中文本向量模型BAAI/bge-small-zh-v1.5它体积小适合学习环境。from sentence_transformers import SentenceTransformer import faiss import numpy as np model SentenceTransformer(BAAI/bge-small-zh-v1.5) def embed_texts(texts): return model.encode(texts, normalize_embeddingsTrue) embeddings embed_texts(chunks) dim embeddings.shape[1] index faiss.IndexFlatIP(dim) index.add(embeddings.astype(float32))IndexFlatIP使用内积相似度。因为向量已经做过 L2 归一化内积结果等价于余弦相似度范围在 -1 到 1 之间。这里normalize_embeddingsTrue是关键如果漏掉内积的大小会受到向量长度影响检索结果可能偏向长文本。3.4 实现检索与生成调用检索函数如下def search(query, k3): query_vec embed_texts([query]) scores, indices index.search(query_vec.astype(float32), k) results [] for i in range(len(indices[0])): idx indices[0][i] score scores[0][i] results.append((score, chunks[idx], idx)) return results query 机器人的供电电压是多少 results search(query, k3) for score, chunk, idx in results: print(fscore{score:.4f} idx{idx}: {chunk})生成环节使用 OpenAI 兼容接口。这样不管是 OpenAI 官方服务、还是本地起的模型服务只要兼容/v1/chat/completions都能用。import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( base_urlos.getenv(LLM_BASE_URL, http://localhost:8000/v1), api_keyos.getenv(LLM_API_KEY, sk-no-key), ) def rag_answer(question, search_results): context \n\n.join([chunk for score, chunk, idx in search_results]) prompt f请根据以下资料回答问题。 资料 {context} 问题{question} 要求 1. 只能基于资料回答。 2. 如果资料中没有相关信息直接说明“资料中未找到”。 resp client.chat.completions.create( modelos.getenv(LLM_MODEL, qwen2.5), messages[{role: user, content: prompt}], ) return resp.choices[0].message.content这段代码把检索结果拼进提示词并要求模型不要编造。实际项目里提示词可以更细致比如要求模型给出引用片段编号、限定回答长度。到这里一个最小 RAG 已经跑通了。接下来要讨论的是为什么有时候检索出来的片段明明相似但生成答案还是不对。这就要进入高级检索实战。4. 高级检索实战从“能搜到”到“搜得准”4.1 切块策略如何影响检索质量切块是 RAG 中最容易被轻视的环节。同一个文档按不同方式切检索结果会差别很大。常见切块方式切块方式优点缺点适用场景按固定字符切分实现简单长度可控容易切断语义块之间信息割裂学习演示、纯文本无结构按 token 切分与模型 token 限制一致会切断词或句子需要 tokenizer需要精确控制 token 长度按段落 / 标题切分保留语义边界某些段落过长仍需二次切分PDF 解析后保留结构的情况按语义切分语义完整检索质量高依赖句子 embedding计算量大高质量知识库父子块切分检索小块生成时用大块存储和索引复杂度增加需要精确片段且上下文完整一个常见问题是块太小检索命中的片段只包含表头没有对应内容块太大检索命中的片段包含大量无关内容向量方向被稀释。更实用的做法是按文档结构切分保留标题和段落。代码中可以用简单规则识别“一、”这类章节标记。def split_by_sections(text): lines text.split(\n) sections [] current_title current_content [] for line in lines: if line.strip() and (line.startswith(一、) or line.startswith(二、) or line.startswith(三、)): if current_title or current_content: sections.append((current_title, \n.join(current_content))) current_title line.strip() current_content [] else: current_content.append(line.strip()) if current_title or current_content: sections.append((current_title, \n.join(current_content))) return sections切分后每一段都带上标题元数据。检索时可以把“标题 内容”整体向量化也可以在返回时把标题展示给用户作为来源。注意没有一种切块策略适用于所有场景。最佳做法是准备几十条有代表性的问题对比不同切块方式下的检索命中率用数据决定策略。4.2 关键词检索与向量检索的互补向量检索擅长找“语义相似”的内容但也有明显弱点对专有名词、精确数字、型号代码不敏感。比如用户问“48V 直流电源”如果训练向量模型时没有充分见过类似表达向量距离可能并不可靠。关键词检索刚好相反。它通过精确匹配词项来召回比如“48V”“500W”“Wi-Fi 6 协议”。缺点是查不到同义词和语义改写。把两者结合就是混合检索。先实现一个简单的关键词检索。这里用rank_bm25需要先对中文做分词。为了演示可以先用字符 n-gram 方式代替但效果更好的是使用 jieba 分词。pip install jiebaimport jieba def tokenize(text): return list(jieba.cut(text)) tokenized_corpus [tokenize(chunk) for chunk in chunks] bm25 BM25Okapi(tokenized_corpus) def bm25_search(query, k3): tokenized_query tokenize(query) scores bm25.get_scores(tokenized_query) top_indices scores.argsort()[-k:][::-1] results [] for idx in top_indices: results.append((scores[idx], chunks[idx], idx)) return resultsBM25 对精确词命中很有效尤其适合产品型号、报错码等场景。但单个关键词检索也有问题如果问题里没有出现知识库里的原词BM25 会把相关文档丢掉。4.3 混合检索结合BM25和向量相似度混合检索的目标是两种检索结果取并集再对并集中的文档做综合打分。常见的融合方法有两种加权得分加权final_score alpha * vector_score beta * bm25_score。RRFReciprocal Rank Fusion对每个结果在不同检索结果中的排名取倒数再求和。RRF 不依赖分数绝对值更适合两种分布不一致的分数融合。示例实现如下def rrf_fusion(vector_results, bm25_results, k60, top_n5): scores {} for rank, (score, chunk, idx) in enumerate(vector_results): scores[idx] scores.get(idx, 0) 1.0 / (k rank) for rank, (score, chunk, idx) in enumerate(bm25_results): scores[idx] scores.get(idx, 0) 1.0 / (k rank) sorted_idx sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n] return [(chunks[idx], idx, score) for idx, score in sorted_idx]每次检索时向量检索召回 TopKBM25 也召回 TopK然后用 RRF 合并。这样做的好处是向量检索负责语义相似BM25 负责精确词匹配互补性很强。4.4 重排序找回被TopK丢掉的正确答案混合检索后候选片段可能增加到 10 到 20 个。如果直接把这么多片段塞给大模型既浪费 token又可能引入噪音。更合理的做法是先用一个重排序模型对候选集精排只取前 3 到 5 个进入提示词。重排序模型和向量检索模型不同。向量检索的目标是“从全库找相关”速度快重排序的目标是“对少量候选精排”精度高但速度较慢。可以使用跨编码器模型做重排序示例from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-base) def rerank(query, candidates, top_n3): pairs [[query, chunk] for chunk, idx, score in candidates] scores reranker.predict(pairs) sorted_pairs sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [cand for cand, score in sorted_pairs[:top_n]]经过重排序后最终进入提示词的片段相关性更高。这里要特别注意不要跳过混合检索直接重排序全库因为重排序模型计算代价高不适合处理超大规模候选集。到这一步我们已经有了一套相对完整的高级检索链路用户问题 - 向量检索 TopK - RRF 融合 - 重排序 - 精排片段 - 大模型生成5. 参数调优与效果验证5.1 关键参数速查表RAG 项目里没有“万能参数”但常见参数的含义和影响可以总结成一张表参数默认值参考调大影响调小影响建议chunk_size200-500 字符上下文更完整但噪音增加语义不完整召回碎片化按文档结构动态切分chunk_overlap10%-20%减少断句损失但增加重复关键内容可能被切断与切块方式配合TopK检索10-20召回更全但噪音增加可能漏掉正确答案用于候选召回阶段TopN重排序后3-5上下文更丰富但 token 消耗大答案可能缺少支撑取决于模型上下文长度rrf_k60排名融合更平滑更关注头部排名一般保持 60Embedding 维度取决于模型更高表达力计算更慢轻量但可能损失语义按模型官方说明这里要强调TopK 和 TopN 是两个不同阶段的参数。先召回一个较大的候选集再精排到很小数量是高级 RAG 的标准做法。5.2 用检索评估指标判断效果没有指标就谈不上调优。RAG 的评估至少包含两个层面检索层面命中的片段是否包含正确答案。生成层面最终答案是否正确、可读、可溯源。检索层面最简单的评估方式是“命中率”Hit Rate对每道测试题看看正确答案所在的片段是否出现在检索结果前 K 个中。def calculate_hit_rate(test_cases, search_func, k5): hits 0 for case in test_cases: query case[query] expected_chunk_id case[chunk_id] results search_func(query, kk) result_ids [idx for score, chunk, idx in results] if expected_chunk_id in result_ids: hits 1 return hits / len(test_cases)也可以计算 MRRMean Reciprocal Rank正确答案出现在第 1 位得 1 分第 2 位得 0.5 分第 3 位得 0.33 分取平均。MRR 对排序位置更敏感适合对比不同检索策略。5.3 一个简单的消融验证流程想验证高级检索是否有用可以按下面的流程做对比实验准备 30 到 50 条覆盖不同类型问题的测试集。在相同切块和相同生成模型下先只用向量检索跑一遍记录命中率和答案分。改成“向量 BM25”混合检索重新跑。加上重排序再跑。对比每组实验结果形成结论。每次只改一个变量否则无法定位是哪个步骤带来的提升。这类消融实验也是给团队或领导讲清楚“为什么要做高级检索”的最好材料。6. 常见问题排查链路与生产环境建议6.1 检索为空或相关度低现象模型回答“资料中未找到”但人类一眼看出知识库里有答案。排查顺序检查原始文档有没有被正确解析。如果 PDF 是扫描件没有 OCR抽取出来可能是空内容。检查切块后的片段是否包含关键信息。打印出 chunks确认“答案所在句子”没有被切散。打印检索分数。如果 Top1 分数很低说明向量检索本身没召回相关片段。用 BM25 再查一次。如果 BM25 能命中说明问题出在向量模型或切块策略。检查查询预处理的归一化逻辑。比如问题里包含英文括号、全角半角混用可能导致向量化后差异增大。6.2 返回结果重复或内容截断现象模型回答里重复出现同一句话或者明显在“硬凑内容”。常见原因和处理建议问题现象常见原因检查方式处理建议多个检索片段内容几乎相同切块重叠过高或重复文本入索引查看返回片段原文和元数据降低重叠率写入索引前去重生成内容截断提示词超出模型上下文长度计算 prompt token 数减少 TopN或使用更长上下文模型回答只有检索片段本身提示词把“总结”写成了“复述”检查 prompt 指令明确要求“基于资料用自己的话概括”引用来源对不上元数据没有随片段保留检查索引是否记录文档 ID检索结果同步返回元数据6.3 生产环境还需要补齐哪些能力学习环境里用IndexFlatIP把全部向量加载进内存即可。生产环境会复杂很多至少还需要关注能力生产环境要求向量库使用分布式向量数据库支持过滤、权限、更新、备份索引更新支持增量写入不阻塞查询权限用户只能检索自己有权限的文档需要在检索时加元数据过滤缓存相同或相似问题可以缓存答案和检索结果日志与监控记录检索耗时、片段来源、生成 token 数、错误率版本回滚文档或模型升级后如果效果下降能快速回退模型部署根据成本、延迟、数据隐私决定使用本地模型还是云 API6.4 可复用的上线前检查清单RAG 系统上线前可以按下面清单逐项检查是否对原始文档做了格式解析和清洗并验证了文本可读性。是否按文档结构切块并保留标题、来源、更新时间等元数据。是否准备了覆盖常见问题、疑难问题、越权问题的测试集。是否对比过至少两种检索策略并用命中率、MRR 等指标验证。是否配置了重排序并调好 TopK、TopN 参数。提示词是否限制了模型只能基于资料回答。是否处理了“资料中未找到”的回答。是否接入日志和监控能追溯每次回答引用了哪些片段。是否评估过大模型的上下文 token 开销和响应延迟。是否做好文档更新、删除、权限隔离和版本回滚方案。7. 从基础 RAG 到 Agentic RAG下一步怎么走这一步是自然的扩展方向。基础 RAG 是“单轮检索 生成”。遇到复杂问题时一次检索经常不够比如用户连续追问“那电池容量呢”“如果断网怎么处理”如果每次都把全部历史问题向量化检索容易丢失上下文。Agentic RAG 是近期工程实践里讨论较多的方向。它的核心思想是让大模型自己决定“要不要检索”“检索什么”“是否需要多轮检索”“如何评估检索结果是否足够”。这不只是加一层循环而是把检索、判断、再检索变成智能体行为。对此本文先不做展开但建议把上面这些检索质量和评估方法掌握扎实。因为不管是基础 RAG 还是 Agentic RAG最终效果依然依赖检索质量。对刚接触 RAG 的开发者来说最有价值的练习是拿一份真实业务文档准备 30 个问题先用纯向量检索跑一版再逐步加入结构化切块、混合检索和重排序观察每个改动带来的命中率变化。这个过程比直接套框架更能理解 RAG 的本质。
返回列表