## 目录
- [快速开始](#quick-start)
- [问题背景](#the-problem)
- [Graft 的作用](#what-graft-does)
- [图是如何构建的](#how-the-graph-gets-built)
- [节点的构成](#whats-in-a-node)
- [运行机制](#what-runs-where)
- [Agent 集成](#agent-integration) — [MCP 服务器](#mcp-server) · [Claude Code (深度集成)](#claude-code-deep-integration)
- [命令行接口](#cli)
- [搜索与定向](#search--orient-graft-grep--graft-map) (`graft grep` / `graft map`)
- [Monorepos 与多代码库文件夹](#monorepos--multi-repo-folders)
- [可视化](#visualize-it-graft-viz) (`graft viz`)
- [基准测试](#benchmark)
- [在热门代码库上的测试](#tested-on-your-popular-repos)
- [开发指南](#development)
- [许可证](#license)
## 快速开始
```
npm install -g @nanonets/graft # install the CLI, once
graft init # build the graph + wire it into Claude Code
```
这就是设置的全部内容。`graft init` 会询问您要连接哪些编程 agent,根据您的代码构建 `graft/` 目录,并将状态栏和钩子注入到 `.claude/` 中。因此,从下一次会话开始,Graft 将在 Claude Code 中协同工作:它会将匹配的节点拉取到每个 prompt 中,并在每轮对话后在后台重建图。默认情况下无需 daemon,无需记住重新索引,也无需运行或维护任何东西——图仅仅是文件。
在您做出选择之前,不会写入任何内容。运行 `graft init --dry-run` 可以首先查看它会触及的每个文件,或者运行 `graft init --agents claude` 跳过提示并仅连接 Claude Code。
`graft build` 会自动将 `graft/` 添加到您的 `.gitignore` 中——图是一个本地、可重新生成的缓存(就像 `node_modules` 一样),而不是您需要提交的内容。您共享的是 `init` 注入到 `.claude/` 中的连接配置;每个团队成员运行 `graft build` 来生成他们自己的图:
```
git add .claude && git commit -m "wire in graft"
```
不想全局安装?`npx @nanonets/graft init` 的效果是一样的。
## 问题背景
每次任务开始时,您的编程 agent 都是盲目启动的。在它更改任何内容之前,它会重新探索代码库:grep 一个术语,打开一个文件,追踪一个 import,退回,重试。它正在重新构建一个小时前映射过但又丢弃的代码库全貌。这种重新发现过程消耗了运行过程中大部分的工具调用、tokens 和延迟,而且这纯粹是开销:
- **不断重复。** 每次任务都要从零开始付出探索成本。
- **用后即弃。** agent 搞清楚的任何内容都会随会话一起消亡。
- **无法共享。** 下一个团队成员及其 agent 也要从零开始。
人类只需熟悉一次代码库。而 agent 每次都要重新熟悉。
## Graft 的作用
Graft **一次性**构建起这种理解,并将其作为包含相互链接的 markdown 文件夹写入您的代码库中,每个系统、API 或概念对应一个节点。
- **真正的解释,而不是符号列表。** 每个节点都用通俗易懂的英文说明系统的某一部分是做什么的,以及它如何与其余部分连接,就像资深工程师讲解的那样。这正是 agent 真正需要的部分,以便它可以跳过探索。它绝不是函数名的简单堆砌。
- **可读的真实图谱。** 没有 embedding,没有相似性搜索,也没有需要保持活跃的索引。图是一组链接的文件,您的 agent 可以像读取代码库中的任何其他文件一样打开、grep 和追踪它们。
- **接入 git。** 图仅仅是 `graft/` 中的文件。提交它,任何 clone 该代码库的人都能拥有它。没有数据库,没有服务器,无需设置。git 负责同步,过时的图会在代码审查中显示为 diff,而不是在某个外部存储中逐渐腐坏。
- **diff 与代码同在。** 当更改引起文件变动时,您可以在同一个 pull request 的图 diff 中看到它,就在导致该变动的代码旁边。
- **您的 provider、您的密钥、您的模型。** 摘要由您选择的任何 provider 编写——OpenAI、Anthropic(原生)、OpenRouter、Fireworks、Groq、LiteLLM 代理或本地模型——并使用您自己的密钥。结构性代码图(`graft build`、`graft check`)是确定性的 tree-sitter,根本不会调用任何模型。
## 图是如何构建的
Graft 通过两个步骤构建图,这两个步骤均由语言模型驱动:
1. **读取每个文件。** 每个源文件都会被概括成一段描述其功能的简短说明。
2. **分组成节点。** 这些摘要被分组成经过精选的节点集(子系统、关键文件和概念),并在它们之间建立带有类型的链接。Graft 会为您选择合适的详细程度,而不是每个文件生成一个节点,因此大型代码库会变成几十个可读的节点。
```
flowchart LR
S[Source files] --> T["Tier 1 — tree-sitter
no model, no key"]
S --> P1["Pass 1 — LLM summarizes
each file (--deep)"]
T --> W["graft/.graph/wiring.json
per-symbol code graph"]
P1 --> P2["Pass 2 — group into nodes
+ typed links"]
P2 --> N["graft/*.md
markdown node graph"]
```
每次步骤都会根据内容哈希进行缓存——无论是 LLM 生成的部分还是 tree-sitter 解析的部分都是如此。重新运行只会触及已更改的文件,因此第二次构建既快又便宜(在这个代码库中,124 个文件:冷启动 0.74 秒,修改一个文件后 0.18 秒,无修改时 0.18 秒)。`graft build --no-reuse` 强制进行冷重新解析。
这种低成本使得 **每次查询都能在回答之前刷新图**。检索调用会将树结构与上次构建的指纹进行对比(约 3 毫秒),仅当发生变动时才重新构建——因此 `ask`/`grep`/`callers`/`skeleton`/`map` 描述的是当前的代码状态,包括尚未提交到 git 的编辑内容:未提交、未暂存或已暂存对 graft 来说都是一样的,它完全不读取 git。这种刷新是结构性的且花费 `$0`;它从不调用 LLM。可以使用 `--no-refresh` 针对特定命令关闭它,或者使用 `GRAFT_NO_REFRESH=1` 全局关闭。
除了 markdown 图谱,`graft build` 还会构建 `graft/.graph/wiring.json`——一个按符号划分的代码图——以及一个镜像您源代码树的按文件划分的连接卡片。第一层是纯粹的 tree-sitter(每一个函数、类和调用边;确定性、无模型、无网络),这就是为什么普通的 `graft build` 不需要密钥。`--deep` 步骤会为每个符号添加一行摘要和关键代码片段,并按函数体哈希进行缓存。
## 节点的构成
节点是一个单一的 markdown 文件。大多数代码映射只停留在一个地址上:这个东西存在于那个文件、那一行。这只告诉 agent 去哪里找,而不是它会找到什么,所以它仍然需要打开源代码并阅读。Graft 节点将含义内嵌其中,因此 agent 可以预先学到所需知识,仅在需要更多内容时才打开文件。
每个节点包含:
| 部分 | 包含内容 |
|---|---|
| **摘要** | 模型编写并缓存的关于代码功能的通俗英文解释。无论代码以前是否有文档记录,它都在那里,并且会在源代码更改时重新生成。 |
| **核心** | 真正承载逻辑的那几行代码:守卫语句、跳过条件、状态改变。直接从源代码中提取并内嵌存储,因此 agent 可以看到它是*如何*工作的,而不仅仅是知道它的作用。 |
| **来源** | 构建该节点的确切文件,每个文件都由内容哈希进行追踪,因此 Graft 可以精确判断节点何时过时。 |
| **链接** | 到其他节点的类型化连接(`depends_on`、`part_of`、`uses`、`implements`、`produces`),编写为您的 agent 可以追踪的 `[[wikilinks]]`。 |
| **备注** | 您在生成块下方编写的任何内容。它会在重新生成时保留,因此您自己的上下文永远不会被覆盖。 |
这就是在一个文件中包含的三个深度:摘要说明代码做*什么*,核心展示*如何*做,如果 agent 需要更多信息,来源指向其余部分。普通的索引会让它读取整个文件来学习一件事。而 Graft 节点将答案内嵌其中,后续的读取通常根本不需要发生。
核心被特意存储为代码本身,而不是行范围。每当它们上方的无关代码发生变动时,行号都会偏移,但重要的代码行不会。保留文本而不是数字,意味着即使周围的文件发生移动,核心依然保持正确。
_摘要、来源、链接和备注目前在 markdown 节点中可用。核心在代码图中按符号提供(`graft build --deep`);将其内联到 markdown 节点是接下来的计划。_
## 运行机制
- **在您的机器上运行,无需密钥,无需网络:** 结构化代码图。`graft build`(连接图 + 按文件划分的卡片)、`graft check` 和 `graft ask` 都是确定性的 tree-sitter——它们从不调用模型。
- **通过您的 provider 密钥:** LLM 编写的部分——`graft build --deep` 会添加概念节点(文件摘要 + 节点合成)以及按符号划分的摘要和核心。graft 是供应商中立的:设置 `GRAFT_PROVIDER`(对于任何兼容 OpenAI 的 endpoint 设为 `openai`,或者原生 API 设为 `anthropic`)、您的 `GRAFT_API_KEY`、`GRAFT_MODEL`,以及——对于 `openai` 网络格式——设置 `GRAFT_BASE_URL` 指向 OpenRouter、Fireworks、Groq、LiteLLM 代理、本地服务器或 OpenAI 本身。或者在命令行中传入 `--provider/--model/--api-key/--base-url`。(`OPENROUTER_API_KEY` 仍然可以作为已弃用的回退方式使用。)
- **无遥测**,无分析——唯一的网络调用就是您配置的 LLM 请求。
有关设置的完整列表(模型、base URL、图目录),请参见 [`.env.example`](.env.example)。
## Agent 集成
只需一条命令即可将 Graft 接入您使用的编程 agent:
```
npx @nanonets/graft init
# 检测你的 agents 并为每个写入其原生 instruction 文件;
# Claude Code 还会获得下方的实时 statusline + hooks
```
在终端中,`init` 会显示它了解的每一个 agent——标记出它检测到的那些(通过它们的配置目录),并列出每个 agent 将要写入的确切文件——并且仅连接您选择的那些。Claude Code 会被预选;其他则不会。被选中的 agent 会在其共享的指令文件中获得一个带有标记的 Graft 部分——`AGENTS.md`(Codex、OpenCode 和其他读取它的 CLI 工具)、`GEMINI.md`、`.github/copilot-instructions.md`——或者对于使用专属文件的 agent,会获得一个完全专属的规则/技能文件——`.cursor/rules/graft.mdc`、`.kiro/steering/graft.md`、`.windsurf/rules/graft.md`、[AdaL](https://adal.sylph.ai) 的 `.adal/skills/graft/SKILL.md`(渐进式披露技能,与下面的 Claude Code 技能形状相同)。重新运行只会更新 Graft 自己的部分(或替换专属文件),绝不会触及您其余的内容。
在没有 TTY 可供提示的情况下——例如 CI、Dockerfile、管道 shell——`init` 不会写入**任何内容**,而是打印出要运行的命令。传入 `--agents
` 或 `--yes` 以使脚本运行变得明确。
| 标志 | 效果 |
|---|---|
| `--agents ` | 仅连接这些,无提示——id 包括:`agents`、`cursor`、`gemini`、`copilot`、`kiro`、`windsurf`、`adal`、`claude` |
| `--yes`、`-y` | 跳过提示并连接**检测到的** agent |
| `--dry-run` | 打印 `init` 将触及的每个文件,然后退出而不写入 |
| `--all-agents` | 为每个已知 agent 写入指令文件,无论是否检测到 |
| `--no-agents` | 仅连接 Claude Code;跳过其他 agent |
| `--list-agents` | 打印已知的 agent id 并退出 |
| `--no-mcp` | 跳过 MCP 服务器注册 |
| `--no-hooks` | 跳过钩子安装 |
| `--no-global` | 跳过在此代码库之外的写入(即下方的 `~/.codex/` 条目) |
#### 代码库之外的写入
选择 `agents` 主机时,如果存在 `~/.codex/`,还会触及您**用户级别**的 Codex 配置:
| 路径 | 更改内容 |
|---|---|
| `~/.codex/config.toml` | 注册 Graft MCP 服务器 (`[mcp_servers.graft]`) |
| `~/.codex/hooks/graft/graft-hooks.cjs` | 编辑后钩子垫片 |
| `~/.codex/hooks.json` | 匹配 `Write\|Edit\|MultiEdit` 的 `PostToolUse` 条目 |
这两个配置都是用户级别的,因此它们适用于您用 Codex 打开的**每个**代码库,而不仅仅是这一个。选择器将这些标记为 `machine-wide`,`--dry-run` 会在它们自己的部分列出它们,而 `--no-global` 会跳过它们,但仍然会连接 `AGENTS.md`。
### MCP 服务器
`graft init` 还会向支持它的 agent 注册 Graft 的 MCP 服务器,因此这六个工具会原生显示,不需要 shell。Claude Code 也会获得此功能:`graft init` 会将服务器写入项目的 `.mcp.json` 中(重启 Claude Code 即可加载)。使用 `--no-mcp` 跳过;使用 `graft mcp [dir]` 手动运行。
| 工具 | 接收参数 | 用途 |
|---|---|---|
| `graft_find_code` | 一个问题 | 带有文件:行号、内联源代码的排序节点——通常是完整的答案,无需后续读取。 |
| `graft_file_api` | 文件路径 | 该文件中的所有签名,无函数体——以十分之一的 token 获取 API 接口面。 |
| `graft_trace_calls` | 一个符号 | 谁依赖它,或者使用 `direction: out` 指定它依赖什么,N 层深度追踪影响范围。 |
| `graft_find_all` | 一个 regex | 每一个匹配项,按封闭符号分组,按该符号的耦合程度排序。 |
| `graft_repo_map` | 无 | 第一次查看陌生代码库:目录集群、枢纽、热点。 |
| `graft_check_freshness` | 无 | 本地图是否与代码产生了偏差。 |
如果您的 agent 需要显式指定,可以手动注册它:
```
{ "mcpServers": { "graft": { "command": "npx", "args": ["-y", "@nanonets/graft", "mcp"] } } }
```
如果 CLI agent 支持用户级别的 `hooks.json`,`init` 还会安装 Graft 的编辑后钩子——影响范围警告和编辑后自动进行 `$0` 图重同步(使用 `--no-hooks` 跳过)。
### Claude Code(深度集成)
`graft init` 始终会连接 Claude Code,并且 Claude Code 获得的不仅是指令文件。从那时起,在该代码库中打开的任何 Claude Code 会话都将获得:
- **实时状态栏** —— 图大小、% 丰富度,以及当代码领先于图时出现的 `⚠ N stale` 警告
- **自动同步** —— 每次 graft 查询都会首先将图更新到最新状态,因此答案总是描述当前的代码,包括未提交的编辑。查询仅刷新其读取的内容;`graft/` 下的 markdown 是由一轮对话结束后触及代码的后台重建来刷新的。两者都是结构性的且花费 `$0` —— 自动同步绝不会自行调用 LLM
- **随时可用的上下文** —— 每个 prompt 都会将匹配的节点拉入会话;编辑文件会呈现依赖它的内容(“影响范围”);新会话从代码库映射开始
edit a file → blast radius appears inline → graph auto-resyncs → confirmed in graft viz
`graft init` 是幂等的,并且永远不会破坏您现有的 `.claude/settings.json`——它只会合并自己的块,其余部分保持不变。想要 LLM 摘要?可以在任何时候运行 `graft build --deep`(需提供密钥);自动同步永远不会替您做这件事。
## 命令行接口
```
graft build [dir] # build graft/ from the code at [dir]: wiring graph + per-file cards (no LLM, no key)
graft build --deep # add the LLM layer: concept nodes + per-symbol summary/crux (cached)
graft build --extensions .ts .py # only include these code extensions
graft build --no-reuse # re-parse every file instead of replaying unchanged ones from cache
graft ask "" [dir] # query the graph — ranked nodes + exact file:line (no LLM, no key)
graft ask "" --json # machine-readable result
graft ask "" --in # narrow to one sub-project of a monorepo/multi-repo folder (see below)
graft skeleton [dir] # every signature in one file, no bodies — the API surface for ~1/10th the tokens (no LLM, no key)
graft callers [dir] # who calls/references/imports/implements/extends a symbol (no LLM, no key)
graft callers --direction out # the reverse: what the symbol itself calls/references (was `graft callees`)
graft callers -d N # walk transitively out to depth N — full blast radius (was `graft impact`)
graft grep "" [dir] # exhaustive regex search over indexed files, grouped by enclosing symbol (no LLM, no key)
graft grep "" --in # narrow to files whose path contains this substring
graft grep "" -i --fixed # case-insensitive; treat the pattern as a literal string, not a regex
graft map [dir] # token-budgeted repo orientation — dir clusters, hubs, hotspots (no LLM, no key)
graft map --max-dirs N # raise/lower the number of directories shown
graft check [dir] # fail (exit 1) if graft/ has drifted from the code (never auto-refreshes — it's the drift report)
graft check --json # print the drift report as JSON
# 如果工作树发生了移动,ask / skeleton / callers / grep / map 都会首先刷新 graph:
# --no-refresh # 直接根据磁盘上 graph 的原样给出答案
# GRAFT_NO_REFRESH=1 # 对于每条命令都一样
# GRAFT_REFRESH=hash # hash 每个文件,而不是信任 size+mtime
graft viz [dir] # see the graph: serves an interactive viewer on localhost
graft viz --port 5000 --no-open # pick a port; don't auto-open the browser
graft init [dir] # pick which agents to wire (prompts on a terminal; writes nothing until you choose)
graft init --dry-run # list every file it would touch, then exit
graft init --agents cursor kiro # wire only these agents, no prompt (ids: agents, cursor, gemini, copilot, kiro, windsurf, adal, claude)
graft init --yes # no prompt; wire every detected agent
graft init --no-global # skip writes outside this repo (~/.codex/ config + hooks)
graft init --no-build # wire the files only; don't build the graph
graft init --all-agents # wire every known agent, detected or not
graft init --list-agents # list known agent ids and exit
graft version # print the installed + latest published npm version
graft upgrade # npm install -g the latest published version
# global
graft --dir # use a context dir other than /graft
graft --version, -v # print the installed version and exit
```
方法调用通过接收者的类型进行解析——构造函数赋值
(`self.router = APIRouter()`)和类型注解,而不仅仅是调用处的
名称——因此 `callers`/`grep --in` 返回的是绑定到正确
类型的方法调用,这对于包含大量方法的代码尤其有效,而不是返回任何地方具有该名称的方法。
## 搜索与定向 (`graft grep` / `graft map`)
`graft grep ""` 会穷举每一个已索引的文件,并按封闭的符号对命中
结果进行分组,其排序方式与 `graft map` 使用的入边耦合度一致——
专为“此模式的每一次出现”类任务而构建,此时 `graft ask` 的
排序前 N 个结果不足以满足需求:
```
"NEEDLE" — 2 hits in 2 symbols across 1 files (searched 1 indexed files)
heavilyCalled · function · src/a.ts:L1-L3 · 3 in-edges
L2: console.log("NEEDLE hit in heavilyCalled");
rarelyCalled · function · src/a.ts:L4-L6 · 0 in-edges
L5: console.log("NEEDLE hit in rarelyCalled");
```
`graft map` 是在 token 预算内对代码库的首次查看——带有
文件/符号计数的目录集群、每个目录的本地枢纽以及全局热点——
全部按入度排序,无需 LLM,无需密钥:
```
repo map — 113 files · 687 symbols · 2186 edges · typescript
src/ 63 files · 527 symbols hubs: contextDirFor (node-file.ts, 21←), wiringPath (write.ts, 14←), buildGraph (build.ts, 11←)
test/ 43 files · 102 symbols hubs: edge (graph-traverse.test.ts, 4←), graphOf (graph-traverse.test.ts, 4←), fileNode (graph-map.test.ts, 3←)
viewer/ 5 files · 58 symbols hubs: $ (main.ts, 9←), activeGraph (main.ts, 5←), cvar (data.ts, 5←)
scripts/ 2 files · 0 symbols
hotspots: contextDirFor · function · src/context/node-file.ts:L100-L103 · 21← wiringPath · function · src/graph/write.ts:L20-L22 · 14← buildGraph · function · src/graph/build.ts:L104-L218 · 11← ...
```
## Monorepos 与多代码库文件夹
Graft 无需任何配置即可处理两种形式:
- **带有单个 `.git` 的 monorepo**(一个 `pnpm-workspace.yaml`/`package.json`
`workspaces`,或者每个包各自的 `go.mod`/`pyproject.toml`/`Cargo.toml`)——
`graft build` 将每个子项目发现为一个排名范围。`ask`/`map`
会根据每个范围自身的条件进行排名并融合结果,因此最大的
子项目不会淹没小的子项目;命中带有 `[scope/]` 标签,并且
`graft map` 首先按范围对其目录集群进行分组。
- **包含多个独立 git 仓库的文件夹**(顶层没有 `.git`)——`graft build`
自动拆分:每个子项目获得自己(被 git 忽略的)`graft/`,而父级
获得一个 `graft/workspace.json` 索引。从父级发起的查询会跨越
每个子项目进行联合搜索,并始终带有 `/` 标签。在子项目内部运行 `graft build`
以仅处理该代码库。
无论是哪种情况,一旦您知道您在哪里工作,就可以使用 `graft ask "" --in /` 缩小到特定的子项目。
## 可视化 (`graft viz`)
`graft viz` 会打开两个图的本地交互视图——无需安装,无需开发
服务器;查看器已预构建并打包在内。
search → jump to a node → dependency graph lights up
- **Context** 标签卡 —— 来自 `graft/*.md` 的架构图。节点按
类型着色,按连接度调整大小。
- **Code** 标签卡 —— 来自 `graft/.graph/wiring.json` 的按符号划分的图(首先运行 `graft build`)。
- **Outline** 标签卡 —— 文件 → 类 → 方法的层次结构,以可折叠树的形式呈现。
边使用代码的语言。每个链接都是一组封闭动词中的一个,每个动词都回答了构建或审查代码的人实际会问的问题:
| 动词 | 它回答的问题 |
|---|---|
| `part_of` / `contains` | 它存在于哪里? |
| `uses` / `calls` / `imports` / `depends_on` | 如果我更改这个,什么会损坏? |
| `produces` | 这个输出是从哪里来的? |
| `configures` | 什么能在不更改代码的情况下改变其行为? |
| `validates` | 什么检查或评判这个?(测试、偏差检查、评分) |
| `extends` / `implements` | 它必须遵守什么契约? |
选择一个节点,它的边就会带有方向性:**琥珀色 = 它依赖什么,
青色 = 什么依赖它**,并且每个高亮的边上都写有动词。画布上方的标签可以按动词进行过滤;由 tree-sitter 提取的边绘制为实线,
而由 LLM 推断的边绘制为虚线。当 `graft/`
在磁盘上发生更改时,查看器会实时重新加载。带有模糊动词(`influences`、`supports`)的旧图在加载时会被
标准化——无需重新生成。
## 基准测试
读取图的 agent 应该变得更便宜、更快速,而不会增加错误的答案。这就是我们的全部主张,因此我们对其进行了测量,而不是仅仅断言。
测试工具使用相同的文件工具运行了同一个 Claude Sonnet 5 agent 的三个变体:**cold**(从零开始探索)、**Graft**(预先注入 `graft ask --source` 包)和 **pull**(graft_find_code/graft_file_api 工具,不注入任何内容——仅在需要时为上下文付费)。一个 Opus 4.8 评判器使用必需的关键词阈值对准确度进行评分,因此快速但错误的答案不能因为速度快而获胜。成本是感知缓存的:读取 ≈0.1×,写入 1.25×,这正是 agent 实际运行的计费模型。
共进行了 162 次运行,涵盖两个代码库(graft 本身和一个真实的 Node/Express 身份验证服务),每个任务进行 3 次试验,任务分为单文件和多文件问题。
| 指标(平均/任务) | Cold | Graft |
|---|---|---|
| 成本 ($) | 0.0429 | **0.0292 (−32%)** |
| 未缓存的输入 tokens | 8,070 | **4,650 (−42%)** |
| 工具调用 | 4.2 | **2.3 (−46%)** |
| 延迟 (s) | 39.8 | **15.8 (−60%)** |
| 准确率 | 93% | 93% (持平) |
在任何语料库中,Graft 的回答都没有比 cold 差。pull 变体为了更大的收益牺牲了大部分速度:准确率跃升至 98%,比 cold 高出 5 个百分点,这是测试中最强的单一结果。当您需要速度时选择 push;当正确性更重要时选择 pull。
## 在热门代码库上的测试
上面的测试衡量的是机制。真正的考验在于 graft 是否能帮助 agent 在人们实际运行的代码上**交付真正的更改**,而不仅仅是回答问题。因此,我们在流行的开源代码库上对其进行基准测试:**每个代码库 15 项任务**,包含 10 个真实的开发者问题以及 **5 项实际的实施任务**(真实合并的 pull request,每一项都从其基础 commit 重新实现,并根据维护者实际更改的文件进行评分)。相同的 agent (Claude Opus),相同的文件工具;唯一的区别在于是否接入了 graft。
在这些代码库中,graft 的运行成本**最多低 4 倍,速度快 3 倍**,且准确率更好或没有损失:它通过修改与维护者相同的文件来重现真实合并的 PR。下面是各代码库的详细数据。
### PocketBase (Go, ~350 个文件)
| 15 项任务的汇总 | 标准 Claude Code | 使用 graft |
|---|---|---|
| 成本 | $13.91 | **$11.02 (−21%)** |
| 挂钟时间 | 2,044s | **1,762s (−14%)** |
| 重现的 PR | 5 / 5 | **5 / 5 (与维护者修改的文件相同)** |
更便宜、更快速且没有准确度损失:graft 重现了所有五个合并的 PR,触及了与维护者相同的文件。这种差距在跨文件理解上最为明显——“auth 如何跨 OAuth2 提供商工作”的成本从 $2.19 下降到了 $0.84。
我们提出的 10 个问题
1. **定向** — 给我一张 PocketBase 的架构图:主要子系统以及 HTTP 请求是如何流转到数据库的。
2. **入口点追踪** — 端到端追踪当客户端通过 REST API 创建记录时发生的事情,从路由处理程序到数据库写入。
3. **功能定位** — 我想添加一个全新的集合字段类型。我应该在哪里接入,哪些部分必须更改?
4. **Bug 定位** — 实时订阅在一段时间后会无声无息地停止发送事件。你会从哪里开始查找,为什么?
5. **影响范围** — 如果我更改记录验证逻辑的签名,什么依赖于它,什么可能会损坏?
6. **跨文件综合** — auth跨 OAuth2 提供商工作:tokens 在哪里颁发、验证、存储和刷新?
7. **可扩展性** — 我如何使用 PocketBase 作为 Go 框架来注册自定义路由以及记录创建时的钩子?
8. **安全性发现** — 用户输入在哪里被验证?在查询运行之前,集合 API 访问规则在哪里被执行?
9. **公共 API** — 作为外部应用程序,我如何通过 REST API 进行身份验证,然后列出和筛选记录?
10. **测试验证** — 记录 CRUD API 的测试在哪里?它们对访问规则有什么断言?
我们重新实现的 5 个合并的 PR
每个 PR 都被重置到其基础 commit;graft 的 diff 根据合并的 PR 修改的文件进行评分。
| PR | 类型 | 功能描述 | 维护者修改的文件 |
|---|---|---|---|
| [#6744](https://github.com/pocketbase/pocketbase/pull/6744) | feat | 生成并提供 WebP 缩略图 | `apis/file.go`, `tools/filesystem/filesystem.go` |
| [#6947](https://github.com/pocketbase/pocketbase/pull/6947) | fix | regex 随机字符串中的均匀字符分布 | `tools/security/random_by_regex.go` |
| [#6690](https://github.com/pocketbase/pocketbase/pull/6690) | refactor | Patreon OAuth2 使用 `x/oauth2/endpoints` | `tools/auth/patreon.go` |
| [#2726](https://github.com/pocketbase/pocketbase/pull/2726) | perf | 移除热点中间件路径上冗余的管理员计数查询 | `apis/middlewares.go` |
| [#3192](https://github.com/pocketbase/pocketbase/pull/3192) | fix | 在自动迁移回滚时恢复之前的 API 规则 | `plugins/migratecmd/templates.go` |
方法
同一个 commit 下的两个 PocketBase 克隆:一个通过 `graft init` 接入,另一个保持原样并验证为无 graft。每个任务都在无头模式(`claude -p`,Claude Opus)下使用空的 MCP 配置运行。理解类问题根据答案是否指向正确的文件和函数来评分;PR 任务根据 agent 的 diff 是否触及与合并的 PR 相同的文件来评分。每份记录都经过审计,以确认在 graft 分支中确实使用了 graft,而在标准分支中则没有使用。
## 开发指南
```
git clone https://github.com/NanoNets/context-graph-engine.git && cd context-graph-engine
npm install
npm run build
npm test
npm run cli -- build --deep . # run the CLI from source
```
## 许可证
MIT。详见 [LICENSE](LICENSE)。