当前位置: 首页 > news >正文

RAG学习(六)——检索优化技术进阶

检索优化

在基础的 RAG 流程中,依赖向量相似度从知识库中检索信息。然而,这种方法存在一些固有的局限性,例如最相关的文档不总是在检索结果的顶端,以及语义理解的偏差等。为了构建更强大、更精准的生产级 RAG 应用,需要引入更高级的检索技术。

一、重排序

1.1 RRF(Reciprocal Rank Fusion)

把来自多路检索器 (BM25、稠密向量、规则召回、实体召回等)的排名结果做"倒数融
合”。同一文档在不同榜单的名次越靠前,融合分越高。公式(常用k=60):k=60):k=60):
RRF(d)=∑i=1m1k+ranki(d)\mathrm{RRF}(d)=\sum_{i=1}^m\frac1{k+\mathrm{rank}_i(d)} RRF(d)=i=1mk+ranki(d)1
其中rank⁡i(d)\operatorname{rank}_i(d)ranki(d)是文档ddd在第iii个召回器中的名次;不存在则忽略。

实现流程(推理)

  1. 对同一 query 跑多路召回,得到若干个 Top-K 排名列表;
  2. 设定 kkk(通常 10~100 之间,越大越“保守”);
  3. 遍历所有出现过的文档,按上式累加分;
  4. 以 RRF 分数降序得到融合榜;
    5)(可选)再接 Cross-Encoder/LLM 做精排。

1.2 RankLLM(用大模型做排序/重排)

把候选文档连同查询一起给到 LLM,通过指令+格式约束让 LLM 输出相关性排序分值。可做 pointwise(逐文打分)、pairwise(两两比较)或 listwise(一次比较整列)。

实现流程
A. 推理(常见 listwise)

  1. 召回 Top-N(如 50 条)候选文档;
  2. 构造 Prompt:包含 query、编号后的候选文档(摘要/片段)、输出格式(如“返回排序后的 doc_id 列表与 0–5 分值”);
  3. 温度设为 0,max_tokens 设定足够覆盖编号与分值;
  4. 解析 LLM 输出:得到排序或分值;
    5)(可选)异常回退:若解析失败→改为 pairwise 小批量裁判;
  5. 输出精排结果。

B. 训练(可选,蒸馏/偏好对齐)

  • 用人工标注或弱监督(点击/阅读时长)构造 (q, d⁺, d⁻);
  • 用 LLM 做 pairwise 反馈或监督微调(SFT/LoRA);
  • 也可把 Cross-Encoder 分数蒸馏给小 LLM,用于离线打分。

一个典型的提示词示例如下:

以下是一个文档列表,每个文档都有一个编号和摘要。同时提供一个问题。请根据问题,按相关性顺序列出您认为需要查阅的文档编号,并给出相关性分数(1-10分)。请不要包含与问题无关的文档。示例格式:
文档 1: <文档1的摘要>
文档 2: <文档2的摘要>
...
文档 10: <文档10的摘要>问题: <用户的问题>回答:
Doc: 9, Relevance: 7
Doc: 3, Relevance: 4
Doc: 7, Relevance: 3

1.3 Cross-Encoder(联合编码精排,MonoBERT/MonoELECTRA 等)

它的工作原理是将查询(Query)和每个候选文档(Document)拼接成一个单一的输入(例如,[CLS] query [SEP] document [SEP]),然后将这个整体输入到一个预训练的 Transformer 模型(如 BERT)中,模型最终会输出一个单一的分数(通常在 0 到 1 之间),这个分数直接代表了文档与查询的相关性

  • 由于是联合编码,模型能捕捉到细粒度交互(词-词对齐、上下文消歧),精排质量很高;
  • 代价是:每个 (q, d) 都要独立前向一次,计算成本高。

在这里插入图片描述

