首页
/ Understand-Anything 仪表盘的宽容图加载:一条让 LLM 生成知识图谱“残缺可用”的四层容错管道

Understand-Anything 仪表盘的宽容图加载:一条让 LLM 生成知识图谱“残缺可用”的四层容错管道

2026-09-05 17:06:42作者:薛曦旖Francesca

这篇文章解析 Understand-Anything 项目为 dashboard 设计的“宽容加载(Permissive Graph Loading)”方案:当 LLM Agent 生成的 knowledge-graph.json 偏离严格 Zod Schema 时,如何用“静默清洗 → 自动修复 → 逐项丢弃 → 致命兜底”四层管道最大化保留可渲染内容,并用琥珀色警告横幅替代红屏报错。读完本文,你能完整理解这套管道在 packages/core 中的逐层实现(sanitizeGraph / autoFixGraph / validateGraph)、WarningBanner 组件的交互细节,以及如何让 Agent 针对具体问题定向修复而不必整体重跑 /understand

问题背景:空白屏幕与无法归因的错误

Understand-Anything 的工作流是:Agent 分析代码库并生成 knowledge-graph.json,dashboard(React + Vite 应用,位于 dashboard 包)加载该文件并渲染交互式知识图谱。设计文档 Dashboard Robustness 计划 指出的原始痛点是:

当 LLM Agent 生成的 knowledge-graph.json 偏离严格的 Zod schema 时,dashboard 显示一个空白屏幕和难以理解的 Zod 错误路径。用户无法判断这是系统 bug 还是 Agent 生成问题,唯一的补救手段是整体重跑 /understand

这个问题的本质是严格校验(strict validation)与 LLM 输出的固有噪声不匹配——模型会遗漏可选字段、把数字写成字符串、给枚举值随意加大小写。一旦整图校验失败,哪怕 99% 的节点是合法的,用户也只能看到一片空白。

设计目标

原计划文档明确了三条目标,它们贯穿整个实现:

  1. 最大化用户可见内容——即使部分节点/边损坏,也要加载有效部分;
  2. 清晰传达生成质量问题——用琥珀色警告(而非红色错误)展示可复制粘贴(copy-paste-friendly)的问题消息;
  3. 支持定向修复——用户可复制问题报告,让 Agent 只修复具体条目,而不是整体重跑。

四层容错管道总览

原计划文档定义的管道如下:

Raw JSON → Sanitize (Tier 1) → Normalize + Auto-fix (Tier 2) → Validate per-item (Tier 3) → Fatal check (Tier 4) → Dashboard

schema.ts 中,这条管道由三个函数串联:sanitizeGraphnormalizeGraph + autoFixGraph,以及 validateGraph 中的逐项校验与致命检查。validateGraph 的入口清晰地标注了各层级的调用顺序(见 validateGraph):

export function validateGraph(data: unknown): ValidationResult {
  // Tier 4: Fatal — not even an object
  if (typeof data !== "object" || data === null) { ... }
  const raw = data as Record<string, unknown>;

  // Tier 1: Sanitize
  const sanitized = sanitizeGraph(raw);

  // Existing: Normalize type aliases
  const normalized = normalizeGraph(sanitized) as Record<string, unknown>;

  // Tier 2: Auto-fix defaults and coercion
  const { data: fixed, issues } = autoFixGraph(normalized);
  // ... Tier 4 fatal checks、Tier 3 per-item validation ...
}

Tier 1:静默清洗(Sanitize Silently)

针对“纯噪声”类的 LLM 小毛病——修复但不上报。原计划文档列出的处理项与 sanitizeGraph 的实现一一对应:

问题 修复方式
可选字段上的 nullfilePathlineRangedescriptionlanguageNotes 删除该键(等效于 undefined
枚举字符串大小写混乱("Forward""SIMPLE" 匹配前统一转小写

实现上的具体行为:

  • 节点上的 filePath / lineRange / languageNotesnull 时直接 delete;边上的 description 同理;
  • 节点的 typecomplexity 和边的 typedirection 若是字符串则 toLowerCase()
  • 顶层 tour / layersnullundefined 时统一置为 [](null 与空数组的语义差异在此抹平);
  • 非对象条目(如数组里混入的 null、字符串)原样透传,留给后续层级判定。

对应测试位于 schema.test.tsdescribe("sanitizeGraph") 块,覆盖了 null 字段转 undefined、枚举小写化、tour/layers 置空数组等全部场景(见 sanitizeGraph 测试)。

Tier 2:自动修复并记录 Info 级问题(Auto-fix With Info Notice)

可恢复的问题——应用合理默认值,并追踪为 auto-corrected 问题。原计划文档的默认值表如下,且与 autoFixGraph 的实现完全一致:

问题 默认值 说明
缺少 complexity "moderate" LLM 最常见的遗漏
缺少 tags [] 空数组是合法值
缺少 weight 0.5 0–1 区间的中值
weight 为字符串 强制转为数字 例如 "0.8"0.8
缺少 direction "forward" 安全的默认方向
缺少 summary 使用节点的 name 好过留空
tour: null / layers: null [] null 与空数组的区分(实际在 Tier 1 完成)
complexity 别名 low/easy→simplemedium/intermediate→moderatehigh/hard→complex COMPLEXITY_ALIASES
direction 别名 to/outbound→forwardfrom/inbound→backwardboth→bidirectional DIRECTION_ALIASES
已有的节点/边类型别名 已由 normalizeGraph 处理 无需改动
节点缺少 type "file" 安全的兜底类型
边缺少 type "depends_on" 通用兜底类型

从源码结构看,别名表被定义为独立的导出常量,便于测试直接断言:COMPLEXITY_ALIASESDIRECTION_ALIASES。每一处修复都会 push 一条结构化的 GraphIssue,消息格式统一为 nodes[3] ("AuthService"): missing "complexity" — defaulted to "moderate",并附带 path(如 nodes[3].complexity)供定位。

normalizeGraph 则负责更大范围的正则化:把 LLM 高频生成的非规范类型名映射回规范枚举,例如 func/fn/method → functioninterface/struct → classextends → inheritsinvokes → callsuses → depends_on 等,完整映射见 NODE_TYPE_ALIASES 与 EDGE_TYPE_ALIASES。节点类型枚举本身有 25 个取值(覆盖代码、基础设施、领域建模、知识图谱、设计稿五类图谱),边类型则有 38 个取值横跨 9 个语义类别,定义在 EdgeTypeSchema

Tier 3:丢弃并记录警告(Drop With Warning)

无法安全猜测的内容——移除该条目,追踪为 dropped 问题。原计划文档的处理表:

问题 动作
边引用了不存在的节点 ID 丢弃该边
节点缺少 id 丢弃该节点
节点缺少 name 丢弃该节点
边缺少 sourcetarget 丢弃该边
无法识别的 type 值(不在规范或别名表中) 丢弃该条目
weight 无法强制为数字 丢弃该边

validateGraph 的 Tier 3 段 中,这通过“逐项 safeParse”实现:先用 GraphNodeSchema 逐个解析节点,失败的记为 droppedcategory: "invalid-node");随后用存活节点的 id 集合构建 nodeIds,对每条通过 Schema 校验的边再做引用完整性检查——sourcetarget 不在集合中即丢弃并记录 category: "invalid-reference"。此外还有两层更细的保全处理:layerstournodeIds 中指向已丢弃节点的 ID 会被静默过滤(整层/整步骤保留),这比直接丢弃整个 layer 更能保留可用结构,对应测试见 permissive validation 测试块

Tier 4:致命错误(Fatal)

图谱已无法挽救——显示红色错误横幅。原计划文档的触发条件:

条件 消息
过滤后有效节点数为 0 "No valid nodes found in knowledge graph"
完全缺少 project 元数据 "Missing project metadata"
输入不是对象 / 不是合法 JSON "Invalid input format"

validateGraph 中这三类检查分别对应:非对象输入直接返回 fatal: "Invalid input: not an object"L565-L568);ProjectMetaSchema.safeParse 失败返回 fatal(实现中措辞为 "Missing or invalid project metadata",覆盖了文档中的 "Missing" 场景);节点逐项过滤后 validNodes.length === 0 返回 fatal。

从源码结构看,实现中还比原计划多覆盖了一类致命情形:nodes / edges / layers / tour任何一个存在但不是数组(例如 edges 被生成为单个对象),会立即返回 category: "invalid-collection" 的 fatal 问题(相关检查),并有对应测试断言 "edges" must be an array when present。这属于计划落地时的合理补充,逻辑上同属 Tier 4。

返回类型:GraphIssue 与 ValidationResult

原计划文档定义的返回结构与 schema.ts 中的实际接口 一致:

interface GraphIssue {
  level: 'auto-corrected' | 'dropped' | 'fatal';
  category: string;      // e.g., "missing-field", "invalid-reference", "type-coercion"
  message: string;       // human-readable, copy-paste friendly
  path?: string;        // e.g., "nodes[3].complexity"
}

interface ValidationResult {
  success: boolean;
  data?: KnowledgeGraph;
  issues: GraphIssue[];
  fatal?: string;
}

