我第一次注意到“text-to-cad”这个词,是在一个3D打印社群里。有人发了一段话,说“帮我做一个能卡在桌沿的理线架,开口要 12 毫米宽”,然后贴出了一张渲染图。底下人第一反应是“你用什么软件画的”,结果回答是“我没画,是用 text-to-cad 生成的”。当时我还在用传统建模软件一点一点拉草图,说实话有点被刺激到。
这个技术说白了,就是让计算机根据自然语言描述直接生成可用的 CAD 模型。它和文生图最大的区别在于:图片可以“看起来像”,CAD 模型必须“真的能加工”。一个圆孔偏了 0.2 毫米,装配的时候就装不进去。所以 text-to-cad 的核心不是“生成好看的样子”,而是“生成有约束、有尺寸、能被制造系统识别的几何体”。
这篇文章我想从自己的实际体验出发,聊聊 text-to-cad 到底怎么工作、现阶段能做到什么程度,以及如何用一套最小可用的流水线,把“一句话描述”变成 STEP 或 STL 文件。不管你是 3D 打印玩家、机械工程师,还是刚开始接触参数化建模的新手,这篇内容应该都能给你一些可以落地的参考。
1. Text-to-CAD 到底是什么?先搞清楚它解决的问题
1.1 不只是一个“翻译器”
很多人第一次看到 text-to-cad,会以为它是一个把自然语言“翻译”成三维形状的黑盒。实际上,它更像是一个“需求理解 + 几何推理 + 工程约束满足”的组合问题。
比如你说“一个直径 40 毫米、高度 60 毫米的圆柱体,顶部开一个直径 10 毫米的沉头孔”,这句话里包含了几个关键信息:基本体素(圆柱)、尺寸参数(直径、高度)、特征操作(开孔)、孔的类型(沉头)。text-to-cad 需要把这些要求拆解成一系列可执行的建模操作,而不是简单地画一个像素图。
这种“结构化输出”的要求,决定了它不能靠纯视觉模型硬扛。大多数成熟的方案都会引入中间表达:要么生成一段参数化建模代码,要么生成一个可以被后处理算法转成网格的隐式几何场。理解了这一点,你就明白了为什么 text-to-cad 的评估标准里,除了“看起来像”,更重要的指标是“尺寸对不对”“能不能出工程图”“能不能被 CAM 识别”。
1.2 两条主流技术路线:代码生成与几何生成
现在市面上讨论最多的 text-to-cad 实现,基本可以分成两条路线。
第一条是代码生成路线。模型输出一段脚本,比如 CadQuery、OpenSCAD、SolidPython 代码,然后由脚本引擎执行,生成真正的实体模型。这个路线的优势非常明显:因为有代码作为中间层,每一步操作都可以被检查、修改和重新执行,生成结果是参数化的,后续改一个尺寸就能重新生成。坏处是模型得“会写代码”,而且写出来的代码得能通过语法检查和几何引擎的校验。
第二条是直接几何生成路线。模型直接输出 SDF(有符号距离场)、点云或者三角网格,再用 marching cubes 之类的算法重建表面。这条路线适合自由曲面、有机形态,比如鼠标外壳、人体工学手柄,但输出通常不容易参数化编辑,而且经常出现非流形网格、破面、缝隙,直接拿去切片或加工会比较痛苦。
在实际项目里,这两条路线经常被混着用。比如先用代码生成参数化主体,再用神经网络生成复杂曲面,最后合并成一个模型。我自己的经验是,如果目标是 3D 打印或者简单机械件,代码生成路线现阶段要可靠得多。
1.3 什么人在用,用在哪
身边的用户大致有三类。一类是创客和 3D 打印玩家,用 text-to-cad 快速产出夹具、外壳、支架这类“一次性”零件;一类是机械工程师,在概念设计阶段用它做方案验证,而不是直接出最终图纸;还有一类是刚学参数化建模的人,把“文本生成代码”当作一个快速入门辅助工具。
一个很典型的场景是:你想做一个 Raspberry Pi 的外壳,传统流程是测量板子尺寸、画草图、考虑螺丝柱和开孔位置,前前后后要折腾几个小时。用 text-to-cad,你可以直接描述“一块 85x56x20 毫米的盒子,壁厚 2 毫米,底部留四个直径 3 毫米的螺丝柱位置”,十几分钟就能得到一个能继续修改的初版模型。但请注意,我说的是“初版”,不是“终版”。把它当成一个能对话的建模助手,而不是全自动总工程师,才是现阶段最合理的心态。
2. 为什么这件事比想象中难,但仍然值得试
2.1 难点在“确定性”
大模型天生是概率系统,同一个问题问两遍,结果可能不一样。但 CAD 模型要求的是确定性:直径 10 毫米的孔,不能有时候是 9.98,有时候是 10.03。每次生成都带一点随机抖动,这在机械设计里是不能接受的。
所以 text-to-cad 的工程难点,不在于“把模型生成出来”,而在于“如何保证每次生成都在公差范围内”。这也是为什么我强烈建议采用代码生成路线:代码是可以被静态检查的,尺寸参数直接写在数值里,引擎一执行就知道是不是 10 毫米。如果代码里写的是10 + random(),那就是另一个故事了。
此外,自然语言本身就有歧义。“大一点的孔”到底是多大?“靠近边缘”是靠近多少毫米?模型需要结合上下文做合理默认,并在输出里把这些默认值明确出来。我遇到过模型把“孔径 10mm”理解成“半径 10mm”,一看实体傻眼了。所以提示词里最好要求模型“把所有尺寸显式标记在代码注释中”,这一步能省掉后面非常多的排查时间。
2.2 一个真实场景:文本生成一个手机支架
为了说明这条流水线长什么样,我用一个最简单的例子串一遍。假设我想要一个“放在桌上的手机支架,倾斜角 60 度,底部开一个充电线走线槽”。
我把这段描述发给一个能写代码的大模型,要求它用 CadQuery 生成。模型的输出大致是:
import cadquery as cq def build_model(): # 底座 base = cq.Workplane("XY").box(70, 20, 8) # 背板 back = cq.Workplane("XY").moveTo(0, -30).box(70, 60, 8) back = back.rotate((0, 0, 0), (1, 0, 0), 60) # 合并 result = base.union(back) # 走线槽 result = result.faces(">Y").workplane().slot(10, 8, taper=0).cutBlind(-5) return result这段代码能不能直接跑通,取决于模型对 CadQuery API 的掌握程度。但至少思路是对的:先建底座,再建背板,旋转出倾角,最后切一个槽。把这个 Python 脚本用 CadQuery 执行,输出 STEP 文件,再用 FreeCAD 打开微调,整个流程是走得通的。
但你如果让它用 OpenSCAD 写,输出的可能是另一种风格。OpenSCAD 更像编程语言,一个rotate([60,0,0])加上一个cube(),也能达到同样效果。选哪个引擎不重要,重要的是你熟悉哪个、后续要接哪条制造链路。
2.3 现阶段的能力边界
用了几个月以后,我给自己总结了一张“能做什么、不能做什么”的清单。
能做的包括:简单的机箱、支架、盒子、法兰、齿轮坯料,以及带圆角、倒角、孔位的基础机械特征。这类零件结构规整、尺寸明确,语言模型只要不犯糊涂,代码通常一次就能跑通。
暂时做不好的是:复杂的装配体(比如需要铰链配合、公差链分析)、A 级曲面(汽车覆盖件那种)、经过拓扑优化的有机结构轻量化件,以及需要大量行业规范判断的零件。不是说完全不能生成,而是生成结果往往需要专业工程师大量返工,省下的时间有限。
所以我的建议是:把 text-to-cad 用在“从 0 到 0.6”的设计阶段,把“从 0.6 到 1.0”留给传统 CAD 工具。它最大的价值是帮你把空白画布变成一个可以被编辑的起点,而不是替你完成全部工程判断。
3. 亲手搭一条可用的 Text-to-CAD 流水线
3.1 选型思路
既然要在本地落一条流水线,第一件事是选语言模型和 CAD 引擎。
语言模型方面,优先选代码能力强、上下文窗口大一点的。上下文窗口重要,是因为你要把 API 文档片段、用户需求、历史报错信息都塞进对话里。窗口太小的模型,塞不进参考文档就容易胡说。如果隐私要求高,可以用本地模型,比如 Qwen 系列、Llama 系列,配合 Ollama 跑起来;如果不在乎数据出网,用云端接口也省心。
CAD 引擎方面,我个人推荐 CadQuery。原因很简单:它是 Python 生态,大模型对 Python 语料的学习量远大于其他脚本方言,生成代码的准确率更高;而且它直接输出 STEP 和 STL,对接 FreeCAD、PrusaSlicer、Cura 都很方便。OpenSCAD 我也试过,但它的函数式语法对语言模型来说踩坑率稍高,特别是$fn、union()这类约定俗成的写法不够显式。
3.2 基础代码骨架
下面这个脚本是我常用的最小实现:输入一段自然语言描述,经过大模型生成 CadQuery 代码,然后执行并输出 STEP/STL 文件。为了演示,我用的是云端接口,实际使用请替换成你本地的模型服务。
import re import os import subprocess import sys import json from openai import OpenAI client = OpenAI( base_url="http://localhost:11434/v1", # 这里可以替换为本地 Ollama 或云端 api_key="EMPTY" ) def build_prompt(user_request: str) -> str: return f""" 你是一个 CadQuery 建模专家。请根据用户描述,生成一段 Python 代码。 要求: 1. 使用 cadquery 库,定义函数 build_model(),返回一个 cq.Workplane 对象。 2. 所有尺寸必须使用毫米,并在注释中标明关键尺寸。 3. 只能使用 cadquery 已存在的 API,不要编造不存在的函数。 4. 输出格式:代码块 ```python ... ```。 用户需求: {user_request} """ def extract_code(raw: str) -> str: match = re.search(r"```python\n(.*?)```", raw, re.S) if match: return match.group(1).strip() # 如果模型没按代码块输出,就直接返回原文 return raw.strip() def run_code(code: str, work_dir: str) -> None: script_path = os.path.join(work_dir, "generated_model.py") with open(script_path, "w", encoding="utf-8") as f: f.write(code) # 使用 subprocess 隔离执行,防止依赖冲突 subprocess.run( [sys.executable, script_path], cwd=work_dir, check=True, capture_output=True, timeout=120, ) def text_to_cad(prompt: str, output_dir: str = "output"): os.makedirs(output_dir, exist_ok=True) user_prompt = build_prompt(prompt) resp = client.chat.completions.create( model="qwen2.5-coder:14b", messages=[{"role": "user", "content": user_prompt}], temperature=0.2, # 低温度,保证确定性 ) code = extract_code(resp.choices[0].message.content) print("生成的代码:\n", code) run_code(code, output_dir) print("完成,结果在", output_dir) if __name__ == "__main__": user_desc = sys.argv[1] text_to_cad(user_desc)你可能注意到,temperature我设成了 0.2。这一步很关键。太高的温度会让模型“发挥”,生成尺寸和结构飘忽不定;太低则会让输出过于机械。0.2 左右是我试下来兼顾稳定和灵活的一个区间。另外,timeout=120要保留,因为某些模型在复杂布尔运算时生成的代码可能会陷入死循环。
3.3 让提示词模板安抚“幻觉”
Text-to-CAD 最常见的失败方式,不是模型不懂几何,而是模型“一本正经地编造 API”。比如它会输出cq.Workplane().sphere(10, 50),实际上 CadQuery 里没有这种写法;又比如它把box()的参数从 (长, 宽, 高) 记成 (中心点, 尺寸列表)。
为了压制这种幻觉,我在系统提示词里固定加了一段“API 记忆锚点”:
CadQuery 常用 API: - Workplane("XY").box(length, width, height) - Workplane("XY").circle(radius) - Workplane("XY").workplane(offset=10).hole(diameter) - Workplane("XY").union(other) - Workplane("XY").faces(">Z").workplane().cutBlind(depth)这段内容不需要完整,但要把最容易写错的基础函数列出来。模型在生成时,会把这些作为“事实约束”,而不是自己发挥。实测下来,加了这段锚点之后,代码的可执行率从不到 50% 提升到了 80% 左右。另外,输出格式也最好强制:让模型只返回一个 Markdown 代码块,不要附带解释性文字,这样解析起来最省事。
3.4 执行与文件输出
生成代码后,我会把它写进一个独立目录,再在 subprocess 里执行。这一步有两个原因。
一是隔离依赖。本机可能装了多个版本的 Python 库,直接把生成代码 import 到主进程里,容易污染环境。二是安全。让模型生成的代码在子进程里跑,即便它写出os.remove之类的东西,摧毁的也只是临时工作目录,不会动到主程序。如果你更谨慎,可以用 Docker 单独开一个只读根目录的容器执行。
执行完以后,工作目录里通常会有 STEP 和 STL 两个文件。CadQuery 默认的导出函数是cq.exporters.export(result, "model.step")和export(result, "model.stl")。脚本里最好把这两个都导出。STEP 拿去做工程评审和精密加工,STL 直接丢进切片软件打样,两边都不耽误。
3.5 快速验收:别只盯着渲染图
生成文件之后,我强烈建议做一次“自动验收”,不是肉眼看一眼就完事。最简单的方法是用trimesh加载 STL,检查网格是否流形、体积是否合理、包围盒尺寸是否贴近预期。
import trimesh mesh = trimesh.load("output/model.stl") print("包围盒尺寸(mm):", mesh.extents) print("体积:", mesh.volume) print("是否为水密模型:", mesh.is_watertight)这个脚本能帮你捕捉两类问题:一是模型单位错了,比如实际尺寸比预期大了 25.4 倍,包围盒一出来就能发现;二是网格破洞、不可打印,is_watertight直接告诉你该不该回炉。我一般会在验收脚本里加一条断言:包围盒的每一项都必须在预期的 95% 到 105% 之间,否则直接抛错,让程序自动触发一次“修正对话”。
4. 我踩过的坑和排查笔记
4.1 单位错位:一切都对,就是尺寸不对
有一次我让模型生成一个“宽 80 毫米的盒子”,输出的 STL 在切片软件里显示宽度是 2032 毫米。翻了代码发现,模型把所有长度都写成了3.149这种值,明显是把 CAD 内部的“英寸”习惯带进来了。这种问题特别隐蔽,因为模型代码里没有单位,只有数字,你不把尺寸打印出来根本看不出来。
解决方法是双管齐下。一方面在提示词里反复强调“所有尺寸使用毫米”,并且在代码注释里要求写出关键尺寸。另一方面,验收脚本里对包围盒做断言。只要有一次尺寸超差,就自动把测得值和预期值喂给模型,让它重新生成。把“尺寸校验”做成年年都能自动跑的一条检查,比每次都靠人眼靠谱得多。
4.2 布尔运算让人怀疑人生
CadQuery 里cut()、union()偶尔会失败,报错信息往往晦涩难懂。我遇到最多的一种情况是:两个体只在一个边缘上轻微相交,浮点误差导致布尔运算找不到有效相交区域,直接抛ValueError: Failed to find intersectable。
这类问题的排查思路是先把布尔操作改成最简单的验证,比如用一个临时脚本把两个体分别导出,在 FreeCAD 里手动观察相交情况。如果你对几何有信心,可以用fuse()替代union(),用cut()之前先intersect()预览一下。还有一个更实用的技巧:在操纵实体之前,给所有尺寸加一个微小间隙,比如把底座加厚 0.1 毫米,让相交体积足够大,运算成功率会高很多。
4.3 大模型的“幻觉 API”是个常态
我统计过自己一星期内生成的 100 次代码,大约有 18 次会调用不存在的函数,最常见的包括Workplane().rect()、Workplane().extrude()的参数写反、cq.Vector()和cq.Workplane()混用。不是说模型不会,而是它在给不出精确记忆的时候,倾向于编一个“听起来合理”的函数名。
应对这个问题的核心手段是“把文档喂进上下文”,而不是靠模型背诵。我在提示词里嵌过的 API 参考片段包括:Workplane(),box(),cylinder(),sphere(),hole(),cutBlind(),union(),rotate(),faces(),workplane(),edges(),fillet()这些核心方法的签名。你不用贴完整文档,太长反而会稀释注意力,选高频操作就够了。
4.4 常见问题速查表
下面这张表是我自己整理的,每次翻车后都会回填一行。你现在拿去用,大概率能少走一半弯路。
| 现象 | 最常见原因 | 处理方法 |
|---|---|---|
| STL 尺寸放大 25.4 倍 | 模型混入英寸单位 | 提示词强制“毫米”,验收脚本断言包围盒 |
| 布尔运算报错 | 两个实体仅边角接触 | 将接触面尺寸微增 0.1mm,或用fuse() |
| 代码调用了不存在的函数 | 幻觉 | 在提示词注入 API 签名片段,提供几个正确示例 |
| 输出文件无法切片 | 网格不是水密模型 | 用trimesh.is_watertight检查,要求模型重新生成 |
| 模型有破面/缝隙 | 高精度网格未合并顶点 | 导出时设linear_deflection=0.01,angular_deflection=0.1 |
| 底部孔位偏移 | 坐标系参考面搞错 | 代码里显式用faces(">Z").workplane(),不要默认XY平面 |
| 生成的模型空壳 | 没有执行export() | 在提示词要求代码块末尾必须包含导出指令 |
4.5 让模型自己修错
与其手工改代码,我更常做的是把报错信息原封不动地塞回对话上下文。比如脚本执行抛了AttributeError: module 'cadquery' has no attribute 'box',我把这行错误贴进模型对话,紧跟着一句“请检查并更正代码”。几次下来,模型就能自己定位到 API 拼写和参数问题。
但你要注意,多轮修正会累积上下文,模型可能在修好一个错误的时候引入另一个新错误。所以我会做一个“最多修正三轮”的限制,超过三轮就从头开始,用新会话重新生成,而不是让它无限打转。这个机制看起来简单,实际上能让最终的输出质量稳定不少。
5. 从 Text-to-CAD 继续延伸
5.1 接上 3D 打印与数控加工
Text-to-CAD 生成的模型,最直接的出口就是增材制造和减材制造。STL 文件可以直接拖进 Cura 或 PrusaSlicer 切片,但前提是网格必须水密。如果你的目标是 CNC 加工,那最好用 STEP 文件,毕竟 STEP 携带精确的边界表示,CAM 软件做刀路规划时信息更全。
我建议在流水线里同时保留 STEP 和 STL 两个输出,内部用 STEP 做验收,外部用 STL 做切片。另外,如果模型是给 3D 打印用的,还要注意最小壁厚。语言模型经常生成 0.5 毫米甚至 0.2 毫米的薄壁,打印出来一碰就碎。所以在提示词里加一句“请确保壁厚不小于 1.2 毫米”能省掉很多返工。
5.2 和 AI 编程工具组合使用
我自己更喜欢把 text-to-cad 和 AI 编程工具配合起来。比如用 AI 编程助手打开生成的 CadQuery 脚本,让它在我要加圆角的地方直接改代码;或者让 text-to-cad 批量生成一系列参数化零件,然后我用循环脚本统一导出。这样单个零件的效果或许还不够惊艳,但胜在速度快,适合做系列化探索。
一个特别有用的组合是:把常用特征抽象成自己的函数库,比如add_screw_boss()、add_rib()、add_snap_fit(),然后让模型在生成代码时优先调用这些函数。你只需要在提示词里贴一份函数签名,模型就会像拼积木一样组合。这相当于把一个通用 CAD 引擎变成你私有的设计语言,非常值得一试。
5.3 一个小技巧:让“模板填充”替代“自由发挥”
最后分享一个我反复使用的小技巧。如果某类零件你已经做过很多次,不要每次都用自然语言让模型自由写代码,而是把上次成功的代码拿过来,挖掉其中几个关键参数,变成一个带__PLACEHOLDER__的模板,再让模型只负责填数字。
比如:
def build_model(): result = ( cq.Workplane("XY") .box(__LENGTH__, __WIDTH__, __HEIGHT__) .edges("|Z").fillet(__FILLET_RADIUS__) ) # 在顶部开孔 result = (result .faces(">Z").workplane() .hole(__HOLE_DIAMETER__) ) return result让模型以这个模板为基础,根据你的自然语言描述填参数。这样做的好处非常明显:API 调用正确率接近百分之百,因为模板本身就是你验证过的代码;你只需要关注数值是否符合需求。我在项目里把常用的盒子、法兰、托架都做成了这样的模板,然后 text-to-cad 就从“试错生成器”变成了“参数填充器”,可靠性一下子完全不同。
我在实际使用中的体会是,text-to-cad 不会替代 CAD 软件,但它确实把“从文本到第一版模型”的距离压缩到了让人上瘾的程度。你不需要背下每个建模命令,只要能把需求拆清楚,剩下的交给模型去试探,你再去做那个最终把关的人。这种工作方式真的很值得尝试,尤其是那些需要快速验证想法的个人项目和原型开发。