Handbooks
核心概念

五个阶段

每个生成阶段做什么、花多少钱、失败时降级成什么,以及如何只重跑其中一个。

handbook generate 运行五个阶段。只有第一个是免费的;其余都要和你的 LLM 端点对话。

Phase产出LLM?可单独重跑?
1调用图
2a每个扫描到的文件一张卡片
2b阶段骨架 + 文件归属
2c阶段内分组与排序
3叙述 + 跨阶段状态寄存器
--phase all        # everything (default)
--phase 1          # just the call graph
--phase 2          # 2a + 2b + 2c
--phase 2a         # one phase
--phase 2c,3       # a comma list

Phase 1——调用图

不用 LLM。确定性。免费。

语言适配器用 tree-sitter 解析每个文件,产出一种与语言无关的中间表示。图构建器随后把边划分为保留与丢弃两类,标注出入度,并为那些被引用但从未显式定义的构造函数合成节点。

它还会为每个扫描过的文件盖上内容哈希。正是这个哈希,让 resync 之后能发现那种行号和签名都没动的就地函数体修改——纯结构性 diff 会完全漏掉的情形。

它读不进来的那些文件

发现阶段列出了、但分析器没能变成事实的文件,会被如实记下,绝不悄悄跳过。每一个都带着原因落进 phase1/scan-coverage.json

  • unreadable——读取这一步本身就失败了(权限位、悬空的符号链接、运行途中被构建删掉的文件)。没有任何事实。
  • unparsable——语法抛错,或者没有返回语法树。没有任何事实。含 case 的 shell 脚本就是最常见的那一类。
  • partial——文件解析出来了,但带着语法错误。在其余部分里找到的函数和调用都是真的;缺的是落在那个错误节点里的东西。

前两类文件还会被从 scannedFiles 中剔除,因为一个什么都没产出的文件,不该被当成一个空文件交给 Phase 2a。Phase 1 收尾时会在日志里把这个缺口点出来:

[scan] coverage: 409 files analyzed; 3 recorded in scan-coverage.json (partial=1 unparsable=1 unreadable=1)

这份产物里 files 数组为空,是同一句话的正面说法:全都解析成功了。

产出: phase1/graph.jsonfunctions.csvgraph.dotdropped-calls.jsonscan-coverage.json

永远先跑这个

handbook analyze 就是这个阶段。它分文不花,而且是唯一能让你在花 token 之前就发现自己正在扫描 node_modules、或者漏掉了一整门语言的办法。


Phase 2a——文件卡片

用 LLM。通常是最贵的阶段。

Phase 1 真正读到的每个文件都会得到一张卡片——也就是 graph.json 里的 scannedFiles,其中不含记在 scan-coverage.json 里的那些 unreadableunparsable 路径:

  • purpose——一两句大白话
  • role——取自封闭词表(entrypointdomain_logicio_transport……)
  • lifecycle——startupmain loopcross-cuttingnone……
  • --detail deep 下还有:一段 120–300 词的讲解,外加合并到图事实上的每函数用途、数据流和关系

如何分批

每个请求 --read-batch-size 个文件,同时在途 --read-workers 个批次。deep 模式默认每批一个文件,因为深卡片的输出量很大,把几个文件塞进同一条回复正是回复被截断的原因。

三级降级

如果一批回复解析不了:

  1. 把这一批拆成单个文件重试;
  2. 对超大的单个文件,改为按函数分块重试;
  3. 仍然失败,就写一张诚实的空卡片——只有结构,没有文字。

一个文件绝不会因为它的文字生成失败而从手册中消失。 每次缺失都列在 phase2/cards/_coverage.json 里,而那些没产出任何可用内容的回复会被保留(上限 20 个,按哈希命名)在 phase2/cards/_rejected/ 下,让你能读到到底哪里出了问题,而不是靠猜。

恢复

卡片完成一张写一张。Ctrl-C 是安全的,--resume 会跳过在请求的深度下已经有完整卡片的文件。

handbook generate --source $REPO --work $WORK --phase 2a --detail deep --resume

Phase 2b——骨架与归属

用 LLM。决定这本手册是什么的阶段。

两种模式。

--synth-mode oneshot(默认)

