全部文章

给语言模型用的 CAD IR,不该是一棵特征树

· 约 7 分钟

上一篇的结论是:让模型写交给 CAD 内核编译的代码。这样一来,语言模型和内核之间就多了一层:模型写它,内核把它变成 B-rep,人读它、改它,或者亲自动手,或者通过界面。编译器管这种东西叫中间表示(IR),放在 CAD 里,就叫 CAD IR。

难处在于它要同时服务三个读者。B-rep 只服务内核。这篇讲这一层要带上什么才能服务三方,以及为什么目前最认真的答案,Zoo 的 KCL,停在了低一层的位置。

这一层要做到什么

做 Forgent3D 这段时间,我们攒下了七条。

短,按顺序写。 Transformer 从左往右写,每多一个 token 就多一次出错的机会。在我们的 28 个生成器上,代码的中位数是 2768 个 token,对应的 STEP 文件中位数是 63746 个。

用名字,不用指针。 lid、boss、HOLE_D。名字不存在,构建就停在用到它的那一行。

每个数只写一次,并写明从哪来。 我们的模型里,每个常量都标着它是用户给的、推出来的,还是默认值:

BORE_D = 9.0   # stated: bearing bore (mm)
WALL = 2.0     # default: printed wall (mm)

构建之后,我们在几何里逐个找用户给的数:BORE_D / 2 应该表现为一个半径 4.5 的圆柱面,报告会说它在不在。有人在界面上拖动一个值,这次改动会沿算式反解回它来自的那个常量,周围的公式还是公式。

按意义引用几何。 顶面、筋和底板相交的那条边、叫 bores 的那个特征,而不是「第 7 个面」。参数化 CAD 给反过来的情形起过名字,叫拓扑命名问题:上游一改,面重新编号,圆角落到了错的边上。Zoo 是从另一头撞上它的,在他们自己的引擎改了拆面的方式之后:「如果你的模型给第 17 条边倒了圆角,而那个面现在成了第 18 条边,这个新算法就改变了哪些边被倒圆角,打破了我们对稳定性的承诺。」我们的选择器按能不能扛住修改给引用排序:具名特征最稳,其次是恰好只命中一个的查询,再次是显式排序,最后才是默认顺序。

每次编译结果一样,失败要响。 同一份源码,在你的浏览器里和在我们的服务器上建出同一个实体。圆角放不下,构建会说出来、说在哪里,绝不悄悄改小。

两个方向都能追溯。 点一个面,看到建它的那一行;拖一个数,看到它来自哪个常量。界面和 agent 能指着同一样东西说话,靠的就是这个。

模型本来就会写。 语言模型读过海量的 Python,CAD 代码却读得少得多:2025 年的一项研究里,现成的大模型写 CadQuery,至少有 47% 建不出有效的零件,专门微调之后降到 2% 以下。一门新语言,只能从文档和例子学起。

KCL,目前最认真的答案

Zoo(原来叫 KittyCAD)专门为这一层做了一门语言:KCL。它的前提和我们一样,用他们的话说:「所有产物,包括零件和装配,都应该表示成人能读的代码」,「所有图形界面上的点选操作,底层都应该是对这份代码的操作」。在 Zoo Design Studio 里每点一下都在写 KCL,他们的 agent Zookeeper 改的也是同一份 KCL。界面和 agent 共用一份源码,这正是上面那张清单想要的性质。

对照清单,KCL 有几处是 Python 做不到的。变量不能重新赋值。参数要写名字。单位是数字的一部分(42mm),每个文件有默认单位。对边的引用正在从 tag 改成「用边两侧的面来指定」,Zoo 说这样「在前面的建模操作改变了实体拓扑时,引用更稳」。语言和应用都开源(MIT),解释器用 Rust 写成,编译为 WebAssembly。

下面是一个支架轮廓的 KCL 3.0 写法(2026 年 10 月起成为默认版本),摘自 Zoo 的迁移指南:

bracketProfile = sketch(on = XY) {
  bottom = line(start = [var 0mm, var 0mm], end = [var 30mm, var 0mm])
  right = line(start = [var 30mm, var 0mm], end = [var 30mm, var 20mm])
  top = line(start = [var 30mm, var 20mm], end = [var 0mm, var 20mm])
  left = line(start = [var 0mm, var 20mm], end = [var 0mm, var 0mm])
  coincident([bottom.end, right.start])
  coincident([right.end, top.start])
  coincident([top.end, left.start])
  coincident([left.end, bottom.start])
  horizontal(bottom)
  vertical(right)
  horizontal(top)
  vertical(left)
}
bracket = extrude(region(point = [15mm, 10mm], sketch = bracketProfile), length = 5mm)

一块 30 × 20 × 5 mm 的板:四条线段、四个重合约束、四个水平或竖直约束,一个 region,一次拉伸。每一行都清楚,合起来就是参数化 CAD 一直在存的东西:一棵特征树。Zoo 自己也这么说,他们的路线图把代码编辑器描述成「你写下定义模型和特征树的代码,1:1」的地方。

随之而来有三个代价:

把特征树写成文本还不够

NX 和 CATIA 存特征树已经几十年了,内核(Parasolid、CGM)被几十年的工业零件磨过。把特征树写成文本,能 diff,也让语言模型有东西可写,但模型要想的东西没变:线段、约束、圆角落在哪条边。

人们要零件,不是这么说的。他们说:一个能扣上的盖子,四个放 M3 热熔螺母的孔,给这块板做个外壳、USB 口露出来。我们看到 agent 出错,就出在这两者之间的翻译上:把手绘草图的方向读反,或者从头去推一个本该来自打印经验的数,比如热熔螺母的孔径。给语言模型用的 CAD IR,应该带上特征树从来不存的东西:

我们现在在哪

我们的模型是 Python,按 build123d 的风格写。清单上「模型本来就会写」那一条,Python 占了;其余的,我们靠约定往上叠了一部分:

这些都是约定,Python 一条也不强制。模型照样可以把孔写在绝对坐标上、按序号挑一条边、把同一个数写在三个地方。构建能在事后抓到一部分,语言在事前一条也抓不到。

所以这两个答案,都还不是清单描述的那个 IR。KCL 把规则写进了语言,但停在特征树那一层。我们把规则放在特征树之上,却只是语言本身不知道的约定。

B-rep 服务内核。写成文本的特征树服务内核和编辑器。给语言模型用的 CAD IR,还得服务模型本身,而且要用人们要零件时说的那套话。

更多文章