首页
/ Understand-Anything 的 /understand 流水线 Token 成本削减:C1–C5 五项优化的实施蓝图与仓库落地

Understand-Anything 的 /understand 流水线 Token 成本削减:C1–C5 五项优化的实施蓝图与仓库落地

2026-09-05 20:12:53作者:殷蕙予

本文基于 Understand-Anything 仓库中的实施计划 2026-03-27-token-reduction-impl.md 展开,解析该计划如何通过导入预解析、批次合并、附录移除、payload 瘦身和 LLM 审核器门控这五项改动,将 /understand 命令在大型代码库上的 token 成本削减约 85%。读完本文,你将掌握每一项优化(C1–C5)的具体任务拆解、参数变更与验证手段,并能对照当前仓库源码,了解这些机制(如 importMapbatchImportData--review 门控、内联校验脚本)最终在代码中的实际形态。

1. 背景:/understand 的 token 成本花在了哪里

/understand 是 Understand-Anything 的核心技能:它扫描整个代码库,分阶段(Phase 0–7)调度多个 LLM 子代理(file-analyzer、architecture-analyzer、tour-builder、graph-reviewer 等),最终产出一个可交互的知识图 knowledge-graph.jsondashboard 展示。整个流水线由 SKILL.md 这份 Markdown 编排文件驱动。

该计划配套的设计文档 2026-03-27-token-reduction-design.md(计划中引用路径 docs/plans/2026-03-27-token-reduction-design.md 已随目录迁移,实际位于 docs/superpowers/specs/)给出了一个 500 文件 TypeScript+React 项目的基线 token 拆解:

来源 阶段 Token(输入) 占比
allProjectFiles 列表 × 67 个批次 Phase 2 ~167,000 ~50%
file-analyzer-prompt.md 模板 × 67 个批次 Phase 2 ~134,000 ~40%
语言/框架附录 × 67 个批次 Phase 2 ~68,000 ~20%
Tour builder payload(全量节点+边) Phase 5 ~80,000 ~24%
Graph reviewer(完整图+文件清单) Phase 6 ~58,000 ~17%
Architecture analyzer payload Phase 4 ~22,000 ~7%
合计 ~529,000

根因一句话概括:Phase 2 以每批 5–10 个文件切出 67 个批次,而每个批次都被独立注入了完整的 500 文件列表,仅用于各自的导入路径解析——同一份数据被重复发送了 67 次,而这份解析工作本身是完全确定性的。优化目标因此设定为:在 500 文件项目上削减 85%+ 输入 token,同时不降低图谱质量、保留 --full/增量/scope 参数、保持 knowledge-graph.json 输出 schema 向后兼容。

2. 优化总览:五项改动与实施顺序

计划把方案拆成五个相互独立、可单独发布的改动(C1–C5),并按"风险从低到高"的顺序上线:

改动 内容 针对的浪费 预估节省
C5 LLM graph-reviewer 改为 --review 门控,默认走内联确定性校验 每次运行固定支付 ~58,500 token 的审核成本 ~58,500
C4 瘦身 Phase 4(架构分析)与 Phase 5(tour 构建)的注入 payload 全量节点/全类型边/完整 layer 对象被注入 ~121,500
C3 从 file-analyzer 批次中移除语言/框架附录,改用内联速查表 每个批次重复注入 ~1,300 token 附录 ~23,000
C1 扫描器预解析导入,importMap 写入 scan-result.json,批次只收 batchImportData 切片 全量文件列表 × N 批次 ~154,100
C2 批量大小 5–10 → 20–30 文件,并发 3 → 5 模板与调度开销随批次数线性放大 ~94,000(叠加 C1)

计划将全部工作组织为 9 个任务,每个任务独立提交(各带一条 perf(understand): ...chore 提交信息),风险分布与依赖关系如下表(源自计划末尾的 Summary 小节):

