检索优化(二):从结构化数据到智能检索的进阶之路

在上一篇《检索优化(一)》中,我们学习了混合检索(如何同时利用稀疏和密集向量的优势)和查询构建(如何让LLM将自然语言转换为元数据过滤和图查询)。

但现实世界的RAG系统,面对的挑战远不止这些:

  • 数据不仅仅在向量库里,还有大量的结构化数据躺在关系型数据库中

  • 用户的原始问题往往不够“完美”,需要预处理和重构

  • 初步检索到的结果可能包含噪音,需要二次精排和压缩

本篇将带你逐一攻克这三个难关:文本到SQL(打通结构化数据)、查询重构与分发(优化检索输入)、检索进阶(精排、压缩与校正),让你的RAG系统从“能用”进化为“好用”。

第一部分:文本到SQL —— 用自然语言查询结构化数据

在数据世界中,除了向量数据库能够处理的非结构化数据,关系型数据库(如MySQL、PostgreSQL、SQLite)同样是存储和管理结构化数据的重镇。

文本到SQL(Text-to-SQL)正是为了打破人与结构化数据之间的语言障碍而生。它利用大语言模型(LLM)将用户的自然语言问题,直接翻译成可以在数据库上执行的SQL查询语句。

一、业务挑战

将自然语言转换为SQL并非易事,主要面临三大挑战:

挑战说明后果
“幻觉”问题LLM可能会“想象”出数据库中不存在的表或字段生成的SQL语句无效,执行报错
对数据库结构理解不足LLM需要准确理解表的结构、字段的含义以及表与表之间的关联关系生成的JOIN和WHERE子句错误
用户输入的模糊性用户的提问可能存在拼写错误或不规范的表达(如“上个月的销售冠军是谁?”)模型需要具备容错和推理能力

二、优化策略

针对上述挑战,业界总结出了五条核心优化策略:

策略核心思想效果
提供精确的数据库模式向LLM提供相关表的CREATE TABLE语句,就像给了LLM一张数据库“地图”让LLM了解表名、列名、数据类型和外键关系
提供少量高质量的示例在提示中加入“问题-SQL”的示例对(Few-shot)让LLM学习如何根据相似问题构建查询
利用RAG增强上下文为数据库构建专门的“知识库”,包含DDL、字段描述、同义词映射、复杂查询示例等用户提问时,先从知识库检索相关信息,再让LLM生成SQL,极大降低“幻觉”风险
错误修正与反思生成SQL后尝试执行,如果报错,将错误信息反馈给LLM,让它“反思”并修正迭代过程显著提高查询成功率
安全执行策略自动为SELECT语句添加LIMIT限制,防止大量数据返回保护数据库性能和系统稳定性

三、Text2SQL框架实现

本节基于RAGFlow的方案,实现了一个简单但完整的Text2SQL框架。该框架使用Milvus向量数据库作为知识库,BGE-M3模型进行语义检索,DeepSeek作为大语言模型,专门针对SQLite数据库进行了优化。

3.1 框架架构概览

整个框架由三个核心模块组成,协同完成从自然语言到SQL的转换:

text 用户查询: "年龄大于30的用户有哪些?" ↓ ┌─────────────────────────────────────────────────────────────┐ │ SimpleText2SQLAgent(代理协调模块) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ ① 知识检索 → ② SQL生成 → ③ 执行与重试 → ④ 返回结果 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ ↓ ↓ ↓ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ KnowledgeBase│ │ SQLGenerator │ │ SQL执行器 │ │ │ │ (知识库模块) │ │ (SQL生成模块) │ │ (带重试) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ 最终输出: {"success": true, "sql": "SELECT * FROM users WHERE age > 30", "results": [...]}
3.2 模块一:知识库(Knowledge Base)

知识库是Text2SQL框架的“大脑”,负责存储和检索所有与SQL相关的知识。

设计思想:将三种类型的知识统一存储在一个Milvus集合中,通过type字段区分,使用BGE-M3进行语义检索。

