110 lines
8.2 KiB
Markdown
110 lines
8.2 KiB
Markdown
---
|
||
name: LLM 识图审图 Agent —— 架构决策(已定稿)
|
||
description: cad_mcp_frame + 自研 Agent 的架构决策与分阶段路线——混合式(流水线骨架+受控只读取证子循环)、基座 B 最小集、标准化路线(先 stdio 桥)
|
||
type: project
|
||
---
|
||
|
||
# LLM 识图审图 Agent —— 架构决策(2026-10-13 定稿)
|
||
|
||
## 决策
|
||
|
||
1. **Agent 形态:混合式自研**(非纯 ReAct、非纯流水线):确定性流水线骨架
|
||
(规划 → 逐图框取证 → 审查 → 汇总)+ 审查阶段给 LLM 一个**受控只读取证工具子循环**
|
||
(白名单 15 个只读工具,`add_*/delete_*/set_*/draw_*` 一律禁止)。
|
||
2. **审查依据**:清单驱动(`review_checklist.json`)+ LLM 自主判断。
|
||
3. **输出**:Markdown + JSON 报告 + 可选 CAD 图上批注回写(统一落 `AI_REVIEW` 层)。
|
||
4. **基座改造:只做「B 最小集」**——换行分帧(`\n` 为消息边界)、去掉单次 `recv(2048)` 限制、
|
||
`isError` 映射、`id` 支持 string/number + 通知不回包;**不做** `initialize`/`ping` 握手。
|
||
5. **标准化路线**:先自研 agent 跑通 → 再做 stdio↔TCP 桥 → 远期才考虑 Streamable HTTP。
|
||
(**2026-10-14 更新**:stdio 桥**已实现并验证通过**,见 `project_stdio_bridge.md`。
|
||
远期仅剩 Streamable HTTP。)
|
||
|
||
**Why(关键结论,非显而易见)**:
|
||
|
||
- 基座是**裸 TCP 自定义传输**。规范虽允许自定义传输,但标准客户端(Claude Desktop /
|
||
CodeBuddy / MCP Inspector)**只实现 stdio 与 Streamable HTTP**——因此裸 TCP 无论怎么补
|
||
生命周期都**无法被标准客户端直连**。"能被标准客户端接入"只有两条路:**stdio 桥**
|
||
(省力、CAD 侧零改动)或 **Streamable HTTP**(彻底、要在 CAD 内做 HTTP+SSE、最贵)。
|
||
- 自研 agent 直接连 TCP,传输是否"标准"**不影响**,故**标准化不是前置条件**。
|
||
- `mcp_server.cpp` 原来**单次 `recv` 仅 2048 字节**,请求超 2KB 会被截断成 `-32700`;
|
||
这是**服务端**限制、客户端绕不过去 → 必须先改基座。
|
||
- image 内容块 `{type:image,data,mimeType}` **本就是 MCP 标准**(`source:{type,mediaType}`
|
||
是 Anthropic Messages API 的格式,别混淆),无需改。
|
||
|
||
**How to apply**:新增审图工具仍走 `cad_mcp_plugins` + `mcp_config.json`;agent 侧工具从
|
||
`tools/list` **动态发现**,加工具零代码。日后要做标准客户端接入,**从 stdio 桥起步**。
|
||
|
||
## 新增产物
|
||
|
||
- `dwg_review/`(Python 包):`config.py`(环境变量,无硬编码 Key)、`mcp_client.py`
|
||
(唯一 TCP 封装:单飞锁 + 换行分帧 + 重试 + 兼容 `error`/`isError`)、`llm_client.py`、
|
||
`cad_tools.py`(动态工具表 + 只读白名单 `READ_ONLY_TOOLS`)、`checklist.py`、
|
||
`phases/{planning,evidence,review,report}.py`、`annotator.py`、`__main__.py`。
|
||
**取代**根目录散落的 `dwg_review_agent.py` / `agent_loop.py` 等重复脚本。
|
||
- `verify_mcp_frame.py`:基座改动验证脚本(分帧 / 大报文 / string id / 通知 / isError)。
|
||
|
||
## 环境事实
|
||
|
||
- **Pillow 已安装**(12.3.0)。截图下采样策略(`dwg_review/llm_client.py: downsample_image_b64`):
|
||
视觉 token 成本由**像素尺寸**决定而非文件大小 → **只在长边超 `IMAGE_MAX_EDGE`(默认 1280)时才缩放**;
|
||
既不需要缩放、格式又没变时**原样返回不重编码**(避免给 CAD 线稿引入无谓压缩损失);
|
||
缩放后默认输出**无损 PNG**(`IMAGE_FORMAT=png`,适合线稿),需要更小体积可设 `IMAGE_FORMAT=jpeg`。
|
||
缺少 Pillow 时自动降级为原图直传。
|
||
- 视觉模型 `deepseek-flash`。
|
||
- **`DEEPSEEK_API_KEY` 设在 Windows `Machine`(系统)级**。因新设的系统变量**不会传播到已启动
|
||
的进程**,`dwg_review/config.py` 的 `_read_env()` 在进程环境读不到时**回退读注册表**
|
||
(`HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment` → `HKCU\Environment`)。
|
||
否则 CodeBuddy 内的命令永远读不到新设的 Key。
|
||
- **AutoCAD 2020 自动加载 ARX(用户配置,不在仓库内)**:在 `bin/Debug-R230/` 下建
|
||
`acad.rx`(内容一行 `cad_mcp_frame.arx`),并把该目录加入 CAD 的**搜索路径与信任路径**。
|
||
这是 AutoCAD 官方 RX 自动加载机制,比 `acaddoc.lsp` + LISP 更直接。
|
||
- AutoCAD 2020 安装于 `C:/Program Files/Autodesk/AutoCAD 2020/`;用户配置目录
|
||
`C:/Users/gjm/AppData/Roaming/Autodesk/AutoCAD 2020/R23.1/chs/`(`acaddoc.lsp` 已用于 PDF2CAD,勿覆盖)。
|
||
- **改 `cad_mcp_plugins` 后重链接必须先关 CAD**:插件 DLL 被 CAD 进程持有(`LNK1168: cannot open
|
||
...dll for writing`)。模板 `cad-mcp-frame.json` 里的 `"hot_reload": true` **只是占位、代码未实现**,
|
||
`mcp_Tools::clear()`(唯一 `FreeLibrary` 处)仅在 ARX 卸载时调用 → **没有热重载**。
|
||
流程:关 CAD → `cmake --build --preset R230 --config Debug` → 重启 `acad.exe`(`acad.rx` 自动加载)
|
||
→ 轮询 `127.0.0.1:8080` 监听。编译阶段可先行(语法错误会被提前暴露,只有链接会因占用失败)。
|
||
|
||
## 基座健壮性:工具执行的异常安全(2026-10-14)
|
||
|
||
**不变量:`cad_mcp_frame` 的工具执行运行在 AutoCAD 主线程的窗口消息处理链上
|
||
(`TcpServerLoop` → `PostMessage` → `WndProc` → `ExecuteTaskInMainThread` → `mcp_Tools::Call`),
|
||
任何异常都不得逃逸出 `WndProc`,且异常路径上不得遗留文档写锁。**
|
||
|
||
**Why**:插件 `Execute` 在参数**类型**不符时会抛 `nlohmann::type_error`(如
|
||
`args.value("bulge", 0.5)` 收到字符串、`parsePoint` 对 `["a","b"]` 取 `get<double>()`——
|
||
它只查 `is_array()/size()>=2`,不查元素类型)。而 `dwg_review` 由 LLM 驱动、
|
||
**模型经常生成类型错误的参数** → 这是日常路径,不是"恶意客户端"场景。
|
||
原实现全程无 try/catch:异常逃逸出 `WndProc` 属未定义行为(表现为 CAD 崩溃),
|
||
且 `mcp_Tool_DllProxy::Execute` 的 `unlockDocument` 在异常路径被跳过 → 写锁泄漏。
|
||
|
||
**How to apply**:基座已统一兜住(`mcp_Tools::Call` try/catch → `-32603` → `result.isError=true`;
|
||
`HandleToolsCall` 校验 `params`/`arguments` 必须为对象、`name` 必须为字符串,否则 `-32602`;
|
||
`ExecuteTaskInMainThread` 兜底 try/catch + `!req.is_object()` → `-32600`;
|
||
DLL 代理写锁改为 `ScopedDocumentWriteLock` RAII)。**插件作者无需自行 try/catch**,
|
||
但仍不要在异常路径上留下副作用。新增/改动工具后跑 `verify_mcp_fault_injection.py` 回归。
|
||
|
||
**同一根因的另一面:阻塞。** 工具响应由 **AutoCAD 主线程**发送
|
||
(`WndProc` → `ExecuteTaskInMainThread` → `SendAll`),所以发送路径也必须有时限——
|
||
`accept` 时同时设了 `SO_RCVTIMEO`/`SO_SNDTIMEO`(各 5s),否则慢客户端能把 CAD 的 UI 线程卡死。
|
||
分帧规则:`\n` 优先,其次「暂时无更多数据、且累积内容已是完整 JSON」(兼容不发 `\n` 的旧客户端);
|
||
**不要再引入基于等待时长的分帧判定**(旧的 150ms select 探测会把分片间隔大的大报文误截断)。
|
||
回归脚本:`verify_mcp_tcp_hardening.py`。
|
||
|
||
## 验证结论(2026-10-13 实测通过)
|
||
|
||
- `verify_mcp_frame.py` 4/4 PASS(换行分帧 / >2KB 报文不截断 / string id 原样回显 / 通知不回包 / isError)。
|
||
- `verify_annotate.py` 9/9 PASS(批注工具无回归)。
|
||
- `dwg_review --list-tools` = 26 个工具;`--plan-only` 取证正常;全流程跑出报告。
|
||
- **受控取证子循环实测生效**:LLM 自主调用了白名单内的 `get_drawing_info`/`get_layers`/
|
||
`find_zero_length`/`find_duplicate_entities`/`get_dimensions` 做二次确认。
|
||
- 批注回写实测通过;注意 `add_leader_annotation` 的 `points` **必须 ≥2 个点**(首个为箭头)。
|
||
|
||
## 验证结论(2026-10-14 补充)
|
||
|
||
- `verify_mcp_fault_injection.py` **22/22 PASS**(畸形/类型错误请求:结构性错误回
|
||
`-32602/-32600/-32700`;工具内部类型异常被兜为 `result.isError=true`;反复注入 10 次后
|
||
CAD 存活、写锁未泄漏)。回归 `verify_mcp_frame` 4/4、`verify_stdio_bridge` 24/24、
|
||
`verify_annotate` 9/9、`dwg_review --check` 均正常。
|