让 LLM 操作 CAD:四条技术路线的取舍
给 agent 接 CAD 能力这件事,我一开始把重心放错了。我以为难点在"让 LLM 听懂'画一个 300×200 的钢板'这种话",于是花了很多时间调 prompt、设计 JSON schema。两周跑下来发现,语义理解是整条链路上最不值一提的一环------真正的坑全在 CAD 那一侧:坐标为什么非得是 VARIANT、插件为什么必须放安装目录、三维建模为什么只能在主线程、几十万张 DWG 凭什么这么难读。这些脏活,跟 LLM 一点关系都没有。
所以这篇不写"选型结论表"那种东西,就把我按什么顺序试、在哪翻了车、最后怎么拍板的过程记下来。想让 agent 画图的人照着这个顺序走一遍,能少踩一半坑。
一、我原来的计划,和它是怎么被打脸的
动手之前我的设想特别简单:让 LLM 输出一个 JSON(长宽高、材料、倒角),再用一个 Python 库把 JSON 渲染成图纸文件,齐活。这个链路我两天就跑通了,用的是 ezdxf:
python
import ezdxf
doc = ezdxf.new("R2010")
msp = doc.modelspace()
msp.add_lwpolyline([(0,0),(300,0),(300,200),(0,200)], close=True)
doc.saveas("plate.dxf")
"画个钢板"从一句自然语言变成一张 DXF,十分钟的事。
但跑了没多久我就意识到两个问题,而这两个问题决定了一切:
第一,生成文件 ≠ 操作软件。设计师真正的工作流是开着 CAD,图要出现在他眼前的窗口里,他还要能继续改。你丢给他一个 DXF 文件,他得手动打开、手动检查,等于把最该自动化的那一步又踢回给了他。
第二,存量图纸是 DWG,不是 DXF。DWG 是 Autodesk 的私有二进制格式,ezdxf 根本读不了。手头几十万张历史图纸,全是 DWG。
于是需求被拆成三块,三块对应的是完全不同的技术栈:
- 生成图纸文件 → ezdxf 能解决
- 控制正在运行的 CAD → 得用各软件的二次开发接口,一条软件一条路
- 读存量 DWG → 得先转格式,再解析
从这儿开始,"让 LLM 画图"才真正变成"让 LLM 操作 CAD",脏活全在后面两块。
二、先捡最软的柿子:ezdxf
ezdxf 放第一位试,不是因为它重要,是因为它便宜:纯 Python、不装任何 CAD、Linux 上能跑。事实证明它对得起"最稳兜底"这个定位------图元(线圆弧多段线样条椭圆)、标注(线性/对齐/半径/直径/角度/弧长)、文字、剖面线、块引用,全都能生成。
但它有两个我一开始没料到的边界:
- 它生成的是静态文件。图纸一旦落盘,就和"交互"绝缘了。后面 CAXA 那条路卡住的时候,我就是靠"ezdxf 生成 DXF 让用户手动开"才没把 CAXA 整条线砍掉。
- 它救了我一个意外的场:CAXA CAD 2027 能直接打开 ezdxf 生成的 DXF R2010 文件,无需任何转换。这个事实在后面做选型时成了关键论据------兜底方案在 CAXA 环境里是通的。
ezdxf 这块我后面(第四篇)会展开,包括怎么解析而不是生成。
三、控制正在运行的 CAD:AutoCAD 的 COM,和 ZWCAD 的白捡
要"控制正在运行的软件",AutoCAD 是绕不开的第一站,因为它有一条最成熟的接口:COM。
COM 一句话说清:Windows 上的一种跨进程对象调用机制,AutoCAD 启动后把自己暴露成一个对象,外部程序拿到这个对象,就能调它的方法去画线画圆。Python 这边用 pywin32 拿对象:
python
import win32com.client
acad = win32com.client.Dispatch("AutoCAD.Application")
doc = acad.ActiveDocument
ms = doc.ModelSpace
看着就三行,但我在这栽了第一个跟头。
坐标传不进去。 我按直觉写 ms.AddCircle([50, 50, 0], 30),结果要么报 com_error,要么更阴险------不报错,但圆不在 (50,50),在别处。后者比报错还难查,因为你会以为自己的逻辑错了,其实错在类型。
排查过程是这样的:先怀疑坐标顺序(不是),再怀疑单位(不是),最后翻 pywin32 文档才明白------CAD 的 COM 接口里,点坐标的签名是 VARIANT 数组,不是 Python 的 list。VARIANT 是 COM 的动态类型,list 传过去会被当成一个普通的 Variant 值而不是坐标数组,CAD 拿到的数就乱了。
修法是包一层,显式声明"这是 double 数组":
python
def V(p):
return win32com.client.VARIANT(
pythoncom.VT_ARRAY | pythoncom.VT_R8, # 显式声明:double 数组
[float(p[0]), float(p[1]), float(p[2])]
)
之后所有坐标都走 V([x,y,z])。这个坑是 AutoCAD COM 自动化里 90% 的人第一个踩的,第二篇会从头讲透,包括更隐蔽的坑------AutoCAD COM 的圆弧角度是弧度不是度,写错了画出来的弧全不对。
转机出现在我意识到真正要支持的是 ZWCAD(国产环境,客户用的是它而不是 AutoCAD)。当时心里咯噔一下:不会要再学一套接口吧?
结果没有。ZWCAD 对 AutoCAD 的 COM 接口做了兼容,只改一个字符串:
python
acad = win32com.client.Dispatch("ZWCAD.Application")
其余代码一行不动。这个发现直接省掉了"再摸一套国产 CAD 接口"的预算,是四条路线里 ROI 最高的一条。
四、三维建模没有 COM:ZW3D 逼我写起了 C++
ZW3D 是国产三维 CAD,麻烦在于:它没有 COM 接口。官方给的是 C API + 插件机制,意思是你要么用 C++ 写插件塞进它进程里,要么别玩。
这就把我从 Python 的舒适区拽到了 C++。我当时的思路是:写一个 C++ DLL 插件,插件加载时起一个 HTTP 服务,外面的程序(包括 LLM 的 MCP 工具)用 HTTP 请求来调它,插件收到请求后调用 ZW3D 的 API 画图。
听着顺,但这里藏着一道几乎所有 GUI 二次开发都会撞的墙:线程安全。
我一开始图省事,想直接在 HTTP 处理线程里调 ZW3D API。结果根本不行------ZW3D 的 API 必须在主线程调用,从别的线程调,要么直接崩,要么静默无效。这不是 ZW3D 独有的毛病,所有带 GUI 的桌面软件都这样(Android 里 UI 只能主线程改,同理)。HTTP 服务天然多线程,每个请求来一个线程,正好踩在雷上。
排法是把"调 API"这件事投递回主线程:HTTP 线程把任务塞进队列,用 ZwCommandPost 唤醒主线程处理,再用条件变量把结果传回来。这套"队列 + 投递 + 等待"的模式是 ZW3D 方案能不能成立的关键,也是我花时间最多的地方,第三篇完整展开。
这还只是逻辑层的坑,部署层的更气人:
- DLL 放错目录。我照着直觉把编译好的 DLL 扔到用户目录
%appdata%\...\custom\apilibs,重启 ZW3D,命令就是没注册。查日志,Init函数压根没被调用。折腾大半天才搞明白:ZW3D 只从安装目录的apilibs加载并调用 Init,放用户目录等于白放。 - 中文乱码。面板上中文全变问号,最后定位到 ZW3D 中文版按 GBK(代码页 936)解析字符串,而我的源码是 UTF-8,编译时还加了
/utf-8,两边一打架就乱了。得把.cpp和.ui全转成 GBK 存。
这些坑单独拿出来都不大,串在一起,ZW3D 成了四条路线里"架构最完整、门槛也最高"的那条。它真正值钱的地方在后面:一旦 HTTP 通了,就能在它之上做质量评估闭环------让 agent 画完图后自己检查"视图齐不齐、标注有没有冲突、内容出没出纸",不合格就重画,最多迭代 5 次。这套"画完自检"的能力,是 COM 那条路线给不了的。
五、CAXA:能画,但"遥控"卡住了,我还没排掉
CAXA 是最后试的,也是最折腾的。它走 CRX 插件路线(可以理解成国产版的 ObjectARX)。命令模式下没问题:注册个命令,画了 49 个图元、5 个标注、存 EXB,全通。
问题出在我要做"远程控制"时:把画图代码搬到 HTTP 回调里执行,appendAcDbEntity 明明返回成功,图形就是不显示。实体写进数据库了,但界面没刷新出来。我列了四个排查方向------文档锁定(lockDocument)、sendStringToExecute 投递命令、PostMessage 发消息、以及找 CAXA 有没有等价于 ZW3D ZwCommandPost 的机制------但两周内没排掉。
我在这里的态度是:不硬撑。CAXA 这条先标成"半通",兜底用 ezdxf 生成 DXF 让用户打开。等哪天真要远程控制 CAXA,再集中 1-2 周啃这个命令上下文问题。调研的价值之一,就是知道什么该现在做、什么该先放着。
六、读存量图纸:50 万张 DWG 怎么啃
生成和控制都摸了一遍,最后是"读"------几十万张历史 DWG。
第一反应是直接解析 DWG 二进制。这条路有个开源库叫 LibreDWG,但它 GPLv3,商用场景 license 是个雷,直接放弃。
最终方案是绕:ODA File Converter(免费)把 DWG 转成 DXF,再用 ezdxf 解析。ODA 是 Open Design Alliance 出的免费命令行工具,一条命令批量转:
arduino
ODAFileConverter.exe "D:\dwg" "D:\dxf" ACAD2018 DXF 1
实测 5-10 文件/秒,50 万张约 14-28 小时。听着吓人,但这是纯一次性批量任务,扔台机器挂一晚上的事。
解析这步看着简单,其实有个"做对了和做错了天差地别"的点:别只数图元。数出"这张图有 200 条线 30 个圆"没用,真正有用的是 get_measurement() 拿到标注的真实数值(200mm、Ø120 这种),以及按坐标位置把标题栏、BOM、技术要求归位出来。这些才是能喂给检索和 LLM 的东西。第四篇会把这套流水线完整拆开。
七、把 LLM 放回它该在的位置
绕了一大圈,回到最开始的问题:LLM 到底站在哪?
答案是:指挥,不是执行。LLM 不直接调 COM、不写 C++,它干的是把"画个 300×200 的板"变成结构化参数,然后从工具列表里挑出"画板件"这个工具、传参。真正的画图动作,是底下的接口干的。
objectivec
用户自然语言
↓
LLM(语义理解 → 结构化参数 → 工具选择)
↓
MCP 工具层(统一接口,LLM 靠工具的 docstring 理解每个工具能干嘛)
├── AutoCAD/ZWCAD → COM 直连
├── ZW3D → HTTP 插件
├── CAXA → HTTP 插件(半通)
└── ezdxf → 直接生成 DXF
↓
CAD 出图 / 文件输出
这里要单独说一句 MCP 工具层为什么必要:LLM 不认识你的代码,它只认识工具的"名字 + 描述 + 参数"。所以封装工具时,docstring 写得清不清楚,直接决定 LLM 会不会用错工具------参数单位是毫米还是米、角度是度还是弧度,不写清楚它就瞎传。这个细节看着小,实际上决定了整条链路的上限。
好消息是这块有现成的:autocad-mcp-pro 已经封装了 154 个 AutoCAD 工具,multiCAD-mcp 一套代码通吃 AutoCAD/ZWCAD/GstarCAD/BricsCAD。想省事直接 pip install,想理解原理就自己写一遍(第二篇有完整例子)。
八、最后怎么选(不是结论表,是四句实话)
试完四条,我的判断是:
- 只要"生成图纸文件",不碰软件 → ezdxf。一天上手,但要知道它只管文件。
- 要控制正在跑的 AutoCAD/ZWCAD → COM + MCP。半天打通,ZWCAD 白捡,ROI 最高的一条。
- 要远程控制 ZW3D 做三维 → C++ HTTP 插件。预算 1-2 周,能换来 COM 给不了的"画完自检"闭环。
- 要远程控制 CAXA → 现阶段别硬啃,用 ezdxf 兜底,等命令上下文问题解决。
接下来这三篇
- 第二篇:AutoCAD/ZWCAD 的 COM 自动化,从 VARIANT 这个坑讲起,全部 API 清单 + 怎么封装成 MCP 工具 + 批量转 50 万张 DWG
- 第三篇:ZW3D 插件,从"Init 死活不被调用"排到"HTTP 通了能远程建模",含线程调度那套
- 第四篇:图纸的正反两面------LLM 生成 DXF,以及 50 万张存量图怎么解析入库喂回 LLM