ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

text-to-cad深度解析:用自然语言生成参数化OpenSCAD代码的工程实践

text-to-cad深度解析:用自然语言生成参数化OpenSCAD代码的工程实践

说实话,最近几个月我一直在关注一个方向:直接用自然语言让AI生成CAD模型。当“text-to-cad”这个项目在开源社区火起来的时候,我第一反应是“又一个文生3D的玩具”,但真正看完它的实现思路之后,我发现事情没那么简单。它不是用扩散模型直接生成网格,而是让Llama语言模型输出OpenSCAD代码,把自然语言翻译成可参数化、可加工的工程模型。这个思路一下子就戳中了我的痛点——做机械设计的人都知道,网上的文生3D模型大多只能看不能用,真正要下料、要加工、要装配的时候,必须回到参数化CAD里重新画一遍。而text-to-cad给出的答案是:与其直接生成不可编辑的网格,不如生成一份“图纸代码”,让模型在代码层面做设计。

这篇文章我会把它拆开讲透,包括整个项目的设计逻辑、模型训练机制、本地部署的实际操作、实测效果,以及我踩过的坑。无论你是搞机器学习的、做机械设计的,还是玩3D打印的创客,这篇东西都能给你一些可复现的参考。

1. 为什么“生成代码”比“生成网格”更接近CAD的本质

1.1 文生3D模型的老问题:能看不能用

过去两三年,AI文生3D的进展其实很热闹,像各种扩散模型直接生成NeRF、SDF或者三角形网格,输入一句“一个带圆角的金属盒子”,几分钟就能给你一个三维模型。但真正用过这些工具做工程的人应该都有同感:生成出来的模型精度一塌糊涂,没有尺寸约束,没有特征树,圆角倒不了,孔位对不上,最致命的是——你在建模软件里根本没法继续编辑它。它就是一个“雕塑”,不是“设计”。

设计是什么?设计是约束的表达。我告诉你的不只是一个形状,而是“直径5毫米的孔、中心距30毫米、壁厚2毫米”。这些约束在CAD里对应的是特征、尺寸、参数关系。如果跳过这些直接出网格,等于把设计过程和结果一起丢弃了。text-to-cad选择生成CAD代码,本质上是把约束用代码的形式表达出来,让形状逻辑本身是可追溯、可调整的。

1.2 为什么偏偏是OpenSCAD

OpenSCAD是一个程序化建模工具,它的建模方式不是鼠标拖拽,而是写代码描述几何体。比如你写cylinder(r=5, h=20),再写一个translate([...])去移动它,最后用difference()做布尔减运算,一个带孔圆柱体就出来了。整个过程非常符合“程序合成”的范式——生成目标就是一段Python或Lisp风格的程序,而语言模型做程序合成早就有成熟经验了。

更重要的是,OpenSCAD代码天然是参数化的。它写的不是死模型,而是“参数+操作”的组合。AI生成一段代码,你改个变量值,尺寸就变了,这在工程上意义巨大。你说“M3螺丝”,它生成的不只是一个螺丝网格,而是一段能控制螺纹直径、螺距、头部长度的代码。你就这么理解:传统文生3D给你一条鱼,text-to-cad给你一根鱼竿,还是可以自己调节长度的鱼竿。

1.3 从自然语言到代码的映射,难度其实比想象中高

把“一个40x30x10毫米的盒子,上面四个角各有一个直径3毫米的孔”翻译成OpenSCAD代码,看起来简单,但真让语言模型做,有几个难点:

  • 尺寸推理:语言里“四个角”“直径3毫米”这种空间表达,要映射到坐标、参数上,需要一定的几何推理能力,模型必须理解每个特征在三维空间中的相对位置。
  • 操作顺序:先画主体还是先画孔?哪个特征先执行布尔运算?顺序错了模型就变形。
  • 语法约束:OpenSCAD有很多函数调用规则、括号匹配问题,生成过程中一旦语法出错,整个模型直接无法渲染。

