拆解 Archify:一个零依赖图表渲染器,如何让 AI 生成的每张图都带"出厂检验"

0 阅读14分钟

拆解 Archify:一个零依赖图表渲染器,如何让 AI 生成的每张图都带"出厂检验"

本文基于 Archify v3.0.1 的源码分析。这是一个 GitHub Trending 周榜第一的开源项目:输入一段描述,AI Agent 调用它生成可交互的架构图、时序图、工作流图,产物是一个自包含 HTML。真正值得看的不是它能画图,而是它为"让 AI 的输出可信"所做的工程决策——约十万行代码里,测试超过六万行;一次交付要过四道门;写一个文件用了硬链接、文件描述符绑定和崩溃恢复协议。原作者在github上的仓库链接:https://github.com/tt-a1i/archify。

先说问题:AI 画图的三个信任缺口

过去一年,让大模型生成图表的工具层出不穷。用起来很爽,但要把产物带进设计评审,问题马上出现:

第一,布局不可控。自动布局算法追求"没有节点重叠"这个最低目标,十台服务堆成放射状,主路径和次要关系一样粗细,读者抓不住故事。

第二,失败不可修。Agent 生成一张图,渲染器报了一屏 Node.js 堆栈,Agent 只能猜哪里错了,猜错就整图重画,越改越偏。

第三,产物不可验。图上画了一条 API 到数据库的连线,这条线是真实代码里存在的,还是模型顺口编的?读者无从分辨。

Archify 的全部设计可以看成对这三个缺口的回应。它把自己定义成"技术仪器"(technical instrument)而非绘图套件,下面七个决策值得逐一拆开看。

agent用Archify能力,对其他项目代码仓库进行分析后生成可交互的架构效果图:

24c9b2d6e61700f4104aecfbe172c36a.png

决策一:Typed JSON IR,把自由关在 Schema 里

Archify 的输入既不是自然语言,也不是 Mermaid 文本,而是一份强类型的 JSON:

{
  "schema_version": 1,
  "diagram_type": "architecture",
  "meta": { "title": "Sample Web App", "output": "web-app.html", "quality_profile": "showcase" },
  "components": [
    { "id": "api", "type": "backend", "label": "API Server", "pos": [670, 300], "size": [130, 60] }
  ],
  "connections": [
    { "id": "jwt-verification", "from": "auth", "to": "api", "variant": "security" }
  ]
}

五种图表各有一份 JSON Schema 2020-12 文件,最严的一条是每层对象都 additionalProperties: false。字段拼错、枚举值乱写、漏了必填字段,在校验阶段就被挡住。

这层中间表示(IR)同时服务三个读者:人可以读和改,Agent 可以做局部修复,渲染器拿到的永远是形状确定的数据。Mermaid 在这里只作为语义参考——Agent 读懂 Mermaid 的拓扑含义后重新创作 IR,而不是机械套用 Mermaid 的样式。项目明确拒绝做"Mermaid 美化器",因为换主题不改善信息架构。

