Files
cad-agent/ai.memory/project_agent_roadmap.md
T
gjm b757892c67 feat(review): agent 侧虚拟工具 inspect_region —— 按可读分辨率放大指定区域
解决三个缺口:① 放大没复用标定/瓦片数学(LLM 一次 zoom 只出一张,宽区域仍读不清);
②「看清没」由 VLM 自评(易过度自信/幻觉);③ 只能按 handle / 手算 bbox 指认,
图上看到但没 handle 的东西指认不了。

核心思路:把「放大」升级成一个语义化工具——LLM 只指认「要看哪里」,由 agent 复用
tiling.py 的标定数学算出该切几张、每张覆盖多大,用构造保证可读。

新增 dwg_review/inspect.py(agent 侧,不编 DLL):
- TOOL_SCHEMA:inspect_region(target, purpose);target 三形态 {handle} / {window} /
  {label,cx,cy,span},purpose = text(8px) / line(6px)。
- InspectState:单图框内「截图标签 → 世界可见范围」登记表,支持逐级下钻
  (返回的 inspect-N 可继续作为 label 指认)。
- resolve_target / _point_box:label+cx+cy → WCS。cy 是图像坐标(上→下),
  世界 y 必须翻转(y = y2 - cy*H)——单测专门钉住。
- plan_tiles:复用 world_per_px + tiles_for_box + subdivide;超 INSPECT_MAX_TILES(4)
  则硬失败并给出「缩小 target / 分多次请求」的指引(不静默降级);拿不到比例/像素
  则退回单张并如实告知。

关键几何:CAD 的 _.ZOOM _W 是等比适配(不拉伸),故「截图上的归一化坐标」必须按
max(W/ew, H/eh) 反算真实可见范围(新增 tiling.visible_box)——直接用送进去的窗口会偏。
zoom_to_window 返回的 view 是「加过 margin 的窗口」,不是可见范围,不能混用。

证据层配套:evidence_model.make_image(..., window=) 记录每张图的可见世界范围;
capture_images 用 view_px(overall 图原始像素)+ visible_box 逐张登记。

子循环接入:_run_readonly_tool 在只读白名单校验之前拦截 inspect_region(它不在
READ_ONLY_TOOLS 里,内部只调白名单内的只读 MCP 工具);_review_tools 启用时用
inspect_region 取代三个裸视口工具;inspect_region 计入 _VIEW_TOOLS → 结束复位视口。
配置:INSPECT_ENABLED(True) / INSPECT_MAX_TILES(4)。

端到端实测(真实图纸 1 图框):取证与审查两阶段均在最大化态、结束精确还原(4 项 PASS);
LLM 的 11 次取证调用里 2 次是 inspect_region,且都显式给 window + purpose:"text"
(x[20000,115000] y[-6500,3000],即 A0 图框底部横条=标题栏所在)。
验收口径达成:报告报出标题栏乱码 ???????????????,而 get_text_content 对全图返回 0 条文字
(本图文字全在块/自定义实体内)→ 块内文字是被视觉放大读出来的。
pytest 343 passed。
2026-10-16 19:15:32 +08:00