这正是text-to-cad投入大量强化学习来训练的原因——靠基础预训练模型直接生成,开十次能对一次就不错了。

2. 模型架构与训练逻辑:强化学习如何教会模型“写图纸”

2.1 底模选择:为什么是Llama不是扩散模型

text-to-cad的初版v0是用Llama-3.2-3B做底模,后面v1上到了8B和7B的规模。选语言模型而不是扩散模型,核心原因有两个:第一,它的输出目标本来就是代码,而代码是离散的序列,这正是LM最擅长的生成域;第二,语言模型有更强的指令跟随能力,能够理解“直径”“中心距”“壁厚”这种精确的约束术语。扩散模型做的是连续空间的映射,很难保证数值精确性。

底模选Llama-3.2而不是更大的模型,我推测官方是综合了推理速度、显存开销和开源许可的考虑。3B参数量的模型做推理,一张RTX 3060 12GB就能带起来。这个门槛对工程师和创客群体极其友好,如果非得要70B的模型才能跑,那这个项目就只能停留在论文层面了。

2.2 最核心的部分:带编译器反馈的强化学习

这里我要重点说一下训练机制,因为这是text-to-cad区别于其他文生CAD论文的关键。多数研究做text-to-CAD用的是监督微调(SFT),也就是喂大量(text, code)对,让模型模仿。但text-to-cad在SFT基础上加了强化学习,并且奖励不是来自人类打分,而是来自OpenSCAD编译器本身。

具体来说,奖励信号分成了几个维度:

  • 语法正确性:生成的代码能不能通过OpenSCAD的语法检查。
  • 格式正确性:是不是输出了有效代码块,有没有夹杂解释性文字。
  • 渲染可行性:代码对应的几何体能不能成功渲染出模型,有没有引发异常。
  • 目标对齐:通过一些几何检查函数,判断渲染出来的模型尺寸是否匹配输入的自然语言描述。

这个思路是非常聪明的做法。传统强化学习需要人工标奖励,而这里编译器就是自动判官。只要能渲染、尺寸对,就给高分;语法报错就压低分。这等于让模型在“写代码→被编译器骂→改代码”的过程中自我进化。一个开源项目能用这种训练方法,说实话,工程量和算力投入都不小。

2.3 数据从哪来:合成数据与代码重组

训练数据的构建,也是这个项目非常值得研究的部分。公开的(text, CAD代码)配对数据集很少,因为建模工程师不会闲到把自己每个模型都配上自然语言描述。所以官方的做法是两条腿走路:

  • 从现有CAD模型库中提取OpenSCAD代码,反推生成对应的文本描述。比如分析代码里的参数值、特征操作,生成“带六角头的螺栓”“直径10mm的圆柱销”这类标注文本。
  • 合成数据增强。用规则生成器创建大量参数化模板,把尺寸、位置、数量随机变化,自动生成对应的自然语言描述。这样就能在有限种子数据基础上,腾讯式地放大出几万、几十万的训练对。

正是因为数据里有大量的参数变化,模型才能学到“尺寸是变量”这种概念,而不是死记硬背形状。

2.4 v0到v1:从只能“看”到更适合“用”

官方在v1升级时做的一个关键改动是扩大了API函数库。v0阶段模型主要会生成OpenSCAD的基础语法,比如cube、cylinder、translate、difference这些,写出来的东西偏教学风格。v1开始引入BOSL2(Belfry OpenSCAD Library)的函数,比如cuboid、cyl、spherical等,BOSL2是社区里功能最完善的参数化建模库,支持圆角、倒角、螺纹等高级特征。

这一步很重要——基础OpenSCAD语法画个方盒子很容易,但你要画一个带圆角、带卡扣、带螺纹的工程零件,用基础语法要写十几行hull和minkowski,而用BOSL2一个带参数的调用就搞定了。v1让模型从“能画简单几何体”进步到了“能画接近真实加工件的模型”,这是工程可用性的一个巨大跨越。