实现中额外保留了一个 @deprecatederrors?: string[] 字段——它把所有 issue 消息扁平化为字符串数组,仅为兼容旧调用方(测试中有专门用例 preserves deprecated errors for dropped-item callers 守护这一契约)。严格 Zod Schema 本身没有放松:GraphNodeSchema 仍要求 idnamesummarytagscomplexity 等字段,GraphEdgeSchema 仍约束 weight: z.number().min(0).max(1)Schema 定义)——宽容性全部来自 Schema 之前的预处理层和 Schema 之后的逐项收集策略,这让“严格模式”与“宽容模式”共享同一套类型定义。

Dashboard UI:WarningBanner 组件

计划要求在 packages/dashboard/src/components/WarningBanner.tsx 新增组件,当前仓库中该组件已实现(WarningBanner.tsx),视觉与交互设计逐条对应原计划:

  • 琥珀/金色主题:默认 bg-amber-900/20 border-amber-700 text-amber-200,与 dashboard 的金色强调色一致,语义是“生成质量问题”而非“系统崩溃”;
  • 默认折叠:汇总行仅显示计数,例如 "Knowledge graph loaded with 5 auto-corrections and 2 dropped items"——汇总文案由组件动态拼接,只提及计数大于 0 的类别(summary 构建逻辑);
  • 可展开:点击后按 Fatal → Auto-corrected → Dropped 三个分组展示问题列表,各组配不同图标与颜色(红色警告图标 / 琥珀色对勾 / 橙色叉号);
  • 一键复制buildCopyText 把全部问题格式化为可直接粘贴给 Agent 的报告,通过 navigator.clipboard.writeText 写入剪贴板,复制成功后按钮短暂显示 "Copied!";
  • 可操作的页脚:非 fatal 场景提示 "Copy these issues and ask your agent to fix them in knowledge-graph.json"。

复制粘贴的问题报告格式

原计划文档规定的输出模板,与 buildCopyText 的实际产出一致:

The following issues were found in your knowledge-graph.json.
These are LLM generation errors — not a system bug.
You can ask your agent to fix these specific issues in the knowledge-graph.json file:

[Auto-corrected] nodes[3] ("AuthService"): missing "complexity" — defaulted to "moderate"
[Auto-corrected] nodes[7] ("utils.ts"): missing "tags" — defaulted to []
[Auto-corrected] edges[12]: weight was string "0.8" — coerced to number
[Dropped] edges[5]: target "file:src/nonexistent.ts" does not exist in nodes
[Dropped] nodes[14]: missing required "id" field — cannot recover

每条 issue 前缀 [Level] 标签按 auto-corrected / dropped / fatal 三级渲染,排序时 fatal 优先(对用户最可操作)。这是整个方案“赋能定向修复”目标的关键落点:问题报告本身就是给 Agent 的修复工单。

Fatal 的视觉升级与分流

原计划规定 fatal 错误保持红色(bg-red-900/30),并提示重新运行 /understand;网络/JSON 解析失败的既有红色横幅不变(那才是真正的系统/基础设施问题)。实现中有一条演进值得注意:WarningBanner 内部对“issues 里含 fatal 级别条目”的情形做了二次分流(fatal 分支)——若 issue 携带 fatal 级别,横幅升级为红色,复制文案也改为引导用户提交 bug 报告(注释明确说明:渲染阶段的 fatal 是 dashboard 自身缺陷,例如布局引擎失败,不应让 Agent 去“修” knowledge-graph.json)。也就是说,“LLM 生成错误”与“dashboard 渲染错误”最终被分到了两条完全不同的用户行动路径,这是比原计划更精细的归因设计。

App.tsx 接线

原计划的 App.tsx 改动(成功且有问题时显示 WarningBanner 并正常加载;fatal 时显示红色横幅;console.warn / console.error 分级输出日志)在 App.tsx 中已落地,关键链路如下:

  1. 从数据服务 fetch knowledge-graph.json;在解析前先检查 res.ok——否则 404 的 JSON 错误体会被 validateGraph 误判为 "Missing or invalid project metadata",代码注释中明确记录了这一防护的动机(引用了 issue #288、#406 的背景);
  2. 调用 validateGraph(data):成功且 result.data 存在时 setGraph(result.data) 并把 result.issues 存入 state,随后对每条 issue 按级别输出 console.warn(auto-corrected)或 console.error(dropped)(加载逻辑);
  3. result.fatal 时走既有红色错误横幅路径(Invalid knowledge graph: ${result.fatal});
  4. 渲染层在 loadError 为空时,只要 issues 非空就渲染 <WarningBanner issues={allIssues} />横幅渲染)。

