1. 从一段文字到一张图纸:text-to-cad 到底在解决什么问题
第一次听到 “text-to-cad” 这个词,很多人脑子里冒出来的画面大概是:对着电脑敲一句“给我画个法兰盘”,然后 SolidWorks 或者 FreeCAD 就自动把三维模型吐出来了。这个理解方向没错,但真正落到工程实践里,它要解决的问题比“省几下鼠标”要深得多。
传统 CAD 工作流的本质是人肉翻译:需求方用自然语言描述一个零件,工程师在脑子里把它翻译成尺寸、约束、特征树,再用手一根线一根线地画出来。这个过程中,信息损耗极大——需求方说的“差不多 50 毫米”,到了图纸上可能是 48 也可能是 52;需求方说的“能装进去就行”,工程师得自己脑补公差配合。text-to-cad 想干的事情,就是把这段“人肉翻译”尽可能自动化:输入一段结构化的文字描述,输出一份机器可读、可编辑、可制造的几何文件。
它涉及的核心技术点其实横跨了好几个领域。自然语言理解负责把“直径 80、厚度 10、中心开 20 的通孔”解析成参数字典;参数化建模引擎负责把这些参数变成实际的几何体;格式转换层负责把内部表示导出成 STEP、URDF、G-code 这些下游工具能吃的格式。这三个环节任何一个掉链子,最终产物都是一堆没法用的垃圾数据。
适合关注这个方向的人,我大致分成三类。第一类是机械设计工程师,手上有大量重复性建模任务,比如标准件库、系列化产品,想用脚本批量生成;第二类是机器人方向的开发者,需要频繁生成 URDF 模型做仿真,手工写 XML 写到吐;第三类是做 CAM 和 3D 打印的玩家,想把参数化设计直接接到切片和加工路径生成上。如果你属于这三类中的任何一类,下面这些内容应该能帮你少走不少弯路。
需要提前说明的是,text-to-cad 目前还不是“一句话出成品”的魔法。它更像是一个参数化模板引擎——你给它足够明确的输入,它给你稳定可复现的输出。模糊的、带歧义的自然语言,它处理起来依然很吃力。所以真正好用的方案,往往是在“自然语言”和“结构化参数”之间找一个平衡点。
2. 整体架构设计:为什么不能直接让大模型画图
2.1 直接生成几何体的三条死路
我见过不少人的第一反应是:既然大模型能写代码,那让它直接输出 STEP 文件的文本内容不就行了?这个思路我实测过,结论是基本不可行,原因有三。
第一,STEP 文件(ISO 10303-21)是一种基于实体边界表示(B-rep)的格式,一个稍微复杂点的零件,其 STEP 文本动辄几千上万行,里面全是CARTESIAN_POINT、ADVANCED_FACE、EDGE_CURVE这类底层拓扑实体。大模型在生成这种长序列结构化数据时,局部一致性极难保证——前面定义的一个点坐标,后面引用时很可能就写错了,导致整个文件无法解析。
第二,就算模型勉强生成了一个能打开的 STEP,它也是不可参数化的。你拿到的是一个死掉的几何体,想改个孔径?对不起,重新生成一遍,而且大概率生成出来的东西和上一次还不一样。工程上要的是可追溯、可修改的模型,不是一次性的雕塑。
第三,URDF 和 G-code 的生成逻辑完全不同。URDF 描述的是运动学树——连杆、关节、惯性矩阵、碰撞体;G-code 描述的是加工路径——刀具轨迹、进给速度、层高。让一个模型同时精通这三种格式的底层语法,还要保证几何一致性,这在当前技术条件下是强人所难。
2.2 分层架构:把“理解”和“生成”拆开
我最终采用的方案是三层分离架构,这也是目前比较主流、比较稳的做法。
最上层是语义解析层。这一层可以用大模型,也可以用规则引擎,甚至可以用一个简单的表单。它的唯一职责是把输入文本转成一个中间参数表示(Intermediate Parameter Representation,我习惯叫它 IPR)。IPR 是一个纯 JSON 结构,里面只有数字、字符串和布尔值,不涉及任何几何实体。比如:
{ "part_type": "flange", "outer_diameter": 80.0, "inner_diameter": 20.0, "thickness": 10.0, "bolt_hole_count": 4, "bolt_hole_diameter": 8.0, "bolt_circle_diameter": 60.0, "material": "steel" }中间层是参数化建模层。这一层用代码(Python 的 CadQuery、build123d,或者 FreeCAD 的脚本接口)读取 IPR,调用几何内核(OpenCASCADE)生成实体。这一层是确定性的——同样的 IPR 输入,永远得到同样的几何输出。这是整个系统可靠性的基石。
最下层是格式导出层。根据目标应用,把生成的实体导出成 STEP(用于 CAD 交换)、URDF(用于机器人仿真)、STL(用于 3D 打印)或 G-code(用于 CNC 加工)。这一层只做格式转换,不做几何修改。
提示:把大模型限制在“语义解析层”,是保证整个系统稳定性的关键决策。大模型可以犯错,但它的错误只会影响参数提取,不会污染几何生成。参数错了,改参数就行;几何错了,整个模型都得推倒重来。
2.3 为什么选 CadQuery 而不是直接调 OpenCASCADE
几何内核的选择上,我对比过三条路线:直接调 OpenCASCADE 的 C++ API、用 FreeCAD 的 Python 脚本接口、用 CadQuery。
直接调 OCCT 的 C++ API 性能最好,但开发效率极低,一个简单的拉伸特征要写几十行样板代码,调试成本太高。FreeCAD 的脚本接口功能全,但它的 API 设计比较老旧,文档散乱,而且 FreeCAD 本身的启动和依赖比较重,做服务化部署不太方便。
CadQuery 是我最终的选择。它基于 OCCT,但提供了一套链式调用的 Python DSL,写起来非常接近“说话”:
import cadquery as cq result = ( cq.Workplane("XY") .circle(40) .circle(10) .extrude(10) .faces(">Z") .workplane() .polarArray(30, 0, 360, 4) .hole(8) )这段代码生成的就是上面那个法兰盘。可读性极强,改参数就是改数字,非常适合和 IPR 对接。而且 CadQuery 支持导出 STEP、STL、DXF 等多种格式,生态比较完整。
3. 核心细节拆解:从文本到参数的映射规则
3.1 尺寸描述的解析策略
自然语言里描述尺寸的方式五花八门:“直径 80”、“外径 80 毫米”、“80mm 外圆”、“外圈 80”。如果全靠大模型去理解,稳定性不够。我的做法是规则优先,模型兜底。
先定义一套正则规则,覆盖最常见的表达模式:
import re PATTERNS = { "outer_diameter": [ r"外[径圆]\s*[为是]?\s*(\d+(?:\.\d+)?)\s*(?:mm|毫米)?", r"直径\s*[为是]?\s*(\d+(?:\.\d+)?)\s*(?:mm|毫米)?", r"[Φφ]\s*(\d+(?:\.\d+)?)", ], "thickness": [ r"[厚高]\s*度?\s*[为是]?\s*(\d+(?:\.\d+)?)\s*(?:mm|毫米)?", r"板厚\s*[为是]?\s*(\d+(?:\.\d+)?)", ], }规则能匹配上的,直接提取,不走模型。匹配不上的,再交给大模型做语义理解,但要求它输出固定 schema 的 JSON,而不是自由文本。这样即使模型理解有偏差,输出格式也是可控的。
注意:单位统一是踩坑重灾区。用户可能说“8 个的孔”,意思是 8 毫米;也可能说“0.8 的孔”,意思是 0.8 毫米。我的做法是在 IPR 里强制统一为毫米,解析时如果发现数值小于 1 且没有明确单位,就弹出一个确认提示,让人工介入。这个“人机确认”环节看起来笨,但能避免大量低级错误。
3.2 特征顺序与依赖关系
参数提取出来之后,还有一个容易被忽略的问题:特征的生成顺序。法兰盘是先拉伸外圆再打孔,还是先打孔再拉伸?结果看起来一样,但特征树的结构不同,后续修改的便利性也不同。
我的原则是从大到小,从外到内:先做基础实体(外轮廓拉伸),再做减材料特征(孔、槽、倒角)。这样特征树清晰,而且减材料特征可以随时抑制掉,方便调试。
对于更复杂的零件,特征之间可能存在依赖关系。比如“在凸台上打孔”,就必须先有凸台才能打孔。我在 IPR 里用一个features数组来显式表达顺序:
{ "features": [ {"type": "extrude", "profile": "circle", "diameter": 80, "height": 10}, {"type": "hole", "diameter": 20, "position": "center"}, {"type": "polar_hole", "diameter": 8, "count": 4, "pcd": 60} ] }建模层按数组顺序依次执行,逻辑非常直白。
3.3 公差与配合信息的处理
工程图纸上的公差标注(比如Φ20 H7、Φ20 +0.021/0)在 text-to-cad 里是个难点。严格来说,公差信息不影响几何体的标称形状,但影响下游的加工和装配分析。
我的处理方式是分离存储:几何体只生成标称尺寸,公差信息作为元数据附加在 IPR 里,导出 STEP 时写入TOLERANCE字段(如果目标格式支持),导出 URDF 时忽略(因为仿真通常不需要),导出 G-code 时根据公差决定加工策略(比如 H7 的孔需要精铰,普通孔钻一下就行)。
{ "hole": { "nominal_diameter": 20.0, "tolerance": {"type": "H7", "lower": 0.0, "upper": 0.021} } }这样做的好处是,几何模型保持干净,公差信息不会污染几何内核,但下游需要的时候又能拿到。
4. 实操过程:从零搭一个可用的 text-to-cad 流水线
4.1 环境准备与依赖安装
我用的技术栈是 Python 3.10 + CadQuery 2.4 + FastAPI(做服务化)+ Pydantic(做数据校验)。安装命令如下:
pip install cadquery==2.4.0 pip install fastapi uvicorn pydantic pip install ocp # OpenCASCADE 的 Python 绑定,CadQuery 会自动装CadQuery 的安装在不同平台上坑比较多。Windows 上建议直接用 conda 装,pip 装有时候会缺 OCCT 的动态库:
conda install -c conda-forge cadqueryLinux 上 pip 装一般没问题,但需要确保系统里有libgl1和libglu1-mesa,否则导入 CadQuery 时会报 OpenGL 相关的错误。
提示:如果你只是想做原型验证,不想折腾环境,可以用 CadQuery 的在线编辑器 CQ-editor,它把 OCCT 和 Python 环境都打包好了,开箱即用。但要做服务化部署,还是得在本地把环境搭起来。
4.2 语义解析层的实现
语义解析层的入口是一个 FastAPI 接口,接收文本,返回 IPR:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class TextInput(BaseModel): text: str class IPROutput(BaseModel): part_type: str params: dict @app.post("/parse", response_model=IPROutput) def parse_text(input: TextInput): # 先走规则 ipr = rule_based_parse(input.text) if ipr is None: # 规则失败,走模型 ipr = llm_based_parse(input.text) return ipr规则解析的部分,我写了一个rule_based_parse函数,用前面提到的正则模式去匹配。匹配成功且所有必填参数都齐了,就直接返回;否则返回None,触发模型兜底。
模型兜底的部分,我用的是一个本地部署的小模型(7B 级别),prompt 里明确要求输出 JSON,并且给了几个 few-shot 示例。实测下来,对于“直径 80、厚 10、中心孔 20、四个 8 个的孔分布在直径 60 的圆上”这种描述,模型能稳定提取出正确的参数。
4.3 参数化建模层的实现
建模层的核心是一个build_part函数,接收 IPR,返回 CadQuery 的 Workplane 对象:
import cadquery as cq def build_flange(params): outer_d = params["outer_diameter"] inner_d = params["inner_diameter"] thickness = params["thickness"] bolt_count = params["bolt_hole_count"] bolt_d = params["bolt_hole_diameter"] pcd = params["bolt_circle_diameter"] result = ( cq.Workplane("XY") .circle(outer_d / 2) .circle(inner_d / 2) .extrude(thickness) ) if bolt_count > 0: result = ( result.faces(">Z") .workplane() .polarArray(pcd / 2, 0, 360, bolt_count) .hole(bolt_d) ) return result这段代码的逻辑很直白:先在 XY 平面上画两个同心圆(外圆和内孔),拉伸成实体,然后在顶面上用极坐标阵列打一圈螺栓孔。polarArray的第一个参数是阵列半径(PCD 的一半),第二个是起始角度,第三个是总角度,第四个是数量。
4.4 格式导出层的实现
导出 STEP 最简单:
cq.exporters.export(result, "flange.step")导出 STL 用于 3D 打印:
cq.exporters.export(result, "flange.stl", tolerance=0.01)tolerance控制网格精度,值越小网格越密,文件越大。对于一般 3D 打印,0.01 毫米的精度足够了。
导出 URDF 稍微麻烦一点,因为 URDF 需要定义连杆和关节。对于单个零件,可以把它作为一个连杆导出:
def export_urdf(shape, name, mass, inertia): urdf_template = f"""<?xml version="1.0"?> <robot name="{name}"> <link name="{name}_link"> <visual> <geometry> <mesh filename="{name}.stl"/> </geometry> </visual> <collision> <geometry> <mesh filename="{name}.stl"/> </geometry> </collision> <inertial> <mass value="{mass}"/> <inertia ixx="{inertia[0]}" ixy="0" ixz="0" iyy="{inertia[1]}" iyz="0" izz="{inertia[2]}"/> </inertial> </link> </robot>""" with open(f"{name}.urdf", "w") as f: f.write(urdf_template)惯性矩阵的计算可以用 CadQuery 的Shape.matrixOfInertia()方法拿到,但要注意单位换算——URDF 默认用千克和米,而 CAD 里通常用毫米,需要除以 1000 的平方。
导出 G-code 是最复杂的一环,因为需要 CAM 规划。我的做法是先用 CadQuery 导出 STL,然后用 PyCAM 或者 FreeCAD 的 Path 模块生成刀具路径。这部分内容比较多,后面单独展开。
4.5 完整流水线的串联
把三层串起来,就是一个完整的 text-to-cad 服务:
@app.post("/generate") def generate(input: TextInput): ipr = parse_text(input) shape = build_part(ipr) cq.exporters.export(shape, "output.step") cq.exporters.export(shape, "output.stl") return {"status": "ok", "files": ["output.step", "output.stl"]}实测下来,从发送文本到拿到 STEP 文件,整个流程在普通笔记本上大约 2 到 5 秒,其中大部分时间花在 OCCT 的几何运算上,语义解析只占几百毫秒。
5. 常见问题与排查技巧实录
5.1 几何内核报错:BRep_API: command not done
这是 CadQuery 用户遇到频率最高的错误之一。原因通常是布尔运算失败——比如两个面完全重合、或者孔的位置超出了实体范围。
排查思路:先检查参数是否合理。比如外径 80、内径 20、螺栓孔分布在直径 60 的圆上,螺栓孔直径 8。螺栓孔的外边缘在 60/2 + 8/2 = 34,内边缘在 60/2 - 8/2 = 26,都在外径 40 和内径 10 之间,没问题。但如果 PCD 改成 70,螺栓孔外边缘就跑到 39,离外径 40 只剩 1 毫米,几何内核在计算布尔运算时可能因为容差问题失败。
解决办法:在 IPR 校验层加一个几何可行性检查,确保所有特征都在实体范围内,且特征之间留有足够的间隙(我一般要求至少 0.5 毫米)。
5.2 STEP 导出后在其他 CAD 里打不开
这个问题通常和单位有关。CadQuery 默认用毫米,导出的 STEP 也是毫米。但有些 CAD 软件(比如某些版本的 SolidWorks)默认用米,导入时会把模型缩小 1000 倍,看起来就像“打不开”。
排查方法:用文本编辑器打开 STEP 文件,搜索SI_UNIT,看看单位定义是什么。如果是MILLI和METRE,那就是毫米,没问题。如果导入后模型尺寸不对,在导入设置里手动指定单位为毫米即可。
另一个可能的原因是 STEP 版本兼容性。CadQuery 默认导出 AP214 版本,有些老 CAD 只认 AP203。可以在导出时指定:
cq.exporters.export(shape, "output.step", opt={"write_pcurves": False})5.3 URDF 导入 CoppeliaSim 后模型位置不对
URDF 里的坐标系原点和 CAD 里的坐标系原点往往不一致。CAD 里通常以零件底面中心为原点,而 URDF 的连杆坐标系默认在关节位置。导入 CoppeliaSim 后,模型可能“飘”在空中或者“陷”在地里。
解决办法:在 URDF 的<visual>和<collision>标签里加<origin>偏移:
<visual> <origin xyz="0 0 -0.005" rpy="0 0 0"/> <geometry> <mesh filename="flange.stl"/> </geometry> </visual>这里的-0.005表示把模型沿 Z 轴下移 5 毫米(假设厚度是 10 毫米,原点在顶面中心)。具体偏移量需要根据你的建模原点来定。
5.4 G-code 生成后加工出来的零件尺寸偏大
这是 CAM 环节的经典问题,原因通常是刀具半径补偿没做对。如果你用直径 6 毫米的铣刀沿着轮廓走,实际切出来的轮廓会比设计轮廓大 3 毫米(刀具半径)。正确的做法是在生成路径时做刀具半径补偿,让刀具中心沿着“设计轮廓向外偏移刀具半径”的路径走。
在 FreeCAD 的 Path 模块里,这个补偿是自动做的,但需要正确设置刀具直径。如果你自己写 G-code 生成器,记得在计算路径点时加上补偿。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| BRep_API 报错 | 布尔运算失败 | 检查特征是否超出实体范围 | 加几何可行性校验 |
| STEP 导入后尺寸不对 | 单位不一致 | 检查 STEP 里的 SI_UNIT | 导入时指定毫米 |
| URDF 模型位置偏移 | 坐标系原点不一致 | 对比 CAD 和 URDF 的原点定义 | 加 origin 偏移 |
| G-code 加工尺寸偏大 | 刀具半径补偿缺失 | 检查路径是否沿设计轮廓 | 加刀具半径补偿 |
| 模型生成速度慢 | 几何运算复杂 | 用 cProfile 分析耗时 | 简化特征树,减少布尔运算 |
提示:几何内核的报错信息通常很晦涩,比如
Standard_Failure、StdFail_NotDone。遇到这类错误,不要试图去读懂错误信息,直接检查参数和特征顺序,90% 的问题都出在这两个地方。
6. 进阶玩法:批量生成与参数扫描
6.1 用 Python 批量修改 CAD 参数
text-to-cad 真正体现效率优势的场景,是批量生成系列化零件。比如你要生成 100 个不同尺寸的法兰盘,手工建模得画到天荒地老,用脚本就是改个循环的事。
import csv with open("flange_specs.csv") as f: reader = csv.DictReader(f) for i, row in enumerate(reader): params = { "outer_diameter": float(row["outer_d"]), "inner_diameter": float(row["inner_d"]), "thickness": float(row["thickness"]), "bolt_hole_count": int(row["bolt_count"]), "bolt_hole_diameter": float(row["bolt_d"]), "bolt_circle_diameter": float(row["pcd"]), } shape = build_flange(params) cq.exporters.export(shape, f"flange_{i:03d}.step")这段代码读取一个 CSV 文件,每行是一组参数,循环生成 STEP 文件。实测下来,生成 100 个法兰盘大约需要 3 到 5 分钟,平均每个 2 到 3 秒。如果参数变化不大,OCCT 还能复用一些中间计算结果,速度会更快。
6.2 参数扫描与优化
更进一步,你可以把 text-to-cad 和参数优化结合起来。比如你要设计一个支架,要求在满足强度的情况下重量最轻。可以写一个循环,扫描不同的板厚和孔径,生成模型,导出 STL,然后用有限元分析工具(比如 CalculiX)算应力,找到最优解。
best_weight = float("inf") best_params = None for thickness in [5, 6, 8, 10, 12]: for hole_d in [6, 8, 10]: params = {"thickness": thickness, "hole_d": hole_d, ...} shape = build_bracket(params) weight = shape.Volume() * 7.85e-6 # 钢的密度,单位 kg/mm³ if weight < best_weight: # 这里应该加应力校验,省略 best_weight = weight best_params = params这个思路在轻量化设计里非常实用。传统做法是手工改参数、重新建模、重新分析,一轮下来半小时。用脚本自动化,一轮几秒钟,一晚上能跑几千组。
6.3 和 CAD 插件生态的衔接
如果你日常用的是中望 CAD、AutoCAD 或者浩辰 CAD,text-to-cad 生成的 STEP 文件可以直接导入。但如果你想让生成的模型自动带上图层、标注、标题栏这些“图纸信息”,就需要写一些插件脚本。
以 AutoCAD 为例,可以用 Python 的pyautocad库操作:
from pyautocad import Autocad acad = Autocad(create_if_not_exists=True) acad.model.InsertBlock((0, 0, 0), "flange.dwg", 1, 1, 1, 0)这段代码把生成的 DWG 文件作为块插入到当前图纸里。更复杂的操作,比如自动标注尺寸、生成 BOM 表,需要调用 AutoCAD 的 ActiveX 接口,代码量会大一些,但思路是一样的。
注意:不同 CAD 软件的二次开发接口差异很大。AutoCAD 用 ActiveX 和 .NET,中望 CAD 用 ZRX 和 LISP,FreeCAD 用 Python。如果你的工作流涉及多个 CAD 平台,建议把 text-to-cad 的输出统一成 STEP 或 IGES 这种中性格式,然后在各个平台里分别写导入脚本。
7. 我踩过的坑和几条实用建议
第一个坑是过度依赖大模型。我一开始想让模型直接从文本生成 CadQuery 代码,结果生成的代码十次有三次跑不通,跑通的里面又有两次几何不对。后来改成“模型只提取参数,代码模板固定”,稳定性立刻上来了。这个教训让我明白:大模型适合做模糊匹配,不适合做精确生成。
第二个坑是忽略单位换算。有一次生成 URDF 时忘了把毫米转成米,导入仿真环境后模型大了 1000 倍,整个场景都被撑爆了。后来我在 IPR 里强制标注单位,所有数值都带unit字段,转换时统一处理,再也没出过这个问题。
第三个坑是STEP 文件的版本兼容性。我生成的 STEP 在 FreeCAD 里打开正常,发给同事用 SolidWorks 打开就报错。后来发现是 AP214 和 AP203 的差异。现在我的导出函数默认生成 AP203,兼容性最好,需要颜色和图层信息时才用 AP214。
几条实用建议:参数校验一定要做,宁可拒绝生成也不要生成一个错误的模型;日志要详细,每次生成都记录输入参数、耗时、输出文件路径,出问题时能快速定位;版本管理要跟上,生成的模型文件建议用 Git LFS 或者专门的 PLM 系统管理,不然文件一多就乱套了。
最后分享一个小技巧:如果你经常需要生成相似但不完全相同的零件,可以做一个参数预设库。把常用的几组参数存成 JSON 文件,生成时直接调用预设,改一两个参数就行。这个做法看起来简单,但实际用起来能省掉大量重复输入的时间。