3. 本地部署完整记录:环境、模型文件与推理参数

3.1 部署前先想清楚一个问题:你要拿来干什么

我在动手之前先明确了自己的使用场景:生成一些早期的概念模型,然后导入OpenSCAD做进一步修改,最后导出STL给3D打印切片。这个定位决定了我的部署要求——本地运行、可批量测试、最好还能自己微调。如果你只是想在线玩玩,huggingface的demo空间就够了,没必要折腾本地。但如果你想把它接进自己的工作流,比如用API批量生成几百个变型零件,本地部署是必须的。

3.2 环境准备:其实比你想象的简单

项目依赖没有想象中那么重,主要就是PyTorch、Transformers和OpenSCAD。

# conda环境建议用Python 3.10或3.11 conda create -n text2cad python=3.11 -y conda activate text2cad pip install torch transformers huggingface_hub # 渲染和合法性检查依赖系统OpenSCAD sudo apt install openscad # Debian/Ubuntu

有一点要说清楚:OpenSCAD装到系统里不只是为了让你渲染看效果,它在训练和推理阶段还充当“编译器验证器”。模型生成代码后,会用OpenSCAD做一次离屏渲染,检查语法和几何合法性。所以这个依赖不是可选项,是必装项。

3.3 模型下载与目录组织

模型在HuggingFace上,Zebra团队的仓库名是Zebra-T2C/Text-to-CAD-v0-3B,v1系列是Zebra-T2C/Text-to-CAD-v1-8B和7B。第一次跑之前最好确认一下HF的下载凭证,有些模型仓库需要同意用户协议,命令行直接下载会报403。我用huggingface_hub下载后手动指定了缓存目录:

export HF_HOME=/mnt/data/hf_cache

这样模型文件会统一落在指定盘,不占用系统盘空间。v1的8B模型如果用FP16加载,显存需求大概是16GB,一张RTX 4080或者4090可以消受;如果你用的是3090,建议直接上8B的量化版本或者退回v0的3B模型。

3.4 推理脚本:最核心的是采样参数

推理时你可能会习惯性地把temperature调到0.7或者1.0,这在通用对话里没问题,但在代码生成场景里是大忌。代码生成必须低随机性,否则很容易出一堆语法错误。我的实测最佳区间是temperature=0.2、top_p=0.9,配上beam search数量5。这个组合在多样性和正确性之间是比较稳的平衡点。

from transformers import AutoTokenizer, AutoModelForCausalLM import torch model_path = "Zebra-T2C/Text-to-CAD-v1-8B" tokenizer = AutoTokenizer.from_pretrained(model_path) model = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype=torch.bfloat16, device_map="auto" ) prompt = "M3 hex head screw with 6mm length and 3mm diameter" messages = [ {"role": "system", "content": "You are an expert OpenSCAD designer."}, {"role": "user", "content": prompt} ] text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) inputs = tokenizer(text, return_tensors="pt").to(model.device) out = model.generate( **inputs, max_new_tokens=1000, temperature=0.2, top_p=0.9, num_beams=5, do_sample=True )

我自己实测,生成一条中等复杂度的模型,3B模型在RTX 3060上大约需要20-40秒,8B模型在4090上大概10-20秒。这个速度在交互场景里可以接受,但如果你要批量生成几十个零件,建议用vLLM做服务化部署,吞吐量会好很多。

3.5 验证生成结果:别迷信“看起来像”

生成代码后一定要自己做两重验证:第一重,把生成的代码喂给OpenSCAD渲染,看能不能出图;第二重,检查关键尺寸是否符合预期。大家最容易偷懒的是只看“渲染出来是个螺丝”,不看直径和长度到底对不对。做设计的人都知道,尺寸错了,螺丝就是废铁。

4. 实测案例:从简单螺丝到工程零件的表现

