跳到正文

Graft

它用 tree-sitter 读一遍仓库,把读到的内容写成一组互相链接的 markdown 节点加一份逐符号的代码图,再把匹配到的片段交给你正在用的那个编码 agent——或在提示里先递过去,或通过六个 MCP 工具由 agent 自己来取。

Screenshot of Graft
编辑截图, 30 Sep 2026Graft ↗

这是什么

Graft 是一个给编码 agent 用的命令行上下文层,TypeScript 写的,MIT 许可。它把仓库读两遍:一遍是确定性的 tree-sitter 解析,产出带跨文件调用与 import 边的逐符号代码图;另一遍是可选的模型层,为每个文件写摘要,再把这些摘要归并成几十个互相链接、用固定动词连边的 markdown 节点。结果落在一个被 gitignore 的目录里;每次查询先用上一次构建的指纹比对工作区——没动过时大约三毫秒——所以未提交的改动也能被如实描述。agent 有三条取用路径:init 命令写进九种宿主 agent 的围栏段落或独立 skill 文件;六个 MCP 工具由 agent 自己调用;Claude Code 另有钩子,在每次提示前注入匹配节点,在文件改完后打印它的爆炸半径。深度摘要用使用者自己的 key,结构化那张图则完全不调用模型。

谁做的仓库归 trailhq 这个组织账号所有,所以这里写的是贡献最多的人,不是所有者:512 次提交里 Shrish Dwivedi 写了 235 次、Anirudh Kumar 160 次,其后是 Dependabot 25 次、Alex Matthews 25 次、Frankie-Xu 20 次。贡献者列表里一共 36 个账号,111 条共同作者尾注里另有约十个名字,多半只出现一次。这个项目是从 NanoNets 名下的仓库长出来的——npm 包至今仍发布在 NanoNets 的 scope 下,仓库自己的开发说明也仍然让人去 clone NanoNets/context-graph-engine。

它是怎么搭起来的

组成 · 6

它的核心主张是:代码库的地图应该是一叠 agent 像读仓库里任何文件那样去读的文件,而不是一个它去查询的索引。一次构建产出两张图:一张由 tree-sitter 得到的确定性逐符号代码图,不需要 key 也不需要网络;另一张是可选的、由模型写的 markdown 节点图,只在显式开启的深度层里生成。检索于是有三扇通向同一批产物的门——一份点明这张图、并告诉 agent 该怎么再问一次 的指令或 skill 文件;六个由 agent 自己调用的 MCP 工具;以及 Claude Code 的钩子,在提示之前注入匹配节点、在改动之后打印爆炸半径。新鲜度放在查询时处理而不是交给守护进程,因为解析便宜、又按内容哈希缓存,所以每条命令都负担得起「先拿工作区和上一次构建的指纹比一次」。这里没有按宿主分家的东西:接线层是一张覆盖九种宿主 agent 的读写注册表,这也是为什么同一段指令必须同时以「用户自己文件里的围栏段落」和「宿主期望的、完全属于 graft 的规则或 skill 文件」两种形态存在。

src/graph/
引擎,也是仓库里最大的目录:45 个文件、455 KB,领头的是一份 118 KB 的提取器、34 KB 的 binding 代码、33.6 KB 的符号解析和 29.7 KB 的工作区处理;queries/ 下有十六个 tree-sitter 查询文件,另有提取缓存、指纹、不变量、可选的语言服务器增强,以及回答 callers、skeleton 和爆炸半径问题的几个外壳。
src/cli.ts 与 src/ai/
整个命令面都塞在一个 88 KB 的文件里,是这里最大的单个文件;旁边是 Anthropic、OpenAI 兼容端点、LiteLLM 和 OrcaRouter 的提供方适配器、一个用于抢救结构化工具调用的模块,以及 src/context/ 下的上下文组装、计价与节省量计算。
test/
138 个文件、1,169 KB,除 src/graph 之外比任何源码目录都大,而且构成不寻常:test/ask.test.ts 有 52 KB,Java 那张图的测试 53 KB;另有三份文件是探针而不是断言——一个审查流程探针、一个参考规模探针、一个 Node 24 探针。
src/brain/ 与 src/app/
二十八个文件、约 274 KB,是挂在 Trail 上的托管那一半:注册、推送、拉取、上传、监听、分块传输、GitHub App、服务端和审查 worker,外加一份 10 KB 的 github-app 文档和一个 App Runner 部署脚本。
src/telemetry/ 与 TELEMETRY.md
十一个文件围绕一个 13.8 KB 的契约模块:落盘队列加每日冲刷、开关、身份与告知模块,以及一份 10.8 KB 的契约测试——「匿名统计」这个承诺是被测试钉住、被文档写清的产物,而不是脚注,另有一个在构建时写入 key 的脚本。
viewer/、.github/ 与 assets/
一个八文件的预构建查看器,由 graft viz 提供;六个工作流,包括一个 15 KB、把每个 pull request 的爆炸半径页面发到 GitHub Pages 的发布器和一个 9.3 KB 的复合 action;以及 40 MB 的演示素材,其中最大的一份是 17.6 MB 的改动后钩子录像。

