Files
cad-agent/ai.memory/project_review_agent_impl.md
T
gjm 17c202549e docs(memory): 记录可信度三件套落地、window 参数实机事实与 CAD 自启偏好
- project_review_agent_impl:三件套链路(结构化结论删除二次抽取、确定性检查前置、
  handle 交叉校验)、开关与数据结构;find_zero_length/find_duplicate_entities 的
  window 参数 schema 未声明但实机生效
- feedback_preferences:需要 CAD 时自己启动(自动加载已配好),不要先问用户
2026-10-14 19:54:53 +08:00

142 lines
11 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: LLM 识图审图 Agent —— 架构决策(已定稿)
description: cad_mcp_frame + 自研 Agent 的架构决策与分阶段路线——混合式(流水线骨架+受控只读取证子循环)、基座 B 最小集、可信度三件套(确定性检查前置/结构化结论/handle 交叉校验)、标准化路线(先 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` 等重复脚本
(这 5 个遗留脚本已于 2026-10-14 删除,见 [环境与外部资源](reference_environment.md))。
- `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-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` 均正常。