最近一直在折腾 text-to-cad 这个方向,说直白点就是“用自然语言直接生成 CAD 模型”。你输入一句话,比如“一个 M8 螺栓,总长 40 毫米,螺纹长度 25 毫米”,系统自动帮你生成对应的三维模型文件。这个需求其实很早就有人提过,早几年的效果基本属于“演示五分钟,落地不可能”,直到大语言模型普及之后,这条路才真正有了被走通的希望。这篇文章就把我踩过的坑、走过的弯路,以及现在跑通的一套完整方案整理出来,希望能给在搞类似项目的同学省点时间。适合看的人主要是做 CAD 二次开发、三维建模工具链,以及想给设计工具加自然语言入口的开发者和产品经理。
1. text-to-cad 的整体思路与方案拆解
1.1 核心需求到底在解决什么
先别急着上手写代码,把问题拆清楚比什么都重要。text-to-cad 表面上是一个生成任务,但深层拆解下来,它实际上包含三个连续的子问题:
- 自然语言理解:把用户输入的模糊描述转换成结构化的设计意图。比如“做一个能放在桌面上的小支架”这种描述,要拆出“放桌面上”“小”“支架”这几个关键语义。
- 几何约束建模:把设计意图转换成具体的尺寸、特征、配合关系。比如“M8 螺栓”意味着螺纹公称直径 8 毫米,牙距 1.25 毫米,头部对边 13 毫米。
- 可编辑模型输出:不只是生成一个三角网格看看效果,而是要输出带特征树、参数可修改的原生 CAD 模型,否则设计师拿到手也没法继续干活。
这三个子问题难点分别在语义消歧、几何推理和拓扑有效性。简单说,模型既要懂人话,又要懂机械设计规范,还要保证生成出来的模型不是个“艺术雕塑”,而是能进加工流程的实体。想清楚这一点之后,项目的整体框架就清晰了:语言理解靠大模型,几何参数靠结构化输出,模型重建靠 CAD 内核脚本。
1.2 方案选型:为什么我放弃“一步到位”的端到端生成
一开始我尝试过直接用“文本到三维模型”的端到端方案,输入文本,输出点云或体素,再用算法转成网格。这条路表面看起来最省事,实际跑下来问题一大堆。
首先是可控性差。端到端模型生成的结果没法精确控制尺寸,你说“直径 30 毫米”,它给你生成一个看起来差不多但实际量出来 29.7 毫米的圆筒,这在工业设计里完全不可接受。其次是编辑性为零。生成的是离散网格,没有特征树、没有参数约束,设计师拿到手只能重新建模。
所以我最终选了一条更“笨”但更稳的路线:利用大语言模型生成参数化建模脚本,然后交给 CAD 内核执行,最后做合规性校验和参数规整。这相当于让 AI 当“设计助理”,而不是“自动建模机器”。AI 负责把自然语言翻译成精确的建模指令,真正干活的是专业内核。
这个方案的另一个好处是可控回退。当模型生成的脚本执行失败时,可以直接看到报错信息,定位到具体是哪个步骤出了问题,而端到端的黑盒模型连“为什么错”都说不清楚。这个可调试性在工程实践中实在太重要了。
1.3 结构化输出的设计思路
既然决定走脚本生成路线,那就要解决一个核心问题:怎么让大语言模型稳定输出符合要求的建模脚本。纯自由文本生成肯定不行,模型很容易自由发挥,写出一堆语义正确但语法错误的代码。
我的做法是定义一套 DSL(领域特定语言),把常见建模操作——拉伸、旋转、扫掠、布尔运算、倒角、阵列——都封装成高度结构化的指令。这套 DSL 有两种使用方式:一种是大模型直接输出 DSL 代码,然后用解释器执行;另一种是让模型输出 JSON 格式的参数列表,再用模板渲染成 DSL。
实践下来我推荐后者,因为 JSON 格式约束性更强,模型更不容易出错,而且可以直接用 JSON Schema 做一次结构校验,把大部分低级错误挡在门外。比如用户说“一个圆柱体,直径 20 毫米,高度 50 毫米”,模型输出的不是自由文本描述,而是{"type": "cylinder", "diameter": 20, "height": 50, "units": "mm"}这样的结构,后面的事情就纯粹是参数装配了。
2. 工具选型与技术架构详解
2.1 大语言模型选型策略
text-to-cad 项目里大语言模型承担的主要工作是意图解析和参数抽取,这个任务的难点在于机械加工知识的储备和数学单位换算能力,而不是纯文本创作能力。所以在模型选型上,我更看重模型的工具调用稳定性和格式遵从能力。
目前主流选择可以分成三类:在线商业 API、可本地部署的开源大模型、以及专门微调过的代码模型。三者的差异我实践下来感受比较深:
| 选型方向 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 在线商业 API | 开箱即用,模型能力强,上下文理解好 | 有网络延迟,可能涉及数据外发,费用随调用量线性增长 | 原型验证、企业级应用、非敏感数据 |
| 本地开源大模型 | 数据不出内网,可完全离线,成本可预测 | 需要 GPU 资源,小参数模型能力有差距,部署运维有门槛 | 数据敏感场景、长期稳定运行、离线环境 |
| 微调代码模型 | 格式遵从度高,输出更固定 | 需要准备训练数据,且业务之外的泛化能力不足 | 用语固定、模板化程度高的生产环节 |
对于个人开发者和中小团队,我的建议是先用在线 API 把整个链路跑通,确认业务流程没问题了,再根据实际需求去评估是否需要迁移到本地模型。不要一上来就陷入“部署一个大模型”这个无底洞。
2.2 CAD 内核脚本接口怎么选
生成模型的脚本最终要落地到实际建模环境中执行,这里的选择空间其实不大,当前从业人员常用的路线大致有三类:
- 使用某个开源参数化建模内核提供的 Python 接口:胜在免费、可批量执行、没有交互界面干扰,非常适合做服务端自动建模。我用这个方案跑了最长的链路,稳定性是最高的。
- 使用某个商业 CAD 软件自带的脚本 API:功能最强,几乎覆盖了软件里所有建模命令,但需要完整的 GUI 环境,批量调用时偶尔会弹对话框把人逼疯。
- 使用隐式建模或程序化建模引擎:适合做有机形态、拓扑优化类的模型,也能处理常规机械件,但与主流制造环节的数据交换不如前两种方便。
对比之后我选择了第一类方案,核心原因有三点:第一,Python 接口可以直接集成到自动化服务里,不需要人工打开软件操作界面;第二,脚本完全基于特征建模,每一步都有对应的建模特征记录,生成的模型带完整特征树;第三,可以无头模式运行在服务器上,配合队列系统批量执行任务。
2.3 系统整体架构
经过几轮重构,我最终的架构分成了四个模块:
- 语义解析服务:接收自然语言输入,调用大模型抽取设计意图和参数,输出标准化的 JSON 描述。这个模块处理单位换算、同义词替换、隐式参数补全。
- 模板渲染引擎:把 JSON 描述映射为具体的建模脚本代码。针对不同类型零件预设好脚本模板,比如回转体、拉伸体、钣金件各用一种模板。
- 执行与校验模块:在隔离环境里执行建模脚本,捕获异常,校验生成的实体属性,比如体积是否为 0、是否有未闭合曲面、关键尺寸是否在合理范围内。
- 结果导出服务:把校验通过的模型导出为目标格式,同时保存一份带参数的描述文件,方便后续参数化回改。
这四个模块串起来以后,整个系统就可以作为一个独立服务对外提供接口了,业务系统只需要发送一行文本,就能拿到一个可用模型文件。
3. 从零搭建的完整实操流程
3.1 环境搭建与依赖准备
基础环境我建议直接用 Linux 服务器,Python 3.10 以上版本。因为后续所有环节都要自动化,Windows 桌面环境虽然也能跑,但服务化部署会麻烦不少。
# 创建虚拟环境 python3 -m venv t2cad_env source t2cad_env/bin/activate # 安装核心依赖:CAD 内核的 Python 绑定、API 调用库、数据处理相关库 pip install cadquery pip install openai # 或者其他大模型 API 的 SDK # 如果需要本地推理,还需要额外装推理框架相关依赖 # pip install vllmCadQuery 这个库是整个流程的核心,它封装了类似 CAD 建模的“拉伸、旋转、倒角、阵列”等操作,而且每一步操作都会记录在建模历史里,适合批量生成参数化零件。
3.2 构造文本-参数映射的训练/示例数据集
如果走微调路线,或者只是用少量示例做 few-shot 提示,都需要准备一套高质量的示例数据。我整理了约一千两百条“自然语言-参数”配对数据,按照零件类型做了分类,包括标准件、轴类、盘类、箱体、支架等大类。每一条数据都包含原始输入文本、标准化的 JSON 参数、以及对应的建模脚本。
数据质量比数据数量重要得多。一条数据如果尺寸标注与常见设计规范相违背,模型学到的就是错误的映射关系。比如“M6 螺栓”的头宽应该是 10 毫米,如果数据里写成 12 毫米,模型就会学到错误关联。实践经验是每条数据都要经过设计规范校验,宁可数据量少几百条,也不放垃圾数据进去。
数据样例:
{ "input": "做一个六角头螺栓 M6,公称长度 30 毫米,螺纹长度 18 毫米", "params": { "type": "hex_bolt", "thread_spec": "M6", "nominal_length": 30, "thread_length": 18, "units": "mm" } }3.3 模型调用与脚本生成
在实际调用大模型时,关键点在于提示词模板的稳定性和参数抽取的健壮性。我给每个零件类型都写了专门的提示词模板,模板里包含:角色设定、零件类型描述、可用的参数列表、单位换算要求、输出格式要求、以及几个示例。
以下是一个简化的调用流程示例:
import json from openai import OpenAI client = OpenAI() SYSTEM_PROMPT = """ 你是一个专业的产品结构工程师,负责将用户对零件的描述转换为结构化参数。 注意: 1. 所有尺寸统一转换为毫米,保留一位小数。 2. 螺栓类零件需要识别螺纹规格,并补全标准头型尺寸。 3. 只输出 JSON,不要包含任何解释性文字。 """ USER_TEMPLATE = """ 用户需求:{query} 请将以上需求转换为 JSON 参数。 """ def parse_query_to_params(query: str) -> dict: resp = client.chat.completions.create( model="gpt-4o-mini", # 实际按需选择模型 messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": USER_TEMPLATE.format(query=query)}, ], temperature=0.1, # 低温控制输出确定性 ) content = resp.choices[0].message.content return json.loads(content)等到解析出 JSON 参数后,下一步就是把它渲染为 CadQuery 建模脚本。这里有一个关键技巧:不要每次都让模型从零写代码,而是用固定模板拼装,这样能极大提升成功率。比如拉伸类零件就套拉伸模板,回转类零件就套旋转模板,让参数去驱动模板生成具体代码。
3.4 脚本执行与模型导出
脚本生成之后,执行阶段其实比很多人想得更复杂。CadQuery 脚本如果参数不合法,比如拉伸距离为负数、截面草图未闭合,执行时会直接报错。所以我在执行外层包了一层异常捕获和日志记录。
import cadquery as cq import traceback def execute_script(script_code: str) -> cq.Workplane: try: exec_globals = {} exec(script_code, exec_globals) result = exec_globals.get("result") if result is None: raise ValueError("脚本未定义 result 变量") return result except Exception: traceback.print_exc() raise RuntimeError("建模脚本执行失败")执行成功之后导出的格式也需要仔细考虑。刚起步时我用 STL 格式,但这个格式只有三角网格,丢失了全部特征信息,后来改成直接导出 STEP 格式,因为 STEP 是制造业普遍接受的中间格式,几乎所有的 CAD 软件都能打开,而且保留实体边界表示。CadQuery 的导出写法比较简单:
cq.exporters.export(result, "output.step")另外,生成过程还要生成一张预览图,帮助用户确认模型是否符合预期。CadQuery 内置了基于某个开源渲染框架的预览功能,可以直接输出 PNG 截图。
from cadquery import exporters exporters.export(result, "preview.svg")3.5 质量评估体系
项目做到一定阶段,光靠人眼抽查肯定不行,需要一套自动化评估体系。我设计的评估维度主要有四个:
- 脚本成功率:一批测试样本中有多少能顺利执行完成。这个指标最基础,只要脚本报错就算不过关。
- 参数准确率:生成参数与标注参数之间的相对偏差。尺寸偏差超过 2% 的就判定为失败。
- 几何拓扑有效:实体体积大于 0、曲面无自交、边界闭合。
- 人工审美评分:把渲染图发给实际设计师打分。这个指标主观但很关键,很多模型参数都对但造型比例就是很奇怪。
四个维度打分以后,加权得到一个总分,用这个分数去横向对比不同提示词模板和模型版本的差异。有了这套评估体系,后续迭代才有依据,不然一切都是凭感觉。
4. 常见问题与排查技巧实录
4.1 生成结果总是“看着像但建模失败”
这是整个项目里最废头发的问题。典型现象是:大模型输出的参数列表看起来很合理,比如尺寸、角度、数量都在正常范围,但脚本一执行就死。排查下来发现主要原因集中在以下三类:
第一类是草图未闭合。CAD 脚本里画截面轮廓时,如果最后一条边没有回到起点,内核会拒绝生成实体。大模型生成坐标时很容易出现微小的首尾不闭合误差。解决办法是在模板层强制处理,对轮廓点序列进行闭合检查,首尾坐标距离小于 0.01 毫米就自动连上。
第二类是布尔运算相交退化。做减料操作时,如果切除体和目标实体刚好处于临界相切状态,内核会报错。处理办法是先对切除体做微小扩量处理,比如扩大 0.001 毫米,再进行布尔运算,结果一样但稳定性大幅提升。
第三类是命名冲突和对象引用问题。长篇脚本里如果多次调用同一个变量名,后一次赋值可能会覆盖前一次的引用,导致几何对象错乱。解决办法是模板生成代码时对每个对象名加一个唯一后缀,比如box_1、box_2。
4.2 尺寸漂移与单位换算的坑
自然语言里用户习惯说“一寸”“一公分”“两米”这类单位,大模型在换算成毫米时偶尔会算错。我自己踩过最离谱的一次,用户输入“长 1.5 米”,模型输出 150 毫米,整整差了十倍。
这个问题的根因是模型对单位换算本身不敏感,单纯在提示词里强调往往效果有限。我的解决方法是:不在模型环节做单位换算,而是让模型原样输出数值和单位,然后在后处理环节用程序做转换。这样单位换算被限定在一个可靠、可测试的代码模块里,而不是依赖模型发挥。
经验教训是:凡是能用代码解决的事,就不要指望模型“自觉”。模型的价值在于理解语义,精确计算全部交给程序。
4.3 运行速度与成本控制
走大模型 API 方案的初期,最大的感受是“每一步都在烧钱”,一次文本解析调用、一次脚本修复调用,叠加起来单次生成成本就上去了。控制成本有几个实用技巧:
- 在提示词里明确要求“直接输出结果,不要解释过程”,避免模型输出大量冗余文本。
- 先用小模型做粗识别,当置信度低时再升级到大模型做二次确认。比如用开源小模型抽取参数后简单校验,不合格再调用大模型重新解析。
- 对相似文本请求做缓存处理,比如“M6 螺栓 30 长”“M6 内六角螺栓 30 长”这类高度相似的描述,可以直接命中缓存。
速度方面,CadQuery 脚本本身执行其实很快,零点几秒就能完成一个中等复杂度零件的建模,瓶颈几乎全在网络请求上。如果部署内网本地模型,延迟会显著下降。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 解决措施 |
|---|---|---|
| 脚本报错但参数表看着正常 | 草图未闭合 / 轮廓自交 | 在模板层强制闭合检查,对坐标序列做公差处理 |
| 生成的模型尺寸明显不对 | 单位换算错误 | 由模型输出原单位,后处理模块统一转毫米 |
| 螺栓生成出来螺距不对 | 模型没有补全标准参数 | 建立标准参数表,根据螺纹规格自动补全 |
| 同一句话多次调用结果不一致 | 模型采样温度设置偏高 | 把 temperature 调到 0.1 以下 |
| 导出 STEP 文件打开后丢特征 | 直出 STEP 未带历史特征 | 搭配导出原生参数化格式再转 STEP |
| 并发请求一多就开始超时 | 未做服务限流与排队 | 在服务端加消息队列,控制并发数 |
5. 一些个人实操心得和后续想做的事
text-to-cad 这个项目做到现在,最大的体会是:技术本身并不神秘,难的是把每个环节打磨到能稳定可靠地工作。大模型完成了意图理解这一步,但离“设计师能用”还有很长一段路要走。
我个人在实际折腾中的感觉是,与其追求“一键生成完美模型”,不如先把“生成七八十分能用的模型,再让用户微调参数”这件事做到极致。设计师不排斥 AI 给出的粗糙结果,但很排斥 AI 给一个无法修改的固定形状。保留参数化能力,就是把“AI 取代设计师”的对抗感,变成“AI 辅助设计师”的协作感,用户的接受度完全不一样。
后续我计划做几个方向的扩展。第一个是引入多轮对话式建模,用户说“改短 5 毫米”时,系统能基于上一轮的上下文做参数增量修改,而不是重新描述一遍需求。第二个是在脚本模板里加入制造约束,比如预留拔模角、避免过于薄的壁厚,让生成结果更贴近量产工艺。第三个方向是构建一个积累下来的“零件-文本-参数”共享库,把日常产生的高质量样本沉淀下来,反向优化数据引擎和提示词。
text-to-cad 现在确实还不算成熟,但我觉得正处于“能跑通”到“好用”的临界点上。谁先把细节磨到位,把误差和稳定性控制住,谁就能真正打开这个方向的应用空间。希望这篇分享能让后来者少走一点我走过的弯路。