首页
/ Understand Anything `/understand-chat` 技能实战:基于知识图谱的代码库问答、高效读取策略与新鲜度校验原理

Understand Anything `/understand-chat` 技能实战:基于知识图谱的代码库问答、高效读取策略与新鲜度校验原理

2026-09-06 13:37:30作者:宗隆裙

本文以 Understand Anything 插件的 /understand-chat 技能文件(understand-anything-plugin/skills/understand-chat/SKILL.md)为核心,完整拆解这套"基于知识图谱回答代码库问题"的工作流:图谱 JSON 的五大区块结构、"先 Grep 后读取"的上下文经济学、七步执行指令中的 git 新鲜度校验细节,以及它与 core 包中 SearchEnginebuildChatContextgetGraphFreshness 等程序化实现之间的对应关系。读完本文,你既能复现这套问答流程的每一步操作,也能理解每个约束(如 -- . pathspec、^{commit} 校验)背后的工程动机。

1. /understand-chat 在技能体系中的位置

/understand-chat 是 Understand Anything 插件的一组"下游问答"技能之一。它的输入不是源码,而是 /understand 技能预先分析生成、落在项目数据目录中的知识图谱文件。README 中给出了典型的调用方式:

/understand-chat How does the payment flow work?

(见 README.md 的命令清单;在 Codex 平台上前缀是 $ 而非 /,即输入 $understand-chat。)

技能元数据(frontmatter)声明了它的触发语义:

name: understand-chat
description: Use when you need to ask questions about a codebase or understand code using a knowledge graph
argument-hint: "[query]"

argument-hint: "[query]" 说明该技能接收一个可选的查询参数 $ARGUMENTS,后续检索步骤会直接拿它作为关键词。

前置条件:数据目录中必须已存在 knowledge-graph.json。按当前仓库约定,数据目录为 .ua/;若项目里已存在旧的 .understand-anything/ 目录,则沿用该旧目录(legacy 兼容,避免强制迁移)。若图谱文件不存在,技能会提示用户先运行 /understand(见 understand-anything-plugin/skills/understand/SKILL.md,其 Phase 7 负责把最终图谱写入 $UA_DIR/knowledge-graph.json)。

理解这一点很重要:/understand-chat 本身不分析源码,它是一个只读消费者——它的全部智能体现在于"如何从一份可能很大的 JSON 里,用尽可能少的 token 捞出与问题最相关的那一小片子图"。

2. 知识图谱文件结构:五大区块与 ID 前缀约定

SKILL 文档的 "Graph Structure Reference" 一节定义了 knowledge-graph.json 的骨架,这是后续所有检索操作的基础,需要完整掌握:

  • project{name, description, languages, frameworks, analyzedAt, gitCommitHash}
  • nodes[] — 每个节点含 {id, type, name, filePath?, summary, tags[], complexity, languageNotes?}
    • 代码节点类型:filefunctionclassmoduleconcept
    • 非代码节点类型:configdocumentservicetableendpointpipelineschemaresource
    • 领域/知识节点类型:domainflowsteparticleentitytopicclaimsource
    • ID 使用节点类型作为前缀,例如 file:pathfunction:path:nameconfig:patharticle:path
  • edges[] — 每条边含 {source, target, type, direction, weight}
    • 关键类型:importscontainscallsdepends_onconfiguresdocumentsdeploystriggerscontains_flowflow_steprelatedcites
  • layers[] — 每个含 {id, name, description, nodeIds[]}
  • tour[] — 每个含 {order, title, description, nodeIds[]}

2.1 与源码类型定义的对照

上述结构与 core 包中的 TypeScript 类型定义一一对应,可对照 understand-anything-plugin/packages/core/src/types.ts 验证:

  • GraphNode(types.ts 第 54–67 行):必填 id/type/name/summary/tags/complexity,可选 filePathlineRangelanguageNotes,以及 domainMeta/knowledgeMeta/figmaMeta 三种扩展元数据。
  • GraphEdge(第 70–77 行):direction 取值 forward | backward | bidirectionalweight 为 0–1 的数值。
  • LayerTourStep(第 80–94 行):与 SKILL 文档描述的 nodeIdsorder 等字段完全一致。
  • 根对象 KnowledgeGraph(第 107–115 行):{version, kind?, project, nodes, edges, layers, tour}

