docs(evals): S01–S10 标注集 v1、评测工具和评测报告

- evals/gold:1,887 个标注单元(AI 标注、逐条裁定,未经人工复核),规范见 GUIDELINES.md
- prepare / run_all / score / gold:导出文字层、批量抽取、打分、标注定位
- docs/architecture/extract-eval.md:修复前后的结果(三项目标全部达标)和剩余问题
- decisions.md 记 M1(只用 Flash)和 Q1–Q4;架构 §13 SK13;AGENTS.md 进度

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 23:19:30 +08:00
parent f98fd51128
commit 602703c47c
21 changed files with 29427 additions and 18 deletions

View File

@@ -4,7 +4,7 @@
../.venv/bin/python ../evals/run_extract.py <文件> ../evals/out/<名字> [--v extract.tech_param=2 ...] [--only extract.substantive]
BD_AI_MODELS='{"reasoning": "deepseek-v4-pro"}' 换模型对比。
同一模型 + 同一提示词版本 + 同一输入只调用一次模型(缓存在 <输出目录>/../cache/),只改规则时重放不花钱。
输出:report.md(结果统计 + 模型输出里不合理模式的审计)、items.json(每条结果和模型原始输出)、normalized.pdf。
输出:report.md(结果统计 + 模型输出里不合理模式的审计)、items.json(每条结果和模型原始输出)、info.json(项目基本信息和统计)、normalized.pdf。
真实招标文件和输出都不进 git(evals/out/ 已忽略)。
"""
@@ -24,11 +24,12 @@ from uuid import UUID
from bid_doctor.ai import prompts
from bid_doctor.ai import service as ai_service
from bid_doctor.ai.providers.base import near_limit
from bid_doctor.ai.service import provider
from bid_doctor.checklist import extract
from bid_doctor.core.db import TenantContext
from bid_doctor.document import formats
from bid_doctor.document.pipeline import convert, structure, text_layer
from bid_doctor.document.pipeline import convert, text_layer
ap = argparse.ArgumentParser()
ap.add_argument("file")
@@ -39,7 +40,7 @@ args = ap.parse_args()
out = pathlib.Path(args.out)
out.mkdir(parents=True, exist_ok=True)
cache_dir = out.parent / "cache"
cache_dir = pathlib.Path(__file__).resolve().parent / "out" / "cache" # 各次评测共用
cache_dir.mkdir(exist_ok=True)
versions = dict(v.split("=") for v in args.v)
@@ -77,8 +78,8 @@ async def cached_run_json(ctx: Any, *, job_type: str, job_id: Any, task: str, pr
"incomplete": r.incomplete}
f.write_text(json.dumps(data, ensure_ascii=False))
calls.append({"task": task, "version": version, "user": user, **data})
if data.get("incomplete"):
raise ai_service.OutputIncomplete(task) # 和线上一样:输出被截断时由 extract.run 对半重跑
if data.get("incomplete") or near_limit(data.get("tokens_out")):
raise ai_service.OutputIncomplete(task, data["data"]) # 和线上一样:输出被截断时由 extract.run 对半重跑
return data["data"]
@@ -92,18 +93,12 @@ async def main() -> None:
(out / "normalized.pdf").write_bytes(pdf)
pages = text_layer.extract("D", pdf)
body = text_layer.body_sentences(pages)
notes = extract.marking_notes(body)
doc = extract.Doc(
sentences=body,
parents=structure.parents(body),
continuations=text_layer.continuations(body),
notes=notes,
declared={m for n in notes for m in n.markers},
)
doc = extract.build_doc(body)
ctx = TenantContext(tenant_id=UUID(int=1), user_id=UUID(int=1))
items, info, run_stats = await extract.run(ctx, UUID(int=2), doc)
by_sid = {f"p{s.page_no}s{s.seq}": s for s in body}
_sid = extract._sid # noqa: SLF001
by_sid = {_sid(s): s for s in body}
# ---------- 结果 ----------
zone = Counter((i.zone, i.pending_reason) for i in items)
cats = Counter((c, i.zone) for i in items for c in i.categories)
@@ -115,6 +110,8 @@ async def main() -> None:
"categories": i.categories,
"title": i.summary,
"importance": i.importance,
"importance_basis": i.importance_basis,
"basis": i.basis,
"sentences": [f"p{s.page_no}s{s.seq}" for s in i.sentences],
"quote_pages": sorted({s.page_no for s in i.sentences}),
"raw": i.ai_values,
@@ -122,6 +119,15 @@ async def main() -> None:
for i in items
]
(out / "items.json").write_text(json.dumps(res, ensure_ascii=False, indent=1))
info_out = {
"fields": {k: {"value": v.value, "unit": v.unit, "sentence": _sid(v.sentence)} for k, v in info.fields.items()},
"status": info.status,
"lots": [{k: v for k, v in lot.items() if k != "sentence"} for lot in info.lots],
"stats": run_stats,
"tokens": [sum(c.get("tokens_in") or 0 for c in calls), sum(c.get("tokens_out") or 0 for c in calls)],
"models": sorted({c.get("model") or "" for c in calls}),
}
(out / "info.json").write_text(json.dumps(info_out, ensure_ascii=False, indent=1))
# ---------- 模型原始输出的审计 ----------
audit: dict[str, Counter[str]] = defaultdict(Counter)