做结构件的人应该都有过这种体验:真正烧脑的是想清楚需求,真正浪费时间的是把需求变成模型。上周我要出一批规格相近的支架,八种外形尺寸、两种安装方式、三组螺纹孔位置,用传统CAD一个个重建,光是约束草图和重新拉伸就耗了大半天。我换了个思路,用text-to-cad的方式,把需求写成人话,让大模型生成Python代码,代码直接驱动CAD内核出STEP文件,实际用时不到两个小时。这篇文章讲的不是理论概念,而是我在这两天里搭起来的一套完整链路:技术选型、环境搭建、Prompt怎么设计、几何怎么校验、批量怎么跑。适合所有想把手头重复建模工作交给自动化的人,无论你是搞机械设计、钣金加工,还是做3D打印快速出样。
1. text-to-cad 不是"一句话变模型",而是建模逻辑的倒置
很多人第一次听到"文字转CAD",以为就是输入一句"帮我画个扳手",软件哗啦一下吐出实体。真正跑过一遍之后你会明白,这个想法错得离谱,但也歪打正着触及了问题的本质。
1.1 传统CAD为什么让人感觉"慢"
回想一下日常建模的过程:新建草图、画轮廓、加约束、标注尺寸、拉伸、打孔、倒角、再改一个尺寸导致约束报错……每一步都是图形界面里的鼠标操作。麻烦的地方在于,大多数工程零件并不是什么创新设计,而是"我知道它长什么样,但就是得把它画出来"。一块支架板,八个孔,两个沉台,一个腰型槽,画完主体只要十分钟,但你得在草图和特征树之间来回点二十多下。改型号更痛苦,尺寸一变,关联的约束可能崩一片。
这种"知道答案还要手动一步步写过程"的场景,恰恰是自动化最擅长处理的事情。而text-to-cad做的是更极端的一步:连"把这个需求翻译成建模操作序列"都不让你干,直接交给大模型。
1.2 两个前提条件:代码化CAD API与代码生成能力
text-to-cad能在最近两年跑通,靠的是两个条件同时成熟。
第一,CAD软件必须提供干净、稳定的编程接口。传统CAD虽然也带宏录制和二次开发接口,但那些接口大多是为GUI操作服务的,状态多、依赖文档对象模型,代码写起来啰嗦且容易被软件版本影响。真正适合text-to-cad的是"代码即模型"这一派:CadQuery、OpenSCAD、Build123d,它们把建模过程写成一段可以反复执行的程序,几何体和代码是绑定关系。
第二,大模型的代码生成能力要够用。模型写常规Python已经很稳了,而CAD建模API本质上就是Python,只要API设计得接近自然语言,模型就能把需求描述翻译成操作链。这比让模型直接操作鼠标、读取屏幕坐标靠谱得多。说白了,大模型擅长的是"文字到代码"的映射,而不是"文字到GUI操作序列"的映射。
1.3 三条能跑通的技术路线
我调研了一圈,目前真正能落地的路线有三条,各有各的脾气。
| 路线 | 代表工具 | 底层内核 | 主要输出 | 适合场景 |
|---|---|---|---|---|
| 代码化CAD库 | CadQuery / Build123d | OpenCASCADE | STEP/STL/DXF | 机械零件、批量变体 |
| 函数式CSG | OpenSCAD | CGAL | STL | 简易几何、3D打印 |
| 桌面软件脚本 | FreeCAD Python API | OpenCASCADE | STEP/STL/FCStd | 已有FreeCAD流程的团队 |
另外还有一个值得一提的项目:Zoo开源的Text-to-CAD,它把一个大模型微调成了CadQuery代码生成器,配合沙箱执行和校验。这个项目最让我受启发的不是模型本身,而是它的工程结构——生成代码、执行、校验、报错重试,这一整套循环才是text-to-cad能落地的关键。我下面要讲的,基本就是围绕这条思路展开的自建版本。
2. 选型决策:为什么我最终押注CadQuery这条链路
三条路线里我选了CadQuery,不是因为它功能最强,而是因为在"喂给大模型"这个场景下,它是最不容易出错的。
2.1 从LLM可控性角度对比三条路线
大模型本质上是个会写代码但不懂"代价"的实习生。你交给它一个任务,它会写出一段看起来像那么回事的代码,但代码能不能跑、跑出来的几何对不对,它心里没数。所以选型的第一标准不是"功能全不全",而是"容易不容易被AI写错,以及写错了容不容易发现"。
OpenSCAD的问题是语法太个性化。它有自己的函数式语言,和主流语言差异大,模型在训练语料里见过的OpenSCAD代码远少于Python。我试过让它生成一个简单的盒子带圆角,模型会把Python的语法混进去,或者把rounded_cylinder这种不存在的模块写出来。而且OpenSCAD只能导出网格STL,对工程制造来说信息损失太大。
FreeCAD的脚本接口理论上最正统,但它要求先建立Document、再创建对象、最后recompute。模型经常搞错顺序,生成一段"语法完全正确但重算后什么都没出现"的代码。这种错误最难排查,因为你连报错都看不到。
CadQuery的优势恰恰在于"线性":每次操作返回一个新的Workplane对象,特征一个接一个地叠加上去,代码顺序基本等于特征顺序。大模型按token逐个生成代码,天然擅长这种顺序叙事。出了问题也容易定位,异常信息直接告诉你是哪一步失败。
2.2 CadQuery的建模心智模型:Workplane链式调用
CadQuery的核心概念叫Workplane,你可以把它理解成一个"带着当前坐标系的临时工作面"。所有操作都发生在这个工作面上:画圆、画矩形、拉伸、打孔、倒角,每一步返回一个新的Workplane,然后继续链式调用下去。这种心智模型非常接近传统CAD里的"草图+特征树",而且比GUI更严格——每一步都是可复现的。
给你看一个最典型的例子,一个带四孔安装座的支架:
import cadquery as cq bracket = (cq.Workplane("XY") .box(80, 40, 4) .edges("|Z").fillet(2) .faces(">Z").workplane() .rect(60, 20, forConstruction=True) .vertices().hole(4) )逐行看:先基于XY平面创建一个80乘40乘4的方块;对竖边倒圆角;选中顶面进入新的工作面;画一个辅助矩形,取其顶点位置;在每个顶点处打直径4的孔。每一行都对应传统CAD里的一个特征,模型只需要按顺序输出这些API调用就行。
2.3 输出格式的通用性:STEP才是CAD世界的通用语
选型还有一个重要考量:生成结果必须能回到传统CAD里继续干活。STL网格适合3D打印,但丢了面、边、拓扑信息,没法在专业软件里做装配和工程图。STEP格式保存的是精确的B-rep实体模型,任何主流CAD都能打开,后续要继续加特征、出图纸都方便。
CadQuery底层是OpenCASCADE内核,导出STEP是天然能力。需要二维轮廓给激光切割或水刀用时,还能直接导出DXF。这样以来,生成出来的东西就不是一个"只能看的模型",而是一个能进工艺链的真零件。我后面所有批量导出的方案,都是建立在STEP这个通用语言上的。
3. 从零跑通第一个用例:环境、代码骨架与可视化调试
理论说完了,直接上手。先从环境开始,这步看着简单,实际坑不少。
3.1 环境搭建里的经典坑位
我刚开始装CadQuery时在Windows上就翻过车,import cadquery直接报DLL加载失败。后来总结出一套相对省心的流程:
python -m venv cad-env source cad-env/bin/activate # Windows下用 cad-env\Scripts\activate pip install --upgrade pip pip install cadquery jupyter-cadquery python -c "import cadquery as cq; print(cq.__version__)"几点心得。第一,Python版本建议3.10到3.12,新版CadQuery对3.13的wheel支持还不算完善。第二,强烈建议用虚拟环境隔离,别往系统Python里装。用过传统CAD的人都知道,装CAD时经常遇到"C++ 2005运行库报错""激活页面脚本错误"这类莫名其妙的系统级问题,Python生态的好处是环境烂了直接重建一个venv,比重装软件快得多。第三,如果pip安装后导入还是报错,试试conda路线:
conda create -n cad python=3.11 conda install -c conda-forge cadqueryconda-forge的包会把OpenCASCADE依赖一起管好,兼容性更省心。
安装完别急着写代码,先把Jupyter搭起来。jupyter-cadquery提供了一个show函数,可以直接在notebook里三维预览实体,这个可视化的调试能力后面会频繁用到。
3.2 最小骨架:把"一个衬套"需求变成可执行代码
我习惯从完全没有特征的简单件开始验证环境。比如一个衬套:外径30,内径16,高度25。
import cadquery as cq bushing = (cq.Workplane("XY") .circle(30 / 2) .circle(16 / 2) .extrude(25) )代码很简单:在XY平面上画外圆,再画内圆,然后拉伸25。拉伸出来就是一个带通孔的实体,因为CadQuery对"一个工作面上的多个封闭轮廓"默认做布尔减。马上验证体积:
print(f"体积 = {bushing.val().Volume():.2f} mm^3")理论体积是π × (15² - 8²) × 25,约等于7734.8,打印出来如果接近这个数,说明内核工作正常。这一步别省,很多环境问题都是到了算体积这步才暴露的。
3.3 用Jupyter做"建模调试台"
环境就绪后,我通常把Jupyter当成模型的"调试台"。流程是这样:生成代码 → 执行 →show(result)看模型 → 检查体积和包围盒 → 导出STEP。每一步都能立刻反馈,哪个环节错了马上知道。
from jupyter_cadquery import show show(bushing)show支持鼠标拖拽旋转、缩放,还能切换线框和着色模式。这一步的意义不在于"看个大概",而在于快速发现几何方向是否正确。比如孔是不是打到了期望的面上、拉伸方向有没有反,这些视觉上一眼就能发现,比盯着坐标数字强多了。
到这里,环境就绪,我能确认链路通畅。接下来才进入重头戏:怎么让大模型稳定地输出可用的CAD代码。
4. 让LLM稳定输出几何的关键:特征拆解、Prompt模板与自检循环
直接拿一句自然语言让模型生成代码,能成功,但成功率撑不起批量使用。我在实测中大概只有一半的生成能一次通过。后来我把流程改成"特征拆解 + 结构化Prompt + 自检重试",成功率才拉到九成以上。
4.1 LLM在CAD生成里的典型幻觉
先看看模型会犯哪些错,才知道Prompt要往哪个方向用力。我总结了五种高频失败模式。
一是虚构API。模型会写出.someExtrude()这种不存在的函数。原因是训练语料里混了大量不同CAD库的代码,模型记混了。
二是半径和直径混淆。用户说"直径60",模型直接写circle(60),实际应该写circle(30)。这个错误极其常见,因为自然语言里人们习惯说直径,而API里circle()接收的是半径。
三是特征顺序错误。比如先打孔再拉伸外形,结果倒角或打孔操作作用在了错误的几何上。这类错误通常不报异常,但模型明显不对。
四是坐标系理解错。希望孔打在顶面,代码却选了底面>Z判断反了。
五是输出格式不守约。模型在代码块里写了一大段解释文字,或者偷偷加了文件保存逻辑,导致直接执行失败。
这些幻觉的本质是:模型在"猜"几何,而不是"算"几何。因此我们要做的不是指望它变聪明,而是把它的猜测空间压到最小。
4.2 把需求拆成特征树,而不是一段话
我的做法是,任何需求先结构化成"特征树"再进Prompt。比如法兰盘的需求,我先拆成:
目标: 法兰盘 主体: 外径100, 内径50, 厚度10 安装孔: 8个, 均布在直径80的圆上, 孔径6.5 注意: 单位为mm, 圆孔穿透这个特征树的作用是消歧。自然语言里"装在直径80的圆上"可能被理解成半径80,但结构化字段里写清楚"直径",模型就不容易搞混。更关键的是,我在Prompt里要求模型"先输出特征树,再按特征树生成代码"。这一步强制模型在写代码之前先梳理清楚自己要干什么,相当于让实习生先列提纲再动笔,出错率立刻下降。
4.3 约束注入与结果自检循环
Prompt的约束条款要具体到"脚本级别"。我用的模板大致是这样的:
你是CadQuery代码生成专家。请根据以下特征树生成Python代码。 要求: 1. 只输出代码,不要任何解释文字,不要markdown代码块包裹。 2. 代码使用单位毫米。 3. 最终变量必须命名为result。 4. 禁止文件读写操作。 5. 禁止使用任何未导入的模块。这些约束每一条都对应一类我踩过的坑:禁止解释文字是因为要直接执行;强制变量名叫result是为了后续统一校验;禁止文件读写是为了避免模型自作主张到处写文件。
光靠Prompt还不够,必须加自检重试循环。我的实现很简单:把生成的代码写到临时文件,用子进程执行,捕获异常,把traceback拼回Prompt,让模型修正,最多重试三轮。
import subprocess def run_generated_code(code, timeout=30): with open("gen_part.py", "w", encoding="utf-8") as f: f.write(code) proc = subprocess.run(["python", "gen_part.py"], capture_output=True, text=True, timeout=timeout) return proc.returncode, proc.stdout, proc.stderr def generate_with_retry(llm_call, prompt, max_retry=3): for i in range(max_retry): code = llm_call(prompt) ret, out, err = run_generated_code(code) if ret == 0: return code prompt += f"\n上一段代码执行失败,错误信息如下,请修正并重新输出完整代码:\n{err}" raise RuntimeError("多次重试仍失败")这套循环的关键是"报错信息要反馈得足够清楚"。CadQuery的异常信息通常已经指出了具体API调用和对象类型,模型拿到这些信息后进行修正的成功率很高。实测中,大约七成的失败代码在第二轮重试时就能跑通。
5. 几何验证、导出与批量落地:让生成结果真正能进产线
代码能跑通只是第一步。CAD模型的可怕之处在于,它能生成并不代表它能用。轻微的面破损、不封闭的壳体、错误的特征位置,都会在后续CAM或装配环节爆炸。所以我在生成后面加了一道四层校验。
5.1 四层校验手段
第一层是API级校验。用CadQuery对象直接算体积、包围盒,和理论值对比。比如法兰的期望体积是π × (50² - 25²) × 10 ≈ 58904.9 mm³,我可以设置5%的容差。
第二层是网格级校验。把实体导出成STL后用trimesh检查网格是否水密。水密意味着壳体封闭,这是3D打印和有限元分析的前提。
第三层是视觉级校验。用Jupyter里的show渲染截图,人工确认孔的分布、面的朝向是否符合预期。
第四层是工程级校验。把STEP文件导入传统CAD,检查实体能否被正常识别、特征树是否干净。这一层最接近最终使用场景,也是最能发现"数学上正确但工程上别扭"问题的地方。
一个合并的校验脚本长这样:
import math from cadquery import exporters import trimesh def check_part(part, name, expected_volume=None, tol=0.05): vol = part.val().Volume() box = part.val().BoundingBox() print(f"[{name}] 体积={vol:.2f}, 边界=" f"{box.xlen:.2f} x {box.ylen:.2f} x {box.zlen:.2f}") if expected_volume and abs(vol - expected_volume) / expected_volume > tol: raise ValueError(f"{name} 体积超出公差") exporters.export(part, f"{name}.step") exporters.export(part, f"{name}.stl", tolerance=0.1) mesh = trimesh.load(f"{name}.stl") print(f"[{name}] 网格水密: {mesh.is_watertight}") return vol我一般把这套校验封装成一个函数,每个生成件都跑一遍。产物不仅是一个STEP文件,还包括一份带体积、边界、校验结论的日志,这个日志在批量场景里非常值钱。
5.2 导出STEP、STL、DXF的正确姿势
导出本身不复杂,但有几个参数值得注意。
exporters.export(part, "part.step") exporters.export(part, "part.stl", tolerance=0.1, angularTolerance=0.5)STL导出时tolerance控制弦高误差,默认值偏大,做精细件时可以收紧到0.01,代价是文件体积变大。对于激光切割、线切割这类二维工艺,DXF更合适:
# 把实体底面投影成二维轮廓导出DXF exporters.export(part, "profile.dxf")DXF里保存的是边线,后续在专业软件里排料、套料就靠它。我专门写过一段代码,把不同型号生成的DXF按板材厚度自动归档到不同目录,排料时直接拖进切割软件。
5.3 批量生成变体:把text-to-cad变成参数化设计引擎
单件生成跑通后,批量才是这套方案真正发挥价值的地方。还是以支架为例,我现在维护一个参数表,一次循环生成所有变体:
import csv from cadquery import exporters def make_bracket(w, h, t, hole_d): return (cq.Workplane("XY") .box(w, h, t) .faces(">Z").workplane() .center(-w/4, 0).hole(hole_d) .center(w/2, 0).hole(hole_d)) variants = [ {"w": 80, "h": 40, "t": 4, "hole_d": 4}, {"w": 100, "h": 50, "t": 5, "hole_d": 5}, {"w": 120, "h": 60, "t": 6, "hole_d": 6}, ] with open("batch_report.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["name", "w", "h", "t", "volume", "status"]) for i, p in enumerate(variants): name = f"bracket_{i:02d}" try: part = make_bracket(**p) vol = part.val().Volume() exporters.export(part, f"{name}.step") exporters.export(part, f"{name}.stl", tolerance=0.1) writer.writerow([name, p["w"], p["h"], p["t"], f"{vol:.2f}", "ok"]) except Exception as e: writer.writerow([name, p["w"], p["h"], p["t"], "", f"failed: {e}"])这里我把"文字生成"和"参数驱动"结合了起来:对于标准化的变体,不再需要每次问模型,而是先把特征结构和Prompt固定下来,只改参数。而模型负责的是最初把自然语言需求变成这个"参数化模板"的过程。整个过程其实就是:一个大模型辅助你搭建参数化设计引擎,引擎一旦搭好,重复劳动归零。
6. 实测一周后的边界认知:哪些场景值得用,哪些别硬凑
跑了整整一周,我对text-to-cad的能力边界有了比较清晰的认识。有些场景它强得离谱,有些场景则是自找麻烦。
6.1 值得用的:规则零件、标准件、钣金展开
最稳的是规则机械件:法兰、支架、盒体、带孔板、齿轮近似、各种座子。这些零件特征少、几何规则、参数清晰,正是大模型最擅长的领域。钣金件也很有潜力,因为大多数钣金结构都是"平板+折弯+孔位",完全可以用代码描述,模型生成的DXF可以直接进激光切割排版。我甚至见过同行把整套钣金柜体的折弯表都参数化,一次生成全部展开图,这个方向值得深耕。
6.2 别硬凑的:自由曲面、装配约束、工程图语义
但也有几个场景我劝你别浪费时间。
一是自由曲面。A级曲面、连续曲率过渡这些讲究,文字描述根本承载不了足够信息,模型没有足够的约束来生成可用曲面。这种活该交给NURBS建模软件。
二是装配约束。生成单个零件没问题,但多个零件之间的配合关系、运动副、干涉检查,靠LLM生成代码去管理,复杂度会指数上升,至少现阶段不划算。
三是工程图语义。模型能给你一个漂亮的实体,但尺寸标注体系、公差配合、基准符号、表面粗糙度这些二维图纸语义,它还没法替你决策。这些必须回到传统CAD里,基于导入的STEP继续完成。类似"CAD转PDF""图纸布局"这类的活儿,目前依然是专业CAD软件的天下。
6.3 我目前推荐的配合方式
这套工具的正确用法不是替代CAD,而是把"从需求到基础实体"这一段最繁琐的活接过去。我现在的工作流是:需求方给一个文字描述 → 我整理成特征树 → 模型生成CadQuery代码 → 沙箱校验和导出STEP/DXF → 导入传统CAD做细化、出图、装配 → 归档。整个流程里,模型承担了"把你说的变成能加工的基础模型"这步重活,而工程师的时间和精力留在真正需要判断力的环节。
最后分享一个我个人的小习惯:把每天用过的Prompt和对应的失败案例存成文本文件,按日期归档。大模型升级速度快,昨天能跑通的Prompt今天可能就变了,有了历史记录,模型一换版本我就能立刻做回归测试,把"哪个Prompt在哪个版本下稳定"这件事变成可追踪的数据。这套带着版本意识的用法,比任何技巧都更能保证text-to-cad在长期使用中不翻车。