省了 82 倍 token 之后,AI Agent 开始读不懂你的代码

发布于 2026年07月20日 00:37 #Github 解读#Agent 基建 原文链接

省了 82 倍 token 之后,AI Agent 开始读不懂你的代码 封面图
  • code-review-graph 用 Tree-sitter 把代码库解析成 AST 结构图存进本地 SQLite,五个月 20K star,主打 AI code review 时 token 削减中位数 82 倍
  • 核心算法是 bounded best-score relaxation:三张 SQLite 临时表循环迭代,每节点只留最高 impact score,在百万级边的稠密有环图上做到亚秒级响应,代价是拿不到具体调用链
  • risk score 是个有态度的打分函数:六因子加起来满分 1.0,故意 over-predict,recall 1.0、precision 仅 0.578,没测试的代码单独拿最高 0.30 权重
  • issue #314 用户自己点破的代价:注入到 CLAUDE.md 的 ALWAYS 指令和纯省钱 preamble 让 Agent 过度信任图摘要,推理变浅、对边界条件和恢复逻辑理解变弱,正确做法是图做范围收敛、然后回源码读实现

大家好,我是若风。

一个 2026 年 2 月才创建的项目,五个月涨到 20K star,30 个贡献者,最近一次提交就在昨天。这种增长曲线在 AI 工具赛道里不算稀奇,但 code-review-graph 这个名字本身就把卖点写脸上了,它要解决的是 AI 编程工具一个很具体的痛点,你的 Agent 在做 code review 的时候,会把同一个文件反复读,把整个代码库来回扫,token 烧得像不要钱。

它的承诺也很直白,把代码库用 Tree-sitter 解析成一张结构图,存进本地 SQLite,Agent 提问的时候只返回它真正需要的那几个节点。README 顶部那张图写着 38x 到 528x 的 token 削减,中位数 82 倍。

数字很漂亮。但我把它装上、把源码翻了一遍之后,发现一个 README 没正面写的问题,这个工具在帮你省钱的同时,有可能让你的 Agent 变笨。这条线索藏在它的 prompt 注入逻辑里,也藏在用户自己开的 issue 里。

这篇文章不是来夸它也不是来踩它。我想把它到底怎么工作的拆开给你看,然后我们聊聊,当一个工具同时掌管「给 Agent 喂什么」和「不让 Agent 看什么」这两件事的时候,边界应该划在哪。

它到底在干什么

一句话,code-review-graph(下面简称 CRG)给你的代码库建一张结构图,不是向量库,不是 RAG,是节点和边。

节点是 File、Class、Function、Test、Type 这些 AST 级别的实体。边是它们之间的关系,CALLS、IMPORTS、INHERITS、TESTS_FOR,还有更抽象的「属于同一个 community」「在同一个执行流里」。这些边都是从 Tree-sitter 解析出来的语法结构里抽出来的,不是靠 embedding 猜的。

为什么要这么做。你想想 Agent 做代码审查的典型问题,「我改了这个函数,会影响哪些地方」,这其实是个图遍历问题。它要找的是调用者、调用者的调用者、覆盖这些函数的测试。用 grep 做,每一跳都是一轮搜索加读文件加推理,token 成本指数级累积。用向量检索做,相似度告诉你「这两个函数长得像」,但告诉不了你「谁调用了 login」,因为相似度和调用关系是两回事。

CRG 把这些关系预先算好存进 .code-review-graph/graph.db,一个 SQLite 文件,WAL 模式。Agent 提问的时候,一次 get_impact_radius 调用就能拿到完整的爆炸半径。

整体架构,五层

在拆具体算法之前,先把整个项目的分层架构摆出来,后面每一节都对应图里的一层或一个模块。

code-review-graph 系统架构
code-review-graph 系统架构

