text-to-cad 技术解析:从自然语言到三维模型的工程实践 1. 从一句话到三维模型text-to-cad 到底在解决什么问题第一次听到 text-to-cad 这个说法很多人脑子里浮现的画面大概是对着电脑说一句给我画个带四个安装孔的方形法兰盘屏幕上就自动长出一个可以导出加工的 3D 模型。这个想象方向没错但真正落地的时候它解决的问题比省几下鼠标要深刻得多。传统 CAD 建模的流程是这样的你先在脑子里把零件的几何形状想清楚然后打开软件选基准面、画草图、加约束、标注尺寸、拉伸、倒角、打孔、阵列。一个中等复杂度的零件熟练工程师也要折腾半小时到几小时。问题在于这个过程中真正有价值的设计意图——比如这是一个要承受轴向载荷的连接件壁厚不能低于 6mm——在建模操作里被稀释成了几百次点击。text-to-cad 想做的事情是把设计意图用自然语言直接表达出来让机器去完成从语义到几何的翻译。它适合谁我观察下来有三类人最需要一是做机械结构但不想被 CAD 软件操作细节拖慢节奏的工程师二是做参数化设计、需要批量生成零件变体的开发者三是做 AI 辅助设计工具、想把文本理解能力接进建模流程的产品团队。如果你属于这三类中的任何一类下面这些内容应该能帮你少走不少弯路。需要先泼一盆冷水text-to-cad 目前不是一个说一句话就出成品的魔法。它更像一个语义到参数的转换器把模糊的自然语言描述转成结构化的几何参数再驱动建模内核生成模型。理解这个分层是理解整个技术栈的关键。2. 拆解 text-to-cad 的技术链路语义、参数、几何三层结构2.1 为什么不能一步到位从文本直接生成网格的困境最直觉的做法是训练一个模型输入文本直接输出三角网格或者点云。学术界确实有这类工作比如基于扩散模型的文本到 3D 生成。但放到工程场景里这条路基本走不通原因有三个。第一是精度问题。工程零件对尺寸的要求是毫米级甚至微米级而生成式模型输出的网格是看起来像不是尺寸对。一个法兰盘的外径是 80mm 还是 82mm在生成模型眼里可能没有本质区别但在装配现场就是装不上。第二是可编辑性问题。生成出来的网格是一坨三角面片你想把孔径从 8mm 改成 10mm没有任何参数可以调只能重新生成而且重新生成的结果和上一次还不一样。工程设计的本质是迭代不可编辑的模型等于废掉了一半价值。第三是可制造性问题。3D 打印或许能接受任意形状但 CNC 加工、钣金、注塑都有工艺约束。生成模型不懂这些约束出来的形状可能根本加工不出来。所以真正可行的路线是把问题拆成文本理解和几何生成两个相对独立的环节中间用一层结构化的参数描述来衔接。这层参数描述就是整个系统的核心。2.2 中间表示层把带四个孔的法兰盘翻译成什么中间表示层要做的事情是把自然语言里的设计意图转成机器能精确执行的指令。我习惯把它设计成一个分层的 JSON 结构大致长这样{ part_type: flange, base_geometry: { shape: cylinder, outer_diameter: 80.0, thickness: 10.0 }, features: [ { type: hole, pattern: circular, count: 4, diameter: 8.0, pcd: 60.0, through: true }, { type: center_bore, diameter: 30.0, through: true } ], units: mm, tolerance: { general: 0.1, hole_diameter: 0.05 } }这个结构里part_type是零件类别base_geometry是基础形体features是特征列表units和tolerance是全局属性。为什么这样设计因为绝大多数机械零件都可以用基础形体 特征操作的方式描述这是 CSG构造实体几何建模的经典思路也是主流 CAD 内核如 OpenCASCADE、Parasolid的原生工作方式。有了这层中间表示文本理解模块的任务就变成了填槽从用户的话里抽取零件类型、尺寸、特征、数量这些信息填进预定义的 schema。这比端到端生成容易得多也可靠得多。2.3 几何内核的选择为什么绕不开 OpenCASCADE中间表示有了接下来要把它变成真正的 B-rep边界表示模型。这一步必须依赖几何内核。商业内核如 Parasolid、ACIS 授权费用高个人项目基本不用考虑。开源方案里OpenCASCADE简称 OCCT是事实标准Python 生态里可以通过pythonocc-core或者cadquery来调用。CadQuery 值得单独说一下。它本身是一个基于 OCCT 的 Python 参数化建模库用链式 API 描述模型代码可读性很好。比如上面那个法兰盘用 CadQuery 写出来是这样import cadquery as cq result ( cq.Workplane(XY) .circle(40) .extrude(10) .faces(Z) .workplane() .hole(30) .faces(Z) .workplane() .polarArray(30, 0, 360, 4) .hole(8) )这段代码和前面的 JSON 结构几乎是一一对应的。所以一个很自然的架构就是文本理解模块输出 JSON一个转换器把 JSON 翻译成 CadQuery 脚本执行脚本得到模型再导出 STEP 或 STL。这个架构的好处是每一层都可以单独测试和替换出问题的时候能快速定位是哪一层的锅。3. 文本理解模块的实战从规则到大模型的分阶段方案3.1 纯规则方案能走多远正则和关键词的边界刚开始做的时候很多人会想用正则表达式硬怼。比如匹配直径(\d)mm提取直径匹配(\d)个孔提取孔数。这个方案在描述非常规范的时候能用但很快就会撞墙。问题在于自然语言的表达太灵活了。外径 80、直径 80 毫米、80mm 的圆盘、半径 40——这四种说法指的是同一个东西但正则要写四套。更麻烦的是隐含信息比如标准法兰盘这种说法背后是一整套默认尺寸规则系统根本没法处理。我的经验是规则方案适合做兜底和校验不适合做主解析。比如用规则去检查大模型输出的参数是否在合理范围内这个用法就很合适。3.2 大模型做槽位填充提示词怎么设计才稳定用大模型做文本理解核心是提示词工程。我试过多轮迭代最后稳定下来的提示词结构是这样的SYSTEM_PROMPT 你是一个机械设计参数提取助手。 用户会用自然语言描述一个零件你需要提取出结构化参数。 规则 1. 所有尺寸单位统一转换为毫米(mm) 2. 如果用户说半径转换为直径乘以2 3. 如果用户没有指定某个参数使用该零件类型的标准默认值 4. 输出必须是合法的 JSON不要有任何额外文字 5. 不确定的参数标记为 null不要猜测 零件类型的标准默认值 - flange: 外径100, 厚度10, 中心孔直径40, 4个直径10的安装孔, PCD 75 - plate: 长100, 宽100, 厚5 - shaft: 直径20, 长100 这里有几个关键点值得展开。第一是单位统一工程上最怕单位混乱让模型在提取阶段就统一成毫米后面所有环节都省心。第二是默认值策略用户说来个法兰盘的时候模型必须给出一个合理的默认尺寸否则下游没法执行。第三是不确定就标 null这一点特别重要宁可让模型承认不知道也不要让它编一个看起来合理的数字因为编出来的数字会一路传到几何生成环节最后出来一个尺寸错误的模型排查起来非常痛苦。实测下来用结构化输出比如 OpenAI 的 function calling 或者 JSON mode比让模型自由输出 JSON 要稳定得多解析失败率能从 15% 降到 2% 以下。3.3 多轮澄清当用户说大一点的时候怎么办真实使用场景里用户很少一次把话说全。更常见的是做个法兰盘→看到模型后说孔太多了→外径再大一点。这种多轮交互需要系统维护一个会话状态。我的做法是维护一个当前参数集每一轮用户输入都作为增量修改指令来处理。用户说孔太多了模型需要输出的是{features[0].count: 2}这样的修改指令而不是重新生成整个参数集。这样既能保留之前已经确认的参数又能响应用户的调整。这里有个坑指代消解。大一点是哪个尺寸大一点外径厚度还是孔径如果上下文里只有一个尺寸被讨论过那还好办如果有多个就必须让模型主动澄清。我一般会在提示词里加一条如果用户的修改指令存在歧义输出一个 clarification 字段列出可能的解释让用户选择。4. 参数到几何的转换CadQuery 脚本生成与执行细节4.1 模板化生成 vs 动态生成哪种更可靠把 JSON 参数转成 CadQuery 代码有两条路。一条是模板化为每种零件类型写一个固定的代码模板参数用占位符替换。另一条是让大模型直接生成 CadQuery 代码。我强烈建议用模板化。原因很简单模板化生成的代码是确定性的同样的参数永远生成同样的代码出了问题容易复现。而大模型生成的代码每次都不一样可能这次能跑下次就报语法错误调试成本极高。模板化的实现方式可以用 Python 的字符串格式化也可以用 Jinja2 模板引擎。对于特征比较复杂的零件Jinja2 更灵活因为它支持条件判断和循环。比如安装孔的数量是变量用 Jinja2 可以这样写.faces(Z).workplane() {% if features.hole_pattern %} .polarArray({{ features.hole_pattern.pcd / 2 }}, 0, 360, {{ features.hole_pattern.count }}) .hole({{ features.hole_pattern.diameter }}) {% endif %}4.2 执行沙箱为什么不能直接 exec生成的代码要执行最直接的方式是exec()。但这里有个安全隐患如果文本理解环节被注入了恶意内容生成的代码可能包含危险操作。虽然个人项目风险不大但养成好习惯很重要。我的做法是用一个受限的执行环境只暴露 CadQuery 的必要接口屏蔽掉os、sys、subprocess这些模块。具体实现可以用exec的 globals 参数控制命名空间safe_globals { cq: cadquery, Workplane: cadquery.Workplane, __builtins__: {} } local_vars {} exec(generated_code, safe_globals, local_vars) result local_vars.get(result)这样即使代码里有import os也会因为__builtins__为空而失败。当然这不是绝对安全的沙箱但对于个人项目来说够用了。4.3 导出格式的选择STEP、STL、还是别的模型生成之后要导出。STEP 是工程界的通用交换格式保留了完整的 B-rep 信息可以被任何主流 CAD 软件打开继续编辑是首选。STL 是三角网格只适合 3D 打印和渲染丢失了参数信息。如果下游要做仿真可能还需要 IGES 或者 Parasolid。CadQuery 的导出很简单cq.exporters.export(result, output.step) cq.exporters.export(result, output.stl)实测下来STEP 导出偶尔会遇到 OCCT 的兼容性问题特别是模型有自相交或者极小特征的时候。如果导出失败可以先做一次clean()操作清理掉冗余的边和面。5. 实测中暴露的问题与排查思路5.1 尺寸单位混乱一个 80 变成 80 米的惨案这是我最开始踩的坑。用户说外径 80模型提取出 80但没标注单位。下游默认按米处理结果生成了一个直径 80 米的圆盘导出的时候 OCCT 直接报错。排查这个问题的过程挺有意思。一开始我以为是几何内核的问题查了半天 OCCT 的日志后来才发现是单位没统一。修复方案是在中间表示层强制加units字段并且在参数提取的提示词里明确要求所有尺寸转换为毫米。提示单位问题在工程软件里是高频坑建议在数据流的每一个环节都做单位校验宁可多写几行检查代码也不要让错误单位流到下游。5.2 特征顺序导致的建模失败CadQuery 是链式操作顺序很重要。比如先打孔再倒角和先倒角再打孔结果可能完全不同。有一次用户描述带圆角的方形板四角有安装孔模板生成的代码是先打孔后倒角结果倒角把孔边缘削掉了一块。这个问题的根因是特征顺序没有在中间表示层体现。修复方案是在 JSON 里加一个feature_order字段明确指定特征的执行顺序。默认顺序是基础形体 → 大特征如中心孔→ 小特征如安装孔→ 修饰特征如倒角、圆角。这个顺序符合大多数加工工艺的实际流程。5.3 大模型幻觉编造不存在的标准尺寸大模型有个坏习惯遇到不确定的参数会编一个看起来合理的值。比如用户说标准 M8 螺栓孔模型可能编出孔径 6.5mm这种数字。实际上 M8 螺栓的过孔标准是 8.4mm 或 9mm取决于精度等级。应对方案有两个。一是在提示词里明确列出常用标准件的尺寸表让模型查表而不是猜。二是在参数校验环节加一层规则检查比如孔径和螺栓规格的对应关系不符合就报警。BOLT_HOLE_STANDARDS { M3: 3.4, M4: 4.5, M5: 5.5, M6: 6.6, M8: 9.0, M10: 11.0, M12: 13.5, M16: 17.5, M20: 22.0 } def validate_hole_diameter(bolt_spec, diameter): expected BOLT_HOLE_STANDARDS.get(bolt_spec) if expected and abs(diameter - expected) 0.5: return False, f孔径 {diameter} 与 {bolt_spec} 标准过孔 {expected} 不符 return True, 5.4 复杂模型的性能瓶颈简单零件生成很快一两秒就出来了。但特征一多比如一个带 50 个孔的板CadQuery 的布尔运算会变得很慢有时候要几十秒。这是因为 OCCT 的布尔运算复杂度随特征数量增长很快。优化思路有几个。一是用阵列特征代替逐个打孔CadQuery 的polarArray和rectangularArray内部做了优化比循环打孔快很多。二是对于纯展示用途可以先生成低精度模型用户确认后再生成高精度版本。三是把生成任务放到后台线程避免阻塞 UI。6. 把 text-to-cad 用起来的几种实际姿势6.1 命令行工具最快跑通的方式如果你只是想快速验证想法做一个命令行工具是最省事的。用户输入一句话程序输出一个 STEP 文件。核心代码不超过 100 行import argparse from text_parser import parse_text_to_json from geometry_builder import build_from_json def main(): parser argparse.ArgumentParser() parser.add_argument(description, help零件描述) parser.add_argument(-o, --output, defaultoutput.step) args parser.parse_args() params parse_text_to_json(args.description) model build_from_json(params) model.export(args.output) print(f模型已导出到 {args.output}) if __name__ __main__: main()用起来就是python main.py 外径80厚10的法兰盘中心孔304个M8安装孔。这种方式适合做原型验证也方便集成到其他脚本里。6.2 Web 界面让非技术用户也能用命令行对工程师友好但对产品经理、设计师就不太行了。做一个简单的 Web 界面左边输入描述右边用 Three.js 渲染模型预览体验会好很多。技术栈上后端用 FastAPI 提供接口前端用 React 加 Three.js。模型生成后导出 STL前端加载 STL 做预览。这里有个细节STL 文件可能很大传输的时候要开 gzip 压缩否则加载会很慢。6.3 集成到现有设计流程API 化的思路如果团队已经有自己的设计工具链把 text-to-cad 做成一个 API 服务是最灵活的。输入是文本描述输出是 STEP 文件的 URL 或者 base64 编码。这样任何能发 HTTP 请求的地方都能调用。API 设计上我建议做成异步的。因为模型生成可能耗时几秒到几十秒同步接口容易超时。流程是提交任务返回 task_id然后轮询或者用 webhook 获取结果。7. 关于精度、可靠性和边界的几点个人体会做了几个月的 text-to-cad最大的体会是这个技术的价值不在于替代工程师而在于把工程师从重复劳动里解放出来。一个标准件库里有几百个法兰盘变体以前要一个个建模现在用文本描述批量生成效率提升是数量级的。但边界也很清楚。复杂的自由曲面、需要大量工程判断的结构设计、涉及装配关系的系统级设计目前还是得靠人。text-to-cad 擅长的是参数明确、结构规整的零件这个范围其实已经覆盖了机械设计中相当大的一部分工作量。精度方面我的建议是不要盲目相信模型输出。生成之后一定要做尺寸校验至少检查关键尺寸是否在合理范围内。我一般会在导出前加一道自动检查把模型的实际包围盒尺寸和参数里的预期尺寸对比偏差超过阈值就报警。最后分享一个实用技巧把常用的零件描述存成一个配方库用户可以直接调用。比如标准法兰盘 A 型对应一套固定的参数这样既保证了输出的一致性又降低了用户的输入成本。这个思路其实和 CAD 软件里的模板功能是一回事只是入口从鼠标点击变成了文本输入。