首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >我把「限流器」画成了一台会动的机器:Anatomy 开源项目拆解

我把「限流器」画成了一台会动的机器:Anatomy 开源项目拆解

原创
作者头像
大盘鸡拌面
发布于 2026-10-20 19:28:50
发布于 2026-10-20 19:28:50
410
举报

一、先说件我自己的破事

上个月给两个新同事讲限流。

我在白板上画了三个框:网关、令牌桶、下游服务,中间连两根箭头,一根写「取令牌」,一根写「放行」。讲完我问听懂没,俩人一起点头。

第二天其中一个小伙子端着电脑过来,问了三个问题:桶满了新请求怎么办?令牌是按秒整批补还是每个请求补一次?被限流的请求是排队等着还是直接吐 503?

我当场卡住。这三个问题,白板上那两根箭头一个都答不上来。

后来我把那张白板照下来,越看越觉得别扭:它没错,但它什么也没说。方框加箭头这种图,天生只能表达「谁连着谁」,表达不了「它到底怎么转起来」。而大部分人真正卡住的,恰恰是后半句。

这篇要讲的 Anatomy,就是冲着这个别扭去的。


二、它不画图,它造东西

一句话:Anatomy 是一个给 AI 编码助手用的技能包(Skill),你描述一个技术概念,它给你画一张等距视角的、能交互的立体插图。

但它的做法很反直觉。它不问你「要什么风格的图」,也不给你套模板。它先干一件事:给这个概念发明一个物理对应物,然后把这个东西一块一块造出来。

举几个作者自己写在 README 里的例子,我看了直呼内行:

  • 「缓存命中就跳过计算」这件事,它做成一把弹子锁——只有每一根弹子都对齐,锁芯才转得动。不命中?锁不转,白干。
  • 「一个 GPU 上下文同时服务多个画布」,它做成一台龙门架绘图仪——只有一个打印头,挨个去 16 个孔位干活。
  • 「令牌桶限流」,就是我上面那个白板难题,它做成一只能看见水位的水桶,加一个控制放行速度的阀门。

左边那种图你肯定画过,右边这个才是我想要的。区别不在于好不好看,在于右边这台机器能回答我同事那三个问题:水位就是当前可用令牌,桶的口径就是容量,阀门开多大就是放行速率。你拖一下阀门,水位掉得就快;你不管它,水位自己涨回上限然后溢出去。

这就是它和一堆「AI 画图工具」最本质的分野:别的工具在帮你表达你已经想清楚的东西,它在逼你把没想清楚的东西想清楚。因为你要造一台机器,你就必须回答「这个零件为什么在这儿」。


三、装起来,五分钟的事

不用 API Key,不用 ​​npm install​​,整个仓库大概 10 MB,依赖只有 Node 内置模块。Node 22.20 以上。

代码语言:javascript
复制
# 装给 Claude Code
npx skills add wheresryan22/anatomy -g -a claude-code

# 用 Codex 就把最后一段换成 -a codex
# 用 OpenCode 换成 -a opencode,用 Antigravity 换成 -a antigravity

想装到项目里而不是全局,把 ​​-g​​ 去掉,它会落到 ​​.claude/skills/​​ 下面。

装完直接在会话里敲斜杠命令:

代码语言:javascript
复制
/anatomy 解释令牌桶限流器怎么工作,要能交互

第一次跑我建议别挑大的。仓库里最小的例子是 ​​dial-indicator​​,一个千分表架在量块上,大概 300 行,先拿它练手,看看它出图的节奏和你的耐心能不能对上。

想先看看成品长什么样,直接跑现成的:

代码语言:javascript
复制
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,它才有东西可画。这也解释了为什么它画出来的图信息密度高:那些数字最后都会变成零件尺寸和卡片上的实时读数。


五、三层夹心:SVG 不够用的时候才上 WebGL

火焰、尾焰、水波、灯光、焦散——这些东西用线条画不出来,怎么画都假。

