text-to-cad实战:用大模型和Python实现自然语言生成CAD模型 CAD 这行有个老生常谈的痛点脑子里想清楚了一个零件长什么样手上却要花十几分钟去拉伸、打孔、倒角最后还要检查尺寸链有没有对。更别提批量改参数的时候改一个尺寸牵动十几个特征报错。这两年大模型能力上来了我一直在琢磨一件事——能不能用一句话直接生成可用的 CAD 模型text-to-cad这个方向就是干这个的把自然语言描述转成结构化的几何数据再输出成 STEP、URDF 这类下游能直接吃的格式。它解决的不是替代设计师而是把重复性的建模劳动压缩掉让参数化建模从手搓特征树变成描述需求 校验结果。这篇内容适合三类人看做机械设计想提效的工程师、搞机器人仿真需要批量生成 URDF 的开发者、以及想用 Python 把 CAD 流程自动化的同学。下面我按自己实际跑通这套链路的顺序把原理、选型、代码和踩过的坑一次讲透。1. 先搞清楚 text-to-cad 到底在转什么很多人第一次听到文字生成 CAD脑子里浮现的是 AI 直接画出一张图纸。这个理解偏了。真正落地的 text-to-cad 管线中间一定要经过一层结构化的中间表示文字不会直接变成几何而是先变成参数和约束再由几何内核去求解。1.1 从自然语言到几何内核的三段式链路我把整条链路拆成三段理解这三段后面所有工具选型和排错都有依据。第一段是语义解析。大模型在这里干的活是把一个 50mm 见方、中心有个直径 10mm 通孔的方块解析成类似{shape: box, size: [50,50,50], features: [{type: hole, diameter: 10, through: true}]}这样的结构。注意这里输出的是参数不是坐标点云。为什么强调这点因为参数化模型可以回改点云不能。你后面想把这个孔改成 12mm只需要改一个数字重新求解而不是重新生成整个网格。第二段是几何求解。拿到参数后交给几何内核kernel去构建 B-rep边界表示实体。这一步是纯确定性的计算跟 AI 没关系。常见的开源内核有 OpenCASCADEOCCTPython 里通过cadquery或build123d调用它。这一步的输入输出都是精确的数学曲面不是三角网格所以尺寸是准的。第三段是格式导出。求解出来的实体要落到具体文件格式上。STEP 是通用交换格式几乎所有 CAD 软件都认URDF 是机器人描述格式带关节和连杆信息给仿真器用STL 是网格格式给 3D 打印用。选哪个格式取决于你下游要干什么。提示如果你的需求只是生成一个能 3D 打印的模型那 STL 就够了链路可以简化。但只要涉及后续编辑、装配、仿真就必须走参数化 STEP/URDF 这条路否则模型是死的。1.2 为什么中间表示必须是参数而不是网格这里展开说一下因为这是整个方案能不能用的分水岭。网格模型mesh本质上是一堆三角面片的集合它只记录表面长什么样不记录这个面是怎么来的。你拿到一个 STL想把它中间的孔从 10mm 改到 12mm没有任何工具能帮你——因为文件里根本没有孔这个概念只有一堆顶点坐标。而参数化模型记录的是特征历史先画了个方块再挖了个孔孔的参数是直径 10。改参数重新求解几何自动更新。text-to-cad 的价值恰恰在于它输出的是参数。大模型擅长的是理解语义、抽取参数不擅长精确计算几何。让模型输出参数、让内核算几何各干各擅长的事这才是靠谱的分工。反过来如果你让模型直接吐坐标那精度和可编辑性都没法保证。1.3 一个最小可跑通的例子长什么样先给个直观感受后面再展开。假设我们用cadquery做几何求解用大模型做语义解析最小闭环大概是这样import cadquery as cq # 假设大模型解析后返回了这样的参数 params { size: 50.0, hole_diameter: 10.0, fillet_radius: 2.0 } # 几何求解方块 通孔 倒角 result ( cq.Workplane(XY) .box(params[size], params[size], params[size]) .faces(Z).workplane().hole(params[hole_diameter]) .edges(|Z).fillet(params[fillet_radius]) ) # 导出 STEP cq.exporters.export(result, part.step)这段代码跑通你就有了 text-to-cad 的骨架。剩下的工作全是围绕怎么让大模型稳定输出正确的 params和怎么把 params 映射到更复杂的几何操作来做的。别小看这个骨架我见过太多人一上来就想搞复杂装配体结果连单零件的参数映射都没调稳。2. 环境搭建Python 几何栈的安装顺序有讲究text-to-cad 的工程实现基本绕不开 Python因为几何内核的绑定、大模型 SDK、文件解析库在 Python 生态里最全。但 Python 几何栈的安装是出了名的容易翻车顺序错了、版本对不上import cadquery直接报一堆 C 符号找不到的错。这一节把我验证过的安装路径讲清楚。2.1 用 conda 而不是 pip 装几何内核先说结论几何内核相关的包优先用 conda 装不要用 pip。原因是 OCCT 这类内核是 C 编译的pip 上的 wheel 经常和你的系统库版本对不上尤其是 Windows 上缺 MSVC 运行库的时候报错信息还特别隐晦。我的标准流程是这样# 1. 建一个干净的虚拟环境Python 版本选 3.10 或 3.11 conda create -n text2cad python3.11 conda activate text2cad # 2. 用 conda-forge 频道装 cadquery它会自动带上匹配的 OCCT conda install -c conda-forge cadquery # 3. 验证内核是否正常 python -c import cadquery; print(cadquery.__version__)为什么选 3.10/3.11 而不是最新的 3.12/3.13因为几何内核的 Python 绑定更新往往滞后新版本 Python 上经常没有预编译包你得自己编译 OCCT那个过程能劝退 90% 的人。我实测 3.11 是目前兼容性最好的版本cadquery、build123d、trimesh 都能顺利装上。如果你实在不想用 condapip 也不是完全不行但要装cadquery-ocp这个预编译包而且得确认你的系统有对应的 C 运行库。Windows 上如果报c2005之类的运行库错误去装一下微软的 VC 运行库合集这个坑我踩过不止一次。2.2 大模型 SDK 和解析库的搭配几何栈装好之后再装语义解析这一侧的东西。这部分用 pip 就行不涉及编译。pip install openai pydanticpydantic这个库我要重点说一下它是保证大模型输出稳定的关键。大模型返回的是文本你要把它变成可靠的 Python 对象中间必须有一层校验。pydantic可以定义参数的结构和类型约束模型输出不符合结构就直接报错而不是让错误悄悄流到几何求解那一步。from pydantic import BaseModel, Field class PartSpec(BaseModel): size: float Field(gt0, description方块边长单位 mm) hole_diameter: float Field(gt0, description中心通孔直径) fillet_radius: float Field(ge0, description棱边倒角半径)有了这个模型大模型的输出先过一遍PartSpec.model_validate_json()类型不对、数值为负当场就拦下来了。这一步能省掉后面大量的调试时间。2.3 验证环境是否真的可用装完之后别急着写业务代码先跑一个端到端的最小验证确认几何内核、导出、文件读写都正常。import cadquery as cq # 建一个最简单的实体 box cq.Workplane(XY).box(10, 10, 10) # 导出 STEP 并读回验证 cq.exporters.export(box, test.step) import os assert os.path.exists(test.step), STEP 导出失败 print(体积:, box.val().Volume())如果Volume()能打印出一个接近 1000 的数值说明内核计算正常。如果报错或者数值离谱那说明安装有问题别往下走先把环境修好。我见过有人跳过这步后面几何求解出错花了两天才定位到是内核没装对。3. 语义解析层让大模型稳定吐出参数环境搞定后真正的难点来了——怎么让大模型每次都输出正确的参数。直接问帮我生成一个方块模型可能给你一段解释文字也可能给你一段它自己编的代码格式完全不固定。这一节讲怎么把它约束住。3.1 用结构化输出而不是自由文本最有效的办法是强制模型走结构化输出structured output。现在主流的大模型 API 都支持指定 JSON Schema模型必须按这个 schema 返回。配合前面定义的pydantic模型可以直接生成 schema。from openai import OpenAI import json client OpenAI() def parse_spec(user_text: str) - PartSpec: schema PartSpec.model_json_schema() resp client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是 CAD 参数解析器只输出 JSON不要解释。}, {role: user, content: user_text} ], response_format{type: json_schema, json_schema: { name: PartSpec, schema: schema }} ) raw resp.choices[0].message.content return PartSpec.model_validate_json(raw)这样拿到的spec一定是符合结构的对象不会出现模型返回了一段 markdown 代码块这种破事。为什么强调这点因为自由文本解析的鲁棒性极差模型稍微换个措辞你的正则就崩了。结构化输出是从源头解决问题。3.2 单位、坐标系和默认值的坑参数解析里最容易出问题的是单位和坐标系。用户说一个 5 厘米的方块模型可能给你 5也可能给你 50。用户说上面打个孔上面到底是 Z 还是 -Z不同模型理解不一样。我的处理办法是在 system prompt 里把规则写死所有长度统一转成毫米mm厘米乘 10米乘 1000。默认坐标系Z 轴朝上XY 平面为底面。上面/顶部指 Z 方向底面指 -Z 方向。未指定的倒角、圆角默认为 0。这些规则看起来啰嗦但能挡掉大量歧义。我实测下来把规则写清楚之后参数解析的准确率能从经常要手动改提升到基本一次过。注意不要让模型自己决定单位换算让它输出原始数值加单位字段换算在 Python 里做。模型做算术是不可靠的尤其是涉及小数的时候。3.3 复杂特征的拆解策略单一方块好办但真实零件往往有多个特征。比如一个 80x60x20 的底板四角各有一个 M4 沉头孔中间开一条 10mm 宽的槽。这种描述直接让模型输出一个大 JSON很容易漏特征或者搞错位置。我的策略是分步解析先解析主体形状再逐个解析特征每个特征单独一次调用。class Feature(BaseModel): type: str # hole / slot / fillet / chamfer position: list[float] # 相对坐标 params: dict class PartSpec(BaseModel): base_size: list[float] features: list[Feature]先让模型输出主体再循环问还有哪些特征每个特征单独结构化。这样虽然调用次数多了但每个环节的准确率都高总体反而更稳。而且出错的时候容易定位——是主体错了还是某个特征错了一目了然。4. 几何求解层把参数翻译成 cadquery 操作参数拿到手接下来是把它变成实际的几何操作。这一层是纯工程活没有 AI 的不确定性但坑一点不少。4.1 特征操作的顺序会改变结果CAD 建模有个反直觉的地方同样的特征操作顺序不同结果可能不一样。最典型的是倒角和打孔。如果你先给方块所有棱边倒角再打一个孔孔边缘是尖的如果你先打孔再倒角孔边缘也会被倒到。用户说方块倒角中间打孔他到底想要哪种我的默认规则是先做减材料操作孔、槽再做加材料操作凸台最后做修饰操作倒角、圆角。这个顺序符合大多数人的直觉也符合机械加工的实际情况——先加工出主体形状最后去毛刺。这个规则我会写进几何求解的调度逻辑里而不是让模型决定顺序。def build_part(spec: PartSpec): wp cq.Workplane(XY).box(*spec.base_size) # 第一轮减材料 for f in spec.features: if f.type hole: wp wp.faces(Z).workplane().center(*f.position[:2]).hole(f.params[diameter]) elif f.type slot: wp wp.faces(Z).workplane().slot2D(f.params[length], f.params[width]).cutThruAll() # 第二轮修饰 for f in spec.features: if f.type fillet: wp wp.edges(f.params.get(selector, |Z)).fillet(f.params[radius]) return wp4.2 选择器selector是 cadquery 的精髓也是难点上面代码里的faces(Z)、edges(|Z)就是 cadquery 的选择器。它用一套字符串语法来选中特定的面或边比如Z表示 Z 方向最靠上的面|Z表示平行于 Z 轴的边。这套语法非常强大但也是新手最容易懵的地方。text-to-cad 场景下选择器往往需要从自然语言里推断。用户说在顶面打孔你就得翻译成faces(Z)说给四条竖边倒角就是edges(|Z)。我建议把常见的选择器映射做成一张表让模型从表里选而不是自由生成选择器字符串。自然语言cadquery 选择器含义顶面faces(Z)Z 最大方向的面底面faces(Z)Z 最小方向的面竖边edges(|Z)平行 Z 轴的边所有边edges()全部边最长的边edges().sort(lambda e: e.Length())[-1]按长度排序取最长这张表能覆盖 80% 的常见需求。剩下的 20% 复杂选择老实说让用户手动指定比让模型猜更靠谱。4.3 布尔运算失败和容差问题几何求解最让人头疼的是布尔运算失败。两个实体做差集如果面刚好重合或者间隙极小内核可能算不出来直接抛异常。这在参数化生成里特别常见因为参数是模型给的可能刚好落在临界值上。我的处理办法是加一层容差保护在布尔运算前检查两个实体是否有重叠如果间隙小于某个阈值比如 0.001mm就主动调整参数避开临界值。def safe_cut(base, tool, tol1e-3): try: return base.cut(tool) except Exception as e: # 记录失败参数方便回溯 print(f布尔运算失败: {e}) raise更重要的是记录失败参数。text-to-cad 是批量生成场景某个参数组合失败很正常关键是要能定位是哪个参数导致的而不是整个批次挂掉。我会把每次失败的 spec 存下来事后分析是不是某类参数特别容易触发。5. 导出层STEP、URDF 和 STL 各管什么几何算出来了最后一步是导出成下游能用的格式。这一步看着简单但格式选错下游用不了前面全白干。5.1 STEP 是通用交换的首选STEP.step / .stp是 CAD 领域的事实标准交换格式它保存的是精确的 B-rep 几何几乎所有 CAD 软件都能导入。text-to-cad 生成的模型如果要给人继续编辑导出 STEP 是首选。cq.exporters.export(result, part.step)STEP 的优点是通用、精确、可编辑。缺点是文件里不带特征历史别人拿到只能看到最终形状改不了参数。如果你希望下游能改参数得把参数 spec 一起存下来或者导出成支持特征的格式比如 FreeCAD 的 FCStd。5.2 URDF 是给机器人仿真用的URDFUnified Robot Description Format是机器人领域的标准格式描述的是连杆和关节不是单个零件。text-to-cad 生成 URDF 的场景通常是批量生成机器人的结构件然后组装成完整的机器人模型。URDF 是 XML 格式核心是link和joint标签。一个连杆的 URDF 片段大概长这样link namebase_link visual geometry mesh filenamebase.stl scale0.001 0.001 0.001/ /geometry /visual collision geometry mesh filenamebase.stl/ /geometry /collision /link注意这里的scaleURDF 默认单位是米而 CAD 里常用毫米所以导出网格的时候要缩放 0.001。这个单位坑我踩过导入仿真器之后模型大了 1000 倍找了半天才发现是单位没转。生成 URDF 的流程是先用 cadquery 生成几何导出 STL 网格再写 URDF 的 XML 引用这个网格。几何和描述是分开的两部分。5.3 STL 只适合展示和打印STL 是三角网格格式只有表面形状没有特征、没有单位、没有颜色。它适合 3D 打印和快速预览但不适合任何需要精确编辑的场景。cq.exporters.export(result, part.stl, tolerance0.01)tolerance参数控制网格精度值越小网格越密、文件越大。做 3D 打印一般 0.01 到 0.05 就够了做仿真碰撞检测可以适当放宽到 0.1减少计算量。格式精度可编辑典型用途STEP精确 B-rep可编辑形状CAD 交换、后续加工URDF引用网格可改关节机器人仿真STL三角网格不可编辑3D 打印、预览6. 实测中那些文档不会写的坑前面讲的都是应该怎么做这一节讲实际做的时候会怎么翻车。这些是我自己踩出来的文档里基本找不到。6.1 大模型对空间关系的理解很弱模型能理解一个方块但对在方块左上角打孔这种空间描述理解经常出错。它可能把孔打在中心也可能把左上角理解成三维空间的某个角。我的经验是凡是涉及位置的描述都要求用户给相对坐标或者明确的方位词并且在 prompt 里把方位词的定义写死。更稳的做法是让模型输出归一化坐标0 到 1 之间再由 Python 换算成实际坐标。比如左上角对应[0.1, 0.9]这样即使模型对绝对尺寸没概念相对位置也不会错太远。6.2 参数范围校验能挡掉一半的报错几何内核在参数不合理的时候会直接崩比如倒角半径大于棱边长度的一半或者孔径大于方块边长。这些错误如果不在参数层拦住就会变成内核层的异常报错信息还看不懂。我的做法是在pydantic模型里加校验器from pydantic import model_validator class PartSpec(BaseModel): size: float hole_diameter: float fillet_radius: float model_validator(modeafter) def check_ranges(self): if self.hole_diameter self.size: raise ValueError(孔径不能大于等于边长) if self.fillet_radius self.size / 2: raise ValueError(倒角半径过大) return self这样参数一进来就被校验不合理的直接拒绝不会流到几何层。实测下来这一层能挡掉一半以上的运行时报错。6.3 批量生成要加缓存和重试text-to-cad 真正有价值的场景是批量生成——一次生成几十上百个零件。这时候两个问题会暴露出来一是大模型调用有失败率二是几何求解偶尔会失败。我的处理是给每个环节加重试 缓存。大模型调用失败就重试最多三次几何求解失败就记录参数跳过这个零件继续下一个最后统一报告哪些失败了。缓存则是把文字描述 - 参数的映射存下来同样的描述第二次直接读缓存省调用费也省时间。import hashlib, json, os def cached_parse(text): key hashlib.md5(text.encode()).hexdigest() cache_file fcache/{key}.json if os.path.exists(cache_file): return PartSpec.model_validate_json(open(cache_file).read()) spec parse_spec(text) os.makedirs(cache, exist_okTrue) open(cache_file, w).write(spec.model_dump_json()) return spec这个缓存机制在调试阶段特别有用因为你会反复用同样的描述测试没有缓存的话每次都要等模型返回效率极低。6.4 别指望一次生成复杂装配体最后说个心态问题。我见过不少人一上来就想让模型生成一个完整的减速箱结果被现实教育。text-to-cad 目前靠谱的能力边界是单零件、中等复杂度。装配体涉及零件间的配合、约束、运动关系这些信息自然语言很难描述清楚模型也很难理解。务实的做法是先用 text-to-cad 生成单个零件装配关系手动在 CAD 软件里做或者用脚本按规则拼装。把 AI 用在它擅长的地方——理解单个零件的语义、抽取参数而不是让它处理它不擅长的空间装配逻辑。7. 从单零件到参数化模板的进阶思路跑通单零件生成之后下一步是把它变成可复用的参数化模板。这才是 text-to-cad 真正能提效的地方——不是每次从零生成而是把常见零件类型做成模板文字只负责填参数。7.1 把高频零件抽象成模板机械设计里大量零件是重复的法兰、支架、轴套、齿轮坯。这些零件的形状固定只是尺寸不同。与其每次让模型从零解析不如把它们做成模板模型只负责识别这是法兰并抽取尺寸。def flange_template(outer_d, inner_d, thickness, bolt_count, bolt_circle_d): wp cq.Workplane(XY).circle(outer_d/2).extrude(thickness) wp wp.faces(Z).workplane().hole(inner_d) # 螺栓孔阵列 wp (wp.faces(Z).workplane() .polarArray(bolt_circle_d/2, 0, 360, bolt_count) .hole(bolt_d)) return wp模板的好处是几何逻辑经过验证不会出错模型只需要判断零件类型和抽取几个关键尺寸准确率大幅提升。我实测下来模板化之后生成成功率从七成提升到九成五以上。7.2 模板库的组织和检索模板多了之后需要管理。我的做法是给每个模板写一段自然语言描述存进一个索引用户输入先做语义匹配找到最接近的模板再抽取参数。templates { flange: { desc: 法兰盘带中心孔和一圈螺栓孔, func: flange_template, params: [outer_d, inner_d, thickness, bolt_count, bolt_circle_d] }, bracket: { desc: L 型支架两个面成直角带安装孔, func: bracket_template, params: [length, height, width, thickness, hole_d] } }匹配可以用简单的关键词也可以用向量检索。零件类型不多的时候关键词就够了上百个模板之后再上向量检索。7.3 参数默认值和继承模板化之后参数默认值就很重要了。用户说来个法兰没给尺寸你得有一套合理的默认值否则生成不出来。我的做法是每个模板带一套默认参数用户没指定的就用默认值指定了的覆盖。defaults { flange: {outer_d: 100, inner_d: 50, thickness: 10, bolt_count: 6, bolt_circle_d: 80} }这套默认值要符合工程常识不能随便填。法兰的螺栓圈直径要大于内孔、小于外径螺栓数量要能被 360 整除方便阵列。这些约束我会写进模板的校验逻辑里保证默认值组合本身是合法的。8. 关于这套方案边界的几点个人判断跑了几个月下来我对 text-to-cad 的能力边界有了比较清楚的认识分享几点判断帮你在投入之前想清楚预期。第一它擅长的是参数填充不是从零设计。你告诉它一个 100mm 的法兰它能快速生成你告诉它设计一个能承受 5kN 载荷的支架它做不了因为那涉及强度计算和结构选型是设计决策不是参数生成。把 AI 用在参数填充环节收益最直接。第二几何精度靠内核不靠模型。所有尺寸的准确性由 OCCT 这类内核保证模型只负责理解语义。这个分工不能乱一旦让模型参与几何计算精度就没保障了。第三模板覆盖率决定实用价值。纯靠模型自由生成成功率有限把高频零件模板化之后实用性会有一个台阶式的提升。所以我的建议是先把你自己工作里最高频的十种零件做成模板再考虑扩展。第四校验环节不能省。生成的模型一定要有自动校验——体积是否合理、是否有干涉、关键尺寸是否在公差内。我一般会算一下生成实体的体积跟理论值对比偏差超过 5% 就报警。这个简单的检查能挡掉大部分几何错误。这套东西我目前用在标准件批量生成和仿真模型准备上省下来的时间相当可观。但它不是银弹复杂曲面、自由造型、装配设计这些还是得人来。把它当成一个高效的参数化建模助手预期就对了。