返回文章列表

RAG 检索前处理:不是把问题润色一下,而是先决定该怎么查

围绕查询构建、查询翻译和查询路由,说明 RAG 在正式检索前为什么要先决定“该怎么查”。

RAG 检索前处理:不是把问题润色一下,而是先决定该怎么查

前面几篇里,我们已经把 RAG 的离线建库走了一遍:资料导入、文本分块、向量嵌入、写入向量数据库或索引。

到这里,一个基础知识库已经搭起来了。

很多 demo 接下来会直接进入在线检索:

用户问题
-> 问题向量化
-> 向量数据库相似度检索
-> 拼接上下文
-> 大模型回答

这个流程没错,但它只覆盖了最简单的一类问题:用户问的是自然语言,知识也以自然语言文本存放,系统只需要做语义相似度匹配。

真实项目里,问题经常不是这样。

用户可能问:

上个月山西 5A 景区游客量最高的是哪个?

这个问题更像数据库统计查询,答案可能在 MySQL 表里。

用户可能问:

糖尿病和哪些并发症有两层以内关系?

这个问题更像图数据库查询,答案可能在 Neo4j 这样的知识图谱里。

用户也可能问:

只看 2024 年以后、浏览量超过 100 万的视频,哪些讲到了山西古建筑?

这里既有语义问题“山西古建筑”,也有结构化条件“2024 年以后、浏览量超过 100 万”。

如果这些问题全部都直接 embedding,然后丢给向量库,系统很容易走错方向。

所以检索之前还需要一个环节,叫做检索前处理。

它要解决的核心问题不是:

怎么把用户问题写得更漂亮?

而是:

这个问题到底应该被翻译成什么查询?
应该查向量库、关系数据库、图数据库,还是几个地方一起查?

为了讲清楚这一节,我们先固定一个更完整的例子:

你要做一个企业内部问答助手。

它背后有三类数据:
1. 文档知识库:制度、FAQ、故障复盘、产品说明
2. 关系数据库:订单、客户、景区游客量、销售额、报销记录
3. 图数据库:疾病关系、系统依赖、人员协作、知识概念网络

用户只会用自然语言提问。
系统必须先判断:这个问题该怎么查。

这就是检索前处理出现的地方。

一、为什么“直接向量检索”不够

基础 RAG 的直觉很简单:

用户问一句话
-> 把这句话转成向量
-> 找语义相似的文本块

如果用户问:

缓存雪崩怎么处理?

文档里刚好有一段:

缓存雪崩的处理方案包括随机过期时间、缓存预热、限流降级和多级缓存。

那向量检索通常能找到。

但下面这些问题就不一样了。

第一个问题:

太原市 5A 景区当月游客量是多少?

它不是要找一段“像”的文字,而是要从结构化表里查:

SELECT scenic_name, monthly_visitors
FROM scenic_spots
JOIN city_info ON scenic_spots.city_id = city_info.id
WHERE city_info.city = '太原'
  AND scenic_spots.level = '5A';

第二个问题:

糖尿病相关概念两跳以内有哪些?

它不是普通文本相似,而是在图里找节点和关系:

MATCH (d:Concept)-[*1..2]-(related:Concept)
WHERE d.name CONTAINS 'diabetes'
RETURN d, related
LIMIT 100

第三个问题:

找 2024 年以后浏览量超过 100 万、讲山西古建筑的视频。

它既需要语义检索:

山西古建筑

也需要元数据过滤:

{
  "publish_year": { "$gte": 2024 },
  "view_count": { "$gte": 1000000 }
}

你会发现,这些问题的共同点是:

用户说的是自然语言,但系统真正需要执行的,不一定是自然语言检索。

有时要生成 SQL。

有时要生成 Cypher。

有时要抽取 Metadata Filter。

有时要把一个模糊问题改写、拆分、扩展。

有时要先判断该走哪个数据源。

这就是检索前处理的技术栈。

二、检索前处理的三件事

检索前置处理 手绘图 1:检索前先决定怎么查

按照极客时间 RAG 第八章的讲法,检索前处理可以先拆成三块:

