code-review-graph 深度解剖:把代码库压成一张图,让 AI 审查少读 82 倍 token 的工程真相
一、背景:AI 编码工具的"token 焦虑症"
如果你重度使用 Claude Code、Codex、Cursor 这类 AI 编码工具,大概率经历过这样的场景:让 AI 审查一个只改了 30 行的 PR,它却先把半个仓库读了一遍——read src/auth/login.py、read src/auth/session.py、read tests/test_auth.py……上下文窗口哗哗地烧,账单蹭蹭地涨,而这些文件里 90% 的内容和这次变更毫无关系。
这不是 AI 笨,而是它没有地图。人类工程师审查代码时,脑子里有一张隐式的依赖图:"改了 login(),那调用它的 SessionMiddleware 和 test_login_flow 得看一眼,其他不用管。"AI 没有这张图,只能靠 grep + 逐文件阅读来重建上下文,每次会话还得从零再来一遍。
这个问题在大仓库里被急剧放大。一个 2000 文件的 monorepo,全量源码可能是几十万甚至上百万 token。就算 agent 学会了先 grep 再精读,一次审查任务读掉几万 token 上下文也是家常便饭。token 是钱,更是上下文窗口里的稀缺资源——塞进去的无关代码越多,模型对真正关键的变更就越"分心"。
2026 年 7 月冲上 GitHub Trending 的 code-review-graph(下文简称 CRG)就是冲着这个痛点来的。它的口号很直接:"Stop burning tokens. Start reviewing smarter."(别再烧 token 了,聪明点审查。)核心思路一句话:用 Tree-sitter 把整个代码库解析成一张结构化知识图谱,存进本地 SQLite,再通过 MCP 协议在审查时按需吐出"最小必读文件集"。
官方给出的数字很唬人:6 个真实开源仓库上,单问题 token 消耗中位数下降约 82 倍,最好情况(fastapi)528 倍。这数字有没有水分?架构上有什么值得借鉴的设计?这篇文章从第一性原理拆一遍——包括它自己在文档里坦白承认的那些"不太好看"的数据。
二、核心概念:代码知识图谱到底是什么
2.1 从文本到图:换一种数据结构看代码
传统上 AI 理解代码的方式是"文本视角":代码是一堆字符串文件,理解靠读。CRG 的核心转换是把代码变成"图视角":
- 节点(Node):函数、类、方法、导入语句、文件
- 边(Edge):调用关系(A calls B)、继承关系(C extends D)、导入依赖(E imports F)、测试覆盖(test_x covers x)
这个转换的价值在于:图上的很多问题有确定性算法解,不需要 LLM 猜。"改了 login() 会影响谁"在文本视角下是一个需要通读代码的理解题,在图视角下就是一次从 login 节点出发的反向可达性遍历——毫秒级、确定性、零 token。
2.2 Tree-sitter:为什么是它
CRG 选择 Tree-sitter 做解析器,这个选型值得说道。Tree-sitter 是 GitHub 出品的增量解析框架(GitHub 网页端的语法高亮、Neovim 的语法解析都在用),它有三个特性正中 CRG 的需求:
- 错误容忍:代码写一半、有语法错误也能解析出局部正确的 AST。真实仓库里永远有暂时编译不过的代码,编译器级前端(比如直接用
go/ast或 clang)在这种场景下太脆。 - 增量解析:文件小改动后不需要全量重新解析,这是 CRG "2 秒内增量更新"的基础。
- 多语言统一:一套 API 接 40+ 语言的 grammar。CRG 目前支持 Python、JS/TS/TSX、Go、Rust、Java、C/C++、C#、Ruby、Kotlin、Swift、PHP、Scala、Solidity、Dart、Elixir、Zig、Julia、SQL、Terraform、Vue/Svelte SFC,甚至 Jupyter/Databricks notebook(
.ipynb)和 Perl XS。
对比一下备选方案就知道这个选型多务实:LSP(Language Server Protocol)语义信息最准,但要为每个语言起一个 language server,重且慢;正则/启发式解析快但漏洞百出;编译器前端准但只能伺候单一语言。Tree-sitter 是"80 分准确率、5 分成本"的甜点位。
代价是什么?Tree-sitter 只做语法级解析,不做类型推导和符号解析。也就是说,obj.process() 这种调用,CRG 只知道"这里调用了一个叫 process 的方法",不能像编译器那样精确知道是哪个类的 process。CRG 的应对是保守匹配——宁可多连几条边(假阳性),不漏掉真实依赖(假阴性)。这个取舍贯穿了整个项目的设计哲学,后面 benchmark 部分会再回到这一点。
2.3 SQLite:本地优先的图存储
图数据库选型上,CRG 没有用 Neo4j 这类专业图数据库,而是直接用 SQLite 存节点表和边表。这个选择同样值得玩味:
- 零部署:
pip install完就能跑,不需要起任何服务。目标用户是开发者个人机器,让人装 Docker 跑 Neo4j 是自寻死路。 - 够快:官方 benchmark 里,fastapi 仓库 6285 个节点、27117 条边,搜索延迟 1.5ms。代码图谱的规模(万级节点)对 SQLite 来说是玩具级负载,递归 CTE 做两三跳遍历绰绰有余。
- 本地优先(local-first):图谱数据完全不出本机。对企业用户来说,"源码结构不上传任何第三方服务"是能不能过安全审查的生死线。CRG 的 GitHub Action 模式也坚持了这一点——图在你自己的 CI runner 上构建和查询。
这里有个通用的工程判断值得记下来:**当图的规模在百万节点以下、查询模式以少数几跳遍历为主时,关系型数据库 + 递归查询几乎总是比引入专业图数据库更划算。**图数据库的价值区间在十亿级边、复杂图算法(PageRank、社区发现)常态化运行的场景。
三、架构分析:四级流水线与三个关键机制
3.1 总体流水线
CRG 的数据流是一条清晰的四级流水线:
代码仓库
│ Tree-sitter 解析
▼
AST(抽象语法树)
│ 抽取节点与边
▼
SQLite 图数据库
(节点:函数/类/导入;边:调用/继承/测试覆盖)
│ 审查时查询
▼
最小必读文件集 ──MCP──▶ AI 助手
四级各司其职:解析层负责"把字符串变成结构",抽取层负责"把结构变成图",存储层负责"让图可查询",查询层负责"把图变成答案"。AI 助手全程只接触最后一级的输出——这是整个架构最重要的设计决策:LLM 被隔离在图构建之外,图谱的构建和查询是纯确定性计算。
3.2 机制一:爆炸半径分析(Blast-radius Analysis)
这是 CRG 最核心的能力。当一个文件发生变更,图会追踪:
- 直接调用者(谁 call 了被改的函数)
- 传递依赖者(调用者的调用者,沿边继续走)
- 关联测试(哪些测试覆盖了受影响的节点)
这三类文件构成变更的"爆炸半径"(blast radius)。AI 审查时只读这些文件,其余全部跳过。
用伪代码表达这个遍历,本质上就是图上的反向 BFS:
def blast_radius(graph, changed_files, max_depth=3):
affected = set()
frontier = {node for f in changed_files
for node in graph.nodes_in_file(f)}
for depth in range(max_depth):
next_frontier = set()
for node in frontier:
# 反向边:谁调用/继承/导入了这个节点
for caller in graph.reverse_edges(node,
kinds=("calls", "inherits", "imports")):
if caller not in affected:
affected.add(caller)
next_frontier.add(caller)
frontier = next_frontier
# 补上覆盖受影响节点的测试
tests = {t for n in affected
for t in graph.tests_covering(n)}
return files_of(affected | tests)
注意 max_depth 这个参数的存在意义:真实调用图里,几乎所有函数最终都传递可达 main()。不加深度截断,爆炸半径会退化成"整个仓库"。深度限制 + 边类型过滤,是把理论上的可达集裁剪成工程上有用的"可能受影响集"的关键。这也解释了 CRG 为什么坦承自己"故意保守"——半径宁大勿小,漏报一个被改坏的依赖比多读三个文件贵得多。
monorepo 场景是这个机制的高光时刻。官方给的数据:一个 27700+ 文件的大 monorepo 中,审查上下文最终只包含约 15 个文件。从 20 多万源码 token 到约 2500 token 的图响应,压缩比约 93 倍。这就是"漏斗过滤"——图把噪声在进入 LLM 上下文之前就滤掉了。
3.3 机制二:增量更新,2 秒内完成
全量构建图谱不算贵(500 文件约 10 秒),但如果每次文件保存都全量重建,开发体验就完蛋了。CRG 的增量更新链路:
- 触发:编辑器保存钩子、git commit hook 或 watch 模式监听文件变化
- 判定:对文件内容做 SHA-256 哈希,与图中记录的哈希比对,找出真正变化的文件
- 定位:通过图的反向边找到变化文件的依赖者(它们的边可能失效)
- 重解析:只对变化文件跑 Tree-sitter,更新对应节点和边
官方数据:2900 文件的项目,一次增量更新中只重新解析 5 个文件、跳过 2910 个,全程 2 秒以内。
这里的工程细节是用内容哈希而非 mtime 做变更检测。mtime 在 git checkout、branch 切换、CI 缓存恢复等场景下会大面积误报(时间戳变了但内容没变),SHA-256 内容哈希则完全免疫这类噪声——多花的哈希计算时间远低于误报导致的无效重解析。这是构建系统领域(Bazel、Buck)早已验证过的经验,CRG 把它搬到了代码图谱上。
3.4 机制三:MCP 集成——图谱如何"喂"给 AI
图建好了,AI 怎么用?CRG 选择了 MCP(Model Context Protocol)作为交付通道。MCP 是 Anthropic 主导的工具协议,如今已是 AI 编码工具的事实标准。CRG 把自己注册为一个 MCP server,向 AI 助手暴露一组图查询工具:搜索节点、查询爆炸半径、获取审查上下文、查看风险评分等。
审查时的交互流变成:
用户: "帮我审查这次改动"
▼
AI 助手检查可用的 MCP 工具
▼
调用 CRG: get_review_context(changed_files)
▼
CRG 返回: 爆炸半径 + 风险评分 + 最小文件集(约 2000~3500 token)
▼
AI 只精读这些文件 → 输出审查意见
这个设计的妙处在于职责边界干净:CRG 不做任何 LLM 推理,只提供确定性的结构化事实;LLM 不做任何图遍历,只消费图给出的上下文。两边通过 MCP 的窄接口解耦,谁升级都不影响对方。
配套的 install 命令则把"最后一公里"做到了极致:一条 code-review-graph install,自动探测本机装了哪些 AI 编码工具——Codex、Claude Code、Cursor、Windsurf、Zed、Continue、Gemini CLI、GitHub Copilot、CodeBuddy、Kiro 等十几个平台——然后给每一个写入正确的 MCP 配置、安装平台原生 hook/skill、往平台规则文件里注入"图感知指令"。它甚至会检测你是用 uvx 还是 pip/pipx 装的,生成对应的启动配置。
别小看这个安装器。开发者工具的采纳率一半死在配置摩擦上——要手工编辑三个 JSON、重启两个工具的产品,用户流失率是"一条命令搞定"的十倍不止。CRG 连 uninstall 都做了对称设计:只删除自己写入的配置条目,不碰无关的 MCP server 和 hook,共享配置文件用原子替换写入,写失败原文件完好无损。这种细节是判断一个开源项目工程成熟度的试金石。
四、代码实战:从安装到 CI 集成
4.1 五分钟上手
# 安装(Python 3.10+,推荐先装 uv)
pip install code-review-graph # 或 pipx install code-review-graph
# 自动检测并配置本机所有 AI 编码工具
code-review-graph install
# 解析代码库,构建图谱
code-review-graph build
构建完成后重启你的编辑器/CLI 工具,然后直接对 AI 说:
Build the code review graph for this project
之后的审查请求,AI 会自动走图查询拿上下文。如果只想配置特定平台:
code-review-graph install --platform claude-code
code-review-graph install --platform cursor
code-review-graph install --platform codex
4.2 让图保持新鲜:watch 模式与 hook
# 方式一:watch 模式,监听文件保存
code-review-graph watch
# 方式二:依赖 install 时注入的平台 hook / git hook
# 文件保存或 commit 时自动触发增量更新
日常开发建议直接用 install 注入的 hook,零心智负担;watch 模式适合 hook 覆盖不到的编辑器。
4.3 自定义语言支持:不用 fork,一个 TOML 搞定
CRG 有一个非常聪明的扩展设计:如果你的仓库用了它没内置支持的语言,不需要改代码、不需要 fork,在 .code-review-graph/ 目录下放一个 languages.toml:
[languages.erlang]
extensions = [".erl"]
grammar = "erlang" # tree_sitter_language_pack 里的 grammar 名
function_node_types = ["function_clause"] # 哪些 AST 节点算函数
class_node_types = ["record_decl"] # 哪些算类
import_node_types = ["import_attribute"] # 哪些算导入
call_node_types = ["call"] # 哪些算调用
通用的 tree-sitter walker 会照着这份映射自动完成节点抽取。这个设计的本质是把"语言支持"从代码问题降维成配置问题:只要 tree_sitter_language_pack 里有对应 grammar(它打包了上百种),你就能用四行节点类型映射接入任何语言。内置语言不可被覆盖,避免用户配置把核心解析搞坏——防御性设计也到位了。
4.4 GitHub Action:把风险评分挂进 PR
CRG 把同一套分析打包成了 composite GitHub Action,在 CI 里对每个 PR 输出风险评分:
# .github/workflows/code-review-graph.yml
on:
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: tirth8205/code-review-graph@v2.3.6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
它会在 PR 下发一条 sticky comment(每次 push 原地更新,不刷屏),内容包括:风险评分的函数列表、受影响的执行流、测试缺口(改了代码但没有测试覆盖的部分)。可选的 fail-on-risk 输入能把它变成合并门禁——高风险变更直接把 CI 打红。
两个细节值得注意:其一,全程 local-first,图谱在你的 CI runner 上构建和查询,源码不发往任何外部服务,这让它能进对安全敏感的企业流水线;其二,CRG 仓库自己就在用这个 Action 审查自己的 PR(dogfooding),.github/workflows/pr-review.yml 里能看到实际配置。
4.5 直接查询图谱:SQLite 就在那里
图存在本地 SQLite 里,这意味着你完全可以绕过 MCP 直接做结构分析。比如想找出仓库里"被调用最多的前 10 个函数"(高风险热点):
import sqlite3
conn = sqlite3.connect(".code-review-graph/graph.db")
rows = conn.execute("""
SELECT n.name, n.file, COUNT(*) AS callers
FROM edges e
JOIN nodes n ON n.id = e.target
WHERE e.kind = 'calls'
GROUP BY e.target
ORDER BY callers DESC
LIMIT 10
""").fetchall()
for name, file, callers in rows:
print(f"{callers:>4} 处调用 {name} ({file})")
这种"数据就是本地文件,schema 可自由消费"的开放性,是 local-first 架构的隐性红利:图谱不只服务 AI 审查,还能喂给你自己的脚本、dashboards、架构检查规则。表结构以实际版本为准(用 .schema 查看),但节点表 + 边表的骨架是稳定的。
4.6 语义搜索:可选的向量层
除了结构化图查询,CRG 还支持可选的语义搜索——用 sentence-transformers 本地跑 embedding,或者接 Gemini、MiniMax 及任何 OpenAI 兼容的 embedding API。这一层解决的是"我不知道函数叫什么名字,但我知道它干什么"的模糊查询(比如"处理用户登录重试的逻辑在哪")。结构图管精确依赖,向量管模糊语义,两层互补。
值得肯定的是这一层被设计成严格可选:不配 embedding,核心的图谱和爆炸半径功能照常工作。不把重依赖强加给所有用户,这是好的分层。
五、benchmark 冷思考:82 倍是真的,但要会读
CRG 的 README 在数据披露上做了一件业内少见的事:把自己 benchmark 的水分主动标出来了。这部分值得细读,因为它示范了怎么诚实地报告性能数据——以及怎么识别别人报告里的坑。
5.1 "528 倍"是最大值,中位数是 82 倍
6 个真实仓库(fastapi、flask、httpx、express、gin、code-review-graph 自身)的实测:
| 仓库 | 全量语料 token | 平均图查询 token | 压缩比 |
|---|---|---|---|
| fastapi | 951,071 | 2,169 | 528.4x |
| code-review-graph | 208,821 | 2,495 | 93.0x |
| gin | 166,868 | 1,990 | 91.8x |
| flask | 125,022 | 1,986 | 71.4x |
| express | 135,955 | 3,465 | 40.6x |
| httpx | 89,492 | 2,438 | 38.0x |
README 明确写着:**中位数约 82 倍;被广泛引用的 528 倍是最好情况(fastapi,最大的语料),不是典型结果。**注意规律:仓库越大压缩比越高——因为图查询的返回大小基本恒定(2000~3500 token),而分母(全量语料)随仓库线性增长。这意味着 CRG 的价值和你的仓库规模正相关,小项目收益有限,monorepo 收益巨大。
更进一步,README 还承认这个"全量语料基线"本身是个没人真付的上界:现实中稍微像样的 agent 都会先 grep 再精读匹配文件,不会通读整个仓库。所以项目另设了 agent_baseline 基准——模拟"grep + 读 top-3 匹配文件"的现实 agent 行为,和图查询成本对比。对比这个现实基线的收益倍数肯定远小于 82 倍,但依然为正——而且图查询附带的调用关系、影响半径是 grep 给不了的。
5.2 "recall 1.0"是循环论证,项目自己承认了
影响分析(爆炸半径)的准确率数据:13 个评估 commit 上平均 F1 0.714、精确率 0.578、召回率 1.0。
召回率 1.0 看起来完美,但 README 直接泼了自己冷水:这个 ground truth(变更文件 + 有调用/导入边指向它们的文件)是从同一张图推导出来的,预测器走的也是这张图——循环论证,只能当上界看,不能宣传成"100% 召回"。
项目为此另做了"co-change 模式"的诚实评估:给预测器一个变更文件,让它预测同一 commit 里作者实际还改了哪些其他文件——ground truth 来自 git 历史,与图独立。README 说这个模式的数字"预计会明显更低",且在实际测出来之前不引用。
一个开源项目在自家 README 里写"我们的 recall 1.0 是循环的,别信"——这种数据诚实度,比数字本身更能建立信任。反过来,下次你看到任何工具吹"100% 召回率",先问一句:ground truth 是怎么来的?
5.3 明牌的弱点
README 的 Limitations 一节列了几条硬伤,同样值得过一遍:
- 小改动场景可能负收益:单文件小 diff 的审查中,图返回的结构化上下文(影响边 + 代码片段)可能比直接读那一个文件还大。结构元数据是为多文件分析买的保险,小变更用不上就是纯开销。
- 搜索排序一般(MRR 0.35):关键词搜索大多能把正确结果排进前 4,但排序质量有明显提升空间;express 因为 JS module 模式的命名习惯甚至查不到结果。
- 执行流检测召回率仅 33%:对 Python 和 PHP/Laravel 的框架入口模式识别最好,JS 和 Go 的流检测还很弱。
- 精确率换召回率:0.578 的精确率意味着爆炸半径里近一半文件其实不受影响。这是有意的保守策略,但大依赖图里假阳性会稀释 AI 的注意力。
5.4 横向对比:CRG 在生态里的位置
| 方案 | 思路 | 与 CRG 的差异 |
|---|---|---|
| DeepWiki / OpenDeepWiki | 用 LLM 给仓库生成 wiki 文档 | 产出给人读的文档;CRG 产出给 AI 用的结构化查询,且构建过程零 LLM 成本 |
| GitNexus | 零服务器代码智能引擎 | 同赛道近邻,均为本地代码图谱路线 |
| Anthropic Code Review | Claude Code 内置审查 | 平台绑定,审查逻辑黑盒;CRG 平台中立,接十几家工具 |
| 裸 grep + agent | agent 自行搜索精读 | 零安装成本;但没有调用图、没有爆炸半径、没有测试缺口分析,每次会话重复劳动 |
CRG 的差异化站位清晰:平台中立的、确定性的、本地优先的结构层,卡在"所有 AI 编码工具"和"你的代码库"之间。它赌的是:无论上层 AI 工具怎么洗牌,"用确定性图计算替代 LLM 重复阅读"这一层的价值恒在。
六、总结与展望
值得抄的作业
- 确定性计算优先于 LLM 推理。凡是图遍历、哈希比对、依赖分析能确定性解决的问题,绝不让 LLM 用 token 去猜。LLM 只消费结构化事实的最终摘要。这是所有 AI 工程的第一性原理:token 应该花在只有 LLM 能干的事上。
- local-first 是企业采纳的入场券。图谱在本机/自家 CI 构建查询,源码不出门——这一条决定了它能不能进大公司。
- 安装体验是增长杠杆。一条命令探测并配置十几个平台、对称的 uninstall、原子写入配置——工具类项目的采纳率一半取决于此。
- 配置化扩展优于代码扩展。
languages.toml四行映射接入新语言,把贡献门槛从"会改解析器"降到"会填表"。 - 诚实的 benchmark 本身是竞争力。主动标注 528x 是最值不是典型值、承认 recall 1.0 循环论证、明牌 MRR 0.35 的弱项——换来的信任比虚高的数字值钱。
留白与走向
短板也明确:语法级解析天花板(无类型推导,动态语言的调用边永远模糊)、搜索排序质量、JS/Go 的流检测、小变更场景的负收益。往前看有三条可能的演进线:语义层加厚(在 Tree-sitter 之上对热点路径做轻量符号解析,提高边的精度)、跨仓库图谱(微服务场景下把 API 调用关系连成服务间图)、以及审查之外的场景外溢——影响分析做测试选择(只跑爆炸半径内的测试)、架构守护(检测跨层违规依赖)、增量文档生成,一张维护良好的代码图谱能喂的下游远不止 code review。
回到最初的问题:AI 编码工具的 token 焦虑,靠更大的上下文窗口解决不了——窗口再大,把 20 万 token 的无关代码塞进去也只会让模型更分心、让账单更难看。真正的解法是 CRG 这个方向:**在 LLM 之前放一层确定性的结构索引,让 AI 像老工程师一样"先看地图,再读代码"。**这层地图今天叫 code-review-graph,明天可能叫别的名字,但它会成为 AI 编码基础设施的标配层——就像索引之于数据库一样理所当然。
参考:tirth8205/code-review-graph(GitHub,MIT 协议)、项目 README 与 docs/REPRODUCING.md 基准复现文档。文中数据均来自项目官方公开的 benchmark 披露,转述时保留其自我修正与局限性声明。