国动真实投标文件查标一次(check-eval §14):44 个问题里有用的 9 个, 致命、重要两级都是误报。按决策 E3 全部修(§15): - 响应错位:「完全满足,无偏离。」这类复合肯定写法整体算肯定 - 附加条件:「不以……另行收费」是承诺 - 扫描件:文字层 v5 记每页图片占比,带一行说明的扫描页也认;提示写明扫描页范围 - 不需要这家投标人回答的:无特定资格要求、总括性资格要求、中标后才做的事、 投标文件没声称适用的联合体和价格扣除 - 「方案应明确 A、B、C……」逐项再检索 - 同一批送出的片段都可引用(引文照样核对);出处没过校验的不沿用 国动重新检查 44 → 11(全是提示,有用的 9 个都在);开发集召回 112 → 111 (模型重跑波动),致命级 25/32 不变,规矩的投标文件问题 239 → 213。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
6.7 KiB
Python
129 lines
6.7 KiB
Python
"""由每条清单项的检查结论生成问题(FR-M3-03、m3-plan §2.3)。
|
||
|
||
- 级别从清单项算,不让模型定(参考项目让模型定级别,模型可以把高危项降成低危);
|
||
- 说明只写事实和出处,不写整改建议(报价类问题的「建议」会写出数字,红线 6);
|
||
- 「已响应」不生成问题,结论留在 check_item_result 里。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from typing import Any
|
||
|
||
from bid_doctor.check.match import Result
|
||
|
||
FATAL_CATEGORIES = {"qualification", "invalidity"}
|
||
REASONS = {
|
||
"evidence_missing": "没有给出投标原文",
|
||
"sid_format": "引用的句子编号写法不对",
|
||
"sid_unknown": "引用的句子不在候选范围内",
|
||
"quote_not_original": "引文和投标原文对不上",
|
||
"quote_too_short": "引文太短,无法核对",
|
||
"missing_answer": "这一条没有得到判断结果",
|
||
"model_error": "模型服务暂时不可用",
|
||
"over_budget": "超出本次检查的预算",
|
||
"response_mismatch": "响应格写的是别的内容",
|
||
}
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class Spec:
|
||
type: str
|
||
severity: str
|
||
title: str
|
||
explanation: str
|
||
detail: dict[str, Any]
|
||
|
||
|
||
def severity(item: dict[str, Any]) -> str:
|
||
"""★ 条款、法规列明或原文写明无效的、资格条件 → 致命;▲ 参数、评分项(缺证明材料)→ 重要;其余 → 提示。"""
|
||
cats = set(item.get("categories") or [])
|
||
if item.get("importance") == "substantive" or cats & FATAL_CATEGORIES:
|
||
return "fatal"
|
||
if item.get("importance") == "important" or "scoring" in cats:
|
||
return "important"
|
||
return "hint"
|
||
|
||
|
||
def _pages(pages: list[int]) -> str:
|
||
return "、".join(f"第 {p} 页" for p in pages)
|
||
|
||
|
||
def spec(result: Result, item: dict[str, Any]) -> Spec | None:
|
||
out = _spec(result, item)
|
||
note = result.detail.get("qualification_note")
|
||
if out is None or not note:
|
||
return out
|
||
return Spec(out.type, out.severity, out.title, f"{out.explanation}{note}", out.detail)
|
||
|
||
|
||
def _spec(result: Result, item: dict[str, Any]) -> Spec | None:
|
||
if result.detail.get("superseded_by"):
|
||
return None # 资质匹配出了更具体的问题(check/qualification.py),扫描件兜底那条不再单独出
|
||
sev = severity(item)
|
||
name = str(item.get("title") or "")
|
||
base = {"importance_basis": item.get("importance_basis"), **result.detail}
|
||
scope = "、".join(result.detail.get("search_scope") or [])
|
||
if result.verdict in ("responded", "implicit_accepted"):
|
||
return None
|
||
if result.verdict == "implicit_review":
|
||
text = (
|
||
"招标文件规定未列入偏离表的视为接受,投标文件里没有找到对这条要求的明确响应。"
|
||
"这是实质性要求,请人工核对所投内容确实满足。"
|
||
)
|
||
return Spec("manual_review", "hint", f"请人工核对:{name}", text, {**base, "mode": "implicit"})
|
||
if result.verdict == "not_found" and result.evidence_status == "unverified":
|
||
claimed = result.detail.get("claimed_pages") or []
|
||
where = f"{_pages(claimed)}有响应" if claimed else "有响应"
|
||
why = REASONS.get(result.reason_code or "", result.reason_code or "")
|
||
text = f"模型认为投标文件{where},但出处没能核对({why}),按「未找到响应」处理。请人工核对。"
|
||
return Spec("unresponded", sev, f"未找到响应:{name}", text, {**base, "evidence_status": "unverified"})
|
||
if result.verdict == "not_found" and result.detail.get("mismatch"):
|
||
text = (
|
||
f"投标文件照抄了这条要求,但同一行的响应格写的是「{result.detail['mismatch'][:40]}」,"
|
||
"和这条要求对不上,像是别的条目的内容。请核对。"
|
||
)
|
||
return Spec("unresponded", sev, f"响应可能对不上:{name}", text, base)
|
||
if result.verdict == "not_found":
|
||
text = f"在投标文件({scope})中没有找到对这条要求的响应。" if scope else "没有找到对这条要求的响应。"
|
||
return Spec("unresponded", sev, f"未找到响应:{name}", text, base)
|
||
if result.verdict == "negative_deviation" and result.detail.get("deviation_unclear"):
|
||
text = (
|
||
f"这一行的偏离栏写的是「{result.detail.get('direct_read')}」,不是明确的「无偏离 / 满足」,"
|
||
"请核对是否满足这条要求。"
|
||
)
|
||
return Spec("negative_deviation", "important" if sev == "fatal" else sev, f"偏离栏需核对:{name}", text, base)
|
||
if result.verdict == "negative_deviation":
|
||
cell = result.detail.get("direct_read")
|
||
text = (
|
||
f"投标文件这一行的偏离 / 响应栏写着「{cell}」,没有找到说明。"
|
||
if cell
|
||
else "投标文件写明了偏离或不满足,没有找到说明。"
|
||
)
|
||
return Spec("negative_deviation", sev, f"负偏离未说明:{name}", text, base)
|
||
if result.verdict == "suspected_unmet":
|
||
value = result.detail.get("response_value")
|
||
text = (
|
||
f"投标文件给出的值({value})按要求值判断可能不满足。"
|
||
if value
|
||
else "投标文件给出的值按要求值判断可能不满足。"
|
||
)
|
||
return Spec("suspected_unmet", sev, f"疑似不满足:{name}", text, base)
|
||
if result.verdict == "no_value":
|
||
text = "投标文件只写了满足或响应,没有给出要求里要的具体值或材料。"
|
||
return Spec("suspected_unmet", "hint", f"未给出具体值:{name}", text, {**base, "kind": "no_value"})
|
||
if result.verdict == "scanned":
|
||
total = result.detail.get("scanned_total") or 0
|
||
where = result.detail.get("scanned_where")
|
||
# 只说扫描件在哪几页,不说「材料可能在扫描件里」:扫描件上是不是这份材料,没有 OCR 分不出来
|
||
text = (
|
||
f"投标文件文字部分没有找到这份材料。{where} 是扫描件(共 {total} 页),系统读不出上面的文字;"
|
||
"材料如果附在这些页上,请人工核对,没有附就是缺了。"
|
||
if where
|
||
else f"投标文件文字部分没有找到这份材料。投标文件里有 {total} 页扫描件,系统读不出上面的文字,"
|
||
"材料可能在扫描件页上,请人工核对。"
|
||
)
|
||
return Spec("manual_review", "hint", f"请人工核对(文字部分没找到):{name}", text, base)
|
||
why = REASONS.get(result.reason_code or "", result.reason_code or "")
|
||
return Spec("manual_review", sev, f"未判定:{name}", f"这条要求本次没有完成判断({why}),请人工核对。", base)
|