Anatomy 的处理方式是夹心:后面一层 SVG,中间一块 WebGL 画布,前面再一层 SVG。中间那层用同一个等距相机渲染,所以那些「气氛」是长在画面里的,不是贴上去的贴图。

这个设计有个我很欣赏的副作用:没有 WebGL 也能看。前后两层 SVG 自己就是一张完整的图,只是少了点氛围。所以它能直接印进 PDF,能贴进不支持 WebGL 的阅读器,能在「减少动态效果」的系统设置下安静地变成一张静图。


六、真正让我服气的,是它会自己挑错

AI 画图最大的问题从来不是画得丑,是画错了还理直气壮。线断在半空、管子从零件中间穿过去、两个东西重叠顺序反了,它照样给你输出,还说「已完成」。

Anatomy 花了很大力气治这个病,靠三件工具。

几何审计(​​audit.mjs​​) 干的事很硬核:它检查有没有零件穿过另一个零件、每个管口是不是都落在支座上、每个遮挡是不是按深度排过。而且不只检查静止状态——火箭发动机那个例子会被拆成 11 个组件,审计要保证拆开的每一帧里管子都没穿模。

线头检查(​​lines.mjs​​) 更土:任何一条线的端点悬在半空,构建直接失败。我第一次看到这条规则的时候笑出声,太朴素了。但你想想,手绘图最假的瞬间是什么?就是一条线画到一半没了。

截图回看(​​capture.mjs​​) 是我觉得最狠的一招:它开一个无头 Chrome,把图截下来,截关节特写,截慢放的连拍,然后让模型去读这些 PNG。不是「我觉得应该没问题」,是「我看到这个接缝有 2 像素的错位」。

这三件事加起来,才让它敢说「这张图是对的」。代价也很实在:慢。复杂的图要跑几分钟,3D 的轨道审计能跑 15 到 20 分钟。作者自己也承认,这玩意儿是故意设计成慢的。


七、完整实战:给「令牌桶限流」做一张能进文档的图

前面铺垫够了,来一遍真的。目标很明确:我们文档站上有一节讲限流,现在只有文字和一张丑图,我要换掉它,并且顺手导一份 PDF 放进交付文档里。

7.1 先想清楚,再开口

写 prompt 这件事,作者给了三条建议,我照着改了一下,确实有用:

  • 说清楚它要解释什么,别描述画面。 说「令牌桶限流」而不是「画个桶」。
  • 把真实数字塞进去。 容量 10、每 200 毫秒补 1 个、下游扛 50 QPS。数字是它的燃料。
  • 说清楚图要住在哪儿。 独立 HTML、单个 SVG 还是 React 组件,出的东西完全不一样。

我最后用的是这段:

代码语言:javascript
复制
/anatomy 用一张等距插图解释令牌桶限流:桶容量 10 个令牌,每 200 毫秒补 1 个,
下游只扛得住 50 QPS。请求从顶部进来,取到令牌就放行,取不到就排队。
用一个带阀门的透明水桶表现:水位是当前令牌数,阀门开度是放行速率。
要能拖动阀门,水位和读数跟着变。输出 React 组件 + 一个独立 HTML 预览,浅色主题。

7.2 它先给你一份零件清单

这一步很多人会跳过,但我觉得这是它最值钱的地方。它会先把零件列出来,每个零件有名字、有尺寸、有位置:

代码语言:javascript
复制
## 零件清单

| 名字        | 是什么             | 关键尺寸                    |
| ----------- | ------------------ | --------------------------- |
| 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。

看到这张表,你就知道它打算怎么画了。如果这张表是错的,后面全错——所以这一步一定要停下来看一眼,别让它一路狂奔到底。

7.3 建几何、跑审计

它生成的构建脚本大概长这样,用的是 ​​kit/​​ 里那套 API(世界坐标 ​​[x, y, z]​​,x、y 在地面,z 朝上):

代码语言:javascript
复制
// 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: "补令牌" }],
    }),
  }),
);

