
上个月给两个新同事讲限流。
我在白板上画了三个框:网关、令牌桶、下游服务,中间连两根箭头,一根写「取令牌」,一根写「放行」。讲完我问听懂没,俩人一起点头。
第二天其中一个小伙子端着电脑过来,问了三个问题:桶满了新请求怎么办?令牌是按秒整批补还是每个请求补一次?被限流的请求是排队等着还是直接吐 503?
我当场卡住。这三个问题,白板上那两根箭头一个都答不上来。
后来我把那张白板照下来,越看越觉得别扭:它没错,但它什么也没说。方框加箭头这种图,天生只能表达「谁连着谁」,表达不了「它到底怎么转起来」。而大部分人真正卡住的,恰恰是后半句。
这篇要讲的 Anatomy,就是冲着这个别扭去的。
一句话:Anatomy 是一个给 AI 编码助手用的技能包(Skill),你描述一个技术概念,它给你画一张等距视角的、能交互的立体插图。
但它的做法很反直觉。它不问你「要什么风格的图」,也不给你套模板。它先干一件事:给这个概念发明一个物理对应物,然后把这个东西一块一块造出来。
举几个作者自己写在 README 里的例子,我看了直呼内行:

左边那种图你肯定画过,右边这个才是我想要的。区别不在于好不好看,在于右边这台机器能回答我同事那三个问题:水位就是当前可用令牌,桶的口径就是容量,阀门开多大就是放行速率。你拖一下阀门,水位掉得就快;你不管它,水位自己涨回上限然后溢出去。
这就是它和一堆「AI 画图工具」最本质的分野:别的工具在帮你表达你已经想清楚的东西,它在逼你把没想清楚的东西想清楚。因为你要造一台机器,你就必须回答「这个零件为什么在这儿」。
不用 API Key,不用 npm install,整个仓库大概 10 MB,依赖只有 Node 内置模块。Node 22.20 以上。
# 装给 Claude Code
npx skills add wheresryan22/anatomy -g -a claude-code
# 用 Codex 就把最后一段换成 -a codex
# 用 OpenCode 换成 -a opencode,用 Antigravity 换成 -a antigravity想装到项目里而不是全局,把 -g 去掉,它会落到 .claude/skills/ 下面。
装完直接在会话里敲斜杠命令:
/anatomy 解释令牌桶限流器怎么工作,要能交互第一次跑我建议别挑大的。仓库里最小的例子是 dial-indicator,一个千分表架在量块上,大概 300 行,先拿它练手,看看它出图的节奏和你的耐心能不能对上。
想先看看成品长什么样,直接跑现成的:
git clone https://github.com/wheresryan22/anatomy.git
cd anatomy
node examples/dial-indicator/build.mjs # 出深色版页面
node examples/dial-indicator/build.mjs --light # 出浅色版页面会生成在例子自己的目录里,双击就能开。
我一开始以为它就是个 SVG 生成器,套了个好看的壳。翻完 kit/ 之后我改口了,它是按工程图纸的逻辑在做。整个 kit/ 就六个模块,全围着同一个相机转:

iso-kit 管实体和渲染,lathe 管绕任意轴车出来的圆件(阀门、法兰、旋钮全靠它),tube 管管路和线缆,gl 是画在图层里的 WebGL,turn 是真 3D,audit 负责挑错。这个结构本身就是它「一套工艺规则」的物理体现。
第一,全场只有一个相机。 所有图共用一套等距投影:方位角 45 度,俯角 30 度。fitProjection() 拿到所有极值点,算出能把这些点刚好装进画布的那个投影。所以它画的每台机器都站在同一块地板上,你并排放在文档里不会打架。
第二,每个零件都是实体,都有四档明暗。 一个零件不是一条线加一个填充,而是一个被挤出来的棱柱:顶面最亮,朝向光源的侧面一档,背光的侧面一档,最暗的一档压边。四档颜色是从同一套色板里按法线算出来的,不是手挑的。
第三,也是我觉得最关键的:它由远及近地画。 先画最远的零件,再画近的,近的盖住远的。遮挡关系不是靠人肉判断的,是靠 depthOf() 排序排出来的。这一条听着朴素,但它决定了这张图在几何上是不是「立得住」。