一个值得注意的差异:SKILL 文档列举的节点类型共 18 种(5 代码 + 8 非代码 + 5 领域 + 5 知识),而当前 types.tsNodeType 实际声明了 27 种——多出的 6 种(pagescreencomponentcomponentSetinstancetoken)服务于 Figma 设计图谱,由 kind: "design" 区分。同理,EdgeType 目前覆盖 9 大类共 38 种边(含 Domain 类的 contains_flow/flow_step/cross_domain、Knowledge 类的 cites/contradicts/builds_on 等),SKILL 文档列出的只是问答场景下的"关键类型"子集。也就是说,/understand-chat 面对的图谱里可能出现领域视图、知识图谱乃至设计视图的节点,其 ID 前缀规则(domain:article: 等)与代码节点一致,检索策略同样适用。

2.2 用仓库自带的示例图谱直观理解

仓库内置了一份真实生成的图谱样本 understand-anything-plugin/packages/dashboard/public/knowledge-graph.json,其头部即可看到 project 区块的完整形态:

{
  "version": "1.0.0",
  "project": {
    "name": "understand-anything",
    "languages": ["typescript", "css"],
    "frameworks": ["React", "Vite", "TailwindCSS", "Zustand", "React Flow", "Monaco Editor", "Vitest", "Zod", "Fuse.js", "Dagre", "tree-sitter", "Anthropic SDK"],
    "description": "An open-source tool combining LLM intelligence with static analysis to produce interactive dashboards for understanding codebases.",
    "analyzedAt": "2026-03-14T16:15:02.617Z",
    "gitCommitHash": "58cfb20ac8f3f98cd7dede428d147dbe9cdc94b2"
  },
  ...
}

节点示例则体现了 ID 前缀约定的两种形态:

{
  "id": "file:packages/core/src/types.ts",
  "type": "file",
  "name": "types.ts",
  "filePath": "packages/core/src/types.ts",
  "summary": "Defines all core TypeScript interfaces and types for the knowledge graph system...",
  "tags": ["data-model", "types", "interfaces", "knowledge-graph", "plugin-api"],
  "complexity": "moderate"
}
{
  "id": "function:packages/core/src/schema.ts:validateGraph",
  "type": "function",
  "name": "validateGraph",
  "filePath": "packages/core/src/schema.ts",
  "lineRange": [72, ...],
  "summary": "Provides Zod runtime schema validation for all KnowledgeGraph data structures...",
  "tags": ["validation", "zod", "schema", "runtime-safety", "knowledge-graph"],
  "complexity": "moderate"
}

注意 function: 型节点的 ID 是 前缀:文件路径:符号名 三段式,且附带 lineRange——这让你在回答"这个函数在哪"时可以直接给出文件与行范围,而不必重新打开源码。

3. 高效读取图谱:为什么是"先 Grep、只读需要的部分"

