为什么要有这个项目
摘要一个代码库并不能帮代理找到东西,路由才能。这里是完整的论证,以及由此推出的设计。
你早就见过的那种失败
你让编码代理做一个横跨整个系统的改动。它 grep 了一个符号,找到一个看起来合理的位置,改了,然后汇报成功。
它漏掉了:
- 真正控制该行为的常量,在三个目录之外;
- 批处理路径里的镜像实现;
- 统计它刚改的那件事的指标;
- 断言旧行为的测试。
代理并不是不懂怎么写这段代码,而是搞不清代码在哪里。而且它无从查起,因为它手里只有文本搜索,和一个装不下整个仓库的上下文窗口。
为什么摘要解决不了问题
显而易见的回应是“把代码库摘要一下,交给代理”。这行不通,原因很具体:
摘要回答的是“这是什么?”,而代理需要的是“它在哪里?”
一段写得再漂亮的上传子系统介绍,也不会告诉代理重试预算还存在于 worker/queue.py,并被 metrics/emit.py 读取。更糟的是,摘要是看起来可信的文字——代理会毫不犹豫地在它之上推理,却分不清哪些句子是承重的事实、哪些只是模型的转述。
由此产生三种失败模式:
- 不可寻址。 文字给出的是概念,不是路径和行号范围。
- 不可验证。 里面没有任何东西能区分解析出的事实和一次猜测。
- 会腐烂。 代码一变,摘要就悄悄错了,而且它自己不会说。
Handbook 换了个做法
它构建索引,而不是摘要
产出只回答一个问题:这次改动必须触碰哪些文件、函数和状态?
每个条目都是一个地址——路径、限定名、行号范围——来自一次真实的解析。围绕这些地址的文字是帮人阅读用的,明确不是代理应该据以行动的东西。SKILL 包的第一行就是这么说的:
本手册是这个代码库的位置索引,不是代码说明。用它来决定一次改动必须触碰哪些文件、函数和状态——然后去读真实源码。
它在构造上就把事实与文字分开
| 来自 | 会错吗? | |
|---|---|---|
| 文件、函数、行号范围、调用边 | tree-sitter | 不会——这是解析结果 |
| 哪些调用无法被解析 | tree-sitter | 不会——它们被隔离,而不是被猜测 |
| 哪些文件无法被解析 | tree-sitter | 不会——它们被披露,而不是被丢弃 |
| 阶段结构 | LLM,随后经过机械校验 | 结构上不会;判断上会 |
| 用途、讲解、总览 | LLM | 会——而且被明确标注为文字叙述 |
这种分离靠的是包边界,不是约定:分析器、渲染器、skill 打包器和补丁器完全不依赖 LLM 包。
它把失败摆在明面上
这里的每个设计决定都遵循同一条规则:行不通的时候,就明说。
- 卡片生成失败的文件依然出现,只是描述为空。它会被列在
_coverage.json里。绝不丢弃,也绝不编造。 - 分析器解析不了的调用进入
dropped-calls.json,带着类别和原始文本。绝不会被猜成一条看似合理的边。 - 分析器读不了、或者只解析出一部分的文件,会带着原因进入
scan-coverage.json。它绝不会被算作已覆盖——没人打开过的文件,不等于“一个没有函数的文件”。 - 由配置驱动引擎分析的语言会在总览中被点名,这样“尽力而为的调用关系”就不会被误读成“精确”。
- 规划器放弃时以非零码退出,不会有脚本把它的道歉误当成计划。
- 匹配零次或匹配两次的补丁锚点直接拒绝,绝不挑一个用。
它以成比例的成本保持新鲜
文档会腐烂,是因为更新它的成本和重写一样高。resync 对比新旧调用图,只重新生成变化的部分——被触碰文件的卡片、新文件的归属、受影响阶段的叙述。改了三个文件,就只为三个文件付费。
内容哈希缓存负责其余部分:输入没变的阶段完全不会被重新叙述。
经济账
生成是昂贵的一步,而它只发生一次。之后的一切——渲染成 markdown、HTML 站点、代理索引、llms.txt,打包成 SKILL,校验这个包——都是确定性的、免费的。你可以在每次提交时都跑它们。
正是这个分界,让 render 和 skill 成为独立命令而不是 generate 上的标志;也正因如此,它们所在的包连误碰 LLM 的可能性都没有。
它不是什么
- 不是代码搜索工具。 它不替代
grep或你的 LSP,而是告诉代理该把它们指向哪里。 - 不是自主编码代理。 规划器在构造上就是只读的;它没有写工具。
apply是没有模型参与的机械执行器。两者之间由人来决定。 - 不能替代你自己写的文档。 架构决策、产品意图和团队约定无法从调用图推导出来,Handbooks 也不假装可以。