向量检索的准确性:从一次搜不准说起
原创 · 约 18 分钟阅读 · 阅读 --

向量检索的准确性:从一次搜不准说起

作者: Alex Xiang


古董级程序员,大厂出来后一直在创业公司,现在仍在一线做 AI 相关的工程。更完整的技术记录写在微信公众号「字与码」:工作经历、对新工具的看法,以及这些年踩过的坑,会不定期发在那里。若这篇对你有用,欢迎顺手关注。

向量检索刚接进系统时,最容易给人一种“终于不用管关键词了”的错觉。把文档、商品、接口、知识库条目都转成 embedding,用户问一句自然语言,拿 query embedding 去最近邻搜索,结果不就出来了吗?

真的上线以后,问题会慢慢冒出来。

用户搜“美股实时行情”,结果里混进一堆“金融数据服务商介绍”;搜“获取公司工商信息”,排在前面的是某个全能数据平台的天气接口;搜“发票识别”,出来的是 OCR 服务商下面所有 API,而不是发票识别那一个。每个结果看起来又都不能说完全无关,因为它们所在的供应商、文档或页面确实提过金融、数据、OCR。可用户要找的不是“沾边”,而是“能用”。

这篇文章借一个虚构的“API 工具市场”来讲这类问题。场景是假的,坑是真的。

先看一个常见错误

假设我们有一批 API 工具,每个工具有这些信息:

  • 工具名称:比如“股票分钟级行情”
  • 工具描述:说明这个接口返回什么
  • 分类:金融、股票、行情
  • 接口地址:https://api.example.com/v1/market/quotes
  • 所属服务商名称
  • 所属服务商介绍:比如“我们是一家全球领先的数据智能服务平台,覆盖金融、企业、地理、天气、内容审核等多种领域……”

最省事的索引方式,是把这些字段拼成一段大文本:

股票分钟级行情
返回指定股票的分钟级成交价、成交量和盘口信息
金融 股票 行情
https api example com v1 market quotes get
某某数据
我们是一家全球领先的数据智能服务平台,覆盖金融、企业、地理、天气、内容审核等多种领域……

然后把这段文本喂给 embedding 模型。

这看起来信息很全,实际却把向量搞脏了。Embedding 模型不是数据库字段过滤器,它会把整段文本压到一个固定维度的向量里。你给它的文本越杂,向量表达的东西就越像一锅粥:工具能力、服务商宣传、URL 片段、通用行业词、HTTP method 都混在一起。

尤其是服务商长介绍。一个服务商下面可能有几十上百个工具,它们共享同一段“全球领先、覆盖多领域、稳定可靠”的介绍。结果是这些工具的向量会被同一段长文本拉近,哪怕一个是股票行情,一个是天气预报,一个是证件识别。

用户搜“股票分钟行情”时,系统不是在问“哪个工具最像这个需求”,而是在问“哪段混合文本整体上最像这个需求”。这两个问题差得很远。

向量检索吃的是“语义主菜”,不是字段自助餐

全文检索喜欢宽一点。多一些别名、缩写、产品名、路径词,往往能提高召回率。向量检索刚好相反,它需要的是干净的语义表达。

一个工具的向量文本,应该尽量回答三个问题:

这个工具做什么? 输入输出是什么? 用户会在什么意图下找它?

不该让它回答太多周边问题:服务商有多厉害、URL 是什么、接口用了 GET 还是 POST、公司覆盖多少业务线。这些信息有价值,但不是同一种价值。

我现在更倾向把“用于向量的文本”和“用于全文的文本”分开。

向量文本可以长这样:

股票分钟级行情
查询单只股票在指定时间范围内的分钟级成交价、成交量、涨跌幅和盘口摘要。
适合用于行情看板、量化回测、交易监控。
分类:股票、行情、金融市场数据。

全文文本可以更宽:

股票分钟级行情 股票 分钟线 K线 quote market candle
查询单只股票在指定时间范围内的分钟级成交价、成交量、涨跌幅和盘口摘要。
路径关键词:market quotes candles
服务商:某某数据

两者服务的目标不同。向量文本让语义空间更清楚;全文文本让关键词入口更多。

噪声不是“错信息”,而是“错位置的信息”

很多检索事故不是因为数据错,而是因为信息放错了位置。

URL 不是没用。用户有时会搜 quotecandlesinvoice 这类路径词,全文索引里保留它们是合理的。但 URL 里还有大量无意义碎片:httpsapiv1comget。这些词进入 embedding 以后,对几乎所有 REST API 都是同一种背景噪声。

