RAG高级检索实战:切块、混合检索与重排序
2026/8/26 13:18:04 网站建设 项目流程

在AI应用开发中,RAG(Retrieval-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、Qdrant
RAG一套应用框架,把知识库、向量检索和大模型串起来LangChain、LlamaIndex、自研流程

简单理解:知识库是“存什么”,向量数据库是“怎么存和怎么查”,RAG 是“查完之后怎么让模型用起来”。做高级检索实战时,这三个层面都要考虑。

2. RAG 的完整链路,每个环节都要先想清楚

RAG 不是“把所有文档塞进向量库”这么简单。一个完整的 RAG 系统由离线和在线两部分组成。

2.1 数据接入与准备

离线阶段的核心任务是把原始数据变成可检索的文本片段。原始数据可能是 PDF、Word、Markdown、HTML,也可能来自数据库表格。这一步通常包括:

  • 格式解析:把 PDF、Word 等内容抽取成纯文本。
  • 清洗:去掉页眉页脚、重复内容、无关水印、乱码字符。
  • 结构识别:尽量保留标题、段落、表格、列表等结构信息。

清洗后的文本质量直接决定切块效果。一个常见错误是:解析 PDF 时带了大量换行和页码,导致切出来的块有一半是空白字符,向量化之后语义被稀释。

2.2 索引构建:切分、向量化、存储

索引构建是离线阶段最核心的步骤,包含三个动作:

  1. 切分:把长文档按策略切成长度合适的块(chunk)。
  2. 向量化:用 Embedding 模型把每个块转换成向量。
  3. 存储:把向量和原始文本、元数据一起写入向量数据库。

切分是 RAG 里最容易被忽略但影响最大的环节。后面第 4 章会详细讲不同切块策略的效果。

向量化时,要选择与查询语言和场景匹配的 Embedding 模型。中文场景下,可以优先考虑支持中文的模型,并在出现术语、行业缩写时注意模型是否认识。

索引存储阶段,除了向量本身,还要保存原始文本片段、文档来源、章节路径、更新时间等元数据。这些元数据在后续过滤、展示引用来源、排错时非常有用。

2.3 查询阶段:检索、重排序、生成

在线阶段是用户每次提问都会执行的链路:

  1. 将用户问题向量化。
  2. 从向量索引中召回 TopK 个相似片段。
  3. 可选地,加入关键词检索,合并结果。
  4. 可选地,用重排序模型对合并结果进行精排。
  5. 将精排后的片段按顺序拼接到提示词中。
  6. 调用大模型生成答案。

第 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-gpufaiss-cpu不要同时安装,否则会出现底层库冲突。

注意:实际项目落地前,需要先确认依赖版本与 Python 版本、操作系统的兼容性。这里给出的命令用于演示思路,具体版本要以自己的环境为准。

3.2 准备测试文档

先用一段模拟产品说明作为测试文档,内容包含可被检索的关键信息。

raw_text = """ 多功能巡检机器人系统说明: 一、设备供电 机器人本体采用 48V 直流电源供电,充电桩输出功率为 500W。 二、通信方式 机器人支持 Wi-Fi 6 和 5G 两种通信方式,在断网情况下可自动切换为本地规划模式。 三、安全机制 当激光雷达检测到前方 30cm 内有障碍物时,机器人会紧急制动。 四、任务调度 机器人可通过管理后台下发巡检任务,任务类型包括日常巡检、指定点位抽查和环境监测。 """

这段文本结构简单,包含编号和标题。切分时如果直接按固定长度切,很容易把“48V 供电”和“通信方式”混在一个块里,导致检索精度下降。后面会看到如何通过结构化切分改善这个问题。

3.3 编写文本切分与向量化代码

先实现一个简单的切分函数。为了演示原理,这里使用按字符固定长度切分,并允许重叠。

def split_text(text, chunk_size=100, chunk_overlap=20): 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_size=80, chunk_overlap=15) for i, c in enumerate(chunks): print(f"chunk {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_embeddings=True) embeddings = embed_texts(chunks) dim = embeddings.shape[1] index = faiss.IndexFlatIP(dim) index.add(embeddings.astype("float32"))

IndexFlatIP使用内积相似度。因为向量已经做过 L2 归一化,内积结果等价于余弦相似度,范围在 -1 到 1 之间。这里normalize_embeddings=True是关键,如果漏掉,内积的大小会受到向量长度影响,检索结果可能偏向长文本。

3.4 实现检索与生成调用

检索函数如下:

def search(query, k=3): 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, k=3) for score, chunk, idx in results: print(f"score={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_url=os.getenv("LLM_BASE_URL", "http://localhost:8000/v1"), api_key=os.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( model=os.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 jieba
import 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, k=3): 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 results