SKILL 文档 "How to Read Efficiently" 一节给出了四条硬性纪律:

  1. 先用 Grep 在 JSON 中检索相关条目,再考虑读取文件(Use Grep to search within the JSON for relevant entries BEFORE reading the full file);
  2. 只读取你需要的部分——不要把整张图谱倒进上下文(Only read sections you need — don't dump the entire graph into context);
  3. 节点名与摘要是最有用的字段(Node names and summaries are the most useful fields for understanding);
  4. 边告诉你组件如何连接——沿 importscalls 追踪依赖链(Edges tell you how components connect — follow imports and calls for dependency chains)。

这四条本质上是上下文窗口经济学:一份完整图谱可能包含数百个节点与上千条边,全量读入既浪费 token,又会在检索时淹没关键信息。把 Grep 当作"图数据库的 WHERE 子句"、把"读文件"当作"只取命中的几行",可以把每次问答的上下文开销压到与问题规模成正比,而不是与代码库规模成正比。

仓库中恰好存在这套策略的程序化孪生实现,可以精确印证每条纪律:

const FUSE_OPTIONS: IFuseOptions<GraphNode> = {
  keys: [
    { name: "name", weight: 0.4 },
    { name: "tags", weight: 0.3 },
    { name: "summary", weight: 0.2 },
    { name: "languageNotes", weight: 0.1 },
  ],
  threshold: 0.4,
  includeScore: true,
  ignoreLocation: true,
  useExtendedSearch: true,
};

权重分配 name 0.4 > tags 0.3 > summary 0.2 > languageNotes 0.1 与文档"节点名和摘要最有用"的排序完全同向。

  • 纪律 1(先检索后读取)对应 SearchEngine.search() 的扩展查询处理:把空格分隔的 token 用 | 连接做 OR 匹配(第 44–47 行,如 "auth contrl" 变成 "auth | contrl"),默认返回上限 limit = 50,空查询直接返回空数组——即"检索是入口,全量数据从不进入结果"。

4. 七步执行指令逐步解析

SKILL 文档的 "Instructions" 是 /understand-chat 的操作主体,共 7 步。下面逐步解析,并指出每步在 core 包源码中的对应实现。

4.1 第 1 步:解析数据目录 $UA_DIR

UA_DIR=$([ -d .understand-anything ] && echo .understand-anything || echo .ua)

判断逻辑很直白:旧目录 .understand-anything/ 若已存在就用它,否则用新目录 .ua/。这与 /understand 技能 Phase 0 的解析规则(understand-anything-plugin/skills/understand/SKILL.md 第 124–128 行,UA_DIR="$PROJECT_ROOT/$([ -d ... ] ...)")保持逐字一致,保证读写两侧永远命中同一目录。随后检查 $UA_DIR/knowledge-graph.json 是否存在;不存在则停止并提示用户先运行 /understand——这是技能唯一的硬性前置闸门。

4.2 第 2 步:回答前先校验图谱新鲜度

这是整个技能中最精巧的一步。完整指令如下:

  1. 从图谱元数据读取 project.gitCommitHash,记为 GRAPH_COMMIT_RAW;在把它用于任何 Git diff 之前先将其解析为 commit,然后与 git rev-parse HEAD 比较,并从项目根检查"项目范围内"的已提交与工作区变更:
GRAPH_COMMIT=$(git rev-parse --verify --end-of-options "${GRAPH_COMMIT_RAW}^{commit}" 2>/dev/null)
git rev-parse HEAD
git diff --name-only "$GRAPH_COMMIT" HEAD -- .
git diff --cached --name-only -- .
git diff --name-only -- .
git ls-files --others --exclude-standard -- .
  1. -- . pathspec 是必须的:只动了 sibling monorepo 子项目的 commit,不应让本项目的图谱变陈旧;哈希不一致本身不构成陈旧,当"项目范围的 diff 为空"时依然视为新鲜。
  2. 所有命令输出中出现的所选数据目录(.ua/ 或旧 .understand-anything/)都要忽略,因为里面装的是生成的图谱产物,不属于项目源码漂移。
  3. 若已提交 diff 或任一工作区命令报告了项目文件,回答前必须告警:图谱派生的上下文可能遗漏这些变更;并建议运行 /understand 刷新图谱。
  4. 只有当 GRAPH_COMMIT_RAW 解析成功时才执行 commit diff;若图谱 commit 或 Git 元数据缺失、非法或不可用,给一个简短的 best-effort 告警后继续,而不是阻塞。

每条规则在 understand-anything-plugin/packages/core/src/staleness.ts 中都有精确对应的程序化实现,getGraphFreshness 函数(第 455–461 行)返回一个四态判别联合:

状态 含义 判定条件(staleness.ts 第 263–351 行)
fresh 图谱可信 项目范围已提交 diff 为空且无脏文件
dirty 有未提交变更 已提交 diff 为空,但 staged/unstaged/untracked 任一非空
stale 已落后 已提交 diff 非空,并附带 relation: behind | ahead | divergedcommitsBehind/commitsAhead
unknown 无法判定 Git 元数据不可读等五种 reason,调用方应"软告警"而非断言图谱最新

几个与 SKILL 文档逐条对应的实现细节:

  • -- . pathspec 与忽略数据目录:PROJECT_PATHSPEC(第 72–79 行)就是 ["--", ".", ":(exclude).understand-anything", ":(exclude).understand-anything/**", ":(exclude).ua", ":(exclude).ua/**"]——文档里那句"必须加 -- ."在这里落为四条 pathspec,monorepo 隔离与"图谱产物不算漂移"两个诉求同时满足。
  • ^{commit} 校验:evaluateGraphFreshness 中执行 git rev-parse --verify --end-of-options "${hash}^{commit}"(第 222–229 行),解析失败即落入 unknowngraph-commit-unavailable)——正是文档要求的"best-effort 告警后继续"。
  • 超时保护:所有 git 调用带 GIT_TIMEOUT_MS = 5_000 与 4MB 缓冲上限(第 70–71、98–99 行),超时归入 git-command-timeout 这一 unknown 原因。
  • behind/ahead/diverged 判定:用 git rev-list --left-right --count graph...head 加双向 merge-base --is-ancestor 探测(第 289–326 行),区分"单纯落后"、"图谱超前"与"历史分叉"三种陈旧形态。

