code-review-graph 深度拆解:Tree-sitter + MCP 如何把代码库编译成图,让 AI 审查省下几百倍 token
一句话先说清楚:
code-review-graph干的事,就是把「让大模型自己 grep 整个仓库找上下文」这套暴力打法,换成「先把代码编译成一张持久化的依赖图,审查时只喂 AI 真正相关的那几个文件」。GitHub Trending 单日 +1800 星不是偶然——它戳中了每个用 AI 写代码的人最疼的地方:token 在无意义的翻仓上被烧光了。
这篇文章我不打算写成 README 的中文复读机。我想从一个后端 + 工程效率的视角,把它的架构、数据结构、增量算法、MCP 集成、embedding 选型这些东西一层层剥开,配上能跑的代码,讲清楚它「为什么快、为什么省、什么时候不该用」。
一、背景:AI 代码审查的 token 是怎么被烧掉的
先还原一个真实场景。你在 Cursor 或 Claude Code 里改了一个 login() 函数,然后让 AI「帮我审查一下这次改动有没有风险」。
AI 助手拿到这个请求后,它并不知道 login() 被谁调用、影响了哪些测试、依赖了哪些模块。于是它开始做一件非常「诚实」但极其低效的事:
grep一遍login这个关键词,命中几十个文件;- 挨个
read_file读进上下文; - 为了理解调用链,又顺藤摸瓜读了一堆间接依赖;
- 上下文窗口被塞满,模型开始「注意力稀释」,审查质量反而下降。
我给一组实测感受一下量级。官方对自己这个仓库(174 文件、3107 个节点、22227 条边)跑基准测试,其中「影响半径分析」这一场景:
| 场景 | 传统全量读取 token | 图查询 token | 节省比例 |
|---|---|---|---|
| 影响半径分析 | 99,375 | 99 | 99.9% |
| 架构探索 | 87,769 | 1,259 | 98.6% |
| 查找调用者 | 2,066 | 449 | 78.3%(且速度 5.7x) |
在 monorepo 里更夸张:源码本身 208,821 个 token,图响应只有约 2,495 token——单个问题 token 减少 93 倍;跨 6 个真实仓库的横向评估里,token 削减在 38× 到 528× 之间波动。
这里的核心洞察其实很朴素:代码不是自然语言,它是有强结构的。函数调用、类继承、import、测试覆盖,这些关系天然是一张图。既然是图,那就没必要每次都用「全文扫描 + 语言模型硬读」去重建这张图——你完全可以一次性把它解析出来存好,之后按需查询。这就是 code-review-graph 的全部立论基础。
二、核心概念:从 AST 到「代码知识图谱」
2.1 节点与边的定义
code-review-graph 用 Tree-sitter 把每个源文件解析成 AST,然后从 AST 里抽取两类东西:
- 节点(Node):函数、类、导入。每个节点带上
id、name、kind、file、line_start/line_end、signature等属性。 - 边(Edge):调用(calls)、继承(inherits)、导入(imports)、测试覆盖(tests)。边是有向的,并且带置信度。
置信度这个设计值得单独说。真实世界的代码解析没法做到 100% 精确——动态分发、反射、鸭子类型都会让「A 调用 B」这件事变得模糊。所以它给边分了三级:
EXTRACTED:直接从 AST 里能确定抽出来的(比如显式的函数调用点);INFERRED:通过启发式推断的(比如同名方法的可能调用);AMBIGUOUS:存在歧义的(比如多个候选目标)。
每条边还附一个浮点分数。这让下游做「影响半径」计算时能按置信度加权,而不是把所有边一视同仁。
用一段伪代码表达这个数据模型:
from dataclasses import dataclass
from enum import Enum
class EdgeKind(str, Enum):
CALLS = "calls"
INHERITS = "inherits"
IMPORTS = "imports"
TESTS = "tests"
class Confidence(str, Enum):
EXTRACTED = "extracted" # 从 AST 直接可得
INFERRED = "inferred" # 启发式推断
AMBIGUOUS = "ambiguous" # 有歧义
@dataclass
class GraphNode:
id: str # 稳定 id,例如 "auth/login.py::login"
name: str
kind: str # function | class | import
file: str
line_start: int
line_end: int
signature: str # "login function (user: User) returns Session"
@dataclass
class GraphEdge:
src: str
dst: str
kind: EdgeKind
confidence: Confidence
score: float # 0.0 ~ 1.0
2.2 为什么用 Tree-sitter 而不是 LSP 或正则
这是个值得展开的工程决策。抽代码结构,理论上有几条路:
- 正则/字符串匹配——快但脆,遇到嵌套、多行签名就崩。
- 语言原生编译器/LSP——最准,但你得为每种语言起一个 language server,环境依赖爆炸,Python 项目里跑 Go 的 gopls 是灾难。
- Tree-sitter——增量式、容错式的解析器生成器,一套 API 覆盖几十种语言,解析速度是毫秒级,而且能容忍语法不完整的代码(你写到一半的文件也能解析出大部分结构)。
code-review-graph 选了第三条,然后在 Tree-sitter 覆盖不到的边角用「针对性回退解析」补齐。它的语言面非常宽:Python、JS/TS/TSX、Go、Rust、Java、C/C++、C#、Ruby、Kotlin、Swift、PHP、Scala、Solidity、Dart、R、Perl、Lua、Objective-C、shell、Elixir、Zig、PowerShell、Julia、Nix、Verilog/SystemVerilog、SQL、Vue/Svelte 单文件组件,甚至 Jupyter 的 .ipynb。
对于 Python,它还有个可选增强 [enrichment],用 Jedi 做调用解析——Jedi 能做静态类型推断,把「这个 self.repo.save() 到底调的哪个类的 save」这种模糊调用解析得更准,从而把边从 INFERRED 升级成 EXTRACTED。
三、架构拆解:一条从代码到最小审查集的流水线
整体架构是一条清晰的单向流水线:
代码库
│ git ls-files(只索引已跟踪文件)
▼
Tree-sitter 解析器 ──► AST
│ 抽取节点 + 边
▼
SQLite 图存储(.code-review-graph/)
│ 后处理:社区检测 / 执行流 / FTS5 索引
▼
影响半径查询
│
▼
最小审查集(喂给 AI)
3.1 为什么是 SQLite 而不是 Neo4j
这是我最欣赏的一个决定。图数据库领域大家第一反应是 Neo4j、或者上个 graph engine。但 code-review-graph 把核心图直接存在 SQLite 文件里,放在仓库的 .code-review-graph/ 目录下。
理由很硬核:
- 零外部依赖。开发者装个 pip 包就能用,不用起数据库、不用连云。这对「本地优先(local-first)」是硬约束——你的代码结构不该为了跑个审查而上传到别人的服务器。
- SQLite 完全够用。代码图的规模通常在几千到几十万节点,SQLite 的 B-tree 索引 + 递归 CTE 完全扛得住图遍历。
- 可移植。整个图就是一个文件,删掉重建、拷贝、diff 都很自然。
它还用了 SQLite 的 FTS5 全文索引做混合搜索——关键词匹配 + 向量相似度结合。这意味着「语义搜索」和「精确名字搜索」用的是同一套存储引擎,不用再引第二个系统。
一个简化版的 schema 大概长这样:
CREATE TABLE nodes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT NOT NULL,
file TEXT NOT NULL,
line_start INTEGER,
line_end INTEGER,
signature TEXT,
file_hash TEXT -- SHA-256,用于增量比对
);
CREATE TABLE edges (
src TEXT NOT NULL,
dst TEXT NOT NULL,
kind TEXT NOT NULL,
confidence TEXT NOT NULL,
score REAL NOT NULL,
PRIMARY KEY (src, dst, kind)
);
CREATE INDEX idx_edges_dst ON edges(dst); -- 反向查「谁调用了我」
CREATE INDEX idx_nodes_file ON nodes(file);
-- FTS5 混合搜索
CREATE VIRTUAL TABLE nodes_fts USING fts5(
name, signature, content='nodes', content_rowid='rowid'
);
注意那个 idx_edges_dst 索引。查「谁调用了 login」(callers)本质是 SELECT src FROM edges WHERE dst = ? AND kind = 'calls',有了 dst 索引这就是一次 O(log n) 查找。这就是为什么「查找调用者」场景能做到 5.7x 加速——它把线性全文扫描换成了索引查找。
3.2 影响半径:一次有边界的图遍历
「影响半径(blast radius)」是这个工具最核心的算法概念。当 login() 改动时,需要找出所有可能受影响的:
- 上游调用者:谁调了 login(顺着
dst=login的边反向找); - 下游依赖:login 调了谁(顺着
src=login的边正向找); - 相关测试:哪些 test 覆盖了这条链。
本质是从变更节点出发做一次有界 BFS。「有界」很关键——如果不设边界,一个核心工具函数的影响半径可能是整个仓库。所以它用 token 预算和深度来剪枝:
def impact_radius(graph, changed_nodes, max_depth=3, token_budget=4000):
"""从变更节点出发,做有界反向+正向 BFS,返回最小审查集。"""
visited = set()
frontier = list(changed_nodes)
result = []
spent = 0
for depth in range(max_depth):
next_frontier = []
# 按边置信度 * score 排序,优先展开高置信关系
ranked = rank_by_confidence(graph, frontier)
for node in ranked:
if node in visited:
continue
visited.add(node)
cost = estimate_tokens(node) # 该节点摘要的 token 成本
if spent + cost > token_budget: # 预算耗尽,停止扩张
return result
spent += cost
result.append(node)
# 反向:调用者;正向:被调用者与测试
next_frontier += graph.callers(node)
next_frontier += graph.callees(node)
next_frontier += graph.tests_of(node)
frontier = next_frontier
return result
这段逻辑里藏着两个我认为很聪明的取舍:
- 按
confidence * score排序再展开——预算有限时,优先把 token 花在「我很确定相关」的节点上,而不是被一堆 AMBIGUOUS 的边带偏。 - token 预算是硬边界——它保证了无论仓库多大,喂给 AI 的上下文永远可控。这就是「AI 只读约 15 个文件」的秘密,不是它猜得准,而是它按预算截断了一次拓扑排序。
四、增量更新:2900 文件的仓库重索引不到 2 秒
全量构建一个 500 文件的项目大约 10 秒,这不算慢,但如果每次保存都全量重建就没法用了。所以增量更新是它能进入日常工作流的关键。
机制是这样的:
- 启用 watch 模式或 git 提交钩子;
- 文件保存 / commit 触发,先做
git diff拿到变更文件列表; - 对每个变更文件算 SHA-256,和上次存的
file_hash比对; - 只重新解析 hash 变了的文件,跳过其余;
- 重解析后,只更新这些文件相关的节点和边,再局部重跑受影响的后处理。
用代码表达核心的「哈希门控」逻辑:
import hashlib
def incremental_update(graph, repo):
changed = git_diff_names(repo) # 例如 5 个文件
reparsed = 0
for path in changed:
new_hash = sha256_file(path)
old_hash = graph.get_file_hash(path)
if new_hash == old_hash:
continue # 内容没变(比如只动了 mtime),跳过
ast = tree_sitter_parse(path)
nodes, edges = extract(ast, path)
graph.replace_file_subgraph(path, nodes, edges, new_hash)
reparsed += 1
# 只有真的有节点变化时,才局部重跑社区/执行流后处理
if reparsed:
graph.postprocess(scope="incremental")
return reparsed
def sha256_file(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
一个 2900 文件的项目里,git 只报了 5 个变更文件,其中可能还有 2 个 hash 没变(格式化工具动了但内容等价),最后只重解析 3 个——这就是「2910 个文件被跳过、重索引 < 2 秒」的来源。
这里有个容易被忽略的工程细节:它默认只索引 git 已跟踪的文件(git ls-files)。也就是说,.gitignore 里的东西自动被跳过,node_modules、vendor、构建产物根本不进图。如果你想在已跟踪文件里再排除一部分(比如 codegen 出来的 *.generated.ts),就写个 .code-review-graphignore:
generated/**
*.generated.ts
vendor/**
node_modules/**
五、MCP 集成:AI 助手如何「自动」用上这张图
前面讲的都是「图怎么建」,这一节讲「图怎么被 AI 用起来」。答案是 MCP(Model Context Protocol)。
5.1 一条命令自动配置全平台
pip install code-review-graph # 或 pipx / uvx
code-review-graph install # 自动检测并配置所有 AI 编码工具
code-review-graph build # 解析代码库
install 这一步做了三件容易被低估的脏活:
- 检测你机器上装了哪些 AI 编码工具(Codex、Claude Code、Cursor、Windsurf、Zed、Continue、Gemini CLI、Qwen、Kiro、Copilot……);
- 为每个工具写入正确格式的 MCP server 配置;
- 把「图感知指令」注入到各平台的规则文件里(比如
CLAUDE.md、AGENTS.md),告诉 AI「优先用图工具,别自己乱 grep」。
它甚至会判断你是用 uvx 还是 pip 装的,从而生成不同的启动命令。生成的 MCP 配置大致是这个形状:
{
"mcpServers": {
"code-review-graph": {
"command": "uvx",
"args": ["code-review-graph", "serve"]
}
}
}
5.2 30 个 MCP 工具与「先要最小上下文」的纪律
图建好后,AI 助手能调用大约 30 个 MCP 工具。这里我挑几个关键的讲,因为它们的调用顺序本身就是一套方法论:
| 工具 | 作用 | token 量级 |
|---|---|---|
get_minimal_context_tool | 超紧凑上下文,第一个调 | ~100 |
get_impact_radius_tool | 变更文件的影响半径 | ~几百 |
detect_changes_tool | 差异映射到受影响函数/执行流/测试缺口,带风险评分 | ~几百 |
query_graph_tool | 查调用者/被调用者/测试/导入/继承 | 按需 |
traverse_graph_tool | 任意节点 BFS/DFS,可设 token 预算 | 受控 |
get_hub_nodes_tool | 连接最多的节点(架构热点) | 小 |
get_bridge_nodes_tool | 介数中心性找架构瓶颈 | 小 |
get_knowledge_gaps_tool | 孤立节点、未测热点、薄弱社区 | 小 |
官方明确建议的调用链是:先 get_minimal_context(约 100 token)拿全局骨架 → 再 detect_changes 定位变更影响 → 只有需要细节时才 query_graph 或 traverse_graph 逐点展开。
这套顺序背后是一个我很认同的理念:别让模型一上来就要全量信息。先给它一张便宜的「地图」,让它自己决定要放大看哪一块。这跟人类 senior 工程师做 code review 的方式几乎一样——先看 PR 的 diff 摘要和影响面,而不是一头扎进每一行。
一个典型的审查会话,AI 内部大概是这么跑的(伪代码还原):
# AI 助手收到「审查这次改动」后的内部编排
ctx = mcp.call("get_minimal_context_tool", task="review changes",
repo_root=".") # ~100 tokens
changes = mcp.call("detect_changes_tool") # 拿到风险评分 + 受影响函数
for hotspot in changes["high_risk"]: # 只对高风险点深挖
detail = mcp.call("query_graph_tool",
node=hotspot, relation="callers",
detail_level="minimal") # 关键:minimal 把 token 压到最低
review(detail)
gaps = mcp.call("get_knowledge_gaps_tool") # 顺带检查测试缺口
注意 detail_level="minimal" 这个参数——它是把 token 消耗压到地板的关键旋钮。审查场景下你往往不需要函数完整实现,只需要签名和关系。
5.3 5 个提示模板
它还内置了 5 个 MCP 提示模板,本质是把常见工作流固化成一键流程:
review_changes:审查自上次提交以来的变更;architecture_map:生成架构概览;debug_issue:从入口点追调用链定位问题;onboard_developer:新人入职,快速理解代码结构;pre_merge_check:合并前检查。
onboard_developer 这个我觉得被严重低估了。新人接手一个陌生 monorepo 最痛的就是「不知道从哪看起」。有了图,你可以直接问「这个仓库的核心 hub 节点是什么、主要执行流有哪些」,AI 靠 get_hub_nodes 和 list_flows 就能给你画出主干,比读一周文档快得多。
六、进阶能力:不只是省 token,还是代码质量雷达
如果只把它理解成「省 token 工具」,那就低估它了。当代码变成图之后,一堆图算法就能直接落地成「代码质量洞察」。
6.1 社区检测(Leiden 算法)
它用 Leiden 算法 把关联紧密的代码聚成「社区(community)」——本质是无监督地发现你代码里的「模块」。有意思的是,当一个社区大到超过整张图的 25% 时,它会递归再分割,并自动调节分辨率。这解决了社区检测的经典问题:大仓库容易聚出一个吞掉半个项目的「巨型社区」,没有分析价值。
6.2 Hub 与 Bridge:两种不同的架构风险
- Hub 节点:连接最多的节点(高度数)。这些是「牵一发动全身」的核心,改它们风险最高。
- Bridge 节点:通过**介数中心性(betweenness centrality)**发现的架构瓶颈——它们不一定连接多,但删掉它们会切断两个模块间的唯一通路。
这两个是不同的风险信号。Hub 告诉你「哪里改动影响大」,Bridge 告诉你「哪里是架构上的单点」。一个健康的架构应该 Bridge 尽量少(模块间有多条解耦路径),Hub 有意识地控制(核心抽象稳定)。
6.3 异常评分与知识缺口
- 异常评分(surprising connections):检测「意料之外的耦合」——跨社区的边、跨语言的边、从外围直连核心的边。这些往往是架构腐化的早期信号,比如某个 UI 组件直接 import 了数据库层。
- 知识缺口(knowledge gaps):孤立节点(没人调用的死代码候选)、未测试热点(高连接但零测试覆盖)、结构性弱点。
这两个能力我会把它当成 CI 里的「架构 linter」用。传统 linter 看的是单文件的代码风格,而这些看的是跨文件的结构健康度,是完全不同维度的检查。
6.4 导出到你熟悉的工具
图不是黑盒,它能导出成多种格式:
code-review-graph visualize # 交互式 HTML(D3.js 力导向图)
code-review-graph visualize --format graphml # Gephi / yEd
code-review-graph visualize --format cypher # 导进 Neo4j 继续玩
code-review-graph visualize --format obsidian # Obsidian 知识库,带 wikilinks
code-review-graph visualize --format svg # 静态图
code-review-graph wiki # 从社区结构自动生成 Markdown Wiki
那个 Obsidian 导出很有意思——它把每个代码实体变成一个笔记,用 wikilinks 连接依赖关系。你可以像逛知识库一样在 Obsidian 里「漫游」自己的代码库。
七、性能与选型:embedding 那些坑
语义搜索是可选能力(pip install "code-review-graph[embeddings]"),支持 sentence-transformers、Google Gemini、MiniMax,以及任何 OpenAI 兼容端点(真 OpenAI、Azure、new-api、LiteLLM、vLLM、LocalAI、Ollama 的 openai 模式)。
配 OpenAI 兼容端点不用装额外依赖,设几个环境变量就行:
export CRG_OPENAI_BASE_URL=http://127.0.0.1:3000/v1 # 或 https://api.openai.com/v1
export CRG_OPENAI_API_KEY=sk-...
export CRG_OPENAI_MODEL=text-embedding-3-small
# 可选
export CRG_OPENAI_DIMENSION=1536 # v3 模型支持维度缩减
export CRG_OPENAI_BATCH_SIZE=100 # 网关有更严格批次限制时下调
当 base URL 指向 localhost 时,它会自动跳过「数据出境」警告——一个很贴心的 local-first 细节。
但这里有两个坑,README 里点到了,我展开说:
坑一:别用 preview / beta / exp 模型做长期 embedding。 像 gemini-embedding-2-preview 这种,供应商可能悄悄换权重(维度一变你就得全量重新 embed 整个图),或者无预警下架。生产用就老老实实上 GA 模型:text-embedding-3-small/large、自宿主的 Qwen3-Embedding-8B、或原生 gemini-embedding-001。
坑二:目前只 embed 函数签名,不 embed 函数体。 这是个很重要的认知——每个节点嵌入的只是类似 "parse_file function (path: str) returns Tree" 这样约 10 token 的签名。这意味着那些靠「读懂长函数体」来拉开差距的顶级 embedding 模型(比如在 MTEB-code 上刷 SOTA 的 Gemini 2、Qwen3-8B),在这种超短输入下的优势会被大幅抹平——你花大钱上大模型 embedding,收益可能远不如预期。所以在当前版本,embedding 选型上『够用就好』是理性的,函数体/docstring 嵌入还在 roadmap 上。
这个坦诚我很欣赏。很多项目会把「支持顶级 embedding 模型」当卖点吹,而它直接告诉你「在我的输入长度下,大模型和小模型差距不大,别浪费钱」。
八、什么时候不该用它
工程上没有银弹,讲讲它的边界:
- 超小项目没必要。 几十个文件的仓库,AI 全量读一遍也就几千 token,建图的开销反而不划算。它的价值随仓库规模非线性增长——monorepo 才是主场。
- 高度动态的语言精度会打折。 重反射、重元编程、大量运行时动态分发的代码,静态解析出来的边会有很多
INFERRED/AMBIGUOUS,影响半径可能偏大或偏小。Python 上可以靠 Jedi 增强缓解,其他语言就得接受一定误差。 - 它不理解运行时行为。 图是静态结构,捕捉不到「这条路径实际上从没被执行过」这类信息。它是「结构地图」,不是「profiler」。
- 签名级 embedding 的语义搜索有天花板。 如前所述,当前版本对「按语义模糊找一段实现逻辑」的能力有限,别指望它替代读代码本身。
理解这些边界,才能把它用在刀刃上:大仓库 + AI 辅助审查 + 频繁增量改动,这三个条件叠加时它的 ROI 最高。
九、上手清单:从零到跑通
给个可复制的最小实践路径:
# 1. 安装(推荐配合 uv,MCP 会自动用 uvx 启动)
pip install code-review-graph
# 或者:pipx install code-review-graph
# 2. 自动配置你所有的 AI 编码工具
code-review-graph install
# 只想配某一个:
code-review-graph install --platform claude-code
# 3. 首次全量构建(500 文件约 10 秒)
code-review-graph build
# 4. 开启 watch,之后保存自动增量更新(< 2 秒)
code-review-graph watch
# 5. 看看图长啥样
code-review-graph status # 节点/边统计、健康度
code-review-graph visualize # 交互式 HTML
# 6. 做一次带风险评分的变更分析
code-review-graph detect-changes
然后回到你的 AI 编辑器,直接说「Build the code review graph for this project」或「审查这次改动的影响半径」,它就会自动走 MCP 工具链,而不是傻乎乎地 grep 全仓。
多仓库场景还能注册进一个注册表做跨仓搜索:
code-review-graph register ~/work/service-a
code-review-graph register ~/work/service-b
code-review-graph repos # 列出已注册仓库
# AI 侧可用 cross_repo_search_tool 跨仓查询
十、总结与展望:AI 编程基础设施的一个缩影
把 code-review-graph 放到更大的图景里看,它代表了 2026 年一个很清晰的趋势:AI 编码工具的竞争,正在从「模型多聪明」转向「喂给模型的上下文多精准」。
同一时间段 GitHub Trending 上冒头的项目——AI 记忆平台 cognee、代码智能图谱 code-review-graph、各种 AI 网关——本质上都在解决同一个问题的不同侧面:大模型的上下文窗口是稀缺资源,谁能把最相关的信息用最少的 token 喂进去,谁就赢。code-review-graph 选择的路径是「用确定性的图结构,替代概率性的全文检索」,这是非常工程师的思路:能用数据结构精确解决的,就别丢给大模型去猜。
往前看,几个值得期待的方向:
- 函数体 / docstring embedding——补上当前语义搜索的短板;
- 运行时信息融合——如果能把 profiler 或 trace 数据叠加到静态图上,「影响半径」就能从「理论可达」收敛到「实际热路径」,价值再上一个台阶;
- 更细的置信度校准——随着各语言 enrichment 的完善,
INFERRED边转EXTRACTED的比例会越来越高,影响半径会越来越准。
最后回到那个最朴素的判断:如果你每天在一个中大型仓库里用 AI 写代码、审代码,并且经常觉得「AI 好像读了一堆没用的文件、上下文一下就满了」,那这个工具值得你花十分钟装上试试。它不改变你的工作流,只是在你和 AI 之间插了一层「代码地图」——而这层地图,可能就是把你的 token 账单砍掉一个数量级的那块拼图。
pip install code-review-graph && code-review-graph install——十秒的事,省下的可能是你接下来每天几十万个被浪费的 token。