示例

  • Query: “Who wrote The Great Gatsby?”
  • 候选 d1: “F. Scott Fitzgerald wrote the novel in 1925 …”
  • 候选 d2: “The story is set in the Jazz Age …”

Cross-Encoder 打分:score(d1)=0.92 > score(d2)=0.41,最终把 d1 排在前。

1.4 ColBERT(Contextualized Late Interaction)

olBERT(Contextualized Late Interaction over BERT)是一种创新的重排模型,它在 Cross-Encoder 的高精度和双编码器(Bi-Encoder)的高效率之间取得了平衡3。采用了一种“后期交互”机制。

其工作流程如下:

  1. 独立编码:ColBERT 分别为查询(Query)和文档(Document)中的每个 Token 生成上下文相关的嵌入向量。这一步是独立完成的,可以预先计算并存储文档的向量,从而加快查询速度。

  2. 后期交互:在查询时,模型会计算查询中每个 Token 的向量与文档中每个 Token 向量之间的最大相似度(MaxSim)。
    s(q,d)=∑t∈qmax⁡u∈dcos⁡(et,eu)s(q,d)=\sum_{t\in q}\max_{u\in d}\cos(\mathbf{e}_t,\mathbf{e}_u) s(q,d)=tqudmaxcos(et,eu)

  3. 分数聚合:最后,将查询中所有 Token 得到的最大相似度分数相加,得到最终的相关性总分。

示例(法律问答)

  • Query tokens:["劳动", "合同", "解除", "赔偿"]
  • 文档 d1 token 向量包含“解除”“经济补偿金”等高相似词;
  • 对每个 query token 在 d1 里找最相似 token,相似度相加→s(q,d1)=3.87
  • d2 缺少“赔偿”相关表达 → s(q,d2)=2.41
  • 排名:d1 > d2。

二、压缩

“压缩”技术旨在解决一个常见问题:初步检索到的文档块(Chunks)虽然整体上与查询相关,但可能包含大量无关的“噪音”文本。将这些未经处理的、冗长的上下文直接提供给 LLM,不仅会增加 API 调用的成本和延迟,还可能因为信息过载而降低最终生成答案的质量。

2.1 两类思路

  1. 内容提取(Extractive)

    • 不改写原文,只“挑句/挑段”。

    • 方式:句级/段级相关性打分 → 选 Top-k 或 Top-p,再按窗口扩展少量上下文。

    • 优点:可追溯、低幻觉风险;缺点:有冗余、可读性一般。

  2. 内容改写(Abstractive / Rewrite)

    • 面向问题做查询聚焦摘要(Query-Focused Summarization, QFS),把多段证据浓缩成要点。

    • 优点:压缩比高、读起来紧凑;缺点:可能引入改写误差,需要“基于证据”的约束或引用。

实践中常把两者串联:先“提取”做粗压,再“改写”做细压。

2.2 通用流程

流程 A:提取式压缩管线(推荐起步)

  1. 细粒度切分
    • 按句子/小段落切分(SSplit),保留段落边界与来源标识(doc_id, start, end)。
  2. 相关性打分(句/段级)
    • 嵌入相似度cos(emb(sentence), emb(query))
    • Cross-Encoderscore = CE([CLS] q [SEP] s [SEP])(更准但慢)。
  3. 选择与阈值
    • 选 Top-k 句/段;或设阈值 τ\tauτ,保留 score ≥ τ 的片段。
    • 对每个命中片段做 窗口扩展:左右各带 n 句,避免断句丢信息。
  4. 文档级过滤
    • 若某文档被保留的片段数/分数总和过低,整篇丢弃(你的“文档过滤”)。
  5. 去重与合并
    • 对近似重复/同义表达做聚类(MinHash/SimHash/向量聚类),只留代表性句。
  6. 预算分配与截断
    • 计算每个文档的权重(来自检索得分/重排分),按 softmax(weight) * B 分配 token。
    • 过长即截断或进一步精炼。
  7. 结构化拼装
    • 以“要点+证据块”的结构返回,保留出处(doc_id, span)便于可追溯与反证。