class SimpleKnowledgeBase: """Text2SQL知识库""" def __init__(self, milvus_uri: str = "http://localhost:19530"): self.milvus_uri = milvus_uri self.client = MilvusClient(uri=milvus_uri) self.embedding_function = BGEM3EmbeddingFunction(use_fp16=False, device="cpu") self.collection_name = "text2sql_kb" self._setup_collection() def _setup_collection(self): """设置集合:定义字段结构""" fields = [ FieldSchema(name="pk", dtype=DataType.VARCHAR, is_primary=True, auto_id=True, max_length=100), FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=4096), # 存储知识内容 FieldSchema(name="type", dtype=DataType.VARCHAR, max_length=32), # 知识类型:ddl, qsql, description FieldSchema(name="dense_vector", dtype=DataType.FLOAT_VECTOR, dim=self.embedding_function.dim["dense"]) ] # ... 创建集合逻辑

三种知识类型

类型标识内容示例作用
DDL知识ddlCREATE TABLE users (id INT, name TEXT, age INT)告诉LLM表的结构和字段定义
Q-SQL知识qsql{"question": "年龄超过25岁的用户", "sql": "SELECT * FROM users WHERE age > 25"}提供“问题-SQL”示例,帮助LLM学习模式
描述知识description"age字段:用户年龄,INTEGER类型,用于存储用户的年龄信息"用自然语言解释表和字段的业务含义

检索机制:当用户提问时,系统将问题向量化,在知识库中检索最相关的知识(默认Top-5),并按类型分组返回。