BM25 对精确词命中很有效,尤其适合产品型号、报错码等场景。但单个关键词检索也有问题:如果问题里没有出现知识库里的原词,BM25 会把相关文档丢掉。

4.3 混合检索:结合BM25和向量相似度

混合检索的目标是:两种检索结果取并集,再对并集中的文档做综合打分。

常见的融合方法有两种:

  • 加权得分加权:final_score = alpha * vector_score + beta * bm25_score
  • RRF(Reciprocal Rank Fusion):对每个结果在不同检索结果中的排名取倒数,再求和。

RRF 不依赖分数绝对值,更适合两种分布不一致的分数融合。示例实现如下:

def rrf_fusion(vector_results, bm25_results, k=60, top_n=5): 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(), key=lambda x: x[1], reverse=True)[:top_n] return [(chunks[idx], idx, score) for idx, score in sorted_idx]

每次检索时,向量检索召回 TopK,BM25 也召回 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_n=3): pairs = [[query, chunk] for chunk, idx, score in candidates] scores = reranker.predict(pairs) sorted_pairs = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) 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排名融合更平滑更关注头部排名一般保持 60
Embedding 维度取决于模型更高表达力,计算更慢轻量,但可能损失语义按模型官方说明

这里要强调:TopK 和 TopN 是两个不同阶段的参数。先召回一个较大的候选集,再精排到很小数量,是高级 RAG 的标准做法。

5.2 用检索评估指标判断效果

没有指标就谈不上调优。RAG 的评估至少包含两个层面:

  • 检索层面:命中的片段是否包含正确答案。
  • 生成层面:最终答案是否正确、可读、可溯源。

检索层面最简单的评估方式是“命中率”(Hit Rate):对每道测试题,看看正确答案所在的片段是否出现在检索结果前 K 个中。

def calculate_hit_rate(test_cases, search_func, k=5): hits = 0 for case in test_cases: query = case["query"] expected_chunk_id = case["chunk_id"] results = search_func(query, k=k) result_ids = [idx for score, chunk, idx in results] if expected_chunk_id in result_ids: hits += 1 return hits / len(test_cases)

也可以计算 MRR(Mean Reciprocal Rank):正确答案出现在第 1 位得 1 分,第 2 位得 0.5 分,第 3 位得 0.33 分,取平均。MRR 对排序位置更敏感,适合对比不同检索策略。

5.3 一个简单的消融验证流程

想验证高级检索是否有用,可以按下面的流程做对比实验:

  1. 准备 30 到 50 条覆盖不同类型问题的测试集。
  2. 在相同切块和相同生成模型下,先只用向量检索跑一遍,记录命中率和答案分。
  3. 改成“向量 + BM25”混合检索,重新跑。
  4. 加上重排序,再跑。
  5. 对比每组实验结果,形成结论。

每次只改一个变量,否则无法定位是哪个步骤带来的提升。这类消融实验也是给团队或领导讲清楚“为什么要做高级检索”的最好材料。

6. 常见问题排查链路与生产环境建议

6.1 检索为空或相关度低

现象:模型回答“资料中未找到”,但人类一眼看出知识库里有答案。