更基础的同步原语也值得了解:同文件的 getChangedFiles/isStale(第 365–392 行)用 git diff <last>..HEAD --name-only 判断是否有文件变更,其测试用例 understand-anything-plugin/packages/core/src/tests/staleness.test.ts 覆盖了"无变更返回空数组"与"git 报错时返回空数组"两种边界(第 81–91 行),与文档"Git 失败不应阻塞问答"的原则一致。

实践要点:第 2 步的告警是回答前发生的——如果 diff 列出了文件,你应该先告诉用户"图谱可能缺少这些变更,建议 /understand 刷新",再基于现有子图作答。这与 dashboard 侧的新鲜度展示是同一套契约:understand-anything-plugin/packages/dashboard/src/freshness.ts 定义了同构的 GraphFreshnessResult 类型,注释明确写道 "Unknown is intentionally distinct from fresh: if Git metadata cannot be read, callers should warn softly rather than imply the graph is current."(unknown 刻意区别于 fresh:读不到 Git 元数据时应软告警,而不是暗示图谱是最新的。)

4.3 第 3 步:只读 project 元数据区块

用 Grep 或带行数上限的 Read 从文件顶部取出 "project" 区块即可——namedescriptionlanguagesframeworks 四样上下文足以让模型在"技术栈坐标系"里理解后续命中的节点,且成本只有几行。对应地,/understand 技能在 Phase 6 组装图谱时也是把 project 作为固定头部写入(understand-anything-plugin/skills/understand/SKILL.md 第 576–593 行),两端字段约定严格对齐。

4.4 第 4 步:按查询关键词检索相关节点

grep -i "query_keyword" <graph file>

文档给出三类检索面:

  • "name" 字段:直接命中标识符(函数名、文件名、类名);
  • "summary" 字段:语义级匹配("处理支付回调的模块"这类自然语言问题靠摘要);
  • "tags" 数组:主题级匹配("auth""middleware" 等主题标签)。

命中后记下所有匹配节点的 id——它们是第 5 步查边的钥匙。这一步与 SearchEngine 的 OR-token 检索互为表里:Grep 是确定性子串匹配,Fuse.js 的 threshold: 0.4 模糊匹配则允许拼写偏差与部分匹配,两者互补(模糊侧的权重分布见第 3 节引用)。

4.5 第 5 步:找到一跳相连的边

对第 4 步收集的每个节点 ID,在 edges 区块中 Grep 该 ID,得到:

  • 下游:它 import 或依赖的东西(importsdepends_oncalls 出边);
  • 上游:谁调用或 import 它(入边);
  • 二者合起来即"查询周围的一跳子图"(1-hop subgraph)。

这一步在程序化实现 understand-anything-plugin/src/context-builder.tsbuildChatContext(第 39–63 行)中有严格等价物:

// 2. Expand to connected nodes (1 hop via edges)
const expandedIds = new Set(matchedIds);
for (const edge of graph.edges) {
  if (matchedIds.has(edge.source)) expandedIds.add(edge.target);
  if (matchedIds.has(edge.target)) expandedIds.add(edge.source);
}
// 3. Collect edges where both endpoints are in the relevant set
const relevantEdges = graph.edges.filter(
  (e) => expandedIds.has(e.source) && expandedIds.has(e.target),
);

注意两个细节:一跳扩展是双向的(source/target 都检查),保证上下游都被纳入;随后只保留"两端都在相关集合内"的边,避免引入集合外悬空引用。buildChatContext 中搜索结果默认上限 limit = maxNodes ?? 15,比 SearchEngine 的 50 更小——问答场景刻意收紧候选节点数,进一步控制上下文体积。

4.6 第 6 步:读取层(layer)上下文

Grep "layers" 区块,弄清命中节点归属哪些架构层。层的定义来自 /understand 的 Phase 4(architecture-analyzer 产出,归一化后形如 {id: "layer:<kebab-case>", name, description, nodeIds[]}),/understand-chat 用它回答"这属于哪一层、为什么"。程序化实现在 context-builder.ts 第 65–68 行:

// 4. Find layers containing any relevant node
const relevantLayers = graph.layers.filter((layer) =>
  layer.nodeIds.some((id) => expandedIds.has(id)),
);

即"任一跳可达节点落在该层"的层被收集为相关层——注意是"相关层"而非"节点所在的全部层",与文档"解释相关层及原因"的要求一致。

4.7 第 7 步:只用相关子图作答

文档规定的回答纪律:

  • 引用图谱中具体的文件、函数与关系,而非泛泛而谈;
  • 解释涉及哪些架构层以及原因;
  • 简洁但完整——把概念锚定到真实代码位置(Be concise but thorough — link concepts to actual code locations);
  • 若查询没有命中任何节点,明说没命中,并从图谱中推荐相关术语,而不是编造答案。

这套指令与技能包中的程序化 prompt 构建器逐条呼应。understand-anything-plugin/src/understand-chat.tsbuildChatPrompt(第 8–29 行)生成的系统指令:

return [
  "You are a knowledgeable assistant that answers questions about a software codebase.",
  "Use the following knowledge graph context to inform your answer.",
  "Reference specific files, functions, classes, and relationships from the graph.",
  "If layers are present, explain which architectural layer(s) are relevant.",
  "Be concise but thorough — link concepts to actual code locations.",
  "",
  "---",
  "",
  formattedContext,
  "---",
  "",
  `**User question:** ${query}`,
].join("\n");

五条系统指令与 SKILL 文档第 7 步的四条回答纪律几乎逐句对应,且上下文渲染由 formatContextForPromptcontext-builder.ts 第 85–147 行)完成,输出结构固定为四段:项目头(名称/描述/语言/框架)→ ## Relevant Layers## Code Components(每个节点带 File/Complexity/Summary/Tags)→ ## Relationships(边渲染为 source --[type]--> target 的可读形式)。SKILL 文档描述的是"LLM 手工执行同一流程"的提示词版本,context-builder 则是"代码执行同一流程"的确定性版本,二者共享同一套上下文组装语义。

5. 把流程连起来:一次典型问答的完整时序

