全部文章

B-rep 不适合 Transformer:为什么我们不让 AI 直接写 B-rep

· 约 8 分钟

写完为什么我们让 AI 写 CAD 代码而不是生成网格之后,最常被问到的是下一个问题。网格不知道自己是什么,这没错;可 B-rep 知道。它精确,STEP 文件里存的就是它,Fusion、SolidWorks 和所有 CAM 软件都读它。那为什么还要让模型写代码?为什么不直接生成 B-rep?

短的回答是:B-rep 是该交付的东西,不是该让 Transformer 写的东西。语言模型不会一个字节一个字节地吐出一份 PDF,它写文字,排版交给一个从不排错的程序。零件也一样,那个程序是 CAD 内核,它的产物就是 B-rep。

长的回答在下面,数字都是在我们自己的零件上量的。

B-rep 由什么组成

拿一个最小的、带特征的零件:一块 40 × 30 × 10 mm 的板,正中一个 Ø6 通孔。写成代码是三行:

plate = Box(40, 30, 10, align=(Align.MIN, Align.MIN, Align.MIN))
hole = Pos(20, 15, 5) * Cylinder(3, 10)
model = Model(bodies=[Body("plate", geometry=plate - hole)])

连同 import 和一行说明,整个文件 148 个 token。我们的导出器给同一块板写出的 STEP 是 504 行、6835 个 token:434 个实体,实体之间 500 处引用,397 个数。

这些实体描述的是两样绑在一起的东西。拓扑:1 个实体、7 个面、15 条边、10 个顶点,哪一圈边围着哪个面,每条边由哪两个面共享。几何:每个面落在哪个平面或圆柱面上,每条边沿着哪条直线或圆,每个顶点在哪个点。在我们的零件里,STEP 文件大约一半是第三样东西:每条边在它所围的每个面的坐标系里,再用 2D 写一遍。

下面是孔壁那个面和它的一条边沿,原样摘自文件:

#402 = ADVANCED_FACE('',(#403),#282,.F.);
#403 = FACE_BOUND('',#404,.F.);
#404 = EDGE_LOOP('',(#405,#406,#427,#428));
#282 = CYLINDRICAL_SURFACE('',#283,3.);
#283 = AXIS2_PLACEMENT_3D('',#284,#285,#286);
#284 = CARTESIAN_POINT('',(20.,15.,0.));
#285 = DIRECTION('',(0.,0.,1.));
#286 = DIRECTION('',(1.,0.,0.));
#265 = EDGE_CURVE('',#266,#266,#268,.T.);
#266 = VERTEX_POINT('',#267);
#267 = CARTESIAN_POINT('',(23.,15.,10.));
#268 = SURFACE_CURVE('',#269,(#274,#281),.PCURVE_S1.);
#269 = CIRCLE('',#270,3.);
#270 = AXIS2_PLACEMENT_3D('',#271,#272,#273);
#271 = CARTESIAN_POINT('',(20.,15.,10.));

它有四个地方,不适合让 Transformer 来写。

它是一张图,写成了指针

Transformer 一个 token 接一个 token 往下写。B-rep 是一张带环的图:面指向它的边圈,边圈指向它的边,每条边被两个面共享,每个顶点被连到它的每条边共享。要把它写成序列,只能给每个实体编号,再写编号:#403、#404、#266。这块板要写 500 个这样的编号,每一个都得落在恰好那个实体上,常常隔着几百个 token。指错一个,得到的不是一块稍微不对的板,而是一个根本不成实体的东西。

代码里也有引用,但引用的是名字:plate、hole。名字不存在,构建就停在用到它的那一行。

同一个数住在好几处,必须处处对上

代码里,孔的半径写一次。STEP 里写了五次:上下两圈边沿的圆、这两个圆在顶面和底面上的 2D 副本、圆柱面本身。另外还隐含了三次:两个顶点和 x = 23 处那条接缝,它们只有落在那些圆上才对。孔的位置出现在八个点里。

把孔从 Ø6 改成 Ø6.4。代码里是一个数。STEP 里恰好有八个实体里的八个值要变,而且必须一起变。漏掉一个顶点,让它还停在 x = 23,而它的圆已经是 3.2,这个顶点就不在自己的边上了。文件里写着的公差是 1E-07 mm。

Transformer 一个一个地采样数字,采出来的数大致是对的。B-rep 要求序列里相隔很远的数,彼此对上到万分之一微米。写代码时,这件事不归模型管:孔和顶面在哪里相交,是内核算出来的交线,天生就对。

