text-to-cad实战:从自然语言到STEP/URDF的完整链路
1. 从一句话到三维模型text-to-cad 到底在解决什么问题第一次听到 “text-to-cad” 这个词很多人脑子里浮现的画面大概是对着电脑说一句“给我画个齿轮”屏幕上就自动蹦出一个带参数的三维模型。这个想象不算离谱但也不完全准确。text-to-cad 本质上是一套把自然语言描述转换成结构化 CAD 数据的技术链路它的输出通常不是某个私有格式的图纸而是像 STEP、URDF 这类通用、可被下游工具继续消费的中间格式。我在实际项目里接触这个方向最初是因为一个很具体的痛点团队里做机械设计的同事和做仿真、做机器人算法的同事中间隔着一道“格式墙”。设计端给过来的是 STEP仿真端要的是 URDF中间还得有人手动重建一遍关节、坐标系、连杆关系。这个过程又慢又容易出错一个尺寸标错后面整条链路都得返工。text-to-cad 想干的事情就是让“描述”直接变成“可用的模型数据”把中间那段重复劳动压缩掉。它适合谁来参考如果你是从业者比如做机器人仿真、做参数化设计、做自动化建模工具链那这套思路能帮你省掉大量手工建模时间。如果你是刚入门 Python、对 CAD 感兴趣的新手那它也是一个非常好的练手项目——因为它把自然语言处理、几何建模、文件格式转换这几个知识点串成了一条完整的线做完一遍你对“数据怎么在工具之间流动”会有完全不一样的理解。需要先说明一点text-to-cad 不是一个开箱即用的成品软件它更像一个技术方向或者项目骨架。市面上有一些商业工具在做类似的事但真正落到自己的业务场景里往往需要自己搭一套流程。下面我就按我实际踩过的路把这条链路拆开讲清楚。2. 整体架构设计为什么是“文本 → 中间表示 → CAD 文件”这条链路2.1 核心思路不要让大模型直接吐 STEP很多人第一反应是既然有大语言模型那直接让它输出 STEP 文件内容不就行了我试过结论是——不靠谱。STEP 是一种基于 ISO 10303 标准的文本格式里面有大量的实体定义、坐标系变换、拓扑关系语法极其严格。让模型直接生成几百行 STEP稍微一个括号或者参数错位整个文件就打不开而且排查起来非常痛苦因为 STEP 的报错信息通常只告诉你“第几行解析失败”不会告诉你几何哪里错了。所以更稳的做法是引入一个中间表示层。我的方案是自然语言 → 结构化参数JSON→ 用代码生成几何 → 导出 STEP/URDF。这个中间层的好处是每一段都可以单独验证。文本解析错了看 JSON 就知道JSON 对了但模型不对那就是几何生成代码的问题。责任边界清晰调试成本大幅下降。这个思路其实和编译器很像源代码不会直接变成机器码中间要经过词法分析、语法分析、中间代码生成。text-to-cad 的“中间代码”就是那份结构化的参数描述。2.2 技术选型Python 生态里的几个关键角色选 Python 几乎是必然的。原因很简单CAD 相关的开源库、自然语言处理的库、文件格式转换的库Python 生态最全。具体来说我用的组合是这样的自然语言解析用大语言模型的 API 做意图识别和参数抽取输出 JSON。这一步也可以用本地的规则引擎做但泛化能力差很多。几何建模CadQuery或者build123d。这两个库都是基于 OpenCASCADE 的能用代码描述几何体而且原生支持导出 STEP。机器人描述如果要生成 URDF用urdfpy或者直接手写 XML 模板。URDF 本质上是 XML结构比 STEP 简单得多手写模板反而更可控。辅助计算numpy做矩阵运算trimesh做网格处理cv2偶尔用来处理图纸截图如果输入是图片而不是纯文本。这里要特别说一下 CadQuery 和 build123d 的选择。CadQuery 更成熟文档多社区大build123d 是后来者API 设计更现代更接近“用代码画图”的直觉。如果你是新手我建议从 CadQuery 入手因为遇到问题更容易搜到答案。如果你已经熟悉了参数化建模的思路build123d 写起来会更顺手。2.3 为什么输出格式选 STEP 和 URDFSTEP 是 CAD 领域的“通用语”。几乎所有的机械设计软件都能打开 STEP它记录的是精确的边界表示B-Rep不是网格所以放大不会失真。如果你要把模型交给别人继续做设计STEP 是首选。URDF 则是机器人领域的“通用语”。它描述的是连杆、关节、坐标系之间的关系本质上是运动学模型不是几何模型。URDF 里可以引用 STL 或 DAE 作为视觉网格但它本身不包含精确几何。所以这两个格式面向的是不同的下游需求STEP 给设计端URDF 给仿真端。text-to-cad 如果能把这两个都生成出来那它就能同时打通两条链路。这也是我在项目里坚持要支持双格式输出的原因。3. 核心细节拆解从文本到参数的每一步3.1 文本解析怎么让模型稳定输出 JSON这一步是整个链路里最“玄学”的部分因为大语言模型的输出有随机性。我的做法是用函数调用function calling或者结构化输出模式强制模型按照预定义的 schema 返回 JSON。如果用的模型不支持结构化输出那就退而求其次在 prompt 里给出严格的 JSON 示例并且在解析的时候做容错。一个典型的参数 schema 长这样{ object_type: gear, parameters: { module: 2.0, teeth: 20, thickness: 10.0, bore_diameter: 8.0 }, units: mm }这里有几个经验点。第一单位必须显式声明。我踩过一次坑模型默认用了英寸我以为是毫米结果生成的模型大了 25.4 倍。第二参数名要标准化。不要用“齿数”“齿的数量”这种自然语言统一成teeth。第三给默认值。如果用户没说厚度schema 里要有默认值否则模型可能返回 null后面代码就崩了。提示在 prompt 里明确告诉模型“如果某个参数没有提到使用默认值并在输出中标记 is_default: true”这样后续可以提示用户确认。3.2 几何生成用代码描述形状的逻辑拿到 JSON 之后下一步是把它变成几何体。以齿轮为例用 CadQuery 写大概是这样的import cadquery as cq import math def make_gear(module, teeth, thickness, bore_diameter): pitch_radius module * teeth / 2.0 outer_radius pitch_radius module root_radius pitch_radius - 1.25 * module # 简化版齿形实际项目需要用渐开线 result ( cq.Workplane(XY) .circle(outer_radius) .extrude(thickness) .faces(Z) .workplane() .hole(bore_diameter) ) return result这段代码是简化版真实的渐开线齿形要复杂得多需要用到齿廓方程。但这里想说明的是几何生成代码是可测试的。你可以写单元测试给定一组参数检查生成的体积、包围盒尺寸是否符合预期。这比直接检查 STEP 文件内容要可靠得多。对于 URDF逻辑不太一样。URDF 描述的是连杆和关节所以文本解析出来的应该是“有几个连杆”“关节类型是什么”“关节轴朝向哪里”。生成的时候用模板填充urdf_template ?xml version1.0? robot name{name} link namebase_link visual geometry box size{length} {width} {height}/ /geometry /visual /link /robot URDF 的坑在于坐标系。每个 link 都有自己的坐标系joint 的 origin 是相对于 parent link 的。如果 origin 写错了模型在仿真里会飞到奇怪的位置。我的经验是先在纸上画清楚坐标系树再写代码。不要一边想一边写那样很容易乱。3.3 文件导出STEP 和 URDF 的注意事项导出 STEP 用 CadQuery 的exportStep就行但要注意版本。不同版本的 OpenCASCADE 导出的 STEP 在兼容性上略有差异。如果下游用的是比较老的 CAD 软件建议导出 AP214 而不是 AP242。URDF 导出更简单就是写 XML 文件。但有一个容易忽略的点mesh 文件的路径。URDF 里引用的 STL 或 DAE 文件路径可以是相对路径也可以是绝对路径。如果是要分发给别人一定要用相对路径并且把 mesh 文件和 URDF 放在同一个包目录下。注意URDF 本身不包含几何只包含运动学。如果你只导出 URDF 而不导出 mesh在仿真里看到的就是一堆线框。所以完整的输出应该是 URDF STL/DAE 网格文件。4. 实操过程搭一套能跑通的 text-to-cad 流水线4.1 环境准备与依赖安装先把环境搭起来。我习惯用虚拟环境避免污染系统 Pythonpython -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows然后安装核心依赖pip install cadquery build123d numpy trimesh urdfpy如果你要用大语言模型做文本解析还需要安装对应的 SDK。这里不指定具体厂商因为各家 API 差异较大按官方文档来就行。安装 CadQuery 的时候可能会遇到编译问题因为它依赖 OpenCASCADE。在 Linux 上通常没问题在 Windows 上建议直接用 conda 安装conda install -c conda-forge cadquery4.2 完整流程代码框架下面是一个最小可运行的框架把整条链路串起来import json import cadquery as cq def parse_text_to_params(text): # 这里调用大语言模型返回结构化参数 # 实际项目中替换成真实的 API 调用 params { object_type: box, parameters: { length: 50.0, width: 30.0, height: 20.0 }, units: mm } return params def generate_geometry(params): obj_type params[object_type] p params[parameters] if obj_type box: result ( cq.Workplane(XY) .box(p[length], p[width], p[height]) ) elif obj_type cylinder: result ( cq.Workplane(XY) .circle(p[radius]) .extrude(p[height]) ) else: raise ValueError(fUnsupported object type: {obj_type}) return result def export_step(model, filepath): cq.exporters.export(model, filepath) print(fExported STEP to {filepath}) if __name__ __main__: text 生成一个长50毫米、宽30毫米、高20毫米的盒子 params parse_text_to_params(text) print(Parsed params:, json.dumps(params, indent2)) model generate_geometry(params) export_step(model, output.step)这个框架跑通之后你就可以逐步替换里面的模块。比如把parse_text_to_params换成真实的大模型调用把generate_geometry扩展成支持更多形状。4.3 参数计算以齿轮为例的完整推导齿轮是一个很好的例子因为它涉及多个参数之间的数学关系。假设用户说“生成一个模数2、20个齿、厚度10毫米的齿轮”我们需要计算分度圆直径( d m \times z 2 \times 20 40 ) mm齿顶圆直径( d_a d 2m 40 4 44 ) mm齿根圆直径( d_f d - 2.5m 40 - 5 35 ) mm这些计算必须在代码里完成不能依赖模型去算。模型可能会算错但代码不会。所以我的原则是模型只负责抽取参数所有计算交给代码。提示如果用户给的参数不足以确定形状比如只说了“一个齿轮”没说齿数那就要在解析阶段返回一个“参数不完整”的状态让用户补充。不要自己瞎猜。5. 常见问题与排查技巧实录5.1 模型输出格式不对怎么办这是最常见的问题。模型可能返回 Markdown 代码块包裹的 JSON也可能在 JSON 前后加一堆解释文字。解决办法有两个一是用结构化输出模式从源头约束二是在解析前做清洗用正则把 JSON 部分提取出来。import re import json def extract_json(text): # 尝试直接解析 try: return json.loads(text) except json.JSONDecodeError: pass # 尝试提取代码块中的 JSON match re.search(r(?:json)?\s*(\{.*?\})\s*, text, re.DOTALL) if match: return json.loads(match.group(1)) # 尝试提取第一个完整的 JSON 对象 match re.search(r\{.*\}, text, re.DOTALL) if match: return json.loads(match.group(0)) raise ValueError(No valid JSON found in model output)5.2 STEP 文件打不开的排查思路如果导出的 STEP 在 CAD 软件里打不开按这个顺序排查排查项可能原因解决方法文件大小0 字节或异常小检查几何生成是否报错版本兼容AP242 太新改用 AP214 导出几何有效性自相交、零厚度用model.val().isValid()检查单位问题尺寸异常大或小确认导出时的单位设置编码问题中文路径改用英文路径测试我遇到最多的是几何有效性问题。CadQuery 生成的模型偶尔会有自相交的面尤其是在做布尔运算之后。解决办法是在导出前做一次clean()model model.clean()5.3 URDF 在仿真里表现异常的排查URDF 的问题通常出在坐标系和惯性参数上。如果模型在仿真里抖动、飞走、或者穿模先检查这几个地方joint origin是不是相对于 parent link 的很多人误以为是全局坐标。joint axis旋转关节的轴向量是不是单位向量inertia惯性矩阵是不是正定的如果随便填的物理引擎会不稳定。mesh scaleSTL 的单位是米还是毫米URDF 默认单位是米如果 STL 是毫米要加 scale 参数。注意URDF 里的惯性参数如果不知道怎么算可以用trimesh根据网格体积和假设密度估算一个近似值。不要填零矩阵那会导致物理引擎报错。5.4 文本解析的边界情况处理用户输入千奇百怪我整理了几种典型情况和处理方式模糊描述“一个大一点的盒子”——没有具体尺寸。处理方式是返回默认尺寸并提示用户确认。矛盾描述“直径10毫米、半径20毫米的圆”——参数冲突。处理方式是标记冲突让用户选择。超出范围“齿数0.5个齿轮”——非法参数。处理方式是返回错误说明参数范围。多对象“一个盒子和一个圆柱”——需要返回数组而不是单个对象。这些边界情况在 demo 里可能遇不到但一旦上线就会冒出来。建议在解析层就做好校验不要等到几何生成阶段才报错。6. 工具链扩展还能往哪些方向走6.1 从文本到图纸二维输出的可能性text-to-cad 不一定只输出三维模型。有时候用户需要的是一张二维工程图。CadQuery 支持生成二维投影可以导出 DXF 或 SVG。这条路我试过可行但细节很多比如视图方向、标注、线型。如果只是做概念验证导出 SVG 预览就够了。6.2 批量处理用 Python 批量修改 CAD热词里有个“python批量对cad修改”这其实是 text-to-cad 的一个自然延伸。如果你已经能把文本变成参数那批量修改就变成了“批量替换参数 重新生成”。我做过一个脚本读取 Excel 里的参数表每一行生成一个 STEP 文件。核心代码就是一个循环import pandas as pd df pd.read_excel(params.xlsx) for index, row in df.iterrows(): params { object_type: box, parameters: { length: row[length], width: row[width], height: row[height] }, units: mm } model generate_geometry(params) export_step(model, foutput_{index}.step)这个思路可以用在任何需要“参数化批量出图”的场景比如盘扣脚手架、标准件库、家具定制。6.3 与仿真工具对接URDF 导入的注意事项URDF 生成之后通常要导入到仿真环境里。不同仿真工具对 URDF 的支持程度不一样。有的工具对 mesh 路径很敏感有的对 inertia 的格式有要求。我的经验是先在 RViz 或者类似的轻量可视化工具里验证 URDF 能不能正常显示再去接复杂的物理仿真。这样能把“模型描述问题”和“物理引擎问题”分开排查。如果 URDF 导入后模型位置不对先检查base_link的坐标系。很多工具默认base_link在地面如果你的模型原点在几何中心就会看起来“陷进地里”。7. 我踩过的坑和总结的经验做这个方向一年多最大的体会是text-to-cad 的难点不在“text”也不在“cad”而在中间的“to”。文本解析和几何生成都有成熟的工具但把两者稳定地串起来需要大量的工程细节。模型输出的随机性、几何库的版本差异、文件格式的兼容性每一个都可能让你卡半天。第二个体会是不要追求一步到位。一开始不要想着支持所有形状、所有格式。先把“盒子”这一种形状跑通从文本到 STEP 完整走一遍。跑通之后再加圆柱、再加齿轮。每加一种形状就加一组测试用例。这样出了问题你知道是新加的代码的问题而不是整个链路的问题。第三个体会是单位、坐标系、路径这三样东西要反复检查。我遇到的 bug 里至少一半和这三个有关。单位错了模型尺寸不对坐标系错了模型位置不对路径错了文件找不到。每次调试先查这三样能省很多时间。最后分享一个小技巧如果你用大语言模型做解析在 prompt 里加一句“如果用户描述不完整请列出缺失的参数并询问”这样模型会主动暴露信息缺口而不是自己编一个值。这个改动很小但能显著提升解析的可靠性。