text-to-cad 实战:从自然语言到三维模型的工程化方案

发布时间:2026/10/8 13:19:40
text-to-cad 实战:从自然语言到三维模型的工程化方案 1. 从一句话到三维模型text-to-cad 到底在解决什么问题把一句自然语言描述直接变成可用的 CAD 模型这件事在几年前还停留在论文里的概念验证阶段现在已经有不少能跑通的工程方案了。text-to-cad 这个方向的核心目标很直接你说“一个 80 毫米见方、壁厚 3 毫米、四角带 M4 沉头孔的法兰底座”系统输出一个能直接进 CAM 或者 3D 打印的模型文件格式可能是 STEP、STL 或者 GLB。它要解决的是传统建模流程里最耗时的那个环节——从需求描述到第一版几何体之间的反复沟通和手动建模。适合关注这个方向的人其实比想象中多。做非标自动化的工程师经常要快速出结构件草图给客户确认做 3D 打印服务的技术支持每天要处理大量“帮我建个这样的东西”的需求还有做参数化设计工具的产品经理需要理解文本到几何这条链路里哪些环节是真正卡脖子的。哪怕你只是经常用 CAD 画零件了解一下 text-to-cad 的现状也能帮你判断哪些重复劳动可以交给工具去做。我自己在这个方向上折腾了大半年从最开始用大语言模型直接生成 OpenSCAD 代码到后来接 FreeCAD 的 Python API再到现在比较稳定的“语义解析 参数化模板 几何内核”三段式方案踩过的坑基本覆盖了这条链路的主要难点。下面就把整套思路和实操细节拆开讲尽量让不同基础的人都能找到能直接用的部分。2. 整体方案设计为什么不能一步到位2.1 直接让大模型输出几何体的三个致命问题最开始我试过最直觉的方案把自然语言描述直接扔给大语言模型让它输出一段 OpenSCAD 或者 CadQuery 代码然后执行代码拿到模型。这个方案在 demo 阶段看起来很美好但实际用起来有三个绕不过去的问题。第一个问题是尺寸幻觉。你告诉模型“一个 100 毫米长的支架”它生成的代码里可能写 100也可能写 120甚至会在不同位置用不同的数值。对于 CAD 模型来说尺寸错了整个零件就废了没有“差不多”这个选项。第二个问题是拓扑不稳定。同样的描述跑两次一次生成的是实体一次可能生成的是壳体布尔运算的顺序稍有变化结果就完全不同。第三个问题是无法增量修改。用户说“把孔改成 M5”模型需要理解这是在原有几何体上做局部修改而不是重新生成一个全新的零件。这三个问题的根源是一样的大语言模型擅长处理语义和逻辑但它对连续几何空间的理解是间接的、离散的。让它直接输出几何体相当于让一个翻译家去画工程图语言能力再强也补不上空间直觉的缺失。2.2 三段式架构语义层、参数层、几何层我最终稳定下来的方案是把整条链路拆成三层每层只做自己擅长的事。语义层负责理解用户到底想要什么。输入是“一个带四个安装孔的法兰盘外径 120内径 60厚度 10孔均布在直径 90 的圆上”输出是一个结构化的意图描述零件类型是法兰盘关键尺寸有外径、内径、厚度、孔位圆直径孔的数量是 4孔的类型是通孔。这一层用大语言模型做完全没问题因为它的输出是结构化的 JSON不涉及连续几何。参数层负责把意图映射到具体的参数化模板。每个零件类型对应一个模板模板里定义了哪些参数是必须的、哪些有默认值、参数之间的约束关系是什么。比如法兰盘模板里孔位圆直径必须小于外径且大于内径厚度必须大于零。这一层可以用规则引擎或者简单的约束求解器实现不需要大模型参与。几何层负责根据参数生成实际的几何体。这一层用成熟的几何内核比如 OpenCASCADE 或者 CGAL通过 CadQuery、FreeCAD 或者直接调用 OCCT 的 API 来建模。几何内核经过几十年验证布尔运算、倒角、抽壳这些操作的可靠性远超大模型生成的代码。这个架构的好处是每一层都可以独立测试和替换。语义层换个模型不影响几何层几何层换个内核也不影响语义层。而且因为参数层是确定性的同样的输入永远得到同样的输出解决了拓扑不稳定的问题。2.3 输出格式的选择STEP、STL、GLB 各管一段text-to-cad 的输出格式不是随便选的每种格式对应不同的使用场景。STEP是精确的边界表示格式保留了完整的几何拓扑信息适合后续在 CAD 软件里继续编辑、出工程图、做 CAM 加工。如果你生成的模型要进生产线STEP 是唯一的选择。但 STEP 文件体积大解析慢不适合做实时预览。STL是三角网格格式只保留表面几何没有拓扑信息适合 3D 打印和快速预览。STL 的优点是几乎所有切片软件和查看器都支持缺点是精度受网格密度影响而且无法直接编辑。GLB是 glTF 的二进制版本专为实时渲染设计适合在网页或者移动端做可视化展示。GLB 支持材质和动画但不适合做工程用途。我的做法是同时输出三种格式STEP 用于后续加工STL 用于快速验证GLB 用于前端展示。生成一次几何体导出三次成本很低但覆盖了所有场景。3. 核心细节解析语义解析与参数化模板的配合3.1 语义解析的提示词设计要点语义层的核心是让大语言模型稳定地输出结构化的零件描述。我试过很多种提示词写法最后稳定下来的模板大概是这样的你是一个 CAD 语义解析器。用户会用自然语言描述一个零件。 你需要输出一个 JSON 对象包含以下字段 - part_type: 零件类型从 [flange, bracket, plate, shaft, gear] 中选择 - dimensions: 一个对象包含该零件类型的所有关键尺寸 - features: 一个数组每个元素描述一个特征孔、槽、倒角等 - constraints: 一个数组描述尺寸之间的约束关系 如果用户描述中缺少某个必要尺寸用 null 表示不要猜测。 如果用户描述中有矛盾在 constraints 中标注出来。这个提示词的关键在于限制输出空间。零件类型限定在几个常见类别里尺寸字段根据零件类型动态变化特征用结构化的方式描述。这样模型不会自由发挥输出的 JSON 可以直接被参数层消费。还有一个细节不要让模型做单位换算。用户说“4 英寸”模型应该输出{value: 4, unit: inch}换算成毫米是参数层的事。模型做算术容易出错而且单位换算涉及精度处理交给确定性的代码更可靠。3.2 参数化模板的编写规范参数层是整套方案里最需要工程经验的部分。每个零件模板本质上是一个函数输入是语义层输出的 JSON输出是一组具体的几何参数。写模板的时候有几个原则。参数必须有明确的类型和范围。比如直径是正浮点数孔的数量是正整数角度是 0 到 360 之间的浮点数。类型不对或者超出范围直接报错不要试图自动修正。约束检查要在建模之前做。法兰盘的孔位圆直径如果大于外径这个零件根本建不出来应该在参数层就拦截而不是等到几何内核报错。我一般会在模板里写一个validate函数把所有约束检查一遍返回具体的错误信息。默认值要合理。用户说“一个法兰盘”没给任何尺寸模板应该有一套行业常用的默认值比如外径 100、内径 50、厚度 10、4 个 M6 孔。这样即使用户描述很模糊也能生成一个可用的初版用户再基于这个版本修改。下面是一个法兰盘模板的简化示例def flange_template(params): od params.get(outer_diameter, 100.0) id_ params.get(inner_diameter, 50.0) thickness params.get(thickness, 10.0) hole_count params.get(hole_count, 4) hole_dia params.get(hole_diameter, 6.6) pcd params.get(pitch_circle_diameter, (od id_) / 2) errors [] if id_ od: errors.append(内径必须小于外径) if pcd od or pcd id_: errors.append(孔位圆直径必须在外径和内径之间) if thickness 0: errors.append(厚度必须大于零) if hole_count 1: errors.append(孔数量必须大于零) if errors: return {success: False, errors: errors} return { success: True, geometry: { type: flange, outer_diameter: od, inner_diameter: id_, thickness: thickness, holes: [ {diameter: hole_dia, pcd: pcd, count: hole_count} ] } }这个模板看起来简单但它把所有的业务逻辑都集中在一个地方测试起来很方便。我一般会为每个模板写一组单元测试覆盖正常情况、边界情况和错误情况。3.3 几何内核的选型与调用几何层我主要用 CadQuery底层是 OpenCASCADE。选它的原因有几个Python 原生和上层代码无缝集成API 设计比较直观写起来像在描述几何体而不是在操作数据结构社区活跃遇到问题容易找到答案。用 CadQuery 建一个法兰盘大概是这样import cadquery as cq def build_flange(geom): od geom[outer_diameter] id_ geom[inner_diameter] thickness geom[thickness] hole geom[holes][0] result ( cq.Workplane(XY) .circle(od / 2) .circle(id_ / 2) .extrude(thickness) ) if hole[count] 0: result ( result.faces(Z) .workplane() .polarArray(hole[pcd] / 2, 0, 360, hole[count]) .hole(hole[diameter]) ) return result这段代码的可读性很好即使不懂 CAD 的人也能大致看出在做什么。polarArray做环形阵列hole打孔都是 CadQuery 内置的操作底层调用的是 OpenCASCADE 的布尔运算可靠性有保障。导出 STEP 和 STL 也很简单cq.exporters.export(result, flange.step) cq.exporters.export(result, flange.stl)GLB 的导出稍微麻烦一点CadQuery 不直接支持。我的做法是先把 STL 转成 GLB用 trimesh 库import trimesh mesh trimesh.load(flange.stl) mesh.export(flange.glb)trimesh 会自动处理材质和法线导出的 GLB 在网页查看器里显示效果不错。4. 实操过程从零搭一个可用的 text-to-cad 服务4.1 环境准备与依赖安装整套系统跑在 Python 3.10 上主要依赖有这些pip install cadquery trimesh openai flaskCadQuery 的安装稍微麻烦一点它依赖 OpenCASCADE 的 Python 绑定。在 Ubuntu 上可以直接pip install cadquery在 Windows 上建议用 condaconda install -c conda-forge cadquery如果安装过程中遇到 OCCT 相关的编译错误大概率是系统缺少开发库。Ubuntu 上装libocct-*-dev系列包Windows 上 conda 会自动处理。大语言模型我用的是 OpenAI 的 API也可以用本地部署的模型替代。如果对数据隐私有要求可以用 Ollama 跑 Llama 3 或者 Qwen语义解析这种任务对模型规模的要求不算高7B 参数的模型就能做得不错。4.2 语义解析接口的实现语义解析的代码结构很简单核心是一个函数输入是用户描述输出是结构化的 JSONimport json from openai import OpenAI client OpenAI() SYSTEM_PROMPT 你是一个 CAD 语义解析器... def parse_description(text): response client.chat.completions.create( modelgpt-4o, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: text} ], response_format{type: json_object} ) return json.loads(response.choices[0].message.content)这里用了response_format{type: json_object}强制模型输出合法的 JSON。这个参数很关键没有它的话模型有时候会在 JSON 外面包一层 markdown 代码块解析起来很麻烦。实测下来对于常见的零件描述解析准确率在 90% 以上。出错的情况主要集中在两类一是描述里包含多个零件模型不知道应该解析哪一个二是描述里有模糊的指代比如“那个孔”但前面没定义过孔。这两类问题在提示词里加一些示例就能大幅改善。4.3 参数校验与几何生成的串联拿到语义解析的结果后先过参数层def process(text): intent parse_description(text) part_type intent[part_type] template TEMPLATES.get(part_type) if not template: return {error: f不支持的零件类型: {part_type}} params template(intent[dimensions]) if not params[success]: return {error: params[errors]} geom build_geometry(params[geometry]) export_all(geom, output) return {success: True, files: [output.step, output.stl, output.glb]}这个流程里每一步都有明确的输入输出任何一步出错都能定位到具体原因。我一般会在每一步加日志记录原始输入、中间结果和最终输出方便排查问题。4.4 前端交互的简单实现如果只是自己用命令行就够了。但如果要给团队用一个简单的网页界面会方便很多。我用 Flask 搭了一个最小化的服务from flask import Flask, request, jsonify, send_file app Flask(__name__) app.route(/generate, methods[POST]) def generate(): text request.json[text] result process(text) return jsonify(result) app.route(/download/filename) def download(filename): return send_file(foutput/{filename})前端用一个简单的 HTML 页面一个输入框、一个按钮、一个模型预览区域。模型预览用 three.js 加载 GLB 文件几十行代码就能搞定。这个服务跑在本地不涉及任何外部依赖数据不出内网对于大多数团队来说够用了。5. 常见问题与排查技巧实录5.1 几何内核报错怎么定位几何内核的报错信息通常很晦涩比如BRep_API: command not done这种看了等于没看。我的经验是把几何构建过程拆成尽可能小的步骤每一步单独执行看哪一步失败。比如建法兰盘先建外圆再建内圆再拉伸再打孔。如果打孔那一步失败大概率是孔的位置超出了实体范围或者孔的数量和阵列参数不匹配。把中间结果导出成 STL 看一眼比盯着报错信息猜要快得多。还有一个技巧是用最简单的参数复现问题。把外径设成 100、内径 50、厚度 10、一个孔如果这样能成功再逐步加复杂度直到找到触发失败的最小配置。5.2 语义解析结果不稳定的处理大语言模型的输出有随机性同样的输入跑两次可能得到不同的 JSON。对于 CAD 这种要求确定性的场景这是不能接受的。我的做法是在语义层加一个结果校验和重试机制。解析出来的 JSON 先过一遍 schema 校验字段类型不对或者缺少必要字段就重新解析最多重试三次。如果三次都失败返回错误让用户重新描述。另外把temperature设成 0 也能大幅降低随机性。虽然不能完全消除但配合重试机制实际使用中基本感觉不到不稳定。5.3 常见问题速查表问题现象可能原因排查方法解决方案生成的 STEP 文件打不开几何体有自相交或非流形边用 STL 预览检查几何检查布尔运算顺序加倒角前先做融合孔的位置不对极坐标阵列的起始角度理解有误导出单孔版本对比明确 polarArray 的角度参数含义语义解析返回空提示词太长或太短打印原始响应调整提示词加 few-shot 示例GLB 显示全黑法线方向反了在查看器里开双面渲染导出前调用 mesh.fix_normals()大模型 API 超时网络问题或请求过大加超时和重试设置 timeout30重试 3 次参数校验通过但建模失败约束检查不完整打印中间几何参数补充约束条件如最小壁厚检查5.4 几个踩过的坑单位问题比想象中严重。用户说“4 英寸”模型输出 4参数层默认单位是毫米结果生成一个 4 毫米的法兰盘。后来我在语义层强制要求模型输出单位字段参数层统一换算成毫米再建模。STL 的精度设置影响很大。CadQuery 导出 STL 时可以设置tolerance和angularTolerance默认值对于小零件来说太粗曲面看起来像多边形。我一般设tolerance0.01、angularTolerance0.1文件大一点但质量好很多。并发请求会互相干扰。如果多个用户同时生成模型输出文件会互相覆盖。解决办法是每次生成用一个独立的临时目录文件名加 UUID生成完再拷贝到目标位置。大模型对专业术语的理解有偏差。“沉头孔”和“埋头孔”在中文里经常混用但对应的几何特征不同。我在提示词里加了一个术语表把常见术语的标准定义写进去解析准确率明显提升。6. 扩展方向从单零件到装配体单零件的 text-to-cad 跑通之后自然会想能不能做装配体。比如“一个电机座包含底座、立柱和顶板用 M6 螺栓连接”。这个问题的复杂度比单零件高一个量级核心难点在于零件之间的配合关系和装配约束。我目前的思路是把装配体描述拆成两部分零件列表和装配关系。零件列表里的每个零件单独走单零件流程生成装配关系用一组约束描述比如“底座的上表面和立柱的下表面重合”、“螺栓孔对齐”。然后用几何内核的装配功能把这些零件按约束摆放到正确位置。这个方向我还在摸索阶段目前能处理一些简单的装配体但复杂装配的约束求解还是容易出问题。如果你也在做类似的事情欢迎交流。另外一个有意思的方向是反向生成给一个 STEP 文件让系统输出一段自然语言描述。这个在零件库管理和检索场景下很有用技术路线和正向生成基本对称语义层从“文本到 JSON”变成“几何特征到 JSON 再到文本”几何层从“参数到几何”变成“几何到参数”。