1. 从一句话到三维模型:text-to-cad 到底在解决什么问题
第一次听到 “text-to-cad” 这个说法,我脑子里蹦出来的画面是:对着电脑敲一句“给我画一个 80×60×20 的法兰盘,中心开直径 30 的通孔,四角各一个 M6 沉头孔”,然后软件自己把模型建好、把工程图出好、把 STEP 文件丢给我。这个画面放在五年前基本属于科幻,但放到今天,它已经是一条能跑通的技术链路了。所谓 text-to-cad,本质上是把自然语言描述翻译成参数化 CAD 模型,再导出成 STEP、GLB、STL 这类通用格式的过程。它要解决的核心痛点很朴素:大量重复性的、结构相似的零件建模工作,占用了工程师太多时间,而这些工作恰恰是最容易被规则化和自动化的。
我身边做机械设计、钣金、建筑构件、甚至做 3D 打印摆件的朋友,几乎都有同一个抱怨——真正花在“想结构”上的时间可能只占两成,剩下八成都在“把想法敲进 CAD 里”。画草图、约束、拉伸、打孔、倒角、改尺寸、再改尺寸,一套流程下来,一个简单支架能磨掉半小时。text-to-cad 想干的事,就是把这八成里的“机械劳动”压缩掉,让人只负责描述意图和校验结果。它适合谁?适合经常做系列化零件的人、适合做参数化建模的工程师、适合想批量生成模型的 3D 打印玩家,也适合做 CAD 二次开发、想把 AI 能力接进自己工具链的程序员。
这里要先泼一盆冷水:text-to-cad 不是“说一句话就出成品”的魔法。自然语言天生是模糊的,而 CAD 要求的是精确到小数点后几位的几何约束。这两者之间的鸿沟,就是整个技术方案要填的坑。所以真正能落地的 text-to-cad,几乎都不是“端到端一个大模型直接吐 STEP”,而是分层拆解 + 中间表示 + 参数化重建的组合拳。下面我就按我自己趟过的路子,把整套思路、关键细节、实操过程和踩坑记录完整拆一遍。
2. 整体方案怎么搭:为什么不能一步到位
2.1 端到端生成 CAD 的幻想与现实
很多人第一反应是:现在大模型这么强,直接让它输出 STEP 文件不就行了?我试过,结论是——能出,但基本不能用。原因有三个。第一,STEP 是边界表示(B-rep)格式,里面是精确的曲面、边、顶点的拓扑关系,而语言模型擅长的是 token 序列,它对“这个面和那个面共享一条边”这种拓扑约束没有天然的建模能力。第二,语言模型输出的数值经常“看起来对”,但公差、配合、壁厚这些工程约束它并不真的理解,生成的东西能看不能用。第三,就算勉强生成了,一旦要改一个尺寸,整个模型可能就崩了,因为它不是参数化的。
所以我的判断是:端到端直接生成 B-rep 这条路,目前只适合做概念草图或者玩具级模型,不适合工程交付。真正靠谱的做法,是把自然语言先翻译成一种“结构化的中间表示”,再由确定性的几何内核去重建模型。这个中间表示,可以是 JSON 描述的参数表,也可以是一段脚本(比如 CadQuery、OpenSCAD、FreeCAD 的 Python 脚本),甚至是一棵特征树。
2.2 分层架构:语言层、参数层、几何层
我最终采用的架构分三层,这个分层是我踩了很多坑之后定下来的,强烈建议你也按这个思路走。
- 语言层:负责把用户那句人话解析成结构化的意图。比如“法兰盘、外径 80、厚 20、中心通孔 30、四角 M6 沉头孔”,要能抽出实体类型、尺寸参数、特征列表。这一层用大模型做意图识别和槽位填充最合适。
- 参数层:把意图转成一份严格的参数 schema,做单位统一、默认值填充、合法性校验。这一层是纯代码逻辑,不依赖模型,保证可复现。
- 几何层:拿参数去驱动几何内核,生成实体,导出 STEP/GLB/STL。这一层用成熟的内核,比如 OpenCASCADE(通过 CadQuery 或 pythonocc 调用),保证几何质量。
这么分的好处是:语言层可以换模型、可以调 prompt,不影响几何;几何层可以换内核、换导出格式,不影响语言理解。每一层都能单独测试,出问题好定位。这就是为什么我说“不能一步到位”——一步到位意味着你没法调试,出了错你都不知道是理解错了还是建模错了。
2.3 中间表示选 JSON 还是脚本
中间表示我两种都用过,说说取舍。JSON 参数表的优点是安全、可控、易校验,你可以在参数层写一堆规则,比如“孔径必须小于外径”“壁厚不能小于 1mm”,不满足就直接拒绝。缺点是表达能力有限,遇到复杂特征(比如阵列、扫掠、放样)描述起来很别扭。脚本(比如 CadQuery 的 Python 代码)的优点是表达能力强,几乎能描述任何几何操作,而且本身就是可执行的,改一个参数就能重跑。缺点是安全性差——让模型直接生成可执行代码,万一它写出个死循环或者乱删文件就麻烦了。
我的折中方案是:简单零件走 JSON,复杂零件走脚本,但脚本必须跑在沙箱里,并且只允许调用白名单内的建模 API。这样既保证了灵活性,又控制了风险。下面实操部分我会以 JSON 路线为主,因为它最容易复现,也最适合新手起步。
3. 核心细节拆解:从一句话到一份参数表
3.1 意图识别:怎么让模型听懂“法兰盘”
语言层最关键的一步是意图识别。用户说的话千奇百怪,有人会说“画个圆盘”,有人会说“做个法兰”,有人直接甩一句“DN50 的法兰”。你得让模型把这些都归到同一个实体类型上。我的做法是维护一份实体类型词典,每个类型下面挂同义词和必填参数。比如法兰盘这个类型,同义词有“法兰、圆盘、盘类件”,必填参数有外径、厚度、中心孔径。
这里有个经验:不要让模型自由发挥实体类型,要给它一个封闭的候选列表。我一开始让模型自己判断“这是什么零件”,结果它经常发明一些不存在的类型,后面几何层根本没法处理。改成“从以下类型中选择最匹配的一个”之后,准确率立刻上来了。这就是典型的“约束比自由更可靠”。
3.2 槽位填充与单位陷阱
槽位填充就是把“外径 80”里的 80 抽出来,绑定到“外径”这个参数上。听起来简单,但单位是个大坑。用户可能说“外径 80”,也可能说“外径 8 公分”,还可能说“外径 80mm”。如果你不统一单位,后面几何层就会把 8 公分当成 8 毫米,模型直接小十倍。我的处理方式是:在参数层强制统一到毫米,并且在 prompt 里明确要求模型输出时带上单位,由代码做换算。换算表很简单,1 公分 = 10 毫米,1 米 = 1000 毫米,1 英寸 = 25.4 毫米。别小看这一步,我见过太多项目栽在单位上。
还有一个隐蔽的坑:默认值。用户说“做个法兰”,没给厚度,你怎么办?我的做法是给每个参数设一个合理的工程默认值,比如法兰厚度默认 10mm,中心孔默认通孔。但默认值必须在结果里明确标注出来,让用户知道“这个数是我替你填的”,否则他拿到模型会一脸懵。这一点在交互设计上很重要,属于“透明化”原则。
3.3 参数合法性校验:把错误挡在建模之前
参数层最容易被忽视、但价值最高的一环是校验。我列几个我实际写进代码的规则:
| 校验项 | 规则 | 不通过的后果 |
|---|---|---|
| 尺寸为正 | 所有长度参数 > 0 | 负尺寸会导致内核报错或生成退化实体 |
| 孔径小于外径 | 中心孔径 < 外径 - 2×壁厚 | 否则壁厚为负,实体自相交 |
| 壁厚下限 | 壁厚 ≥ 1mm(塑料)/ ≥ 0.5mm(金属) | 太薄会导致 STL 导出后破面 |
| 孔位不越界 | 孔中心到边缘距离 ≥ 孔径 | 否则孔会切穿外壁 |
| 沉头孔匹配 | 沉头直径 > 通孔直径 | 否则沉头特征无法生成 |
这些规则看着琐碎,但每一条都是我用真实失败案例换来的。比如“孔位不越界”这条,就是因为我生成过一个四角孔直接切到外圆上的法兰,导进切片软件一看,模型是破的。把校验做在建模之前,比建模之后再去修要省事一百倍。
4. 实操过程:手把手跑通一条 text-to-cad 链路
4.1 环境准备与工具选型
先说工具链。几何内核我选OpenCASCADE,因为它开源、成熟、对 STEP 支持好。上层封装用CadQuery,它把 OpenCASCADE 包了一层 Pythonic 的 API,写起来像搭积木,非常适合做参数化重建。语言层我用一个通用大模型做意图解析,输出 JSON。导出格式方面,STEP 用于工程交付,STL 用于 3D 打印,GLB 用于网页预览,三个都从同一个实体导出,保证几何一致。
安装这块,CadQuery 官方推荐用 conda 装,因为它的依赖(尤其是 OpenCASCADE 的二进制)用 pip 装容易出问题。我实测下来,conda 环境最稳:
conda create -n text2cad python=3.10 conda activate text2cad conda install -c conda-forge cadquery装完之后跑一句import cadquery as cq; print(cq.__version__),能打印版本号就说明环境通了。这里提醒一句:CadQuery 对 Python 版本比较挑,3.10 是我验证过最稳的,3.12 有时候会碰到依赖冲突。别问我怎么知道的,重装了三遍。
4.2 语言层:把一句话变成 JSON
语言层我写了一个函数,输入是用户那句话,输出是一份 JSON。prompt 的核心是给它一个 schema 模板,让它照着填。我简化后的 schema 长这样:
{ "part_type": "flange", "params": { "outer_diameter": {"value": 80, "unit": "mm"}, "thickness": {"value": 20, "unit": "mm"}, "center_hole": {"value": 30, "unit": "mm"}, "bolt_holes": { "count": 4, "diameter": {"value": 6, "unit": "mm"}, "circle_diameter": {"value": 60, "unit": "mm"}, "type": "counterbore" } } }prompt 里我会明确写:“只输出 JSON,不要解释,单位统一用 mm,缺失的参数不要瞎编,留空即可。” 实测下来,加了“不要瞎编”这句之后,模型乱填参数的情况明显减少。这就是 prompt 工程里的“负向约束”,有时候比正向描述还管用。
4.3 参数层:校验、补默认值、算派生量
拿到 JSON 之后,参数层做三件事。第一,单位换算,把所有值统一到毫米。第二,补默认值,比如用户没给沉头深度,我按通孔直径的 0.6 倍估算。第三,算派生量,比如螺栓孔的中心圆半径 = 中心圆直径 / 2,孔位的 x、y 坐标 = 半径 × cos/sin(角度)。这些派生量不交给模型算,全部用代码算,因为三角函数这种确定性计算,代码比模型可靠得多。
这里分享一个计算细节:四孔均布时,角度间隔是 360/4 = 90 度,起始角我习惯从 45 度开始,这样四个孔落在对角线上,视觉上更对称。孔位坐标就是:
import math r = circle_diameter / 2 for i in range(count): angle = math.radians(45 + i * 360 / count) x = r * math.cos(angle) y = r * math.sin(angle)这段代码简单,但它是整个链路里最不该出错的地方,因为孔位错了整个零件就废了。所以我给它单独写了单元测试,输入已知参数,断言输出坐标在预期范围内。
4.4 几何层:用 CadQuery 重建实体
几何层是重头戏。以法兰盘为例,CadQuery 的建模逻辑非常直观,基本就是“画圆、拉伸、挖孔、阵列”:
import cadquery as cq result = ( cq.Workplane("XY") .circle(outer_diameter / 2) .extrude(thickness) .faces(">Z") .workplane() .hole(center_hole) ) # 螺栓孔阵列 result = ( result.faces(">Z") .workplane() .pushPoints(bolt_positions) .cboreHole(bolt_diameter, cbore_diameter, cbore_depth) ) cq.exporters.export(result, "flange.step") cq.exporters.export(result, "flange.stl") cq.exporters.export(result, "flange.glb")这段代码里,cboreHole是沉头孔,参数依次是通孔直径、沉头直径、沉头深度。pushPoints接收一个坐标列表,把孔打到指定位置。导出三个格式各一行,STEP 给下游 CAD,STL 给打印,GLB 给预览。实测下来,一个法兰盘从解析到导出,全程不到两秒,比手动画快太多了。
4.5 导出格式怎么选:STEP、GLB、STL 的适用场景
这三个格式经常被混用,我按实际经验说清楚它们的区别,避免你导错格式返工。
| 格式 | 本质 | 适用场景 | 注意事项 |
|---|---|---|---|
| STEP | 精确 B-rep | 工程交付、下游 CAD 编辑、CNC 加工 | 保留精确曲面和拓扑,文件较大 |
| STL | 三角网格 | 3D 打印、快速预览 | 只有网格,丢失精确曲面,精度靠细分控制 |
| GLB | 三角网格 + 材质 | 网页预览、AR/VR、可视化 | 支持颜色材质,适合展示不适合加工 |
我的建议是:只要涉及后续编辑或加工,一律以 STEP 为准,STL 和 GLB 都是从 STEP 派生出来的展示副本。千万别拿 STL 去反推工程图,网格转 B-rep 是个逆问题,精度损失不可逆。热词里那个“sw 中 stl 转 stp”就是这个痛点,转出来的面全是碎三角面,根本没法做特征编辑。
5. 常见问题与排查技巧实录
5.1 模型导出后破面、切片软件报错
这是最高频的问题。表现是 STL 导进切片软件后,模型显示为破面、有孔洞、或者切片直接失败。原因通常有三个:一是壁厚太薄,网格细分时相邻面重叠;二是实体本身自相交,比如孔切穿了外壁;三是导出精度设置太低。排查顺序我一般是:先看参数校验有没有拦住异常尺寸,再把 STL 的线性偏差(linear deflection)调小,CadQuery 里可以这样设:
cq.exporters.export(result, "flange.stl", tolerance=0.01, angularTolerance=0.1)tolerance是线性偏差,越小网格越密,默认值有时候偏大。我一般设 0.01mm,兼顾精度和文件大小。如果调小之后还是破面,那基本就是实体本身有问题,回去查参数校验。
5.2 单位错乱导致模型尺寸离谱
前面提过单位坑,这里给个排查技巧:在参数层打印一份“归一化后的参数表”,每次生成都看一眼。如果发现某个尺寸明显不对(比如法兰外径变成 8mm),立刻能定位是解析错了还是换算错了。我还会在导出后读一下模型的包围盒尺寸,跟预期值比对,差太多就报警。这个“包围盒自检”是个很实用的兜底手段,几行代码就能加:
bb = result.val().BoundingBox() print(f"X: {bb.xlen:.2f}, Y: {bb.ylen:.2f}, Z: {bb.zlen:.2f}")5.3 复杂特征描述不清,模型生成失败
用户说“做个带加强筋的支架”,这种描述对模型来说太模糊了。我的处理方式是主动追问,而不是硬猜。在参数层发现必填参数缺失或特征描述不完整时,返回一个“需要补充信息”的响应,列出缺哪些参数。这比生成一个错误模型让用户去改要友好得多。交互设计上,这叫“fail fast”,快速失败、快速反馈。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 模型尺寸差十倍 | 单位未统一 | 检查归一化参数表 |
| STL 破面 | 壁厚太薄或实体自相交 | 调小 tolerance,查参数校验 |
| 孔位偏移 | 阵列角度算错 | 检查孔位坐标计算逻辑 |
| STEP 导入下游软件报错 | 内核版本不兼容 | 换用标准 AP214 协议导出 |
| 生成速度慢 | 网格细分过密 | 适当放大 tolerance |
| 模型无法编辑 | 导出成了 STL | 改用 STEP 导出 |
5.5 几个我踩过的坑和独家心得
第一个坑:别用模型算三角函数。我早期偷懒,让模型直接输出孔位坐标,结果它算的 cos/sin 值精度不够,孔位偏了零点几毫米。后来全部改成代码算,问题消失。第二个坑:prompt 里一定要禁止模型输出注释和解释。它一旦开始“我觉得这个法兰应该……”,JSON 就解析失败了。第三个心得:给每个实体类型写一个最小可复现样例,作为回归测试。每次改 prompt 或改代码,先跑一遍样例,确认没退化再上线。这个习惯帮我省了无数次返工。
6. 这套方案还能怎么扩展
跑通基础链路之后,我做了几个扩展,效果都不错。一个是批量生成,把一份 Excel 参数表读进来,循环调用整条链路,一次生成几百个系列化零件,导出 STEP 和 STL。热词里那个“python 批量对 cad 修改”说的就是这个场景,只不过传统做法是操作 CAD 的 API,而 text-to-cad 是从参数直接重建,更干净。另一个扩展是接入预览,把 GLB 丢到网页里用 three.js 渲染,用户改一个参数就能实时看到模型变化,体验比等文件下载好太多。
还有一个方向是反向能力:给一个已有的 STEP 文件,让系统识别出它的参数表,然后用户就能用自然语言去改它。这个比从零生成难,因为要解析 B-rep 的特征,但价值也更大,适合做“老图翻新”的场景。我目前只做到简单回转体和拉伸体的识别,复杂曲面还在摸索。
最后分享一个小技巧:把每次生成的参数表和用户原话都存下来,攒一段时间就是一份高质量的微调数据集。等你数据够了,可以拿它去微调一个专门做意图解析的小模型,推理更快、成本更低,还不用每次都调大模型。这条路我刚开始走,等有结果了再跟大家汇报。