代码检索如何补上调用关系这块拼图
向量检索能找到语义相近的代码,却可能遗漏调用链上真正相关的函数。文章用开源 Graphify 的实际输出展示依赖路径,并区分源码证据与推断关系。
AI 深度解读
向量搜索能找到「像是在处理 token 过期」的代码,却未必能说明异常在哪里抛出、经过哪些调用、最终被谁捕获。Graphify 团队的主张是:编程 Agent 还需要代码关系图,并且每条关系都要标明来源,让 Agent 知道哪些有源码直接支持,哪些需要回头核对。
原文拿一个可以逐行检查的小仓库演示了这件事。它最有价值的部分,是把「找到相关代码」推进到「查清修改会碰到什么」,同时暴露出一个容易被忽略的问题:图里的最短路径,也可能只是推断出的关联。
找到相似代码,为什么还不足以动手改
向量检索擅长按语义找入口,但程序的执行关系不按语义相似度排列。
原文引用的 WonderLab 案例里,查询「process payment」,向量 top-5 返回了 get_payment_history 和 verify_webhook_signature。名字都与支付有关,真正需要的 calculate_order_total 却要从 process_checkout 沿一条 CALLS 关系找到。它在文字表达上离问题较远,在调用结构上却很近。
Tian Pan 的例子更直接:processPayment → validateCard → checkFraudScore → fetchUserHistory。检索命中入口,问题可能藏在三跳之后。InkyLab 则指出,训练和推理的预处理代码看起来几乎一样,只靠相似度容易认错,需要解析调用关系来区分。
这些案例共同指出:检索命中只是调查的起点,不能直接当作修改依据。
原文还提到,Aider 的 repo map 用 tree-sitter 提取定义和引用,再根据依赖图排序,把内容装进 token 预算;RepoGraph 构建行级定义与引用图,围绕关键词提取局部关系图,接入四个 SWE-bench 系统后,报告平均相对提升 32.8%。这个数字属于 RepoGraph 的实验,不能算成 Graphify 的效果。
一次 token 过期查询,图里究竟返回了什么
作者选择了 pallets/itsdangerous,固定在 commit 672971d。这是 Flask 用来签名 cookie 和 token 的小型库,规模足够小,便于人工核对。
作者运行 graphify update src,仅使用解析器,没有调用 LLM,得到 152 个节点、346 条边和 7 个社区,也就是图中聚集的关系群组。
随后执行 graphify query "where does an expired timestamp raise SignatureExpired"。输出指向 src/graphify-out/graph.json,以 BFS 广度优先遍历、depth=2 展开,起点包括 SignatureExpired、TimestampSigner 和描述文本 Raised if a signature does not match.,共找到 64 个节点。
下面这些边把异常的定义、抛出位置和关联类放到了一起:
EDGE TimestampSigner --method [EXTRACTED]--> .unsign() at=src/itsdangerous/timed.py:L72
EDGE .unsign() --calls [EXTRACTED context=call]--> SignatureExpired at=src/itsdangerous/timed.py:L142
EDGE SignatureExpired --inherits [EXTRACTED]--> BadTimeSignature at=src/itsdangerous/exc.py:L60
EDGE TimestampSigner --inherits [EXTRACTED]--> Signer at=src/itsdangerous/timed.py:L22
EDGE TimestampSigner --uses [INFERRED]--> SignatureExpired at=src/itsdangerous/timed.py:L142
EDGE TimedSerializer --uses [INFERRED]--> SignatureExpired at=src/itsdangerous/timed.py:L213
要留意最后一行:uses [INFERRED] 只表示解析器推导出的使用关系,不能直接读成「这里抛出了异常」。
源码核对给出了差别。timed.py:L142 在 if age > max_age: 后执行 raise SignatureExpired(,这里才是过期异常的抛出点。exc.py:L60 则定义了继承关系;原文指出,捕获 BadTimeSignature 或 BadSignature 的处理分支也会接住过期异常。
而 timed.py:L213 做的是 except SignatureExpired: 后再次 raise。它已经成功解签,只是发现签名过期,因此明确停止尝试下一个 signer。
同一个异常名,在不同位置承担的职责完全不同。查到它出现在哪里,还得继续查它在那里做什么。
最短路径为什么仍然需要读源码
作者又运行了 graphify path "TimedSerializer" "SignatureExpired",结果是 1 跳:TimedSerializer --uses [INFERRED]--> SignatureExpired。
这条路径很短,却不是一条完整的运行时调用链。Graphify 的解析器因为类体引用了该异常而建立关联,实际对应的是捕获后重抛。
这也是原文强调来源标记(provenance)的原因。EXTRACTED 表示直接从源码提取,INFERRED 表示由 Graphify 的关系解析逻辑推导。这里的「推断」不等于 LLM 猜测,前面的建图过程没有调用 LLM。
关系图的价值,既在于补充关联,也在于让每条关联可以被核查。
例如任务是「让过期 token 返回 401,而不是 500」,这些信息能帮助 Agent 找到抛出点、异常继承关系,以及刻意重抛异常的位置。但原文没有展示 HTTP 层的处理代码,也没有完成这个修改。仅凭这张库内部关系图,还不能决定响应映射应该改在哪里。
我的判断
我认可「语义检索找入口,结构关系补上下文」这个方向。它直接对应日常 AI 编程里的一类失败:Agent 找到了一段相关代码,就开始修改,却漏掉调用方或统一异常处理。
Graphify 比单纯返回代码片段多提供了文件、行号和关系类别,也比没有来源说明的关系图更便于审查。不过,EXTRACTED 说明关系提取有源码依据,不代表 Agent 已经理解完整行为;INFERRED 提醒人去验证,也不代表这条边一定不可靠。
证据的边界同样清楚。原文展示的是一个小型 Python 仓库中的单条问题追踪,没有给出 Graphify 与向量搜索、全文搜索或语言服务器的对照数据。它证明了这条路径可以展示和核查,没有证明它能普遍提高修复成功率。
作者也承认,下一步才是在更大仓库、包含数百条推断边的情况下,测量这些边经源码核对后成立的比例。因此,「对复杂团队仓库同样有效」目前仍是待验证的判断。
对异常名已经明确的任务,全文搜索加引用跳转也可能足够。图的优势更可能出现在需要反复跨文件追踪、多次检索才能拼齐上下文的场景。是否值得引入,应该用自己仓库里的真实问题检验。
能拿来做什么
-
让 Agent 修异常处理前,先列出抛出点、继承关系、捕获点和重抛点,并附文件及行号。即使暂时不用 Graphify,这份证据要求也能直接放进日常提示词。
-
在熟悉的仓库里复现一次查询,先检查路径是否正确。原文提供的安装与使用命令如下,包名是
graphifyy:
uv tool install graphifyy
graphify install # sets up the skill for Claude Code; use --platform for other assistants
graphify update . # build the code graph
graphify query "where does an expired token get rejected?"
graphify path "TimedSerializer" "SignatureExpired"
要留意 graphify install:默认配置 Claude Code,其他助手使用 --platform;原文还说明,Codex 使用 $graphify,对应的另一种调用形式是 /graphify。
- 如果已有 MCP 客户端,可以评估
query_graph、shortest_path和get_node。把推断边对应的源码核对加入 Agent 流程,并挑选已知答案的问题检查它是否漏掉关键路径;原文没有提供 Node 或前端项目的实测结果,这部分需要自行验证。