def search(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]: """搜索知识库中与查询最相关的内容""" query_embeddings = self.embedding_function([query]) search_results = self.client.search( collection_name=self.collection_name, data=query_embeddings["dense"], anns_field="dense_vector", search_params={"metric_type": "IP"}, # 内积相似度 limit=top_k, output_fields=["content", "type"] ) return search_results
3.3 模块二:SQL生成器(SQL Generator)

SQL生成器负责将自然语言问题转换为SQL查询语句,并具备错误修复能力。

class SimpleSQLGenerator: """SQL生成器:生成SQL并支持错误修复""" def __init__(self, api_key: str = None): self.llm = ChatDeepSeek( model="deepseek-chat", temperature=0, # 确保结果的确定性 api_key=api_key or os.getenv("DEEPSEEK_API_KEY") )

核心方法1:生成SQL

python def generate_sql(self, user_query: str, knowledge_results: List[Dict[str, Any]]) -> str: """根据用户查询和检索到的知识生成SQL""" context = self._build_context(knowledge_results) prompt = f"""你是一个SQL专家。请根据以下信息将用户问题转换为SQL查询语句。 数据库信息: {context} 用户问题:{user_query} 要求: 1. 只返回SQL语句,不要包含任何解释 2. 确保SQL语法正确 3. 使用上下文中提供的表名和字段名 4. 如果需要JOIN,请根据表结构进行合理关联 SQL语句:""" response = self.llm.invoke(prompt) return response.content.strip()

核心方法2:修复SQL(错误反思)

python def fix_sql(self, original_sql: str, error_message: str, knowledge_results: List[Dict[str, Any]]) -> str: """根据错误信息修复SQL语句""" context = self._build_context(knowledge_results) prompt = f"""请修复以下SQL语句的错误。 数据库信息: {context} 原始SQL: {original_sql} 错误信息: {error_message} 请返回修复后的SQL语句(只返回SQL,不要解释):""" response = self.llm.invoke(prompt) return response.content.strip()

上下文构建策略:将检索到的知识按类型分组,分层次组织——先展示表结构(DDL),再展示字段描述,最后展示查询示例。

python def _build_context(self, knowledge_results: List[Dict[str, Any]]) -> str: """构建结构化的上下文信息""" ddl_info = [] # 表结构信息 qsql_examples = [] # 查询示例 descriptions = [] # 表描述信息 for result in knowledge_results: content = result['entity']['content'] doc_type = result['entity']['type'] if doc_type == 'ddl': ddl_info.append(content) elif doc_type == 'qsql': qsql_examples.append(content) elif doc_type == 'description': descriptions.append(content) context = "" if ddl_info: context += "=== 表结构信息 ===\n" + "\n".join(ddl_info) + "\n\n" if descriptions: context += "=== 表和字段描述 ===\n" + "\n".join(descriptions) + "\n\n" if qsql_examples: context += "=== 查询示例 ===\n" + "\n".join(qsql_examples) + "\n" return context
3.4 模块三:代理协调器(Agent)

代理模块是整个框架的“指挥官”,协调知识库检索、SQL生成和执行的完整流程。

python class SimpleText2SQLAgent: """Text2SQL代理:协调知识检索、SQL生成和执行""" def __init__(self, milvus_uri: str = "http://localhost:19530", api_key: str = None): self.knowledge_base = SimpleKnowledgeBase(milvus_uri) self.sql_generator = SimpleSQLGenerator(api_key) # 配置参数 self.max_retry_count = 3 # 最大重试次数 self.top_k_retrieval = 5 # 检索数量 self.max_result_rows = 100 # 结果行数限制

主查询流程

python def query(self, user_question: str) -> Dict[str, Any]: """执行完整的Text2SQL查询流程""" # 1. 从知识库检索相关信息 knowledge_results = self.knowledge_base.search(user_question, self.top_k_retrieval) # 2. 生成SQL语句 sql = self.sql_generator.generate_sql(user_question, knowledge_results) # 3. 执行SQL(带重试机制) retry_count = 0 while retry_count < self.max_retry_count: success, result = self._execute_sql(sql) if success: return { "success": True, "sql": sql, "results": result, "retry_count": retry_count } else: # 尝试修复SQL sql = self.sql_generator.fix_sql(sql, result, knowledge_results) retry_count += 1 return {"success": False, "error": "SQL执行失败,已超过最大重试次数", "sql": sql}

安全执行策略

python def _execute_sql(self, sql: str) -> Tuple[bool, Any]: """安全执行SQL语句""" # 自动添加LIMIT限制,防止大量数据返回 if sql.strip().upper().startswith('SELECT') and 'LIMIT' not in sql.upper(): sql = f"{sql.rstrip(';')} LIMIT {self.max_result_rows}" # ... 执行SQL并结构化返回结果
3.5 完整流程示例:从自然语言到SQL

让我们以查询“年龄大于30的用户有哪些”为例,完整演示框架的运作过程。

Step 1:知识库检索

BGE-M3将查询转换为向量,在知识库中检索到最相关的5条知识:

类型相似度内容
DDL0.85CREATE TABLE users (id INT PRIMARY KEY, name TEXT, email TEXT UNIQUE, age INT, city TEXT)
Q-SQL0.82{"question": "查询年龄超过25岁的用户", "sql": "SELECT * FROM users WHERE age > 25"}
描述0.78"age字段:用户年龄,INTEGER类型;name字段:用户姓名,TEXT类型"

Step 2:SQL生成

DeepSeek分析用户意图:

  • 识别关键信息:字段age、比较操作>、阈值30

  • 参考示例模式:从WHERE age > 25学习到WHERE age > 数值的模式

  • 生成目标SQL:SELECT * FROM users WHERE age > 30

Step 3:SQL执行与结果处理

假设users表包含以下数据:

ID姓名邮箱年龄城市
1张三zhangsan@email.com25北京
2李四lisi@email.com32上海
3王五wangwu@email.com28广州
4赵六zhaoliu@email.com35深圳
5陈七chenqi@email.com29杭州

执行SQL后,自动添加LIMIT 100安全限制,筛选出符合条件的记录:

json { "success": true, "sql": "SELECT * FROM users WHERE age > 30 LIMIT 100", "results": { "columns": ["id", "name", "email", "age", "city"], "rows": [ {"id": 2, "name": "李四", "email": "lisi@email.com", "age": 32, "city": "上海"}, {"id": 4, "name": "赵六", "email": "zhaoliu@email.com", "age": 35, "city": "深圳"} ], "count": 2 }, "retry_count": 0 }
3.6 为什么不直接使用封装好的框架?

💬 因为淋过雨,所以想为你撑把伞。

市面上确实有很多成熟的Text2SQL框架,但这些高度封装的工具往往存在黑盒问题——当查询结果不符合预期时,很难定位是检索环节、SQL生成环节还是执行环节出了问题。正如我们在上一章LangChain的SelfQueryRetriever示例中遇到的“时间最短的视频”查询异常,很难深入到框架内部进行精确调试和优化。

自主实现框架的核心价值

优势说明
可调试性每个环节(检索→生成→执行→重试)都可单独调试和日志记录
可定制性可根据业务需求灵活调整知识库结构、提示词模板、重试策略
可优化性方便针对特定数据库类型(如SQLite、MySQL、PostgreSQL)进行针对性优化
透明可控所有逻辑清晰可见,不依赖第三方黑盒,便于维护和迭代

第二部分:查询重构与分发 —— 让问题更“好搜”

在RAG系统中,用户的原始问题往往不是最优的检索输入。它可能过于复杂、包含歧义,或者与文档的实际措辞存在偏差。查询重构与分发就是在检索之前对用户查询进行“预处理”,让检索更精准。

四、查询翻译

查询翻译的目标是弥合用户自然语言提问与文档库中存储信息之间的“语义鸿沟”。

4.1 提示工程(Prompt Engineering)

这是最直接的查询重构方法。通过精心设计的提示词,引导LLM将用户的原始查询改写得更清晰、更具体。

实战案例:解决“最值查询”的排序问题

在上一章中,我们遇到一个问题:SelfQueryRetriever无法正确处理“时间最短的视频”这类需要排序的查询。原因在于它不支持ORDER BY操作。

解决方案:让LLM直接生成结构化的排序指令,而不是依赖于SelfQueryRetriever的有限能力。

python # 设计提示词,要求LLM输出JSON格式的排序指令 prompt = f"""你是一个智能助手,请将用户的问题转换成一个用于排序视频的JSON指令。 你需要识别用户想要排序的字段和排序方向。 - 排序字段必须是 'view_count' (观看次数) 或 'length' (时长) 之一。 - 排序方向必须是 'asc' (升序) 或 'desc' (降序) 之一。 例如: - '时间最短的视频' → {{"sort_by": "length", "order": "asc"}} - '播放量最高的视频' → {{"sort_by": "view_count", "order": "desc"}} 请根据以下问题生成JSON指令: 原始问题: "{query}" JSON指令:""" # 调用LLM并解析结果 response = llm.invoke(prompt) instruction = json.loads(response.content) # 根据指令在代码中执行排序 sort_by = instruction.get('sort_by') # 'length' 或 'view_count' order = instruction.get('order') # 'asc' 或 'desc' reverse_order = (order == 'desc') sorted_docs = sorted(all_documents, key=lambda doc: doc.metadata.get(sort_by, 0), reverse=reverse_order) result = sorted_docs[0] # 取第一个

通过这种方式,我们将LLM从“文本改写员”提升为能够理解复杂意图并生成可执行计划的“智能代理”,优雅地解决了“最值”查询的难题。

4.2 多查询分解(Multi-Query)

当用户提出一个复杂的问题时,直接用整个问题去检索可能效果不佳,因为它可能包含多个子主题。

核心思想:将复杂问题拆分成多个更简单、更具体的子问题,分别检索,然后合并结果。

text 原始问题: "在《流浪地球》中,刘慈欣对人工智能和未来社会结构有何看法?" ↓ 分解 子问题1: "《流浪地球》中描述的人工智能技术有哪些?" 子问题2: "《流浪地球》中描绘的未来社会是怎样的?" 子问题3: "刘慈欣关于人工智能的观点是什么?" ↓ 分别检索后合并 最终答案(综合三个子问题的检索结果)

LangChain提供了MultiQueryRetriever来完成这一过程。它在内部利用LLM将原始问题从不同角度分解成多个子问题,并行为每个子问题检索相关文档,最后合并去重。

4.3 退步提示(Step-Back Prompting)

由Google DeepMind团队提出的提示工程技术。当面对一个细节繁多或过于具体的问题时,模型直接作答容易出错。

核心思路:先引导模型“退后一步”,生成一个更概括的“退步问题”,获取通用原理,再基于这个原理推理具体问题。

流程对比

步骤传统方法(直接推理)退步提示方法
Step 1直接回答问题先生成退步问题:“这个问题背后的物理原理是什么?”
Step 2可能出错回答退步问题:“理想气体定律 PV=nRT”
Step 3-基于定律代入具体数值,推出正确答案

示例

原始问题退步问题推理过程结果
“如果理想气体的温度增加2倍,体积增加8倍,其压力会如何变化?”“这个问题背后的物理原理是什么?”先得到“理想气体定律PV=nRT”,代入数值计算压力变为原来的1/4 ✅
4.4 假设性文档嵌入(HyDE)

HyDE(Hypothetical Document Embeddings)是一种无需微调即可显著提升向量检索质量的查询改写技术。

核心问题:用户的查询通常简短、关键词有限,而数据库中的文档则内容详实,两者在向量空间中可能存在“鸿沟”。

解决思路:不直接使用原始查询,而是先用LLM生成一个“假设性”的、能够完美回答该查询的文档,然后将这个假设文档向量化,用它去检索真实文档。

text 用户查询: "如何快速入门Python?" ↓ LLM生成假设文档: "Python入门需要掌握基础语法、数据类型、控制流、函数定义...(一段详尽的回答)" ↓ 将假设文档向量化 ↓ 用假设文档向量检索真实文档("文档到文档"的匹配,更准确) ↓ 返回最相关的真实文档

为什么有效:HyDE将困难的“查询到文档”匹配(查询短、文档长,语义不对齐),转化为相对容易的“文档到文档”匹配(假设文档与真实文档长度相近、语义对齐)。

五、查询路由

当RAG系统接入了多个不同的数据源或具备多种处理能力时,就需要一个“智能调度中心”来分析用户的查询,并动态选择最合适的处理路径。这就是查询路由(Query Routing)

5.1 应用场景
路由类型说明示例
数据源路由根据查询意图路由到不同的知识库产品问题→产品文档库;订单问题→SQL数据库
组件路由根据问题复杂性分配给不同处理组件简单FAQ→直接向量检索;复杂操作→Agent执行
提示模板路由为不同类型的任务选择最优提示模板数学问题→分步推理模板;代码生成→代码优化模板
5.2 实现方式一:基于LLM的意图识别

通过设计包含路由选项的提示词,让LLM直接对用户查询进行分类。

python from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnableBranch, RunnableLambda # 第一步:定义分类器 classifier_prompt = ChatPromptTemplate.from_template( """根据用户问题中提到的菜品,将其分类为:['川菜', '粤菜', '其他']。 不要解释你的理由,只返回一个单词的分类结果。 问题: {question}""" ) classifier_chain = classifier_prompt | llm | StrOutputParser() # 第二步:定义各分支的处理链 sichuan_chain = PromptTemplate.from_template( "你是一位川菜大厨。请用正宗的川菜做法,回答关于「{query}」的问题。" ) | llm | StrOutputParser() cantonese_chain = PromptTemplate.from_template( "你是一位粤菜大厨。请用经典的粤菜做法,回答关于「{query}」的问题。" ) | llm | StrOutputParser() general_chain = PromptTemplate.from_template( "请回答关于「{query}」的问题。" ) | llm | StrOutputParser() # 第三步:构建路由分支 router_branch = RunnableBranch( (lambda x: "川菜" in x["topic"], sichuan_chain), (lambda x: "粤菜" in x["topic"], cantonese_chain), general_chain # 默认选项 ) # 第四步:组合完整路由链 # 并行执行:分类器生成topic,同时保留原始question # 然后将结果传递给路由分支 full_router_chain = { "topic": classifier_chain, "question": lambda x: x["question"] } | router_branch # 调用示例 result = full_router_chain.invoke({"question": "麻婆豆腐怎么做?"}) # 自动路由到川菜大厨处理 ✅
5.3 实现方式二:嵌入相似性路由

这种方法不依赖LLM进行分类,延迟更低。通过计算用户查询与预设“路由描述”之间的向量相似度来做决策。

python import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 第一步:定义路由描述并向量化 sichuan_desc = "你是一位处理川菜的专家。用户的问题是关于麻辣、辛香、重口味的菜肴,例如水煮鱼、麻婆豆腐、鱼香肉丝等。" cantonese_desc = "你是一位处理粤菜的专家。用户的问题是关于清淡、鲜美、原汁原味的菜肴,例如白切鸡、老火靓汤、虾饺等。" route_descs = [sichuan_desc, cantonese_desc] route_names = ["川菜", "粤菜"] route_embeddings = embeddings.embed_documents(route_descs) # 第二步:定义路由函数 def semantic_route(info): # 计算用户查询与各路由描述的相似度 query_emb = embeddings.embed_query(info["query"]) similarities = cosine_similarity([query_emb], route_embeddings)[0] # 选择最相似的路由 best_idx = np.argmax(similarities) chosen_route = route_names[best_idx] # 调用对应的处理链 return route_map[chosen_route].invoke(info) # 第三步:组合成链 full_chain = RunnableLambda(semantic_route)
5.4 LlamaIndex中的路由实现

LlamaIndex通过RouterQueryEngine实现查询路由。其思路是将不同的数据源或查询策略包装为“工具(Tool)”,然后通过路由器为用户查询动态选择最合适的工具。

python from llama_index.core.query_engine import RouterQueryEngine from llama_index.core.tools import QueryEngineTool # 定义多个工具(每个工具包含一个查询引擎和一段描述) tools = [ QueryEngineTool.from_defaults( query_engine=product_engine, description="用于回答产品相关问题" ), QueryEngineTool.from_defaults( query_engine=order_engine, description="用于回答订单和物流相关问题" ), ] # 创建路由查询引擎 router_engine = RouterQueryEngine.from_defaults( tools=tools, selector=LLMSingleSelector.from_defaults() # LLM根据语义选择工具 ) # 用户查询自动路由到最合适的工具 response = router_engine.query("我上次买的手机什么时候发货?")

第三部分:检索进阶 —— 重排、压缩与校正

在基础的RAG流程中,我们依赖向量相似度从知识库中检索信息。但这种方法存在一些固有局限:最相关的文档不总是在检索结果的顶端,语义理解可能有偏差,检索结果可能包含噪音。

为了构建更强大的生产级RAG应用,需要引入更高级的检索技术。

六、重排序(Re-ranking)

重排序的目标是:对初步检索结果进行二次排序,把最相关的内容排到最前面。

6.1 RRF(倒数排名融合)

我们在混合检索章节中已详细介绍了RRF。它是一种零样本重排方法,不依赖模型训练,纯粹基于文档在多个检索器结果列表中的排名计算分数。

优点缺点
无需训练,零样本使用只考虑排名信息,忽略原始相似度分数
计算成本极低可能丢失部分有用信息
融合多路检索优势-
6.2 RankLLM(基于LLM的重排)

直接利用大型语言模型本身来进行重排。基本逻辑非常直观:既然LLM最终要负责根据上下文生成答案,为什么不直接让它判断哪些上下文最相关?

提示词示例

text 以下是一个文档列表,每个文档都有一个编号和摘要。同时提供一个问题。请根据问题,按相关性顺序列出您认为最需要查阅的文档编号,并给出相关性分数(1-10分)。 文档 1: <文档1的摘要> 文档 2: <文档2的摘要> ... 文档 10: <文档10的摘要> 问题: <用户的问题> 回答: Doc: 9, Relevance: 7 Doc: 3, Relevance: 4 Doc: 7, Relevance: 3
优点缺点
理解能力最强,能处理复杂的相关性判断API调用成本高、延迟大
可提供相关性解释受限于LLM的上下文窗口
6.3 Cross-Encoder重排

Cross-Encoder将查询和每个候选文档拼接成一个输入([CLS] query [SEP] document [SEP]),输入到Transformer模型中,输出一个0-1之间的相关性分数。

📝[SEP]是BERT等Transformer模型中用于分隔不同文本片段的特殊标记。

text 工作流程: ① 初步检索: 从知识库召回Top-50文档 ② 逐一评分: 对每个"查询-文档"对进行独立的Cross-Encoder推理 ③ 返回重排结果: 根据新分数重新排序
优点缺点
精度最高(同时分析查询和文档的交互)需要N次独立的模型推理(延迟高)
能捕捉细粒度的相关性信号计算成本大

常用模型ms-marco-MiniLM-L-12-v2ms-marco-TinyBERT-L-2-v2

6.4 ColBERT重排

ColBERT(Contextualized Late Interaction over BERT)在Cross-Encoder的高精度和双编码器的高效率之间取得了平衡,采用“后期交互”机制。

text 工作流程: ① 独立编码: 分别为查询和文档中的每个Token生成上下文相关的嵌入向量 ② 后期交互: 查询时,计算每个查询Token与文档Token之间的最大相似度(MaxSim) ③ 分数聚合: 将所有查询Token的最大相似度相加,得到最终总分

ColBERT vs Cross-Encoder vs 双编码器

维度双编码器(Bi-Encoder)Cross-EncoderColBERT
编码方式查询和文档独立编码查询和文档联合编码独立编码,后期交互
精度较低最高
速度最快(可预计算文档向量)最慢(N次联合推理)中(向量点积计算)
交互粒度无(仅比较[CLS]向量)全量交互Token级MaxSim
6.5 重排方法对比总结
特性RRFRankLLMCross-EncoderColBERT
核心机制融合多个排名LLM推理,生成排序列表联合编码,计算相关分独立编码,后期交互
计算成本极低中(API费用与延迟)高(N次模型推理)中(向量点积计算)
交互粒度无(仅排名)概念/语义级句子级Token级
是否需要训练是(需微调)是(需微调)
适用场景多路召回融合高价值语义理解场景Top-K精排Top-K重排

七、压缩(Compression)

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

7.1 LangChain的ContextualCompressionRetriever

LangChain提供了ContextualCompressionRetriever组件来实现上下文压缩。它像一个包装器,包裹在基础检索器之上,对检索结果进行处理后再返回。

内置压缩器

压缩器工作方式类型
LLMChainExtractor利用LLM提取文档中与查询相关的句子内容提取
LLMChainFilter利用LLM判断整个文档是否相关,不相关的直接丢弃文档过滤
EmbeddingsFilter计算查询和文档的嵌入相似度,只保留超过阈值的文档文档过滤
7.2 自定义重排器与压缩管道

以ColBERT为例,展示如何集成未被官方支持的功能。实现步骤如下:

python from langchain.retrievers.document_compressors.base import BaseDocumentCompressor from langchain.retrievers.document_compressors import DocumentCompressorPipeline from langchain.retrievers import ContextualCompressionRetriever class ColBERTReranker(BaseDocumentCompressor): """自定义ColBERT重排器""" def __init__(self, model_name: str = "bert-base-uncased", **kwargs): super().__init__(**kwargs) self.tokenizer = AutoTokenizer.from_pretrained(model_name) self.model = AutoModel.from_pretrained(model_name) self.model.eval() def compress_documents( self, documents: Sequence[Document], query: str, callbacks=None, ) -> Sequence[Document]: """对文档进行ColBERT重排序""" # 1. 编码查询和文档(独立编码) query_emb = self._encode_text([query]) doc_embs = self._encode_text([doc.page_content for doc in documents]) # 2. 计算MaxSim相似度(后期交互) scores = self._calculate_colbert_similarity(query_emb, doc_embs) # 3. 排序并返回Top-5 scored_docs = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True) return [doc for doc, _ in scored_docs[:5]] 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_state return F.normalize(embeddings, p=2, dim=-1) def _calculate_colbert_similarity(self, query_emb, doc_embs): """MaxSim操作:每个查询Token找最相似的文档Token""" scores = [] for doc_emb in doc_embs: # 计算相似度矩阵 sim_matrix = torch.matmul(query_emb, doc_emb.transpose(-2, -1)) # MaxSim: 每个查询Token取最大相似度,然后求和 max_sim = sim_matrix.max(dim=-1)[0].sum().item() scores.append(max_sim) return scores

组装完整管道

python # 1. 创建基础检索器 base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20}) # 2. 创建处理管道:ColBERT重排 + LLM压缩 pipeline_compressor = DocumentCompressorPipeline( transformers=[ ColBERTReranker(), # 先重排 LLMChainExtractor.from_llm(llm) # 再压缩 ] ) # 3. 创建最终检索器 final_retriever = ContextualCompressionRetriever( base_compressor=pipeline_compressor, base_retriever=base_retriever ) # 4. 执行查询 results = final_retriever.get_relevant_documents("AI还有哪些缺陷需要克服?")
7.3 LlamaIndex中的检索压缩

LlamaIndex提供了SentenceEmbeddingOptimizer作为后处理器。它检索后把文档分解成句子,计算每个句子与查询的嵌入相似度,只保留相似度最高的句子,去除无关部分。

八、校正(Corrective-RAG)

传统RAG流程有一个隐含假设:检索到的文档总是与问题相关且包含正确答案。但现实中检索系统可能失败,返回不相关或错误的文档。校正检索(Corrective-RAG,C-RAG)正是为解决这一问题而提出的策略。

8.1 C-RAG的工作流程

C-RAG引入了一个“自我反思”循环,分为三个阶段:

text ┌─────────────────────────────────────────────────────────────┐ │ 阶段一:检索(Retrieve) │ │ 根据用户查询从知识库中检索文档 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 阶段二:评估(Assess) │ │ 检索评估器判断每个文档的相关性,给出标签: │ │ ✅ 正确(Correct) ❌ 不正确(Incorrect) ❓ 模糊(Ambiguous)│ └─────────────────────────────────────────────────────────────┘ ↓ ┌───────────────────┼───────────────────┐ ↓ ↓ ↓ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ ✅ 正确 │ │ ❌ 不正确 │ │ ❓ 模糊 │ │ → 知识精炼 │ │ → 知识搜索 │ │ → 知识搜索 │ │ 分解、过滤、 │ │ 查询重写后 │ │ 直接使用原查询 │ │ 重组上下文 │ │ 进行Web搜索 │ │ 进行Web搜索 │ └───────────────┘ └───────────────┘ └───────────────┘

三种行动路径详解

评估结果行动名称具体操作
✅ 正确知识精炼将文档分解为更细粒度的知识片段,过滤无关部分,重新组合成精准上下文
❌ 不正确知识搜索对原始查询进行重写(优化关键词),然后进行Web搜索获取外部信息
❓ 模糊知识搜索直接使用原始查询进行Web搜索,获取额外信息辅助生成

通过这种方式,C-RAG极大地增强了RAG系统的鲁棒性。它不再盲目信任检索结果,而是在检索失败时主动寻求外部帮助,有效减少幻觉,提升答案的准确性。

第四部分:总结

核心技术一句话概括主要应用
Text-to-SQL用自然语言生成SQL查询打通结构化数据(MySQL、SQLite等)
Text2SQL知识库DDL+Q-SQL+描述三类知识统一存储降低LLM生成SQL的“幻觉”
提示工程用Prompt改写和优化查询解决排序、最值等复杂查询
多查询分解复杂问题拆分为多个子问题提升多主题问题的召回率
退步提示先获取通用原理,再推理具体问题复杂推理问答
HyDE生成假设文档,用文档向量检索解决查询与文档的语义鸿沟
查询路由(LLM)LLM分类意图,选择处理路径多数据源、多组件的智能分发
查询路由(语义)嵌入相似度匹配路由低延迟、不依赖LLM的路由
RRF重排基于排名融合多路检索结果混合检索的结果融合
RankLLMLLM直接判断文档相关性并排序高精度语义重排
Cross-Encoder联合编码查询和文档Top-K高精度精排
ColBERT独立编码+后期交互精度与效率的平衡
上下文压缩提取相关句子或过滤不相关文档降低噪音、节省成本
Corrective-RAG评估检索质量,失败时主动补救增强系统鲁棒性

如果本文对你有帮助,欢迎点赞、收藏、转发!有任何疑问,欢迎在评论区交流讨论~