feat(review): 审查阶段对齐绘图区分辨率 + 放大取证闭环
发现的缺口:maximized_drawing_area 原先只包住取证循环,审查阶段没被包住 → LLM 在受控子循环里 zoom + 截图拿到的是未最大化的 2110x462(正是上一轮刚解决的 低分辨率问题,在最需要放大看细节的审查阶段又出现了);且审查中的 zoom 不复位视口, 跑完把用户的 CAD 留在最后一次放大处。 改动: 1. __main__.run_review:把 maximized_drawing_area 从「只包取证」扩到「包取证 + 审查」 (两者都要截图);report/annotate 留在 with 之外。 2. phases/review.py:新增 _VIEW_TOOLS;review_frame 拆成外壳 + _review_frame, 外壳 try/finally 里仅当确实动过视口才复位到该图框整幅视图 (zoom_to_window(win, margin=0.02))。纯查结构化数据的路径不受影响。 3. checklist.build_system_prompt:把「可调用只读工具」改写成明确的放大取证协议—— 截图看不清时用 zoom_to_window(坐标由图框/实体 bbox 算)或 zoom_to_entity, 再 get_viewport_screenshot 复核;并约束不要为看图而反复放大。 次生问题(同轮修复):_MAX_OBS_IMAGES 原为硬编码 3,而模型一轮内会发 8 次截图 → 拍了 8 张只回灌 3 张。改为配置项 REVIEW_OBS_IMAGES(默认 6),并把预算耗尽的占位 文案改成明确告知模型「不要再请求截图,请给出结论」。 实测(真实图纸 1 图框,新增 verify_review_maximized.py):取证与审查两阶段窗口均为 [-4,-2,3444,1442](最大化态),结束精确还原 [68,0,2316,960],三项全 PASS; 子循环里 LLM 连发 8 组 zoom_to_window + get_viewport_screenshot,提示词生效。 pytest 304 passed。
This commit is contained in:
1 parent
931a430ba4
commit
e8140f2bc5
11 files changed
+320
-40
No files matched your search
@@ -65,6 +65,14 @@ def test_build_system_prompt_tool_hint_toggle():
|
||||
assert "只读" not in without
|
||||
|
||||
|
||||
def test_build_system_prompt_instructs_zoom_when_unreadable():
|
||||
prompt = build_system_prompt(DEFAULT_CHECKLIST, allow_tools=True)
|
||||
assert "看不清" in prompt
|
||||
assert "zoom_to_window" in prompt
|
||||
assert "zoom_to_entity" in prompt
|
||||
assert "get_viewport_screenshot" in prompt
|
||||
|
||||
|
||||
def test_build_system_prompt_requires_structured_json_output():
|
||||
# allow_tools=False 以隔离「工具提示块」里的「只读」字样
|
||||
prompt = build_system_prompt(DEFAULT_CHECKLIST, allow_tools=False)
|
||||
|
||||
@@ -101,6 +101,11 @@ def test_tiling_defaults_are_sane():
|
||||
assert config.HEATMAP_CELL_DIV > 0
|
||||
|
||||
|
||||
def test_review_obs_images_default_is_sane():
|
||||
# 审查子循环回灌截图上限:须为正,且不低于旧硬编码值 3
|
||||
assert config.REVIEW_OBS_IMAGES >= 3
|
||||
|
||||
|
||||
def test_drawing_area_constants_have_expected_types():
|
||||
assert isinstance(config.DRAWING_AREA_MAXIMIZE, bool)
|
||||
assert isinstance(config.DRAWING_AREA_ENTER_WAIT, float)
|
||||
|
||||
@@ -260,3 +260,82 @@ def test_review_frame_includes_multiple_image_blocks():
|
||||
assert len(image_blocks) == 2
|
||||
assert any(b.get("text") == "(截图:overall)" for b in user_content)
|
||||
assert any(b.get("text") == "(截图:tile-1)" for b in user_content)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 视口复位(放大取证不残留给用户)
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_review_frame_resets_viewport_after_zoom():
|
||||
llm = FakeLlmClient(
|
||||
script=[
|
||||
llm_calls([("zoom_to_window", {"min_x": 1, "min_y": 2, "max_x": 3, "max_y": 4})]),
|
||||
llm_text("done"),
|
||||
]
|
||||
)
|
||||
client = FakeMcpClient()
|
||||
ctx = make_ctx(client=client, llm=llm)
|
||||
review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
zooms = client.called("zoom_to_window")
|
||||
assert len(zooms) == 2 # LLM 放大一次 + 结束时复位一次
|
||||
reset = zooms[-1]["arguments"]
|
||||
assert (reset["min_x"], reset["min_y"]) == (0.0, 0.0) # 复位到图框整幅视图
|
||||
assert (reset["max_x"], reset["max_y"]) == (100.0, 50.0)
|
||||
assert reset["margin"] == 0.02
|
||||
|
||||
|
||||
def test_review_frame_resets_viewport_after_zoom_to_entity():
|
||||
llm = FakeLlmClient(
|
||||
script=[llm_calls([("zoom_to_entity", {"handle": "A1"})]), llm_text("done")]
|
||||
)
|
||||
client = FakeMcpClient()
|
||||
ctx = make_ctx(client=client, llm=llm)
|
||||
review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert client.called_names().count("zoom_to_window") == 1
|
||||
|
||||
|
||||
def test_review_frame_resets_viewport_at_iteration_limit():
|
||||
llm = FakeLlmClient(
|
||||
script=[llm_calls([("zoom_to_window", {})]), llm_text("收口结论")]
|
||||
)
|
||||
client = FakeMcpClient()
|
||||
ctx = make_ctx(client=client, llm=llm, max_iters=1)
|
||||
out = review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert out["summary"] == "收口结论"
|
||||
assert client.called_names().count("zoom_to_window") == 2 # 强制收口路径也复位
|
||||
|
||||
|
||||
def test_review_frame_does_not_touch_viewport_without_zoom():
|
||||
llm = FakeLlmClient(script=[llm_calls([("get_layers", {})]), llm_text("x")])
|
||||
client = FakeMcpClient()
|
||||
ctx = make_ctx(client=client, llm=llm)
|
||||
review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert "zoom_to_window" not in client.called_names()
|
||||
|
||||
|
||||
def test_review_frame_viewport_reset_failure_is_swallowed():
|
||||
seen = {"n": 0}
|
||||
|
||||
def zoom(_args):
|
||||
seen["n"] += 1
|
||||
if seen["n"] > 1:
|
||||
raise McpToolError("复位失败")
|
||||
return ok_text({})
|
||||
|
||||
llm = FakeLlmClient(
|
||||
script=[llm_calls([("zoom_to_window", {})]), llm_text("结论")] # noqa: E501
|
||||
)
|
||||
client = FakeMcpClient(responses={"zoom_to_window": zoom})
|
||||
ctx = make_ctx(client=client, llm=llm)
|
||||
out = review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert out["summary"] == "结论" # 复位失败不影响结论
|
||||
assert seen["n"] == 2
|
||||
|
||||
|
||||
def test_review_frame_no_viewport_reset_without_window():
|
||||
llm = FakeLlmClient(script=[llm_calls([("zoom_to_window", {})]), llm_text("x")])
|
||||
client = FakeMcpClient()
|
||||
ctx = make_ctx(client=client, llm=llm)
|
||||
ev = make_evidence()
|
||||
ev["window"] = None # 非 CAD 源(无窗口)→ 无从复位
|
||||
review_frame(ctx, ev, "sys", log=lambda *a: None)
|
||||
assert client.called_names().count("zoom_to_window") == 1
|
||||
Reference in new issue
Block a user