1. 从一句话到三维模型:text-to-cad 到底在解决什么问题
第一次听到 text-to-cad 这个词,我脑子里蹦出来的画面是:对着电脑敲一行字,比如“一个 80×60×20mm 的法兰盘,中心开直径 30mm 通孔,四角各一个 M6 沉头孔”,然后软件直接吐出一个可以编辑、可以导出、可以拿去加工的实体模型。这个画面放在五年前还属于科幻范畴,但放到今天,它已经是一条能跑通的技术链路了。
text-to-cad 的核心价值,说白了就是把自然语言描述转换成 CAD 可识别的几何数据。传统流程里,你得打开 SolidWorks、Fusion 360 或者中望 CAD,手动拉伸、打孔、倒角,一个中等复杂度的零件少说半小时。而 text-to-cad 想做的事情,是让这段描述直接变成 STEP、GLB 或 STL 文件——STEP 给加工和工程软件用,GLB 给渲染和 Web 展示用,STL 给 3D 打印和切片软件用。这三个格式基本覆盖了从设计到制造到展示的全链路。
我之所以对这个方向感兴趣,是因为它踩中了几个真实的痛点。第一,非专业设计人员想快速验证结构想法,比如做机器人底盘的创客、做夹具的产线工程师,他们不需要精通参数化建模,只需要一个能用的模型。第二,批量化的标准件生成,比如一批不同孔径的法兰、不同长度的支架,用脚本生成比手动建模快十倍。第三,AI 辅助设计的入口,大语言模型能理解文字,但理解不了几何,text-to-cad 就是那座桥。
这篇文章适合谁看?如果你是会写 Python、懂一点三维几何概念、想自己搭一套文字转模型流水线的开发者,那这篇就是给你写的。如果你只是好奇这个方向怎么落地、有哪些坑,也能从里面拿到足够的信息。我会把整体架构、格式选型、核心代码、参数计算、踩坑经验全部摊开讲,尽量做到你照着做就能跑出一个能用的版本。
需要先说明一点:text-to-cad 目前没有哪个开源方案能做到“任意文字描述直接生成任意复杂模型”,它更适合结构化描述生成参数化零件这个场景。指望它一句话生成一个完整的汽车外壳,现阶段不现实。但生成法兰、支架、齿轮、外壳盒子这类规则几何体,完全可行,而且效果相当稳定。
2. 整体架构设计:为什么我选择“LLM 解析 + 参数化建模”这条路线
2.1 三种技术路线的取舍分析
在动手之前,我调研过三条主流路线,这里把它们的优劣摆出来,方便你判断自己该走哪条。
| 路线 | 核心思路 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 端到端生成 | 用扩散模型或 Transformer 直接生成网格顶点 | 理论上能生成任意形状 | 训练成本极高,可控性差,尺寸不准 | 学术研究、艺术造型 |
| 代码生成 | LLM 生成 CadQuery/OpenSCAD 代码再执行 | 灵活,可表达复杂逻辑 | 代码容易出错,需要沙箱执行 | 有一定编程能力的用户 |
| 参数化解析 | LLM 抽取结构化参数,喂给建模库 | 稳定、可控、尺寸精确 | 只能生成预定义类型的零件 | 工程零件、标准件批量生成 |
我最终选了第三条路,理由很直接:工程场景对尺寸精度的要求是刚性的。一个法兰盘的中心孔如果是 30mm,那它就必须是 30mm,不能是 29.7mm 也不能是 30.3mm。端到端生成和代码生成在这点上都不够可靠,而参数化解析把“理解文字”和“生成几何”两件事解耦了——LLM 只负责把文字变成 JSON,建模库只负责把 JSON 变成几何。每一段都可测试、可调试、可替换。
这个思路还有一个隐藏好处:LLM 的输出是可校验的。JSON 里的字段类型、数值范围、必填项都能用 schema 卡住,一旦 LLM 抽错了参数,你在解析阶段就能发现,而不是等到模型生成出来才发现孔打歪了。
2.2 完整流水线的四个阶段
整条流水线我拆成四段,每段职责清晰:
- 文本理解阶段:用户输入自然语言,LLM 抽取零件类型和参数字典。比如“直径 100mm、厚 10mm、中心孔 30mm 的法兰”,抽成
{"type": "flange", "outer_dia": 100, "thickness": 10, "bore_dia": 30}。 - 参数校验阶段:检查数值是否合理、单位是否统一、必填项是否齐全。这一步是防止 LLM 幻觉的关键闸门。
- 几何生成阶段:用 CadQuery 或 build123d 这类参数化建模库,根据参数字典构造实体。
- 格式导出阶段:把实体导出成 STEP、GLB、STL,按需分发。
为什么用 CadQuery 而不是直接操作 OpenCASCADE 的 Python 绑定?因为 CadQuery 的 API 是链式的,写起来接近自然语言,比如cq.Workplane("XY").circle(50).extrude(10)就是“在 XY 平面画半径 50 的圆,拉伸 10mm”。这种表达方式对后续维护和扩展极其友好,而且它底层就是 OpenCASCADE,导出的 STEP 是真正的 B-Rep 实体,不是网格近似。
2.3 为什么格式导出要分三路走
很多人会问:生成一个 STL 不就行了吗,为什么要同时支持 STEP 和 GLB?这里涉及三种格式的本质差异,我用一个表格说清楚。
| 格式 | 数据本质 | 精度 | 典型用途 | 导出要点 |
|---|---|---|---|---|
| STEP | B-Rep 边界表示 | 精确数学曲面 | 工程软件、CNC 加工 | 保留实体拓扑,可再编辑 |
| STL | 三角网格 | 近似,有弦高误差 | 3D 打印、切片 | 需设置合适的线性偏差 |
| GLB | 三角网格 + 材质 | 近似,可带颜色 | Web 展示、渲染 | 需三角化并打包材质 |
关键点在于:STEP 是精确的,STL 和 GLB 是近似的。如果你把模型发给加工厂,必须给 STEP,因为 CAM 软件需要精确的曲面信息来生成刀路。如果你只是想在网页上转一转看看效果,GLB 最合适,体积小、加载快、还能带材质。STL 则是 3D 打印的通用语言,切片软件只认它。
所以我的导出模块设计成三个独立函数,共享同一个实体对象,各自处理各自的三角化参数。这样既保证精度,又保证灵活性。
3. 核心细节解析:LLM 提示词设计与参数 Schema 定义
3.1 提示词怎么写才能让 LLM 稳定输出 JSON
这是整个项目里最容易被低估的环节。我一开始觉得“让 GPT 输出 JSON”很简单,结果实测下来,不加约束的话,LLM 会给你返回带解释文字的 JSON、字段名大小写不一致、数值带单位字符串、甚至把毫米和厘米混着用。
我的解决方案是三段式提示词:角色设定 + 输出格式约束 + 少量示例。
角色设定部分告诉 LLM 它是一个 CAD 参数抽取器,只做抽取不做解释。输出格式约束部分用 JSON Schema 的简化版描述字段,明确每个字段的类型和单位。示例部分给两到三个典型输入输出对,覆盖法兰、支架、盒子三种零件。
实测下来,加上示例之后,字段名错误率从 30% 降到 5% 以下。这个投入产出比非常高,值得花时间打磨示例。
3.2 参数 Schema 的设计原则
Schema 设计我遵循三条原则:
- 单位统一为毫米:所有长度字段一律用 mm,角度用度。LLM 在抽取时如果遇到“5cm”,必须转换成 50。这个转换在提示词里明确要求。
- 字段名用蛇形命名:
outer_dia、bore_dia、thickness,避免驼峰和空格,方便后续代码直接映射。 - 必填项和可选项分开:零件类型、主要尺寸是必填,倒角半径、圆角半径是可选,缺省时用默认值。
下面是我实际用的一个简化 Schema 示例,用 Python 的 dataclass 表达:
from dataclasses import dataclass, field from typing import Optional @dataclass class FlangeParams: outer_dia: float # 外径 mm thickness: float # 厚度 mm bore_dia: float # 中心孔直径 mm bolt_count: int = 4 # 螺栓孔数量 bolt_circle_dia: Optional[float] = None # 螺栓孔分布圆直径 bolt_hole_dia: Optional[float] = None # 螺栓孔直径 fillet_radius: float = 0.0 # 边缘圆角这个 dataclass 既是校验依据,也是建模函数的入参。LLM 输出的 JSON 直接**kwargs展开就能构造,非常顺滑。
3.3 参数校验的边界条件
校验阶段我设了几条硬规则,任何一条不满足就拒绝生成并返回错误信息:
- 所有尺寸必须为正数,且不超过 1000mm(防止 LLM 把单位搞错,比如把 100mm 写成 100000)。
- 中心孔直径必须小于外径,否则几何上无法构造。
- 螺栓孔分布圆直径必须在外径和中心孔之间,否则孔会打到外面或里面。
- 厚度不能小于 0.5mm,否则实体太薄,导出 STL 时容易破面。
这些规则看起来简单,但每一条都对应我实际踩过的坑。比如有一次 LLM 把“直径 100”理解成了“半径 100”,生成出来的法兰外径 200mm,直接超出打印平台。加上上限校验之后,这类错误在解析阶段就被拦住了。
4. 实操过程:从零搭一套可运行的 text-to-cad 流水线
4.1 环境准备与依赖安装
我用的环境是 Python 3.10 + CadQuery 2.4。CadQuery 的安装是第一个坑,因为它依赖 OpenCASCADE,在 Windows 上直接 pip install 经常失败。我的建议是用 conda 装,命令如下:
conda create -n text2cad python=3.10 conda activate text2cad conda install -c conda-forge cadquery=2.4 pip install openai trimesh为什么用 conda 而不是 pip?因为 conda-forge 渠道的 CadQuery 包已经把 OpenCASCADE 的二进制依赖打包好了,pip 版本需要你自己编译或者找预编译 wheel,在 Windows 上极其折腾。我试过 pip 装,卡在 OCP 编译上两个小时没动静,换 conda 五分钟搞定。
trimesh 是用来做 GLB 导出的,CadQuery 原生只支持 STEP 和 STL,GLB 需要先转成 trimesh 的 mesh 对象再导出。openai 库是用来调 LLM 的,如果你用其他模型,换成对应的 SDK 即可。
4.2 文本解析模块的实现
解析模块的核心是一个函数,输入自然语言,输出参数字典。我把它拆成两步:调 LLM 拿原始 JSON,再用 dataclass 校验。
import json from openai import OpenAI client = OpenAI(api_key="your-key") SYSTEM_PROMPT = """你是一个CAD参数抽取器。用户会用自然语言描述一个零件, 你需要抽取零件类型和尺寸参数,输出纯JSON,不要任何解释文字。 所有长度单位统一为毫米,角度单位为度。 如果用户给的单位是厘米,乘以10;如果是米,乘以1000。 """ def parse_text(user_input: str) -> dict: resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_input} ], response_format={"type": "json_object"} ) raw = resp.choices[0].message.content return json.loads(raw)这里有个细节:response_format={"type": "json_object"}这个参数非常关键,它强制 LLM 输出合法 JSON,省去了自己写正则提取的麻烦。我用过不带这个参数的版本,LLM 会在 JSON 前后加“好的,以下是抽取结果:”这类废话,解析起来很烦。
4.3 几何生成模块:以法兰盘为例
法兰盘是我用得最多的测试零件,因为它的几何特征典型:一个圆柱主体、一个中心通孔、一圈螺栓孔、可选倒角。用 CadQuery 实现大概二十行代码。
import cadquery as cq def build_flange(p: FlangeParams) -> cq.Workplane: # 主体圆柱 body = cq.Workplane("XY").circle(p.outer_dia / 2).extrude(p.thickness) # 中心通孔 body = body.faces(">Z").workplane().hole(p.bore_dia) # 螺栓孔 if p.bolt_count > 0 and p.bolt_circle_dia and p.bolt_hole_dia: body = (body.faces(">Z").workplane() .polarArray(p.bolt_circle_dia / 2, 0, 360, p.bolt_count) .hole(p.bolt_hole_dia)) # 可选倒角 if p.fillet_radius > 0: body = body.edges("|Z").fillet(p.fillet_radius) return body这段代码里有几个值得说的点。faces(">Z")是选择 Z 方向最高的那个面作为工作平面,这样打孔方向就是从上往下。polarArray是极坐标阵列,参数依次是半径、起始角、总角度、数量,用来均匀分布螺栓孔。edges("|Z")是选择所有平行于 Z 轴的边,对这些边做圆角,这样法兰的外圆柱面上下边缘会变圆润。
参数计算方面,螺栓孔分布圆直径如果用户没给,我会按经验公式取(outer_dia + bore_dia) / 2,也就是外径和孔径的中间值。这个位置既不会太靠外导致孔壁太薄,也不会太靠内导致和中心孔干涉。实测下来这个默认值在大多数场景下都合理。
4.4 三格式导出与参数设置
导出模块我写了三个函数,分别对应 STEP、STL、GLB。
def export_step(model, path): cq.exporters.export(model, path, exportType="STEP") def export_stl(model, path, tolerance=0.01, angular_tolerance=0.1): cq.exporters.export(model, path, exportType="STL", tolerance=tolerance, angularTolerance=angular_tolerance) def export_glb(model, path): import trimesh vertices, faces = model.val().tessellate(0.01) mesh = trimesh.Trimesh(vertices=vertices, faces=faces) mesh.export(path, file_type="glb")STL 导出的tolerance参数是线性偏差,单位是毫米,0.01 意味着曲面被三角化时,实际曲面和三角面之间的最大距离不超过 0.01mm。这个值越小,网格越密,文件越大。对于 3D 打印,0.01 到 0.05 都够用;对于精细展示,可以调到 0.005。angular_tolerance是角度偏差,控制圆弧被分成多少段,0.1 弧度大约是 5.7 度一段,视觉上已经足够圆滑。
GLB 导出走的是 trimesh 的路径,因为 CadQuery 没有原生 GLB 支持。tessellate方法把 B-Rep 实体转成三角网格,然后交给 trimesh 打包成 GLB。这里要注意,GLB 的坐标系和 CAD 的坐标系可能不一致,trimesh 默认 Y 轴向上,而 CAD 通常 Z 轴向上,导出后可能需要在查看器里旋转一下。
4.5 完整调用示例
把上面几块拼起来,一个完整的调用长这样:
user_input = "生成一个外径120mm、厚15mm的法兰,中心孔直径40mm,6个直径8mm的螺栓孔均匀分布,螺栓孔分布圆直径90mm" params = parse_text(user_input) flange = FlangeParams(**params) model = build_flange(flange) export_step(model, "flange.step") export_stl(model, "flange.stl") export_glb(model, "flange.glb")跑通之后,你会得到三个文件。STEP 拖进 FreeCAD 或者中望 CAD 能看到精确实体,STL 拖进切片软件能直接打印,GLB 拖进浏览器能实时旋转查看。整条链路从文字到模型,熟练之后不到十秒。
5. 常见问题与排查技巧实录
5.1 LLM 抽取参数错误的典型模式
这是最高频的问题,我整理了四种典型错误和对应的解法。
| 错误模式 | 具体表现 | 根因 | 解法 |
|---|---|---|---|
| 单位混淆 | 把 5cm 当成 5mm | 提示词未强调单位转换 | 在 system prompt 里明确要求转换 |
| 半径直径混淆 | 直径 100 抽成半径 100 | 中文“直径”和“半径”语义接近 | 字段名用 dia 而非 radius,提示词强调 |
| 数值幻觉 | 用户没提螺栓孔,LLM 自己编了 4 个 | LLM 倾向于补全常见模式 | Schema 里可选项默认 None,不自动填充 |
| 字段名漂移 | 输出 outerDiameter 而非 outer_dia | 提示词未固定字段名 | 在提示词里列出所有合法字段名 |
其中数值幻觉最隐蔽,因为生成的模型看起来“很正常”,但多了用户没要求的特征。我的做法是在校验阶段对比用户原文和抽取结果,如果某个可选字段被填充了但原文里找不到对应关键词,就标记为可疑并询问用户确认。
5.2 STL 导出破面与修复
STL 破面是 3D 打印玩家的老朋友了。text-to-cad 生成的模型破面,通常有三个原因:壁厚太薄、三角化偏差太大、实体本身有自相交。
壁厚问题最好解决,在校验阶段加一条规则:任何特征的最小尺寸不小于 0.8mm。三角化偏差问题通过调小 tolerance 解决,但要注意文件体积会膨胀。自相交问题最麻烦,通常是布尔运算的数值误差导致的,解法是在 CadQuery 里对实体做一次clean()操作,它会合并重合的面和边,消除微小间隙。
如果 STL 已经导出且发现破面,可以用 trimesh 的修复功能补救:
import trimesh mesh = trimesh.load("broken.stl") mesh.fill_holes() mesh.fix_normals() mesh.export("fixed.stl")fill_holes补洞,fix_normals修正法线方向。这两个操作能解决 80% 的常见破面问题。剩下 20% 需要手动在 MeshLab 里修,那就超出自动化范畴了。
5.3 不同 CAD 软件的 STEP 兼容性
STEP 虽然是国际标准,但不同软件的实现有差异。我实测下来,CadQuery 导出的 STEP 在 FreeCAD、中望 CAD、SolidWorks 里都能正常打开,但在某些老版本软件里可能出现曲面丢失。
如果你遇到 STEP 导入后曲面变平面,大概率是导出时的精度设置问题。CadQuery 的 STEP 导出默认精度是 1e-6,一般够用。如果目标软件对精度敏感,可以在导出时显式指定:
cq.exporters.export(model, "part.step", exportType="STEP", tolerance=1e-7, angularTolerance=1e-6)另外提醒一句:STEP 文件里不包含材质和颜色信息,如果你需要带颜色的模型,得用 GLB 或者专门的渲染格式。这是格式本身的限制,不是导出代码的问题。
5.4 性能优化:批量生成时的瓶颈
单次生成一个零件,耗时主要在 LLM 调用上,大概 2 到 5 秒。但如果你要批量生成几百个零件,瓶颈就转移到几何生成和导出上了。
我的优化经验是:LLM 调用可以并发,几何生成必须串行。因为 CadQuery 底层的 OpenCASCADE 不是线程安全的,多线程同时建模会崩溃。所以架构上我用一个线程池处理 LLM 调用,把解析结果放进队列,另一个单线程消费者从队列取参数、建模、导出。这样 LLM 的等待时间被重叠掉了,整体吞吐量提升三到四倍。
导出环节,STL 的三角化最耗时。如果批量生成的是同一类零件只是尺寸不同,可以考虑缓存三角化模板,只对变化的部分重新计算。不过这个优化比较复杂,除非你的批量规模上千,否则不值得做。
6. 扩展方向:从单零件到装配体与参数化模板库
6.1 多零件装配的描述方式
单零件跑通之后,自然会想生成装配体。比如“一个盒子,底盖厚 3mm,侧壁厚 2mm,内部空腔 80×60×40mm,盖子可分离”。这涉及多个实体的定位和配合。
我的做法是在 Schema 里引入assembly类型,包含一个parts数组,每个零件有自己的参数和变换矩阵。LLM 负责把描述拆成多个零件,几何生成模块逐个建模,最后用 CadQuery 的Assembly类组装。
assy = cq.Assembly() assy.add(base, name="base", loc=cq.Location((0, 0, 0))) assy.add(lid, name="lid", loc=cq.Location((0, 0, 43))) assy.save("box.step")Location控制零件的位置,(0, 0, 43)表示盖子放在底盖上方 43mm 处,正好是底盖高度加壁厚。装配体的 STEP 导出后,在 CAD 软件里会显示为多个独立实体,可以单独选中和移动。
6.2 参数化模板库的积累思路
text-to-cad 的长期价值不在于单次生成,而在于模板库的积累。每跑通一种零件类型,就把它固化成模板,后续同类需求直接调用,不再依赖 LLM 抽取。
我的模板库目前覆盖了法兰、支架、齿轮、盒子、轴套五类。每类模板有独立的参数 dataclass 和建模函数,注册到一个字典里,根据 LLM 输出的type字段分发。新增模板只需要写一个 dataclass 和一个 build 函数,注册进去即可,扩展成本很低。
模板库还有一个好处:可以脱离 LLM 独立使用。如果你已经知道参数,直接构造 dataclass 调用 build 函数就行,不需要联网调模型。这在批量生成标准件时特别有用,速度快且完全可控。
6.3 与现有 CAD 工作流的衔接
生成的 STEP 文件怎么融入现有工作流?我的经验是把它当作“初稿”而不是“终稿”。text-to-cad 负责快速生成 80% 的几何,剩下的 20% 细节——比如特殊倒角、螺纹特征、工程标注——在 CAD 软件里手动补。
具体操作上,我会把生成的 STEP 导入中望 CAD 或 SolidWorks,然后在此基础上添加工程图、标注尺寸、设置公差。这样比从零建模快得多,而且参数化模板保证了基础几何的准确性。
对于 3D 打印场景,STL 直接进切片软件,调整打印参数即可。我通常会把 text-to-cad 生成的 STL 和手动修复的版本对比,如果尺寸偏差在 0.1mm 以内,就直接用生成的版本,省去手动建模的时间。
6.4 精度与效率的平衡点
最后聊一个实操中反复权衡的问题:精度和效率怎么平衡。我的经验值是,对于大多数工程零件,STL 的线性偏差设 0.02mm 是个甜点。再小,文件体积翻倍但视觉和打印质量提升不明显;再大,圆弧面会出现肉眼可见的多边形棱角。
STEP 导出没有这个权衡,因为它本身就是精确的,文件大小主要取决于几何复杂度,和精度设置关系不大。所以我的策略是:STEP 永远导出最高精度,STL 按用途调偏差,GLB 用中等偏差加材质。这样既保证工程可用性,又控制文件体积。
我在实际使用中发现,text-to-cad 最大的价值不是替代 CAD 软件,而是把“想法到初稿”的时间从半小时压缩到十秒。这个压缩带来的迭代速度提升,才是它真正改变工作方式的地方。你可以在一分钟内试十个不同的尺寸组合,快速找到最优解,然后再用传统工具精修。这种“快速试错 + 精细打磨”的组合,比单纯依赖任何一方都高效。