最上面是接入层,一个 install 命令把 CRG 接进十几种 AI 编程工具,MCP server、CLI、VS Code 扩展、GitHub Action 各占一扇门。第二层是 30 个 MCP 工具和 5 个 prompt 模板,这层是薄入口。第三层(图里高亮的那层)是项目的灵魂,blast radius 的图遍历、risk score 的打分、执行流检测、社区发现、混合搜索都在这里。第四层是 Tree-sitter 解析和增量更新。最底下是 SQLite 存储,一个文件,跟着仓库走。

下面几个小节会挑核心层里最值得拆的几块展开。

核心算法,blast radius 怎么算

这是我觉得整个项目最值得拆的一块。看 code_review_graph/graph.py:771get_impact_radius_sql

朴素的做法是 BFS 或者 DFS,从变更的文件出发沿着边往外走。但 CRG 的作者遇到一个问题,代码库的调用图是个稠密有环图,函数 A 调 B,B 调 C,C 又回调 A,这种环到处都是。如果用递归 CTE 枚举所有路径,路径数量是指数级的,一个中型项目就能把 SQLite 跑炸。

他的解法是bounded best-score relaxation,受限于「每节点只保留最佳分数」的松弛算法。核心是三张临时表在 SQLite 里循环迭代,思路如下。

# graph.py:843 附近
for table in ("_impact_best", "_impact_frontier", "_impact_next"):
    self._conn.execute(
        f"CREATE TEMP TABLE IF NOT EXISTS {table} "
        "(node_qn TEXT PRIMARY KEY, score REAL NOT NULL)"
    )

_impact_best 存每个节点到目前为止收到的最高 impact score,_impact_frontier 是当前轮要扩展的边界,_impact_next 是下一轮的边界。每一轮扫一次 edge 表,把分数沿着边传过去,如果传到某个节点的分数比它现有的 best 还高,就更新,否则丢弃。这样不管原图有多少条路径,每个节点在任何时刻最多占一行记录,迭代次数等于深度上限 MAX_IMPACT_DEPTH=2(可以通过 CRG_MAX_IMPACT_DEPTH 调)。

这个设计很巧。它牺牲了「路径完整性」(你拿不到具体的某一条调用链),换来了「在百万级边的图上亚秒级响应」。README 那张 express 项目 141 文件、17553 条边、flow detection 106ms 的 benchmark,跑的就是这个引擎。

边的权重不是均等的。IMPACT_EDGE_WEIGHTS 这个映射给不同关系类型不同的权重,CALLS 比 IMPORTS 重要,TESTS_FOR 单独算。这意味着一个函数的「爆炸」沿着真实调用传播得快,沿着 import 关系传播得慢,符合直觉。

risk score,一个有态度的打分函数

光知道谁受影响还不够,Agent 需要知道先看哪个changes.py:312compute_risk_score 给每个变更节点打一个 0 到 1 的分。这个函数的设计本身就值得琢磨,它不是中立的统计,是一个有态度的优先级判断

六个因子,每个都有 cap。

  • Flow participation 上限 0.25。函数参与的关键执行流越多,越危险。一个函数如果在多个 critical flow 里出现,它的权重按 criticality 加权累加,不是简单计数。
  • Community crossing 上限 0.15。调用者来自不同 community 的数量。跨社区的调用是架构耦合点,改一处影响多个模块。
  • Test coverage 是最大的一项,无测试给 0.30,有 5 个以上 TESTED_BY 边降到 0.05。这条单独看就是个产品立场,没测试的代码改起来最危险,所以给最高权重。
  • Security sensitivity 0.20。函数名或全限定名里出现 security 相关关键词(authtokenpassword 之类)就加。
  • Caller count 上限 0.10。调用者数量除以 20。
  • Change frequency 上限 0.15,可选。基于 git churn,经常改的文件再改一次风险累积。

把这几个 cap 加起来,满分 1.0。注意它故意 over-predict。README 的 limitations 章节写得很坦白,「deliberately conservative,better to flag too many files than miss a broken dependency」。benchmark 表里 average precision 0.578,recall 1.0,意思是为了不漏掉任何一个真实受影响的文件,它宁可多报一半误报。这个取舍是 review 场景定的,如果是生产环境做 CI gate,你得知道这个特性,不然 fail-on-risk 会经常误触发。