任务 改动 风险
Task 1 C5: 门控 reviewer
Task 2 C4a: 瘦身 Phase 4 payload
Task 3 C4b: 瘦身 Phase 5 payload
Task 4 C3: 批次去附录
Task 5 C1a: 扫描器导入解析
Task 6 C1b: file-analyzer 改用 batchImportData
Task 7 C1c+C2: SKILL.md 编排 + 批量大小
Task 8 版本号同步
Task 9 冒烟测试

其中 Tasks 1–4 彼此独立,可与 Tasks 5–7 分开发布;而 Task 5、6、7 构成一条紧耦合的链路(扫描器产出 importMap → SKILL.md 按批次切出 batchImportData → file-analyzer 直接消费),必须一起上线。

3. C5(Task 1):把 LLM 审核器关进 --review 门后面

3.1 动机

Phase 6(REVIEW)原本总是调度 LLM graph-reviewer 子代理去读取完整的 assembled graph(~500 节点、全部边、layers、tour)做质量审核。但设计文档指出:这个审核流程的"Phase 1"其实完全是一个确定性脚本,"Phase 2"只是一个阈值判断——issues.length === 0 就通过。Happy path 上没有任何需要 LLM 判断的地方,却要为它支付约 58,000 输入 + 500 输出的 token。

3.2 方案:默认内联校验 + --review 才走 LLM

计划修改 SKILL.md 的 Phase 6:检查 $ARGUMENTS 中是否有 --review 标志,然后走两条路径之一。

默认路径(无 --review):写入并执行一个内联 Node.js 校验脚本,输出 { issues, warnings, stats }review.json。计划给出的完整脚本逻辑如下(核心校验项):

#!/usr/bin/env node
const fs = require('fs');
const graphPath = process.argv[2];
const outputPath = process.argv[3];
try {
  const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8'));
  const issues = [], warnings = [];
  const nodeIds = new Set();
  const seen = new Map();
  // 1) 节点必填字段:id / type / name / summary / tags 缺一记为 issue
  graph.nodes.forEach((n, i) => {
    if (!n.id) { issues.push(`Node[${i}] missing id`); return; }
    if (!n.type) issues.push(`Node[${i}] '${n.id}' missing type`);
    if (!n.name) issues.push(`Node[${i}] '${n.id}' missing name`);
    if (!n.summary) issues.push(`Node[${i}] '${n.id}' missing summary`);
    if (!n.tags || !n.tags.length) issues.push(`Node[${i}] '${n.id}' missing tags`);
    if (seen.has(n.id)) issues.push(`Duplicate node ID '${n.id}' at indices ${seen.get(n.id)} and ${i}`);
    else seen.set(n.id, i);
    nodeIds.add(n.id);
  });
  // 2) 边的 source/target 必须指向真实存在的节点
  graph.edges.forEach((e, i) => {
    if (!nodeIds.has(e.source)) issues.push(`Edge[${i}] source '${e.source}' not found`);
    if (!nodeIds.has(e.target)) issues.push(`Edge[${i}] target '${e.target}' not found`);
  });
  // 3) 文件级节点必须恰好属于一个 layer(缺层 / 多层冲突均记 issue)
  // 4) tour 每一步引用的 nodeId 必须存在
  // 5) 无边的节点记为 warning(orphan),不阻塞
  const stats = { totalNodes, totalEdges, totalLayers, tourSteps, nodeTypes, edgeTypes };
  fs.writeFileSync(outputPath, JSON.stringify({ issues, warnings, stats }, null, 2));
  process.exit(0);
} catch (err) { process.stderr.write(err.message + '\n'); process.exit(1); }

执行命令与失败处理策略:

node $PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.js \
  "$PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json" \
  "$PROJECT_ROOT/.understand-anything/intermediate/review.json"

脚本非零退出时读取 stderr、修复脚本并重试一次。--review 路径则保持原样:读取 graph-reviewer 提示模板,附加 Phase 1 文件清单({path, sizeLines} 列表)与 Phase 2–5 累积的警告,并要求子代理做交叉验证——扫描清单中的每个文件都应有对应 file: 节点,图中 filePath 不在清单里的节点也要被标记。