流程 B:LLM 改写式压缩管线(QFS)

  1. 证据准备:用流程 A 先做粗提取,保证输入干净。
  2. 压缩提示词(Prompt)
    • 明确任务:仅保留与查询直接相关事实;
    • 强制 引用来源(句末加 [doc_id:line_start-line_end]);
    • 禁止外推(“不得引入未在证据中出现的信息”)。
  3. 生成与解析:输出“要点清单/表格/三元组”之一(结构化有利于控制长度)。
  4. 一致性校验
    • 逐条要点用检索到的证据反查(entailment/是否可在原文找到支撑)。
    • 不一致则丢弃或回退到提取式结果。
  5. 最终拼装:把压缩后的“要点+引用”作为 RAG 上下文传给回答模型。

三、校正

传统的 RAG 流程有一个隐含的假设:检索到的文档总是与问题相关且包含正确答案。然而在现实世界中,检索系统可能会失败,返回不相关、过时或甚至完全错误的文档。如果将这些“有毒”的上下文直接喂给 LLM,就可能导致幻觉(Hallucination)或产生错误的回答。

校正检索(Corrective-RAG, C-RAG) 正是为解决这一问题而提出的一种策略6。思路是引入一个“自我反思”或“自我修正”的循环,在生成答案之前,对检索到的文档质量进行评估,并根据评估结果采取不同的行动。

在这里插入图片描述