服务商介绍也不是没用。用户搜“某某数据 股票接口”时,服务商名称很关键;用户比较供应商能力时,服务商介绍也重要。但如果每个工具 embedding 都塞一整段服务商宣传,同一供应商下面的工具就会变得过分相似。

分类同样要小心。分类词通常短而稳定,适合放进向量文本;但过粗的分类,比如“数据”“工具”“API”“企业服务”,价值不大,还会把本来不同的东西拉近。

所以更准确的说法不是“删掉某个字段”,而是给字段安排正确的位置:

  • 工具名称、核心描述、能力标签:适合进 embedding
  • 路径资源名、别名、缩写、英文术语:适合进全文索引,也可以少量进 embedding
  • URL、method、host、版本号:多数情况下只适合过滤、展示或调试
  • 服务商名称:可以少量进入 embedding
  • 服务商长介绍:更适合供应商级检索,不适合重复塞进每个工具向量

这里有个朴素原则:越是会被大量条目共享的文本,越不该无脑进入每个条目的向量。

不要指望一个向量解决所有检索

向量检索擅长语义近似,但它也有盲区。

用户搜“SEC 10-K filing”时,10-K 是强关键词;搜“ISIN 查询”时,ISIN 这个缩写本身很重要;搜“v2 invoice parse”时,版本和英文路径词可能就是线索。纯向量可能知道“年报”“证券披露”“发票识别”这些语义,但对精确 token 的敏感度不如全文检索。

反过来,全文检索也不懂“查企业工商信息”和“公司注册资料查询”是相近意图,除非你把同义词都铺进去。

所以生产系统里,比较稳的方案通常是混合召回:

  • 向量路负责语义召回
  • 全文路负责精确词、缩写、路径、专有名词
  • 结构化过滤负责状态、地区、权限、分类等硬约束
  • rerank 或融合排序负责把两路候选重新排一次

混合召回不一定复杂。最简单可以用 RRF(Reciprocal Rank Fusion):不纠结两路分数尺度是否一致,只看各自排名,再把排名靠前的候选融合起来。它不完美,但足够稳,尤其适合向量分数和全文分数很难直接比较的场景。

关键是别把融合当魔法。如果向量文本本身很脏,RRF 只能减少损失,不能从根上修好语义空间。

阈值不是配置项,是产品判断

很多系统会有一个向量相似度阈值,比如低于 0.45 就不召回。这个阈值很容易被当成技术参数,实际上它更像产品判断。

阈值高,结果更干净,但可能漏掉可用答案。 阈值低,召回更多,但噪声会压垮排序。

更麻烦的是,阈值不具有跨模型、跨语料、跨文本模板的可迁移性。你把 embedding 文本从“全字段拼接”改成“干净能力描述”以后,分数分布会变;换 embedding 模型以后,分布也会变;从中文为主变成中英混合,分布还会变。

因此每次改索引文本、模型、维度或召回策略,都应该重画一遍分数分布。至少看三类样本:

  • 明确相关的 query-document 对
  • 明确不相关的 query-document 对
  • 容易混淆的近邻,比如同供应商不同工具、同分类不同能力

阈值应当从这些样本里来,而不是从默认配置或某篇博客里抄一个数字。

TopK 也会影响准确性

向量检索的 TopK 不只是性能参数。K 太小,后面的 rerank 没东西可排;K 太大,噪声候选太多,融合排序压力变大,延迟也会上去。

如果系统有 rerank,召回阶段通常要比最终返回多拿一些候选。比如最终展示 10 条,召回 50 或 100 条,再让 rerank 处理。但这不是越大越好。召回池里的噪声太多,LLM rerank 或 cross-encoder rerank 也会被迫在垃圾堆里找金子。

我更喜欢把 TopK 拆成几个层次看:

召回 TopK:宁可稍宽,保证相关结果进池子。 融合后 TopK:控制进入 rerank 的候选规模。 最终 TopK:按产品展示需求决定。

这三个 K 不是一个数,也不该共用一个配置。

索引文本要有版本

很多团队会给 embedding 模型记版本,却忘了给“生成 embedding 的文本模板”记版本。

这会带来一个排查噩梦:数据库里都是同一个模型生成的向量,但有一半来自旧模板,包含服务商长介绍;另一半来自新模板,只含工具能力描述。它们在同一个向量空间里,但语义分布不一样。搜索结果忽好忽坏,很难从单条数据看出来。