同一套 validateGraph 也被复用于 domain-graph.json 的加载(失败仅 console.warn 降级,不阻断主图),从源码结构看,这条宽容管道已成为 dashboard 多数据源加载的公共底座。

测试覆盖

原计划要求所有层级测试集中在 packages/core/src/__tests__/schema.test.ts,当前测试文件(schema.test.ts)的覆盖与计划的“Test Coverage”清单逐条对应并有增量:

  • Tier 1null 可选字段静默变 undefined、枚举字符串小写化、tour/layers 置空数组、非对象条目透传;
  • Tier 2:缺 complexity/tags/summary/type/direction/weight 各得默认值且 issue 被记录;字符串 weight 强制转换("0.8"0.8);全部 complexity / direction 别名的映射表逐一断言;
  • Tier 3:悬空边引用丢弃、缺 id 节点丢弃、非法 type 节点/边丢弃且 issue 记录、layer/tour 中悬空 nodeIds 过滤;
  • Tier 4:过滤后 0 有效节点 → fatal;缺 project → fatal;输入非对象 → fatal;edges 存在但非数组 → fatal;
  • 集成:好坏节点混合的图 → 加载成功且节点计数与 issues 列表都正确(loads graph with mixed good and bad nodes);“原本严格校验必挂”的脏图(大写类型、null 字段、字符串 weight 混合出现)→ 全部 auto-corrected 后成功加载;
  • 回归守护:别名表无链式映射(alias 的值不得再是 alias 的键)、deprecated errors 字段兼容性。

实现边界:改了与不改的文件

原计划的 “Files Changed” 与 “Files NOT Changed” 清单:

文件 变更
packages/core/src/schema.ts 清洗、扩展的 normalize、宽容 validate、新类型
packages/dashboard/src/components/WarningBanner.tsx 新组件
packages/dashboard/src/App.tsx 把 issues 接入 WarningBanner
packages/core/src/tests/schema.test.ts 全部层级的测试

明确不改的边界:

  • Agent prompts——留待后续作为独立工作收紧生成质量(管道是兜底,不是替代品);
  • GraphView / store 逻辑——它们本就只消费合法的 KnowledgeGraph 对象,无需感知校验细节;
  • 既有节点/边类型别名表——保留并围绕其扩展,不破坏已有序列化数据。

后续演进:从源码结构看到的三处延伸

对比计划文档与当前源码,有三处实现是计划之后长出来的能力,理解它们有助于把握这套管道的演进方向:

  1. weight 区间钳制autoFixGraph 在类型转换之外还会把越界的数值 weight 钳制到 [0, 1] 并记录 category: "out-of-range" 的 issue(钳制逻辑)。原计划对“无法转数字的 weight”的处理是丢弃边,而实现选择了更宽容的路径——"not_a_number" 这类字符串直接默认 0.5(测试 handles non-parseable string weight by defaulting to 0.5 固化了这一行为),真正的丢弃只留给 Schema 层面的结构性错误;
  2. kind 感知的别名表pageinstance_of 在 design 图谱(Figma 设计稿)中是一等公民类型,但在 knowledge 图谱中长期依赖 page → articleinstance_of → exemplifies 的归一化。normalizeGraph 依据顶层 kind 字段切换别名表(kind 分支),并专门处理了 componentSet 这个唯一 camelCase 节点类型在 Tier 1 小写化后的还原,配套测试见 kind-aware alias normalization 块。这体现了该方案的一个不变量:宽容层必须对图谱“种类”敏感,避免为一种图谱修的默认值污染另一种图谱
  3. fatal 的语义分流:如前所述,WarningBanner 把“issues 内 fatal”(dashboard 自身渲染缺陷)与“validateGraph 返回 fatal”(图谱不可救)区分开,前者引导提 bug,后者引导重跑 /understand

小结

这套宽容加载管道的设计要点可以概括为:严格 Schema 不变,宽容性前置到预处理层、后置到逐项收集策略;每个修复/丢弃动作都产出结构化、可复制、可定位(带 path)的问题记录;UI 用颜色(琥珀 vs 红)承担“生成问题 vs 系统问题”的归因,用一键复制把问题报告变成给 Agent 的定向修复工单。对于任何“LLM 生成结构化数据 + 前端强类型校验”的场景,这四层管道(静默清洗 / 默认值修复 / 逐项丢弃 / 致命兜底)与 GraphIssue 的问题模型,都是一个可以直接参考的落地范式。

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