
平常让 AI 帮你画张图解释个概念,路子一般就两条。第一条是让它吐 mermaid 或者 ASCII,好处是准,坏处是那玩意儿看着跟 2008 年的企业 PPT 一个味道;第二条是直接上文生图,好看是好看,但你让它画"令牌桶限流",它能把令牌画成薯片,桶的容量是 100 还是 10 全靠缘分。
Anatomy 走的是第三条路,而且这条路上目前我没见过第二个人走。
它官方给自己的定义是这么一句话:An agent skill for creating detailed, interactive isometric illustrations of real-world machines and complex systems. 翻成人话就是——它不是画示意图,它是给你造一个东西出来。你要它解释令牌桶算法,它不会画个方框写"Token Bucket",它会真的建一个桶、一个进料口、一个出料阀、一圈管道,按固定的等距相机(等轴测投影)把这一堆零件投到平面上,再按光源方向一层一层刷上明暗。
作者是 Ryan(GitHub 上 @wheresryan22),MIT 协议,仓库开源在 wheresryan22/anatomy。它本质上不是一个 npm 包,而是一个 Agent Skill——就是一个根目录放着 SKILL.md 的文件夹。Claude Code 能读,Antigravity、OpenCode、Codex 也能读。你甚至不用打字打出"anatomy"这个词,只要你说"给我一张等距插画""给我的文档配张解释图",它自己就会被触发。
装起来简单到有点不像话:
# 全局安装,CLI 会问你要给哪几个 agent 配上
npx skills add wheresryan22/anatomy -g
# 不想回答,直接指定
npx skills add wheresryan22/anatomy -g -a claude-code
npx skills add wheresryan22/anatomy -g -a codex然后 Claude Code 里就这么用:
/anatomy explain how a token bucket rate limiter works, interactiveCodex 里换成 $anatomy。没有 API Key,没有 npm install——因为按作者的说法,整套 kit 只用 Node 的内置模块。这一点我后面会专门聊,因为它是这个项目最"反直觉"的设计取舍之一。
它产出三种东西,共用同一套相机和同一套工艺规则:
档次 | 用来画什么 | 交付形态 | 仓库里的样板 |
|---|---|---|---|
SVG | 主力档。等距实体零件 + 四色明暗,严格从后往前刷 | 独立 HTML 页 / 单个 .svg 文件 / React・Next.js 组件 | dial-indicator、desk-computer、arcade-cabinet、test-rig |
SVG + WebGL | 线画不出来的东西:火焰、尾焰、水、焦散、灯光、辉光、蒸汽 | 前后两层 SVG 中间夹一层 canvas 着色器 | raptor-engine、ripple-tank |
3D(beta) | 真的会动的零件:转盘旋转、手指弯曲、零件脱落坠地 | 动件每帧重投影、重新着色,其余部分仍是静态画 | turning-dial |
有意思的是那个 WebGL 档:着色器不是随便糊在画上的一层滤镜,它跑在和这只图同一个等距相机里,夹在一张后置 SVG 和一张前置 SVG 之间。所以那团火是真的"长在"喷口里的,而不是"贴"在上面的。更关键的是——就算浏览器不给 WebGL,那张 SVG 本身依然完整可读,只是火没了。这个降级设计我挺欣赏,做前端的人都懂,能优雅降级的组件才是能上线的组件。
整体结构大概长这样:

很多人第一反应是:"不就是让模型写 SVG 吗,我让 GPT 写也能写。" 能写,但写出来大概率是一堆歪七扭八的矩形。
Anatomy 能画得像,靠的不是模型更聪明,而是它把"画得像"这件事从模型身上挪走了,挪到了一个确定的、可验证的几何层里。
第一件事:相机只有一个。 仓库里的例子统一用 40° 方位角、31° 俯角。所有零件先用 fitProjection 把三维包围盒拟合进画布,再统一投影。这意味着所有零件的透视关系天然一致,不会出现"这边桶是俯视、那边管道是平视"的鬼故事。你在 build.mjs 顶部能看到这四个常量被钉死:
const WIDTH = 640;
const HEIGHT = 470;
const AZIMUTH = 40;
const ELEVATION = 31;
export const P = k.fitProjection(
[...k.boxCorners(BASE, -BASE_H - FOOT_H, 0), [0, 0, 40]],
WIDTH, HEIGHT,
{ pad: 22, azimuth: AZIMUTH, elevation: ELEVATION }
);第二件事:明暗只有四档。 每块实体按受光面分四层色调(four-tone shading)来刷,而不是靠渐变糊。这么做的好处是画出来"像手绘工程图"而不是"像 3D 渲染翻车现场",文件也小。
第三件事,也是最狠的一件:从后往前刷。 说白了就是画家算法——先画最远的,再用近的盖住它。这不是为了好看,这是为了正确。因为等距图里零件会互相遮挡,顺序错了就会出现"螺丝穿过钢板"这种一眼假的画面。
说到这儿就得提它的审计机制了。Anatomy 不是说"画完了看着还行"就收工,它有一套几何自检,硬性要求三件事:
跑法就一行:
node build.mjs --audit另外还有无头 Chrome 里跑的 scripts/lines.mjs,专门抓"线头悬在半空"这种细节错误;scripts/turn-check.mjs 管 3D 图。而且技能里明确要求 Claude:自己生成的每一张 PNG 都要亲眼看一遍。这点我觉得是整个项目最"工程"的地方——它把"自检"写进了流程,而不是寄希望于模型一次就能写对。
再看它最核心的那份东西:SKILL.md 里的九步工作流。这九步基本就是把人类插画师的工作习惯抄了一遍:

第一步"先收真实数字"是整条链子的地基。你告诉它"桶容量 100、每秒补 20 个、超时就排队",它才能把桶画成 100 的容积、把进料管画成每秒 20 的流速感。你要是只说"画个限流器",那它就只剩瞎编——这一点其实跟我们写技术方案一个道理,输入越糊,输出越像摆样子。README 里那句"Name the idea, not the drawing"(说清楚要解释什么,别去描述怎么画)就是这个意思。
顺带贴一下仓库骨架,看结构比看文字快:
anatomy/
├── SKILL.md ← Claude 读的那份:工作流 + 配置 + 工艺规则
├── kit/ ← 绘图库
│ ├── iso-kit.ts / .mjs ← 核心:相机、实体、零件库、SVG 渲染、卡片、CSS
│ ├── iso.css ← 明暗两套主题
│ ├── react/ ← draw.tsx(服务端安全)+ live.tsx(客户端 hooks)
│ ├── lathe.mjs ← 任意轴上的回转体(筒、塞、盖)
│ ├── tube.mjs ← 管道、软管、线缆
│ ├── audit.mjs ← 几何审计
│ ├── gl.mjs ← 图内 WebGL 着色器层
│ └── turn*.mjs ← 3D 运行时 / 构建器 / 轨道审计
├── references/ ← 长文档:craft / kit / motion / react / verify / webgl / walkthrough / 3d
├── scripts/ ← 无头 Chrome 检查与构建助手
├── examples/ ← 8 张完整样板图,构建产物已提交
└── evals/evals.json ← 测试 prompt 与"好答案"的判定标准注意 evals/evals.json 这个文件。一个画图技能还带评测集,这事儿本身就说明作者是把它当正经工程在做,而不是当 prompt 收藏夹。
光看图好看不够,得知道它给你的东西能不能改。Anatomy 交付的是源码,不是一张黑盒图片。每个 example 底下都是一个 Node 构建脚本,跑一下就地在旁边写出独立的 HTML 页。加 --light 出浅色主题,加 --audit 顺带跑几何审计。
我扒一个真实的重头戏——examples/turning-dial/build.mjs 的开头,感受一下这个 API 的密度:
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import * as k from "../../kit/iso-kit.mjs";
import * as G from "../../kit/lathe.mjs";
import * as A from "../../kit/audit.mjs";
import { turning } from "../../kit/turn-build.mjs";
import * as TA from "../../kit/turn-audit.mjs";
import { turnScript, glScript } from "../../scripts/inline-kit.mjs";
import { glCamera } from "../../kit/gl.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const argv = process.argv.slice(2);
const theme = argv.includes("--light") ? "light" : "dark";k 是核心,G 是回转体,A 是审计,turning 是 3D 构建器,TA 是 3D 轨道审计。这五个 import 基本就是整个体系的全貌——它是分层的,你需要哪一层拎哪一层。
再往下看它怎么把冗长的调用压短。这种"给常用操作起个单字母别名"的写法,在这种几何代码里几乎是必需的,不然一行能写 200 个字符:
const at = (p) => k.iso(p, P); // 三维 → 二维投影
const S = (paths, style) => k.solidSvg(paths, style); // 实体面
const L = (d, style) => k.lineSvg(d, style); // 线
const D = (points, style) => k.dotsSvg(points, style); // 点(螺栓头什么的)
// 按深度排序——「从后往前刷」这条规则在这里被代码化了
const byDepth = (points) => [...points].sort(
(a, b) => k.depthOf([a[0], a[1], 0], P) - k.depthOf([b[0], b[1], 0], P)
);
export const R = A.recorder(P); // 录像机:记录每一块该画在哪一层
export const T = turning(P, { recorder: R });byDepth 这几行特别值得看一眼。前面说的"从后往前刷"听起来像画师的经验,但在这儿它就是一个 sort——把审美规则翻译成排序函数,这就是这个项目能稳定复现质量的原因。
动效也是纯函数,不是时间轴动画。比如这段是"手在表盘上一顿一顿地拨":
// 给定时间 t,算出这一刻的抬升量。0 = 贴住盘面,1 = 抬起来
export const tapAt = (time) => {
const phase = ((time % 4) + 4) % 4;
const up = Math.min(1, Math.max(0, (phase - 0.4) / 0.9));
const down = Math.min(1, Math.max(0, (phase - 2.2) / 1.1));
const ease = (x) => x * x * (3 - 2 * x); // smoothstep 缓动
return ease(up) * (1 - ease(down));
};
// 一个动作联动四五个零件:指轮跟着表盘反着转,棘爪走正弦
export const follow = (values) => ({
...values,
wheel: -(values.dial ?? 0) / 4,
pawl: 4 * Math.sin(((values.dial ?? 0) * Math.PI) / 18),
"f2.mcp": -22 * (values.lift ?? 0),
"f2.pip": -14 * (values.lift ?? 0),
});这个 follow 函数我觉得是整个仓库里最"工程味"的一段。真实机械里零件是联动的,你不可能让手指自己动、指轮不动。Anatomy 的做法是:所有动件都挂在同一个状态变量上,用纯函数推导出各自的姿态。这意味着你把 dial 从 0 扫到 1440,整套机构会自己按正确的传动比动起来——图是"可计算的",不是"画死的"。
最后是验收环节,也是最容易被人忽略的一步:
// 静态层 + 动态姿态一起构建
const out = T.build({ statics: R.items, poses, verify });
A.settle(R, P); // 结算所有重叠的深度关系
if (argv.includes("--audit")) {
A.auditOrExit(R, P); // 几何审计:穿透 / 悬空 / 深度顺序,不合格直接退出
TA.orbitOrExit(T, R, P, {
sweep: { at: (x) => follow({ dial: x }), from: 0, to: 1440, frame: 0.3 },
clips: [
{ name: "tap", at: (t) => follow({ dial: 40 + 12 * t, lift: tapAt(t) }), from: 0, to: 4, frame: 1 / 60 },
{ name: "break", at: (t) => follow({ dial: 100 + 12 * t, lift: 0.3 }), from: 0, to: BREAK_END, frame: 1 / 60 },
],
// 明确列出哪些零件在运动时会互相擦碰,审计器会重点盯这几个
crowd: [
["cable.red", "hand.f1.p2"],
["cable.red", "hand.f2.p1"],
["cable.blue", "hand.f3.p2"],
],
});
}看出来了吧。orbitOrExit 不是"截个图看看",它是把整个机构在轨道上扫一遍、每一帧都检查有没有零件互相穿模。crowd 那一项尤其老练——作者知道哪些零件在运动时容易打架,就手动把它们列成观察名单重点盯。这已经是机械设计软件里"干涉检查"的思路了,不是一个画图脚本该有的东西。
把这三方跑起来,整条链路是这样:

图里那个"不合格就回炉"的循环,就是它跟"让模型一次性吐个 SVG"最本质的区别。
光讲原理没意思,咱们走一遍完整的。假设场景是:我们组有个 API 网关,限流策略是令牌桶——桶容量 100,每秒补 20 个令牌,请求进来先要一个令牌,要不到就进等待队列,等超过 40ms 就直接 429。现在官网的开发者文档里需要一个能一眼看懂的图,最好还能放进 Next.js 文档站当组件用。
这一步是成败关键。别写"画个限流器",要写清楚三件事:解释什么、真实数字是多少、最后放哪。
/anatomy 解释 API 网关的令牌桶限流:桶容量 100 个令牌,
每秒稳定补 20 个,请求到达时先取一个令牌,
取不到就进入等待队列,等待超过 40ms 直接返回 429。
要有令牌从上方落入桶中、请求从左侧取走令牌的动作。
输出一个 React 组件加一份独立 HTML 预览,深色主题。对比一下两种写法的差别:前一种它只能给你个通用桶,后一种它知道桶里该画 100 个单位刻度、上游管道流速该比下游快、右侧要有一根溢流管代表 429。这就是"Name the idea, not the drawing"的实操版。
跑完你大概会拿到这么一套东西(各例子的实际布局,路径名随项目走):
token-bucket/
├── build.mjs ← Node 构建脚本,几何全在这儿
├── parts.mjs ← 零件清单:每个零件的尺寸、材质、色调
├── figure.html ← 跑一下就能开的独立页(--light 出浅色版)
├── figure-light.html
└── react/
├── draw.tsx ← 服务端安全:静态骨架,能 SSR
└── live.tsx ← 客户端:挂动效、接管交互React 那条线拆成两个文件是有讲究的:draw.tsx 不含任何浏览器 API,所以能在服务端渲染;live.tsx 才上 "use client" 挂交互。这跟现在 Next.js App Router 的最佳实践是严丝合缝的,你把 draw.tsx 直接扔进 Server Component 就完事,首屏不用等 hydration。
真正会去改的其实就一小段——顶部的常量。改数字,图就变,这比重新描述一遍需求快多了:
import * as k from "./kit/iso-kit.mjs";
import * as A from "./kit/audit.mjs";
// ① 真实数字,直接从上文需求里抄下来,全篇只在这里出现一次
const CAPACITY = 100; // 桶容量
const REFILL_PS = 20; // 每秒补桶数
const QUEUE_MS = 40; // 等待阈值,超了就是 429
const WIDTH = 640, HEIGHT = 470;
const AZIMUTH = 40, ELEVATION = 31; // 全仓库统一的相机,别乱改
const TANK = { x: -46, y: -46, w: 92, d: 92, r: 8 }; // 桶身包围盒
const TANK_H = 74;
const PIPE_IN_R = 5.2; // 上游进料管半径
const PIPE_OUT_R = 3.4; // 下游出料管半径,故意细一点,视觉上表达"限流"
export const P = k.fitProjection(
[...k.boxCorners(TANK, 0, TANK_H), [0, 0, TANK_H + 30]],
WIDTH, HEIGHT,
{ pad: 22, azimuth: AZIMUTH, elevation: ELEVATION }
);注意 PIPE_IN_R 和 PIPE_OUT_R 这两个值我故意没写成一样。进料管比出料管粗,这个视觉细节自己就在说"进得快、出得慢,所以会积压"——这是等距插画相对流程图最大的便宜:能用尺寸说话的地方就不用文字。
这是让图从"静态"变"能讲清楚事"的地方。补桶和消耗都能用一条状态曲线描述:
// t 时刻桶里有多少令牌:从空桶开始,被补到 CAPACITY 封顶
export const bucketAt = (t) => Math.min(CAPACITY, REFILL_PS * t);
// 水位线高度直接映射成几何——高度就是令牌数
export const levelOf = (t) => (bucketAt(t) / CAPACITY) * TANK_H;
// 液面与请求的联动:一次突发流量打进 30 个请求,桶被抽干
export const follow = (values) => {
const drained = Math.max(0, (values.burst ?? 0) - (values.t ?? 0) * REFILL_PS);
return {
...values,
level: levelOf(values.t ?? 0) - drained, // 液面下降
inlet: values.t > 0 ? 1 : 0, // 进料口开始滴水
spill: drained > 0 ? Math.min(1, drained / 30) : 0, // 溢流管亮起来 = 429
};
};挂上这段之后,图上那个桶的水位会随 t 往上涨,而你一旦把 burst 拉高,水位会掉下去、右侧溢流管会亮起来。"突发流量会导致部分请求被拒"这句话,就这么被画成了能看见的过程。 这比在文档里写三行小字管用得多。
# 构建并跑几何审计:穿透、悬空、深度顺序,一个都不能错
node build.mjs --audit
# 出浅色主题
node build.mjs --light
# 自己在终端里截图,不依赖 agent
node ../../scripts/capture.mjs figure.html --out shots/fig.png审计挂了会直接非零退出,这点很好——能塞进 CI 的东西才叫真的检查。前面提过 lines.mjs 还能抓"线头悬在半空",你完全可以把它接到 PR 流水线里,防的就是"改了一版图,结果某根管子没接上"这种事。
收到图之后我会按这几条过一遍,也就十几秒的事:
检查项 | 怎么看 | 不通过就 |
|---|---|---|
数字对不对 | 桶上的刻度、管道粗细、读数 | 回改 build.mjs 顶部常量 |
有没有穿模 | 螺丝有没有插进钢板里 | 重跑 --audit 看它报哪 |
有没有悬空 | 线头、管口是不是都落在安装点上 | 同上,lines.mjs 会指名道姓 |
遮挡顺序对不对 | 近的零件有没有被远的盖住 | 看 byDepth 排序用的层有没有放错 |
深色/浅色都能读 | 两个主题各开一次 | 浅色是它的设计首选主题,一般反而不容易翻车 |
动效有没有"鬼畜" | 用 drive.mjs 出慢动作接触表 | 调 follow 里的系数 |
整个流程串起来是这样:

第 ⑨ 步是我额外建议加的,README 里没写。理由很简单:一件事只要没进 CI,三个月后准没人记得跑。
顺便提一句,仓库里还带了个发布脚本,做出来的图想公开可以一键提 PR 到社区画廊:
node scripts/publish.mjs --submit它会把要提交的内容先给你看一遍、问你要标题和 GitHub 用户名,你点头才开 PR。不用 fork、不用手动 git。这个交互设计挺克制,值得学。
聊完优点,得说点实话,不然就成了软文。
首先,别拿它当图表库。 作者自己在 README 里写得很直白:没有坐标轴、没有系列、没有数据绘图。数字在图里只有两种存在方式——一种是某个模型驱动的实时读数,另一种就是零件本身的真实尺寸和数量。你要画折线图、柱状图,老老实实用 ECharts。
其次,它只画机器、仪器、工具、家具。 不画人,也不画精确地图。这条限制挺硬,因为它整套几何系统是建立在"可制造的实体"这个前提上的。一个没有机构可以依托的纯抽象概念——比如"微服务治理""组织架构",硬塞给它画,出来的一定比别的工具弱。判断标准很简单:这事你能不能想象出一台机器来描述它。 能,就用它;不能,换个工具。
第三,它慢,而且是故意的。 这一点得提前有心理准备。一张图 = 一份零件清单 + 一轮几何构建 + 一轮审计 + 两个主题下每个关节的特写截图。README 里说 Raptor 那张图的构建要几分钟,而一次完整的 3D 轨道审计,30 个零件要跑 15 到 20 分钟。它不是用来做"随手配图"的,是用来做那种会被反复引用、放在落地页首屏、要经得起放大看的图的。你要的就是张会议纪要里的示意图,大可不必上这个重武器。
第四,3D 还是 beta。 作者很诚实地挂了 beta 标:只在它自己那个例子和一张测试图上验证过,还没上过生产图。性能数据也给了——一张小的 3D 图能跑 60 fps;那个 380 零件的压力测试手,在 1440px 宽下大约 20 fps,跑手机降频档就只剩 13 到 15 fps。所以 3D 适合"一个小转盘、几根手指"这种量级,别一上来就想让它渲整台发动机。
第五,也是我实际踩到的坑——Codex 的沙箱会拦住它自检。 因为验收要起无头 Chrome,Chrome 得写临时 profile 文件、还要走本地端口通联,而 Codex 默认在沙箱里跑命令,这个动作会被挡掉。症状是:图确实建出来了,但检查全挂了,你会一脸懵地看到一堆失败日志,然后发现图片本身没啥问题。
三种解法,按优先级:
approval_policy = "on-request"。~/.codex/config.toml:approval_policy = "on-request"
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
network_access = true
writable_roots = ["/tmp"]node scripts/capture.mjs figure.html --out shots/fig.png--sandbox danger-full-access 也行,但那等于整个会话放开所有限制,只建议在你完全信得过的项目上用。
另外两个小坑记一下:安装时 skills CLI 会把整个仓库拷过来,大概 10 MB(examples 全在里面),介意的话就只用 git 克隆单目录;还有 CLI 版本更新挺快,v1.7.1 就要求 Node 22.20+ 了,而技能本身只要 Node 20.10+,这两个版本号不是一回事,别混。
最后说两个让我觉得这项目"对"的细节。
一个是零依赖。除了 React 组件那一小块,整套绘图库只用 Node 内置模块。这意味着你今天 clone 下来,三年后大概率还能跑。前端圈的项目活得久的,往往就是这种"不依赖任何东西"的。
另一个是它的贡献规则里有一条:改 kit 必须保证所有现存 example 的输出字节级不变。这条规则看着变态,但它守住的是整件事的根基——几何库一旦不确定了,所有图的正确性都无从谈起。加上"代码里不写注释"这条,能看出作者对"确定性"这件事有多在意。
所以回到最开始那个问题:为什么不能用 GPT 直接吐 SVG?因为那张 SVG 你没法验证。而 Anatomy 给你的东西,每一块几何都能被审计、被截图、被排序函数证明。它把"画得好不好看"这件主观的事,拆成了"几何对不对"这件客观的事。 我觉得这才是这套技能真正的价值所在——不是画得漂亮,是画得可证明。
想上手的话,一条命令的事:
npx skills add wheresryan22/anatomy -g然后从仓库里那八张现成的图开始改。dial-indicator 和 desk-computer 结构最简单,适合用来摸 API;turning-dial 有 3D,能看全套机制;raptor-engine 有 WebGL,想看火焰怎么长在喷口里的就从它开始。改数字、跑 --audit、看它报什么错——这个循环走三遍,你对等距图的理解会比看十篇教程都深。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。