然后跑审计。这一步别省,它是唯一能证明「这张图没画错」的东西:

代码语言:javascript
复制
node examples/token-bucket/build.mjs --audit
# 有问题会直接打印出来并返回非零退出码

审计要过,得把每个零件登记进去,让它知道每个形状占哪儿:

代码语言:javascript
复制
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​​,我盯着屏幕笑了半天——这不就是我讲白板时犯的错吗,图画得挺像回事,物理上根本不成立。

7.4 接进 Next.js 文档站

它出的 React 组件是拆开的两半:​​draw.tsx​​ 是服务端安全的静态图,​​live.tsx​​ 是客户端的动效钩子。所以静态部分不会把 JS 打包进首屏,动效只在需要的时候水合。

代码语言:javascript
复制
// 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>​​,图没出来的时候不挡正文:

代码语言:javascript
复制
// app/docs/rate-limit/page.mdx
## 限流是怎么生效的

<TokenBucket tokens={6} />

最后那句注释是我觉得整个项目最优雅的地方。很多「可交互插图」是假交互:图在动,旁边那个数字是写死的。这里数字和零件同源,所以你永远看不到「水都漏光了但读数还写着 6」这种鬼画面。

7.5 顺手印一份 PDF

这套流程最后还有个我很喜欢的收尾。交付文档要打印,动效全没了,怎么办?

代码语言:javascript
复制
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​​:

代码语言:javascript
复制
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 版。

先做浅色,再做深色。 它的浅色主题是设计的第一目标,着色器也是先为浅色调的。反过来做,往往会得到一张「深色还行、浅色发灰」的图。


九、什么情况下别用它

说点反向的。

如果只是要一张「谁调用谁」的架构图,那它属于杀鸡用牛刀,你画十个框连九根线,两分钟搞定,用不着等五分钟。

如果图里没有任何机械隐喻可借用——比如一个纯逻辑的权限模型、一张状态机——它硬造出来的机器会很牵强。这时候老老实实用流程图。

如果内容每周都在变、图要跟着频繁重生成,那这套「造机器 + 审计 + 截图回看」的流程太重了,维护成本会把你拖垮。

反过来,下面这几种情况它简直是降维打击:

  • 新人 onboarding 文档里,那些「讲十遍还是不懂」的核心机制
  • 对外技术博客里需要一张让人记住的图
  • 售前 / 交付材料里要一张能印、能讲、能拆开看的原理图
  • 教学场景,学生可以自己拖着玩

我写这篇不是因为它图好看。好看的工具太多了,Midjourney 十秒一张,比它漂亮。

它打动我的是另一个东西:它把「画图」这件事,重新定义成了「把机制想清楚」。

你必须先知道桶容量是多少、令牌怎么补、满了之后新请求去哪,才能造出这台机器。造不出来,说明你还没想明白。这个约束很讨厌,但正是这个约束,让最后那张图有了信息量。

我那个同事后来没再问第二个问题。他直接去拖阀门了,拖到水位见底,看着新请求排起队,自己嘟囔了一句「哦,原来满了是这样」。

那一刻我觉得,这五分钟等得值。

项目地址:​​https://github.com/wheresryan22/anatomy​​(MIT 协议,可以商用)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先说件我自己的破事
  • 二、它不画图,它造东西
  • 三、装起来,五分钟的事
  • 四、它画得像「人画的」,是因为它真的在施工
  • 五、三层夹心:SVG 不够用的时候才上 WebGL
  • 六、真正让我服气的,是它会自己挑错
  • 七、完整实战:给「令牌桶限流」做一张能进文档的图
    • 7.1 先想清楚,再开口
    • 7.2 它先给你一份零件清单
    • 7.3 建几何、跑审计
    • 7.4 接进 Next.js 文档站
    • 7.5 顺手印一份 PDF
  • 八、我踩过的坑,你大概率也会踩
  • 九、什么情况下别用它
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档