feat(check): 偏离栏直读、响应错位判断;复查复用和硬预算(C2)
- deviation.py:模型判已响应的条目,证据所在表格行的偏离格写着「偏离 / 部分满足」等 就改判负偏离;照抄要求格、响应格写的是别的内容的改判「响应可能对不上」 - reuse.py:按「提示词 + 要求 + 候选句原文」算缓存键,存模型原始结论;复查时内容没变、 证据找得回的条目直接沿用,不调模型 - 每次检查送给模型的字数有上限,超出的条目列为请人工核对,写进覆盖范围 - 开发集 17 份:召回 61% → 73%,负偏离未说明 64% → 90%,规矩投标文件问题数不变 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -191,3 +191,91 @@ def test_manual_topics_merge_by_topic_and_skip_irrelevant() -> None:
|
||||
assert list(got) == ["DECRYPT", "SIGN_ENCRYPT", "PAPER", "SUBMIT"]
|
||||
assert got["SIGN_ENCRYPT"] == ["须用CA数字证书加密投标文件"]
|
||||
assert got["SUBMIT"][0].startswith("投标文件递交截止") # 写明截止时间的做代表
|
||||
|
||||
|
||||
def test_direct_read_turns_negative_deviation_cell_into_finding() -> None:
|
||||
"""偏离栏直读:证据句所在的表格行里偏离格写「偏离」,模型判了已响应也改判负偏离(check-eval §8)。"""
|
||||
from bid_doctor.check import deviation
|
||||
|
||||
def c(seq: int, text: str, x0: float, y: float) -> Sentence:
|
||||
s = Sentence(key=f"b:p1:s{seq}", page_no=1, seq=seq, text=text, quads=[[x0 + 2, y + 4, x0 + 90, y + 16]])
|
||||
s.cell = [x0, y, x0 + 100, y + 30]
|
||||
return s
|
||||
|
||||
req, resp, dev = c(1, "支持链路聚合", 100, 50), c(2, "满足,支持链路聚合", 200, 50), c(3, "偏离", 300, 50)
|
||||
req2, resp2, dev2 = c(4, "支持堆叠", 100, 80), c(5, "满足,支持堆叠", 200, 80), c(6, "无偏离", 300, 80)
|
||||
prose = Sentence(
|
||||
key="b:p1:s7", page_no=1, seq=7, text="如不满足上述要求,按负偏离处理。", quads=[[74, 120, 500, 132]]
|
||||
)
|
||||
d = match.BidDoc(uuid4(), 1, "技术分册", [req, resp, dev, req2, resp2, dev2, prose])
|
||||
results = {
|
||||
uuid4(): match.Result("responded", "ai", "verified", evidence=[(d, resp)]),
|
||||
uuid4(): match.Result("responded", "ai", "verified", evidence=[(d, resp2)]),
|
||||
uuid4(): match.Result("responded", "ai", "verified", evidence=[(d, prose)]),
|
||||
}
|
||||
assert deviation.direct_read(results, [d]) == 1
|
||||
first, second, third = results.values()
|
||||
assert first.verdict == "negative_deviation" and [s.text for _, s in first.evidence] == [
|
||||
"满足,支持链路聚合",
|
||||
"偏离",
|
||||
]
|
||||
assert first.detail["model_verdict"] == "responded"
|
||||
assert second.verdict == third.verdict == "responded"
|
||||
|
||||
|
||||
def test_mismatch_read_only_when_response_cell_is_unrelated() -> None:
|
||||
"""响应对象错位(D09):证据只是照抄的要求格、同一行响应格写的是别的内容 → 未找到响应。
|
||||
指引、相关内容、只写满足的不算。"""
|
||||
from bid_doctor.check import deviation
|
||||
|
||||
def c(seq: int, text: str, x0: float, y: float) -> Sentence:
|
||||
s = Sentence(key=f"b:p1:s{seq}", page_no=1, seq=seq, text=text, quads=[[x0 + 2, y + 4, x0 + 90, y + 16]])
|
||||
s.cell = [x0, y, x0 + 100, y + 30]
|
||||
return s
|
||||
|
||||
rows = [
|
||||
("支持云管理、VLAN隔离、标准交换三种工作模式", "显示器升降行程:150mm"),
|
||||
("支持端口汇聚、端口流量统计", "满足,支持端口汇聚和端口流量统计"),
|
||||
("批量数据处理在非工作时段内完成", "满足,详见技术方案第12页"),
|
||||
("系统支持国产操作系统", "满足"),
|
||||
]
|
||||
sents, results, items = [], {}, {}
|
||||
for n, (req, resp) in enumerate(rows):
|
||||
a, b, dev = (
|
||||
c(n * 3 + 1, req, 100, 50 + n * 40),
|
||||
c(n * 3 + 2, resp, 200, 50 + n * 40),
|
||||
c(n * 3 + 3, "无偏离", 300, 50 + n * 40),
|
||||
)
|
||||
sents += [a, b, dev]
|
||||
k = uuid4()
|
||||
items[k] = {"title": req, "requirement_text": req}
|
||||
results[k] = (a, b)
|
||||
d = match.BidDoc(uuid4(), 1, "技术分册", sents)
|
||||
res = {k: match.Result("responded", "ai", "verified", evidence=[(d, a)]) for k, (a, _) in results.items()}
|
||||
assert deviation.mismatch_read(res, items, [d]) == 1
|
||||
verdicts = [r.verdict for r in res.values()]
|
||||
assert verdicts == ["not_found", "responded", "responded", "responded"]
|
||||
first = next(iter(res.values()))
|
||||
assert first.detail["mismatch"] == "显示器升降行程:150mm" and first.reason_code == "response_mismatch"
|
||||
|
||||
|
||||
async def test_over_budget_items_are_undetermined_without_model_calls() -> None:
|
||||
"""硬预算(C2):超出预算的批次不调模型,条目记「超出预算」,覆盖范围另外写明。"""
|
||||
from bid_doctor.core.db import TenantContext
|
||||
|
||||
d, _ = bid()
|
||||
items = {
|
||||
uuid4(): {
|
||||
"title": "服务器内存不低于32GB",
|
||||
"requirement_text": "服务器内存不低于32GB",
|
||||
"categories": ["tech_param"],
|
||||
}
|
||||
}
|
||||
|
||||
async def progress(_: float) -> None:
|
||||
return None
|
||||
|
||||
ctx = TenantContext(tenant_id=uuid4(), user_id=uuid4())
|
||||
results, stats = await match.run(ctx, uuid4(), items, [d], progress, budget=10)
|
||||
(r,) = results.values()
|
||||
assert (r.verdict, r.reason_code, stats.calls, stats.over_budget) == ("undetermined", "over_budget", 0, 1)
|
||||
|
||||
Reference in New Issue
Block a user