数量一变,就是另一份文件

只要拓扑不变,改尺寸只动局部。在我们的盒子生成器上,把内腔加长 5 mm,代码里改一个 token,STEP 里变 753 个实体,占文件的 11%。改数量就是另一份文件了:直齿轮从 20 齿改成 21 齿,代码里还是一个 token,STEP 里四分之三的实体都变了,从 #16288 起全部重新编号。

人们找助手改模型,多半是第二种:多一个齿、多两个孔、再加一格。在 B-rep 上,每一次都是重新生成。

同一个实体有很多种写法

我们把这块板建了两次:一次是长方体减圆柱,一次是在草图里画一个矩形、挖掉一个圆,再拉伸。同样 7 个面,体积对到百分之一立方毫米。两份 STEP 有 61% 的行不一样:圆柱面在一份里是第七个面,在另一份里是第五个;几个平面的原点不同;孔的接缝参数方向相反;还有一个边圈的方向标记是反的。

说句公道话,导出器在能一致的地方是一致的:两个孔换个顺序挖,导出来的文件逐字节相同。但「这块板」仍然不是一个序列,而是一族同样正确的序列。训练模型去复现其中一种,它写出另一种同样对的,也会被扣分。

而且它不知道每样东西是干什么的

这 434 个实体里,没有一处写着「孔」,更没有「Ø6 是因为要过 M6 螺丝」「这四个孔是一组阵列」「这面墙 2 mm 是因为要打印」。文件里每个面、每条边、每个点的名字都是空的。意图在代码里,在名字、常量和注释里。B-rep 是代码跑完之后剩下的东西。

大家最先想到的理由是体积,其实它没有上面四条要紧。在我们的 28 个生成器上,STEP 的 token 数中位数是 model.py 的 22 倍;但简单的车削件或方块件只有 3 到 9 倍,法兰盘的 OCCT 原生 B-rep 格式和它的代码差不多长。比值是在几何重复的地方爆掉的:齿轮、Gridfinity 底板把每个齿、每个格子都完整写一遍,代码里只是一个循环。

别人走过的路

学界两条路都试过。最早的 CAD 生成模型之一 DeepCAD(2021)是有意选了命令序列而不是 B-rep:「命令序列很容易转成 B-rep,反过来却很难,因为不同的命令序列可能得到同一个 B-rep。」

直接生成 B-rep 的模型,都把「有效率」当头号指标:

最清楚的例子是一家公司。Zoo 在 2023 年 12 月推出 Text-to-CAD 时,生成的是 B-rep 曲面,下载下来是 STEP。按他们 2026 年 2 月自己的说法:「我们最初走的是直接从文本生成 B-rep 的路子……有两件事最终让我们改了方向」,一是他们自己的 CAD 语言 KCL,二是大语言模型的崛起。他们的 agent 现在只返回 KCL 代码,直接产出 STEP 的那几个 API 在 2026 年 8 月下线了。谈到生成网格的模型时,他们一句话说完了整个道理:「这就像拿编译产物而不是源代码来训练。」

也有人持相反意见。Autodesk 的 Mike Haley 说,他们的 neural CAD 模型「直接在几何里推理,而不是通过 API 去谈论几何」,并且把反对写代码的理由说得很直白:「让 LLM 去调参数化 CAD 的 API,就像雇一个读遍了所有管道书、却从没碰过一根水管的水管工。」这个诊断我们认,药方不认。只写代码、从不看结果的模型,确实就是那个水管工;我们的 agent 每改一次都要构建,再把内核建出来的东西读回来。

B-rep 该待的地方

上面这些都不是在反对 B-rep。它就是我们交付的东西:你从 Forgent3D 下载的每一份 STEP,都是内核建出来的 B-rep。它也是 agent 检查自己工作的依据:每次构建之后,agent 读一份从 B-rep 算出来的报告,每个圆柱面的半径和轴线位置、每个平面所在的位置(具体怎么做)。模型读的是 B-rep 的摘要,从来不是 B-rep 本身。

B-rep 也是很好的输入。把 B-rep 当成一张由面和边组成的图来读的神经网络,用来给零件分类、给面打标签,效果很好。被图网络读,和被序列模型写,是两件不同的事。

那模型该写什么?

写交给内核编译的代码:短到人能读,用名字而不是指针,每个数只写一次,几何自洽这件事交给内核。具体是什么样的代码,除了几何还要带上什么,是另一个问题,也就是下一篇。

更多文章