检索前处理
├── 查询构建:把自然语言构造成可执行查询
├── 查询翻译:把原始问题改写、拆解、澄清或扩展
└── 查询路由:决定问题应该去哪查、用哪套提示或检索器

这三块的关注点不一样。

查询构建关心的是:

用户这句话能不能变成 SQL、Cypher 或过滤条件?

查询翻译关心的是:

用户原问题太模糊、太短、太散,能不能改成更好检索的表达?

查询路由关心的是:

这个问题应该走向量库、关系数据库、图数据库、多模态检索,还是不同提示模板?

用一句话记:

查询构建解决“怎么写查询”,查询翻译解决“怎么问得更清楚”,查询路由解决“去哪查”。

后面我们就沿着这三件事展开。

三、查询构建:把自然语言变成可执行查询

查询构建是检索前处理中最硬的一块。

因为它不是简单改写一句话,而是要把自然语言变成某个系统能执行的查询表达。

比如:

  • 面向关系数据库:生成 SQL
  • 面向图数据库:生成 Cypher
  • 面向向量数据库:生成 Metadata Filter

它背后的核心思想是:

如果数据源不是纯文本,就不能只用纯文本检索。

1. Text2SQL:让用户用自然语言查关系数据库

检索前置处理 手绘图 2:Text2SQL 查询链路

Text2SQL 要解决的问题很直接:

业务人员不会写 SQL,但数据在关系数据库里。
能不能让他用自然语言问,系统自动生成 SQL?

比如用户问:

太原市 5A 景区当月游客量是多少?

如果数据库里有两张表:

city_info(id, city_name, province)
scenic_spots(id, city_id, scenic_name, level, monthly_visitors)

系统就要生成一条 SQL,去 join 城市表和景区表,过滤 city_name = 太原level = 5A,再取游客量。

这和向量检索完全不同。

向量检索是在找“相似文本块”。

Text2SQL 是在生成“可执行数据库查询”。

所以它最需要的不是 embedding,而是 schema。

也就是数据库结构:

有哪些表?
每张表有哪些字段?
字段是什么意思?
表和表之间怎么关联?
有没有常见查询样例?

如果模型不知道 schema,它就只能凭空猜字段。

比如它可能生成:

SELECT visitors
FROM tourist_sites
WHERE city = '太原' AND rating = '5A';

这条 SQL 看起来像对的,但真实数据库里也许根本没有 tourist_sitesvisitorsrating 这些字段。

这就是 Text2SQL 最常见的失败:

SQL 写得像真的,但字段和表是幻觉。

一个更靠谱的 Text2SQL 流程通常是:

用户自然语言问题
-> 理解意图
-> 选择相关表和字段
-> 注入 DDL / 字段说明 / 示例 SQL
-> 生成 SQL
-> 清理输出格式
-> SQL 安全校验
-> 执行查询
-> 把查询结果转回自然语言

这里面有两个点很重要。

第一,不要把所有 schema 一股脑塞给模型。

企业数据库可能有几百张表、几千个字段。如果全部塞进上下文,模型既看不过来,也容易选错。

更好的做法是先做一次 schema 检索:

用户问题
-> 找最相关的表和字段
-> 只把这些 schema 交给模型
-> 再生成 SQL

这其实也是一种 RAG。

只不过检索的不是业务文档,而是数据库 schema 和 SQL 样例。

第二,生成 SQL 不能直接无脑执行。

生产系统至少要做这些保护:

  • 只允许只读查询
  • 禁止 DROPDELETEUPDATEINSERT
  • 限制返回行数
  • 设置查询超时
  • 校验表名和字段名是否存在
  • 检查用户权限
  • 记录审计日志
  • 执行失败时让模型基于错误信息修复,但要限制重试次数

RagFlow 这类产品化方案的思路也类似:先准备 DDL 知识库、字段说明知识库、Query-to-SQL 样例知识库,再让 Agent 检索相关信息、生成 SQL、连接数据库执行,并根据错误结果循环修复。

这个思路给我们的启发是:

Text2SQL 不是一次提示词调用,而是一条带 schema 检索、执行校验和错误修复的查询链路。

还有一种场景要单独说。

