feat(review): 审查结论改为结构化 JSON,移除二次 LLM 抽取
- checklist.py:系统提示加入结构化输出契约(只输出 {summary, issues[]},
引用 handle 必须真实存在、不得编造)
- review.py:review_frame 返回 {summary, issues, raw};仅在「不再请求工具」的回合
解析结论,中间工具回合的 content 永不解析;强制收口那次 chat 不传 tools
- report.py:删除 extract_issues 与 _EXTRACT_SYSTEM(不再需要第二次 LLM 调用);
新增 format_finding(用 .get,顺带修复缺 frame 键 KeyError);save_report 的
issues 省略时从 findings 拍平
- __main__.py:主流程改为 确定性检查 → review_frame → build_finding(合并+校验),
issues 由 flatten_issues 派生
顺带修复既有 gap:summarize 硬下标 KeyError、extract_issues 单行围栏解析失败。
This commit is contained in:
1 parent
f6d61f8d07
commit
f7fa8f14d9
8 files changed
+224
-140
No files matched your search
+41
-5
@@ -111,11 +111,12 @@ def test_readonly_empty_response_marker():
|
||||
# ---------------------------------------------------------------------------
|
||||
# review_frame
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_review_frame_returns_text_when_no_tools():
|
||||
def test_review_frame_returns_summary_when_no_tools():
|
||||
llm = FakeLlmClient(script=[llm_text("结论A")])
|
||||
ctx = make_ctx(client=FakeMcpClient(), llm=llm)
|
||||
out = review_frame(ctx, make_evidence(), "系统提示", log=lambda *a: None)
|
||||
assert out == "结论A"
|
||||
assert out["summary"] == "结论A" # 非 JSON → 降级为纯文本 summary
|
||||
assert out["issues"] == []
|
||||
assert len(llm.calls) == 1
|
||||
assert llm.calls[0]["tools"] is not None # 传了只读工具 schema
|
||||
|
||||
@@ -147,7 +148,7 @@ def test_review_frame_runs_tool_then_concludes():
|
||||
client = FakeMcpClient(responses={"query_entities": ok_text({"total": 1})})
|
||||
ctx = make_ctx(client=client, llm=llm)
|
||||
out = review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert out == "结论B"
|
||||
assert out["summary"] == "结论B"
|
||||
assert len(llm.calls) == 2
|
||||
assert client.called_names() == ["query_entities"]
|
||||
|
||||
@@ -179,7 +180,7 @@ def test_review_frame_force_closes_at_iteration_limit():
|
||||
)
|
||||
ctx = make_ctx(client=FakeMcpClient(), llm=llm, max_iters=1)
|
||||
out = review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert out == "收口结论"
|
||||
assert out["summary"] == "收口结论"
|
||||
last_msg = llm.calls[-1]["messages"][-1]
|
||||
assert last_msg["role"] == "user"
|
||||
assert "上限" in last_msg["content"]
|
||||
@@ -189,7 +190,7 @@ def test_review_frame_with_zero_iters_forces_immediate_close():
|
||||
llm = FakeLlmClient(script=[llm_text("直接结论")])
|
||||
ctx = make_ctx(client=FakeMcpClient(), llm=llm, max_iters=0)
|
||||
out = review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert out == "直接结论"
|
||||
assert out["summary"] == "直接结论"
|
||||
assert len(llm.calls) == 1
|
||||
|
||||
|
||||
@@ -207,3 +208,38 @@ def test_review_frame_payload_is_json_serializable():
|
||||
review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
text = llm.calls[0]["messages"][1]["content"][0]["text"]
|
||||
json.loads(text.split(":\n", 1)[1]) # 不抛异常即通过
|
||||
|
||||
|
||||
def test_review_frame_parses_structured_json_conclusion():
|
||||
text = (
|
||||
'{"summary": "整体尚可", "issues": ['
|
||||
'{"category": "几何", "severity": "medium", "message": "零长度", "handle": "A1"}]}'
|
||||
)
|
||||
llm = FakeLlmClient(script=[llm_text(text)])
|
||||
ctx = make_ctx(client=FakeMcpClient(), llm=llm)
|
||||
out = review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert out["summary"] == "整体尚可"
|
||||
assert out["issues"] == [
|
||||
{"category": "几何", "severity": "medium", "message": "零长度", "handle": "A1"}
|
||||
]
|
||||
|
||||
|
||||
def test_review_frame_parses_fenced_json():
|
||||
llm = FakeLlmClient(script=[llm_text('```json\n{"summary": "s", "issues": []}\n```')])
|
||||
ctx = make_ctx(client=FakeMcpClient(), llm=llm)
|
||||
out = review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert out["summary"] == "s"
|
||||
assert out["issues"] == []
|
||||
|
||||
|
||||
def test_review_frame_intermediate_tool_turn_json_not_parsed():
|
||||
# 中间工具回合的 content 里即使塞了 JSON,也不能被当成最终结论
|
||||
intermediate = '{"summary": "这不是最终结论", "issues": [{"message": "x"}]}'
|
||||
llm = FakeLlmClient(script=[
|
||||
llm_calls([("query_entities", {})], content=intermediate),
|
||||
llm_text('{"summary": "最终结论", "issues": []}'),
|
||||
])
|
||||
ctx = make_ctx(client=FakeMcpClient(), llm=llm)
|
||||
out = review_frame(ctx, make_evidence(), "sys", log=lambda *a: None)
|
||||
assert out["summary"] == "最终结论"
|
||||
assert out["issues"] == []
|
||||
Reference in new issue
Block a user