流程解析

  1. Retrieval(初次检索)

    • 输入:问题 xxx。

    • 输出:候选文档 d1,d2,...d_1,d_2,...d1,d2,...。来源可以是向量库、知识库或数据库。

    • 目的:拿到可能相关的材料,质量未判定。

  2. Retrieval Evaluator(检索评估器)

    • 核心问题(图中蓝色对话框):“这批检索文档对回答 xxx 是否足够且正确?”

      • 输出三类标签:
        • Correct:信息充分且一致;
        • Ambiguous:有信息但不完备/互相矛盾/指向多答案;
        • Incorrect:明显答非所问或关键信息缺失。
    • 实现要点(可多信号融合):

      1. 支持度/蕴含:NLI 或 LLM 判断“文档是否支持对问题的关键断言”;
        1. 一致性:对每篇文档独立问答,做多数表决/一致性分;
        2. 覆盖度:是否包含回答所需槽位(人名、时间、定义等);
        3. 阈值:给出 support_scoreconflict_scorecoverage,映射到三类标签。
  3. Knowledge Refinement(内部证据精炼 → kink_{in}kin

    • 触发:Ambiguous(也可在 Correct 时做轻度压缩)。

      • 图中管线:Decompose → Strip → Filter → Recompose
        • Decompose:把文档切为“原子事实/句子/三元组”;
        • Strip:去脚注、例子、广告、非答案句;
        • Filter:按“与 xxx 的相关性 + 互证一致性”筛选片段(Cross-Encoder/MMR/LLM 判别);
        • Recompose:把保留片段重组为查询聚焦摘要(带引用),即 kink_{in}kin
    • 作用:降噪、去冲突、补充上下文完整性,但只用内部检索到的材料。

  4. Knowledge Searching(外部搜证 → kexk_{ex}kex

    • 触发:Incorrect(信息错位/缺失严重),有时 Ambiguous 也会补搜。

    • 图中管线:Rewrite → Web Search → Select

      • Rewrite:把 xxx 改写成可检索性强的查询(补关键词/限定来源,如“site:wikipedia 关键字”),必要时做分解查询
        • Web Search:到外部(Web/API/新库)检索一批候选 k1,k2,…k_1,k_2,…k1,k2,
        • Select:重排+核验(与 kexk_{ex}kex一致性、来源可信度、时效),选出 kexk_{ex}kex
  5. Generation(最终生成)

    • Correct: 直接用x+dx+dx+d或轻度kink_{in}kin生成;

    • Ambiguous: 用x+kinx+k_{in}x+kin (内部精炼)生成;

    • Incorrect: 用x+kin+kexx+k_{in}+k_{ex}x+kin+kex (内部精炼 + 外部新证)生成。。

    • 模型提示会要求引用证据、避免无依据推断。

四、代码示例

import os
from langchain_community.vectorstores import FAISS
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
from langchain_deepseek import ChatDeepSeek# 导入ColBERT重排器需要的模块
from langchain.retrievers.document_compressors.base import BaseDocumentCompressor
from langchain.retrievers.document_compressors import DocumentCompressorPipeline
from langchain_core.documents import Document
from typing import Sequence
import torch
from transformers import AutoTokenizer, AutoModel
import torch.nn.functional as Fclass ColBERTReranker(BaseDocumentCompressor):"""ColBERT重排器"""def __init__(self, **kwargs):super().__init__(**kwargs)model_name = "bert-base-uncased"# 加载模型和分词器object.__setattr__(self, 'tokenizer', AutoTokenizer.from_pretrained(model_name))object.__setattr__(self, 'model', AutoModel.from_pretrained(model_name))self.model.eval()print(f"ColBERT模型加载完成")def encode_text(self, texts):"""ColBERT文本编码"""inputs = self.tokenizer(texts,return_tensors="pt",padding=True,truncation=True,max_length=128)with torch.no_grad():outputs = self.model(**inputs)embeddings = outputs.last_hidden_stateembeddings = F.normalize(embeddings, p=2, dim=-1)return embeddingsdef calculate_colbert_similarity(self, query_emb, doc_embs, query_mask, doc_masks):"""ColBERT相似度计算(MaxSim操作)"""scores = []for i, doc_emb in enumerate(doc_embs):doc_mask = doc_masks[i:i+1]# 计算相似度矩阵similarity_matrix = torch.matmul(query_emb, doc_emb.unsqueeze(0).transpose(-2, -1))# 应用文档maskdoc_mask_expanded = doc_mask.unsqueeze(1)similarity_matrix = similarity_matrix.masked_fill(~doc_mask_expanded.bool(), -1e9)# MaxSim操作max_sim_per_query_token = similarity_matrix.max(dim=-1)[0]# 应用查询maskquery_mask_expanded = query_mask.unsqueeze(0)max_sim_per_query_token = max_sim_per_query_token.masked_fill(~query_mask_expanded.bool(), 0)# 求和得到最终分数colbert_score = max_sim_per_query_token.sum(dim=-1).item()scores.append(colbert_score)return scoresdef compress_documents(self,documents: Sequence[Document],query: str,callbacks=None,) -> Sequence[Document]:"""对文档进行ColBERT重排序"""if len(documents) == 0:return documents# 编码查询query_inputs = self.tokenizer([query],return_tensors="pt",padding=True,truncation=True,max_length=128)with torch.no_grad():query_outputs = self.model(**query_inputs)query_embeddings = F.normalize(query_outputs.last_hidden_state, p=2, dim=-1)# 编码文档doc_texts = [doc.page_content for doc in documents]doc_inputs = self.tokenizer(doc_texts,return_tensors="pt",padding=True,truncation=True,max_length=128)with torch.no_grad():doc_outputs = self.model(**doc_inputs)doc_embeddings = F.normalize(doc_outputs.last_hidden_state, p=2, dim=-1)# 计算ColBERT相似度scores = self.calculate_colbert_similarity(query_embeddings,doc_embeddings,query_inputs['attention_mask'],doc_inputs['attention_mask'])# 排序并返回前5个scored_docs = list(zip(documents, scores))scored_docs.sort(key=lambda x: x[1], reverse=True)reranked_docs = [doc for doc, _ in scored_docs[:5]]return reranked_docs# 初始化配置
hf_bge_embeddings = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-large-zh-v1.5"
)llm = ChatDeepSeek(model="deepseek-chat", temperature=0.1, api_key=os.getenv("DEEPSEEK_API_KEY")
)# 1. 加载和处理文档
loader = TextLoader(r"all-in-rag\data\C4\txt\ai.txt", encoding="utf-8")
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
docs = text_splitter.split_documents(documents)# 2. 创建向量存储和基础检索器
vectorstore = FAISS.from_documents(docs, hf_bge_embeddings)
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})# 3. 设置ColBERT重排序器
reranker = ColBERTReranker()# 4. 设置LLM压缩器
compressor = LLMChainExtractor.from_llm(llm)# 5. 使用DocumentCompressorPipeline组装压缩管道
# 流程: ColBERT重排 -> LLM压缩
pipeline_compressor = DocumentCompressorPipeline(transformers=[reranker, compressor]
)# 6. 创建最终的压缩检索器
final_retriever = ContextualCompressionRetriever(base_compressor=pipeline_compressor,base_retriever=base_retriever
)# 7. 执行查询并展示结果
query = "AI还有哪些缺陷需要克服?"
print(f"\n{'='*20} 开始执行查询 {'='*20}")
print(f"查询: {query}\n")# 7.1 基础检索结果
print(f"--- (1) 基础检索结果 (Top 20) ---")
base_results = base_retriever.get_relevant_documents(query)
for i, doc in enumerate(base_results):print(f"  [{i+1}] {doc.page_content[:100]}...\n")# 7.2 使用管道压缩器的最终结果
print(f"\n--- (2) 管道压缩后结果 (ColBERT重排 + LLM压缩) ---")
final_results = final_retriever.get_relevant_documents(query)
for i, doc in enumerate(final_results):print(f"  [{i+1}] {doc.page_content}\n")

