RAG 召回不准,别急着换模型,可能是你把文档压扁了
一篇平均 941 token 的医疗文档,进了很多检索系统后,只剩开头 256 或 512 个 token。
后面的内容不是召回得不够好。
它压根没参加检索。
这件事听着有点荒唐,但它正好解释了不少 RAG 项目里那种很熟悉的场面。业务同学拿着一条明明在知识库里的答案来问,系统却找回三段看着沾边、实际没用的材料。工程师开始调 chunk size、换 embedding、加 reranker、改 prompt,忙了一圈,效果偶尔好一点,下一批问题又回去了。
我有时候觉得,RAG 最容易被误诊成模型能力问题。很多时候,问题发生得更早,发生在你决定用一个向量代表整份文档的那一刻。
Hugging Face 在 8 月 26 日发布的 Sentence Transformers v6.0,把 MultiVectorEncoder 放进了正式训练链路。它提供的是 ColBERT 风格的 late interaction 检索。名字不新,思路也不是刚出现,但这次值得开发者认真看一眼。原因不在于又多了一个可选的 embedding 模型,而在于它把一个经常被忽略的取舍摆到了台面上。
单向量检索很省事,但它会丢失局部信息。多向量检索保住了局部信息,但它要你为索引、延迟和工程复杂度付费。
这不是一次库升级那么简单。
一条召回链路,往往在压缩时就决定了上限
先把 RAG 的典型路径摊开。用户问一个问题,系统把问题编码成向量,从向量库取 Top K 文档或 chunk,再把这些文本交给 LLM 生成答案。团队通常把注意力放在最后两步,觉得检索只要余弦相似度够高就行。
但 dense embedding 做了一件很激进的事。它把整段文本压成一个固定长度的向量。查询也是一个向量。两者做一次点积,就给出相关性分数。
这种设计非常优雅。存储好算,ANN 索引成熟,接口也简单。对短 FAQ、标题检索、语义相近的内容聚类,它确实够用。不是说 dense retrieval 不行,而是说它有一个无法绕开的动作,模型必须把一段文本里所有信号揉成一份摘要。
你想想看,一份 Spring Boot 故障复盘里可能同时有异常栈、Redis key、版本号、时间窗口、根因推断和最终修复。用户问「WebFlux 下连接池耗尽时的超时配置」,真正决定相关性的,也许只是文档后半段某个类名和一个参数。可一个全局向量要同时表达整篇文章的主题、语气、背景和结论,它很容易把这点细粒度信号平均掉。
这不是模型犯傻。
这是压缩的代价。
Sentence Transformers 的文章给了一个很具体的提醒。很多公开检索模型是在短文档任务上训练的,经典 ColBERT checkpoint 常把文档截到 180 或 300 token,常见 dense 模型也常停在 256 或 512 token。作者在医疗检索评测 MIRIAD 上观察到,文档平均长度达到 941 token 时,截断本身最高会带来 0.24 的 NDCG@10 损失。
这个数字不该被拿去套到任何业务系统上。它来自一个特定领域和评测集,不是通用承诺。可它足够说明一件事,模型架构之间那点微小差别,可能还不如「你有没有让文档内容真的进入比较过程」重要。
回到 RAG 这块,先别把问题归咎于向量模型。检查一下你传入 encoder 的最大长度,再看看真实文档的长度分布。这个动作很朴素,却经常比盲目换榜单第一的 embedding 更值钱。
MultiVectorEncoder 做的事,是不替你过早下结论
多向量模型不把一篇文档浓缩成一个向量。它保留 token 级别的一组小向量。查询也会被编码成一组向量。打分时,查询里的每一个 token,都去文档里找和自己最匹配的 token,再把这些最优匹配累加起来。
这套计算通常叫 MaxSim。
如果把 dense retrieval 想成「给整篇文章写一句摘要,再比较两句摘要像不像」,那么 late interaction 更像「把问题里的关键字逐个拿出来,在候选文档里找最能对上的局部证据」。它不会只因为两段文字都在聊 Java 就给高分,也更有机会捕捉到「连接池」「超时」「Reactor Netty」这类共现但并不总是高频的细节。
这里有个容易被误解的地方。多向量不是 lexical search 的回归。它匹配的仍然是语义向量,不是纯字符串相等。一个查询 token 可以找到语义相近的文档 token。它保留的是局部匹配的自由度,而不是退回关键词检索。
为什么这个自由度在企业知识库里很重要?因为内部文档的相关性经常不是主题相关,而是条件相关。
比如「订单补偿怎么做」和「订单补偿失败后如何回滚」主题很近,但后者需要的是失败路径与回滚边界。再比如「LangGraph memory」和「跨线程恢复时 checkpoint 如何隔离」也不能只靠主题相似来判断。Agent 系统里,工具名、状态字段、异常码、权限条件和版本号都可能是答案的门槛。
单向量会努力把这些东西都装进一个语义摘要里。MultiVectorEncoder 的选择是,别急着合并,让查询里的每个局部要求分别说话。
真正应该警惕的,不是慢,是索引会膨胀
听到 token 级向量,很多人的第一反应是「那一定很贵」。这个判断是合理的。
dense embedding 为每个 chunk 放一个向量。多向量模型会为一段文本保留多个 token 向量。文档一长,索引空间、构建时间、候选重排开销都会上来。它并不是给已有向量库换个类名就能上线的优化。
这也是我不太喜欢把多向量检索写成「RAG 的终极解法」的原因。短文本知识库、问答库、对实时性要求极端苛刻的服务,dense retrieval 仍然是很好的默认值。它便宜、稳定、可解释,运维成本也低。没有必要因为一个技术趋势,就把所有检索链路推倒重来。
不过,反过来想,很多团队已经在 dense Top 50 后面接 CrossEncoder reranker。那条链路本身就承认了一个事实,单向量的粗排不够精细,需要第二个更贵的阶段补救。
late interaction 所在的位置很有意思。它比单向量保留更多信息,又不像完整 CrossEncoder 那样把每一对 query 和文档拼起来跑一遍 Transformer。它把一部分计算延后到检索阶段,所以才叫 late interaction。
这是一条新的中间带。
别只问「它比 dense 好多少」。更应该问「在我的语料和延迟预算里,它能不能替换掉一部分昂贵的 rerank」。如果答案是能,索引变大未必是坏账。它可能是在把计算从线上生成阶段挪到更可控的检索阶段。
文档长度,比模型名称更像一个架构参数
Hugging Face 的案例里有个细节很扎眼。作者没有只拿一个模型微调,而是把长度配置当作训练的一部分。mLateOn 系列可使用底座的完整 8192 token 上下文。对那些默认只读 180 到 512 token 的 checkpoint,文章建议解除 query 和 document 的任务级长度限制,让它回退到 tokenizer 的 model_max_length。
这事听着像一个配置项,其实是架构决定。
你如果把长文先切成小 chunk,的确能绕开 encoder 长度限制。多数 RAG 都这么做。问题是切得太碎,前后依赖断了。切得太大,又撞上模型截断。于是 chunk overlap 越堆越高,重复内容越来越多,召回集合也越来越难治理。
很多朋友可能不知道,chunk 策略不是一个静态参数。它应该跟文档长度、问题类型、索引模型一块定。
对产品说明、规范文档、合同条款、代码仓库这类长文本,我更倾向于把它看成两层问题。第一层用结构切分保住章节语义,比如按函数、标题、表格和段落边界切。第二层再判断检索器有没有能力读取足够长的上下文。只做第一层,常常是把「截断」藏起来,不是解决它。
这话听着有点刺耳但,很多 RAG 指标的改善,来自换了一个更符合评测集长度分布的方案,不一定来自某个模型更聪明。没有真实 query 集和真实文档长度分布,任何 benchmark 都只是参考。
微调的价值,不是把行业名塞进模型
官方文章里把领域微调讲得很克制。医疗、法律、金融、代码检索的词汇、提问方式、相关性标准并不一样。multi-vector 的 token 级匹配会放大这些细粒度信号,因此它对少量领域内数据也可能有不错反应。
但这里也最容易走偏。
很多团队一听到领域微调,就准备攒几百条问答对开训。结果训练数据全是显而易见的正例,负例又太随机,模型学到的是「领域词出现就相关」。上线后,遇到两个都带同一个业务词、但边界条件不同的文档,还是会错。
更有用的数据不是漂亮的问答,而是会让系统犯错的近邻。比如同一个接口的成功流程和失败流程,同一业务实体的创建与撤销,同一套 Agent 的开发环境与生产环境权限。它们字面相似,语义也接近,可对用户而言答案完全不能混用。
这才是检索训练真正该花时间的地方。
Sentence Transformers v6.0 提供的训练链路包含模型、数据集、loss、训练参数、evaluator 与 trainer。它降低了把 ColBERT 风格模型纳入工程栈的门槛,但不会替你生产相关性定义。数据里如果没有「什么看着像但其实不该召回」,模型就学不到边界。
文章中的 mLateOn-medical 在一张 RTX 3090 上训练 14.5 小时,并在作者构建的医疗评测中超过了其测试到的通用 dense、sparse、lexical 和 multi-vector 检索器。这个结果很鼓舞人,但也必须保留它的范围。它是作者的领域数据、作者的评测与作者的训练设置,不是「一张消费卡训练 14.5 小时就能赢所有企业检索」的结论。
说实话我也不确定每个中文企业语料能从 multi-vector 获得同样的收益。中文分词、代码混排、表格、扫描 PDF、权限过滤,都会让结果变得复杂。可这正是该做小规模实验的理由,而不是继续拿通用榜单猜答案。
一个很小的优化,也暴露了工程味道
文章还做了一个四组消融,比较不跳过 token、跳过标点、跳过停用词、两者都跳过。作者最终采用标点 skiplist。在他的数据上,这个配置质量略胜,并且让文档索引缩小了 9.6%。
这类细节很讨人喜欢。它没有神奇到让人兴奋,但是真正上线后会遇到的事。
索引不是模型的附属品。你决定存哪些 token,决定了空间、吞吐和噪声。对中文语料,标点是否应该跳过也不能照搬。中文标点有时是章节边界、枚举结构甚至表格替代符号。代码片段中的符号更不能随便删。作者给的是一个值得验证的方向,不是一张可以直接复制的处方。
我自己的感受是,RAG 工程越来越像搜索工程。模型当然重要,可语料清洗、字段设计、长度控制、负例构造、索引压缩和离线评测同样决定结果。只盯着生成模型,反而容易把最影响答案质量的那半截链路放过去。
给正在做 Agent 知识库的团队,一个务实的试法
不需要一上来就全量迁移。可以从一组最让人头疼的问题开始。
挑 50 到 100 条真实失败 query,最好来自工单、开发者反馈或人工追问。为每条 query 标注至少一个真正支持答案的段落,同时标出几个「看上去相关但不能回答」的近邻文档。再把当前 dense 方案的 Top K、命中位置、文档截断比例和最终答案可用性记下来。
然后做两个对照。一个是保持现有 chunk 与索引,只把候选重排换成 late interaction。另一个是允许更长文档参与编码,观察 NDCG@10、Recall@K、P95 延迟、索引体积与单位查询成本怎么变化。别只看一个平均分,也要看那些权限、版本、异常路径相关的难题有没有改善。
如果 MultiVectorEncoder 只是把离线分数抬高,线上 P95 却无法接受,那就承认它不适合这条链路。这个结论并不丢人。反正我觉得,一个清楚的边界比一段漂亮的模型宣传更有价值。
如果它让失败 query 的证据命中显著改善,再去考虑把它放在 dense 召回后的二阶段,或者针对长文档、代码和规范库单独启用。检索架构没有必要一刀切。把不同工具放在不同的错误模式上,才是工程上的成熟。
别再只给文档写摘要了
一开始那篇 941 token 的医疗文档,真正提醒我的不是某个 benchmark 的冠军是谁。
它提醒我,系统在生成答案之前,已经替用户做过一次信息删减。删得太早,后面再强的 Agent、再长的 prompt、再贵的 reranker,都只能在残缺的候选里找答案。
MultiVectorEncoder 不会替 RAG 消灭所有问题。它会带来更大的索引,也会让部署和评测更麻烦。可它逼着我们重新面对一个问题,用户问的是一个带条件的具体问题时,我们为什么要这么早把文档压成一句模糊的摘要。
该保留的细节,别急着替它们做结论。
参考资料
- Hugging Face, Training and Finetuning Multi-Vector Embedding Models with Sentence Transformers, 2026-08-26
- Sentence Transformers, Multi-Vector Encoder 文档