增量更新,SHA-256 是关键

首次 build 一个 500 文件项目大约 10 秒。但 CRG 真正的卖点不是首次构建,是增量。2,900 文件的仓库,改几个文件后重新索引不到 2 秒。

schema.md 里 File 节点的字段,有一个 file_hash,SHA-256 of file contents(for change detection)。增量更新的逻辑就建立在它上面,整条链路是这样跑的。

# incremental.py 的核心思路
# 1. git diff --name-only 拿到变更文件列表
# 2. 对每个文件算 SHA-256,和 graph.db 里存的 file_hash 比
# 3. hash 不一致的才重新解析,一致的跳过
# 4. 重新解析后,沿着边找到所有 dependent,更新受影响节点

这里有个细节值得注意。incremental.py:559get_changed_files 先用 git diff --name-only -z 拿变更文件,再用 hash 二次过滤。为什么要双层。因为 git diff 只告诉你「文件改了」,但有时候改动是 trivial 的(比如换行符),hash 比对能进一步缩小真正需要重解析的范围。SVN 工作副本也支持,走的是 svn diff --summarize

35 种语言怎么 Cover

CRG 支持 Python、TypeScript、Go、Rust、Java、C++、Swift、Kotlin、Solidity、Zig、Julia、ReScript 这种长尾语言,加起来 35 种左右,还包括 Jupyter notebook 和 Databricks 的 .ipynb。

但如果你仔细看 parser.py,会发现它不是给每种语言都写了独立的解析器。核心是一个通用的 Tree-sitter walker,靠四张映射表驱动。

  • _CLASS_TYPES,每种语言的 AST 里什么节点代表 class
  • _FUNCTION_TYPES,什么节点代表 function
  • _IMPORT_TYPES,什么节点是 import
  • _CALL_TYPES,什么节点是 call site

加一种新语言,说到底就是往这四张表里塞东西,「这种语言的 AST 里,function 长这个样子」。更狠的是,你甚至不用改代码。在项目根目录放一个 .code-review-graph/languages.toml 就行

[languages.erlang]
extensions = [".erl"]
grammar = "erlang"
function_node_types = ["function_clause"]
class_node_types = ["record_decl"]
import_node_types = ["import_attribute"]
call_node_types = ["call"]

只要 tree_sitter_language_pack 里打包了这个 grammar,generic walker 就能处理。这个设计让语言扩展的成本几乎为零,也是它能在五个月覆盖这么多语言的原因。

不过有个诚实的边界。PHP 是个特例,CRG 给它额外做了 Composer PSR-4 命名空间解析、Blade 模板引用、Laravel 的 Route-to-controller 和 Eloquent relationship 边。这等于承认一件事,通用 walker 只能给你语法层的边,框架语义层级的边还得专门写 resolver 去抠spring_resolver.pyjedi_resolver.pytsconfig_resolver.py 这些文件的存在说明,真正深的语义理解还得 per-framework 死磕。

注入机制,这是我想重点说的

前面都是夸。现在说说我读完源码之后真正在意的事。

CRG 不是个被动工具。code-review-graph install 这条命令会主动改你的项目配置。它检测你装了哪些 AI 编程工具(Codex、Claude Code、Cursor、Windsurf、Zed、Continue、Gemini CLI、Copilot 等十几个),然后往对应的全局或项目级配置文件里写东西。以 Claude Code 为例,它往你的 CLAUDE.md 里注入这么一段(code-review-graph.instruction.md 生成),内容是这样的。

**IMPORTANT: This project has a knowledge graph. ALWAYS use the
code-review-graph MCP tools BEFORE using Grep/Glob/Read to explore
the codebase.**

注意那个 ALWAYS 和 BEFORE。它在告诉你的 Agent,先问图,再考虑读文件