流程分析:

  1. 加载向量模型/LLM
  • HuggingFaceBgeEmbeddings("BAAI/bge-large-zh-v1.5"):中文领域强的稠密向量,用来建库+粗召回。
  • ChatDeepSeek("deepseek-chat"):给 LLMChainExtractor抽取式压缩(从候选文档中只保留与查询相关的句/段)。
  1. 加载原始文档并切块
  • TextLoader(...).load() 读入 ai.txt;
  • RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100) 语义/字符混合的“滑窗切块”。
  1. 建立向量库与基础检索器
  • FAISS.from_documents(docs, hf_bge_embeddings):把每个 chunk 编码进 FAISS;
  • vectorstore.as_retriever(k=20)第一阶段召回(粗召回),返回 Top-20 个 chunk。
  1. 构建压缩流水线
  • 你自定义的 ColBERTReranker(Token 级 MaxSim 晚交互)对 Top-20 做重排,保留前 5;
  • LLMChainExtractor 对这 5 个 chunks 做查询聚焦的抽取式压缩(剔掉不相关内容)。
  • 这两步被 DocumentCompressorPipeline([reranker, compressor]) 串起来。
  1. 最终检索器
  • ContextualCompressionRetriever(base_retriever=..., base_compressor=pipeline)
    每次 get_relevant_documents(query) → 先粗召回 20 → 交给重排+压缩管道 → 输出更“干净”的上下文。
  1. 打印对比
  • 先打印粗召回(Top-20)的前 100 字预览;
  • 再打印重排+压缩后的最终文本(内容已被 LLM 抽取过,更短更相关)。

