一个研发同学给知识库提了个很具体的问题。

「订单取消后,优惠券会不会退回?」

向量库召回了一篇讲退款、一篇讲优惠券、一篇讲下单失败的文档。它们都不算错,甚至语义上相当接近。可真正写着「取消订单且优惠券未核销时,原券退回」的那段规则,排在十几名之后。

这就是很多 RAG 项目最折磨人的时刻。模型没有胡说,检索也不是完全失灵。它只是丢掉了一个人会很在意的小词,取消、已核销、退回,恰好构成了答案的边界。

Sentence Transformers 6.0 新增的 MultiVectorEncoder 让我重新看了一眼这个老问题。它把 ColBERT 风格的 late interaction 训练流程正式纳进库里。新闻本身不该被夸成「RAG 终于有救了」,检索没有一招鲜。但它提醒了一个很容易被我们忽略的事实。

很多检索错误,发生在生成答案之前。

而且往往发生在我们把整段文本过早压成一个向量的那一刻。

一条向量,是一份很昂贵的摘要

主流 RAG 的路径很熟悉。把一段文档编码成一个 dense vector,把用户问题也编码成一个 vector,二者做一次相似度计算,取 Top K,再交给大模型回答。

这个设计有两个优点,快,也便宜。对于 FAQ、帮助中心、语义相近的短文档,它直到今天仍然是很好的默认解。不是所有系统都值得为了更高的检索表达力背上一套更复杂的索引。

但它也有一个天然的取舍。一段几百 token 的文档被压成一个固定长度的表示后,主题、语气、常见概念通常还能留下来,细粒度的限定条件却要彼此竞争。

你想想看,下面两段话放进一个企业知识库。

  • 退款成功后,已使用的优惠券不退回
  • 取消未支付订单时,未核销优惠券自动退回

它们都在说优惠券和退回。单向量很可能把这两段放得很近。可一个真正有用的系统,不能只知道它们相关。它要分得清退款、取消、支付、核销这些词的组合关系。

很多团队遇到这个问题,会立刻换一个更大的 embedding 模型,或者把 chunk 切得更小。我理解这个反应。换模型和切 chunk 都很容易接进现有链路,也确实可能改善结果。

只是它们有时是在给摘要换更好的笔,或者把原文切成更碎的纸片。那次压缩仍然发生了。

Multi-vector 把匹配推迟到最后一刻

ColBERT 一类 multi-vector 检索不再让整段文本只留下一个向量。它为 query 和 document 中的 token 保留多个小向量,计算相关性时,让 query 里的每个 token 去文档里找最像自己的 token,再把这些最佳匹配累加起来。

常见的写法叫 MaxSim。

1
2
score(query, document)
= 每个 query token 的最佳 document token 相似度之和

这不是逐字匹配。token 向量仍然带着上下文,模型可以学到「退款」和「退回」的相近,也可以从周围语境里理解「核销」不是一个可有可无的装饰词。

它和 Cross-Encoder 的位置也不一样。Cross-Encoder 会把 query 与每篇候选文档一起送进模型,效果常常很好,但在大库里逐篇跑成本太高,通常放在最后重排。Multi-vector 保留了更细的信号,却仍可先建索引做大规模候选召回。

这就是 late interaction 的意思。不要在入库时急着把文档压成一句摘要,先把细节留住,等 query 真来了再决定哪些细节相互匹配。

听起来很优雅。

代价也是真实的。一个文档从一条向量变成许多 token 向量,索引更大,检索系统和向量数据库的支持情况也不再像 dense vector 那样整齐。不能只看离线榜单涨了几个点,就把生产检索全部翻新。

这次更新真正有用的地方,不只是多了一个类

Hugging Face 的文章里给出了一组很有参考价值的数据。作者在医疗检索评测上,用单张 RTX 3090 训练约 14.5 小时的 mLateOn-medical,超过了其测试中的通用 dense、sparse、lexical 和 multi-vector 检索器。