八步是写在 SKILL.md 里的硬流程,不是建议。少一步它自己会翻回去补。我特别喜欢第 1 步——先收真实数字,不许编。你得告诉它桶容量是 10、每 200 毫秒补 1 个、下游只扛得住 50 QPS,它才有东西可画。这也解释了为什么它画出来的图信息密度高:那些数字最后都会变成零件尺寸和卡片上的实时读数。
火焰、尾焰、水波、灯光、焦散——这些东西用线条画不出来,怎么画都假。
Anatomy 的处理方式是夹心:后面一层 SVG,中间一块 WebGL 画布,前面再一层 SVG。中间那层用同一个等距相机渲染,所以那些「气氛」是长在画面里的,不是贴上去的贴图。

这个设计有个我很欣赏的副作用:没有 WebGL 也能看。前后两层 SVG 自己就是一张完整的图,只是少了点氛围。所以它能直接印进 PDF,能贴进不支持 WebGL 的阅读器,能在「减少动态效果」的系统设置下安静地变成一张静图。
AI 画图最大的问题从来不是画得丑,是画错了还理直气壮。线断在半空、管子从零件中间穿过去、两个东西重叠顺序反了,它照样给你输出,还说「已完成」。
Anatomy 花了很大力气治这个病,靠三件工具。

