上一篇的结论是:让模型写交给 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」的地方。
随之而来有三个代价:
- 模型得现学。 参与过 KCL 设计的 Nick Cameron 写过:「KCL 是一门新语言,能拿来训练 AI 的代码不多。」2025 年 8 月,Zoo 说他们把生成的出错率从 50% 降到了 16%。
- 它在变。 KCL 1.0 在 2025 年 5 月发布,2.0 在 2026 年 5 月成为默认,3.0 在 2026 年 10 月;KCL 最早的管道写法已经废弃。旧文件钉住了版本,照样能跑,但模型去年学会的写法,已经不是文档现在推荐的写法。
- 几何来自 Zoo 的引擎。 它闭源,跑在 Zoo 的云上,或者企业自己的服务器上,断了网就建不了模。
把特征树写成文本还不够
NX 和 CATIA 存特征树已经几十年了,内核(Parasolid、CGM)被几十年的工业零件磨过。把特征树写成文本,能 diff,也让语言模型有东西可写,但模型要想的东西没变:线段、约束、圆角落在哪条边。
人们要零件,不是这么说的。他们说:一个能扣上的盖子,四个放 M3 热熔螺母的孔,给这块板做个外壳、USB 口露出来。我们看到 agent 出错,就出在这两者之间的翻译上:把手绘草图的方向读反,或者从头去推一个本该来自打印经验的数,比如热熔螺母的孔径。给语言模型用的 CAD IR,应该带上特征树从来不存的东西:
- 每个特征是干什么的:M3 的过孔、热熔螺母柱、一个卡扣,而不是「在某个坐标减掉一个圆柱」;
- 哪些数是用户给的,哪些是默认值,这样构建才能核对要的东西是不是就是出来的东西;
- 需求:最小壁厚、配合间隙、零件要放得进的那块打印床;
- 零件之间的关系:这个盖子在那条槽里滑动,留 0.2 mm 余量;
- 对设计对象的引用:盖子的配合面、第三颗螺丝的柱,而不是面和边。
我们现在在哪
我们的模型是 Python,按 build123d 的风格写。清单上「模型本来就会写」那一条,Python 占了;其余的,我们靠约定往上叠了一部分:
- 组件库:一个热熔螺母柱是一次调用,数字都已经算好,而不是每次让模型从头推一个圆柱(为什么);
- 每个常量标
stated/derived/default,构建后核对; - 把常量变成页面滑块的标记;
- 按能不能扛住修改来排序的选择器;
- agent 每改一次都要读的构建报告(怎么做的)。
这些都是约定,Python 一条也不强制。模型照样可以把孔写在绝对坐标上、按序号挑一条边、把同一个数写在三个地方。构建能在事后抓到一部分,语言在事前一条也抓不到。
所以这两个答案,都还不是清单描述的那个 IR。KCL 把规则写进了语言,但停在特征树那一层。我们把规则放在特征树之上,却只是语言本身不知道的约定。
B-rep 服务内核。写成文本的特征树服务内核和编辑器。给语言模型用的 CAD IR,还得服务模型本身,而且要用人们要零件时说的那套话。