笔记本退货事件暴露 RAG 检索准确性问题

几个月前,我们收到一份让我印象深刻的 bug 报告。用户基于 RAG(检索增强生成)管道构建了一个客服智能体,遇到了这样的场景:客户询问三周前购买的笔记本电脑能否退货。智能体检索到一份退货政策文档,引用了 30 天退货窗口,并告知客户可以寄回。回答信心十足,但完全错误。
文档是真实的——只是它来自 2023 年,而当前政策已改为电子产品 14 天退货期。向量相似度没有时效性或范围概念,查询向量嵌入与 2023 年政策之间的余弦距离非常理想。这很正常,因为措辞几乎完全相同。
深入调查后,我意识到这不是 bug,而是架构问题。这个笔记本退货请求改变了我对 AI 时代数据库需求的看法。
无人提及的鸿沟
随着团队将 RAG 从原型推向生产,这类问题越来越常见。过去两年,整个行业都专注于解决模型幻觉(Hallucination)问题。RAG 是答案:将模型锚定在真实文档中。它确实有效。但在这个过程中,我们开始把检索当作已解决的问题。事实并非如此。
我反复思考这一点:语义相似度与事实正确性不是一回事。向量搜索能找到与查询语义相近的文档,这很有用,但“语义相近”不等于“在当前上下文中正确”。过时的政策与当前政策语义相似。面向企业客户的文档与免费层用户的查询语义相似。租户 A 命名空间中的机密文档与租户 B 的查询语义相似。
“向量搜索能找到与查询语义相近的文档,这很有用,但‘语义相近’不等于‘在当前上下文中正确’。”
我称之为“检索准确度鸿沟”。这是向量相似度认为相关的内容与你的应用实际需要正确内容之间的距离。你无法通过更好的向量嵌入(Embedding)来弥合这个鸿沟。缺失的信息——时间戳、范围、权限——是结构化数据。它们存在于列中,而不是向量空间里。
这是个数据库问题。
混合搜索的真正含义
我说的混合搜索(Hybrid Search)有特定含义:将向量相似度与结构化 SQL 谓词结合的单一数据库查询。不是那种先做向量搜索、返回 100 个候选结果、然后在应用代码中过滤的两阶段管道。而是由数据库引擎整体优化的单一查询。
这种差异比听起来更重要。当过滤发生在应用代码中时,你是在应用廉价约束之前先做昂贵的工作——扫描完整的向量索引。这顺序反了。理解向量和关系操作的数据库可以使用选择性估计来决定是先过滤还是先扫描。这是我们几十年来在关系数据库中已有的查询规划逻辑,只是需要扩展到向量索引。
让我具体展示一下。假设有这样的模式:
CREATE TABLE documents (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
content TEXT,
embedding VECTOR(1536),
team_id BIGINT NOT NULL,
doc_type VARCHAR(50),
updated_at DATETIME NOT NULL,
status ENUM('active','deprecated','draft'),
INDEX idx_embedding USING HNSW (embedding),
INDEX idx_team_status (team_id, status)
);
没什么特别的。标准关系模式加一个向量列。以下是解决我描述的故障模式的三种查询模式。
模式 1:时效性过滤
添加时间约束后,陈旧文档问题就消失了:
SELECT id, content,
VEC_COSINE_DISTANCE(embedding, @query_vec) AS distance
FROM documents
WHERE status = 'active'
AND updated_at >= NOW() - INTERVAL 90 DAY
ORDER BY distance
LIMIT 5;
WHERE 子句在向量扫描之前修剪候选集。在 1000 万行的语料库中,这通常能消除 60-80% 的行。数据库同时变得更快、更准确。这正是我喜欢的权衡。
模式 2:通过连接实现租户隔离
这是我最担心的模式,因为一旦失败,就是安全事件,而不仅仅是错误答案:
SELECT d.id, d.content,
VEC_COSINE_DISTANCE(d.embedding, @query_vec) AS distance
FROM documents d
JOIN user_permissions p
ON p.team_id = d.team_id
WHERE p.user_id = @current_user
AND d.status = 'active'
ORDER BY distance
LIMIT 5;
与权限表的关系连接。无论文档与查询的语义多么相似,用户都看不到其权限范围之外的内容。约束由数据库引擎强制执行,而不是由可能忘记更新的应用代码。
试试用独立的向量数据库实现这个功能。你必须将整个 ACL 复制到元数据标签中,每次权限变更时重新索引,并希望基于标签的过滤能处理权限组的组合爆炸。我见过团队尝试这样做,结果都不太好。
模式 3:带聚合的分类排名
有时正确答案不是单个文档,而是跨多个文档的模式。这个查询按类型分组匹配,找出信号最密集的区域:
SELECT d.doc_type,
COUNT(*) AS match_count,
MIN(VEC_COSINE_DISTANCE(d.embedding, @query_vec)) AS best_dist,
GROUP_CONCAT(d.id ORDER BY
VEC_COSINE_DISTANCE(d.embedding, @query_vec)) AS doc_ids
FROM documents d
WHERE d.status = 'active'
AND VEC_COSINE_DISTANCE(d.embedding, @query_vec) < 0.3
GROUP BY d.doc_type
ORDER BY match_count DESC, best_dist ASC
LIMIT 3;
这告诉 LLM:“答案很可能在 FAQ 文档(7 个匹配)中,而不是博客文章(2 个匹配)中。”然后你从获胜类别中检索顶部文档。这是个 GROUP BY。向量数据库无法执行 GROUP BY。这是关系代数,当你的语料库包含重叠的文档类型时,它能显著改变检索质量。
数据表现
我们根据生产工作负载对 1000 万行的企业知识库进行了建模。这包括 18 个月的内容、混合的文档类型,以及 500 个带人工标注真实值的查询。结果如下:
| 指标 | 纯向量(前 5) | 混合搜索(前 5) |
|---|---|---|
| Recall@5 | 72% | 94% |
| Precision@5 | 58% | 87% |
| 前 5 中陈旧文档比例 | 23% | < 1% |
| 跨租户泄露率 | 8% | 0% |
| p50 延迟 | 45 ms | 62 ms |
| p99 延迟 | 120 ms | 155 ms |
延迟成本为 15-30 毫秒,用户感知不到。零跨租户泄露率不是统计改进,而是关系连接强制执行保证。这是你可以带到安全审查中的属性。
有趣的是,在许多实际情况下,混合搜索实际上更快,因为结构化过滤器大幅减少了向量搜索空间。当 70% 的语料库在向量扫描开始前就被修剪掉时,实际耗时就会下降。请注意,结果会因语料库分布和过滤选择性而异。
“向量边车”反模式
我想谈谈一个常见的架构模式,我认为它是生产环境中大多数 RAG 质量问题的根源。
这种模式是这样的:你有一个主数据库(通常是 MySQL 或 PostgreSQL),存放应用数据。然后你搭建一个独立的向量数据库来存储向量嵌入。现在你需要一个同步管道来保持两者一致。每个文档的插入、更新和删除都必须传播到两个系统。你要维护两个模式、两个连接池、两个监控仪表板,以及中间脆弱的 ETL 作业。
我称之为向量边车(Vector Sidecar),它会随着时间推移产生三个相互叠加的问题:
- 一致性窗口:两个系统总存在不一致的间隙。文档可能在主数据库中被标记为已弃用,但在同步完成前仍会被向量存储返回。在退货政策示例中,这正是发生的情况:政策在主数据库中更新了,但向量索引是陈旧的。
- 无法跨系统连接:你无法在单一查询中将 ACL 表与向量索引连接。所以你最终会将权限数据复制为元数据标签,这意味着每次权限变更都需要重新索引。在规模上,这变得昂贵且容易出错。
- 双倍运维负担:两个数据库意味着两套值班轮换、两种容量规划模型和两种故障模式。我构建分布式系统十多年了,提高可靠性的最有效方法就是减少移动部件数量。
“我构建分布式系统十多年了,提高可靠性的最有效方法就是减少移动部件数量。”
替代方案很简单:将向量和结构化数据放在同一个数据库中。一个连接字符串。一个事务边界。一个一致性模型。数据库处理查询规划,根据选择性估计决定是先扫描向量索引还是先过滤。
这也是我们直接在 TiDB 中构建向量支持的原因之一。当我们开始看到用户遇到这些问题——一致性 bug、跨租户泄露、运维复杂性——答案不是更好的同步管道,而是完全消除对同步管道的需求。
为什么 SQL 兼容性在这里很重要
这一点有个实际维度,我认为被低估了。多年前我们决定在 TiDB 中实现 MySQL 有线协议时,是为了降低采用摩擦。但在 AI 时代,这带来了更深层的好处。
SQL 是应用开发的通用语言。每个 ORM 都懂它。每个连接池都支持它。你团队的每个工程师都写过 SQL 查询。当你的 AI 数据库使用相同协议时,我上面描述的混合搜索模式并不新奇,因为它们只是 SQL 查询。你的团队不需要学习新的查询语言、新的客户端库或新的运维模型。他们编写一直使用的 SQL,只是加了一个向量距离函数。
通过观察数千个 TiDB 部署,我学到采用障碍比功能列表更重要。最好的架构是你的团队真正能交付的架构。
何时不需要混合搜索
我想诚实地说明纯向量搜索完全适用的情况,因为我认为任何技术建议的可信度都取决于承认其局限性。
- 单租户、单文档类型的语料库:如果你正在为一个团队构建一个产品文档的知识库搜索,纯向量搜索配合好的嵌入模型就能很好地服务你。我描述的故障模式源于异构性,例如多个租户、文档类型或时间范围。
- 探索性或创意用例:如果用户正在头脑风暴——“找一些与这个想法相关的东西”——近似检索正是你想要的。严格正确性不是目标。
- 人机协同(Human-in-the-Loop)工作流:如果人类在每次结果被采取行动前都进行审查,偶尔出现陈旧文档的成本是可管理的。当智能体自主采取行动时,风险就变了。
如果正确性是可选的,向量就足够了;如果正确性是必需的,它们就不够。但一旦你拥有多个租户、过期的文档,或任何不正确检索导致不正确行动且无人审查的场景,你就需要混合搜索。对于大多数生产 RAG 系统来说,这是第一天就需要考虑的问题。
中间层
我一直在思考 AI 栈中真正的杠杆点在哪里。行业在两个层面投入了巨大精力:嵌入模型(我们编码含义的效果如何?)和生成模型(我们合成答案的效果如何?)。两者都很重要。但它们之间还有一个被视为商品的第三层:实际检索上下文的数据库查询。
这个中间层是正确性所在之处。嵌入模型将问题转换为向量。生成模型将文档转换为答案。但检索查询决定了模型看到哪些文档。如果这一步出错,下游的一切都会出错,无论你的嵌入或 LLM 有多好。
混合搜索——在单一数据库的单一查询中结合向量相似度和关系过滤器——是弥合检索准确度鸿沟的方法。这不是复杂的技术。它是带有距离函数的标准 SQL。唯一的前提是一个不强迫你在向量和关系之间做出选择的数据库。
十多年前我们开始构建 TiDB 时,我们的论点是数据库应该适应应用,而不是相反。这意味着 MySQL 兼容性,这样你就不必重写应用。这意味着水平可扩展性,这样你就不必在增长拐点重新架构。现在这意味着原生向量支持,这样你就不必为了构建 AI 功能而附加一个单独的系统。
论点没有改变。改变的是应用。
觉得有用?分享给更多人