有意思的是 Schema 校验本身的工程处理。项目用 AJV 做校验,但 AJV 只活在开发依赖里:构建脚本把五份 Schema 编译成一个独立 ESM 模块,连 AJV 运行时里那个 ucs2length 辅助函数都被抠出来内联,生成结束后断言产物里不存在任何 require(。最终分发到用户机器上的代码,运行时依赖为零,只有 Node.js 标准库。

决策二:人负责叙事,机器负责几何

通用自动布局是 Archify 刻意不做的东西,但它也没有把所有坐标都甩给人。它把"画图"拆成了两层职责:

叙事层交给作者(人或 Agent):主路径是哪条、哪些是分支和回流、节点按什么层级相邻、留白在哪里。项目的 Agent 创作手册把这套经验固化成顺序——先给每条关系分类(主路径、分支/存储、回流、第二入口、扇出),再决定落位,最后才写坐标。

几何层全部交给渲染器确定性求解:共享一个连接面的多个端点自动均匀展开,扇出的兄弟节点被分进平行通道,一排节点挡住去路时走统一的竖向绕行,标签按实测文本宽度避让。所有几何判定共用 geometry.mjs 里的原语和 spatial-grid.mjs 的空间索引:矩形重叠比较带着 0.0001 的浮点容差(有 issue 记录过求解器恰好以最小间距分开的两列被误判重叠的 bug),标签和路线之间要留出像素级净空。

这个切分的产品后果是:同一套输入在任何机器上渲染出完全相同的图,但烂故事不会被自动布局救成好图。渲染器还会反过来给作者提建议——finalize 发现图里存在被自动消解的交叉时,回执给出"共享节点是哪个、哪个连接面太挤"的测量证据,允许一次只动节点位置和尺寸的受限修复;关系、标签、源码证据一律不动,再不过就恢复原版,不许摆位摆到第三轮。

决策三:错误信息是给机器读的契约

这是整个项目里我认为最值得借鉴的设计。

普通 CLI 的错误是给人看的字符串。Archify 的错误是一个稳定的数据结构:

{
  "code": "layout/constraint",
  "severity": "error",
  "subject": { "nodeId": "api", "surface": "route" },
  "evidence": { "actual": 4.2, "required": 12, "relation": "clearance" },
  "supportedFixes": ["move the named node", "shorten the label", "switch the connection side"]
}

四个字段各司其职:code 跨版本稳定,Agent 可以按规则码决策;subject 精确指向出问题的节点和字段,修复时只改这个对象和它的直接邻域;evidence 是测量值(实际净空 4.2px,要求 12px),不是"间距太小"这种形容词;supportedFixes 列出这道规则真正允许的修复旋钮,不在列表里的动作不许做。

渲染器进程入口装了诊断边界,任何异常最终都被归一成这种结构,Agent 永远不会收到 Node 堆栈。配合 Skill 契约里"最多两轮聚焦修复、禁止整图重写"的纪律,修复过程从"看图猜谜"变成了定位问题、查可用手段、执行、重跑的循环。

这个设计的影响超出了错误处理本身:它定义了人和 Agent 之间的分工接口。人定义规则(什么算缺陷、能怎么修),机器执行规则并在失败时汇报结构化事实。

决策四:用硬链接计数守护每一次文件替换

如果说前面三个决策还属于架构审美的范畴,原子输出模块(renderers/shared/atomic-output.mjs)就是纯粹的硬核系统编程了。它要保证一件事:目标路径上的 HTML,永远要么是上一份完整文件,要么是新一份完整文件,不会有中间态。

朴素做法"写临时文件再 rename 覆盖"在这里被认为不够安全:Windows 上 rename 语义不一致,无法证明被替换的文件就是刚校验过的那个,安全软件和同步盘还可能在背后移动或复活文件。Archify 的答案建立在一个 Unix 时代的事实上——硬链接计数(nlink)。

发布新文件前,它先用 O_RDONLY | O_NOFOLLOW 打开候选文件并持有描述符,fstat 检查 dev/ino、模式、大小,分块算 SHA-256,然后要求 nlink 精确等于 1:为 0 说明身份不可知,大于 1 说明文件在别处还有名字,替换一个名字没法保证安全,直接拒绝发布。

删除旧文件也不能"验完再删",两步之间有竞争窗口。它的协议是先把文件挪进一个名字随机、权限 0700 的私有隔离目录(.archify-remove-<32位随机十六进制>),再通过一直开着的描述符验证隔离区里的 inode 就是自己绑定的那个,确认无误才 unlink。如果移动瞬间发现公开名字被别的进程重新占了,就把"继任者"用硬链接恢复回去并报告 preserved,绝不误删别人的文件。

完整发布流程像一个微型事务:

  1. 在隔离区给旧文件建硬链接备份(两个名字都要验证 nlink=2),写入 publication-recovery-v1.json 恢复记录并 fsync,POSIX 上还要 fsync 父目录;
  2. 把候选硬链接到目标路径,在两个名字上分别验证同一 inode、nlink=2;
  3. 退役暂存名,目标路径恢复 nlink=1,做最终身份、模式、字节校验;
  4. 清理备份和恢复记录。任一步失败就回滚:摘掉新名字、恢复旧文件、按结果保留或作废恢复记录。

代码注释还诚实标注了协议的边界:替换已存在文件"可恢复但不是崩溃原子的",旧名退役和新名链接之间公开名短暂缺席——因为可移植 Node.js 没有能同时保护迟到竞争者的替换型 CAS。Windows 上连 fstat 和 lstat 的差异都单独处理:Windows 的 lstat 在拿不到句柄时会回退成目录枚举并报告一个合成的 nlink=1,代码只信句柄支撑的 fstat,对不上就失败关闭。

失败关闭(fail closed)是整个模块的基调:身份存疑、链接数存疑、摘要对不上,宁可不发布,留下恢复材料让用户处理。在真实世界里这套机制确实会咬人——某些安全软件的删除保护会把删除的文件以硬链接形式放进回收站,导致 nlink 永远大于 1,发布被按设计阻断;解决办法是把项目目录加入安全软件白名单。

决策五:finalize,一条命令串起四道门

单个校验函数再严,也挡不住"校验的是 A、发布的是 B"。所以 Archify 提供了一个编排命令 finalize,把四个阶段串成流水线,在第一道失败的门前停下:

validate   Schema + 源码证据 + 布局几何
   ↓
deliver    合成 HTML/SVG、组合检查、原子发布
   ↓
check      重新打开磁盘上的成品做严格静态校验和 provenance 校验
   ↓
browser-check  真实浏览器加载,验证 Viewer 运行时真的能跑

这里有一个防作弊细节:每个阶段的回执都带交付关联 ID,finalize 会校验回执的命令名、图表类型、检查数量和产物归属——你不能拿昨天另一次运行的通过回执冒充这次的结果。

Agent 的标准工作流因此非常简单:写好 JSON,跑一次 finalize,过了就交付,不过就照着结构化诊断修。浏览器门禁之外还有一个视觉复核层(visual-check),按多视口、多主题截图,但项目反复强调一条纪律:没有人真正看过截图,就只能声称"自动检查通过",机器检查和人工感知检查在回执里是分开报告的。

决策六:728KB 一个文件,把便携性做成默认值

产物 HTML 的模板大约 728KB,打开看一眼内容就明白体积花在哪:字体是 JetBrains Mono 的 WOFF2 子集,以固定字节内联,并且显式不使用 CSS 的 local() 源——本机装了同名字体也不能覆盖内置字形,保证任何人打开看到的版面一致;样式、SVG、Viewer 交互脚本、多语言消息全部内联。一个文件发出去,对方双击就能用全部交互,不需要装任何东西、不需要服务器、不需要网络。

交互层做得相当完整:点击节点看详情护照(Semantic Passport),沿作者定义的关系追上游/下游可达范围,探查两节点间的有向路径,用语义透镜对比角色,还有演示模式、全局雷达、深链恢复(#route=web~db 这样的状态直接编码在 URL fragment 里)。

但功能多并没有放松一条原则:交互不编造拓扑。图上可达就是可达,文案只写"节点数、连线数、最大跳数",绝不说成"爆炸半径"或"故障影响"。导出的分享卡片也在标题里写明自己的作用域——这是一次带视角的阅读,不是规范全图。运行时的发光、临时面板、镜头状态一律不进导出,静态帧本身必须讲完整故事。

连颜色都有语义纪律:青是前端和焦点、绿是后端和已验证、紫是存储、玫瑰红是安全边界,饱和色只表达类别,不做装饰;深浅主题和四个视觉预设(经典、信号流、编辑风、蓝图)只换材质,几何和信息优先级不动。

决策七:源码证据钉在 40 位 SHA 上

回到开头的第三个缺口:怎么证明图上的线在真实代码里存在。

Archify 的答案是让节点可选携带 sources,渲染时用 --repo-root 指向本地仓库,由 Git 做校验。实现里有几个讲究的细节:所有 Git 命令统一带 --no-replace-objects,忽略本地 replacement refs,防止有人用 git replace 篡改历史文件内容骗校验;证据必须钉在 40 位完整 SHA 上;校验批量执行——先一次 cat-file --batch-check 会话核对所有引用对象的类型,再只对需要验证行内容的 blob 读内容,单个 blob 上限 16MB;路径必须在仓库内,行号必须落在那个固定版本文件的真实行数里。

通过校验的节点在图上只显示一个很小的 SRC n 胶囊,点开是固定到公开 commit 的文件与行号链接。证据是显式开启的能力,普通图不带任何源码信息。它的措辞边界同样小心:证据只陈述"作者在该提交读到过这些文件和行",不推断代码当前的行为。

十万行代码里,测试占六万多

最后说一组让我印象深的数字:项目 221 个 JS 文件约 10.4 万行,其中 170 个测试文件、6.4 万行测试,测试与实现代码比大约 1.6:1。测试只用 Node.js 内置测试运行器,不引框架。

测试的覆盖面和它的设计焦虑完全对应:几何与布线有大量单元测试冻结判定结果;诊断码、CLI 输出格式、回执结构有契约测试,Agent 依赖的接口被焊死;原子输出有专门的恢复测试,人工制造崩溃和竞争;提交在仓库里的示例产物有 golden 测试,一个非预期的字节变化都能被发现;Viewer 的相机、动效、导出清理由真实浏览器测试把关。Schema 改了但忘记重新生成验证器、模板漂移、版本徽章不一致,都会在 test 脚本开头的四组生成物一致性检查里失败。

仓库里还有两组持续运行的基准实验:一组测量普通模型一次写出合格 IR 的下限,支撑"不依赖顶尖模型也能用";一组给 Agent 喂真实缺陷、统计门禁引导下修复所需的轮次和 token 消耗,验证"两轮修复上限"到底够不够。

给 Agent-first 软件的几点启示

拆完这个项目,我觉得它对正在做 AI 编码工具的人有三个直接可借的思路。

把不确定性留在生成阶段,把确定性做进交付阶段。 IR 允许 Agent 自由叙事,但从校验到发布全是确定性代码把关,人(和机器)不必信任模型,只需要信任门禁。

为非人类参与者设计接口。 结构化诊断不是锦上添花的日志,它是 Agent 的 API;稳定规则码、精确对象、测量证据和枚举修复手段,让自动化修复成为可能。错误信息的读者是机器,这是过去十年工具设计里很少被认真对待的视角。

在"无法保证"面前显式划线。 Archify 频繁地说不:不推断运行时影响、不做崩溃原子的承诺、没有人工看图就不报视觉通过、nlink 存疑就拒绝发布。一个生成式系统建立信任的方式,恰恰是把自己能证明的边界讲得比能力还响。

项目本身是 MIT 协议开源的,node archify/bin/archify.mjs doctor 跑完二十来项自检就能上手。比起把它当成一个画图工具,我更建议把源码当材料读一遍——尤其是那个用硬链接计数写出来的文件发布协议,读完会重新理解什么叫"对正确性较真"。