再看 prompts.py:17_TOKEN_EFFICIENCY_PREAMBLE,这是 5 个 MCP prompt 模板共用的开头,内容我直接贴出来。

_TOKEN_EFFICIENCY_PREAMBLE = """\
## Rules for Token-Efficient Graph Usage
1. ALWAYS call `get_minimal_context` first with a task description.
2. Use `detail_level="minimal"` on all tool calls unless the minimal output \
is insufficient.
3. Only escalate to `detail_level="standard"` or `"verbose"` for the specific \
entities that need deeper inspection.
4. Never request more than 3 tool calls per turn unless absolutely necessary.
5. Prefer targeted queries (query_graph with a specific symbol) over broad \
scans (list_communities with full members).
6. When reviewing changes: detect_changes(detail_level="minimal") → only \
expand on high-risk items.
"""

六条规则,全是教 Agent 怎么省 token、怎么少调用。没有一条说,「当代码语义复杂时,去读源文件」。detail_level="minimal" 是默认,get_minimal_context 只有 100 token,每轮不超过 3 次工具调用。

这套 preamble 配合注入到 CLAUDE.md 的 ALWAYS 指令,叠加起来的行为是什么,你的 Agent 会被推向「信任图的摘要,少读原始代码」。

issue #314,用户自己发现了

这不是我脑补的。issue #314 标题就写得很直接,原话是下面这样。

Warning: blindly using code-review-graph can make model faster but noticeably worse at code understanding

开 issue 的人描述得很具体。默认注入的指令让 Agent 「over-trust graph summaries and miss important implementation details in source code」,结果是「shallower reasoning, more template-like edits, and weaker understanding of edge cases, recovery logic, compatibility code, and tests」。

翻译一下,Agent 变快了、变便宜了,但它的推理变浅了,给出的修改更像模板,对边界条件、恢复逻辑、兼容性代码和测试的理解变弱。

这条 issue 下面的讨论很有意思。有人问「这个 Guardrails 该写在哪」,有人回答「装完之后会有 GEMINI.md、CLAUDE.md 这些文件,把 guardrails 加到对应文件的末尾」。也就是说,用户得自己手动补一段 prompt 来对冲 CRG 注入的 prompt

这个现象本身就值得停下来想一想。一个帮你省 token 的工具,最终需要用户手动写 guardrails 防止它把 Agent 带偏。说明 token 优化和语义理解之间,存在一个没有被工具本身管理的 trade-off

CRG 的作者也不是不知道。prompts.py 里的规则在 v2.3.6 之后做了一些调整,issue #314 也在跟进。但截至我读源码的版本,_TOKEN_EFFICIENCY_PREAMBLE 仍然是六条纯省钱规则,没有任何关于「何时该放弃图摘要去读源码」的引导。

README 的数字到底怎么说

既然说到这,就得聊聊它那个 82x 的数字。

README 里写得相当诚实(这点值得给好评)。528x 是 fastapi 那一个仓库的 best case,不是典型值,中位数是 82x,区间 38x 到 528x。但更关键的是它自己接下来的这段话。

The whole-corpus baseline above is an upper bound no real Agent pays: a competent Agent greps for identifiers and reads only the best-matching files.

翻译过来,「拿整个代码库当 baseline 是个上界,没有真实 Agent 会这么做,一个合格的 Agent 会先 grep 标识符再读最匹配的几个文件」。CRG 自己也承认这一点,所以它还跑了一个 agent_baseline benchmark,模拟「grep top-3 文件」的真实基线,而不是 naive 的 whole-corpus。

这意味着 82x 这个数字的实际参考价值要打折扣。它衡量的是「读整个代码库 vs 查图」的差距,不是「一个会用 grep 的 Agent vs 查图」的差距。后者的差距会小很多。FAQ 里甚至直说,「For single-hop lookups in a small repo, grep is cheap and good. The multi-hop review workflow is where the graph earns its keep.」,单跳查找小项目,grep 就够了,CRG 真正发力是在多跳的 review 流程。