4.1 经典入门:M3六角头螺丝

我首先测的是M3六角头螺丝,长度6mm、直径3mm。官方演示里,v0就能完成这个任务,但我实测发现v0生成的代码是“画一个圆柱+一个六棱柱”的基础版本,看起来还行但不够精确——六角头的对边距离没标,实际上做出来的东西没法装配。

v1版本的输出有了明显提升,它会调用BOSL2的screw()函数,自动生成螺纹特征。这里我要说一句公道话:v1版本的“螺丝”从视觉上已经非常接近真实可用的3D打印件了,螺纹的齿形清晰可见,头部尺寸写在参数里。这才是“可用”的开始。

4.2 中等复杂度:带孔法兰盘

第二步我测了“直径40mm带4个直径5mm通孔的法兰,孔距30mm”。这个描述里有几个关键信息:主体直径、孔直径、孔数量、位置分布。v1模型生成的代码至少做了三件正确的事:

  • 用cylinder生成主体,
  • 用线性分布或极坐标循环生成四个孔位,
  • 用difference把孔从主体中减掉。

我得到的结果是四个孔均匀分布在30mm的圆周上,尺寸无误。这已经不是一个“玩具级”的表现了,它证明了模型确实理解了“阵列”“布尔运算”这些工程概念,而不只是背下了螺丝的代码模板。

4.3 失败案例:混淆尺寸单位与对称性丢失

当然,没那么完美。我特意测了一个歧义性很强的描述:“一个120mm长的滑动导轨,两侧带安装孔”。结果生成代码里出现了“长120宽10高10”这种混用尺寸的典型错误——导轨的长度方向被理解成了X轴方向,孔距和导轨总长的比例也完全失调。

这个失败让我意识到一个关键局限:模型对空间关系的理解,目前还停留在“有孔”“有槽”这种特征级粒度,对“哪里开孔”“怎么排布才合理”这种工程常识仍然薄弱。所以在使用它的时候,最好把位置关系在描述里写得很明确,甚至带上坐标系提示,比如“沿X方向排列”“在中心距20mm的位置”。

5. 踩坑记录与排查思路:代码生成的魔鬼在细节里

5.1 输出被截断导致OpenSCAD直接报错

这是我遇到的第一个高频问题。生成的代码超过了max_new_tokens,OpenSCAD拿到半截代码,当然直接语法报错。解决方式除了调高token上限,更重要的是在prompt里明确要求“只输出代码块,不要解释”。如果模型在回复末尾加一句“This is a simple part”,解释性的文字混进代码,渲染直接崩溃。

我的处理是用正则把输出中的代码块内容抽出来,再喂给OpenSCAD:

import re def extract_code(raw_output: str) -> str: # 匹配```openscad ... ```块 pattern = r"```(?:openscad)?\s*(.*?)```" blocks = re.findall(pattern, raw_output, re.DOTALL) if blocks: return blocks[-1] # 取最后一个代码块,通常最完整 # 没有代码块标记时,去掉show品位前后杂讯 return raw_output.split("\\n")[0]

这一步看起来基础,但能帮你过滤掉80%的渲染失败。你要是直接拿模型的原始输出去做离线渲染,报错率会高到怀疑人生。

5.2 采样温度过高引发语法错误激增

我在调参阶段做过一个对比,temperature=0.7时生成10次,只有3次能成功渲染;temperature=0.2时生成10次,8次成功。这不是玄学,是因为beam search在高随机性下容易陷入括号匹配失败、函数名拼写漂移这类问题。文本生成多几个错别字没关系,代码生成多一个括号错,整体就崩了。

如果你确实想要更多样化的输出,建议保持低temperature、但增加beam search的候选数量,这样既保证语法正确,又能让模型在多个候选里挑一个最好的。

5.3 OpenSCAD渲染失败的根因:不是代码问题,是库缺失