这里最该记住的不是「一张 3090 就够了」。不同数据规模、负样本质量和目标延迟,成本会差很多。更重要的是文章里另一个发现。

医疗文本平均长度约 941 token,而不少通用检索 checkpoint 的文档长度上限只有 180、300 或 512 token。作者报告,截断本身最多带来 0.24 的 NDCG@10 损失,比若干模型架构间的差异还大。

这个结论有点刺耳。我们经常花几周讨论选 bge、e5 还是某个新 embedding 模型,却没有先检查文档的后半截是否根本没进模型。

先把输入是否完整这件事查清楚。

再谈模型好坏。

Sentence Transformers 这次把模型、数据集、损失函数、训练参数、评估器和 Trainer 串成了完整路径。对 Python 训练侧而言,它降低的是从「我知道 ColBERT」到「我能做一次可复现微调」之间的摩擦。对于 Java 或 Spring 团队,它不代表你需要把线上服务改成 Python。训练和服务本来就该分开看。

一个更正常的分工是,Python 负责训练、离线评估和导出模型,Java 服务继续拥有业务权限、编排、审计和 API。检索服务可以是独立组件,用 HTTP 或 gRPC 给 Spring AI、LangGraph 或自建 Agent 提供候选文档。

别因为模型工具在 Python,就把业务系统的边界一起搬过去。

RAG 质量差时,先别急着 fine-tune

我见过不少团队把 fine-tune 当成检索问题的终点。其实吧,它更像一次昂贵的放大镜。数据和评估没准备好,微调只会更稳定地学会你的噪音。

先做四个不那么性感的检查。

看看问题到底卡在召回还是生成

先把生成模型拿掉,只看 query 对应的标准证据是否出现在 Top 5、Top 10 或 Top 20。若正确文档根本没有被召回,应该查 chunk、索引、查询改写和检索模型。若证据已在上下文里,回答仍然错,才该去看 prompt、引用约束、工具编排和模型推理。

这一步看上去基础,实践里却总有人跳过。因为最终用户只看见一个错答案,团队很容易把所有锅丢给 LLM。

不要让生成模型替检索背锅。

用真实问题构造评估集

官方文章强调了领域差异。网页搜索、法律发现、代码搜索、医学文献里的相关性标准根本不是一回事。企业内部知识库也一样。

与其从公开数据集拿一堆漂亮分数,不如从真实工单、客服追问、研发排障记录里抽 100 到 300 个问题。每题标出允许作为证据的段落,并保留难例。

难例特别重要。比如「取消」和「退款」、「已开票」和「未开票」、「生产环境」和「预发环境」。这些一词之差的规则,才是业务真正愿意为检索质量付费的地方。

评估至少拆成 Recall@K、MRR 或 NDCG,再加一项人工可读的失败分类。不要只盯一个平均数。平均数上涨,可能只是简单问题更简单了,最贵的误召回仍然没动。

审计 chunk,而不是迷信固定长度

把所有文档按 512 token 切片是常见做法,也常常是方便做法。可订单规则、接口契约、故障复盘和代码文件并不按 512 token 长度组织。

我更建议先按业务结构切。标题与正文绑定,表格的标题、条件和结论不要拆开,代码块与它解释的配置不要分家。对于长文档,保留父子关系。子 chunk 用于召回,父段或父文档用于给模型补全上下文。

如果一条规则跨了两个 chunk,单向量模型常常会把两个半句都判成「有点相关」。multi-vector 可能改善 token 级匹配,却救不了被切断的业务语义。

数据入口错了,后面的模型只是在认真执行错误。

把检索成本写进验收标准

multi-vector 的索引膨胀不是小事。业务方常常只问准确率,平台团队必须补上几个问题。

  • 索引体积从多少变到多少
  • P95 检索延迟是否仍满足接口预算
  • 高峰期并发下的 CPU、内存和 GPU 消耗怎样
  • 重建索引需要多久,能否灰度回滚
  • 候选召回后是否还需要 Cross-Encoder 重排

