--- name: Agent 路线与多源架构(2026-10-15) description: dwg_review agent 的总目标、多源图纸分层架构(视觉为最大公约数)、工具层与 agent 层的正交分工、多源证据层、截图去 UI、grounding 实测、热力图 + 自适应瓦片规划 + VLM 分辨率标定、绘图区最大化(set_drawing_area)与空瓦片过滤、审查阶段分辨率对齐 + 放大取证闭环、后续路线 type: project --- # Agent 路线与多源架构 ## 总目标(用户 2026-10-15 明确) 实现 **AI 自动识别 + 评审 DWG 图纸**。分阶段:**先 MCP(已完成),再 agent(进行中)**。 最终要支持的图纸来源**不限于自家自定义实体**: 1. 自家软件的自定义实体 DWG; 2. **其他软件出的 DWG**(各种自定义实体); 3. 将来 **PDF** 图纸; 4. 将来**纯图片**图纸。 ## 架构分层(用户认可) **核心洞察:视觉模型(VLM)是"最大公约数"**。DWG 自定义实体 / PDF / 图片本质是 "拿不到结构化数据时靠视觉理解"的同一件事——**任何图纸只要能渲染成图,VLM 就能审**。 所以结构化数据(ARX 读普通实体)是**增强**,视觉是**兜底**。 ``` Agent 推理层(审查逻辑 / 清单 / 规则) ▲ 统一 evidence schema 感知适配层(能力从高到低降级) CAD结构化(ARX) → CAD几何 → 离线解析(DWG/PDF) → 视觉(VLM) ``` - **MCP 工具层只管 CAD**(进程内能力);**PDF / 图片在 agent 侧**(PyMuPDF / Pillow), **不塞进 MCP**、不编 DLL。 - **统一证据模型 + 多源适配 + 能力降级**;agent 推理层不关心来源格式。 **How to apply**:新增"支持某格式"时,优先在 agent 侧感知层加适配器;只有"CAD 进程内的 正交感知能力"才考虑加 MCP 工具。 ## 第一步已落地(2026-10-15,提交 `1c9fc72`) **取证层多源化 + 视觉多图**: - 新增 `dwg_review/evidence_model.py`:统一证据模型(`source_type` + `images:[{b64,mime,label}]`),访问器 `evidence_images()` 兼容旧 `image_b64/image_mime` 单图字段(历史 evidence.json 仍可读)。 - `phases/evidence.py`:`collect()` 多源分发 seam(当前仅 CAD)、`capture_images()` 整体 + 等分象限多图(结束**复位视口**)、`sub_windows()`。 - 默认 `EVIDENCE_DETAIL_TILES=4` / `EVIDENCE_MAX_IMAGES=5`(每图框 5 图,**默认开启**)。 - 实测:真实 Plant 3D 图纸上多图让审查更细(多报出线型误用 `CENTER2`、图签块属性为空、 按 handle 定位的图层混用等)。 ## 截图去 CAD 界面元素(2026-10-15,提交 `f8bd12d`) `get_viewport_screenshot` 原先截 `adsw_acadDocWnd()` 客户区,含视口控件、ViewCube、滚动条, 污染截图并**误导 VLM**(曾把视口标签当成"需放大的内容")。已修复: - 改用 **`acedGetAcadDwgView()`** 只截图形区(排除滚动条/状态栏)。 - 截图期间临时置 **`NAVVCUBEDISPLAY=0`**(关 ViewCube)与 **`VPCONTROL=0`**(关视口控件), 用 RAII 守卫(`TempSysVarOff`)自动恢复原值,**不影响用户 CAD 界面设置**; 配合 `RedrawWindow(RDW_UPDATENOW)` 同步重绘。 - 实测:三项 UI 均消失,VLM 不再提界面元素。 **残留(无解,已知)**:绘图区右上角仍有**绘图窗口**的最小化/最大化/关闭按钮——由 AutoCAD 画在图形区内的固有 UI,**无系统变量可关**。它很小且在角落,实测未影响 VLM。 (区别于应用窗口的按钮;用户最初要清的是 视口控件/ViewCube/滚动条,不含此项。) ## grounding 实测(2026-10-15) 用 `verify_grounding.py` 拿真实截图问 `deepseek-flash`"哪里要放大":**grounding 可用**—— 给出的归一化坐标**区域级定位准确**(放大后确实套住目标,如图签栏、图框角), 但**语义描述略有偏差**(把图签栏说成"状态栏")。结论:可支撑"按区域放大",但**位置对、语义别全信**。 ## 后续路线(未做) 1. **VLM 主动放大(自适应取证)**:复用受控只读取证子循环,让 VLM 请求放大看不清处。 - **2026-10-16 部分落地**:审查阶段已对齐分辨率 + 提示词已明确放大协议 + 视口复位, 实测 LLM 真的会连发 `zoom_to_window`+`get_viewport_screenshot`(见下节)。 仍缺:① 按区域放大时的 **grounding→WCS 映射**(目前靠 LLM 自己用 bbox 算窗口,够用); ② 把「看清没」的判定**不交给 VLM**(防过度自信)——现靠回合上限 + 截图预算兜底。 - 现有 `zoom_to_window`/`zoom_to_entity`/`get_viewport_screenshot` 已在只读白名单, **无需新增工具**。 2. **PDF / 图片源**:在 `phases/evidence.collect()` 加分支,产出**同形** evidence (`window=None`、结构化空信封、`images` 带 label);规划阶段抽象"审查单元" (CAD=图框 / PDF=页 / 图片=单);批注对非 CAD 源降级(`annotator` 已天然跳过无 handle 的 issue)。 3. **审图规范接入**:清单升级为规范库。 ## 热力图 → 自适应瓦片规划 → VLM 分辨率标定(2026-10-15,进行中) **动机**:固定「整体 + 2x2 象限」截图对 A0/A1 大图幅读不清。用户拍板:**驱动信号是实体分布 (热力图),不是文字**(有的图没有文字),且**实际工况下实体占据图框大部分**,稀疏/稠密都要能处理。 用户方案:图框类型 → 纸张真实尺寸 + 比例 → CAD 世界尺寸 → 换算像素 → 知道 VLM 分辨率 → 算出**一个图框要截几张、截哪里**。 **用户已拍板的关键决定**: - 热力图在 **Python 侧(agent 层)** 算(派生量,工具层只给原始事实)。密集图若受 `query_entities` 的 `limit≤1000` / 帧上限拉不全 → **给现有 `query_entities` 加 `brief` 模式** (只回 handle/type/bbox、放开 limit),**而不是新增"热力图工具"**。 - `CollisionDetection.h/cpp`(用户写的 BVH)**本步不用**,留作后续**几何相交/干涉检查**引擎。 其按质心中位分割 → 节点框**可能重叠**,不适合铺屏瓦片;瓦片切分用四叉树/网格(空间不相交)。 - 标定判据:**同时测「读出文字」与「分辨线细节」**;标定字高 **200 图形单位**(1:100 = 纸面 2mm,国标最小)。 **已落地(提交 `9b8a232` 热力图;`cba05b0` 瓦片规划 + 离线标定)**: - `dwg_review/heatmap.py`(纯函数):`entity_boxes` / `build_grid`(counts+coverage)/ `cluster_cells`(4-连通,可选膨胀)/ `grid_to_windows` / `grid_stats` / `render_overlay`。 **`drop_oversized`**:丢掉 bbox 占满图框的实体(实测每框恰 1 个,即图框块),否则热力图全满。 - `dwg_review/tiling.py`(纯函数):`frame_scale`(`sheet_scale>0` 用之;否则由 bbox/纸张**反算**; 否则 default)/ `world_per_px` / `is_text_type` / `plan_regions`(文字簇优先、空白区不出图、 `max_tiles` 截断、窗口夹到图框)。 - `phases/evidence.capture_images`:先截整体图并**测绘图区像素** → 规划成功用规划窗口, 否则退回 `sub_windows`(行为零变化);**任一信封 `total>returned`(截断)→ 退回固定策略**。 - `verify_heatmap.py`(不耗 token,出 PNG + bbox 缓存)、`verify_vlm_resolution_offline.py`、 `verify_text_resolution.py`(CAD 实测)。 **标定结论**: - **该模型无法可靠计数线条**(6 条线在 2~10px 间距下答 4/10/4/5/4/4)→ 线条判据改用 **「1 条 vs 2 条」二选一**。 - **可读下限与字体强相关**:离线合成(Consolas 抗锯齿填充字体)**12px**(白底 10px); 真实 CAD **单线体 .shx(描边字体)6px 就能读对**(3/3,两轮)。 - 真实 CAD 线条:**2/3/4px 失败,5/8/12px 成功** → R_line ≈ **5px**。 - **标定必须用 ACI 7(白/黑自适应)**:实测**红字(ACI 1)12px 完全读不出**——否则把 "对比度不足"误当"分辨率不足"。 - 实测绘图区(**窗口未最大化时**)**2110×462** → 有效 **1280×280**(长边 1280),视口宽高比 **4.567**。 **用户拍板的默认值(2026-10-15)**:`TEXT_READABLE_PX=8`(折中,兼顾描边与 TrueType)、 `LINE_READABLE_PX=6`、`EVIDENCE_MAX_IMAGES=12`(原 5 会把规划结果截到只剩 4 张瓦片)。 **瓦片粒度内容过滤**:簇 bbox 是**包围盒**,L 形/斜向分布(如管道走向)含大片空白 → `plan_regions(keep=...)` 剔除**空瓦片**(实测有效)。 **已知局限**:本图纸的文字在**块 / 自定义实体**内,`get_text_content` 检不到 → 该簇会按 **线条判据**出图(6px),块内文字可能读不清。要更保险可把 `LINE_READABLE_PX` 调到 8 (等价于全用文字判据,代价是同样预算下覆盖面积减半)。 ## 绘图区最大化 + 内容过滤覆盖回退路径(2026-10-15,本轮) **用户诉求**(原话):「截屏之前,要把 CAD 铺满屏幕,并且把命令行也最小化,反正一句话, **让绘图区最大化**,然后再去截屏。是不是更好?还有,我发现你有几次**截屏区里面没有实体**,浪费了。 截屏之前判断一下这个区域有没有实体。」 **A. 新增 MCP 工具 `set_drawing_area(mode="max"|"restore")`**(`src/cad_mcp_plugins/dwg_review_ui.cpp`) - `max`:保存 `WINDOWPLACEMENT` + `CLEANSCREENSTATE`(**只存首次**)→ `ShowWindow(SW_MAXIMIZE)` → `sendStringToExecute("_.CLEANSCREENON")`(隐藏功能区/命令行/面板)。 - `restore`:`CLEANSCREENOFF` + 还原窗口布局。 - **关键坑:CLEANSCREENOFF 是异步命令,会按「进入干净屏幕时的布局」重设窗口几何,把同步执行的 `SetWindowPlacement` 覆盖掉**(表现为窗口停在最大化)。解法:restore 里用 `SetTimer(NULL, 0, 700, delayedRestoreProc)` **延迟 700ms 再压回**布局。 实测窗口在 **0.5~1.0s** 之间收敛 → 调用方须等待 ≥1.5s。 - **实测收益(屏幕 3440×1440)**:绘图区 **2110×462 → 3310×1084**(面积 **3.68×**), 窗口矩形 `[68,0,2316,960]` → `[-4,-2,3444,1442]`,还原**精确回位**。 - 验收脚本:`verify_drawing_area.py`(基线 → max → restore,断言矩形/最大化态/绘图区尺寸全回位)。 - 工具注册:`mcp_config.json` 的 `tools[]`(现 **27** 个),工厂 `CreateSetDrawingAreaTool`。 `set_drawing_area` 以 `set_` 开头 → 被 `cad_tools.is_write_tool` 判为写类(不重试), 且**不在** `READ_ONLY_TOOLS` 里 → LLM 的受控子循环**不能**动用户界面。 **B. 取证层接入**(`phases/evidence.py`) - `maximized_drawing_area(ctx)` 上下文管理器:`with` 内 `max`、退出 `finally` 里 `restore`。 受 `config.DRAWING_AREA_MAXIMIZE` 控制;工具缺失 / 调用失败 → **静默降级**; **restore 失败不掩盖业务异常**。`__main__.run_review` 用它包住整个取证循环(多图框只进出一次)。 - **内容过滤统一到两条路径**:原先只有规划路径过滤,回退路径(`sub_windows` 象限)不过滤。 现在 `keep = _content_predicate(boxes)` 一次构造,规划与回退**共用**; `_apply_keep` 在「过滤后为空」时**退回原集合**(宁可多拍不漏)。 - **改用「实体 bbox」而非「占用格」判定**(上一版用 `heatmap.occupied_boxes`): 格子量化会把跨界内容算进相邻格,**反而更粗**(实测某实体 bbox 正好压象限边界时 4 个象限全判有内容)。 → 直接 `boxes_intersect(tile, entity_box)`,更精确也更简单;`heatmap.occupied_boxes` 随之**删除**。 语义:**接触即算有内容**(保守)。 **实测(真实图纸,3 个图框 A0/A1/A2)**:截图张数 **12/8/4 → 8/5/4**(收益同时来自 绘图区变大 → 单张覆盖更多世界尺寸,与空瓦片被剔除)。端到端 `--plan-only` 跑通, 运行中窗口峰值 `[-4,-2,3444,1442]`(面积 2.31×),结束**精确还原** `[68,0,2316,960]`。 **新增配置**:`DRAWING_AREA_MAXIMIZE`(True) / `DRAWING_AREA_ENTER_WAIT`(1.5) / `DRAWING_AREA_RESTORE_WAIT`(1.5,**必须 > 插件内 700ms 延迟还原**)。 ## 审查阶段分辨率对齐 + 放大取证闭环(2026-10-16,本轮) **发现的缺口**:`maximized_drawing_area` 原先**只包住取证循环**,审查阶段没被包住 → LLM 在受控子循环里 zoom + 截图拿到的是**未最大化的 2110×462**(正是上一轮刚花力气解决的 低分辨率问题,在**最需要放大看细节的审查阶段**又出现了);且审查中的 zoom **不复位视口**, 跑完把用户的 CAD 留在最后一次放大处。 **三项改动**: 1. **`__main__.run_review`**:把 `with evidence_phase.maximized_drawing_area(ctx)` 从 「只包取证」扩展到「**包取证 + 审查**」(两者都要截图);`report`/`annotate` 留在 with 之外。 2. **`phases/review.py`**:新增 `_VIEW_TOOLS = {zoom_to_window, zoom_to_entity}`; `review_frame` 拆成外壳 + `_review_frame`,外壳 `try/finally` 里**仅当确实动过视口**才 `_reset_viewport`(复位到该图框整幅视图 `zoom_to_window(win, margin=0.02)`)。 —— 只在动过时复位,既省一次调用,也不影响"纯查结构化数据"的路径。 3. **`checklist.build_system_prompt`**:把「可调用只读工具」改写成**明确的放大取证协议**: 截图看不清时用 `zoom_to_window`(坐标由图框/实体 bbox 算)或 `zoom_to_entity`, 再 `get_viewport_screenshot` 复核;并约束**不要为看图而反复放大**。 **实测(真实图纸,1 图框,`verify_review_maximized.py`)**: - 取证阶段采样 `[-4,-2,3444,1442]`、**审查阶段采样同样是 `[-4,-2,3444,1442]`**(最大化态), 结束**精确还原** `[68,0,2316,960]` → 三项全 PASS。 - 子循环里 LLM **真的连发 8 组 `zoom_to_window`+`get_viewport_screenshot`** (提示词生效,"看不清→放大重看"闭环跑通)。 **同一轮发现的次生问题**:`_MAX_OBS_IMAGES` 原为硬编码 **3**,而模型一轮内会发 8 次截图 → **拍了 8 张只回灌 3 张**(其余被"已省略"占位)。改为配置项 `REVIEW_OBS_IMAGES` (默认 **6**,`config.py`),并把预算耗尽时的占位文案改成**明确告知模型"不要再请求截图, 请给出结论"**(原先只说"已省略")。实测模型截图次数不变(8),但**能看到 6 张而非 3 张**。