docs: 查标评测开发集第一版(14 份投标文件)结果;汇总脚本 report.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-04 13:15:57 +08:00
parent 85d6ba67b8
commit 912c2b8acf
4 changed files with 118 additions and 5 deletions

View File

@@ -208,4 +208,5 @@ samples/
- 2026-11-04:查标第 1 步(链路打通)完成:上传投标分册(类型、版本、加密格式识别)→ 检查(六个阶段,响应匹配先只有检索 + 模型判断,提示词 `check.match` v1)→ 每条清单项留带证据的结论 → 生成问题 → 逐条处理(致命确认无误要写说明并进误报反馈)→ 复查沿用状态;计费三种方式和失败退回都有测试,后端 80 个测试通过。前端加了上传投标文件并检查页和查标页。SK1、SK2 的补丁已做。真实模型在虚构样例上走通了链路,效果还没评。见 `docs/architecture/m3-plan.md` 进度一节。
- 2026-11-04:查标第 0 步(评测集)。规范 v0.2(`evals/check/GUIDELINES.md`,Fable 审查后修改):响应方式按「文件 × 类别」记,规矩投标文件靠构造保证,问题只挂复核过的要求,留出集词库和风格 C 封存。C12(响应方式)、E1(复核由 Fable 做、用户抽查 20%)已确认。开发集 S02、S04、S10 六份试跑(`docs/architecture/check-eval.md` §1):查标召回 38%,规矩投标文件的误报大头是不需要响应的条目、表格里看不见同一行的响应格、扫描件和格式文件找不到。
- 2026-11-04:用户抽查 Fable 的复核 17 条,全部同意。
- 下一步:修评测集待修项、做风格 B、扩开发集;查标第 2 步(投标侧结构识别、规则、响应方式抽取)。
- 2026-11-04:查标评测开发集第一版:7 份招标文件、14 份投标文件(风格 A / B 轮换),注入问题 222 个、Fable 复核通过 188 个。查标召回 40%(致命级 49%),带标志词的问题只有 22%;规矩投标文件报出 699 个问题,近一半在不需要响应的要求上(`docs/architecture/check-eval.md` §2)。
- 下一步:用户填第二批复核抽查(`evals/out/check/review/spot-check-2.md`);修评测集待修项;查标第 2、3 步按 check-eval §2.2 的顺序改(整行上下文 / 偏离表直取 → 响应方式 → 扫描件和 presence)。

View File

