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:
YANG JIANKUAN
2026-11-04 17:47:12 +08:00
parent 81c58798d2
commit df3ebbb6a1
12 changed files with 430 additions and 23 deletions

View File

@@ -339,3 +339,24 @@ async def test_manual_checks_are_kept_per_project(user: User) -> None:
assert any("列入「需人工核对」2 条" in x for x in again["coverage"]["not_checked"])
kept = (await user.get(f"/projects/{project_id}/manual-checks")).json()["items"]
assert [i["checked"] for i in kept] == [True, False]
async def test_recheck_reuses_unchanged_model_results(user: User) -> None:
"""复查复用(C2):同一份投标文件再查,送给模型的内容没变,模型那一层沿用上次的结论,不再调模型。"""
from bid_doctor.ai.models import AiCall
from bid_doctor.core.db import TenantContext, tenant_tx
project_id, bid, first = await checked_project(user)
second = await run_check(user, project_id, [bid["id"]])
assert second["counts"]["fatal"] == first["counts"]["fatal"]
reused = [x for x in second["coverage"]["checked"] if "沿用上次的结论" in x]
assert reused, second["coverage"]["checked"]
ctx = TenantContext(tenant_id=UUID(user.tenant_id), user_id=UUID(str(user.me["user"]["id"])))
async with tenant_tx(ctx) as s:
from sqlalchemy import func, select
n = (await s.execute(select(func.count()).where(AiCall.job_id == UUID(second["id"])))).scalar_one()
assert n == 0
results = (await user.get(f"/check-runs/{second['id']}/item-results")).json()["items"]
assert all(r["bid_citations"] for r in results if r["verdict"] == "responded") # 沿用的结论证据也找回来了