跳到主要内容
检索不是找“像”的,是找“有关系”的

检索不是找“像”的,是找“有关系”的

林默
林默

· 阅读约 6 分钟

我一直在想,为什么用 embedding 去找“相关代码”这件事,在仓库里会这么容易扑空。上周我又干了一回。

给一个老项目的某个函数补逻辑,我把函数代码完整贴给 LLM,又从仓库里用 embedding 检索捞了五个“最相关”片段塞进去。生成的结果语法对,风格对,测试跑不过。

我回头看了一眼那五个片段。每个单独看都跟目标函数有点关系,但没有一个是它真正在调的。它真正依赖的那个工具函数,藏在另一个目录里,长得一点都不像。

先停一下,问个问题:为什么相似度检索在代码仓库这种地方会这么容易扑空?

最表层的答案是,embedding 衡量“像不像”,代码里的“相关”却是执行依赖、数据流、类型约束这些。一个函数依赖另一个函数,不代表它俩长得像;往往被依赖的那个最不像,因为它干的是另一件事。

这个答案对,但只对最外层。再往下呢?为什么 RAG 用在自然语言问答里没这么糟,一搬到代码就露馅?

我一开始也以为是训练数据的锅。后来才慢慢想明白:自然语言里“相似”和“相关”高度重叠。你问“苏州园林有哪些”,检索回来的段落里含“苏州园林”四个字,基本就能用。代码不一样。代码的相关是由执行依赖定义的,而执行依赖的信号在训练语料里很弱——模型见过太多代码,但它读的是代码的文本规律,不是运行时谁调了谁。所以 embedding 抓出来的相似,在仓库级代码生成里,经常和实际需要错开。

说到这该收了,不然又成吐槽 embedding。真正让我对这事有新想法的,是最近看到 arXiv 上一篇讲 DyRetriever 的论文,ASE 2026 接的。它最初的问题跟我那天晚上的困惑是同一个:检索到底该按什么找?

DyRetriever 的思路说穿了挺直觉:别让检索器一次找齐所有“相关”片段,而是像人读代码那样,先找到一个入口,再顺着依赖关系往下摸。摸到一个,验证一下,再摸下一个。最后拼出来的是一张很小的、按需生成的依赖图,用完就扔。

这么说吧,它把检索从“一把梭抓一堆相似的”,改成了“沿着一条调用链,一站一站走”。

这里头藏着一个我很喜欢的设计决定:没有预先建一张全局依赖图。

预建全局图这事我以前真干过。用静态分析把整个仓库的调用关系预先抽出来,存着,查询的时候再爬。最开始几天很爽,图画好了,看起来专业得不行。越往后越难受。仓库一改,图要重建;语言升级、依赖格式一变,规则得重写。最烦的是,很多查询根本用不到图里 99% 的内容,你却得为它付每次构建和维护的成本。

DyRetriever 把这个倒过来了:不预建,不维护,查询的时候靠 LLM 顺着依赖关系现走。每次走的路径很短,走完那张部分图直接丢掉。代价是检索阶段多了 LLM 的推理开销,好处是把“维护全局图”这个长期负担清零了。

论文里给了个数:比静态构建全局图的基线快 7.4 倍。我第一反应是“废话,你不用付构建图的成本”。后来才觉得,这个数字真正说明的不是快,而是那个 trade-off 的尺度——你用查询时多出来的一点推理时间,换掉了整个预先建索引的工程复杂度。值不值,得看仓库多大、变多频。但至少它把这个交换摆到台面上了。

再往下剥一层。这里 LLM 的角色跟传统 RAG 里不一样。传统 RAG 中,LLM 是检索结果的消费者;检索器给什么,它就用什么。DyRetriever 里,LLM 自己是检索器的一部分——它选入口,在每一步判断接下来该看谁,验证候选函数是不是真的在当前这个调用语境里。

这个变化值得停下来想想。以前的检索是“外部系统负责找,LLM 负责用”,边界清楚。现在检索本身变成了一种推理活动。查找不再是查询和排序,而是一连串判断:这个依赖是真依赖还是假依赖?这个候选函数在这条调用链里到底需不需要?

当然,用 LLM 做这种判断也有代价。它可能选错入口,可能在某一跳上被一个长得像依赖、其实不是的函数带偏,可能漏掉真正关键的边。但因为每次走的路径短、图按需生成,错一次的代价比维护一张错误全局图小多了——你错了,丢掉再来;全局图错了,你根本不知道哪里错了。

那天晚上我调不好那段代码,最后自己翻了半天目录,找到那个藏在深处的工具函数,手动粘进 prompt,一次就过了。事后我在想,我做的这件事——从入口出发,顺着调用关系翻文件,验证哪个是我真正要的——不就是 DyRetriever 在干的事吗?只不过我靠 grep 和直觉,它靠 LLM 的语义判断。

最让我在意的是论文里提到的那个说法:灵感来自“人类开发者沿着部分依赖图迭代检查代码”的行为。这话很容易被当成一句漂亮套语,但我觉得它踩中了要紧的东西。检索的默认真题从来不是“把相关的都找出来”,而是“在最短路径上找到恰好够用的那个”。人读代码从来不先看整个仓库的依赖关系,他就顺着一条链走,走到哪算哪,不够再补。

所以我们以前做仓库级 RAG 的思路,可能本来就把问题定大了。我们问的是“怎么从一堆代码里筛出相关的”,可真正的场景是“怎么顺着一条可能的执行链,把需要的那几个点一个个摸出来”。这两个问题长得像,其实完全不同。

再往下是什么?我猜是“怎么知道什么时候该停”。DyRetriever 的多跳推理总得有终止条件,论文里应该有,我没细读透。但“摸到哪里算够”这个问题,人类开发者自己也说不太清。有时候我们就是知道,再看下去也没用了。这个判断目前看起来还是 LLM 的某种隐式能力,没法完全规则化——这层的代价和边界,我还没想明白,先放着。

那天晚上的事,根子不在于我用的检索器太蠢,而在于我把“找相关代码”这件事外包给了一个只懂相似度的系统,然后指望它能蒙对。DyRetriever 让我重新想明白的是:检索代码本质上不是相似度匹配,是导航。导航就得一步步走,每步都要看着路标判断一下,而不是在起点闭着眼撒一张网。

林默
林默

从具体轶事入口,一问一答把默认对的判断剥到设计权衡,主动暴露走过的弯路。

查看主页 →