这块很容易被忽视。但如果一个检索器让答案更准 3%,却把写入成本放大十倍、查询 P95 拉到两秒,它未必适合客服入口,却可能非常适合法务检索或研发知识库。

同一个模型,在不同工作流里会得出不同结论。

一个适合企业 Agent 的渐进式方案

如果你正在做 Java、Spring 或 LangGraph 的知识 Agent,我不建议第一天就押注 multi-vector。先把检索链路拆出可替换的接口,再让数据说话。

第一层是召回。保留你现在的 dense retrieval,同时接入 BM25 或 sparse retrieval。用 hybrid 做基线,因为精确术语、错误码、类名、配置项往往需要 lexical 信号。

第二层是重排。对 Top 50 或 Top 100 候选,先用成熟 reranker 验证收益。很多场景里,dense 加 BM25 再加 reranker 已经能解决大部分问题,系统复杂度也可控。

第三层才是 multi-vector。把它放到最容易出现细粒度歧义、文档长且规则密集的语料集上做 A/B。比如内部 API 契约、支付规则、医疗或法务材料、复杂代码库。不要一上来把公司所有 wiki 重建一遍。

第四层是证据约束。无论检索器多聪明,Agent 都要把引用片段、文档版本、命中分数和最终回答一起记录。高风险回答要求它指出依据在哪一段,没有证据就明确说不知道。

这套顺序有点慢。

但它比一开始就为一个新架构押上全部线上流量更稳。

微调的收益,来自你终于定义了「相关」

multi-vector 还有一个值得认真看的点。它对领域微调的反应往往很好,不只是因为模型结构细,而是因为企业终于有机会把自己的相关性定义喂给模型。

通用模型不知道「退款成功」和「取消未支付订单」在你们的业务里是两条互斥路径。它也不知道某个 Java 包名、某个 Spring 配置前缀或某条告警码,哪个才是排障问题真正要找的证据。

这些知识不是模型参数越大就会自动拥有。

它来自高质量的 query、正例、难负例和持续回收的失败样本。

Hugging Face 的示例使用了训练集、损失函数、评估器和 Trainer 的完整组合。把它迁移到企业里,最难的通常不是调一个 learning rate,而是建立反馈闭环。用户点了「没解决」,Agent 引用了错误文档,人工修正了答案,这些都应该变成可复查的训练候选。

当然,不能把所有线上对话直接倒进训练集。隐私、权限和暂时性的错误判断都要先处理。训练数据需要脱敏、去重、版本化,还要把评估集和训练集隔开。否则你会得到一个特别擅长背熟内部测试题的模型。

我自己的感受是,检索微调最成熟的信号不是团队说「我们有一堆数据」,而是能回答两个问题。

这条 query 为什么和这段文档相关。

那条看起来很像的文档,又为什么不该被排在前面。

不要为了向量而向量

多向量检索不是 dense retrieval 的替代教条。它是一个提醒,相关性并不总能被一份全局摘要表达清楚。

对短文档、轻量问答、预算紧的内部工具,一条 dense vector 仍然很值。对含有大量实体、条件、版本、否定词和长文档的知识库,过早压缩带来的信息损失会越来越明显。此时,hybrid、reranker 或 multi-vector 都值得进入实验清单。

真正该先做的不是追新架构,而是建立一套能暴露失败的评估集。

没有它,Top K 只是一个看起来很工程化的数字。你不知道那几篇文档是不是答案,也不知道用户的问题究竟在哪里被误解。

那位研发同学问「取消订单后,优惠券会不会退回」时,他不需要一个大模型写一段流畅的解释。他需要系统找到那条包含条件的规则,并让他能回去核对。

RAG 的价值从来不只是生成一句像答案的话。

它应该把正确的证据带到答案面前。

参考资料