几何审计(audit.mjs) 干的事很硬核:它检查有没有零件穿过另一个零件、每个管口是不是都落在支座上、每个遮挡是不是按深度排过。而且不只检查静止状态——火箭发动机那个例子会被拆成 11 个组件,审计要保证拆开的每一帧里管子都没穿模。
线头检查(lines.mjs) 更土:任何一条线的端点悬在半空,构建直接失败。我第一次看到这条规则的时候笑出声,太朴素了。但你想想,手绘图最假的瞬间是什么?就是一条线画到一半没了。
截图回看(capture.mjs) 是我觉得最狠的一招:它开一个无头 Chrome,把图截下来,截关节特写,截慢放的连拍,然后让模型去读这些 PNG。不是「我觉得应该没问题」,是「我看到这个接缝有 2 像素的错位」。
这三件事加起来,才让它敢说「这张图是对的」。代价也很实在:慢。复杂的图要跑几分钟,3D 的轨道审计能跑 15 到 20 分钟。作者自己也承认,这玩意儿是故意设计成慢的。
前面铺垫够了,来一遍真的。目标很明确:我们文档站上有一节讲限流,现在只有文字和一张丑图,我要换掉它,并且顺手导一份 PDF 放进交付文档里。
写 prompt 这件事,作者给了三条建议,我照着改了一下,确实有用:
我最后用的是这段:
/anatomy 用一张等距插图解释令牌桶限流:桶容量 10 个令牌,每 200 毫秒补 1 个,
下游只扛得住 50 QPS。请求从顶部进来,取到令牌就放行,取不到就排队。
用一个带阀门的透明水桶表现:水位是当前令牌数,阀门开度是放行速率。
要能拖动阀门,水位和读数跟着变。输出 React 组件 + 一个独立 HTML 预览,浅色主题。这一步很多人会跳过,但我觉得这是它最值钱的地方。它会先把零件列出来,每个零件有名字、有尺寸、有位置:
## 零件清单
| 名字 | 是什么 | 关键尺寸 |
| ----------- | ------------------ | --------------------------- |
| base.plate | 底座 | 148 × 118 × 8 |
| drum | 透明桶身 | Ø88,高 96,壁厚 3 |
| water | 液面(不是实体) | 高度随 tokens 变化,0–72 |
| valve.body | 阀体 | Ø18,中心在 (150, 45, 53) |
| valve.wheel | 手轮 | Ø34,三根辐条 |
| pipe.in | 进水管 | Ø12,从左上进入桶口 |
| pipe.out | 出水管 | Ø12,从阀体向右 26 |
| card | 读数卡片 | 标题 / 提示 / readout |
## 世界
占地 200 × 140,层高 0–150。相机 45°/30°,画布 900 × 640。看到这张表,你就知道它打算怎么画了。如果这张表是错的,后面全错——所以这一步一定要停下来看一眼,别让它一路狂奔到底。
它生成的构建脚本大概长这样,用的是 kit/ 里那套 API(世界坐标 [x, y, z],x、y 在地面,z 朝上):
// examples/token-bucket/build.mjs
import { mkdirSync, writeFileSync } from "node:fs";
import * as K from "../../kit/iso-kit.mjs";
// 把所有极值点交给 fitProjection,它会算出刚好装得下的投影
const P = K.fitProjection(
[
...K.boxCorners({ x: -14, y: -14, w: 148, d: 118, r: 10 }, 0, 150),
[176, 45, 60], // 出水口最右端,别忘了它
],
900,
640,
{ pad: 40, azimuth: 45, elevation: 30 },
);
const parts = [
{ at: [60, 45, 0], svg: K.solidSvg(K.slabOf({ x: -14, y: -14, w: 148, d: 118, r: 10 }, 0, 8, P), { tone: "mid" }) },
{ at: [60, 45, 8], svg: K.solidSvg(K.cylinder(60, 45, 44, 8, 96, P, 64, 0.6), { tone: "hi" }) },
{ at: [150, 45, 40], svg: K.solidSvg(K.cylinder(150, 45, 9, 40, 26, P, 20), { tone: "mid" }) },
];
// 由远及近:depth 小的先画,近的盖住远的
const body = parts
.sort((a, b) => K.depthOf(a.at, P) - K.depthOf(b.at, P))
.map((p) => p.svg)
.join("");
const fig = K.figureSvg({
width: 900,
height: 640,
label: "令牌桶限流器:请求从顶部进入,阀门按固定速率放行",
body,
});
mkdirSync("out", { recursive: true });
writeFileSync("out/figure.svg", fig);
writeFileSync(
"out/index.html",
K.pageHtml({
title: "令牌桶限流",
theme: "light",
body: K.plateHtml({
fig,
title: "令牌桶 · 限流器",
hint: "拖一下阀门试试",
readout: "6 / 10",
keys: [{ mark: "raised", label: "桶" }, { mark: "dotted", label: "补令牌" }],
}),
}),
);然后跑审计。这一步别省,它是唯一能证明「这张图没画错」的东西:
node examples/token-bucket/build.mjs --audit
# 有问题会直接打印出来并返回非零退出码审计要过,得把每个零件登记进去,让它知道每个形状占哪儿:
import * as A from "../../kit/audit.mjs";
const R = A.recorder(P, { order: "parts", spring: 6 });
R.put({
piece: "drum",
at: [60, 45, 8],
svg: drumSvg,
shapes: [R.solid(A.cylinder(60, 45, 44, 8, 96))],
});
A.settle(R, P);
R.items.sort((a, b) => a.key - b.key);
A.auditOrExit(R, P); // 不加 --audit 时它会立刻返回,不拖慢构建我第一版就翻车了:出水管直接从桶壁中间穿过去。审计报了一行 pipe.out crosses drum,我盯着屏幕笑了半天——这不就是我讲白板时犯的错吗,图画得挺像回事,物理上根本不成立。
它出的 React 组件是拆开的两半:draw.tsx 是服务端安全的静态图,live.tsx 是客户端的动效钩子。所以静态部分不会把 JS 打包进首屏,动效只在需要的时候水合。
// app/docs/rate-limit/figure.tsx
"use client";
import { IsoFigure, Plate, Solid, Line } from "@/anatomy/kit/react/draw";
import { useReadout, useEased } from "@/anatomy/kit/react/live";
export function TokenBucket({ tokens }: { tokens: number }) {
const { anchor } = useReadout();
const level = useEased(tokens, 0.6);
return (
<div ref={anchor}>
<Plate title="令牌桶 · 限流器" hint="拖一下阀门试试" readout={`${tokens} / 10`}>
<IsoFigure width={900} height={640} label="令牌桶限流器">
<Solid paths={drumPaths} tone="hi" crease="faint" />
<Solid paths={waterPaths(level)} tone="mid" />
<Line d={valveWheel} tone="lo" />
</IsoFigure>
</Plate>
</div>
);
}页面里就这么用,外面再包一层 <Suspense>,图没出来的时候不挡正文:
// app/docs/rate-limit/page.mdx
## 限流是怎么生效的
<TokenBucket tokens={6} />最后那句注释是我觉得整个项目最优雅的地方。很多「可交互插图」是假交互:图在动,旁边那个数字是写死的。这里数字和零件同源,所以你永远看不到「水都漏光了但读数还写着 6」这种鬼画面。
这套流程最后还有个我很喜欢的收尾。交付文档要打印,动效全没了,怎么办?
node scripts/print.mjs examples/token-bucket/sheet.mjs --format all它会把页面里某个姿态冻结成矢量静帧(WebGL 那层导成 2 倍 PNG),出三种 A4/Letter 版式:一页海报、一页技术图纸、两到四页的详解手册(带步骤和组件图)。火箭发动机和街机柜这两个例子都支持。
一份东西,三种形态:文档站上是能玩的,PDF 里是能印的,SVG 是能塞进任何地方的。
它真不是图表库。 没有坐标轴、没有数据系列、不能画折线图。数字只能以「实时读数」和「零件的真实尺寸」的形式出现。想画 K 线图、想画桑基图,出门左转找 ECharts,别难为它。
它画不了人,也画不了地图。 机器、仪器、工具、家具都行,人不行,精确地图也不行。而且越是「没有机械结构」的概念,它画出来越虚。抽象的东西(比如「依赖注入」)硬要它画,结果往往是自我感动。
慢,是真的慢。 复杂图几分钟,3D 审计十几二十分钟。我第一次等的时候以为卡死了。它的策略是慢工出细活,你的策略应该是:先让它出小的验证思路,对了再放大。
Codex 用户注意沙箱。 截图那一步要开 Chrome,沙箱会拦。要么在沙箱外批准这条命令,要么改 ~/.codex/config.toml:
approval_policy = "on-request"
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
network_access = true
writable_roots = ["/tmp"]3D 模式还是 beta。 官方自己说了,只在一个示例和一个测试图上验证过。小的 3D 图能跑 60 帧,但 380 个零件的压力测试在 1440 宽下只有 20 帧,手机上 13 到 15 帧。想上生产环境,先用 SVG 版。
先做浅色,再做深色。 它的浅色主题是设计的第一目标,着色器也是先为浅色调的。反过来做,往往会得到一张「深色还行、浅色发灰」的图。
说点反向的。
如果只是要一张「谁调用谁」的架构图,那它属于杀鸡用牛刀,你画十个框连九根线,两分钟搞定,用不着等五分钟。
如果图里没有任何机械隐喻可借用——比如一个纯逻辑的权限模型、一张状态机——它硬造出来的机器会很牵强。这时候老老实实用流程图。
如果内容每周都在变、图要跟着频繁重生成,那这套「造机器 + 审计 + 截图回看」的流程太重了,维护成本会把你拖垮。
反过来,下面这几种情况它简直是降维打击:
我写这篇不是因为它图好看。好看的工具太多了,Midjourney 十秒一张,比它漂亮。
它打动我的是另一个东西:它把「画图」这件事,重新定义成了「把机制想清楚」。
你必须先知道桶容量是多少、令牌怎么补、满了之后新请求去哪,才能造出这台机器。造不出来,说明你还没想明白。这个约束很讨厌,但正是这个约束,让最后那张图有了信息量。
我那个同事后来没再问第二个问题。他直接去拖阀门了,拖到水位见底,看着新请求排起队,自己嘟囔了一句「哦,原来满了是这样」。
那一刻我觉得,这五分钟等得值。
项目地址:https://github.com/wheresryan22/anatomy(MIT 协议,可以商用)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。