Files
cad-agent/ai.memory/project_review_agent_impl.md
gjm 1e5bff268a feat(review): 截屏前最大化绘图区 + 空瓦片过滤覆盖回退路径
新增 MCP 工具 set_drawing_area(mode="max"|"restore"):最大化主窗口 + CLEANSCREENON
(隐藏功能区/命令行/面板)。实测绘图区 2110x462 -> 3310x1084(面积 3.68x),
restore 精确回位(窗口矩形/最大化态/绘图区尺寸全部还原)。

坑:CLEANSCREENOFF 是异步命令,AutoCAD 会按「进入干净屏幕时的布局」重设窗口几何,
把同步的 SetWindowPlacement 覆盖掉(表现为窗口停在最大化)。解法是插件内
SetTimer 延迟 700ms 再压回;实测窗口在 0.5~1.0s 之间收敛,故调用方等待取 1.5s。

取证层:
- 新增 maximized_drawing_area(ctx) 上下文管理器,__main__ 用它包住整个取证循环
  (多图框只进出一次);受 DRAWING_AREA_MAXIMIZE 控制,工具缺失/调用失败静默降级,
  restore 失败不掩盖业务异常。
- 内容过滤统一到规划与回退两条路径,共用同一谓词;过滤后为空则退回原集合。
- 判定改用「实体 bbox」而非「占用格」:格子量化会把跨界内容算进相邻格,反而更粗
  (实测实体 bbox 正好压象限边界时 4 个象限全判有内容)。heatmap.occupied_boxes 随之删除。

实测(真实图纸 A0/A1/A2 三图框):截图张数 12/8/4 -> 8/5/4;pytest 296 passed。
新增 verify_drawing_area.py 作为该工具的验收脚本。
2026-10-15 23:44:32 +08:00

12 KiB
Raw Permalink Blame History

name, description, type
name description type
LLM 识图审图 Agent —— 架构决策(已定稿) cad_mcp_frame + 自研 Agent 的架构决策与分阶段路线——混合式(流水线骨架+受控只读取证子循环)、基座 B 最小集、可信度三件套(确定性检查前置/结构化结论/handle 交叉校验)、标准化路线(先 stdio 桥) 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 等重复脚本 (这 5 个遗留脚本已于 2026-10-14 删除,见 环境与外部资源)。
  • tests/(pytest 回归网):假 LLM + 假 MCP 基座的离线快层,tests/fakes.py 提供 FakeLlmClient / FakeMcpClient / make_ctx。锁定 dwg_review 既有行为。 陷阱:绝不构造真 LlmClient(无 key 抛错)、无参 CadTools() 或 ReviewContext.create() (构造即 list_tools() 触网);一律 make_ctx() 注入假件。
  • verify_mcp_frame.py:基座改动验证脚本(分帧 / 大报文 / string id / 通知 / isError)。

可信度三件套(2026-10-14 落地)

根因是**「LLM 从自由文本里回忆 handle」:原 review_frame 输出无格式约束的自由文本, 再用第二次 LLM 调用**(extract_issues)转 JSON,handle 靠回忆 → 批注错位或丢失。

改后:review_frame 直接输出受约束 JSON {summary, issues[]}(删除二次抽取)

  • checks.run_deterministic_checks(确定性,handle 来自工具/数据本身) → findings.build_finding(合并 + handle 交叉校验)→ annotator(只批注 verified 的 handle)。
  • 确定性检查前置(dwg_review/checks.py):find_zero_length / find_duplicate_entities (都传 window)+ 纯 Python 派生(空文字、未闭合多段线 vertex_count>=3 and closed==false)。 实机验证:四类全部检出。
  • 结论结构化:checklist.build_system_prompt 强约束只输出 {summary, issues:[{category,severity,message,handle?}]};review_frame 仅在 「不再请求工具」的回合解析(中间工具回合的 content 永不解析);解析失败降级 summary=原文, issues=[](信息不丢,只是不进批注)。实机验证 deepseek-flash 遵守契约。
  • 证据交叉校验(findings.normalize_issue):LLM 引用的 handle 不在已采集证据中时 保留问题、剥掉 handle、verified=False(永不批注);handle 比对统一 upper()。
  • 开关(config.py,默认全开):DETERMINISTIC_CHECKS / VALIDATE_HANDLES / CHECK_UNCLOSED_POLYLINES。
  • 结构:issue = {frame,category,severity,message,handle,handles?,source,verified}; findings 元素 = {frame,sheet,summary,issues[],counts{deterministic,llm,unverified}}。

环境事实

  • 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。
  • find_zero_length / find_duplicate_entities 的 window 参数:schema 未声明,但 C++ 侧 parseCommonQuery 实际支持(2026-10-14 实机验证生效)。Python 侧直接传 window 即可, 不必改 JSON schema、不必重编 C++。注意 find_duplicate_entities.limit 语义是 最大重复组数(非条目数),返回 {count, groups:[{reason,type,handles[]}]}(非分页)。
  • 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-15):遇到 LNK1168 直接 Stop-Process acad -Force 即可,不必先问。

基座健壮性:工具执行的异常安全(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 均正常。

Plant 3D 图纸实体语义与圈注(2026-10-14 实机确认)

这是 Plant 3D / AEC 自定义实体图,插件无法内省——get_entity_detail 对它们返回 空 properties,query_entities 也只给 bbox/layer/type。类型语义:

  • AECDBEWPIPE = 管道(一根管子一个实体)。
  • AECDBEWFITTING = 管件,两种形态靠 bbox 长宽比区分:
    • 近正方(aspect = min/max ≥ 0.5,约 250×250)= 弯头;
    • 细长条(aspect < 0.5,约 20×220)= 法兰。
  • AECDBEWEQUIPMENT = 设备,本图中即阀门(蝶阀符号)。

不变量(交叉校验用):法兰数 == 2 × 阀门数(阀门两侧各一法兰)。

How to apply(识别/统计这类图):不要依赖属性,改用「query_entities(type_filter=...) 取 bbox + 几何分类 + 不变量交叉校验 + get_viewport_screenshot 视觉复核」。图框用 detect_drawing_frames(本图返回 3 个块图框 A0/A1/A2,kind=block);把实体按「中心点落在 哪个图框 bbox 内」归类到框。

圈注配方(已验证):draw_revision_cloud {shape:"circle", center:[cx,cy], radius:max_side*1.1, layer:"AI_REVIEW", color:3}——center 取阀门 bbox 中心,半径取包围盒 长边 × 1.1(本图阀门 bbox ≈230×220 → r≈253),绿色云线正好套住蝶阀符号。逐框 zoom_to_window(frame_bbox) + 截图可验证。画前若要重来,delete_entities 按 handle 删旧圆。