所以除了模型版本,还应该记录索引文本版本。哪怕只是一个简单字符串:

embedding_model = text-embedding-xxx
embedding_template = tool-capability-v3

当模板变化时,明确决定是全量重建,还是后台渐进重建。不要让新旧模板长期混在一起却没有标记。

评估集比感觉可靠

“搜不准”最难讨论,因为每个人脑子里的“准”不一样。

比较好的办法是维护一个小而精的检索评估集。它不需要一开始就很大,三五十条高价值 query 就能发现很多问题。每条 query 记录:

  • 用户原始问题
  • 期望出现的结果,可以是 top1、top3 或 top10
  • 不能出现的明显错误结果
  • query 类型:同义表达、缩写、长句需求、精确名词、跨语言等

比如在虚构的 API 工具市场里,可以有:

query: "查询 A 股股票分钟 K 线"
期望:分钟行情、K线接口
不期望:供应商介绍、宏观经济日历、天气接口

query: "识别发票图片里的金额和税号"
期望:发票 OCR、票据识别
不期望:通用图片压缩、身份证 OCR

每次改索引文本、阈值、融合权重、rerank 模型,都跑一次评估集。只看线上几条 bad case,很容易修好一个、打坏三个;评估集至少能提醒你“别把原来能搜到的东西弄丢了”。

向量检索的核心原则

写到这里,可以把经验压成几条更通用的原则。

第一,先定义检索单元。 你到底是在搜工具、搜服务商、搜文档段落,还是搜某个字段?检索单元不同,embedding 文本就不同。把供应商级信息塞进工具级向量,常常是准确性下降的开始。

第二,短文本不一定差,干净更重要。 很多人担心 embedding 文本太短,模型理解不够,于是拼进去一堆上下文。实际经验是:只要名称、能力描述、核心标签写清楚,短文本往往比全字段拼接更稳定。

第三,共享文本要克制。 模板话术、公司介绍、页脚、免责声明、URL 前缀、导航栏,这些文本在很多条目里重复出现,会把向量空间拉歪。

第四,全文和向量分工。 精确 token、缩写、路径、型号、版本号交给全文;意图、同义表达、自然语言需求交给向量。别要求一种索引同时做好所有事。

第五,阈值和 TopK 要用数据调。 不要凭感觉调一个“看起来差不多”的相似度阈值。分数分布、召回率、误召回率,至少要有一组样本支撑。

第六,别只看召回,也要看排序。 相关结果在第 50 名,对用户来说约等于没搜到。检索系统的目标不是“数据库里有”,而是“用户看得见”。

第七,持续记录 bad case。 每次用户说“这个搜不到”,不要只修这条 query。把它归类:是文本模板噪声、同义词问题、阈值过高、过滤条件过严,还是 rerank 判断错。bad case 是检索系统最有价值的训练材料。

一个可落地的改造顺序

如果一个已有系统已经出现向量搜不准,我不会第一步就换模型。换模型成本高,也容易掩盖真正的问题。

我通常会这样排:

先抽样看当前 embedding 文本。随机拿几十条,人工读一遍。如果你自己都觉得“这段不像一个可检索对象,而像一份字段大杂烩”,模型大概率也不会帮你变聪明。

然后删噪声。先去掉大量共享的长文本,比如服务商介绍、模板页脚、无关导航;再去掉 URL、method、host 这类结构噪声;保留名称、核心描述、少量高质量标签。

接着重建一小批向量,跑评估集。不要一上来全量重建。选一批覆盖典型问题的数据,做旧索引和新索引对比,看 top10 有没有明显改善。

再调融合。向量路变干净以后,全文权重、RRF 参数、rerank 候选池大小都可能要重新调。以前为了补救向量噪声而加的规则,此时可能反而成了副作用。

最后再考虑换模型。模型当然重要,但一个被污染的输入模板,换再好的 embedding 模型也只是把噪声编码得更精致。

结尾

向量检索的准确性,核心不在“有没有 embedding”,而在“你让 embedding 表达什么”。

把所有字段拼起来,是数据工程上最省事的方案,却常常是检索质量上的慢性毒药。好的向量索引更像一次编辑:删掉无关的,压住重复的,保留真正能代表检索对象的语义。全文检索、结构化过滤、rerank、监控评估,各自站好位置,系统才会稳。

很多时候,搜不准不是模型不懂,而是我们给它看的东西太杂。