Handbooks
核心概念

为什么要有这个项目

摘要一个代码库并不能帮代理找到东西,路由才能。这里是完整的论证,以及由此推出的设计。

你早就见过的那种失败

你让编码代理做一个横跨整个系统的改动。它 grep 了一个符号,找到一个看起来合理的位置,改了,然后汇报成功。

它漏掉了:

  • 真正控制该行为的常量,在三个目录之外;
  • 批处理路径里的镜像实现;
  • 统计它刚改的那件事的指标;
  • 断言旧行为的测试。

代理并不是不懂怎么写这段代码,而是搞不清代码在哪里。而且它无从查起,因为它手里只有文本搜索,和一个装不下整个仓库的上下文窗口。

为什么摘要解决不了问题

显而易见的回应是“把代码库摘要一下,交给代理”。这行不通,原因很具体:

摘要回答的是“这是什么?”,而代理需要的是“它在哪里?”

一段写得再漂亮的上传子系统介绍,也不会告诉代理重试预算还存在于 worker/queue.py,并被 metrics/emit.py 读取。更糟的是,摘要是看起来可信的文字——代理会毫不犹豫地在它之上推理,却分不清哪些句子是承重的事实、哪些只是模型的转述。

由此产生三种失败模式:

  1. 不可寻址。 文字给出的是概念,不是路径和行号范围。
  2. 不可验证。 里面没有任何东西能区分解析出的事实和一次猜测。
  3. 会腐烂。 代码一变,摘要就悄悄错了,而且它自己不会说。

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,校验这个包——都是确定性的、免费的。你可以在每次提交时都跑它们。

正是这个分界,让 renderskill 成为独立命令而不是 generate 上的标志;也正因如此,它们所在的包连误碰 LLM 的可能性都没有。

它不是什么

  • 不是代码搜索工具。 它不替代 grep 或你的 LSP,而是告诉代理该把它们指向哪里
  • 不是自主编码代理。 规划器在构造上就是只读的;它没有写工具。apply 是没有模型参与的机械执行器。两者之间由人来决定。
  • 不能替代你自己写的文档。 架构决策、产品意图和团队约定无法从调用图推导出来,Handbooks 也不假装可以。

下一步

本页目录