两条路径共用后处理逻辑:若 issues 非空,执行自动修复(删除悬空边、用合理默认值填充缺失字段——空 tags["untagged"]、空 summary"No summary available"、删除非法类型节点),修复后重跑校验;若一次修复后仍有严重问题,则照常保存图谱,但在最终报告中带上警告并跳过 dashboard 自动启动。

3.3 仓库中的现状

当前仓库的 SKILL.md--review 已是一级选项:"Run full LLM graph-reviewer instead of inline deterministic validation",Phase 6(SKILL.md#L572-L731)与计划完全同构,且有两点后续演进值得注意:

  1. 内联脚本落盘文件名从计划中的 ua-inline-validate.js 变为 ua-inline-validate.cjs,数据目录也泛化为 $UA_DIR.ua/,或已存在时沿用旧的 .understand-anything/);
  2. "文件级节点"的判定从仅 type === 'file' 扩展为一个类型集合 ['file', 'config', 'document', 'service', 'pipeline', 'table', 'schema', 'resource', 'endpoint']——因为节点体系后来扩展到了 13 种类型,配置、文档、基础设施文件同样要求必须恰好属于一个 layer。

4. C4(Task 2–3):瘦身 Phase 4 与 Phase 5 的注入 payload

4.1 C4a:Phase 4 节点字段裁剪

Phase 4(architecture-analyzer)负责架构分层。计划将其 dispatch prompt 中的文件节点格式从:

[list of {id, name, filePath, summary, tags} for all file-type nodes]

改为:

[list of {id, filePath, summary, tags} for all file-type nodes — omit name, complexity, languageNotes]

理由:name 恒等于 filePath 的 basename,可推导;complexitylanguageNotes 对分层决策无用。预计每个节点少 15–20% token,Phase 4 合计省 3,000–5,000 token。

4.2 C4b:Phase 5 三处裁剪(最大单项收益)

Phase 5(tour-builder)原本收到全部节点(含 function/class)、全部边类型、完整 layer 对象(含 nodeIds 数组)。计划做了三处裁剪:

数据 改造前 改造后
节点 全量(500 文件项目约 1,500+ 节点,~67,500 token) 仅 file 类型(500 节点,~15,000 token)
节点字段 {id, name, filePath, summary, type, tags, complexity, languageNotes?} {id, name, filePath, summary, type}
全部类型(3,000+ 条,~60,000 token) importscalls(~400–800 条,~12,000 token)
Layers {id, name, description, nodeIds: [...]} {id, name, description}(丢弃 nodeIds)

依据是 tour 本身只在文件粒度导航:步骤引用的是 file: 节点 ID,BFS 遍历只走 imports/calls 边,layer 数据只用于叙事弧线的先后顺序——nodeIds 大数组对 tour 设计没有价值。Phase 5 合计从 ~132,500 降到 ~27,500 token,单项节省 ~105,000

计划同时要求同步更新 tour-builder-prompt.md 的输入 schema 示例(layers 去掉 nodeIds、nodes 标注 file-type only),并在 "Node Summary Index" 一节补一句说明:输入节点仅含 file 类型,索引表也只含文件节点。

仓库现状:提示模板文件已演化为 agent 定义文件(tour-builder.md 等)。从当前 SKILL.md#L518-L539 看,Phase 5 payload 保持了"不注入 function/class 节点、layers 省略 nodeIds"这两条核心裁剪,但节点范围从"仅 file: 类型"扩大到所有文件级节点(config、document、service 等),边也改回注入全部类型——这是计划落地后针对"知识图覆盖非代码资产"这一新能力做出的后续平衡调整,说明 payload 瘦身与图谱完整度之间经历了二次调参。

5. C3(Task 4):移除批次附录,换成一次性速查表

5.1 动机

languages/typescript.md(~600 token)与 frameworks/react.md(~700 token)这类附录文件原本被注入到每一个 file-analyzer 批次的 prompt 里,成本是 ~1,300 token × N 批次。而模型对这些主流语言已有深入训练知识,逐批次重复注入是低效的。

