跳到主要内容

当 RAG 本该是一次 JOIN 时

· 阅读需 10 分钟
Tian Pan
Software Engineer

一个支持团队向他们的新 AI 助手提了一个简单的问题:“上周有哪些企业客户提交了工单?”助手给出了一个自信、流利的回答,列出了六个账户。其中五个是正确的。有一个客户在两个月前就已经流失了,而另一个提交了三个工单的企业账户却完全遗漏了。直到一次续约会议谈崩了,才有人发现这个问题。

错误不在于模型,而在于架构。在设计评审的某个环节,一个带有硬性谓词(如方案层级、日期范围、工单计数)的问题被路由到了向量索引。团队因为拥有检索系统,所以就选择了检索。他们将工单记录向量化,将问题向量化,然后试图让余弦相似度(cosine similarity)来完成 WHERE 子句的工作。它做不到,也从来都做不到。

这是生产环境 AI 系统中最常见且讨论最少的故障模式之一:当真正的查询是关系型时,却动用了语义搜索。数据明明整齐地存储在带有外键的行中,答案本可以通过一个 JOIN 轻松获得。结果却经过了嵌入模型,导致精度化为乌有。

嵌入模糊了你本需要的精确细节

向量嵌入(vector embedding)是将意义有损压缩到几百个维度中。这种压缩正是语义搜索在处理非结构化文本时表现强大的原因——“取消我的订阅”和“我该如何停止计费”在向量空间中距离很近,尽管它们几乎没有共同的词汇。但当问题依赖于精确数值时,这种压缩就是破坏性的。

嵌入模型通常将“产品价格在 50 到 100 美元之间”和“产品价格恰好是 101 美元”视为几乎完全相同——相似度分数在 0.98 左右——即便一个满足过滤条件而另一个不满足。模型捕获了主题(价格,该产品),却丢弃了谓词(真正决定答案的边界)。WHERE price <= 100 子句永远不会犯这种错误。它并不更聪明,它只是基于数值操作,而不是基于数值的模糊映像。

否定句的情况更糟。众所周知,稠密检索器(dense retrievers)会忽略否定词:关于矛盾感知检索的研究描述了一种“语义坍缩”现象,即“患者患有糖尿病”和“患者没有患有糖尿病”在向量空间中几乎重叠,因为周围的标记(tokens)主导了嵌入,而单个否定词几乎无法改变它的位置。如果你问向量索引“哪些客户 在企业方案中”,你就像在掷骰子。

计数和聚合则完全超出了这种范式。最近邻搜索返回的是 k 个最相似的数据块。它没有“全部”的概念,没有 COUNT,没有 SUM,也没有 GROUP BY。向向量数据库询问“第三季度的平均解决时间”,它能做的最好的事就是递给你一份刚好提到这些词的文档。关于“有多少”的诚实回答并不是相似性搜索能提供的,因为它执行的操作是“寻找类似的东西”,而不是“枚举匹配的东西”。

判别特征:硬性谓词、聚合、多跳关联

你通常可以在写代码之前就诊断出路由错误的查询。阅读问题并寻找以下三个特征。

硬性谓词(Hard predicates)。 任何非真即假的子句,没有中间地带——如状态、方案层级、布尔标志、精确 ID、日期范围。“活跃订阅”、“5 月 1 日之后提交的工单”、“欧洲地区的账户”。这些都是过滤器。过滤器有正确答案和错误答案之分,而相似性搜索两者都给不了——它给你的是排名。排名是“最相关”的正确工具,但绝不适用于“符合条件”。

聚合(Aggregations)。 “有多少”、“平均值是多少”、“按月统计的总收入”、“按使用量排序的前五名”。一旦问题要求你将多行数据合并为一个数字或排序列表,你描述的就是 GROUP BYORDER BY 或窗口函数。检索返回的是行,它不会对行进行计算。

多跳关系(Multi-hop relationships)。 “上周有哪些企业方案的客户提交了工单”涉及三个跳转:客户 → 方案,客户 → 工单,工单 → 日期。对反规范化(denormalized)的数据块进行向量搜索,可能 会碰巧找到提到企业客户和近期工单的文档,但连接它们的关系在散文中是隐性的,模型必须通过阅读重新推导。在关系型模式中,这种关系是显性的——即外键——而 JOIN 可以精确地遍历它。在知识图谱中,它是一条边,图查询可以直接沿路径行进,而不是寄希望于语义相似性能重建它。

如果一个问题带有这三个特征中的任何一个,能正确回答它的存储系统应该是数据库或图,而不是向量索引。你的技术栈中存在嵌入模型,并不是在这里使用它的理由。

为什么优秀的团队仍然会犯错