结果:

  [1] 行业巨头谷歌公司也没闲着。该公司在5月推出整体性能和智能推理能力均较以往版本大幅提升的多个“双子座2.5”系列模型,并发布了多个多模态模型,如图像生成模型Imagen 4和视频生成模型Veo 3,具备...[2] 一个比较明显的问题是,AI生成内容虽然已非常流畅,但提供的信息很多时候 还是不准确。5月,日本研究人员在德国《先进科学》杂志发表的一项研究成果中指出,这一问题与人类的语言障碍——失语症类似。...[3] 业界也确实在努力从不同角度去寻求优化大模型的解决方案。中国科学院自动 化研究所联合鹏城实验室提出了一种高效推理策略AutoThink,可让大模型实现自主切换思考模式,避免“过度思考”。据研究...[4] 一些国家已在积极尝试通过优化政策、法规来营造更好的AI创新环境。日本参 议院全体会议5月28日以多数赞成票通过该国首部专门针对AI的法律,旨在促进AI相关技术研发和应用并防止其滥用。依据这部《人工智能相...[5] 5月,全球多家科技公司发布新的大模型,它们在语义理解、多模态等方面进一步提升,人工智能(AI)的能力边界在不断扩大。随着无人驾驶、机器人等技术借助AI快速进化并逐步投入市场,不少国家通过推进法规建设、...--- (2) 管道压缩后结果 (ColBERT重排 + LLM压缩) ---[1] 一个比较明显的问题是,AI生成内容虽然已非常流畅,但提供的信息很多时候
还是不准确。
大模型在出现严重错误时仍表达流畅,这与感觉性失语症的症状有相似之处,即说话 流利却总说不出什么意思。
它们可能被锁定在一种僵化的内部模式中,限制其灵活运用所储存知识。[2] AutoThink提供了一种简单而有效的推理新范式——通过省略号提示配合三阶段强化学习,引导大模型不再“逢题必深思熟虑”,而是根据问题难度自主决定“是否思[3] AI仍有不少缺陷需克服尽管当前AI应用已相当广泛,但不少缺陷还是会影 响其实用性。研究人员正努力分析导致这些缺陷的原因 并寻求新的解决方法,从而改善AI的性能。一个比较明显的问题是,AI生成内容虽然已非常流 畅,但提供的信息很多时候还是不准确。

参考文献

1.检索进阶技术

http://www.dtcms.com/a/346914.html

相关文章:

  • Sqlserver存储过程
  • 拼豆设计生成器(支持大写字母、数字,颜色自定义)
  • 力扣 30 天 JavaScript 挑战 第38天 (第九题)学习了 语句表达式的区别 高级函数 promise async await 节流
  • 三、Bpmnjs 核心组件与架构介绍
  • 深入剖析结构体内存对齐
  • 达梦数据库巡检常用SQL(一)
  • Base64 编码优化 Web 图片加载:异步响应式架构(Java 后端 + 前端全流程实现)
  • Linux问答题:分析和存储日志
  • [特殊字符] 在 Windows 新电脑上配置 GitHub SSH 的完整记录(含坑点与解决方案)
  • JUC之AQS
  • csrf漏洞学习笔记
  • C++ 20: Concepts 与Requires
  • 告别SaaS数据绑架,拥抱数据主权:XK+独立部署版跨境商城定制,为海外物流企业深度赋能
  • CentOS创建管理员用户feixue并设置密码全教程
  • 【c++进阶系列】:万字详解多态
  • 快速掌握Java非线性数据结构:树(二叉树、平衡二叉树、多路平衡树)、堆、图【算法必备】
  • STM32学习笔记19-WDG
  • linux shell测试函数
  • 百度深度学习面试:batch_size的选择问题
  • Linux总线设备驱动模型深度理解
  • 玩转Vue3高级特性:Teleport、Suspense与自定义渲染
  • 内联函数是什么以及的优点和缺点
  • ICP语序文字点选验证逆向分析(含Py纯算源码)
  • 基于SpringBoot+vue校园点餐系统
  • 【升级版】从零到一训练一个 0.6B 的 MoE 大语言模型
  • RabbitMQ面试精讲 Day 28:Docker与Kubernetes部署实践
  • JAVA核心基础篇-枚举
  • 【Linux网络编程】分布式Json-RPC框架 - 项目设计
  • Java试题-选择题(16)
  • 2025年渗透测试面试题总结-29(题目+回答)