让 LLM 控制 AutoCAD/ZWCAD:COM 自动化 + MCP 封装

让 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),在莫名其妙的位置。第二种比第一种还坑,因为不报错,你会顺着"是不是我坐标算错了"的方向查,越查越偏。

我的排查顺序是这样的:

  1. 先怀疑坐标顺序------XYZ 是不是写反了?不是。
  2. 再怀疑单位------是不是默认单位不对?不是。
  3. 最后翻 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-propip install 就行,154 个工具,双引擎(COM + ezdxf),还带 ISO 标注、齿轮生成器、质量循环。
  • multiCAD-mcp:7 个统一工具覆盖 56 个命令,AutoCAD、ZWCAD、GstarCAD、BricsCAD 一套代码通用。

八、两个坑,值得写进备忘录

  1. 坐标必须 VARIANT :90% 的人第一个坑。症状是"不报错但图形位置不对",比报错更坑。用 V() 包一层解决。
  2. 角度是弧度:隐蔽,不报错,画出来"是弧但不对"。

另外一句大实话:先确认你要控制的是 AutoCAD 还是 ZWCAD 。国内很多环境用的是 ZWCAD,它兼容 AutoCAD 的 COM,只改一行 Dispatch("ZWCAD.Application"),其余代码全复用。搞清楚这个,能少做一半无用功。

下一篇进 ZW3D 的坑,从"DLL 放错目录导致 Init 死活不被调用"讲起,那是真·折腾。

相关推荐
水獭比特2 小时前
localhost 不是安全边界:给 Agent Web 入口补上四层门禁
人工智能·python
赟爸2 小时前
直播切片素材杂乱不好复用,易元AI要怎么处理
大数据·人工智能·python
demo007x2 小时前
CoT(Chain-of-Thought)
程序员·llm·agent
qpsj2 小时前
让 LLM 操作 CAD:四条技术路线的取舍
人工智能·llm
qpsj2 小时前
ZW3D 插件开发实战:从搜索面板到 LLM 远程建模
llm
怪奇云呼军2 小时前
从声音特征到 CRM 回流:闪电智能 Voice Agent 沟通策略自适应系统 v1 实战
android·人工智能·python·音视频·语音识别
jufeng13072 小时前
【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 6 篇】
python·ai agent·记忆系统
kevinnett3 小时前
别再把模型地址写死了:用 Python 设计一个可切换的 LLM 调用层
python
林一l3 小时前
大模型推理全流程
llm