综合以上,/understand-chat How does the payment flow work? 的完整执行时序为:

  1. 定位数据目录UA_DIR=$([ -d .understand-anything ] && echo .understand-anything || echo .ua),确认 $UA_DIR/knowledge-graph.json 存在;
  2. 新鲜度检查:取 project.gitCommitHashgit rev-parse --verify --end-of-options "<hash>^{commit}" → 与 git rev-parse HEAD 对比 → 四条带 -- . 的 diff/ls-files 命令检查项目范围漂移 → 有漂移则先告警建议 /understand,Git 不可用则软告警继续;
  3. 取元数据:只读文件顶部 "project" 区块,获得技术栈坐标系;
  4. 检索节点:以 paymentflow 等关键词 Grep "name"/"summary"/"tags",收集命中节点 ID(代码库图谱中可能命中 file:src/payments/*.ts,领域图谱中则可能命中 domain:/flow:/step: 节点);
  5. 展开一跳:Grep 每个命中 ID 在 edges 中的出现,得到上下游依赖(沿 imports/calls 追依赖链);
  6. 补层上下文:Grep "layers" 确定归属层;
  7. 作答:引用具体文件/函数/边,说明层归属与原因;零命中则如实说明并推荐图谱中的相近术语。

每一步的"读多少"都由 Grep 命中量决定,而不是文件总大小——这就是该技能能在大型代码库图谱上保持低 token 开销的结构化原因。

6. 如何保持图谱新鲜(配套机制)

/understand-chat 的新鲜度告警指向的刷新手段在仓库中有明确出处:

  • 手动刷新:重跑 /understand。增量模式下它用 git diff <lastCommitHash>..HEAD --name-only 得到变更文件清单,只重新分析变更文件,然后把旧节点按 filePath 剔除、旧边按"source 或 target 命中被删节点"剔除、再合并新节点新边并更新 project.gitCommitHash——这套合并规则正是 staleness.tsmergeGraphUpdate(第 472–508 行)实现的逻辑,测试用例(staleness.test.ts 第 117–256 行)逐条验证了"变更文件节点被替换、悬空边被移除、未变更文件保留、analyzedAtgitCommitHash 被更新"四条断言。
  • 自动刷新/understand --auto-update 写入 autoUpdate: true$UA_DIR/config.json,由 post-commit 钩子在每个提交后增量修补图谱(见 understand-anything-plugin/hooks/hooks.jsonunderstand-anything-plugin/hooks/post-tool-use-auto-update.mjs),README 原文建议:"Keep it fresh: enable /understand --auto-update — a post-commit hook incrementally patches the graph so each commit lands with a matching graph."

7. 速查表

要素 内容 出处
触发命令 /understand-chat [query](Codex 用 $understand-chat SKILL.mdREADME.md
输入文件 .ua/knowledge-graph.json(或 legacy .understand-anything/knowledge-graph.json SKILL.md 第 9 行
前置闸门 图谱不存在 → 提示先运行 /understand SKILL.md 第 34 行
数据目录解析 `[ -d .understand-anything ] && echo .understand-anything
新鲜度命令 git rev-parse --verify --end-of-options "<hash>^{commit}" + 四条 -- . diff 命令 SKILL.md 第 39–44 行、staleness.ts
陈旧判定核心 "哈希不一致 + 项目范围 diff 为空" = 不陈旧 SKILL.md 第 46 行、staleness.ts 第 263–287 行
读取纪律 先 Grep 后读;只读需要部分;name/summary 优先;沿 imports/calls 追边 SKILL.md 第 25–30 行
节点 ID 前缀 file:pathfunction:path:nameconfig:patharticle:path SKILL.md 第 19 行、types.ts
检索权重(程序化侧) name 0.4 / tags 0.3 / summary 0.2 / languageNotes 0.1,threshold 0.4 search.ts
一跳子图 双向沿边扩展;问答默认候选上限 15 节点 context-builder.ts
回答纪律 引用具体代码位置、解释层归属、零命中如实说明 SKILL.md 第 66–70 行、understand-chat.ts

8. 小结

/understand-chat 的价值不在检索算法本身——它只是 Grep 加一跳展开——而在于它把上下文预算答案可信度两件事制度化:用"先 Grep 后读取、只取子图"控制 token 开销,用"^{commit} 校验 + -- . pathspec + 数据目录排除 + 软告警不阻塞"的新鲜度协议控制错误答案风险。core 包中的 SearchEngine(加权模糊检索)、buildChatContext(15 节点上限 + 双向一跳 + 相关层收集)、getGraphFreshness(fresh/dirty/stale/unknown 四态)与 staleness 测试用例,为这套提示词流程提供了逐条可验证的程序化对应,使"技能文档怎么写"与"库代码怎么实现"互为注脚,也给出了把同类"图谱问答"模式移植到其他项目时的完整参照。

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