这种错误很少是因为无知。它源于三种悄然将每个问题推向向量索引的力量。

首先是 RAG 成为了默认的思维模型。 “在产品中加入 AI”几乎条件反射式地被转化为“搭建一套检索增强流水线”。一旦向量数据库成为架构的核心,每个问题看起来都像是一个检索问题,包括那些显然是关系型的问题。你构建的工具变成了你观察世界的透镜。

其次是 将所有内容向量化感觉既统一又整洁。 一个摄取流水线、一个索引、一个查询路径。将某些问题路由到 SQL,另一些路由到向量数据库,意味着需要构建分类器,维护两条检索路径,并处理它们之间的衔接。统一的设计在白板上画起来更简单。但它是错误的,而且这种错误不会在图表中显现出来。

最后是 失败是隐蔽且流利的。 路由错误的逻辑查询不会抛出报错。它会返回一个自信、文采斐然但带有细微缺陷的答案——五个名字是对的,一个是错的。没有堆栈跟踪,没有 500 错误,仪表盘也不会泛红。系统看起来运行正常,并且会一直保持这种假象,直到某个具备领域知识的人发现遗漏的账户。到那时,架构已经定型,错误的模式已经被复制到了更多的功能中。

基准测试数据强化了这种自满。Text-to-SQL 系统在学术数据集上报告了 80–90% 的执行准确率,听起来已经可以投入生产。但企业的数据库架构有数百张表、含义模糊的列名以及存在于局部知识(tribal knowledge)中的业务逻辑,实际工作负载下的准确率会远低于基准测试。这给我们的启示不是“SQL 也不可靠”,而是 每条 检索路径都需要审视,且关系型路径至少是以一种你可以进行确定性测试的方式失败。

让模型编写查询,而非猜测答案

解决方法并非放弃 RAG。而是将每个问题引导至能够正确回答它的路径上,并认识到对于结构化数据,模型的工作是编写查询,而不是接收一堆大致相关的片段并对其进行推理。

路由(routing)开始。在检索之前,先对问题进行分类。它是否包含硬性谓词、聚合或多跳连接(multi-hop joins)?将其发送到结构化路径。它是否真正关乎非结构化文本的内容——例如“客户对入职流程的评价是什么”、“总结一下关于延迟的投诉”?将其发送到向量索引。这个路由器可以是一个小模型,甚至是针对解析后问题的规则;它不需要很聪明,但需要保持一致。这两条路径之间的衔接是真正的工程任务,但它是你可以观察和测试的工程,不像它所取代的那种隐性数据损坏。

在结构化路径上,模型将意图翻译为查询,并由数据库执行。LLM 将“上周有工单的企业客户”转化为 SQL 或图遍历;数据库返回精确的行。模型永远不会虚构行——它只描述它想要的内容,并由确定性引擎生成结果。这反转了通常的 RAG 流程:它不是“检索后生成”,而是“生成查询后执行”。事实准确性不再是概率性的,因为过滤、计数和连接发生在一个能够精确执行这些操作的系统中。

向量索引留给它所擅长的领域:没有模式(schema)的非结构化文本,即问题的本质是“寻找类似的内容”。例如自由文本知识库、通话记录语料库、技术文档。这是一个巨大且有价值的领域——只是不包含外键的领域。

许多实际系统需要两者组合。元数据——订阅方案、日期、地区、状态——存储在列中并使用 SQL 进行过滤。剩余的非结构化内容——实际的工单文本、通话记录正文——被嵌入并进行语义搜索。查询首先通过精确谓词缩小到正确的行,然后仅在该过滤后的集合中运行相似性搜索。你在问题需要精度的地方获得精确度,在文本需要模糊匹配的地方获得模糊性,而不是强迫一种机制糟糕地处理这两项工作。

每次设计评审中都要问的问题

当你在规格说明书中看到“AI 驱动的搜索”或“智能助手”时,在问其他任何问题之前,请先问一件事:数据长什么样,问题长什么样?

如果数据具有模式(schema)——行、列、外键、类型——并且问题涉及谓词、计数或连接,那么答案就是一个查询。在其前端放置嵌入模型并不会增加智能;它增加了一个有损层,将精确的答案变成了似是而非的答案。向量数据库对于它所针对的问题来说是一个真正强大的工具。但它也是错误的默认选择,而且“我们已经有了 RAG 流水线”并不是通过余弦相似度来执行 JOIN 操作的理由。

最好的 AI 系统不是那些将所有内容都嵌入的系统。而是那些知道哪些问题值得获得精确答案,并将其路由到能够提供该答案的系统的系统。

References:Let's stay in touch and Follow me for more thoughts and updates