让 LLM 控制 AutoCAD/ZWCAD:COM 自动化 + MCP 封装
上一篇定了调:让 LLM 操作 CAD,AutoCAD 的 COM 是 ROI 最高的一条------半天能打通,ZWCAD 白捡。这篇把它拆开,从"坐标传不进去"这个坑讲起,把 API 挨个过一遍,最后讲怎么把这些能力封装成 LLM 能调的 MCP 工具。所有代码都是实测跑通的,不是文档抄的。
一、三行代码连上,然后第一个坑就来了
环境就一个依赖:
bash
pip install pywin32
连 CAD 是真的三行:
python
import win32com.client
acad = win32com.client.Dispatch("AutoCAD.Application")
doc = acad.ActiveDocument
ms = doc.ModelSpace
Dispatch("AutoCAD.Application") 拿的是正在运行的 AutoCAD 实例,没有就起一个。ModelSpace 是模型空间,画图都往这上面加。
到这儿都很顺,直到我画第一条线。
二、坐标传不进去:VARIANT 这个坑
我按 Python 的直觉写:
python
ms.AddLine([0, 0, 0], [100, 0, 0])
结果分两种情况,都让人怀疑人生。有时候直接抛 com_error;有时候不报错,但画出来的线不在 (0,0) 到 (100,0),在莫名其妙的位置。第二种比第一种还坑,因为不报错,你会顺着"是不是我坐标算错了"的方向查,越查越偏。
我的排查顺序是这样的:
- 先怀疑坐标顺序------XYZ 是不是写反了?不是。
- 再怀疑单位------是不是默认单位不对?不是。
- 最后翻 pywin32 文档,看到 "VARIANT" 这个词才反应过来。
问题出在 COM 的类型系统。AutoCAD 的 COM 接口里,点的参数签名是 VARIANT 数组 。VARIANT 是 COM 的动态类型,什么都能装,但"装的是哪种类型"要靠调用方显式声明。Python 的 list 传给 pywin32 后,被转成"装着三个 Variant 的 Variant 数组",而 AutoCAD 要的是"装着三个 double 的 double 数组"。类型对不上,AutoCAD 读到的就是垃圾值。
修法只有一种:自己声明类型,告诉 COM"这是个 double 数组":
python
import pythoncom
def V(p):
return win32com.client.VARIANT(
pythoncom.VT_ARRAY | pythoncom.VT_R8, # 数组 + 8 字节浮点
[float(p[0]), float(p[1]), float(p[2])]
)
VT_R8 就是 double,VT_ARRAY 声明是数组。之后所有坐标,不管画线画圆还是标注,一律走 V([x, y, z])。这函数就几行,但少了它后面全白搭。我把它放这儿,因为后面每个例子都要用。
三、更隐蔽的坑:角度是弧度
坐标刚解决,画圆弧又翻车。
AddArc 要传起始角和终止角,我理所当然填了度数:
python
ms.AddArc(V([0,0,0]), 50, 0, 90) # 我以为是 0° 到 90°
画出来是条奇怪的弧。查文档:AutoCAD COM 的角度参数是弧度 ,不是度。90 会被当成 90 弧度(约 5157°),转了好多圈。
这个坑比 VARIANT 隐蔽,因为它不报错、不报类型错误,画出来"是条弧,但不对",你很难第一时间想到是单位问题。正确写法:
python
import math
ms.AddArc(V([0,0,0]), 50, 0, math.pi/2) # 0 到 90°
记住一个结论:AutoCAD COM 里凡是角度,都是弧度。这也引出一个后面讲 MCP 的要点------把角度参数暴露给 LLM 时,docstring 里必须写"弧度",否则 LLM 大概率给你传度数。
四、把 API 挨个过一遍
解决了坐标和角度,剩下的就是体力活:把要用的 API 一个个试,确认能跑。下面这些全是实测过的。
基本图形,9 种 :直线 AddLine、圆 AddCircle、圆弧 AddArc、椭圆 AddEllipse、点 AddPoint、单行文字 AddText、多行文字 AddMText、多段线 AddLightWeightPolyline(可画任意多边形,矩形只是特例)、样条 AddSpline。有两个小地方要留意:椭圆传的是"主轴向量 + 短长轴比"而不是长短半径;多行文字里 \P 是换行。
尺寸标注,4 种 :对齐 AddDimAligned、旋转 AddDimRotated、半径 AddDimRadial、直径 AddDimDiametric。标注的坑少,主要就是坐标照旧走 V()。
3D 实体,5 种 :长方体 AddBox、圆柱 AddCylinder、球 AddSphere、楔体 AddWedge、圆环 AddTorus。很多人以为 AutoCAD 只能画二维,其实这五个函数直接能建三维,参数就是中心点 + 尺寸,简单到不像 CAD。
文件操作 :新建 Documents.Add()、打开 Documents.Open(path)、存 DWG SaveAs(path)、存 DXF SaveAs(path, 1)(第二个参数 1 表示 DXF)。
图层:
python
layer = doc.Layers.Add("粗实线")
layer.color = 7 # 1红 2黄 3绿 4青 5蓝 6紫 7白
图层这玩意儿,画简单图看不出价值,画几十个图元的工程图时才知道重要------没有图层,后面想改线型、开关显示都没法做。
五、一个完整例子:画一块带孔的板
把这些串起来,画一块 300×200 的板,四角各一个孔,标上长宽。这是我最常用的测试样例:
python
import win32com.client, pythoncom
def V(p):
return win32com.client.VARIANT(
pythoncom.VT_ARRAY | pythoncom.VT_R8,
[float(p[0]), float(p[1]), float(p[2])]
)
acad = win32com.client.Dispatch("AutoCAD.Application")
doc = acad.ActiveDocument
ms = doc.ModelSpace
# 外框:四条直线首尾相接
corners = [[0,0,0], [300,0,0], [300,200,0], [0,200,0]]
for i in range(4):
ms.AddLine(V(corners[i]), V(corners[(i+1) % 4]))
# 四个孔(半径 15)
for x, y in [(30,30), (270,30), (270,170), (30,170)]:
ms.AddCircle(V([x, y, 0]), 15)
# 长宽标注
ms.AddDimAligned(V([0,0,0]), V([300,0,0]), V([150,-30,0]))
ms.AddDimAligned(V([0,0,0]), V([0,200,0]), V([-30,100,0]))
doc.SaveAs("plate.dxf", 1)
六、批量转 50 万张 DWG:COM 能做,但不是首选
有个场景是批量把 DWG 转成 DXF。COM 方案能做:
python
acad = win32com.client.Dispatch("AutoCAD.Application")
acad.Visible = False # 不显示窗口,明显更快
for dwg in all_dwg_files:
doc = acad.Documents.Open(dwg)
doc.SaveAs(dwg.replace(".dwg", ".dxf"), 1)
doc.Close(False) # 不保存修改,直接关
Visible = False 提速明显,省掉的是 GUI 刷新那部分开销。COM 方案的好处是转的过程中能顺带清理图层、修图,这是纯格式转换工具干不了的。
但我得说实话:几十万张这个量级,COM 方案有内存和稳定性问题,长时间跑 AutoCAD 会越来越慢甚至崩。真到 50 万张的规模,我更推荐第四篇讲的 ODA File Converter,那个是专门干这活的,稳得多。COM 适合"控制 + 少量转换",不适合"海量批量"。
七、封装成 MCP 工具:docstring 决定 LLM 的上限
上面所有代码都是"人手写、人执行"。要让 LLM 用,得把能力封装成 MCP 工具。一个工具就是一个函数 + 一句 docstring:
python
# ms、V 是前面连上 CAD 后得到的 modelspace 和 VARIANT 辅助函数
@mcp.tool()
def autocad_draw_circle(cx: float, cy: float, radius: float) -> str:
"""在 AutoCAD 当前文档画一个圆。cx/cy 是圆心坐标(毫米),radius 是半径(毫米)。"""
ms.AddCircle(V([cx, cy, 0]), radius)
return f"已画圆:圆心({cx},{cy}) 半径{radius}"
为什么 docstring 是上限?因为 LLM 不认识你的代码,它只读得到工具的"名字 + 描述 + 参数签名"。autocad_draw_circle(cx, cy, radius) 这个签名,LLM 看到的是"一个画圆的工具,需要 cx/cy/radius 三个数"。至于这三个数什么单位、cx 是不是圆心,全靠 docstring 传达。
我踩过这个坑:docstring 只写"画一个圆",LLM 就把 radius 当成了直径,画出来的圆大了一倍。后来把单位写死("毫米")、角度单位写死("弧度"),才稳定。这个经验一句话总结:写工具描述时,把自己当成第一次看这个 API 的人。
现成的封装已经有人做好了,不用自己从头写:
autocad-mcp-pro:pip install就行,154 个工具,双引擎(COM + ezdxf),还带 ISO 标注、齿轮生成器、质量循环。multiCAD-mcp:7 个统一工具覆盖 56 个命令,AutoCAD、ZWCAD、GstarCAD、BricsCAD 一套代码通用。
八、两个坑,值得写进备忘录
- 坐标必须 VARIANT :90% 的人第一个坑。症状是"不报错但图形位置不对",比报错更坑。用
V()包一层解决。 - 角度是弧度:隐蔽,不报错,画出来"是弧但不对"。
另外一句大实话:先确认你要控制的是 AutoCAD 还是 ZWCAD 。国内很多环境用的是 ZWCAD,它兼容 AutoCAD 的 COM,只改一行 Dispatch("ZWCAD.Application"),其余代码全复用。搞清楚这个,能少做一半无用功。
下一篇进 ZW3D 的坑,从"DLL 放错目录导致 Init 死活不被调用"讲起,那是真·折腾。