5.2 方案

  1. SKILL.md Phase 2 的 "Build the combined prompt template" 块从三步(读基础模板 → 按语言注入 ./languages/<id>.md → 按框架注入 ./frameworks/<id>.md)收缩为一步——只读基础模板,明确"Phase 2 批次禁止追加附录文件,附录保留给 Phase 4(只有一次子代理调用,成本可接受)"。dispatch 的附加上下文里也删除 "Frameworks detected" 行。

  2. file-analyzer-prompt.md 在 "Critical Constraints" 之前插入紧凑的 "Language and Framework Quick Reference" 段(~150 token,只付一次),用两张速查表捕捉附录里最高信号量的模式:

    Tag 信号:

    信号 应打的标签
    文件在 hooks/ 下,导出以 use 开头的函数 hook, service
    文件在 contexts/context/ 下,导出 Provider 组件 service, state
    文件在 pages/views/ ui, routing
    文件在 store/slices/reducers/state/ state
    文件在 services/api/client/ service
    包根目录带再导出的 __init__.py entry-point, barrel
    项目根的 manage.py entry-point
    目录中的 mod.rs barrel
    cmd/ 子目录下的 main.go entry-point

    Edge 信号:

    模式 应建的边
    React 组件在 JSX 中渲染另一组件 父→子 contains
    组件/hook 调用自定义 hook(useX 消费方→hook 文件 depends_on
    Context provider 包裹组件 provider→context 定义 publishes
    组件调用 useContext 或自定义 context hook 消费方→context 定义 subscribes
    Python from x import y(x 为项目内文件) imports(与 JS/TS 同规则)
    Go import 内部包路径 指向解析文件的 imports

验证要求:Phase 2 的附录注入步骤必须彻底消失,而 Phase 4 的同名块必须保持不变——仓库现状印证了这一点,SKILL.md#L420-L424 的 Phase 4 仍然完整执行语言上下文注入与框架附录注入。

6. C1(Task 5–6):扫描器预解析导入,importMap 贯穿全流程

这是整个方案中数据流改动最大的一项:导入解析只应做一次(在 Phase 1,且是确定性工作),而不是在 67 个批次里各做一次

6.1 C1a:扫描器新增 Step 8 — Import Resolution

计划为扫描脚本要求新增一步:对每个源文件抽取并解析相对导入,产出 importMap 写进 scan-result.json。各语言的抽取模式:

语言 导入模式
TypeScript/JavaScript import ... from './...' / '../'require('./...')
Python 仅相对导入:from .x import yfrom ..x import y
Go import (...) 块中以 go.mod module 路径开头者
Rust use crate::use super::mod x
Java/Kotlin 无法按路径解析——跳过
Ruby require_relative '...'

解析规则:相对导入从导入方所在目录解析;无扩展名时按序尝试 .ts.tsx.js.jsx/index.ts/index.js/index.tsx/index.jsx.py.go.rs.rb;解析结果必须存在于已发现文件列表中才记录,否则视为外部/动态导入而跳过。输出格式与约束:

"importMap": {
  "src/index.ts": ["src/utils.ts", "src/config.ts"],
  "src/utils.ts": [],
  "src/components/App.tsx": ["src/hooks/useAuth.ts", "src/store/index.ts"]
}

键为项目相对路径(与 files[*].path 对齐),值为已解析的项目内路径;文件清单中的每个文件都必须有键(无导入则为 []),外部包一律不出现。同时要求最终组装阶段"不得丢弃 importMap"(原 IMPORTANT 注记追加:所有其他字段——包括 importMap——必须原样保留)。

6.2 C1b:file-analyzer 输入 schema 换血

file-analyzer 的输入从"全量文件列表 + 本批文件"变为"本批文件 + 本批预解析导入":

Before:

{
  "projectRoot": "/path/to/project",
  "allProjectFiles": ["src/index.ts", "src/utils.ts", "..."],
  "batchFiles": [
    {"path": "src/index.ts", "language": "typescript", "sizeLines": 150}
  ]
}

After:

{
  "projectRoot": "/path/to/project",
  "batchFiles": [
    {"path": "src/index.ts", "language": "typescript", "sizeLines": 150},
    {"path": "src/utils.ts", "language": "typescript", "sizeLines": 80}
  ],
  "batchImportData": {
    "src/index.ts": ["src/utils.ts", "src/config.ts"],
    "src/utils.ts": []
  }
}

配套改动包括:抽取脚本不再解析导入(输出格式中删除 imports 数组,保留 metrics.importCount,改由 batchImportData[path].length 直接得出);建边规则改为"对 batchImportData[filePath] 中每个已解析路径建 imports 边,禁止自行重新解析";Critical Constraints 中关于 resolvedPath 的旧措辞一并替换。

仓库现状:这条链路如今是仓库中最"工程化"的部分。计划里"让 LLM 照着散文描述写解析脚本"的 Step 8,后来被一个完全确定性的捆绑脚本 extract-import-map.mjs 取代——其文件头注释直言:此前"运行时 LLM 产出的脚本质量不一、只靠正则、语言覆盖稀疏"。该脚本基于 @understand-anything/core 的 TreeSitterPlugin 抽取原始导入,再按语言执行解析规则,支持面远超计划初版:

  • TS/JS:相对导入 + tsconfig 路径别名(按 importer 向上找最近的 tsconfig,monorepo 友好),扩展名探测顺序见 TS_EXT_PROBES,并额外处理 NodeNext/ESM 约定下"源码导入 ./config.js 但磁盘上只有 ./config.ts"的改写(NODENEXT_REWRITES,注释注明这是 issue #294 的修复——否则 ESM-TS 项目会产出一张几乎没有边的图);
  • Go:多模块 monorepo 支持,按 importer 向上找最近 go.mod 剥离 module 前缀,包级导入展开为该目录下全部 .go 文件;
  • Python:相对导入按前导点数上溯,绝对导入把每个祖先目录都当作候选 root 探测,天然适配多服务仓库;
  • Java/Kotlin/Scala/C#:点分 FQN → 后缀索引匹配;PHP 走 composer.json PSR-4 自动加载;Swift 解析 Package.swift target 声明;
  • 单文件失败只降级为 importMap[path] = [] 并发 stderr 警告,不中断整体。

project-scanner.md 的 Step C 规定:importMap 必须逐字合并进 scan-result.json(不得编辑、重排或过滤),且明确列出了 13 种受支持语言、"集合之外的语言得到空数组、没有 LLM 回退"。回归测试见 test_extract_import_map.test.mjs

6.3 C1c + C2(Task 7):SKILL.md 编排接线

Phase 1 读到的 scan-result.json 清单中新增一项 "importMap:每个文件的预解析项目内导入",并明确"存为内存变量 $IMPORT_MAP 供 Phase 2 使用"。Phase 2 的派发块在 dispatch 前按批次切片:

batchImportData = {}
for each file in this batch:
  batchImportData[file.path] = $IMPORT_MAP[file.path] ?? []

dispatch prompt 中 allProjectFiles 整段删除,替换为"本批预解析导入数据(建 imports 边直接用,禁止从源码重新解析)"。增量更新路径同步注明:变更文件同样走 20–30/批、5 并发、batchImportData 构造流程。

C2 同任务完成两项参数调整:批量大小从"每批 5–10 文件"改为"每批 20–30 文件(目标 ~25/批)",并发上限从 3 提到 5。计划的权衡表很直白:

小批次(旧) 大批次(新)
500 文件的批次数 ~67 ~20
模板重复次数 67× 20×
质量风险 低(聚焦) 略高
并发 3 5

质量风险被评估为低:抽取脚本对批次大小不敏感,每个子代理操作互不重叠的文件组,20–30 个文件仍远在上下文窗口内。C1+C2 叠加后,Phase 2 从 ~301,500 降到 ~53,500 token(~82%)。

仓库现状:批量策略已从"固定 20–30 文件"演进为语义批处理SKILL.md 新增 Phase 1.5,运行捆绑脚本 compute-batches.mjs:读取 scan-result.jsonimportMap,在导入图上跑 Louvain 社区发现runLouvain),把互相紧耦合的文件聚进同一批次,再为每批补上 batchImportDataneighborMap(跨批邻居文件及其导出符号,供跨批边置信度使用),输出 batches.json。并发上限保持计划的值——SKILL.md#L303:"Run up to 5 subagents concurrently"。file-analyzer.md 则把计划的"直接用 batchImportData 建边"强化成硬性自检:输出中 imports 边数必须等于本批所有 batchImportData[file].length 之和,"不是 90%,不是'有意义的那些',是全部";merge-batch-graphs.py 的后处理恢复通道只作为兜底。相关测试见 test_compute_batches.test.mjs

7. Task 8:版本同步

按项目约定,插件版本在四个文件中保持同步,计划执行 patch bump(1.2.11.2.2,属内部优化、无 API 变更):

  • understand-anything-plugin/package.json
  • .claude-plugin/marketplace.jsonplugins[0]version
  • .claude-plugin/plugin.json
  • .cursor-plugin/plugin.json

验证方式是对四个文件 grep '"version"',确认全部显示新版本。当前仓库中 package.json 的版本已演进到 2.9.4,说明这套降本方案落地后插件又经历了多轮迭代。

8. Task 9:构建与端到端冒烟验证

计划以真实项目跑 /understand --full 做整体验证,检查清单值得任何改造 LLM 流水线的工程直接借鉴:

  1. 构建pnpm --filter @understand-anything/core build 与 skill 包构建零错误;本地构建拷入插件缓存(~/.claude/plugins/cache/understand-anything/understand-anything/<version>)。
  2. 小项目(~20 文件):Phase 0–7 无错误、knowledge-graph.json 生成、节点/边数量合理、layers 与 tour 齐全、输出中不出现 allProjectFiles 或附录相关报错。
  3. 大项目(100+ 文件):批次数应降到 ~4–6(不再是 10–20);scan-result.jsonimportMap 存在且完整;图谱质量与改造前持平(summary 有信息量、分层正确)。
  4. --review 路径/understand --full --review 应确实派发 LLM graph-reviewer(而非内联脚本),review.jsonapproved 字段,流水线正常完成。
  5. 如有修复,单独提交 fix(understand): smoke test fixes for token reduction changes

9. 收益汇总与结论

计划给出的合计账目(500 文件 TypeScript+React 项目):

改动 改造前 改造后 节省
C1+C2(import map + 批次合并) ~301,500 ~53,500 ~248,000
C3(去附录) ~26,000 ~3,000 ~23,000
C4(Phase 4+5 瘦身) ~154,500 ~33,000 ~121,500
C5(默认门控 reviewer) ~58,500 ~0 ~58,500
合计 ~540,500 ~89,500 ~451,000(~83%)

并指出节省随项目规模放大:1,000 文件项目的批次数更多,C1+C2 消除的重复量更大。计划还列出主要风险与缓解:扫描器漏解析(复杂再导出/动态导入)→ 漏边行为与旧版一致、可接受;大批次降低 summary 质量 → 影响仅限单文件分析深度、风险低;tour 剥离 function/class 节点 → 现有 tour 步骤只引用 file: ID,无兼容性问题;默认去掉 LLM reviewer → 内联脚本覆盖全部关键结构问题(悬空引用、缺层、重复 ID),LLM 的增量价值是孤儿节点、笼统 summary 这类非阻塞性质量警告。

方法论上,这份计划的价值在于三个可复用的模式:其一,把"每批次重复注入的确定性数据"上移到流水线最早的一次性阶段(importMap),下游只做切片消费;其二,用"门控 + 默认确定性路径"替代无条件的 LLM 调用(--review),把质量审核降级为 opt-in;其三,LLM 编排中的散文式脚本要求最终都收敛为确定性捆绑脚本 + 回归测试(extract-import-map.mjscompute-batches.mjs),从"提示词工程"走向"提示词 + 代码"的双轨实现——这正是计划蓝图(2026-03-27)与当前仓库(插件 2.9.4)之间的主要演化轨迹。

登录后查看全文
热门项目推荐
相关项目推荐