从 nav-pack(目录汇总 + 入口点)合成一个阶段骨架,然后分批把每个文件归入且仅归入一个阶段。

便宜,而且通常足以判断形状对不对。

--synth-mode doctor

一个 actor–critic 修复循环。每一轮:

  1. actor 依据真实调用图的统计数据,提出至多三项结构改动——拆分、合并、移动、改标题、改父级;

  2. 三个 critic 并行评审,各盯一种失败:

    Critic找什么
    工程师这和代码实际做的事对得上吗?被引用的条目真实存在吗?
    架构师边界不清、臃肿的阶段、饥饿的阶段、放错位置的横切关注点
    读者结果更好读了吗?页面内聚、标题直观、叙事能一路跟下来
  3. 幸存的改动会针对调用图再做一次机械校验——点名了不存在的阶段、或会让文件失去归属的改动,在触碰骨架之前就被拒绝;

  4. 受影响的文件被重新归属。

当没有文件未归属、也没有改动通过评审时停止;或到达 --max-doctor-rounds(默认 6);或连续两轮没有进展。

回复解析失败的 critic 按 REJECT 计。失效的评审者绝不能放行改动。

自带骨架

handbook generate --source $REPO --work $WORK --skeleton my-skeleton.yaml

文件会被归入你的阶段。使用 --strategy member 时,改为对单个函数分类,文件级产物由此推导。

产出: phase2/skeleton.yamlphase2/assignment.jsonphase2/strategy.json


Phase 2c——组织

用 LLM,但便宜。可降级为确定性顺序。

在每个阶段内部,文件按调用图拓扑排序,并分成 2–8 个带标题的小组,每组配一行摘要。

任何失败都降级为确定性的扁平顺序。文件绝不会被丢弃。 整个阶段就是围绕这条不变式写的:分组不好看是外观问题,文件缺失是正确性问题。

使用 --strategy member 时,此阶段是空操作——组织在 2b 就已经确定性地推导出来了,所以单独跑一次 --phase 2c 根本不需要 LLM。

产出: phase2/organization.yaml


Phase 3——叙述与寄存器

用 LLM。缓存力度很大。

自底向上的叙述

先叶子阶段,再父阶段——这样父阶段的总结在动笔时已经知道子阶段说了什么——最后是通晓一切之后写下的系统总览。

每次文字生成调用都缓存在 phase3/cache/ 下,键由提示词版本、语言和完整提示词哈希组成。改动一个阶段后重跑 Phase 3,只会重新叙述一个阶段

状态寄存器

“寄存器”是一段跨阶段流动的状态——连接池、功能开关、重试预算、认证令牌。提取采用问到没有为止的补漏循环:不断追问,直到某一轮再也找不出新东西。

对于扇出式改动,这是最有用的一件产物,因为*“哪些阶段碰这份状态”*正是一次分散式改动要问的问题。

产出: phase3/narration.jsonphase3/registers.json


两种策略

--strategy file(默认)--strategy member
骨架由 LLM 合成你来编写 skeleton.yaml
叶子单元一个源文件一个函数或方法
Phase 2b把文件归入阶段对每个成员分类,再推导文件级产物
Phase 2cLLM 分组已经完成——确定性
适合你还不了解的仓库你已经心中有数的仓库
成本较低较高——每个成员都要分类

所选策略记录在 phase2/strategy.json。换了 --strategy 却不带 --phase 2b 的部分重跑会被拒绝——file 策略的默认值悄悄覆盖掉由 member 推导的组织,正是那种事后极难察觉的损坏。

一次运行会为自己记录什么

<work>/run-manifest.json
{
  "version": 1,
  "model": "gpt-4o-mini",
  "phases": ["1", "2a", "2b", "2c", "3"],
  "startedAt": "2026-08-08T13:02:11.004Z",
  "finishedAt": "2026-08-08T13:19:44.881Z",
  "usage": { "promptTokens": 1840221, "completionTokens": 214880, "totalTokens": 2055101 },
  "stats": { "phasesRun": ["1", "2a", "2b", "2c", "3"], "nCards": 412, "nStages": 9, "nRegisters": 6 }
}

它描述的是最后一次成功的运行。失败的运行不动之前的清单,被中止的运行则什么都不写。

下一步

本页目录