取舍,以及它替代了什么

  • 用一叠 markdown 文件当那张图 替代 向量与相似度索引

    README 的论证是:agent 应该像读仓库里任何其它文件那样去打开、grep 和跟随这张图——不用 embedding、不做相似度检索、没有需要焐热的索引。每个节点都带着白话摘要、从源码里摘出的 crux、带哈希的来源和带类型的 wikilink,理由是「只给地址」的地图仍然会把 agent 打发去读原文件。

  • 默认走确定性的 tree-sitter,模型层放在显式开关后面 替代 把那张图做成模型的产物

    普通的构建、check 和 ask 不需要 key 也不需要网络,而由 Claude Code 钩子驱动的自动同步被明确写成从不自己调用模型。深度层是可选的,提供方按环境变量或命令行参数选择,OpenAI 线路格式可以指向 OpenRouter、Fireworks、Groq、一个 LiteLLM 代理或本地服务。

  • 每次查询前先刷新 替代 由守护进程维护一个热索引

    什么都没动时,拿上一次构建的指纹比一次大约三毫秒,于是答案可以描述带着未提交改动的工作区,也没有陈旧索引要照看。例外是刻意的:graft check 从不刷新,因为它就是漂移报告,图落后了就退出 1。

  • crux 存源码文本本身 替代 存一个行号区间

    README 用一句话写明:上面无关代码一挪,行号就漂,而真正要紧的那几行不会漂;存文本而不是存数字,文件在它周围变动时这段摘录依然是对的。

  • graft/ 是被 gitignore 的本地缓存 替代 把图提交进仓库

    它的说法是像 node_modules 一样可重新生成,所以团队共享的是 init 写下的接线,每个队友自己构建自己的图。这个选择的代价写在 issue 列表里:用户反对的正是「构建悄悄把这份策略本来指望被提交的接线文件加进了 .gitignore」。

  • 生成段落加围栏,且绝不碰用户的 CLAUDE.md 替代 把指令当成一个完全归项目所有的文件来写

    被选中的 agent 拿到的是共享指令文件里的围栏段落,或者一份完全归 graft 的规则/skill 文件;Claude Code 拿到自己的 skill 文件,用户的 CLAUDE.md 原样不动;而没有终端可以询问时,init 什么都不写,只打印出该跑的命令。

依据README 全文(从 raw.githubusercontent.com 取得,44,245 字节),重点是「图是怎么建的」「节点里有什么」「什么在哪里跑」「agent 接入」「CLI 参考」「搜索与定位」「monorepo 与多仓库目录」以及基准附录几节。另有 recon 报告的 357 个文件树及其体积、两级目录汇总,以及写明并发开关与模型适配器改动理由的 pull request 正文。

制作过程

