引言
RAG的基本思路并不复杂:把问题转成向量,去文档库里检索相关内容,然后让大模型基于这些内容生成答案。但在实际落地中,同样是 RAG,不同团队做出来的效果差异极大。有些系统问一句"公司报销流程是什么"能准确返回对应制度条文,有些系统同样的问题却返回一堆不相关的内容,或者答案和文档对不上。
造成这种差距的原因,往往不是大模型本身,而是整条链路上的细节处理。文档解析的噪声、分块策略的粗糙、检索方式的单一、缺少精排步骤......任何一个环节没做好,都会拖累最终效果。
本文把 RAG 的优化拆成几个独立但相互关联的模块:
- 离线解析(包括多格式文档解析、内容清洗与规范、文本分块(Chunking)、chunk元数据标注、向量生成(Embedding)、索引构建与存储)
- 在线检索优化(包括query改写和检索策略与召回优化)
- Rerank(精排)
- RAG评估(包括检索质量评估、生成质量评估、评测数据集构建)
Part1 离线解析
很多人在检索效果不好时,第一反应是去调整 embedding 模型或者检索参数,但真正的问题经常出在数据源头,例如文档解析不干净,分块策略不合理,导致再好的检索算法也无从发挥。
1.1 多格式文档解析
企业内部的知识文档格式通常很杂:有原生 PDF、扫描件 PDF、Word 文档、Markdown、HTML 页面,还有夹杂着表格和图片的 PPT等。不同格式的解析难度差异很大,需要分别对待。
其中,PDF 是最麻烦的格式,原因有几个:
- PDF 有两种不同的类型------原生 PDF(文字可以直接提取)和扫描件 PDF(本质上是图片,需要 OCR)。用同一套解析逻辑处理两种类型,必然有一种会出问题;
- 有些PDF文档是双栏排版结构,例如学术论文、报告等,而PDF解析库通常按从上到下的顺序读取,会把两栏的文字交错在一起;
- 页眉、页脚、页码几乎在所有 PDF 中都存在,但它们对语义没有贡献,反而会污染分块内容;
- 当PDF中存在大量的表格、公式、图片时,若不对其单独处理,会丢失大量有用的信息。
-
PDF文档 :对于扫描PDF,第三方库(如
PyMuPDF、pdfplumber)无法直接提取文字,需要借助OCR技术进行识别。对于原生PDF,上述库可以提取文字,但面对表格、双栏、公式等复杂版面时,提取结果往往缺乏结构,被分页或换行打断的内容也难以正确还原。- 因此,目前主流的处理思路是,无论原生还是扫描PDF,均走 "版面检测 + OCR" 的流程,将内容重建为Markdown或json格式,便于后续分块处理。具体工具选择可参考以下情况:
- 若PDF结构简单(无表格、双栏、公式、图片等),可使用轻量的OCR 引擎,如
RapidOCR、EasyOCR。需要注意的是,这类工具本质上只做字符识别,不具备版面理解能力,若需要结构化输出,还需额外的后处理步骤。 - 若PDF含有表格、公式、双栏等复杂结构,建议使用集成了版面识别功能 的工具,如
PP-StructureV3、MinerU、Marker等。 - 若PDF中的图片本身承载了大量信息,则需要引入多模态模型 进行理解;部分工具(如
PP-StructureV3)也具备一定的图片内容解析能力。
-
Word文档 :使用
python-docx库或LangChain的DocumentLoaders读取Word文件,提取文本、表格、标题等元素,保留元数据(如标题、作者、创建时间等)。 -
PPT文档 :可通过
python-pptx库或视觉大模型(如豆包Vision模型)解析PPT,提取每页文本、图表、标注等信息;若包含复杂布局或图像,可将其转为PDF或将每页渲染为图片,结合OCR技术提取图像内容。 -
Excel文档 :使用
pandas(read_excel)或openpyxl库读取 Excel 文件,提取各 Sheet 的表格数据。两者定位有所不同:pandas更适合将数据直接转为 DataFrame 进行后续处理;openpyxl则能访问单元格级别的信息,如字体、颜色、合并单元格等格式信息。需要注意以下几类常见问题:- 合并单元格:合并区域只有左上角单元格保留值,其余为空,读取后需要手动填充;
- 多级表头 :
pandas默认处理单行表头,遇到多级表头时需指定header参数或手动解析; - 多 Sheet:默认只读取第一个 Sheet,若需处理全部 Sheet,需显式遍历;
- 嵌入图表/图片 :
pandas和openpyxl均无法提取 Excel 中嵌入的图表或图片内容,若有此类需求,需将对应 Sheet 渲染为图片后,借助 OCR 或视觉大模型处理。
1.2 内容清洗与规范
文档解析完成后,原始文本可能存在各类噪声,例如水印、解析过程中产生的乱码、多余的换行和空格等。具体解决方案包括:
- 通过正则表达式过滤乱码、特殊符号与冗余符号串;
- 对多余换行、连续空格、制表符等进行统一替换与格式规整;
- 对重复文本、无意义短句进行去重与过滤。
1.3 文本分块(Chunking)
分块策略是整个离线流程中对检索质量影响最大的环节。 chunk 越小,检索越精准,但单个 chunk 的上下文信息越少,可能让模型无法生成完整答案;chunk 越大,上下文信息越完整,但也更容易混入无关内容,导致精准匹配变得更困难。因此,没有一个普适的最优解,需要根据文档类型和业务场景调整。
以下是几种常见的分块策略:
-
固定大小分块
按照固定字符数或 token 数切割,超出长度就截断。实现简单,但完全忽略语义边界,很容易把一个完整的句子或段落切成两半。只适合文档结构极其规整的场景。
-
递归字符分割(Recursive Character Splitting)
按照优先级逐级尝试分割符(这个分割符及其顺序可自行定义),例如先尝试按双换行(段落)切割,切完之后如果某个段落还超过 chunk_size,再按单换行切,还超就按句号切,以此类推。这种方式在尊重文档自然结构的前提下控制了 chunk 大小,实际效果比纯固定大小切割好得多。
-
基于文档结构分块
如果文档本身有明确的结构信息(Markdown 的
#标题、Word 的标题样式、HTML 的<h>标签),则可以直接按结构分块。用于分块的结构同样可以自定义,例如按照#、##的结构进行分块。每个 chunk 对应一个逻辑章节,语义完整性有保障。前提是文档作者遵循了规范的写作格式。 -
语义分块(Semantic Chunking)
不按字数切,而是按语义相似度切。做法是先把文档切成句子,对每个句子生成 embedding,当相邻句子的 embedding 相似度下降超过阈值时,认为这里是语义边界,在此处分割。 这种方式理论上能找到最自然的语义切分点,但实际上有几个问题:计算成本高(每个句子都要算 embedding);阈值超参数难以调整,不同主题的文档需要不同的阈值;而且 embedding 模型本身的语义感知能力也会影响分割效果。适合对分块质量要求极高且有算力预算的场景。
-
父子 Chunk 策略(Parent-Child Chunk)
基本思路是:建立两套 chunk,小 chunk(child)用于向量检索,大 chunk(parent)用于实际传入 LLM 的上下文。在检索时,用小 chunk 做精准匹配,命中后找到它对应的父级大 chunk,把大 chunk 作为生成的上下文。这样既保留了小 chunk 检索精准的优点,又给模型提供了足够的上下文信息,是目前工程上比较成熟的折中方案。
实现上可以在 chunk 的 metadata 中存储
parent_id,检索命中后根据parent_id取出父 chunk。
Chunk 大小的经验参考:
- 短文档问答(FAQ、制度条文):chunk_size 在 256~512 token,overlap 约 50 token
- 长文档理解(技术手册、报告):chunk_size 在 512
1024 token,overlap 约 100200 token- 代码文件:按函数/类分割比按字数分割效果好得多。
overlap 的作用是缓解边界截断问题------当一段关键信息恰好跨越两个 chunk 的边界时,overlap 保证两个 chunk 都包含这段信息的一部分,减少漏检的概率
1.4 Chunk 元数据标注
分块完成后,还有一步容易被忽略但很实用的工作:给每个 chunk 打上元数据标签。
-
层级标签 :大多数文档都有层级结构,解析阶段可以维护一个层级栈,实时追踪当前位置。具体做法是:在遍历文档节点时,检测标题级别并更新栈状态。检测到"1 总则"时压入一级节点,检测到"1.1 范围"时压入二级节点,遇到同级或更高级别的标题时弹出栈顶,保持层级栈始终反映当前所在位置。每个 chunk 生成时,把当前栈的状态序列化为路径字符串,比如
"总则 > 范围",写入 metadata。层级路径写入 metadata 后,在索引构建时可以把它作为一个独立字段参与全文检索。用户搜索"解决方案"时,除了匹配 chunk 正文,层级路径中包含该关键词的 chunk 也会被召回,相当于在不改动检索架构的前提下,变相扩展了匹配范围。
-
内容标签:常见的分类有:
-
内容形式 :
paragraph(普通段落)、table、code、list等。内容形式标签在解析阶段就能确定------表格、代码块通常有明显的结构特征,且OCR模型一般会返回内容块的坐标和类型,只需要从OCR结果中取值即可。 -
业务属性 :
policy(政策条例)、guide(操作指南)、faq、glossary(术语表)等。业务属性标签相对复杂,可以通过关键词规则(比如文件名包含"操作手册"则标为guide)或者用一个轻量分类模型来判断,不需要很精确,能做粗粒度区分就够用。这类标签的价值体现在检索时的差异化策略上:表格类 chunk 可以走专门针对结构化数据优化的检索路径;政策条例类 chunk 在 Rerank 时可以给予更高的权重。
-
1.5 向量生成(Embedding)
分块完成后,每个 chunk 需要转换成向量,用于后续的相似度检索。这一步在工程上看起来简单,但有几个细节需要注意。
-
首先是Embedding模型的选择。不同模型在向量维度、语义表达能力、成本上存在差异。一旦知识库构建完成,后续查询阶段必须使用同一模型生成 query 向量,否则向量空间不一致,会导致检索结果失效。可以从以下几个维度来进行模型的选择:
模型 厂商 向量维度 上下文窗口 多语言支持 价格 核心特点与适用场景 BGE-M3 BAAI 智源 1024 8192 tokens 中英多语言(100+种) 免费开源 中文场景首选。支持稠密向量、稀疏向量和ColBERT多向量三种检索模式,MTEB中文榜长期领先。适合不知道选什么时的"无脑选择" BGE-large-zh BAAI 智源 1024 512 tokens 仅中文 免费开源 专注中文的大尺寸版本,纯中文场景精度略高于M3,但不支持多语言且上下文较短。适合纯中文短文档场景 E5-small/base/large 微软 384-1024 512 tokens 多语言 免费开源 完整尺寸梯度,small版仅33M参数,资源紧张或边缘设备首选。精度略低于BGE但推理速度快很多 Jina Embeddings v2 Jina AI 768-1024 8K tokens 多语言 免费开源 超长上下文首选。支持8K token,适合长文档(如法律条文、技术文档章节)不被截断的场景 Qwen3-Embedding 阿里巴巴 1024-4096(可动态裁剪) 32K tokens 119+种语言 免费开源 2025年新开源,MTEB多语言榜第一(70.58分)。支持MRL技术动态降维、自定义指令适配。8B版本精度超越Gemini,0.6B即可击败BGE-M3。适合多语言RAG、代码检索、长文档处理 text-embedding-3-small OpenAI 1536(可缩短至512) 8192 tokens 多语言 $0.02/1M tokens 性价比最高的商业API。比ada-002性能更强(MTEB 62.3% vs 61.0%),价格降低5倍。适合大规模应用、快速原型 text-embedding-3-large OpenAI 3072(可缩短至256/1024) 8192 tokens 多语言 $0.13/1M tokens OpenAI最强Embedding。精度最高,支持动态降维节省存储。MIRACL得分54.9%,MTEB 64.6%。适合高精度要求的关键任务 -
向量与原始数据的绑定存储。embedding 生成后,不能只存向量,还需要将元数据信息一并存储到向量数据库,例如:
- 原始文本 chunk
- 文档来源(file_id、url 等)
- chunk 在原文中的位置(page、offset)
- 结构信息(标题层级、段落编号)
- 这些信息在召回后用于重构上下文,否则仅有向量无法支持可读的回答生成。
-
批量处理与性能优化。在实际场景中,chunk 数量往往达到万级,如果逐条调用 embedding 接口,会带来明显的延迟和成本问题。常见做法包括:
- 使用 batch 接口(一次提交多个 chunk)
- 控制 batch size(避免请求过大或超时)
- 引入异步队列(如 Celery、Kafka)进行离线处理
- 在高并发场景下做请求限流,避免触发 API 限制
1.6 索引构建与存储
向量生成完成后,需要构建索引以支持高效检索。
- 向量数据库选型
| 数据库 | 定位 | 适合场景 |
|---|---|---|
| Faiss | 本地纯向量库,无服务端 | 快速验证,数据量不大 |
| Qdrant | 轻量,支持 payload 过滤 | 中小规模,功能均衡 |
| Milvus | 分布式,大规模向量 | 生产环境,TB级数据 |
| Weaviate | 内置混合检索(BM25+向量) | 想减少技术栈复杂度 |
| pgvector | PostgreSQL扩展 | 已有 PG 数据库,不想引入新服务 |
-
索引类型的选择
Flat Index(精确检索): 暴力计算所有向量的距离,精度 100%,但随数据量增大线性变慢。数据量在十万以内可以接受,超过后查询延迟显著增加(百万级数据查询可达秒级)。
IVF(倒排文件索引): :先把向量聚类,检索时只在最近的几个类簇中搜索。速度快,但有精度损失(ANN,近似最近邻)。带过滤条件时性能稳定,内存占用较低,适合高过滤比例或内存受限场景。
HNSW(分层导航小世界图): 一种基于图结构的 ANN 算法,通过构建多层导航图实现快速检索。上层为稀疏的"高速公路"用于快速定位,下层为稠密图用于精确搜索,查询时可从顶层逐层下降,快速逼近目标。这是目前综合性能最好的 ANN 算法,检索速度快(对数级复杂度),精度损失小,但内存占用较高。适合无过滤或低过滤场景下的生产环境,但高过滤比例下性能可能劣于 IVF。
-
元数据存储
向量数据库中除了存向量,还应该存 chunk 的元数据,用于后续的过滤和溯源,例如:
json{ "doc_id": "policy_2024_001", "source": "员工手册 v3.pdf", "section": "第四章 报销制度", "page": 12, "chunk_index": 3, "created_at": "2024-01-15", "department": "HR" }合理的元数据设计可以支持"只在 HR 相关文档中检索"、"只检索 2024 年以后的文件"等过滤条件,在不降低精度的前提下缩小检索范围,提升效率。
-
混合索引
只依赖向量检索,很多时候覆盖不全。比如用户输入的是专有名词、缩写,或者某个关键词必须精确匹配时,向量相似度不一定能把相关内容找出来。因此,在向量数据库之外,通常还会再维护一套基于关键词的全文索引(常见做法是基于 BM25 或 Elasticsearch)。
在实际系统中,这两套索引是同时存在的 :一套负责语义相似度检索,一套负责关键词匹配。查询时会同时走两条路径,各自召回一批候选结果,然后再统一做排序和筛选,得到最终结果。这是目前生产环境中比较常见的索引架构。
Part2 在线检索优化
离线链路保证了知识库的质量,而在线检索决定了能否从这些数据中找到真正相关的内容。
2.1 query改写
用户的原始输入是在线检索质量最大的不确定因素之一。真实的用户 query 往往存在表达模糊、多轮对话中的指代缺失、意图模糊等问题。但不是所有场景都需要 query 改写。单轮、表达清晰的查询直接检索即可,改写反而增加延迟。建议根据 query 的复杂度动态决定是否改写,或者针对多轮对话场景单独部署改写模块。
以下是几种常见的改写策略:
-
Step-Back Prompting
Step-Back Prompting(后退提示法)一种提升LLM推理能力的提示工程技术。它通过引导模型从细节问题退一步,思考更抽象、高层次的原则或原理解,从而在解决复杂问答、逻辑推理和科学问题时产生更精确的答案。
例如,用户问"Python 的 GIL 锁对多线程 IO 密集型任务有影响吗",Step-Back 会先检索"Python GIL 是什么、它的作用机制",获得这个背景知识后再回答原始问题。
代码示例:
jsonimport os import openai os.environ['OPENAI_API_KEY'] = str("xxxxxxxxxxxxxxxxxxxxxxxxxxx") from langchain.chat_models import ChatOpenAI from langchain.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate from langchain.schema.output_parser import StrOutputParser from langchain.schema.runnable import RunnableLambda examples = [ { "input": "Could the members of The Police perform lawful arrests?", "output": "what can the members of The Police do?" }, { "input": "Jan Sindel's was born in what country?", "output": "what is Jan Sindel's personal history?" }, ] example_prompt = ChatPromptTemplate.from_messages( [ ("human", "{input}"), ("ai", "{output}"), ] ) few_shot_prompt = FewShotChatMessagePromptTemplate( example_prompt=example_prompt, examples=examples, ) prompt = ChatPromptTemplate.from_messages([ ("system", """You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples:"""), # Few shot examples few_shot_prompt, # New question ("user", "{question}"), ]) question_gen = prompt | ChatOpenAI(temperature=0) | StrOutputParser() question = "was chatgpt around while trump was president?" question_gen.invoke({"question": question}) -
Multi-Query
把用户的原始 query 改写为多个角度的子查询,分别检索,最后合并结果去重。
比如用户问"这个产品和竞品相比有什么优势",可以改写为:
- "产品的核心功能特点是什么"
- "竞品的主要不足有哪些"
- "用户选择我们产品的主要原因"
多个 query 覆盖了问题的不同维度,召回率通常会有提升。代价是 LLM 调用次数增加,延迟和成本随之上升。
-
HyDE(Hypothetical Document Embeddings)
HyDE(Hypothetical Document Embeddings,假设性文档嵌入)让 LLM 先根据用户问题生成一个"假设性答案",然后用这个假设答案的 embedding 去检索,而不是直接用问题的 embedding。其背后的逻辑是,问题的表达方式和文档中答案的表达方式往往差异很大(问题是疑问句,答案是陈述句),用假设答案去检索,embedding 空间的距离会更近。 HyDE 在专业领域问答(医疗、法律)中场景中提升较为明显,适用于文档措辞和用户提问差异很大的情况。但它也存在明显的风险:如果 LLM 生成的假设答案本身就是错的(幻觉),用错误的答案去检索会带偏结果。建议在评估集上验证效果后再决定是否上线。 代码示例:
jsonfrom langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain.vectorstores import FAISS # 1. 初始化 llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) emb = OpenAIEmbeddings() vs = FAISS.load_local("your_index", emb) # 2. 简单 reranker(用 LLM 打分) def rerank(query, docs, top_k=3): scored = [] for d in docs: prompt = f"问题:{query}\n文档:{d.page_content[:300]}\n相关性打分(0-10):" score = llm.invoke(prompt).content.strip() try: score = float(score) except: score = 0 scored.append((score, d)) return [d for _, d in sorted(scored, reverse=True)[:top_k]] # 3. HyDE + multi + hybrid def hyde_retrieve(query, k=5): # ---- 控制长度 + multi HyDE ---- prompt = f"针对问题生成3个不同的简短答案(每个不超过50字):\n问题:{query}" hypos = llm.invoke(prompt).content.split("\n") # ---- HyDE 检索 ---- docs = [] for h in hypos: docs += vs.similarity_search(h, k=2) # ---- 原始 query 检索(hybrid)---- docs += vs.similarity_search(query, k=2) # ---- 去重 ---- unique_docs = list({d.page_content: d for d in docs}.values()) # ---- rerank ---- return rerank(query, unique_docs, top_k=3) # 4. 使用 query = "Python 的 GIL 对多线程 IO 有影响吗?" docs = hyde_retrieve(query) for d in docs: print(d.page_content[:200]) -
对话历史压缩(多轮对话场景)
在多轮对话中,用户的每一轮提问都可能依赖之前的上下文。直接把当前轮的 query 拿去检索通常效果很差。
常见做法是用 LLM 将对话历史和当前问题合并改写为一个独立的、完整的检索 query。例如:
erlang对话历史: 用户:员工报销有哪些类型? 助手:分为差旅报销、日常费用报销和项目报销三类。 用户:差旅报销需要哪些材料? 改写后的独立 query:差旅报销需要提交哪些材料和凭证?
2.2 检索策略与召回优化
检索策略包括稀疏检索、密集检索和混合检索:
-
稀疏检索(BM25) :基于词频(TF)和逆文档频率(IDF)计算相关性得分,在词汇空间中直接进行匹配,不依赖向量表示。 适用于以关键词为核心的查询场景,例如专有名词、型号、编号等精确匹配需求,同时具有较强的可解释性,可以明确看到哪些词对最终得分产生影响;计算开销较低,响应速度较快。
但其对词汇形式较为敏感,无法处理词汇表外(OOV)的词,也难以识别同义或语义相近的表达,因此在语义检索场景中效果有限。
-
密集检索(向量检索) :基于 embedding 将文本映射为向量,通过向量相似度进行匹配。更适合语义驱动的查询,可以处理同义词、近义表达以及不同语言下的相同语义,在开放问答或自然语言查询中表现更稳定。在需要精确匹配关键词的场景中,结果可能不够可靠,并且效果依赖于 embedding 模型的质量。
-
混合检索(Hybrid Search) :同时执行稀疏检索和密集检索,并对两路结果进行融合。这种方式可以在一定程度上兼顾关键词匹配能力与语义理解能力,适合大多数实际应用场景。由于两类检索的得分不在同一尺度上,通常不会直接相加,而是采用基于排序的位置融合方法,例如 RRF(Reciprocal Rank Fusion),这里不再赘述。
另外,在向量检索的基础上,利用 metadata 做 pre-filter(前置过滤),可以在不牺牲相关性的前提下缩小检索范围。并且过滤应该在向量数据库层面做,而不是先检索 top-1000 再在应用层过滤。后者不仅浪费计算资源,还可能导致过滤后结果数量不足 top-k。
比如:只在"2024 年度文件"中检索、只检索"财务部"相关文档、排除已下线的制度文件。
而召回阶段 的核心目标是:在经检索返回的 top-k 结果里,尽量让所有真正相关的文档都出现,同时控制不相关文档的比例。若k 太小(如 3),相关文档可能漏掉;k 太大(如 50),引入大量噪声,后续 Rerank 和生成的压力都会上升。
一个实用的思路是根据问题类型(可用LLM做分类)动态调整 k:简单单文档问题取较小的 k,复杂多文档综合类问题取较大的 k,再交给 Rerank 做精排。
Part3 Rerank(精排)
经过检索阶段,我们得到了 top-k 个候选文档。这些候选文档的质量参差不齐,排序也未必可靠------向量检索的 top-1 不一定是最相关的文档。Rerank 的作用就是对这个候选集做更精细的重新排序,把真正相关的文档排到前面。
那为什么 Rerank 更准确呢?要理解这个问题,需要先理解向量检索(Bi-Encoder) 和 Rerank(Cross-Encoder) 在计算方式上的本质区别。
- Bi-Encoder(向量检索阶段): Query 和 Document 分别独立编码,生成各自的向量,再计算两个向量的相似度(余弦相似度或点积)。因为 query 和 document 在编码时没有互相"看到"对方,表达能力有限。但速度很快,适合在百万级文档中做粗检索。
- Cross-Encoder(Rerank 阶段): Query 和 Document 拼接在一起,送入 Transformer 模型做联合编码。模型在计算时,query 的每个 token 都能通过注意力机制看到 document 的每个 token,反之亦然。这种深度交叉计算的精度比 Bi-Encoder 高得多,但代价是无法预先计算文档的向量,每次都要在线计算,速度慢,不适合大规模检索。正因如此,Rerank 适合在小候选集(20~100个)上做精排,而不是全量文档上做检索。
-
使用Rerank模型
Rerank 通常对检索阶段的 top-20 到 top-50 做精排,最终保留 top-5 到 top-10 送入生成阶段。候选集太小,可能漏掉相关文档;候选集太大,Rerank 的计算时间会线性增加。使用Rerank模型通常带来 100~500ms 的额外延迟(取决于候选集大小和模型规模)。对延迟敏感的场景(如实时对话),可以考虑使用轻量级的 Rerank 模型(如
bge-reranker-base)而不是大模型,或者在 top-3 这样的小候选集上做 Rerank 以控制延迟。Rerank模型的选择:
模型 核心优势 典型延迟 适用场景 BGE-Reranker-v2-m3 中文优化好,多语言支持,轻量易部署 50-150ms 通用 RAG、中文场景、资源受限环境 Cohere Rerank 工业级稳定,多语言,有 Fast 版可选 100-300ms 企业级应用、生产环境、多语言需求 Qwen3-Reranker-0.6B 小模型 SOTA,6 亿参数超 BGE,端侧友好 30-80ms 延迟敏感、移动端/边缘部署、中文优先 Jina Reranker v2 100+ 语言、代码/JSON 支持、Agentic RAG 优化 50-200ms 多语言检索、代码搜索、Agent 工具调用 Voyage Rerank 顶级精度,金融法律领域表现突出 200-500ms 高精度需求、专业领域、不差钱场景 -
使用LLM Rerank
用大语言模型直接对候选文档打分或排序,是 Rerank 的一种进阶形式。常见方式:
- Pointwise: 逐个让 LLM 对 (query, document) 给出相关性得分(1~5分),再按得分排序
- Listwise: 把所有候选文档一次性给 LLM,让它直接输出排序结果
LLM Rerank 的效果通常比专门的 Cross-Encoder 模型更好,尤其是在需要复杂推理才能判断相关性的场景。主要障碍是延迟(大模型推理慢)和成本(每次召回都要调用 LLM)。适合对准确性要求极高、延迟容忍度较高的离线处理或低频查询场景。
Part4 RAG评估
没有评估,优化就是盲目的。RAG 的评估比单纯的 NLP 任务评估更复杂,因为它涉及检索和生成两个相互独立的环节,需要分别衡量。
检索质量评估
检索评估的前提是有标注数据:给定一组 (query, 相关文档列表) 的标注对,对检索结果打分。
-
MRR(Mean Reciprocal Rank)------平均排序倒数
- MRR 关注的是第一个相关文档出现在检索结果中的位置。其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> r a n k i rank_i </math>ranki第 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i个 query 对应的第一个相关文档的排名。如果第一个相关文档排在第 1 位,得分为 1;排在第 2 位,得分为 0.5;排在第 3 位,得分为 0.33,以此类推。
<math xmlns="http://www.w3.org/1998/Math/MathML"> M R R = 1 ∣ Q ∣ ∑ i = 1 ∣ Q ∣ 1 r a n k i MRR = \frac{1}{|Q|} \sum_{i=1}^{|Q|} \frac{1}{rank_i} </math>MRR=∣Q∣1∑i=1∣Q∣ranki1
MRR 适合"用户只需要找到一个相关文档就够"的场景,比如问答系统中用户希望找到一个能回答问题的文档段落。它对排第一个的相关文档非常敏感,如果相关文档排在第 10 位,MRR 贡献只有 0.1,几乎可以忽略。
-
NDCG(Normalized Discounted Cumulative Gain)
NDCG 考虑了排序中所有相关文档的位置,并且支持多级相关性 (而不仅仅是相关/不相关的二分类)。其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> r e l i rel_i </math>reli是位置 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i的文档的相关性得分(如 0、1、2 分别表示不相关、弱相关、强相关), <math xmlns="http://www.w3.org/1998/Math/MathML"> I D C G k IDCG_k </math>IDCGk是理想排序下的 DCG,用于归一化。
<math xmlns="http://www.w3.org/1998/Math/MathML"> D C G k = ∑ i = 1 k r e l i log 2 ( i + 1 ) \mathrm{DCG}k = \sum{i=1}^{k} \frac{rel_i}{\log_2(i + 1)} </math>DCGk=∑i=1klog2(i+1)reli
<math xmlns="http://www.w3.org/1998/Math/MathML"> N D C G k = D C G k I D C G k \mathrm{NDCG}_k = \frac{\mathrm{DCG}_k}{\mathrm{IDCG}_k} </math>NDCGk=IDCGkDCGk
-
Precision@k
Precision@k 是指 top-k 结果中相关文档占的比例。它简单、直观,但它不区分排序位置------排第 1 位和排第 k 位的相关文档对 Precision@k 的贡献相同。适合用于粗略评估,或者作为业务指标("我们的 top-5 里有多大比例是相关的")。
<math xmlns="http://www.w3.org/1998/Math/MathML"> P r e c i s i o n @ k = top- k 中相关文档数 k \mathrm{Precision}@k = \frac{\text{top-}k\ \text{中相关文档数}}{k} </math>Precision@k=ktop-k 中相关文档数
三个指标的适用场景对比:
指标 最关注的问题 支持多级相关性 位置敏感度 适用场景 MRR 第一个相关结果在哪里 否 高 (仅最高位) 单答案问答 NDCG@k 整体排序质量 是 高 (全体) 文档推荐、搜索 Precision@k top-k的命中率 香 无 业务粗粒度监控
生成质量评估
检索质量好不代表最终答案质量好,还需要评估生成环节。通常从以下方面进行考量:
-
Faithfulness(忠实度):答案中的每个陈述是否都有检索内容作为依据,有没有产生幻觉(凭空生成的内容)。
-
Answer Relevance(答案相关性):生成的答案是否真正回答了用户的问题,而不是返回了相关但没有回答问题的内容。
-
Context Precision / Context Recall:
- Context Precision:检索到的文档中,真正对生成答案有帮助的比例(排除无关的检索结果)。
- Context Recall:生成答案所需的信息,在检索到的文档中的覆盖率(有没有漏检关键信息)。
RAGAS 是目前最常用的 RAG 评估框架,可以自动化计算上述指标,支持用 LLM 作为评判者(LLM-as-judge)。但使用时也需注意:LLM 评判者本身也会犯错,建议在关键决策前做人工抽查验证。
评测数据集的构建
评估体系中最难的部分往往不是跑指标,而是有没有好的评测数据集。
- 合成数据生成:没有标注数据时,可以用 LLM 对文档自动生成问题-答案对:给 LLM 一段文档,让它生成若干个基于该文档能回答的问题,以及参考答案。这种方式成本低、速度快,但生成的问题往往过于直接、语言表达与真实用户有差距。
- 人工标注:让真实用户或领域专家提供真实问题,再标注相关文档。质量高但成本高,建议优先标注高频问题和边界 case。
- 增量维护:线上系统运行一段时间后,把用户的真实 query 和反馈(点赞/点踩、追问行为)作为评测数据的补充来源,持续更新评测集,避免评测集和实际分布脱节。
总结
RAG 的优化点很多,但精力和时间总是有限的。具体从哪个环节进行改进,需要依赖对自己业务数据和用户行为的深度理解。本文提供的是方向和方法,具体参数和策略的选择,还需要在真实数据上反复实验和评估。