代码图谱也会认错函数,如何核验依赖
作者核对 graphify 图谱中的调用边,发现多个文件中的同名辅助函数被合并成一个虚假核心节点。文章以此讨论代码图谱的可验证性,以及直接提取与推断关系的可信边界。
AI 深度解读
代码图谱里,连接最多的函数有 124 条边,却不是一个真实存在的函数,而是多个文件、两种语言里的同名对象被合并出来的结果。作者借 graphify 的一份公开样例,讨论了 AI 编程中很容易被忽略的问题:工具给出的调用关系,到底是源码证据,还是工具替你做出的判断?
结论不是放弃图谱,而是给它换个位置:用来缩小搜索范围,再回到源码核实。尤其在重命名、删除函数之前,一条看起来很精确的关系,也可能把 agent 带错方向。
图谱为什么比搜索好用,也更容易让人信错
作者把代码检索分成三类。grep 返回匹配的文本行,不负责解释;向量嵌入(embeddings)返回语义相近的片段,不保证存在调用关系;代码图谱则直接宣称「A 调用了 B」,附上文件、行号和关系标签。
图谱的价值与风险,都来自它替使用者做了判断。
agent 可以据此少读很多文件,但错误关系和正确关系长得一样,都像一条已经整理好的事实。
原文中的 graphify 是 Python CLI,也提供 /graphify skill,支持 Claude Code、Codex、Cursor 等约二十种助手。代码部分使用 tree-sitter 解析,不用 LLM,也不用 embeddings,输出 graph.json,包含函数、类、文件,以及 calls、imports、inherits 等关系。查询入口有 graphify query、explain 和 path。
关系分为直接提取的 EXTRACTED 和经工具解析的 INFERRED。问题在于,从源码提取出调用表达式,不等于已经正确认出它调用的是哪个函数。作者引用的热度是一天新增 1,434 stars、总计约 131,000;这些数字能解释关注度,不能证明关系准确。
184 条异常调用,是怎样查出来的
作者没有安装 graphify,也没运行会注册 skill 的 graphify install,或添加助手 hook 的 graphify claude install。他检查的是仓库自带的公开案例:glitchwerks/rsl-siege-manager,包含 FastAPI 后端、React 前端和 Discord bot。
案例固定在提交 6085fd6,五月生成的图谱包含 1,886 个节点、3,876 条边。作者检出对应代码,用一个 36 行 Node 脚本逐条核对调用边。下面是核心检查,分别寻找跨语言关系、调用者文件不一致,以及引用行没有目标名称的问题:
for (const e of g.links.filter(l => l.relation === 'calls')) {
const src = node[e.source], tgt = node[e.target]
const name = tgt.label.replace(/\(\)$/, '').replace(/^\./, '')
const text = lineOf(e.source_file, e.source_location)
const faults = []
if (family(src.source_file) !== family(tgt.source_file)) faults.push('wrongLanguage')
if (src.source_file !== e.source_file) faults.push('callerFromOtherFile')
if (!new RegExp(`(^|[^\\w])${esc(name)}\\s*[(<]`).test(text)) faults.push('nameNotOnLine')
}
要看清最后一行的边界:它检查的是目标名称是否出现在调用位置,无法证明同名对象绑定正确。
脚本耗时不到半秒,两次运行结果一致:
| 关系标签 | 调用边数 | 全部检查通过 | 跨 Python/TS | 调用者在别处 | 引用行无目标名 |
|---|---|---|---|---|---|
| EXTRACTED | 989 | 853 | 83 | 57 | 84 |
| INFERRED | 153 | 105 | 34 | 33 | 7 |
一条边可能触发多项检查,因此后三列不能直接相加。1,142 条调用边中,958 条通过,184 条至少有一项异常,其中 136 条标着 EXTRACTED。
这不是完整准确率评测。绑定到同语言、同名字的错误函数,仍可能通过检查,因此 184 只是这套检查发现的错误下限。
真正出错的地方,是把名字相似当成同一个对象
184 条异常边里,有 145 条涉及被多个文件共同声称包含的节点。最直观的例子是 TypeScript 的 markChangelogSeen():它只是发送 POST 的两行函数,却接上了 Python 接口 mark_changelog_seen 的内部调用,包括 L89 的 _require_member_session() 和 L95 的 ChangelogStatusResponse。
名字被合并,调用关系也跟着搬家。另有 22 次 Python 内置 list() 调用,竟然指向 TypeScript 测试中的 const list = screen.getByTestId("feature-list")。
受影响的不只是个别节点。被分析文件中的 769 个顶层 Python 函数,有 200 个在自身文件下找不到对应节点;其中 164 个,与其他地方的节点名称相同或仅大小写、下划线不同。共有 121 个节点被多个文件共同包含。
报告前十个高连接节点里,六个是跨文件合并的辅助函数。前三个是 _make_siege() 的 124 条边、_make_member() 的 92 条边、_make_position() 的 85 条边。
案例分析曾把它解释成「测试工厂被大量测试复用」。但作者用下面的命令确认,仓库里存在十个定义该函数的文件:
$ grep -rln "def _make_siege(" . | wc -l
10
关键是 -l 按文件输出匹配结果。这十个 Python helper,加上两个 TypeScript makeSiege helper 和三行实际定义其他对象的代码,被折叠成了榜首节点。报告随后把合并错误解释成架构特征。
我的判断
这个案例最有说服力的地方,是能沿着边上的引用位置回查源码。但它只能证明那份旧图谱有问题,不能据此判定当前版本仍有相同表现。
原文也交代了修复:6 月 9 日的 issue #1205 报告,一次 --update 让约 6,000 节点的图丢掉约 1,300 个节点,次日修复。代码节点随后只按 ID 去重,不再按相似标签合并;0.9.0 把完整路径加入节点 ID;跨语言的 INFERRED 调用在构建时丢弃。原文所称当前版本是 0.9.93,作者读过修复源码,但没有运行它重建案例,文档仍指向五月的旧输出。
历史问题还包括:短名称 log、find 聚集数百条伪调用的 #543,npm 导入被连到无关 Python 文件且标成 EXTRACTED 的 #1638,以及仅靠三条虚假边连接 2,409 个 Python 节点与 1,403 个 TypeScript 节点的 #1749。它们各自得到修复,但也说明「解析出名字」和「解析对目标」之间仍有距离。
我赞同把图谱用于导航,把源码用于确认。grep 的噪声需要人读,图谱漏掉的调用者却可能完全不露面。项目公布的关键事实覆盖率是 82.0%,grep 加阅读的基线是 70.8%,但只测了六个问题;这不足以推导出复杂仓库里的普遍优势,也没有证明改代码时更安全。
能拿来做什么
- 在 React、Node 或混合语言仓库里重命名、删除函数前,先用图谱列候选调用者,再用 grep 补查同名引用,并打开实际调用位置确认,尤其留意不同文件里的同名 helper。
- 给 agent 增加明确要求:任何会影响代码修改的调用边,都要回读引用行并核实目标身份。当前
graphify explain会展示调用位置,可以把核验接在查询之后。 - 检查安装后的默认指令。原文提到,常驻 hook 要求 agent 在 grep 前「MUST run graphify query」,同时要求按图谱定位后读取原始文件;后一项是必要步骤。
- 评估图谱工具时,选取仓库里的同名测试工厂、短函数名和跨语言接口做抽查,同时记录生成版本与提交号,避免把旧样例或高连接排名直接当成架构结论。