如果企业表特别多,业务逻辑特别复杂,直接让模型生成 SQL 未必是最好的选择。

这时可以把常见业务查询封装成函数:

get_monthly_sales(region, month)
predict_next_quarter_sales(product_line)
get_customer_risk_score(customer_id)

模型不直接写 SQL,而是识别用户意图,再调用这些预先设计好的业务函数。

这种方式牺牲了一些自由度,但安全性、稳定性和业务可控性更好。

所以 Text2SQL 的边界是:

简单表结构、小中型 schema、只读分析场景,可以尝试生成 SQL。
复杂核心业务系统,更适合函数封装 + 工具调用。

2. Text2Cypher:让用户用自然语言查图数据库

Text2Cypher 和 Text2SQL 很像。

区别是 SQL 面向关系数据库,Cypher 面向图数据库。

关系数据库的核心是:


字段

join

图数据库的核心是:

节点
关系
属性
路径

所以它更适合回答这类问题:

糖尿病和哪些疾病有两层以内关系?
这个故障影响了哪些服务,分别依赖哪些中间件?
某个客户和哪些产品、工单、销售人员有关联?

这些问题不是简单查一行数据,而是在找实体之间的关系。

如果用普通向量检索,系统可能找到几段提到“糖尿病”的文本,但很难稳定回答“两层以内关系”。

如果图数据库已经建好,就可以通过 Cypher 查:

MATCH (d:Concept)-[*1..2]-(related:Concept)
WHERE d.name CONTAINS 'diabetes'
RETURN d.name, related.name
LIMIT 100

但 Cypher 对很多业务用户来说更陌生。

所以 Text2Cypher 的目标是:

用户说自然语言
-> 系统生成图查询语句
-> 图数据库返回节点和关系
-> 再转成自然语言解释

它同样依赖 schema。

只不过这里的 schema 不是表结构,而是:

  • 有哪些节点类型
  • 有哪些关系类型
  • 节点有哪些属性
  • 关系方向是什么
  • 哪些路径查询是允许的

如果 schema 不准确,模型生成的 Cypher 也会像 SQL 一样“看起来对,执行为空”。

课程里的经验很重要:第一版只靠手写的图 schema 描述,可能生成语句却查不到结果;更靠谱的做法是先从真实图数据库里动态查询节点类型、关系类型和属性,再把这些实际 schema 传给模型。

也就是:

先查图数据库真实结构
-> 把实际节点 / 关系 / 属性交给模型
-> 再生成 Cypher
-> 执行并验证

这和 Text2SQL 的 schema 注入是同一个思想。

区别只是数据源换成了图。

这里也要强调安全和边界:

  • 限制可查询的节点和关系类型
  • 限制路径深度
  • 限制返回数量
  • 校验 Cypher 语法
  • 避免危险写操作
  • 对空结果做诊断,而不是直接生成貌似合理的答案

一句话记:

Text2Cypher 的难点不是“让模型会写 Cypher”,而是让模型基于真实图 schema 写出能查到东西的 Cypher。

3. Metadata Filter:向量检索前先过滤范围

前面两个例子都是自然语言转数据库查询。

但查询构建不只发生在关系数据库和图数据库里。

向量数据库里也有查询构建。

最典型的就是 Metadata Filter。

我们先看一个问题:

找 2024 年以后浏览量超过 100 万、讲山西古建筑的视频。

这里有两类信息。

第一类是语义信息:

山西古建筑

这适合向量检索。

第二类是结构化条件:

2024 年以后
浏览量超过 100 万

这不适合只靠向量相似度。

如果每个视频文档都有 metadata:

{
  "title": "跟着悟空游山西",
  "author": "行迹旅途中",
  "publish_year": 2024,
  "view_count": 1200000,
  "duration": 960
}

那系统应该先从用户问题里抽取过滤条件:

{
  "publish_year": { "$gte": 2024 },
  "view_count": { "$gte": 1000000 }
}

再在过滤后的候选里做语义检索:

山西古建筑

这就叫 Metadata Filter 生成。

它看起来像一个小功能,但思想很重要:

向量检索负责找语义相关,元数据过滤负责保证条件正确。

比如用户问:

只看 2025 年的政策,报销标准是多少?

如果不抽取 year = 2025,系统可能召回 2023 年旧制度。

比如用户问:

只看浏览量超过 100 万的视频。

如果不抽取 view_count > 1000000,系统可能召回内容相似但热度很低的视频。

所以 Metadata Filter 是一种“轻量级 Text2SQL”。

它不生成完整 SQL,但会从自然语言中生成结构化条件。

它的输入是:

用户问题 + 可用 metadata 字段说明

它的输出是:

语义查询文本 + 结构化过滤条件

比如:

{
  "query": "山西古建筑 视频内容",
  "filter": {
    "publish_year": { "$gte": 2024 },
    "view_count": { "$gte": 1000000 }
  }
}

这里也有坑。

如果 metadata 字段没有说明,模型可能不知道 view_countpublish_dateauthor 分别怎么用。

如果字段类型混乱,把年份存成字符串,比较操作就容易出错。

如果过滤过严,可能把真正相关的文档误杀。

所以元数据过滤要遵守一个原则:

能从用户问题明确抽出的条件,才进入 filter。
不确定的内容,宁可保留给语义检索和后续重排。

四、查询翻译:让问题变得更适合检索

检索前置处理 手绘图 3:查询翻译工具箱

查询构建解决的是“把自然语言变成 SQL、Cypher、Filter”。

查询翻译解决的是另一个问题:

用户原问题质量不高,直接检索会漏掉重点。

这里的“翻译”不是从中文翻英文,而是把一个不适合检索的问题,转换成更适合检索的表达。

它常见有四类:

  • 查询重写
  • 查询分解
  • 查询澄清
  • HyDE 假设文档

1. 查询重写:把口语问题改成检索问题

用户经常不会按知识库里的术语提问。

比如游戏客服场景里,用户问:

我刚开始玩,这关感觉很难,应该先学啥?

这句话对人能懂,但检索系统不好查。

查询重写会把它改成:

普陀山关卡新手优先学习哪些技能

这样检索系统更容易命中“普陀山关卡”“技能推荐”“新手攻略”这些文档。

在 RAG 流程里,这意味着:

原始问题
-> LLM 重写
-> 重写后的问题 embedding
-> 向量检索

注意,这里的 LLM 不是在最终回答问题。

它是在 retrieve 之前做重写器。

所以 RAG 里不只有生成阶段会用大模型,检索阶段也可能用大模型。

查询重写的目标是提高可检索性,不是改变用户意图。

如果用户只问“这关技能怎么点”,系统可以补成“新手技能推荐”。

但不能擅自扩成“装备选择、Boss 攻略、地图路线、隐藏任务”全部都查。

重写是翻译,不是加戏。

2. 查询分解:把一个大问题拆成多个子问题

有些问题不是问得不清楚,而是问得太大。

比如:

这款游戏新手怎么玩?

这个问题可能包含:

1. 游戏整体难度怎么样?
2. 新手应该优先学习哪些技能?
3. 普陀山关卡有哪些难点?
4. 有没有角色培养建议?
5. 通关过程中有哪些常见陷阱?

如果只检索一次,系统可能只找到一类内容。

查询分解会把一个大问题拆成多个子查询,然后多路检索,再合并结果。

这就是 Multi Query Retriever 这类方法的价值。

它把“一问一搜”变成:

一个用户问题
-> 多个子问题
-> 多路检索
-> 候选合并
-> 后续重排 / 压缩

这会提升召回覆盖率,但也会增加成本和延迟。

所以查询分解要控制数量。

不是所有问题都要拆成五个。

拆分越多,不一定越好;如果每个子问题都偏一点,最后召回的噪声也会更多。

3. 查询澄清:把模糊问题逐步细化

有些问题最大的麻烦不是太大,而是太模糊。

比如:

孙悟空有什么能力?

这个问题可以从很多角度回答:

  • 战斗能力
  • 移动能力
  • 变化能力
  • 法术能力
  • 剧情设定
  • 游戏技能

查询澄清的思路是先构建一个问题澄清树,把大问题分成更明确的方向。

大概像这样:

孙悟空有什么能力?
├── 战斗能力
│   ├── 武器能力
│   └── 招式技能
├── 变化能力
│   ├── 七十二变
│   └── 分身变化
└── 移动能力
    └── 腾云驾雾

这个思路来自 Tree of Clarifications 一类方法。

它的核心不是某个具体算法,而是一个朴素原则:

模糊问题不要急着检索,先把可能的解释方向展开。

在企业系统里,这也很常见。

用户问:

这个项目风险怎么样?

你可能要先澄清:

是进度风险?
是成本风险?
是质量风险?
是客户风险?
还是合规风险?

如果系统不澄清,直接检索“项目风险”,召回结果会非常散。

4. HyDE:先生成假设答案,再拿答案去检索

HyDE 是 Hypothetical Document Embeddings。

它的想法有点反直觉:

不要直接拿问题去检索。
先让大模型根据问题写一段“假设答案”。
再拿这段假设答案去检索真实知识库。

比如用户问:

游戏主人公有哪些技能?

这个问题本身很短,语义信息很少。

如果先让模型生成一段假设文档,它可能写出:

主人公可能拥有金箍棒、变化术、腾云驾雾、分身、闪避和战斗连招等技能。

这段话虽然不一定完全正确,但它比原问题包含更多检索线索。

拿它去知识库里检索,可能更容易命中真正讲技能设定的文档。

HyDE 的本质是:

用一个“可能的答案形状”,去接近真实答案所在的文档。

它适合原问题太短、太抽象、缺少关键词的场景。

但它也有风险。

如果假设文档生成得太偏,检索方向也会偏。

所以 HyDE 更适合作为一种增强策略,而不是所有问题默认都用。

五、查询路由:先判断该去哪查

检索前置处理 手绘图 4:查询路由交换台

讲完查询构建和查询翻译,再看查询路由就很自然了。

因为现在系统手里有很多可能的路:

  • 向量数据库
  • 关系数据库
  • 图数据库
  • 多模态检索
  • 关键词检索
  • 不同提示模板
  • 不同业务工具

用户问题进来以后,不能每次都全部跑一遍。

查询路由要解决的是:

这个问题最应该走哪条路?

比如:

Python 中列表和元组有什么区别?

这应该路由到 Python 文档知识库。

上个月销售额是多少?

这应该路由到关系数据库或业务函数。

糖尿病和哪些疾病有关联?

这应该路由到图数据库。

这张截图里有什么错误?

这应该路由到多模态检索或视觉模型。

黑悟空这个 Boss 怎么打?

如果系统有多个提示模板,也许应该路由到“战斗技巧专家”模板,而不是“剧情路线专家”模板。

所以查询路由可以有两种常见形态。

第一种是数据源路由:

问题 -> 判断数据源 -> 选择向量库 / SQL / 图数据库 / 多模态

第二种是提示模板路由:

问题 -> 判断意图 -> 选择对应专家提示词

,它们都是意图识别。

只不过一个决定“查哪里”,一个决定“用什么方式回答”。

一个简单的路由规则可以这样理解:

有统计、排名、数量、时间聚合 -> 优先 SQL
有实体关系、多跳路径、上下游依赖 -> 优先图检索
有制度、FAQ、解释性问题 -> 优先向量检索
有精确编号、错误码、函数名 -> 优先关键词检索
有图片、视频、截图 -> 优先多模态检索
问题复杂且跨类型 -> 多路路由后融合

查询路由看起来简单,但它很关键。

因为路由一错,后面再强也容易错。

比如本来应该查 SQL 的统计问题,被送进向量库,系统可能找一段“销售额定义”来回答。

本来应该查图数据库的关系问题,被送进普通文档库,系统可能只能返回零散描述。

所以成熟系统要记录路由日志:

原始问题
识别出的意图
选择的数据源
选择的提示模板
是否走了回退路径
最终答案是否命中

这样才能不断发现误路由样本,更新规则或模型。

六、把三块串成一条链路

现在我们把检索前处理串起来。

用户问题进入系统以后,可以先走这样一条逻辑:

05.检索前置处理 图 1

当然,真实系统不一定每次都跑这么完整。

简单问题可以走轻链路:

问题
-> 查询重写
-> 向量检索
-> 回答

结构化问题走 SQL 链路:

问题
-> 路由到关系数据库
-> 选择相关 schema
-> Text2SQL
-> 校验执行
-> 回答

图关系问题走 Cypher 链路:

问题
-> 路由到图数据库
-> 获取图 schema
-> Text2Cypher
-> 执行图查询
-> 回答

带条件的文本问题走混合链路:

问题
-> 抽取 Metadata Filter
-> 重写语义查询
-> 过滤后向量检索
-> 回答

这就是检索前处理最重要的变化:

它让 RAG 从“拿问题去搜文本”,升级为“先理解问题该怎样被查询”。

七、检索前处理最容易踩的坑

第一个坑,是把所有问题都当向量检索问题。

向量检索很强,但它不是万能入口。

统计、聚合、排序、精确过滤、多跳关系,这些问题往往需要 SQL、Cypher、Metadata Filter 或专门工具。

第二个坑,是只让模型生成查询,不给 schema。

Text2SQL 和 Text2Cypher 的质量很大程度取决于 schema 是否准确。

没有表结构、字段说明、关系方向和样例查询,模型很容易生成“像真的但不可执行”的查询。

第三个坑,是生成后直接执行。

SQL 和 Cypher 都可能出错,也可能越权。

任何可执行查询都应该经过校验、权限控制、行数限制、超时和审计。

第四个坑,是查询翻译过度。

重写、分解、HyDE 都可能提高召回,但也可能引入语义漂移。

用户问 A,系统不能为了“更完整”擅自查 B、C、D。

第五个坑,是路由后没有回退。

路由器一定会犯错。

所以应该有保守回退策略。

比如 SQL 生成失败时,可以回退到文档解释;图查询为空时,可以回退到向量检索;多路结果冲突时,可以提示证据不足,而不是硬答。

八、怎么判断检索前处理做得好不好

不要只看重写后的问题是否通顺,也不要只看 SQL 是否生成出来。

要看它有没有提升端到端检索效果。

可以观察这些问题:

  • 路由是否选对了数据源
  • Text2SQL 是否使用了真实存在的表和字段
  • Text2Cypher 是否基于真实图 schema
  • Metadata Filter 是否正确抽取了年份、作者、浏览量、权限等条件
  • 查询重写后,正确证据是否更靠前
  • 查询分解后,是否覆盖了原问题里的多个子意图
  • HyDE 是否带来了更相关的召回,还是让问题跑偏
  • 生成的可执行查询是否通过安全校验
  • 查询失败时有没有回退路径

工程上最好记录一份检索前处理日志:

原始问题
路由结果
查询翻译结果
抽取的 filters
选中的 schema
生成的 SQL / Cypher
校验结果
执行结果
回退路径
最终进入生成阶段的证据

如果没有这些日志,RAG 调优很容易变成“凭感觉改 prompt”。

而检索前处理一旦可观测,你就能知道:

答案不对,是路由错了?
是 schema 选错了?
是 SQL 生成错了?
是 filter 太严了?
是 query rewrite 漂移了?
还是后处理阶段把证据丢了?

这才是真正可调试的 RAG。

九、把这一节放回 RAG 全链路里

现在把这一节放回链路里看,检索前处理站在用户问题和检索系统之间。

它的职责不是简单“优化问题”,而是先做判断:

这个问题是不是 SQL 问题?
是不是图查询问题?
是不是带 metadata 条件的向量检索问题?
是不是需要重写、拆解、澄清或 HyDE?
是不是需要路由到不同数据源或提示模板?

所以这一节可以用一句话记住:

检索前处理,就是把自然语言问题变成正确的查询入口。

基础 RAG 关心的是:

怎么从向量库里找相似文本?

检索前处理开始关心的是:

这个问题到底该不该查向量库?
如果该查,又应该带着什么查询、什么过滤条件、什么路由策略去查?

这一步做好了,后面的检索才有机会找对。

下一篇就可以继续看索引优化。

如果检索前处理解决的是“问题怎么进入系统”,那索引优化解决的就是:

知识库本身应该怎么组织,才能让正确内容更容易被找出来。