“text-to-cad”这个词我第一次认真接触,是在一个加工报价系统的内部开发里。当时想着既然大模型能写代码,那让它直接写一段CadQuery脚本生成三维模型,应该不算难事。结果真跑起来才发现:从“能生成代码”到“能稳定生成对的几何”,中间隔着一整条工程化的深坑。如果你也想搭一套从自然语言到CAD模型的工具,或者单纯想搞明白这个方向到底行不行,我把自己跑通流程、踩坑排查、落地的经验都写在这里,希望能帮你省掉几周瞎折腾的时间。
1. text-to-cad到底在解决什么:从“画图两小时,改图一秒钟”的真相说起
1.1 传统CAD建模的“时间黑洞”在哪
做过机械设计的人都有这种体验:新建一个零件,选基准面、画草图、加约束、拉伸,再打孔、倒角、阵列,一套动作下来,哪怕结构很简单,也得花上十几分钟。真正痛苦的还不是第一次建模,而是改模型。早期的建模步骤如果没有按“参数化特征”的思路组织好,客户跟你说“把孔距从80改成85”,你很可能要把整个特征树从头到尾顺一遍,然后发现某个草图约束早就锁死了,一改全崩。
行业里那句“画图两小时,改图一秒钟”其实是反讽——画的只是体力活,改的才是技术活。text-to-cad瞄准的正是这个痛点:让自然语言直接变成可编辑的CAD模型,缩短从“脑子里想的”到“CAD里有的”这段距离。它不是把文字变成一张图片,而是变成真正的三维几何体,能导出STEP、STL,能进CAM,能继续在CAD里改特征。
1.2 什么是text-to-cad:先看一个最小例子
一句话解释:text-to-cad让模型读入一句自然语言描述,输出一个可执行的CAD建模脚本或直接输出模型数据。比如你输入“一个直径40mm、高20mm的圆柱体,中心带一个直径10mm的通孔”,理想情况下系统会生成类似这样的CadQuery代码:
import cadquery as cq result = ( cq.Workplane("XY") .cylinder(20, 40) # 高20,直径40 .faces(">Z") .hole(10) # 直径10通孔 )看起来只是简单的几个API调用,但关键在于:这段话本身是文本,而模型要从文本里提取尺寸、几何类型、特征位置、布尔关系,再映射成参数化API。跟你直接命令ChatGPT写一段“计算圆面积”的代码完全不同,这里每一步都可能产生几何语义漂移。
1.3 它真正带来的增量价值
有人问,这不就是“AI替代设计师”吗?我觉得方向完全相反。text-to-cad真正改变的是三类场景的成本结构:
- 降低入门门槛:不需要先学一个月的CAD软件,只要会说尺寸和特征,就能得到初版模型。
- 批量参数化设计:搭好提示词模板后,同类型的法兰、支架、壳体,改几个参数就能批量生成,效率是手动建模没法比的。
- 把“口头描述”变成“可追溯的模型”:很多需求沟通发生在三维建模之前,text-to-cad可以把需求描述直接转成初始几何,让需求方和设计方在同一个模型上对齐,而不是靠几张截图来回猜。
说白了,它把CAD工程师从“重复堆特征”里解放出来,让人有精力去做更复杂的解释、判断和验证。
2. 主流实现路线拆解:LLM是在“写代码”而不是“画图”
2.1 为什么中间表示选择了“代码”
我最初以为text-to-cad会让大模型直接“画”一个三维模型出来,后来发现完全不现实。CAD底层的B-rep(边界表示)和CSG树是高度结构化的几何数据,动辄上百KB,而且对精确度要求极高。让Transformer直接输出这种底层数据,相当于让一个人背出整个STEP文件的二进制格式,几乎不可能。
更聪明的做法是让LLM输出“建模指令”,也就是一段代码。代码是离散的、有语法规则的,正好是语言模型最擅长生成的东西。然后由CAD内核去执行这段代码,把几何生成交给可靠的底层引擎。所以本质上,text-to-cad不是在“画图”,而是在“写一份让CAD内核执行的设计说明书”。
2.2 两条主流技术路线:特征序列生成与程序合成
目前业内做得比较多的有两类方法,我用自己的话总结一下。
第一类是特征序列生成。这类方法不生成代码,而是直接输出一个特征操作序列,比如“拉伸一个圆柱->在顶面打孔->在圆周阵列”。每个特征用固定的参数表示,模型本质上是一个序列生成器。公开的DeepCAD、Text2CAD相关研究大致属于这个方向。优点是输出结构紧凑、相对容易训练;缺点是特征类型和草图能力受限于数据集,一旦遇到非规则断面,模型就抓瞎。
第二类是程序合成,也就是让LLM输出OpenSCAD、CadQuery、FreeCAD的Python脚本。刚才那个圆柱打孔的例子就属于这类。优点很明显:可以借用大模型已有的代码能力,且输出理论上可以表达任意复杂几何;缺点也很明显——代码本身可能跑不过,API用错、坐标系搞错、特征顺序错,样样都是坑。
我自己落地时选的是程序合成路线。原因是工程上可维护性更好:脚本是文本,可以做diff、做缓存、做静态检查,万一模型生成了怪东西,人打开代码一眼就能看出哪里出了问题。
2.3 两类代码生成方式的一个直观对比
OpenSCAD和CadQuery是两种常见的程序合成目标。
OpenSCAD的写法很像CSG:
$fn = 50; difference() { cylinder(h = 20, d = 40); cylinder(h = 20, d = 10); }CadQuery的写法更贴近参数化特征建模:
import cadquery as cq result = ( cq.Workplane("XY") .cylinder(20, 40) .faces(">Z") .hole(10) )两者都能生成同一个圆柱体带通孔。区别在于CadQuery的特征语义更强,后续可以继续在顶面加特征、倒角、镜像,比较适合需要反复修改的零件。OpenSCAD更简洁,但对复杂特征的支持弱,生成出来以后也很难跟传统CAD数据交换。所以我后来主力选CadQuery。
2.4 当前路线的天花板在哪
程序合成路线最大的问题是“几何事实”和“自然语言”之间存在鸿沟。LLM知道“圆角”这个词,但它不一定知道.fillet()选哪些边才符合用户脑子里的“四角倒圆角”。它也不知道一个轮廓是不是封闭、一个草图会不会自交、一个拉伸方向会不会导致布尔失败。这些CAD内核的隐性规则,训练语料里很少以显性约束出现,所以自然就成了错误重灾区。
理解了这一点,后面所有优化方向其实都是在补一个“几何常识层”。你可以把它理解成:LLM负责把语言翻译成大致正确的指令,但真正拍板的必须是CAD内核和校验器。这个思路贯穿后面的所有实操。
3. 复现一个最小可用的text-to-cad流程:选型、环境和代码骨架
3.1 选型:为什么我选CadQuery而不是更“AI原生”的方案
前面提到,DeepCAD那种直接生成特征序列的方案确实更“原生”,但对工程化不友好。我选CadQuery主要是因为三点:
- 它基于OpenCASCADE内核,导出的STEP文件可以直接给CAM、PLM系统用。
- 建模思路是“在平面上画草图->拉伸/切除->继续选面操作”,跟传统CAD心智模型一致。
- 有活跃的社区,API相对稳定,出了问题容易搜到答案。
如果你想快速验证效果,OpenSCAD也可以,但后续做参数化约束会绕路。我的建议是:只要不是自己玩,直接上CadQuery。
3.2 环境准备与最小化验证
先搭环境。我这里用的是Python 3.10和conda:
conda create -n text2cad python=3.10 -y conda activate text2cad pip install cadquery装完以后先跑一个最小脚本验证内核能用:
import cadquery as cq from cadquery import exporters shape = cq.Workplane("XY").box(10, 10, 10) exporters.export(shape, "test.step", exportType="STEP") print(shape.val().Volume())能输出1000.0就说明环境没问题。这一步别跳过,很多翻车其实是OpenCASCADE版本或conda依赖没装好导致的。
3.3 核心代码:LLM生成CadQuery脚本并自动纠错
接下来是最核心的部分:调用LLM生成CadQuery代码,执行它,如果报错就把错误信息回喂给LLM让它修复。下面这段骨架是我实际在跑的最小闭环,你可以直接抄走改一改。
import cadquery as cq from cadquery import exporters from openai import OpenAI client = OpenAI( base_url="http://localhost:11434/v1", # 本地模型服务地址 api_key="ollama", # 本地服务随便填 ) PROMPT = """你是资深CadQuery工程师。请把用户需求转换成可执行的CadQuery脚本。 规则: - 单位一律用毫米 - 只能输出纯Python代码,不要任何解释 - 在脚本里定义 result 变量 - 不要写 import,直接使用 cq - 默认基准面是XY平面 需求:{description} 代码:""" def execute_script(script: str, output_path: str) -> cq.Workplane: namespace = {} exec(script, {"cq": cq}, namespace) if "result" not in namespace: raise ValueError("脚本里没有定义 result 变量") exporters.export(namespace["result"], output_path, exportType="STEP") return namespace["result"] def generate_cad(description: str, output_path: str = "output.step", max_retries: int = 3) -> str: messages = [{"role": "user", "content": PROMPT.format(description=description)}] for _ in range(max_retries): resp = client.chat.completions.create( model="qwen2.5-coder:14b", messages=messages, temperature=0.1, ) code = resp.choices[0].message.content.strip() # 去掉可能出现的markdown代码围栏 if code.startswith("```"): code = code.split("```", 2)[1] if code.startswith("python\n"): code = code[len("python\n"):] try: shape = execute_script(code, output_path) print(f"体积: {shape.val().Volume()}") return code except Exception as e: messages.append({"role": "assistant", "content": code}) messages.append({"role": "user", "content": f"执行报错:{e}。请修复后重新输出完整代码。"}) raise RuntimeError("重试多次仍然失败")这里有个关键点:temperature设成0.1。写代码不是生成文案,温度越低越稳定。执行失败后的反馈信息必须包含异常内容,但不能让LLM看到整个文件系统的日志,否则容易把不该有的错误拉进上下文。
3.4 一个实测跑通的例子:法兰盘
拿一个典型的加工件试一下。用户描述:“一个外径80mm、内径40mm、厚度10mm的法兰盘,周围均匀分布4个直径8mm的安装孔,孔中心圆直径60mm。”
模型可能生成类似这样的代码:
result = ( cq.Workplane("XY") .circle(40) # 外径80 .extrude(10) # 厚度10 .faces(">Z") .circle(20) # 内孔半径20 .cutBlind(-10) # 通孔 .faces(">Z") .workplane() .pushPoints([(30, 0), (0, 30), (-30, 0), (0, -30)]) .hole(8) # 4个安装孔 )跑完后得到STEP文件,用查看器打开,能看到一个法兰形状。这一步是整个流程的“味觉测试”:如果这个例子都跑不通,说明你的模型推理能力或者提示词模板有问题,先别急着往下做功能。
4. 实测中的常见翻车现场:不是模型错了,是“语义漂移”
4.1 一次完整的翻车案例:四角圆角板
我拿一个更接近真实工作的需求来测试:“一块长100、宽50、高5的板,四角倒圆角R10,中间一个直径25的沉头孔,沉头直径40,沉头深度3。”
第一次生成的代码我简化一下:
result = ( cq.Workplane("XY") .box(100, 50, 5) .edges().fillet(10) # 错:把所有边都倒了圆角 .faces(">Z") .workplane() .hole(25) # 错:只做了通孔,没有沉头 )表面看代码没语法错误,但几何完全不对。.edges()把所有12条边都选了,倒出来的不是“四角圆角”,而是整个长方体变成一个圆角鼓包,尤其顶面和底面的周边棱线全变成了弧形。沉头孔也丢了。
4.2 定位问题的完整排查链路
遇到这种问题,我的排查顺序是固定的:
- 先打开生成的代码,做静态检查。看它调用的API是不是符合语义。
.edges()在这里几乎肯定是错的,应该用.edges("|Z")只选竖直边。 - 再执行代码,导出STEP,用CAD查看器旋转观察。这一步能最直观地发现“显示出来的形状和需求对不上”。
- 用几何校验输出几个关键指标,跟预期对比。比如打印包围盒:
bb = shape.val().BoundingBox() print(bb.xlen, bb.ylen, bb.zlen)如果是四角圆角,包围盒应该仍然是100×50×5,但如果你想验证“只有四角被倒”,包围盒不够用,得检查边数。更实用的方法是把模型导出成STL再用可视化工具截图看。
- 把“错误现象”而不是“错误代码”反馈给LLM。让模型重新生成时,不要只说“你写错了”,而是说:“用户要的是四角倒圆角,现在的模型顶面和底面四边也变成圆角了;另外缺少沉头孔(直径40,深度3)。”我实测下来,把几何差异描述清楚,修复成功率比只贴报错信息高很多。
4.3 用约束“钉死”语义:提示词里的术语表与后置校验
安抚完翻车现场后,会发现一个深层问题:自然语言本身就是模糊的。“四角倒圆角”到底是哪个视角的四个角?沉头孔是通孔还是盲孔?这类歧义不能指望LLM每次都能猜对。我的做法是两层兜底。
第一层,在提示词里加固定术语表。比如:
- “四角倒圆角”默认指垂直于厚度方向的所有棱边,使用
edges("|Z")选择。 - “沉头孔”默认使用
hole(..., cbore=..., cboreDepth=...)表达,除非用户明确说不需要。 - “通孔”默认完全穿透当前实体。
第二层,写几何校验器。生成代码执行完以后,自动检查:
def validate_shape(shape, expected_volume=None): solid = shape.val() if not solid.isValid(): raise ValueError("几何无效") if expected_volume: actual = solid.Volume() if abs(actual - expected_volume) / expected_volume > 0.02: raise ValueError(f"体积偏差过大:期望{expected_volume},实际{actual}")把校验结果作为错误信息重新喂给LLM。这一层比任何提示词都可靠,因为几何内核不会说谎。
5. 能落地和不能落地的场景:text-to-cad不是替代设计师,而是替代重复劳动
5.1 适合text-to-cad的几类高价值场景
我把跑过半年以后觉得真正有价值的场景分成四类。
- 标准件生成:M8六角螺栓、弹簧垫片、平垫圈这类东西,描述清楚以后生成效果极稳,因为结构规则、参数固定。
- 参数化改型:例如“把底座长度从100改到120,安装孔跟随向Y方向移动”,这种增量修改如果能维护好上下文,比手动改动整个特征树快得多。
- 教学演示:学生用自然语言描述设计意图,立刻得到三维模型,对理解“特征思维”帮助很大。
- 批量配置:同一类型的零件,把一组参数写进表格,逐行调用text-to-cad生成不同规格的模型,适合搭流水线。
5.2 硬边界:为什么复杂曲面和装配体很难
硬边界要讲清楚,免得你投入错了方向。
第一,复杂曲面。汽车A级曲面、鼠标外壳、人体工学手柄,这些需要曲率连续、光顺性约束的自由曲面,程序化建模脚本本身就很难描述,LLM自然也无能为力。不是LLM不够聪明,是几何表示方式不适合。
第二,装配体。哪怕只有两三个零件,也需要定义零件间的接触面、配合关系、自由度。文本描述一句话说不清,多轮对话又容易丢失上下文。我建议暂时不要把text-to-cad用在多零件装配上。
第三,制造信息。text-to-cad生成的是纯几何模型,它不含公差、表面粗糙度、热处理要求。你不可能让它直接出一张能下发车间的工程图。想用它替代工艺设计,目前不现实。
5.3 落地形态:从“一次生成”到“人机协作”
在我自己的工具链里,text-to-cad不是无人驾驶,而是辅助驾驶。正确用法是:LLM先生成一版模型,工程师在CAD里做检查和修改,然后把修改后的特征树保存成模板。下一次类似零件过来,直接调用模板,让LLM只需要填参数,不做自由发挥。
这个模型很关键。不要追求“一次生成最终模型”,而是追求“生成一版能改、能看、能用来对齐需求的初稿”。当我把心态从“AI完全替代建模”切换到“AI帮我起稿”之后,几乎所有失败案例都变得可接受了。
6. 进阶优化:从“能出模型”到“敢放进产线”的几个改造方向
6.1 先给LLM一根拐杖:封装标准函数库
基础流程跑通以后,你会发现让LLM直接写CadQuery原始API,错误率还是偏高。这时候应该封装一套业务函数库,让LLM只调用你定义好的高层函数。
比如你可以定义:
def make_plate(length, width, height, corner_radius=0): return ( cq.Workplane("XY") .box(length, width, height) .edges("|Z") .fillet(corner_radius) if corner_radius > 0 else cq.Workplane("XY").box(length, width, height) ) def make_hole(plate, diameter, cbore=None, cbore_depth=None): ...然后在提示词里明确:“你只能使用工具库里的函数,不要直接调用cq.Workplane。”这么做有两个好处:语法错误率大幅下降;模型输出的代码更像配置文件,人一眼能看懂。
6.2 加一道“质量门禁”:几何校验和参数提取
从“能出模型”到“敢放进产线”,中间必须加质量门禁。除了前面提到的isValid()和体积校验,还可以提取参数表,把模型里的尺寸自动抽出来:
import ast def extract_constants(script: str) -> dict: tree = ast.parse(script) constants = {} for node in ast.walk(tree): if isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Name) and isinstance(node.value, ast.Constant): constants[target.id] = node.value.value return constants提取出来的参数可以跟用户输入做交叉验证,比如用户说板厚5,脚本里如果出现厚度不是5,就该报警。这是最容易被忽略、但也最实用的一层。
6.3 支持增量修改:不必每次从头生成
我一开始的接口设计是:用户每次输入完描述,模型就从头生成一个完整脚本。结果发现,一个100行的脚本,只要改一个圆角半径,模型很可能把其他特征也改乱。更好的做法是把上一版脚本作为上下文,让模型生成“从旧版到新版的差异补丁”,或者明确告诉模型:“只允许修改跟需求相关的行,其余保持原样。”
我自己的做法是维护一个对话栈:
用户:一个长100宽50高5的板,四角倒圆角R10 助手:<脚本A> 用户:圆角半径改成15,其余不变 助手:<脚本B,且A中的其他部分不变>这比每次空白对话重新生成稳定得多,因为上下文里有了实际几何作为锚点。
6.4 控制成本与安全:缓存、沙箱和审计
最后两条是工程上线必须做的事。
第一是缓存。文本到CAD脚本的很多请求是高度重复的,特别是标准件。把“规范化描述+生成成功的脚本”存进数据库,下一次先做相似度检索,命中就直接用,不调用LLM。我实测命中率能做到40%以上,token开销省一大截。
第二是安全。LLM生成的代码本质上不可信,exec执行前一定要放进沙箱。至少做到容器隔离、限制文件系统访问、禁止网络请求。这个工具如果给团队用,还要保留审计日志,记录谁在什么时间生成了哪个模型,方便回溯。
如果你只是自己在本地跑,用普通Python环境也能接受,但只要多一个人用,沙箱就必须上,别在这个问题上省事。
一个我自己特别深的体会是:text-to-cad的瓶颈从来不是“大模型能不能理解图纸”,而是你有没有能力把“语言”变成“参数”、把“几何”变成“校验结果”,并且让这些信息在模型和CAD内核之间循环起来。真正让这个工具从玩具变成生产力的,不是某一个提示词写得好,而是那套自动纠错、几何校验、缓存复用和人工介入的闭环。我建议你照着上面的最小骨架先跑通一个法兰或一个板件,然后把校验和缓存加上,再去考虑复杂场景。先让一小撮完全可控的流程稳定跑起来,比一开始就想“什么都能生成”靠谱得多。