这条诚实是双向的。它既说明了 CRG 的真实价值区间(多跳、大型、长期项目),也提示了它的局限(单跳、小型、一次性任务)。

什么时候用,什么时候别用

把它和同类工具摆一起看会更清楚。

工具方法持久化外部依赖review 专用
code-review-graphTree-sitter AST → 结构图本地 SQLite,增量更新核心无,embedding 可选是,blast radius + risk score
SerenaLSP 支撑的符号工具语言服务器状态每种语言一个 LSP否,通用 coding Agent 工具
claude-contextchunk + embed 语义搜索向量库embedding provider + 向量库否,搜索向
repomix整个仓库打包成一个文件无,每次重新生成Node.js否,一次性上下文打包

CRG 的位置很清楚,持久化的结构图,专门为 review 场景做的。Serena 用 LSP,单符号精度更高但跨语言不行;claude-context 是语义搜索,答不了「谁调用谁」;repomix 是粗暴打包,适合小仓库塞进 context window。

CRG 自己的 FAQ 也给了「不要用」的场景,写得挺清楚。

  • 几百文件以下的小仓库,Agent 本来就能 hold 住,图的结构元数据反而是负担
  • 单文件 trivial 改动,图的响应(带 impact edge 和 source snippet)可能比裸读文件还贵 token
  • 一次性问题,不会复访的仓库,build 成本摊不回来
  • JS/Go 的 flow detection(33% recall,README 自己标的),目前主要在 Python 框架模式上可靠

一个判断框架

拆完这么多,我想提炼一个比「该不该用 CRG」更通用的判断。

当一个 AI 编程工具同时控制**「给 Agent 看什么」和「不让 Agent 看什么」,它就在做两件本来应该分开的事。给什么,是检索问题,可以用图、用向量、用 grep,哪个好用哪个。不让看什么,是信息过滤**,它隐含一个假设,「被过滤掉的东西 Agent 不需要」。但代码理解的难点恰恰在于,最重要的细节往往藏在看起来不相关的文件里,一个边界条件、一段兼容性代码、一个看似无关的测试,这些恰恰是图结构检索会优先丢弃的,因为它们不在「爆炸半径」内。

CRG 的 prompt 注入把这两件事捆在了一起。它一边给 Agent 提供了图(好事),一边主动告诉 Agent 优先信图、少读源码(有代价的事)。正确的用法应该是把它们拆开,这么做三件事。

  • 用图做范围收敛,回答「这次改动可能影响哪些文件」
  • 然后让 Agent 去读这些文件的实际实现,尤其是边界条件、恢复逻辑、测试
  • 图和源码冲突时,信源码

这正好是 issue #314 那位用户总结的 guardrails。

所以一个更通用的判断是,context 优化工具应该管「检索」,不应该管「信任」。它应该说「这里有相关的 15 个节点」,而不应该说「你看这 15 个就够了,别再去读别的了」。前者是放大 Agent 的视野,后者是替 Agent 做了它本来最擅长的事,读代码然后自己判断。

CRG 的工程实现是漂亮的,bounded relaxation 算法、SHA-256 增量、6 因子 risk score、35 种语言的通用 walker,这些都值得学。但它对 prompt 的激进注入提示了一个边界,工具越深地介入 Agent 的决策链,就越要谨慎地声明「我只是建议,不是裁决」。下次你看到任何宣称「让 Agent 少读 X% 代码」的工具,都可以用这条标准去量一下,它是在帮你检索,还是在替你判断。


项目地址在 https://github.com/tirth8205/code-review-graph,MIT 协议,本地优先,不 phone home。如果你做的是几千文件以上的长期项目,每天有大量 review 工作流,值得装上试试,记得把 issue #314 的 guardrails 也一起抄进你的 CLAUDE.md。如果你只是偶尔让 Agent 看个小项目,grep 就够了,别给自己增加心智负担。

评论互动

© 2026 王若风的技术博客 · Powered by Astro