Files
bid-doctor/evals/check/report.py
YANG JIANKUAN 47c6a1258e feat(check): 文件要在、扫描件兜底(C10);响应方式存进清单并可改(C12)
- check/presence.py:「提供《授权委托书》」「须提交投标函」这类要求按标题里有没有这个文件名判断,不调模型。
- 扫描件兜底:要附证明材料的要求没找到、投标文件里有扫描件页时,改为提示级「可能在扫描件里,请人工核对」。
- 响应方式解析时识别、带依据句子存进 checklist.response_modes(迁移 0008);清单页显示、可改(写审计,
  重新解析不覆盖人工改过的);查标用存下来的。新接口 PUT /projects/{id}/checklist/response-modes/{category}。
- check_item_result.layer 加 presence(迁移 0007);契约同步默认接受的两种结论。

开发集 17 份:规矩投标文件的致命问题 120 → 51,致命级召回不变(22 / 32)(docs/architecture/check-eval.md §5)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:53:45 +08:00

77 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""汇总一批投标文件的查标评分(evals/check/GUIDELINES.md §7)。先对每份跑过 score.py。
cd server && ../.venv/bin/python ../evals/check/report.py --run base <投标文件> [...]
"""
from __future__ import annotations
import argparse
import json
import pathlib
from collections import Counter
OUT = pathlib.Path(__file__).resolve().parents[1] / "out" / "check"
def pct(a: int, b: int) -> str:
return f"{a}/{b}({a / b:.0%})" if b else "—"
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("bids", nargs="+")
ap.add_argument("--run", default="base")
a = ap.parse_args()
rec: Counter[str] = Counter()
tot: Counter[str] = Counter()
by_style: Counter[str] = Counter()
by_style_tot: Counter[str] = Counter()
fp = ok = unmapped = other = 0
traps: Counter[str] = Counter()
tokens = [0, 0]
findings_clean = items_clean = 0
for bid in a.bids:
d = OUT / bid
m = json.loads((d / "manifest.json").read_text())
s = json.loads((d / "runs" / a.run / "score.json").read_text())
r = json.loads((d / "runs" / a.run / "results.json").read_text())
tokens[0] += r["summary"]["usage"]["tokens_in"]
tokens[1] += r["summary"]["usage"]["tokens_out"]
for k, (hit, n) in s["recall"].items():
rec[k] += hit
tot[k] += n
if "查标" in s["recall"]:
by_style[m["style"]] += s["recall"]["查标"][0]
by_style_tot[m["style"]] += s["recall"]["查标"][1]
if m["kind"] == "clean":
fp += s["fp"]
ok += s["ok_units"]
other += s["other_findings"]
unmapped += s["unmapped"]
traps.update(s["fp_by_trap"])
findings_clean += sum(1 for it in r["items"] if it["finding"])
items_clean += len(r["items"])
lines = [
f"# 汇总:{len(a.bids)} 份投标文件 · 运行 {a.run}",
"",
f"- 查标召回:{pct(rec['查标'], tot['查标'])};端到端:{pct(rec['端到端'], tot['端到端'])};致命级:{pct(rec['致命级'], tot['致命级'])}",
"- 按风格:" + ";".join(f"{k} {pct(by_style[k], by_style_tot[k])}" for k in sorted(by_style_tot)),
"",
"| 分类 | 召回 |",
"|---|---|",
*[f"| {k} | {pct(rec[k], tot[k])} |" for k in sorted(tot) if k not in ("查标", "端到端", "致命级")],
f"| 召回里只是「扫描件待人工核对」的 | {rec['其中只是扫描件待人工核对']} |",
"",
f"- 规矩的投标文件:清单 {items_clean} 条,报出问题 {findings_clean} 个;其中规矩响应的要求上 {fp}/{ok}(每 100 条 {100 * fp / ok:.1f})"
if ok
else "",
f"- 规矩的投标文件里,不需要响应(na / unknown)的要求上报了问题 {other} 个,对不上标注单元的问题 {unmapped} 个",
f"- 干扰项误报:{dict(traps)}",
f"- token:输入 {tokens[0]:,},输出 {tokens[1]:,}({len(a.bids)} 次检查)",
]
print("\n".join(lines))
if __name__ == "__main__":
main()