另一个很容易混淆的坑——生成的代码调用了BOSL2函数,但你的OpenSCAD没有安装BOSL2库。这时候渲染报错不是语法错误,而是“找不到模块”。网络上很多反馈说是“模型生成的代码质量差”,其实根本原因是少了库。解决办法很简单:

# 将BOSL2克隆到OpenSCAD库搜索路径 git clone https://github.com/BelfrySCAD/BOSL2 # 库目录一般在 ~/.local/share/OpenSCAD/libraries/

我在排查这个问题时走了不少弯路,所以特别写出来:任何涉及BOSL2调用报错的情况,先去检查库文件是否在搜索路径中,再回头看代码逻辑。

5.4 批量生成场景下的显存管理

如果你像我一样写了循环测试脚本,反复加载模型推理,可能会遇到显存碎片化导致OOM。建议每次运行前清一次PyTorch缓存,或者直接用vLLM做服务化推理。实测在8B模型上,12条请求并发,单条返回速度几乎不受影响,显存占用也稳定在16GB以内。

python -m vllm.entrypoints.openai.api_server \ --model /mnt/models/Text-to-CAD-v1-8B \ --dtype bfloat16 \ --gpu-memory-utilization 0.85 \ --max-model-len 4096

6. 我的判断:text-to-cad这类工具会在哪里真正落地

6.1 它改变了“AI生成模型”的复用价值

把生成结果从网格变成代码,这件事的连锁反应很大。首先是复用性——网格模型你拿到手基本就是死的,不想改也改不了;代码模型你拿到手是整个设计历史,任何一个尺寸都可以改。其次是工程追溯——网格没有特征信息,出了问题没法问“为什么这里有个孔”;代码有,你可以逐行审查逻辑,找出设计错误。

我身边越来越多的创客和机械工程师开始把这类工具当“方案草稿器”来用:先让AI出一版满足基本约束的代码,人再在OpenSCAD里调参数、改特征。相比从零开始Ctrl+N新建文件,这个过程节省的时间非常可观,尤其是画标准件、常见连接件这类重复性劳动。

6.2 对制造业标准化流程的可能冲击

现在很多非标自动化公司,大量时间花在画铝型材支架、钣金盒、法兰盘、电机安装座上。这些零件结构相似、参数不同,本质上就是“文本描述+参数表”就能说清楚的事。text-to-cad如果在这些细分领域积累足够数据,未来完全可以做成“订单描述直接出模型”的自动化工序。当然,这取决于模型在这些专项数据上的精度能不能达到图纸级标准——至少目前还差一口气,但方向上我判断是通的。

另外要说一句,BOSL2支持螺纹、齿轮、轴承座等大量工程特征,而v1模型已经开始调用这些库函数。一旦模型对库的覆盖率提升,生成的零件可以直接进入3D打印切片或者CAM软件走一遍工序,这个闭环就成了。

6.3 给想自己上车的人三个建议

第一,别想着一步到位。先从标准件、简单结构件开始测,摸清它能做什么、不能做什么,再逐步扩大场景。

第二,prompt写作要像给实习生下需求一样清楚。描述里带上“毫米”“中心距”“沿X方向排列”等约束词,输出质量会明显上升;光说“好看一点的支架”,模型就给你一个“好看但没法装配的支架”。

第三,寄存器级别的调参不要迷信默认值。如果你是接入自动化流程,强烈建议把temperature设为0.1到0.2,同时固定随机种子,这样每次输出更可预测,排查问题时也更容易定位到是prompt问题还是模型问题。

最后一个私货:我试过把生成代码裁剪成模板,保留其中的尺寸变量,做成一个带参数输入的OpenSCAD库。这样就等于把text-to-cad输出的“一次性设计”升级成了“可复用组件库”,对新项目的启动效率提升非常明显。这也是我目前认为这个项目最有实用价值的使用方式——不是替代设计师,而是把重复劳动的下限兜住,把人的时间留给真正需要判断力的地方。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进