前阵子在技术社区刷到一个演示:有人用一句话“帮我做一个直径120的圆盘,中心开一个直径20的圆孔”直接在CAD工具里生成了模型。底下评论最多的不是“好酷”,而是“这玩意儿真的能改参数吗”。这个问题问到了点子上。text-to-cad(文本到CAD)这个方向,这几年确实从小众玩具变成了被认真对待的工程课题。它要解决的核心事,是把自然语言描述直接转成可编辑、可加工的CAD模型,而不是一张看着像的渲染图。
这篇文章我想从自己的视角,把这几年关注和实操text-to-cad的经验完整梳理一遍。会讲到它背后的几何本质、几种主流实现路径、一个能跑通的最小代码Demo,以及大量落地时才会遇到的坑。适合三类人看:做AI工程落地、想给大语言模型接“手”的开发者;机械和产品设计背景、想搞清楚AI建模边界在哪的设计师;以及单纯对“用一句话生成3D模型”好奇、想自己动手跑一遍的爱好者。
1. 一句话到图纸的差距,比想象中大得多
1.1 CAD不是“画图”,是一套严格的几何描述体系
先说一个最常见的误解:很多人觉得text-to-cad和文生图差不多,把描述“翻译”成画面就行。但CAD模型本质上不是一张图,而是一套精确到微米、可被程序逐层解析的几何描述。一个工业级的CAD文件里,不仅有点、线、面,还包含拓扑关系、尺寸约束、特征历史树,甚至公差信息。一根轴和孔之间到底是有0.02毫米间隙,还是过盈配合,都直接决定零件能不能装上、能不能转。
所以text-to-cad真正要做的,不是“让模型看起来像”,而是构造一个逻辑自洽、能被下游CAM软件或3D打印机直接消费的几何体。文生图场景下,模型把猫画成三条腿,人眼大概率察觉不到,或者说即使察觉了,也只是一张图的事。但在CAD场景里,一个面片的法线方向反了、一个边缘没闭合,后续就没法做切片、没法做布尔运算。这种“零容错”的特性,决定了这个方向天然比文生图更硬核。
1.2 语义的模糊性怎么量化,才是真正的分水岭
你可能会问:“一段自然语言本身就不精确,凭什么要求生成绝对精确的几何?”这就是另一个核心矛盾。说“一个大一点的杯子”,大多少?说“能放下一部手机”,手机是长165毫米、宽75毫米、厚8毫米,还是带壳后加10毫米?文生图可以让模型自己发挥,画出来的手机越大越好。但CAD不行,因为下游要建模、要装配、要开模,“大一点”必须变成一个具体的数值。
这也是为什么text-to-cad不能简单套用“文本到图像”的扩散模型路线。你要做的是在语义和参数之间搭一座桥:先把模糊描述转成明确的约束方程组,再通过求解得到精确的几何参数。这个概念我会在后面的管线里重点拆解。
1.3 为什么最近两年才真正进入大众视野
严格来说,文本驱动建模的尝试在CAD圈子已经存在很多年,早期主要靠关键词匹配和脚本模板。真正让这个方向“破圈”的,是两股力量汇合:一方面,大语言模型把“意图解析”的能力拉高了一大截,以前需要写一堆规则才能从一句话里抽出“直径”“高度”这类参数,现在模型可以直接结构化输出;另一方面,参数化建模内核和开源几何库已经足够成熟,生成一个带完整拓扑的实体模型不再是难事。两股力量一接,text-to-cad就从一个学术Demo变成了可以动手验证的工程方向。
2. 从文本到模型的完整链路:五个环节拆开看
我习惯把text-to-cad的完整流程拆成五个环节:意图识别、模板匹配、参数解算、几何构建与校验、导出后处理。每一环都有自己独立的难点,任何一个环掉链子,最终模型都会翻车。
2.1 第一步:意图识别与实体抽取
这一步要回答两个问题:用户想建什么?用户给了哪些参数?
“想建什么”是意图分类。“给了一个直径120、高度30的圆柱底座”这句话里,“圆柱”是对象,“直径120”和“高度30”是参数。常规做法是让大语言模型做结构化抽取,输出类似“{type: cylinder, diameter: 120, height: 30}”的JSON;同时用一套关键词正则做兜底,因为LLM也有抽错的时候,兜底逻辑能保证在关键短语缺失时依然有输出。
我实际试下来的经验是:把“提示词模板”写清楚比换更大的模型更有效。比如强制模型只输出JSON、字段名固定、单位默认毫米,这些约束能让抽取准确率明显提升。反直觉的是,如果你让模型自由发挥“友好地解释一下”,反而更容易出错。
2.2 第二步:匹配建模模板或生成路径
拿到意图和参数之后,下一步是决定“怎么把这东西造出来”。这里分两条路:检索已有模型库里的相似组件,和调用参数化函数现场生成。
检索适合标准件,比如内六角螺丝、深沟球轴承、铝型材。这些东西本身就有国家标准和成熟三维模型,没必要重新建模,直接从库里捞出来改改参数就好。参数化生成适合新设计,比如一个特定尺寸的杯子、一个按需求定制的外壳。这时候要有一组“模板函数”——每个函数知道某种类型对象的建模步骤、特征顺序、约束逻辑。
打个比方,模板库像菜谱:鱼香肉丝有固定做法,只是咸淡甜辣可以调;但如果你想做一道厨师没见过的菜,菜谱就失灵了。这也是目前text-to-cad和真正“通用设计智能”之间最大的鸿沟。
2.3 第三步:参数解算与约束求解
这一步是整个链路里最容易被忽略、又最影响成品质量的环节。很多输入描述根本不会直接给出全部参数,而是给出约束条件。比如“做一个能放进一个鸡蛋的杯子”,你没法直接抽到“直径”“高度”,得先知道鸡蛋的大概尺寸,再反推杯子的内径和深度。
再比如“M8的螺丝,配一个通孔”,孔的直径不能直接等M8,标准里要留间隙,通常通孔直径是8.4毫米或9毫米。这类知识不在文本里,也不在模板库里,而在约束求解层。参数解算环节就是把“模糊约束”换算成“具体数值”,必要时还要处理互相矛盾的条件,比如“总高度不超过50,但放得下一个高70的瓶子”,这就要报错或者反问用户。
2.4 第四步:几何构建与合法性校验
参数算完,终于到了真正“生成模型”的环节。不管底层用的是参数化内核还是网格布尔运算,构建完后必须做合法性校验:检查壳体是否闭合、面片法线是否一致、有没有自相交、布尔运算是否产生退化面。这一步在学术Demo里经常被跳过,但在工程里是生死线。一个看起来没问题的模型,可能内部有非流形边、微小的破面,加工设备根本不会认。
我自己的做法是生成之后强制跑三道检查:统计几何体体积是否为正值,检查边界边是否成对出现,再随机抽样若干点测试是否落在体内。体积为负基本就是面片法线反了,边界不成对说明有裂缝,抽样测试能发现内部面交错之类的隐性问题。
2.5 第五步:导出与后处理
最后一步是把几何体导出成目标格式。如果是3D打印,用STL或者3MF;如果是机械加工,用STEP;如果是激光切割,可能是DXF或SVG。每种格式的侧重点不一样:STL只关心三角形网格,STEP要保留精确曲面和拓扑,DXF则要求扁平面的导出形态。格式选错了,下游工具很有可能直接拒绝打开或者加工变形。
后处理还有一个容易忽略的点:单位。文本里可能写“50厘米”“50mm”“50cm”,不统一换算的后果是模型尺寸直接差10倍。我在Demo里会把所有输入默认当成毫米,遇到“厘米”就乘10,即便如此,我还是建议在系统里把“默认单位”显式告诉用户。
3. 技术路线对比:端到端生成、参数化生成、组件检索,到底哪条靠谱
这个方向目前没有一条路线是万能的。我把常见的几条路线放在一起对比,再说说各自的适用场景和翻车点。
| 技术路线 | 基本原理 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 端到端生成 | 用大模型直接输出体素/点云/网格 | 不需要模板库,理论上能生成任意形状 | 精度差,难以转成B-Rep,无法参数化编辑 | 快速概念草模、文创造型 |
| 参数化程序生成 | 从语义中抽取参数,调用建模内核API生成实体 | 精度可控,结果可编辑、可复用 | 依赖模板库覆盖率,没见过的东西生成不了 | 标准件异形化、外壳设计、结构件 |
| 组件检索拼接 | 从标准件库/历史模型库中检索、组合排列 | 可靠性高,符合实际制造需求 | 只能做“拼积木”,无法真正创新 | 装配体布局、标准件调用 |
| 多模态协同 | 文本加草图/图像约束生成 | 消除文本歧义,条件更明确 | 需要额外输入,不好采集 | 草图改图、图片驱动逆向建模 |
3.1 端到端生成:听着酷,工程上却最头疼
端到端这条路基本思路是把“文本编码成条件,再让生成网络直接输出三维表示”。问题出在中间表示上:体素效率低、分辨率上不去;点云松散、不是实体;网格生成看起来最接近可用,但生成出来的网格大多只适合渲染,没法缝合、没法倒角、没法做成可编辑特征。你拿一个端到端生成的“杯子”去打开特征树,里面什么都没有——它只是一个三角形壳,而不是一个“旋转体加抽壳加倒角”的历史结构。
我见过很多做AIGC的朋友在这条路上死磕,最后结论都类似:端到端生成适合做“手办造型”和“概念可视化”,但要进入工程链,还得自己做一层“网格转B-Rep”的逆向,复杂度不亚于重新造一个CAD内核。短期来看,它不是最优解。
3.2 参数化程序生成:目前最踏实的一条路
参数化这条路的思路反而更“老派”:先定义一组可参数化的模板,每个模板对应一类对象的建模步骤,然后用自然语言抽取参数去调用这些模板。好处非常直接:生成的模型天生就是可编辑的实体,特征树完整,尺寸改起来只要改参数,下游加工链路完全畅通。
代价是模板覆盖率。你准备了多少模板,就只能处理多少类对象。我自己的经验是:与其追求覆盖大量类型,不如把每一类模板的参数做深。比如“外壳”模板,不仅要有长宽高,还要有拔模角、壁厚、加强筋、螺丝柱高度等几十个参数。这样用户说“一个带USB开孔的方形外壳”时,系统能在核心模板上做特征扩展,而不是从头生成一个不可控的形状。
3.3 组件检索拼接:适合“搭积木”的场景
组件检索的核心是把“从零生成”变成“从库里选”。如果用户要的是一个带法兰的轴、一个减速箱外壳、一台标准电机座,最快最稳的方法就是检索已有模型,再根据尺寸参数缩放组合。这条路径在装配体级别的text-to-cad里特别实用,因为它天然符合工程师的思考方式:优先用成熟零件,实在没有才单独设计。
它的天花板也明显:只能做已有零部件的排列组合。用户如果说“我要一个有机曲线造型的底座”,库里没有,检索就会失败。所以这条路线的最佳形态是和其他生成方式配合,而不是单打独斗。
3.4 多模态是一条加分项
纯文本最大的问题是歧义——“一个凳子”到底几个腿?方的还是圆的?高多少?但如果用户同时画了一张草图,哪怕画得很潦草,很多歧义就能消除。多模态合并方案在实践中的表现远好于纯文本,因为草图本身就是一种粗糙的几何约束。我建议所有做text-to-cad的人,在设计输入接口时一开始就把“画一笔/传图片”作为可选项,它能把成功率拉高一截。
4. 最小可运行的文本转CAD Demo:写代码实测一遍
光讲原理容易虚,我做一个不需要任何第三方依赖、纯Python就能跑通的最小Demo。思路就用最老实的参数化生成:从文本里抽关键词和数字,调用模板函数生成三角网格,再导出一个STL文件。你跑完这个Demo,就能理解text-to-cad的核心链路,后面换成真正的CAD内核只是接口问题。
4.1 设计一个最小的模板系统
先定义两个模板:圆柱和圆台。它们的基本参数是半径、高度,分段数决定表面光滑度。下面这个函数生成一个圆柱体表面的三角形网格,注意侧面、顶盖、底盖三个部分都要覆盖,否则最后导出的STL不是闭合壳体。
import math def make_cylinder(radius, height, segments=32): top = [ (radius * math.cos(2 * math.pi * i / segments), radius * math.sin(2 * math.pi * i / segments), height) for i in range(segments) ] bottom = [ (radius * math.cos(2 * math.pi * i / segments), radius * math.sin(2 * math.pi * i / segments), 0.0) for i in range(segments) ] triangles = [] for i in range(segments): nxt = (i + 1) % segments # 侧面两个三角形 triangles.append((bottom[i], bottom[nxt], top[nxt])) triangles.append((bottom[i], top[nxt], top[i])) center_top = (0.0, 0.0, height) for i in range(segments): nxt = (i + 1) % segments triangles.append((top[i], top[nxt], center_top)) center_bottom = (0.0, 0.0, 0.0) for i in range(segments): nxt = (i + 1) % segments triangles.append((bottom[nxt], bottom[i], center_bottom)) return triangles这里三角形顶点顺序不是随便排的,它决定了面片的法线方向。侧面两个三角形的绕序都是从底部外侧指向顶部外侧,顶盖的绕序朝上,底盖朝下。法线一乱,STL文件在很多切片软件里会出现表面发黑、切片失败的问题。
4.2 自己写一个STL导出器
STL格式很简单,就是把每个三角形面片连同法向量写进文件。不需要任何第三方库,用纯Python写一个ASCII版:
def sub(a, b): return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) def cross(a, b): return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) def normalize(v): length = math.sqrt(v[0] ** 2 + v[1] ** 2 + v[2] ** 2) if length == 0: return (0.0, 0.0, 0.0) return (v[0] / length, v[1] / length, v[2] / length) def write_stl_ascii(path, triangles): with open(path, "w") as f: f.write("solid text_to_cad\n") for tri in triangles: normal = normalize(cross(sub(tri[1], tri[0]), sub(tri[2], tri[0]))) f.write(" facet normal {} {} {}\n".format(*normal)) f.write(" outer loop\n") for v in tri: f.write(" vertex {} {} {}\n".format(*v)) f.write(" endloop\n") f.write(" endfacet\n") f.write("endsolid text_to_cad\n")这个导出器只有三十来行,但已经能生成可被大多数三维查看器和切片软件正常识别的STL。这里法向量是用三角形的两条边叉积算出来的,所以前面三角形顶点的绕序必须保证一致。
4.3 把文本解析和几何生成串起来
接下来写一个非常简陋的意图解析器。真实项目里这里会接大语言模型,但为了这个Demo可以离线跑通,我直接用关键词和正则匹配:
import re def parse_intent(text): if "杯" in text: return "cup" if "圆柱" in text or "cylinder" in text.lower(): return "cylinder" if "底座" in text or "托盘" in text: return "base" return "unknown" def extract_param(text, key, default, alias=None): patterns = [key] + (alias or []) for pattern in patterns: m = re.search(pattern + r"[\s:=:]*([0-9]+(?:\\.[0-9]+)?)", text, re.IGNORECASE) if m: return float(m.group(1)) return default def build_from_text(text): intent = parse_intent(text) if intent == "cup": diameter = extract_param(text, "直径", 60.0, alias=["口径"]) height = extract_param(text, "高度", 40.0, alias=["高"]) return make_cylinder(diameter / 2.0, height, segments=48) if intent == "cylinder": diameter = extract_param(text, "直径", 30.0) height = extract_param(text, "高度", 50.0) return make_cylinder(diameter / 2.0, height, segments=32) if intent == "base": diameter = extract_param(text, "直径", 100.0) height = extract_param(text, "厚度", 10.0, alias=["高"]) return make_cylinder(diameter / 2.0, height, segments=32) return []在命令行里跑一下试试:
python text_to_cad_demo.py "生成一个直径60、高度40的杯子"它会输出一个cup.stl文件。用任意免费的STL查看器打开,就是一个带底、带盖、侧面光滑的圆柱体。虽然它还没杯壁厚度,但整个链路——从自然语言到参数,从参数到三角网格,从网格到文件——已经完整跑通了。
4.4 实测下来发现的几个问题
这个Demo跑通之后,我再往里面加功能时踩了一堆坑,值得写下来。
第一,关键词正则非常脆弱。用户说“杯子高度40,直径60”,顺序一换,正则就抽不到“直径”了。真实场景里用LLM做抽取是必须的,但LLM也会偶尔输出非法JSON,所以解析层永远是“大模型+规则兜底”双保险。
第二,单位混乱。“60”到底是多少?我默认当成毫米,但用户习惯说厘米、英寸,甚至直接说“20的孔”这种省略单位的话。Demo里不做单位换算,真实工具里一定要有显式的单位声明和换算逻辑。
第三,缺省参数全靠猜。真实输入很少把所有参数都写全,比如用户就说“一个杯子”,那我默认直径60、高度40。这个“默认值策略”看起来简单,实际上直接影响生成结果和用户预期。后来我改成缺参时反问用户,效果反而好了很多。
第四,结构简单不等于能加工。Demo生成的杯子就是一个实心圆柱,没有壁厚、没有圆角、没有拔模角。如果直接拿去加工,要么材料浪费严重,要么脱模都困难。这也印证了我前面说的:参数化模板必须把工艺参数做进模板里,而不是只搭一个大致外形。
5. 真正落地时躲不开的坑与心得
5.1 数据是最卡脖子的地方
text-to-cad方向想训练一个端到端模型,或者想打磨一个检索系统,最大的问题不是算法,而是数据。网上公开的CAD模型本来就少,很多还因为版权不能直接用;带自然语言标注的CAD模型更是稀缺。自己造合成数据吧,模型很容易学会“看起来像CAD”但实际几何完全不合格;人工标注吧,一个复杂零件的标注可能要一个熟练工程师花一整天。
我见过有些团队用程序化生成大量“带参数描述”的模型来做预训练,方向是对的,但要特别小心合成数据的分布偏差。比如只生成圆柱、方块、简单孔板,模型就学不会曲面造型;只生成100毫米以下的小零件,大尺寸建模表现就会崩。数据多样化不是数量问题,是覆盖维度的问题。
5.2 几何质量校验是生成工具的生命线
“生成出来了”和“能用了”之间差着一条河。我自己用端到端模型跑过一个手机支架,打开一看,整个模型是二十多块互相悬浮的碎片,面片之间有缝隙,法线方向乱成一团,连切片都没法切。从那之后我学乖了:无论算法多高级,输出之后必须挂一道硬性的几何校验关卡。
通用的校验项至少包含这些:壳体是否闭合;网格是否有悬挂边、重复面;法线方向是否一致;体积是否为正;布尔运算后有没有退化元素;最小特征尺寸是否满足下游工艺要求。这些校验不需要多聪明,但它能把一大批“看起来对、实际废”的结果拦在门外。做这个方向,校验模块应该和生成模块同等重要,甚至更重要。
5.3 和用户“讨价还价”比直接猜更有效
我早期设计过一个转换系统,文本缺什么参数就补默认值,结果经常生成一些让人哭笑不得的模型。后来改成对话式交互:脚本识别出用户没说“高度”,就反问一句“高度需要多少?还是用默认40毫米?”。用户给出明确数值之后,再进入生成环节。这个改动看起来简单,却把首次生成的满意率提升了不止一倍。
这背后其实是一个很朴素的道理:自然语言的信息密度有限,与其依赖模型去猜用户的隐藏意图,不如主动把缺失信息补回来。text-to-cad作为一个交互系统,天然适合做成“生成-反馈-修改”的循环,而不是一次性的文本到模型映射。
5.4 从“生成出来”到“能加工出来”还有很长的路
现在很多text-to-cad的Demo,本质上做的还是“把自然语言变成三维模型的外形”。但一个真正能进工厂的CAD模型,还需要考虑公差、装配约束、材料属性、加工工艺,比如注塑件要有拔模角,钣金件要考虑折弯半径,机加工件要留余量。这些信息用户不会写进一句话里,甚至很多用户自己都没意识到。
所以我认为下一步的进化方向不是“让模型生成更复杂的曲面”,而是“让模型理解制造约束”。一个牛掰的text-to-cad系统,应该是你说“一个铝合金的方形外壳”,它不仅生成形状,还自动把壁厚、拔模角、加强筋、螺丝柱位置全部按工艺规范排好。那才是真正能替代工程师日常重复建模工作的状态。
我把这个Demo跑通之后,最大的感受是:text-to-cad的难点,不在“文本”一头,也不在“CAD”一头,而在两端之间的约束传递。把一个模糊想法变成一组精确参数,再把参数变成一段无懈可击的几何,这个过程里每一步都藏着可以深挖的工程细节。如果你也想试试这个方向,我的建议很简单:先跑一个最笨的最小系统,把从文字到文件的完整闭环打通,再去考虑模型选型和效果优化。毕竟,能跑起来的系统才有资格谈迭代。