多模态融合搜索:为每一次查询挑选正确的检索器
> 像“parseConfig 是在哪里定义的”这样的查询,和“身份验证是怎么工作的”所需要的搜索方式截然不同。Maguyva 会对查询意图进行分类,据此为四种检索模态分配权重,再用加权的 Reciprocal Rank Fusion(倒数排序融合)把结果融合起来。
搜索查询从来都不是单一的一种东西。
“parseConfig 在哪里定义”想要的是一个精确的符号——一个准确的位置,越快越好。“身份验证是如何工作的”想要的是含义——一整片能解释某个概念的相关代码。“如果我改动这个函数会破坏什么”想要的是依赖关系图。“查找字符串 ECONNREFUSED“想要的是字面匹配,不需要任何巧思。
Grep 非常擅长字面匹配,对某些引用查找也很有用,但它不是依赖关系图,也不理解含义。嵌入向量覆盖了语义这一侧,但对精确字符串匹配和影响分析来说,它是错误的工具。大多数代码搜索工具只挑一种引擎,然后让每一个查询都活在这个选择里。Maguyva 不做这种取舍。它会先判断你问的是哪一类问题,再按这个问题应得的比例混合四种检索器。
四种模态
底层有四种独立的代码查找方式:
- 语义(semantic)——基于 Voyage 二进制嵌入的向量搜索;按含义查找代码。
- 文本(text)——trigram 匹配;查找字面量、错误字符串、精确标识符。
- 结构(structural)——AST 查询;查找定义、签名和语言构造。
- 图(graph)——依赖关系图;查找调用者、被调用者和影响范围(blast radius)。
每一种模态都擅长应对不同类型的问题。诀窍在于针对眼前这个查询,决定该在多大程度上信任每一种模态。
意图分类
在任何检索开始之前,一个轻量级分类器会先把查询归入六种意图之一,并给出一个置信度分数。它被刻意做得很“便宜”——按顺序排列的启发式规则,第一个命中就采用——因为它运行在热路径上,只会额外增加一两毫秒:
- 以
def、class、func、import开头…… → find_definition(置信度 0.95) - “who calls”、“usages of”、“references to” → find_references(0.90)
- “impact”、“blast radius”、“what depends on” → impact_analysis(0.90)
- 带引号的
"string"或像traceback这样的错误标记 → exact_match(0.85–0.90) CamelCase或snake_case这样的标识符 → find_definition(0.60–0.80)- “how”、“why”、“explain”、“architecture” → understand_code(0.75)
- 什么都不匹配 → understand_code,低置信度(0.40)
每种意图在四种模态上都带有一套权重配置。以下是实际数值:
| 意图 | semantic | text | structural (AST) | graph |
|---|---|---|---|---|
| find_definition | 0.2 | 0.1 | 0.6 | 0.1 |
| find_references | 0.1 | 0.2 | 0.2 | 0.5 |
| understand_code | 0.5 | 0.2 | 0.2 | 0.1 |
| find_similar | 0.4 | 0.3 | 0.2 | 0.1 |
| impact_analysis | 0.1 | 0.1 | 0.1 | 0.7 |
| exact_match | 0.0 | 0.9 | 0.1 | 0.0 |
所以“parseConfig 在哪里定义“会强烈偏向 AST(0.6)。“身份验证是如何工作的”会偏向语义向量(0.5)。“这个改动会影响到什么”几乎完全落在图模态上(0.7)。“查找 ECONNREFUSED“几乎全靠 trigram(0.9),嵌入模型完全被关闭——因为对精确字符串来说,语义相似度恰恰是错误的工具。
快速路径与融合路径
当分类器足够自信时——分数 ≥ 0.85——并且查询是一个常规查询,Maguyva 会完全跳过融合,直接路由到那个唯一占主导地位的模态。“X 在哪里定义”不需要四个检索器,它现在就需要 AST 索引。这条直接路径会被回报为 fusion_strategy: "direct"。
一切模糊不清的查询都要经过融合。四种(或在默认预设下是三种)模态并行运行,各自返回自己的排序列表,然后我们把它们合并起来。
加权倒数排名融合(Weighted Reciprocal Rank Fusion)
融合异构检索器比听起来更难:0.82 的余弦相似度、137 的 trigram 分数和 0.004 的图中心度并不在同一个量纲上,所以不能直接相加。倒数排名融合(Reciprocal Rank Fusion)绕开了这个问题:它丢弃原始分数,只保留每个引擎给出的排名。一个结果来自某一模态的贡献值是:
contribution = weight × 1 / (k + rank + 1)
其中 rank 是它在该模态列表中的位置,k 是一个平滑常数。对于被不止一个引擎找到的结果,各模态的贡献会被累加——检索器之间的一致意见自然会浮到最上面。默认预设下我们使用 k = 40,在 thorough 上使用 60(quick 只运行语义检索,所以那里从不会触发融合)。RRF 原始论文对通用检索场景给出的是 k = 60;我们的默认值略微更“锐利”一些,这让排名靠前、被多个模态一致认可的结果获得更多一点的权重——我们不建议手动去调它。
除此之外,结果还会带有一个图重要性加成。一个枢纽节点——整个代码库都依赖的一个函数——即便文本相关性相同,也应该排在一个不起眼的叶子节点前面,所以我们会把每个贡献值乘以:
boost = min(1 + 0.3 × ln(1 + centrality), 1.5)
中心度来自流水线预先计算好的 PageRank / 度数指标,加成被限制在 1.5 倍以内,这样一个热门函数就不会把一个更相关但不起眼的函数完全埋没。最后,我们会降权来自 vendor、build 和 archive 路径的结果,并按文件对最佳片段去重。
仍不完美之处
意图分类器是一堆正则表达式,而不是一个学习出来的模型。它很好地覆盖了查询的常见形态——引入它的那项决策记录显示,零结果率从大约 15% 降到了不到 5%——但它终究是启发式的,一个真正模糊的查询会落入 understand_code,得到一个偏语义的混合结果。这是一个安全的默认选择,而不是一个聪明的选择。我们没有把它换成一个训练出来的分类器,因为这个便宜版本足够快、也足够好,而一个错得很自信的分类器,比一个诚实的兜底方案更糟。这些权重本身也是手工挑选的先验值,而不是从我们并未收集的点击数据中学来的。
问问题,而不是问该用哪个工具
智能体不应该需要知道该用 grep 还是嵌入向量还是调用图——它应该用大白话问出自己的问题,然后得到正确的答案。多模态融合正是让 find_symbol、语义搜索和依赖分析能够共处于同一个查询入口背后的原因:系统会读出问题的形状,并悄悄组装出适合它的检索器。给嵌入打分的模型固然重要,但知道什么时候不该用它同样重要。为每个查询挑选正确的工具,本身就是一种质量,而这是我们宁愿自己承担、也不愿推给调用方的事情。
// you bring the question. it brings the tools.
相关阅读
更多来自 Maguyva 开发日志的内容
我们为什么把代码搜索升级到了 voyage-4-large_
我们把代码 Embedding 迁移到了 voyage-4-large——目前公开的 RTEB 代码检索排行榜上排名第一的模型。诚实的版本是这样的:我们做出的取舍、我们实际索引的内容,以及我们为什么愿意为高端 Embedding 付费。
语言递归自我提升:在约 280 种语言上打磨代码智能_
我们为约 280 种语言提供代码智能支持,没有任何人能靠人工逐一审核这个规模。于是我们搭建了一套语言递归自我提升循环——抽查、LLM 担任评审、修一处、重新验证——并用一支相互隔离的智能体舰队运行它,直到提取结果真正正确,而不只是“绿灯通过”。
智能体可观测性:Hooks、Alloy 与 Grafana_
我们用 OpenTelemetry 和 Alloy,把 Claude Code 与 Codex 接入同一套 Grafana 技术栈,再借助 Trace 和日志从源头定位并修复智能体的行为问题。