ZHANG.LOG

2026-02-1410 分钟 阅读

改代码之前先看地图:用 GitNexus 给 Agent 一份代码知识图谱

改一个函数,漏掉五处调用——这是 Agent 最典型的翻车方式,而且不是它笨。它手里的「地图」是 grep 出来的:一串文件名和行号,看不出谁在调谁,也看不出这个改动会流进哪几条执行路径。

GitNexus 补的就是这张地图。它把仓库索引成一张知识图谱,再把调用关系、依赖链、执行流程和变更影响通过 CLI 和 MCP 交给 Agent。我在这个站上用它,目的不是「看得更全」,是让每次改动之前都多看一眼。

它建了什么

索引之后,这个仓库是 313 个符号、505 条关系、9 条执行流。符号是函数、类、组件这类可寻址的东西;关系是调用、导入、继承;执行流是从入口出发能走通的一条完整路径。

索引落在项目根目录的 .gitnexus/ 里,里面有图库、解析缓存和一份 gitnexus.json。它是构建产物,不该提交,也不该指望跨机器复用——换台机器重新索引比搬缓存干净。

建索引,以及看它新不新

Bash
node .gitnexus/run.cjs analyze --index-only --repo .
node .gitnexus/run.cjs status --repo .

status 会告诉你版本和索引到哪个提交。我最近一次看到的输出是 GitNexus 1.6.12、索引到提交 4d3c8c6。

这一步的意义常被低估:索引过期比没有索引更危险。没有索引时 Agent 会去读代码;索引过期时它会理直气壮地引一条已经改过的调用链,而你看不出这句话是三天前的。

改之前:upstream 影响

我最近的例子是给 lib/tag-display.ts 的 getDisplayTag 加几条英文映射。这个函数把中文标签翻成英文,看着无害:

Bash
node .gitnexus/run.cjs impact "getDisplayTag" \
  --direction upstream --repo .

结果是 risk: HIGH、影响 5 个调用点、牵涉 3 条执行流:HomeClient、PracticeDetailPage、BlogDetailPage。

严格说,这次改动只是往查表里加键,getDisplayTag 的逻辑一行没动,真实风险不高。但 HIGH 的价值不在于判定谁对谁错,而在于它逼我先看清那三条流程——首页、实践详情页、博客详情页都会调这个函数,而我只在首页验证过标签。

这就是我把它放在「理解和修改之间」的原因:它不替我决策,它让我在决策前看见牵连。

最容易犯的错:把 UNKNOWN 当安全

有调用者就有风险,这是直觉。反过来不成立:空的调用者集合不等于没人调用。

普通对象的属性访问、动态分派、跨语言调用,图都可能解析不到。工具在这种情况下会给 risk: UNKNOWN,配套一句提示:这代表「没查到」,不是「没问题」。我踩过一次——把 UNKNOWN 当成低风险,改完才发现另有一条链路在用旧签名。

现在我的处理方式很死板:看到 UNKNOWN 就用文本搜索把符号名再搜一遍,搜不到才继续。图负责看结构,文本负责补漏,两者不能互相替代。

改之后:detect-changes

Bash
node .gitnexus/run.cjs detect-changes \
  --scope all --repo .
node .gitnexus/run.cjs detect-changes \
  --scope compare --base-ref main --repo .

--scope all 看整个工作区的改动波及了哪些符号和流程,提交前跑;--scope compare 拿某个分支当基线,适合回归审阅。

这里有个容易误读的结果:partial: true 或 truncated: true 不算通过。它说明这次只检查了一部分,零命中可能意味着「没看见」。遇到这种情况我的做法是重跑,必要时缩小范围,而不是把绿色当成结论。

索引多久刷一次

我的习惯是三个时机:拉完别人的改动、大规模重命名之后、以及准备动一个自己很久没碰的模块之前。

前两个是常识,第三个才是关键。图谱最容易在「你以为自己熟」的地方骗人——几个月前改过的模块,调用方早就换了两茬,而你的记忆还停在上一版。

刷新本身不贵,analyze --index-only 只重建索引、不重跑整条分析流水线;贵的是冷启动那一次,第一次解析全仓库要等几分钟。所以我把索引当成「开工前的一次编译」,而不是「随时可以点一下的按钮」。

两种接法:CLI 和 MCP

CLI 是我兜底用的:手上有终端,一条命令就能查。MCP 是把这些能力直接交给 Agent,让它自己在中途查。

常用的几个 MCP 工具分工很清楚:query 查概念和流程(「这个功能是怎么跑起来的」),context 查某个符号的定义与调用者,impact 查改动波及面,explain 做污点分析,detect_changes 查变更。资源侧还提供仓库级的 context、clusters、processes,以及按流程名取单条的 process/{name}。

分工上我更倾向让 Agent 用 MCP 自查、我用 CLI 复核。同一份图谱,两个入口,结论能对上才敢往下一步走。

它和 grep 怎么分工

我不用它替代 grep。两者回答的是不同问题:

  • 「这个字符串出现在哪」——grep。配置键、文案、错误信息、写死的路径,这类字面量在代码里搜一遍最快。
  • 「改这个函数会波及谁」——图谱。调用关系、执行流程、跨文件依赖,文本搜索给不出结构。

举个我实际碰到的例子:给标签加英文映射,grep getDisplayTag 能立刻列出所有出现位置,但它不告诉你哪几处是在渲染路径上、哪几处只是测试。图谱给的是后者。

反过来说,图也有盲区:字符串拼出来的调用、运行时才确定的 handler,它一样看不到。所以我的默认顺序是「先图后文」——图给结构,文补细节。

它解决不了什么

第一,动态语言里通过字符串拼出来的调用,图看不见;这类问题只能靠测试兜。

第二,它读代码不读部署。配置、环境变量、运行时开关造成的行为差异,它给不出答案。

第三,索引陈旧时它会给你一份自信的错答案,所以 status 要真的看一眼,而不是顺手跑过。

第四,也是最重要的一条:risk 是提示,不是裁决。HIGH 不等于不能改,LOW 也不等于可以闭眼改——它只负责把「你可能没想到的地方」摆到台面上。

我把三条硬规则写进了 AGENTS.md

工具的意义在于不依赖我每次记得用,所以这三条被写进了项目级指令:

  • 改任何函数、类或方法之前,先跑 impact 看 upstream,并报告调用者、流程和风险等级。
  • 提交之前跑 detect-changes --scope all,partial 或 truncated 视为未通过,必须重跑。
  • risk: UNKNOWN 按「未解决」处理,用文本搜索二次确认,不能当低风险放行。

这几条规则听着啰嗦,但它们解决的是一个结构性问题:Agent 的默认行为是「先动手」,而图谱要求它「先看」。我没有能力在每次会话里都记得提醒它,所以只能把提醒写进它每次都会读的文件。 写了这三条之后,最明显的变化是 Agent 的提问变多了:它开始先问「这个符号还有谁在用」,而不是直接给出一版改完的代码。

收尾

地图的价值不在全,在于它让你在动手前多看一眼。看一眼的代价是几十秒,不看一眼的代价通常是一次返工——而返工里最贵的部分,是重新判断哪些东西已经被改坏了。

后来我给这套东西加了一句话:图谱负责告诉你「还有谁」,测试负责告诉你「还对不对」。两件事都做完,才算改完。

GitNexusAgent代码图谱