feat(check): 响应方式识别(C12)和「这条要不要投标人回答」;评测集纠错

- checklist/response_mode.py:按类别(技术 / 商务)识别招标文件的响应方式(逐条响应 / 默认接受),规则优先;
  两种写法冲突按逐条,认不出按逐条并写进覆盖范围。
- check/applicability.py:无效情形(★ / 斜体下划线标注的条款除外)、日期与递交、没要求附材料的评分标准、
  无需提供证明或由采购人查询的资格,不逐条找响应,写进覆盖范围;要求附证明材料按原文判断,中标后才交的不算。
- 默认接受的要求没找到响应:一般要求记 implicit_accepted、不出问题;实质性要求记 implicit_review、提示级
  人工核对;要附证明材料的不免(迁移 0006)。
- match.py:修同一行单元格又是别的条目候选句时的 KeyError(每条清单项单独记候选句,句子从总表里查)。
- 评测集:注入器同一要求的所有表格行一起改;散文、附件、格式文件里也响应了的要求不挂漏响应类问题;
  错位响应必须是另一个参数;复核判不成立的单元不再挂;regen.py 按固定种子重新生成。

开发集 17 份:规矩投标文件报出的问题 639 → 229,误报 21.3 → 12.4 / 100,召回 68% → 65%,致命级 20 → 21 / 32
(docs/architecture/check-eval.md §4)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-04 14:25:14 +08:00
parent b7432643dd
commit cb0c2fcafe
13 changed files with 492 additions and 21 deletions

View File

@@ -0,0 +1,56 @@
"""每条清单项要不要在投标文件里找响应、按什么响应方式判(决策 C12,`docs/architecture/check-eval.md` §2.2 第 ② 项)。
开发集上规矩的投标文件报出的问题,近一半落在不需要投标人回答的条目上(无效情形的列举、日期、评分标准、
采购人自己查询的资格)。这些条目不逐条找响应,写进检查覆盖范围:
- 无效情形、评审规定(只有「无效」类别、不是标注出来的条款):由规则检查(第 2 步)和人工核对覆盖;
- 日期与递交:由规则检查(落款日期、有效期)和需人工核对覆盖;
- 评分标准本身:没写要附什么材料的不找;写了的按证明材料找;
- 资格条件里写明「无需提供」或由采购人查询(信用中国)的不找。
其余按类别取招标文件的响应方式:技术参数用技术类,其余用商务类。
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Any
from bid_doctor.checklist.response_mode import Mode
# 要求投标时附证明材料(读标的 evidence_required 常常是空的,按原文判断);中标后 / 签合同时才交的不算
EVIDENCE = re.compile(r"(提供|附|出具)[^。;]{0,20}(证明|报告|截图|复印件|扫描件|证书|合同|承诺|声明|社保|发票)")
AWARD = re.compile(r"(中标后|成交后|签订合同(前|时)|合同签订(前|时))")
NO_PROOF = re.compile(r"无需提供|不需提供|无须提供|采购人[^。;]{0,12}(查询|核查|查验)|信用中国|信用记录")
SKIP_LABELS = {
"condition": "无效情形和评审规定",
"schedule": "日期与递交要求",
"scoring": "评分标准(没要求附证明材料)",
"no_proof": "资格条件(写明无需提供证明,或由采购人查询)",
}
@dataclass(frozen=True, slots=True)
class Decision:
match: bool
mode: str = "explicit" # explicit / implicit
skip: str | None = None # 不找响应的原因(SKIP_LABELS 的键)
mode_status: str = "found"
evidence: bool = False # 要求投标时附证明材料:默认接受也不能免(没附就是没附)
def decide(item: dict[str, Any], modes: dict[str, Mode]) -> Decision:
cats = set(item.get("categories") or [])
text = f"{item.get('title') or ''}{item.get('requirement_text') or ''}"
if cats == {"invalidity"} and item.get("importance_basis") != "marker":
return Decision(False, skip="condition") # 「……的,投标无效」这类情形;★ / 斜体下划线标注的条款是要求,照常找
if "schedule" in cats and not cats & {"tech_param", "qualification", "scoring"}:
return Decision(False, skip="schedule")
evidence = bool(item.get("evidence_required") or EVIDENCE.search(text)) and not AWARD.search(text)
if cats <= {"scoring"} and not evidence:
return Decision(False, skip="scoring")
if "qualification" in cats and "tech_param" not in cats and NO_PROOF.search(text):
return Decision(False, skip="no_proof")
mode = modes.get("technical" if "tech_param" in cats else "commercial")
if mode is None:
return Decision(True, evidence=evidence)
return Decision(True, mode=mode.mode, mode_status=mode.status, evidence=evidence)