排查顺序:

  1. 检查原始文档有没有被正确解析。如果 PDF 是扫描件,没有 OCR,抽取出来可能是空内容。
  2. 检查切块后的片段是否包含关键信息。打印出 chunks,确认“答案所在句子”没有被切散。
  3. 打印检索分数。如果 Top1 分数很低,说明向量检索本身没召回相关片段。
  4. 用 BM25 再查一次。如果 BM25 能命中,说明问题出在向量模型或切块策略。
  5. 检查查询预处理的归一化逻辑。比如问题里包含英文括号、全角半角混用,可能导致向量化后差异增大。

6.2 返回结果重复或内容截断

现象:模型回答里重复出现同一句话,或者明显在“硬凑内容”。

常见原因和处理建议:

问题现象常见原因检查方式处理建议
多个检索片段内容几乎相同切块重叠过高,或重复文本入索引查看返回片段原文和元数据降低重叠率,写入索引前去重
生成内容截断提示词超出模型上下文长度计算 prompt token 数减少 TopN,或使用更长上下文模型
回答只有检索片段本身提示词把“总结”写成了“复述”检查 prompt 指令明确要求“基于资料,用自己的话概括”
引用来源对不上元数据没有随片段保留检查索引是否记录文档 ID检索结果同步返回元数据

6.3 生产环境还需要补齐哪些能力

学习环境里,用IndexFlatIP把全部向量加载进内存即可。生产环境会复杂很多,至少还需要关注:

能力生产环境要求
向量库使用分布式向量数据库,支持过滤、权限、更新、备份
索引更新支持增量写入,不阻塞查询
权限用户只能检索自己有权限的文档,需要在检索时加元数据过滤
缓存相同或相似问题可以缓存答案和检索结果
日志与监控记录检索耗时、片段来源、生成 token 数、错误率
版本回滚文档或模型升级后,如果效果下降,能快速回退
模型部署根据成本、延迟、数据隐私决定使用本地模型还是云 API

6.4 可复用的上线前检查清单

RAG 系统上线前,可以按下面清单逐项检查:

  • 是否对原始文档做了格式解析和清洗,并验证了文本可读性。
  • 是否按文档结构切块,并保留标题、来源、更新时间等元数据。
  • 是否准备了覆盖常见问题、疑难问题、越权问题的测试集。
  • 是否对比过至少两种检索策略,并用命中率、MRR 等指标验证。
  • 是否配置了重排序,并调好 TopK、TopN 参数。
  • 提示词是否限制了模型只能基于资料回答。
  • 是否处理了“资料中未找到”的回答。
  • 是否接入日志和监控,能追溯每次回答引用了哪些片段。
  • 是否评估过大模型的上下文 token 开销和响应延迟。
  • 是否做好文档更新、删除、权限隔离和版本回滚方案。

7. 从基础 RAG 到 Agentic RAG,下一步怎么走

这一步是自然的扩展方向。

基础 RAG 是“单轮检索 + 生成”。遇到复杂问题时,一次检索经常不够,比如用户连续追问“那电池容量呢”“如果断网怎么处理”,如果每次都把全部历史问题向量化检索,容易丢失上下文。

Agentic RAG 是近期工程实践里讨论较多的方向。它的核心思想是:让大模型自己决定“要不要检索”“检索什么”“是否需要多轮检索”“如何评估检索结果是否足够”。这不只是加一层循环,而是把检索、判断、再检索变成智能体行为。

对此,本文先不做展开,但建议把上面这些检索质量和评估方法掌握扎实。因为不管是基础 RAG 还是 Agentic RAG,最终效果依然依赖检索质量。

对刚接触 RAG 的开发者来说,最有价值的练习是:拿一份真实业务文档,准备 30 个问题,先用纯向量检索跑一版,再逐步加入结构化切块、混合检索和重排序,观察每个改动带来的命中率变化。这个过程比直接套框架更能理解 RAG 的本质。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询