247 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: Agent 路线与多源架构(2026-10-15)
description: dwg_review agent 的总目标、多源图纸分层架构(视觉为最大公约数)、工具层与 agent 层的正交分工、多源证据层、截图去 UI、grounding 实测、热力图 + 自适应瓦片规划 + VLM 分辨率标定、绘图区最大化(set_drawing_area)与空瓦片过滤、审查阶段分辨率对齐、agent 侧虚拟工具 inspect_region(放大取证闭环)、后续路线
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 主动放大(自适应取证)**:**2026-10-16 已落地**(见下节 `inspect_region`)。
剩余可选打磨:把「看清没」的判定进一步形式化(目前靠 agent 用标定数学**构造保证**可读 +
回合上限 + 截图预算兜底),以及 grounding→WCS 之外的其它指认方式。
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 张**。
## A 收尾:agent 侧虚拟工具 `inspect_region`(2026-10-16)
**要解决的三个缺口**:① 放大**没复用标定/瓦片数学**(LLM 一次 zoom 只出一张,宽区域仍读不清);
② 「看清没」由 **VLM 自评**(易过度自信/幻觉);③ 只能按 handle / 手算 bbox 指认,
**图上看到但没 handle** 的东西指认不了。
**核心思路**:把「放大」升级成一个**语义化工具**——LLM 只指认「要看哪里」,
由 agent 复用 `tiling.py` 的标定数学算出该切几张、每张覆盖多大,**用构造保证可读**。
**新增 `dwg_review/inspect.py`**(agent 侧,**不编 DLL**):
- `TOOL_SCHEMA`:`inspect_region(target, purpose)`。`target` 三形态任选其一——
`{handle}` / `{window}` / `{label, cx, cy, span}`;`purpose` = `text`(8px) / `line`(6px)。
- `InspectState`:单图框内的「截图标签 → **世界可见范围**」登记表。初始登记取证阶段的
`overall`/`tile-*`,每次放大再追加 `inspect-1`、`inspect-2`… → **LLM 可用上一轮返回的标签
继续下钻**(逐级放大)。
- `resolve_target` / `_point_box`:`label+cx+cy` → WCS。**cy 是图像坐标(上→下),
世界 y 必须翻转**(`y = y2 - cy*H`)——最容易写错的一处,单测专门钉住。
- `plan_tiles`:复用 `tiling.world_per_px` + `tiles_for_box` + `subdivide`。
超出 `INSPECT_MAX_TILES`(默认 4)→ **硬失败**并给出「缩小 target / 分多次请求」的具体指引
(不静默降级,否则等于给一堆读不清的图);拿不到比例/像素 → 退回单张并如实告知。
**关键几何:`tiling.visible_box`**——CAD 的 `_.ZOOM _W` 是**等比适配(不拉伸)**,
指定窗口按视口宽高比放大到刚好装下,多余一维露出更多内容。所以「截图上的归一化坐标」
要准确映射回 WCS,必须按 `max(W/ew, H/eh)` 反算真实可见范围,**不能**直接用送进去的窗口。
(`zoom_to_window` 返回的 `view` 是**加过 margin 的窗口**,不是可见范围,别混用。)
**证据层配套**:`evidence_model.make_image(..., window=)` 记录每张图的可见世界范围;
`capture_images` 用 `view_px`(overall 图的原始像素)+ `visible_box` 逐张登记。
**子循环接入**(`phases/review.py`):`_run_readonly_tool` 在**只读白名单校验之前**拦截
`inspect_region`(它不在 `READ_ONLY_TOOLS` 里,内部只调白名单内的只读 MCP 工具);
`_review_tools` 启用时用 `inspect_region` **取代**三个裸视口工具(`zoom_to_window` /
`zoom_to_entity` / `get_viewport_screenshot`)——一个语义化入口比三个裸工具更好用;
`inspect_region` 计入 `_VIEW_TOOLS` → 结束时复位视口。
配置:`INSPECT_ENABLED`(True) / `INSPECT_MAX_TILES`(4)。
**端到端实测(真实图纸 1 图框,`verify_review_maximized.py`)**:
- 取证与审查两阶段均在最大化态、结束精确还原 → 4 项全 PASS。
- LLM 的 11 次取证调用里 **2 次是 `inspect_region`**,且**都显式给了 `window` + `purpose:"text"`**:
`x[20000,115000] y[-6500,3000]` 与 `x[-4000,30000] y[-6500,3000]`——A0 图框
(`y[-6434,77666]`)的**底部横条**,即**标题栏**所在。模型自己把范围收窄到横条再放大。
- **验收口径达成**:报告报出标题栏**乱码/占位字符 `???????????????`**,
而 `get_text_content` 对全图返回 **0 条文字**(本图文字全在块/自定义实体内)——
即**块内文字是被视觉放大读出来的**,不是结构化数据给的。