@@ -46,8 +46,45 @@
| 「提供某文件」类要求被当成参数注入偏离 | 待修:推响应方式时识别 presence |
| 风格 B 的改写(截断、同义替换、只写值)还没做;所有数字都是风格 A 的 | 待做 |
## 2. 下一步
## 2. 开发集第一版(2026-11-04)
1. 修 §1.3 的待修项,做风格 B 的改写,再扩到开发集其余 4 份(S01、S05、S07、S08)。
2. 第 2 步(投标侧结构识别、规则、C12 响应方式抽取)和第 3 步(整行上下文、偏离表直取、presence 判断)按 §1.2 的方向改,每改一轮在开发集上重跑对比。
**范围**:开发集 7 份招标文件各两份投标文件,共 14 份:S02、S05、S08、S10 是 A 规矩 + B 马虎,S01、S04、S07 是 B 规矩 + A 马虎(GUIDELINES §4)。生成器修了试跑的待修项(「暂不支持」不再套在数值参数上、表格响应方式以档案为准、「提供……文件」不注入参数类问题),风格 B 由模型改写、数字对不上的改写丢弃。查标代码仍是第 1 步的版本。
**复核**:注入的要求项问题两批共 222 个,Fable 复核通过 188 个。第二批 140 个里不通过 22 个,其中 14 个在 S07:S07 须知要求逐条标明,格式注又写未列入视作接受,且只有 ★ 项写明未响应无效,非 ★ 参数删一行算不算问题拿不准,金标准从严不算(查标侧仍按 C12 取更严的逐条)。用户抽查第一批 17 条全部同意;第二批抽查 28 条(`evals/out/check/review/spot-check-2.md`)待填。
### 2.1 结果
| 指标 | 结果 |
|---|---|
| 查标召回(复核通过的问题) | 51 / 128(40%);风格 A 25 / 70(36%),风格 B 26 / 58(45%) |
| 端到端召回(含读标漏抽) | 51 / 132(39%) |
| 致命级召回 | 18 / 37(49%) |
| 不带标志词 / 带标志词 | 40 / 81(49%)/ 11 / 51(22%) |
| 连续一段 / 单点 | 7 / 12(58%)/ 44 / 120(37%) |
| 规矩的投标文件报出的问题 | 清单 2049 条,报出 699 个 |
| 其中规矩响应的要求上的误报 | 153 / 620(每 100 条 24.7) |
| 其中不需要响应的要求上报的 | 334 个(占 48%) |
| 干扰项误报 | 默认接受的要求没复述(T06)39,改写后的要求(T02)20,响应写在别的分册(T01)3 |
| 费用 | 14 次检查输入 150 万 token、输出 25 万 token |
按问题类型:资格证明缺失(D11)9 / 14、评分材料缺失(D13)9 / 12、漏响应(D01)15 / 23 较好;负偏离未说明(D06)0 / 20、响应对象错位(D09)1 / 14、证明材料缺失(D02)2 / 9、写明不支持(D05)11 / 31 很差。
### 2.2 读法
- **试跑的四个判断都成立,而且更清楚了**:「带标志词」的问题(写明不支持、负偏离)召回只有 22%,比不带的还低,正是表格里同一行的响应格、偏离格不在模型输入里;风格 B 反而比 A 高,因为 A 照抄要求原文,检索更容易命中照抄的那一格。规矩的投标文件上近一半的问题落在不需要响应的要求上,默认接受的要求被报了 39 次。
- **第 2、3 步按这个顺序改收益最大**:① 整行上下文 / 偏离表直取(直接对应 D05、D06、D09,51 个问题里的 45 个);② 响应方式和「这条要不要投标人回答」(C12,对应 334 + 39 个误报);③ 扫描件兜底和 presence 判断(证明材料、格式文件类误报)。
### 2.3 评测集还要改的
| 问题 | 状态 |
|---|---|
| 「暂不支持」「可选配置」仍会套在证书、供货清单、服务义务、文档交付这类非功能要求上(两批共 12 条被复核拒掉) | 待修:注入前按「功能类要求」过滤 |
| 同一要求被两个标注单元挂了同一个问题(实质性 + 技术参数、资格 + 符合性,第二批约 20 对);「第二十二条总述」这类伞状单元和具体条目不重叠 | 待修:注入前按 anchor 重叠合并标注单元;伞状单元不挂问题 |
| 条件性要求(「如依法免税则须提供」)被当作必须提供的材料挂 D11 | 待修:按投标人情况排除 |
| 低密度(每份 5~8 个问题)的投标文件还没做 | 待做:选 3 份 |
## 3. 下一步
1. 修 §2.3 的待修项,补 3 份低密度的投标文件。
2. 第 2 步(投标侧结构识别、规则、C12 响应方式抽取)和第 3 步(整行上下文、偏离表直取、presence 判断)按 §2.2 的顺序改,每改一轮在开发集 14 份上重跑对比。
3. 留出集(S03、S06、S09,风格 B + C,封存的词库)和真实国动投标文件,在第 3 步冻结代码后只跑一次。

View File

@@ -8,7 +8,7 @@
| 步骤 | 状态 | 说明 |
|---|---|---|
| 第 0 步 评测集 | 试跑完成,继续扩充 | 规范 v0.2、留出集划分已定,留出集词库和风格 C 已封存;生成、注入、渲染、离线查标、评分工具已写;开发集 S02、S04、S10 六份试跑完,结果和发现见 `check-eval.md` §1 |
| 第 0 步 评测集 | 试跑完成,继续扩充 | 规范 v0.2、留出集划分已定,留出集词库和风格 C 已封存;生成、注入、渲染、离线查标、评分工具已写;开发集 7 份招标文件、14 份投标文件第一版跑完,结果和发现见 `check-eval.md` §2 |
| 第 1 步 链路打通 | ✅ 2026-11-04 | 见下方「第 1 步落地记录」 |
| 第 2~6 步 | 未开始 | |

75
evals/check/report.py Normal file
View File

@@ -0,0 +1,75 @@
"""汇总一批投标文件的查标评分(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"- 规矩的投标文件:清单 {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()