6 个阶段
  1. 01

    十三周、两个人、十四个 tag

    仓库建于 2026-07-03,第一次提交叫 Initial commit: Context Graph Engine;到 2026-09-30 已有 512 次提交——7 月 243 次、8 月 213 次、9 月 56 次,正是第一波冲刺在项目转入维护之后慢下来的形状。绝大部分由两个人写完:Shrish Dwivedi 235 次、Anirudh Kumar 160 次,后面是 Dependabot 25 次、Alex Matthews 25 次、Frankie-Xu 20 次。贡献者列表里一共 36 个账号,111 条共同作者尾注里另有约十个名字,多半只出现一次:其中 47 条署名 Claude 系列模型、20 条写 Cursor、25 条是依赖机器人。版本以 tag 的形式存在而没有 release——GitHub 上一个 release 都没有——tag 从 v0.7.1 起,经 v0.8.1、v0.8.2、v0.9.0、v0.12.1 一路到 v0.21.1,其中 v0.10、v0.11、v0.17 没有留下记录,另有 39,765 字节的 CHANGELOG.md 承担发布说明的角色。升级路径刻意安静:CLI 每天查一次 npm,有新版本就提示,装完之后的下一个会话会自己把仓库里的接线刷新一遍。这个项目也在自己身上用自己,而且看得见——每个 pull request 下面都有机器人贴出的爆炸半径图和一张托管页面,blast 工作流、它的缓存工作流、15 KB 的 Pages 发布器和一个 9 KB 的复合 action 就是为此存在的。周围是 9,431 个星、865 个 fork、188 个未关的 issue。

  2. 02

    两层索引、两级缓存、一次三毫秒的比对

    理解分两层做,而这两层的成本毫无关系。第一层是确定性的 tree-sitter:每个函数、每个类、每条调用边,不用模型也不用 key,写进 graft/.graph/wiring.json,外加一份镜像源码树的逐文件卡片。可选的深度层为每个符号补一行摘要和一段 crux 摘录,再把逐文件摘要归并成用固定动词连边的概念节点。所有环节——包括解析本身——都按内容哈希缓存,所以 README 给出它自己这个仓库的数字(124 个文件)是:冷启 0.74 秒,改一个文件后 0.18 秒,什么都没改也是 0.18 秒;--no-reuse 用来强制走冷路径。这份便宜买下的正是检索的设计:与其用守护进程焐着一个索引,不如让每次查询先拿上一次构建的指纹比对工作区——约三毫秒、结构化、不调用模型——只有字节真的变了才重建,于是 ask、grep、callers、skeleton、map 对未提交和未暂存的改动一视同仁,也于是有了 GRAFT_REFRESH=hash,给不肯相信体积与修改时间的人用。graft check 是刻意的例外:它从不刷新,发现漂移就退出 1,因为它就是那份漂移报告。注入走的是同一批产物,共三扇门——init 命令按九种宿主 agent 写下的围栏段落或独立 skill 文件;六个由 agent 自己调用的 MCP 工具;以及 Claude Code 的钩子,在每次提示时探一次,在文件改完后打印爆炸半径。

  3. 03

    速度与省钱的说法,以及每个数字怎么测的

    最上面那句话是项目自己的:最高便宜 4 倍、快 3 倍,而旁边就写着方法,来自三组实验。第一组让同一个 Claude Sonnet 5 agent 跑三种变体、用同一套文件工具——冷启、把 graft ask --source 的结果先推上去、以及只给 MCP 工具的拉取变体——由 Opus 4.8 当裁判、带必含关键词的下限,成本按缓存计价(读约 0.1×、写 1.25×),共 162 次运行、两个仓库、每个三次。它报出的每任务成本从 $0.0429 降到 $0.0292(+32%),token 从 8,070 降到 4,650(+42%),工具调用从 4.2 降到 2.3(+46%),延迟从 39.8 秒降到 15.8 秒(+60%),正确率两边一样是 93%;而什么都不注入的拉取变体到了 98%。第二组是 SWE-bench Verified:50 个实例、两边同一模型、官方 4.1.0 评分器——50 个里解决 27 个对 33 个,142.0M token 对 109.4M,$52.34 对 $42.43,1,370 次工具调用对 1,031 次,13,094 秒墙钟对 8,922 秒。第三组把 PocketBase 五个已合并的 pull request 从各自基线提交重做一遍,共 15 个任务:$13.91 降到 $11.02,2,044 秒降到 1,762 秒,五个全部复现。README 顶上那张表把前两组混在了一起——它那一行正确率 54% 到 66% 来自 SWE-bench,而受控对比里正确率没有变化。这些数字没有任何一个是第三方复现过的。

  4. 04

    用户测回来的东西

    最近三十个 issue 里有几个不是 bug 报告而是反向测量,也是整份历史里最锋利的部分。一位用户在 291 个文件的 Swift 与 Kotlin 应用上拿 graft ask --source 和自己「一次 ripgrep 加一次定点阅读」的基线对比:构建 3.7 秒,得到 3,217 个节点和 9,743 条边,三个问题里正确答案都在结果包里,但排第一的命中只有一个是对的,而且包的体积是 grep 路径的 2.5 到 4 倍——一条带着数字的排序投诉。另一位把 331 次钩子输出和触发它们的提示一一配对,发现注入的那些「没有强匹配」里多数来自机器生成的后台任务通知,而不是用户写的提示。第三位记录的是一个装着若干仓库的普通文件夹(从未被索引):每一轮都把它标脏,每一次 Stop 都触发一次撞上 120 秒超时的构建——持续占着 0.8 个核和 2.5 GB 内存,统计文件里的 syncedAt 是 null、节点数为零。这一条还带着材料里最坦白的一句话:被旧版本污染过的文件夹会被一直认作已接入,所以新的门槛照样放它过去,除非手动删掉那个残留缓存。

  5. 05

    会覆盖用户手改内容的生成文件

    最响的两个 bug 共用一个成因:graft 会往并非完全属于它的文件里写。第一个里,会话启动时的接线刷新用一个裸 JSON.parse 读 .claude/settings.json,解析失败就落进空对象,然后再用截断写回合并结果;于是任何留着合并冲突标记的文件——或在另一个写入者持有时被读到的文件——回来时用户的 permissions.deny、permissions.allow、outputStyle 和钩子全没了,而且改动足以被提交。修它的那个 pull request 直接删掉了这条兜底,理由是:解析失败的 settings 文件决不能被覆盖。第二个是一位用户一口气开了七个 issue,全是关于生成的指令段落:重新生成会无声地回滚 marker 围栏里手写的更正;删掉生成的 ignore 文件会在同一个会话里被自动恢复;graft build 把接线文件追加进 .gitignore,而它的输出只提到缓存目录;模板指向 graft/INDEX.md,构建写出的却是 graft/index.md——在任何大小写敏感的文件系统上都是死路径;而那份 ignore 文件用一个没锚定的模式又把卡片树放了回来。节点文件恰恰是按这种压力设计的:用户写在生成块下面的任何内容都能活过重新生成。

  6. 06

    适配器、一个被关掉的 pull request,和先查指标再上功能

    这些 pull request 里有很大一部分是同一个题材:让模型网关听话。一条报告说,OpenAI 兼容网关会用 HTTP 200 返回一个错误对象、里面没有 choices,这让响应解析器抛出 TypeError;另一些模型根本吃不下强制的 tool_choice,于是文件和概念批次都是空的——用 OpenRouter 上的一个隐身模型复现过。紧接着的一条专门处理推理模型在长度上限处被截断的结构化调用:每一档重试一次,把 token 额度乘四、最多 32,768,并且不再把这种停止原因当成「该端点忽略强制 tool_choice」的证据。再一条修的是 crux 环节:有些模型把整行目标原样回填成标识符,于是所有摘要都被悄悄丢掉。最新的一条把概念综合批次并行化,用一个新的 --synth-concurrency 开关、默认 4;它特地为第二个旋钮辩解,因为第一阶段是几百次小的逐文件调用,第二阶段只是几次 48,000 字符的请求——图与串行跑出来的结果逐字节相同。这些周围是维护者的习惯:一个把 SQLite 统计库、五个宿主归因适配器和抬高 Node 版本下限捆在一起的 52 文件 pull request 被直接关掉,要求先开 issue,再拆成保住 Node 20 下限的小改动发过来;给托管的 Trail 流程加的遥测事件只带封闭集合和分桶,并被一份契约测试钉住;而在加「有多少建议在等你审」这个计数之前,作者先查了数据库,七天内这样的事件是零、唯一的动静来自一个内部测试者,然后照样发了。

相关档案

全部档案 →