docs(evals): 查标评测集规范 v0.2、留出集划分;渲染和离线查标脚本;新增待定事项 C12
- evals/check/GUIDELINES.md:请 Fable 审查 v0.1 后修改。响应方式按「文件 × 类别」记(逐条 / 默认接受 / 证明材料(投标时、中标后)/ 文件要在 / 平台内应答),10 份招标文件逐份核对;规矩投标文件靠构造保证 (表格脚本生成,LLM 只写散文和改写),对全部要求响应,问题只挂人工复核过的标注单元;注入分单点 / 连续块 / 全局三种粒度、带不带标志词、两档密度;留出集注入词库和 C 风格说明开工时封存;评分补只检索 基线、按单元去重、端到端召回。 - evals/check/split.json:开发集 S01 S02 S04 S05 S07 S08 S10,留出集 S03 S06 S09 + 真实国动投标文件。 - evals/check/render.py、run_check.py。 - m3-plan.md:新增待定事项 C12(每条要求怎么才算「响应了」)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
| 步骤 | 状态 | 说明 |
|
||||
|---|---|---|
|
||||
| 第 0 步 评测集 | 未开始 | 第 2 步写规则之前要定稿问题分类表、响应清单格式和留出集 |
|
||||
| 第 0 步 评测集 | 进行中 | 规范 v0.2(`evals/check/GUIDELINES.md`,Fable 审查后修改)和留出集划分(`evals/check/split.json`)已定;渲染、离线查标脚本已写。下一步先做 S02、S04、S10 六份跑通 |
|
||||
| 第 1 步 链路打通 | ✅ 2026-11-04 | 见下方「第 1 步落地记录」 |
|
||||
| 第 2~6 步 | 未开始 | |
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
| C9 | **新增:每条清单项的检查结论** | 新增 `check_item_result` 表(检查批次 × 清单项:结论、走的哪一层、投标侧出处、校验状态、原因码、token)。「已响应」也是 AI 结论,按红线 1、2 要能复核、能反馈;复查复用结果、覆盖范围也靠它。问题仍然只为没响应好的条目生成。需要新迁移,并同步 `data-model.md` |
|
||||
| C10 | **新增:扫描件投标文件** | 真实投标的资格证明、业绩合同多是盖章扫描件,OCR(S5)还没做,不处理的话会出一大片致命级「未找到」。MVP:候选分册里图片页占比超过阈值的条目,不出「未找到响应」,改出「该部分为扫描件,请人工核对」;资格类按名称回到资质库匹配;覆盖范围列出没有文字层的页码。S5 排在 M3 之后 |
|
||||
| C11 | **新增:检索方案** | 内存 BM25 + jieba(§2.2),改掉 `ref-bid-engine-analysis.md` §5.4 的 pg_trgm 写法 |
|
||||
| C12 | **待定(2026-11-04 新增):每条要求怎么才算「响应了」** | 招标文件对响应方式的规定按类别不同(`evals/check/GUIDELINES.md` §2):逐条响应、默认接受(只列偏离)、要证明材料(分投标时和中标后)、文件要在、平台内应答,同一份文件里常有几种,S07 还自相矛盾。建议:① 读标新增「响应方式」抽取,粒度是「类别 × 方式 × 出处」,规则优先、模型兜底,条款原文写了「提供……证明材料」的按条覆盖并记投标时 / 中标后;在清单页显示、可改,作为人工确认点。② 两处写法冲突时按更严的(逐条)处理,问题说明里同时引两处原文。③ 默认接受的要求没找到响应:一般要求记「按招标文件默认接受」、不出问题,结论带出处;实质性 / ★ 要求出「提示」级「请人工核对」,不出致命;正文里找到了不满足的值照常报。④ 证明材料是中标后提供的,投标时没附不报。⑤ 识别不出响应方式时按逐条处理(宁可多报),这一组「未找到响应」标明「招标文件未写明是否要求逐条响应」,写进覆盖范围;项目上给一个「本项目只列偏离」的开关,打开后这一组降为提示 |
|
||||
| SK1 | 进度推送用 Valkey stream | 确认。补两处:发布用的客户端设短超时(现在 `valkey()` 没设,Valkey 挂掉时会卡住 worker,而不是只记日志);SSE 连接池设上限 |
|
||||
| SK2 | 集成测试连 Compose 的 PostgreSQL | 确认。补三处:CI 直接跑 `init-roles.sh`,不再单独写一份建角色的 SQL;CI 加 Gotenberg;架构 §8 改掉 testcontainers 的写法 |
|
||||
|
||||
|
||||
@@ -22,3 +22,11 @@ cd .. && evals/run_all.sh base && cd server
|
||||
```
|
||||
|
||||
改了规则或提示词之后,换一个运行名重跑、打分,和上一次的 `summary.md` 对比。目前的基线是 `fix3`(`docs/architecture/extract-eval.md` §7;分数怎么读、为什么不能当达标证据,见 `extract-status.md` §2);`base` 是修复前的那一轮,用的是旧切句(s3-v3),不能拿现在的标注集重新打分。正式清单里多出来、对不上标注的条目要逐条裁定(GUIDELINES §4)。
|
||||
|
||||
## 查标评测集(2026-11-04 起)
|
||||
|
||||
规范见 `check/GUIDELINES.md`,留出集划分 `check/split.json`(开工前选定,不改)。
|
||||
|
||||
- `check/render.py <投标文件>`:把投标文件稿 `out/check/<投标文件>/spec.json` 渲染成 PDF,扫描件页换成图片,回填响应清单里的位置。
|
||||
- `check/run_check.py <投标文件>`:拿读标评测的清单离线跑查标的响应匹配,模型输出缓存在 `out/cache/`。
|
||||
- 生成器 `build.py`、注入 `mutate.py`、评分 `score.py` 待写。投标文件稿、PDF、响应清单都在 `out/check/`(不进 git);留出集专用的注入词库和 C 风格写作说明封存在 `out/check/_sealed/`。
|
||||
|
||||
246
evals/check/GUIDELINES.md
Normal file
246
evals/check/GUIDELINES.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# 查标评测集规范 v0.2(2026-11-04)
|
||||
|
||||
> 查标第 0 步(`docs/architecture/m3-plan.md` §4,决策 C1)。本文件在写第一条查标规则之前定稿:响应方式、问题分类表、投标文件怎么写、响应清单格式、留出集、评分和裁定规则。
|
||||
> 读标的教训(`docs/architecture/extract-status.md`)在这里分别这样堵:AI 标注偏宽 → **响应清单在构造时就确定,挂问题的要求经人工复核**;评分照搬抽取规则 → **只对照响应清单打分,另报只检索基线**;规则照着样本调 → **按招标文件整体留出,留出一种写法和一套注入词库,开工时封存**。
|
||||
> v0.2:请 Fable 审查 v0.1 后修改(响应方式按「文件 × 类别」记、规矩投标文件靠构造保证、注入分粒度、评分补三样口径、分类表补漏项),关键事实对照招标原文核对过。
|
||||
|
||||
## 1. 总体做法
|
||||
|
||||
1. 没有真实投标文件(真实的国动投标文件只有一份,留作最终检验,见 §4),所以按 S01–S10 的真实招标文件**自己造虚构的投标文件**。
|
||||
2. **规矩的投标文件靠构造保证,不靠复核**:偏离表、响应表、报价表、资格和评分材料清单由脚本从清单生成,技术参数按 `op / value / unit` 算一个满足的值;LLM 只写散文(技术方案、投标函叙述)和按风格改写表格行(截断、同义替换、重新编号),逐行小提示,不整本生成。
|
||||
3. 投标文件对招标文件的**全部要求**响应:「全集」用读标正式清单(`evals/out/runs/fix3/<ID>/items.json`),按「招标侧页码 + 片段」记,不只响应标注集的单元。
|
||||
4. 再用确定性的脚本往里**注入问题**得到马虎的投标文件。注入了什么、注在哪里,脚本同时写进**响应清单**,它就是金标准。**问题只挂在人工复核过的标注单元上**(`evals/gold/<ID>.json`,优先实质性条款和资格条件);`optional` 和复核时补的单元不挂问题。
|
||||
5. 评分只对照响应清单,不用检索、偏离表对齐这些查标自己的逻辑来判断对错。
|
||||
|
||||
## 2. 每条要求怎么才算「响应了」:响应方式
|
||||
|
||||
招标文件对投标人怎么响应的规定**按类别不同**,同一份文件里常有几种(S01–S10 逐份核对):
|
||||
|
||||
| 方式 | 含义 | 招标原文的写法 |
|
||||
|---|---|---|
|
||||
| `explicit` 逐条响应 | 在响应表 / 偏离表里逐条写明满足与否 | 「逐条 / 逐项作出响应」「逐一逐条响应填写」 |
|
||||
| `implicit` 默认接受 | 只列和要求不同的地方,没列出的视为接受 | 「否则将认为投标人接受」「未列入上表的视作完全接受」「除本表所列明的偏离外,均视作已理解和响应」「其余无偏离内容无需赘述」 |
|
||||
| `evidence` 要证明材料 | 要附证明材料(检测报告、截图、承诺函、证书),有的还要在材料里标出参数 | 「提供……证明材料」;`when`:`bid`(投标文件中提供)或 `award`(中标后、签订合同时提供,投标时只要承诺或什么都不要) |
|
||||
| `presence` 文件要在 | 不是逐条响应,而是某份文件必须有、必须填全 | 「必须提供,否则投标无效」的格式文件、资格证明文件、声明函 |
|
||||
| `platform` 平台内应答 | 在电子投标客户端里应答,PDF 里看不到 | S04「在电子投标系统逐条应答」 |
|
||||
|
||||
**10 份文件的记录**(类别 × 方式;单条原文另写了证明材料的,按条覆盖):
|
||||
|
||||
| 文件 | 技术参数 | 实质性 / ★ | 商务 | 资格 | 评分项 | 备注 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| S01 | explicit | explicit + evidence | explicit(商务要求均为实质性) | presence | evidence | 「中标后签订合同前提供」11 处(`award`),投标文件中提供 2 处 |
|
||||
| S02 | implicit | 符合性条件:explicit(符合性条件响应表);部分 ★:evidence | implicit | presence | evidence | 偏离表只列「除采购需求符合性条件外」的偏离 |
|
||||
| S03 | implicit(以偏离表为准) | ★ 14 条:evidence(要标注参数)+ 实质性响应条款表 | implicit | presence | evidence | ▲ 31 条也要 evidence(级别重要) |
|
||||
| S04 | explicit(全部逐一逐条) | ★:explicit(实质性响应偏离表)+ evidence | explicit | presence | evidence | 另有平台内逐条应答(platform) |
|
||||
| S05 | implicit(只列正偏离) | 无标注 | implicit | presence(第 1~5 项必须全部提供) | evidence | 偏离表几乎为空是正常的 |
|
||||
| S06 | explicit | explicit(斜体下划线逐条) | explicit | presence | evidence | ▲ 要 evidence,未提供视为负偏离;格式写「(如有请提供)」和须知矛盾 |
|
||||
| S07 | **冲突** | ★ 国产化承诺:evidence(承诺函) | **冲突** | evidence(逐一提供证明文件) | evidence | 须知「逐条标明满足与否」vs 格式注「未列入上表的视作完全接受」 |
|
||||
| S08 | explicit(非实质性也逐条) | explicit + evidence | explicit | presence | ▲ 7 项:evidence | |
|
||||
| S09 | explicit | explicit | explicit(实质性) | presence | evidence | 投标时要的证明材料很少 |
|
||||
| S10 | explicit | explicit | explicit | presence | evidence(人员证书 + 社保) | 技术证明材料「签订合同时提供」(`award`) |
|
||||
|
||||
响应清单给每条要求记下方式和招标原文依据。**问题只在这种方式要求了、投标文件却没做到时才算**:`implicit` 的要求没被复述不是问题;`evidence` + `award` 的要求投标时没附材料不是问题。
|
||||
|
||||
> 查标怎么处理响应方式是待定事项 C12(`m3-plan.md` §7)。评测集按本节记录,C12 怎么定都能评。
|
||||
|
||||
## 3. 问题分类表
|
||||
|
||||
注入的问题分两类:**要求项问题**挂在某条要求上;**规则问题**不挂要求(或挂报价类要求)。另有**干扰项**:看起来像问题、其实不是,查标不应该报。
|
||||
|
||||
### 3.1 要求项问题
|
||||
|
||||
| 编号 | 问题 | 期望的结论 / 问题类型 | 期望级别 | 变体 | 实现阶段 |
|
||||
|---|---|---|---|---|---|
|
||||
| D01 | 漏响应(要求 explicit,整条没写) | `unresponded` | 按要求项 | 删一行;删连续一段;整节缺失 | 第 1、3 步 |
|
||||
| D02 | 投标时要的证明材料缺失(evidence + bid) | `unresponded`(证明材料) | 按要求项 | 写「见附件 5」但附件不存在;根本没提。「附件在但没标出参数」单列,不进头条指标 | 第 3 步 |
|
||||
| D03 | 数值不满足 | `suspected_unmet` | 按要求项 | 低于下限;高于上限;数量不够;单位不同换算后不满足(≥2TB 写 1024GB、≤50ms 写 0.1 秒、≥10Gbps 写 1000Mbps);偏离列写「正偏离」值却更差 | 第 3 步 |
|
||||
| D05 | 功能写明不支持 | `negative_deviation` | 按要求项 | 「暂不支持」;「需二次开发后支持」;「可选配」(`negative_deviation` 或 `no_value` 都算对) | 第 3 步 |
|
||||
| D06 | 负偏离没有说明 | `negative_deviation` | 按要求项 | 说明栏空;「部分满足」;只写「偏离」不写方向 | 第 3 步 |
|
||||
| D07 | 偏离表写无偏离,正文的值不满足 | `suspected_unmet` | 按要求项 | 技术方案正文的值;配置清单的值;彩页摘录的值。MVP 可能不查(m3-plan §2.1),评分单列 | 第 3 步 |
|
||||
| D08 | 只写「满足」,没给要的具体值或材料 | `no_value` | 提示 | 单条;整张表都写「满足」 | 第 3 步 |
|
||||
| D09 | 响应对象错位 | `unresponded` | 按要求项 | 写的是另一台设备的同名参数;另一个采购包;整章是另一个型号 | 第 3 步 |
|
||||
| D10 | 符合性条件响应表填「未完全响应」或空着 | `negative_deviation` | 致命 | 空着;「未完全响应」;「部分响应」 | 第 3 步 |
|
||||
| D11 | 资格证明材料缺失 | `unresponded` | 致命 | 目录列了、正文没有;整份缺失;给的是别的证书 | 第 3、4 步 |
|
||||
| D12 | 资格证书过期(按投标截止日) | `qualification`(过期) | 致命 | 早于截止日;整本证书过期 | 第 4 步 |
|
||||
| D13 | 评分项要的证明材料缺失 | `unresponded` | 重要 | 业绩合同缺;人员证书缺;社保缺 | 第 3 步 |
|
||||
| D14 | 证书主体和投标人不一致 | `qualification` | 致命 | 子公司;分公司;曾用名 | 第 4 步 |
|
||||
| D15 | 业绩 / 人员证明的时间窗口不符 | `qualification` / `unresponded` | 资格致命、评分重要 | 业绩合同在近三年之外;社保月份不在要求区间 | 第 4 步 |
|
||||
| D16 | 授权代表的社保证明缺失(文件要求时) | `unresponded` | 致命 | 没附;附的是别人的;月份不对 | 第 3 步 |
|
||||
| D17 | 多包:采购包号空着或写错 | `formality` | 重要 | 偏离表包号空;授权书包号写错;分项表包号写错 | 第 2 步 |
|
||||
|
||||
(v0.1 的 D04「换算后不满足」并入 D03 当变体。)
|
||||
|
||||
### 3.2 规则问题
|
||||
|
||||
| 编号 | 问题 | 期望的规则编号 | 期望级别 | 变体 | 实现阶段 |
|
||||
|---|---|---|---|---|---|
|
||||
| P01 | 报价超过总价限价 | `PRICE.OVER_CEILING_TOTAL` | 致命 | 超 1 元;超 1%;万元 / 元的数量级错 | 第 2 步 |
|
||||
| P02 | 报价超过采购包限价 | `PRICE.OVER_CEILING_LOT` | 致命 | 同上(S10) | 第 2 步 |
|
||||
| P03 | 分项或单价超上限 | `PRICE.OVER_CEILING_ITEM` | 致命 | 单项超;单价 × 数量超 | 第 2 步(依赖 C7) |
|
||||
| P04 | 大写和小写金额不一致 | `PRICE.CAPITAL_MISMATCH` | 重要 | 差一位;大写多一个「拾」;小数点错 | 第 2 步 |
|
||||
| P05 | 开标一览表和分项合计不一致 | `PRICE.SUM_MISMATCH` | 重要 | 漏加一项;单价 × 数量算错;四舍五入差 | 第 2 步 |
|
||||
| P06 | 出现别的项目名称、编号或采购人 | `FORM.FOREIGN_PROJECT` | 重要 | 页眉留旧项目名;授权书写旧项目编号;投标函写错采购人 | 第 2 步 |
|
||||
| P07 | 该填的地方没填 | `FORM.BLANK_FIELD` | 提示 | 投标人名称空;落款日期「年 月 日」;授权书受托人空 | 第 2 步 |
|
||||
| P08 | 落款日期晚于投标截止日 | `FORM.DATE_AFTER_DEADLINE` | 提示 | 晚一天;年份错;月份错 | 第 2 步 |
|
||||
| P09 | 投标有效期短于要求 | `FORM.VALIDITY_SHORT` | 致命 | 90 天写 60 天;写「30 日」 | 第 2 步 |
|
||||
| P10 | 附加条件 | `FORM.CONDITION_ADDED` | 重要 | 「报价不含税」;「付款方式另议」;「以实际到货为准」 | 第 2 步 |
|
||||
| P11 | 缺必须提供的格式文件(presence) | `FORM.MISSING_DOC` | 致命 | 缺授权委托书;缺投标函;缺符合性条件响应表 | 第 2 步 |
|
||||
| P12 | 给了两个报价 | `PRICE.MULTIPLE_QUOTES` | 致命 | 两行总价;「方案一 / 方案二」;「优惠价」 | 第 2 步 |
|
||||
| P13 | 分项报价表缺项、漏项,和采购需求清单对不上 | `PRICE.ITEM_MISSING` | 致命 | 漏一项;名称对不上;数量对不上 | 第 2 步 |
|
||||
| P14 | 格式文件的固定文字被改动 | `FORM.FIXED_TEXT_CHANGED` | 重要 | 删承诺句;改有效期措辞;改主体 | 第 2 步 |
|
||||
| P15 | 授权委托书的受托人和投标函签字人不一致 | `FORM.SIGNER_MISMATCH` | 致命 | 姓名不同;职务不同;身份证号不同 | 第 2 步 |
|
||||
| P16 | 声明函填报不全 | `FORM.DECLARATION_INCOMPLETE` | 致命(本国产品声明函三项缺一;专门面向中小企业时的中小企业声明函)/ 提示(其他) | 缺一项;未逐一填报;类型选错 | 第 2 步 |
|
||||
| P17 | 签章位置没有图像 | `FORM.SEAL_ABSENT` | 提示 | 投标函;授权书;开标一览表 | 第 2 步 |
|
||||
| P18 | 报价低于文件写明的阈值却没附说明 | `PRICE.LOW_NO_EXPLANATION` | 重要 | 低于阈值没说明;说明不在投标文件里 | 第 2 步 |
|
||||
|
||||
### 3.3 干扰项(不应报问题)
|
||||
|
||||
| 编号 | 情形 | 期望 |
|
||||
|---|---|---|
|
||||
| T01 | 响应写在别的分册 | 已响应 |
|
||||
| T02 | 要求被截断、改写、重新编号后再响应 | 已响应 |
|
||||
| T03 | 只写值不复述要求(「内存:64GB DDR4」) | 已响应 |
|
||||
| T04 | 单位不同但满足(≥1024GB 写 2TB) | 已响应 |
|
||||
| T05 | 正偏离 | 已响应 |
|
||||
| T06 | `implicit` 的要求,投标文件没有复述 | 不报问题(按 C12 的结论) |
|
||||
| T07 | 正文里出现**本项目**的名称、编号、采购人 | 不报 P06 |
|
||||
| T08 | 「(盖单位章)」「(签字)」括注保留、名称和日期已填 | 不报 P07 |
|
||||
| T09 | 条款写「中标后 / 签订合同时提供」,投标文件只写承诺或没写 | 不报 D02 |
|
||||
| T10 | 声明函里「/」划除处未填(文件写明无需填写) | 不报 P07 |
|
||||
| T11 | 评分项的可选加分没响应 | 不报致命,最多提示 |
|
||||
| T12 | `implicit` 文件的偏离表只有一行「无偏离」 | 不报 D01、不报缺偏离表 |
|
||||
| T13 | 正文写「见附件 N」且附件存在 | 不报 D02 |
|
||||
|
||||
### 3.4 注入的分布
|
||||
|
||||
真实投标人的错是成簇、相关、不声张的,均匀分布的单点问题会让分数虚高。所以:
|
||||
|
||||
- **三种粒度**:单点;连续块(相邻 5~20 行一起漏、一起错);全局模式(整张表写「满足」、整章换成另一个项目或型号、整本证书过期)。响应清单记 `granularity`。
|
||||
- **有一部分不带标志词**:没有「暂不支持」这类字样,只是合理位置上的错数字、错单位、错型号。响应清单记 `worded: false`,按带不带标志词分别报召回。
|
||||
- **两档密度**:多数马虎的投标文件注入 25~40 个(覆盖各类);另选 3 份只注入 5~8 个(接近真实密度),看低密度下误报会不会淹没真问题。
|
||||
- 注入词库分**开发集用**和**留出集用**两份(§4)。
|
||||
|
||||
## 4. 样本和留出集(开工前选定,之后不改)
|
||||
|
||||
| 集合 | 招标文件 | 选它的理由 |
|
||||
|---|---|---|
|
||||
| 开发集 | S01、S02、S04、S05、S07、S08、S10 | 逐条响应(S01、S08、S10)、默认接受(S02、S05)、全逐条 + ★ 双表 + 证明材料(S04)、冲突(S07)、中标后提供(S01、S10)、多包和单价上限(S10)、磋商(S08)、WPS 旧版 DOC(S07) |
|
||||
| 留出集 | S03、S06、S09 | S03:★ / ▲ 证明材料、一般参数默认接受;S06:分项最高限价、只用斜体下划线、清单最大;S09:磋商、★ 和斜体下划线双重标注、WPS 旧版 DOC。留出集里没有 S05 那种「只列正偏离」的写法,报告里写明 |
|
||||
| 留出集(真实) | 国动小程序投标文件(用户提供) | 唯一的真实投标文件,第 3 步冻结代码后要来跑一次,之前不看、不按它调。它对应的招标文件不在 S01–S10 里,届时单独走一遍读标、人工看过清单再评 |
|
||||
|
||||
**写法风格**:
|
||||
|
||||
| 风格 | 写法 | 用在 |
|
||||
|---|---|---|
|
||||
| A 复述型 | 响应表、偏离表照抄要求原文,再写响应值和「无偏离」(苏采云范本的常见填法) | 开发集 |
|
||||
| B 简写型 | 要求被截断、改写、重新编号;技术方案正文只写值不复述要求;部分响应写在别的分册 | 开发集、留出集 |
|
||||
| C 叙述型 | 以叙述性技术方案为主,表格少;自己的条目编号;同义词替换 | **只用在留出集** |
|
||||
|
||||
**封存**:留出集专用的注入词库和 C 风格写作说明,在开工时由另一个会话写好,放进 `evals/out/check/_sealed/`,之后不打开;写它的会话不接触查标规则代码。写 C 风格的会话不看 A / B 的产出。`check.match` 提示词的例子和注入词库做文本比对,不允许出现同样的短语。写作说明里的例子不取自 S01–S10 的招标文件和开发集投标文件。
|
||||
|
||||
每份招标文件写两份投标文件:开发集是 A 规矩 + B 马虎(按文件轮换);留出集是 B 规矩 + C 马虎。一共 20 份。**先做 S02、S04、S10 这 6 份**,端到端跑通「生成 → 渲染 → 离线查标 → 评分」,修好生成器和评分脚本,再做其余开发集,留出集最后做。
|
||||
|
||||
## 5. 投标文件怎么生成
|
||||
|
||||
1. **格式用招标文件自己的**:投标函、开标一览表、分项报价表、偏离表、响应表、授权委托书、各种声明函,用这份招标文件格式章给的格式和表头。
|
||||
2. **生成流程**:`build.py` 从读标正式清单和格式章生成规矩投标文件的结构化稿(`spec.json`)→ LLM 按风格改写表格行、补写散文 → `mutate.py` 注入问题、写响应清单 → `render.py` 渲染并回填位置。
|
||||
3. **渲染**:多数走 Chromium(HTML → PDF);**至少 2~3 份走 DOCX → LibreOffice**(产品的真实路径),带合并单元格、多行单元格。资格证明、业绩合同中有一部分做成图片页,检验 C10。
|
||||
4. **自检**:冻结前用另一个模型跑一遍「找无意的偏离」,脚本扫一遍占位符和空表格,再由人抽查。
|
||||
5. **人工复核**:只复核**挂了问题的标注单元**(每份马虎的投标文件 25~40 个,20 份约 600~800 个)和各文件的响应方式记录;`ok` 的单元不复核。
|
||||
|
||||
## 6. 文件格式
|
||||
|
||||
### 6.1 投标文件稿 `spec.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"bid": "S02-A",
|
||||
"tender": "S02",
|
||||
"style": "A",
|
||||
"project_name": "……",
|
||||
"bidder": {"name": "某某信息技术有限公司(虚构)", "credit_code": "91320000MA0000000X"},
|
||||
"volumes": [
|
||||
{
|
||||
"id": "v1",
|
||||
"kind": "whole",
|
||||
"header": null,
|
||||
"blocks": [
|
||||
{"type": "heading", "text": "一、投标函"},
|
||||
{"type": "para", "text": "……", "responds": []},
|
||||
{"type": "table", "title": "偏离表", "header": ["序号", "招标文件的要求", "投标文件中的内容", "偏离", "说明"],
|
||||
"rows": [{"cells": ["1", "……", "……", "无偏离", ""], "responds": ["s2"], "req": {"p": 53, "t": "支持硬件BFD"}}]},
|
||||
{"type": "scan", "text": "营业执照(副本)扫描件", "lines": ["……"], "responds": ["q3"]}
|
||||
]
|
||||
}
|
||||
],
|
||||
"price": {"total": "1498000.00", "items": [{"name": "汇聚交换机", "qty": 3, "unit_price": "52000.00"}]}
|
||||
}
|
||||
```
|
||||
|
||||
`responds` 写标注单元编号(有的话);`req` 写招标侧「页码 + 片段」(全部响应块都写,不在标注集里的要求也有)。
|
||||
|
||||
### 6.2 响应清单 `manifest.json`(金标准)
|
||||
|
||||
```json
|
||||
{
|
||||
"bid": "S02-B-sloppy",
|
||||
"tender": "S02",
|
||||
"style": "B",
|
||||
"split": "dev",
|
||||
"kind": "sloppy",
|
||||
"modes": [
|
||||
{"category": "tech_param", "mode": "implicit", "basis": [{"p": 76, "t": "否则将认为投标人接受"}]},
|
||||
{"category": "substantive", "mode": "explicit", "scope": "符合性条件", "basis": [{"p": 20, "t": "必须提供,否则投标无效"}]}
|
||||
],
|
||||
"units": {
|
||||
"s2": {
|
||||
"mode": "evidence", "when": "bid",
|
||||
"reviewed": true,
|
||||
"expect": "defect", "defect": "D02", "variant": "附件不存在",
|
||||
"granularity": "single", "worded": false,
|
||||
"where": [{"volume": "v1", "p": 31, "t": "支持硬件BFD,最小发送间隔3.3ms"}],
|
||||
"expected_location": {"volume": "v1", "section": "技术证明材料"}
|
||||
},
|
||||
"s3": {"mode": "implicit", "expect": "ok", "trap": "T06", "where": []}
|
||||
},
|
||||
"rule_defects": [
|
||||
{"id": "P04-1", "type": "P04", "variant": "大写多一个拾", "where": [{"volume": "v1", "p": 2, "t": "壹佰肆拾玖万捌仟元整"}]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `expect`:`ok`、`defect`、`na`(不要求投标人响应,或 `platform` 平台内应答)。
|
||||
- `reviewed`:挂了问题的单元必须为 true(人工复核过要求本身和响应方式)。
|
||||
- `where` 在渲染后由脚本回填;`expected_location` 写这条响应本该在哪个分册、哪一节(整节缺失时用来核对「找过哪些分册」)。
|
||||
|
||||
## 7. 评分
|
||||
|
||||
`score.py` 读一次离线检查的输出(`run_check.py` 的 `results.json`),对照响应清单:
|
||||
|
||||
1. **映射口径**:问题的任一招标侧出处和标注单元的任一 anchor 重叠即对上(复用读标评分的定位)。
|
||||
2. **按单元去重**:同一要求在清单里有几条(公告、须知、审查表),只要一条报出就算召回,几条都报只算一个对的、不算误报;几条都没报算一个漏报。
|
||||
3. **问题类型按类型族比**(`unresponded` 含证据没核对上的那种),级别单独比。
|
||||
4. 主要指标:
|
||||
- **致命级问题召回率**(按分类、粒度、带不带标志词分别报;漏报比误报严重);
|
||||
- **端到端召回**(读标抽到 × 查标查出),致命级单独报:对用户来说读标漏抽也是漏报;「查标召回」只在读标抽到的要求上算;
|
||||
- **问题准确率**:报出的问题里对上注入问题的比例,「对上注入的问题」和「其他」分两列;
|
||||
- **规矩投标文件的误报数**(每 100 条要求)、**干扰项误报率**(按 T01–T13 分别报);
|
||||
- **只检索基线**:不调模型,检索第一名命中就算已响应,按风格分别报;全系统和基线差不多,说明评的是检索不是判断;
|
||||
- 证据没通过校验的问题占致命问题的比例(目标 < 10%,C3);每次检查的 token 和费用(C2)。
|
||||
5. 对不上标注单元的问题单列「无法对照」,不算对也不算错。
|
||||
6. 开发集、留出集分开报;所有数字带风格标签。**头条是留出集 C 风格和国动真实投标文件**,开发集 A 风格的数字只作调试用。
|
||||
|
||||
## 8. 裁定规则
|
||||
|
||||
- 开发集上查出了响应清单里没有的问题:由人裁定一次。投标文件里确实有这个问题(生成时无意留下的),就改响应清单并记录;没有就算误报。
|
||||
- 留出集:允许改响应清单,但只限投标文件里客观可查的错;每一次记为「响应清单错误」,不算查标误报也不算查标对,单独报这个数(它大说明生成器差)。**不许据此调规则和提示词**。
|
||||
- 裁定记录写在 `evals/out/check/adjudication/<bid>.md`。
|
||||
|
||||
## 9. 放在哪里
|
||||
|
||||
| 内容 | 位置 | 进不进 git |
|
||||
|---|---|---|
|
||||
| 本规范、留出集划分 `split.json`、开发集写作说明和注入词库、生成 / 渲染 / 评分脚本 | `evals/check/` | 进 |
|
||||
| 留出集专用的注入词库和 C 风格写作说明 | `evals/out/check/_sealed/` | 不进(封存) |
|
||||
| 投标文件稿、渲染出的 PDF、响应清单、检查输出、裁定记录 | `evals/out/check/` | 不进(照着真实招标文件写的) |
|
||||
| 真实的国动投标文件 | 用户本地,第 3 步冻结后提供 | 不进 |
|
||||
184
evals/check/render.py
Normal file
184
evals/check/render.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""把投标文件稿(spec.json)渲染成 PDF,并回填响应清单里的位置(evals/check/GUIDELINES.md §5、§6)。
|
||||
|
||||
cd server && ../.venv/bin/python ../evals/check/render.py <投标文件编号> [...] # 需要 Gotenberg(:3109)
|
||||
|
||||
- 每个分册一份 PDF:evals/out/check/<bid>/<分册编号>.pdf;
|
||||
- `scan` 块单独成页,渲染后把那一页换成图片(没有文字层),用来检验扫描件的处理(C10);
|
||||
- 页眉写 volume.header(默认是本项目名称;注入 P06 时改成别的项目名称);
|
||||
- 渲染后用读标同一套切句找到每个响应块所在的页,写进 manifest.json 的 where(页码 + 片段)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import io
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pypdfium2 as pdfium
|
||||
|
||||
from bid_doctor.document.pipeline import text_layer
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent
|
||||
OUT = ROOT.parent / "out" / "check"
|
||||
GOTENBERG = "http://127.0.0.1:3109/forms/chromium/convert/html"
|
||||
SNIPPET = 24 # 回填位置时取块文字的前多少个字
|
||||
|
||||
CSS = """
|
||||
@page { size: A4; margin: 22mm 18mm 20mm; }
|
||||
body { font-family: "Noto Serif CJK SC", "Noto Sans CJK SC", serif; font-size: 11pt; line-height: 1.75; margin: 0; }
|
||||
h1 { font-size: 17pt; text-align: center; margin: 0 0 12pt; }
|
||||
h2 { font-size: 14pt; margin: 14pt 0 6pt; }
|
||||
h3 { font-size: 12pt; margin: 10pt 0 4pt; }
|
||||
p { margin: 0 0 6pt; text-align: justify; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 6pt 0 10pt; font-size: 10pt; }
|
||||
th, td { border: 0.6pt solid #333; padding: 3pt 4pt; vertical-align: top; }
|
||||
th { background: #f2f2f2; }
|
||||
.page { page-break-before: always; }
|
||||
.scan { page-break-before: always; page-break-after: always; padding: 24mm 10mm; border: 1pt solid #999; min-height: 150mm; }
|
||||
.scan h3 { text-align: center; }
|
||||
.sign { margin-top: 18pt; text-align: right; }
|
||||
"""
|
||||
|
||||
|
||||
def esc(s: Any) -> str:
|
||||
return html.escape(str(s if s is not None else ""))
|
||||
|
||||
|
||||
def block_html(b: dict[str, Any]) -> str:
|
||||
t = b.get("type")
|
||||
if t == "title":
|
||||
return f"<h1>{esc(b['text'])}</h1>"
|
||||
if t == "heading":
|
||||
level = int(b.get("level", 2))
|
||||
cls = ' class="page"' if b.get("new_page") else ""
|
||||
return f"<h{level}{cls}>{esc(b['text'])}</h{level}>"
|
||||
if t == "para":
|
||||
return f"<p>{esc(b['text'])}</p>"
|
||||
if t == "sign":
|
||||
return "<div class='sign'>" + "".join(f"<p>{esc(x)}</p>" for x in b["lines"]) + "</div>"
|
||||
if t == "table":
|
||||
head = "".join(f"<th>{esc(h)}</th>" for h in b.get("header") or [])
|
||||
rows = "".join("<tr>" + "".join(f"<td>{esc(c)}</td>" for c in r["cells"]) + "</tr>" for r in b["rows"])
|
||||
title = f"<h3>{esc(b['title'])}</h3>" if b.get("title") else ""
|
||||
return f"{title}<table><thead><tr>{head}</tr></thead><tbody>{rows}</tbody></table>"
|
||||
if t == "scan":
|
||||
lines = "".join(f"<p>{esc(x)}</p>" for x in b.get("lines") or [])
|
||||
return f"<div class='scan'><h3>{esc(b['text'])}</h3>{lines}</div>"
|
||||
raise ValueError(f"未知的块类型:{t}")
|
||||
|
||||
|
||||
def volume_html(spec: dict[str, Any], vol: dict[str, Any]) -> tuple[str, str]:
|
||||
body = "".join(block_html(b) for b in vol["blocks"])
|
||||
header = esc(vol.get("header") or spec.get("project_name") or "")
|
||||
# 页眉用 Gotenberg 的 header.html 传,每页都有;读标切句会把各页重复的行认成页眉页脚带,
|
||||
# 查标的「别的项目名称」规则要专门扫这里(m3-plan §2.4)
|
||||
return (
|
||||
f"<!doctype html><html><head><meta charset='utf-8'><style>{CSS}</style></head><body>{body}</body></html>",
|
||||
header,
|
||||
)
|
||||
|
||||
|
||||
def render_pdf(page_html: str, header: str) -> bytes:
|
||||
head = (
|
||||
"<html><head><style>body{font-family:'Noto Sans CJK SC',sans-serif;font-size:8pt;color:#555;"
|
||||
"width:100%;margin:0 18mm;}</style></head><body>" + esc(header) + "</body></html>"
|
||||
)
|
||||
files = [
|
||||
("files", ("index.html", page_html.encode(), "text/html")),
|
||||
("files", ("header.html", head.encode(), "text/html")),
|
||||
]
|
||||
data = {"marginTop": "0.9", "marginBottom": "0.8", "marginLeft": "0.7", "marginRight": "0.7"}
|
||||
r = httpx.post(GOTENBERG, files=files, data=data, timeout=120)
|
||||
r.raise_for_status()
|
||||
return r.content
|
||||
|
||||
|
||||
def rasterize(pdf: bytes, pages: set[int]) -> bytes:
|
||||
"""把指定页(从 1 起)换成图片页:没有文字层,模拟扫描件。"""
|
||||
if not pages:
|
||||
return pdf
|
||||
src = pdfium.PdfDocument(pdf)
|
||||
out = pdfium.PdfDocument.new()
|
||||
for i in range(len(src)):
|
||||
if i + 1 in pages:
|
||||
img = src[i].render(scale=1.6).to_pil().convert("L")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PDF", resolution=115)
|
||||
out.import_pages(pdfium.PdfDocument(buf.getvalue()))
|
||||
else:
|
||||
out.import_pages(src, [i])
|
||||
buf = io.BytesIO()
|
||||
out.save(buf)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def page_texts(pdf: bytes) -> dict[int, str]:
|
||||
pages = text_layer.extract("B", pdf)
|
||||
return {p.page_no: text_layer.norm("".join(s.text for s in p.sentences)) for p in pages}
|
||||
|
||||
|
||||
def block_text(b: dict[str, Any], row: dict[str, Any] | None = None) -> str:
|
||||
if row is not None:
|
||||
# 定位用投标人写的那一格(anchor 指定;默认第 3 格「投标文件中的内容」),不用照抄的要求原文
|
||||
cells = [str(c) for c in row["cells"]]
|
||||
at = row.get("anchor", 2 if len(cells) >= 3 else len(cells) - 1)
|
||||
return cells[at]
|
||||
if b.get("type") == "scan":
|
||||
return str(b["text"])
|
||||
return str(b.get("text") or " ".join(b.get("lines") or []))
|
||||
|
||||
|
||||
def locate(texts: dict[int, str], snippet: str) -> int | None:
|
||||
q = text_layer.norm(snippet)[:SNIPPET]
|
||||
for p, t in texts.items():
|
||||
if q and q in t:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def render(bid: str) -> None:
|
||||
d = OUT / bid
|
||||
spec = json.loads((d / "spec.json").read_text())
|
||||
manifest_path = d / "manifest.json"
|
||||
manifest = json.loads(manifest_path.read_text()) if manifest_path.exists() else None
|
||||
where: dict[str, list[dict[str, Any]]] = {}
|
||||
problems: list[str] = []
|
||||
for vol in spec["volumes"]:
|
||||
page_html, header = volume_html(spec, vol)
|
||||
pdf = render_pdf(page_html, header)
|
||||
texts = page_texts(pdf)
|
||||
scans = {p for b in vol["blocks"] if b.get("type") == "scan" and (p := locate(texts, b["text"]))}
|
||||
located: list[tuple[list[str], str]] = []
|
||||
for b in vol["blocks"]:
|
||||
if b.get("type") == "table":
|
||||
located += [(r.get("responds") or [], block_text(b, r)) for r in b["rows"]]
|
||||
else:
|
||||
located.append((b.get("responds") or [], block_text(b)))
|
||||
for units, snippet in located:
|
||||
if not units:
|
||||
continue
|
||||
page = locate(texts, snippet)
|
||||
if page is None:
|
||||
problems.append(f"{vol['id']}:找不到「{snippet[:SNIPPET]}」")
|
||||
continue
|
||||
for u in units:
|
||||
where.setdefault(u, []).append(
|
||||
{"volume": vol["id"], "p": page, "t": text_layer.norm(snippet)[:SNIPPET]}
|
||||
)
|
||||
(d / f"{vol['id']}.pdf").write_bytes(rasterize(pdf, scans))
|
||||
print(f"{bid}/{vol['id']}.pdf {len(texts)} 页,扫描页 {sorted(scans)}")
|
||||
if manifest is not None:
|
||||
for uid, entry in manifest["units"].items():
|
||||
entry["where"] = where.get(uid, [])
|
||||
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=1))
|
||||
for p in problems:
|
||||
print(" ! " + p)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for b in sys.argv[1:]:
|
||||
render(b)
|
||||
143
evals/check/run_check.py
Normal file
143
evals/check/run_check.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""离线查标评测:拿读标评测的清单(evals/out/runs/<运行>/<招标文件>/items.json)对一份虚构投标文件跑响应匹配。
|
||||
|
||||
用法(在 server/ 下,模型配置读根目录 .env):
|
||||
../.venv/bin/python ../evals/check/run_check.py <投标文件编号> [--checklist fix3] [--name <运行名>]
|
||||
输入:evals/out/check/<投标文件编号>/v*.pdf(render.py 渲染的)、manifest.json 里的 tender。
|
||||
输出:evals/out/check/<投标文件编号>/runs/<运行名>/results.json(每条清单项的结论、投标侧证据、生成的问题)。
|
||||
同一模型 + 同一提示词版本 + 同一输入只调用一次模型(和读标评测共用 evals/out/cache/),只改规则时重放不花钱。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
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 OutputIncomplete, provider
|
||||
from bid_doctor.check import findings, match
|
||||
from bid_doctor.core.config import get_settings
|
||||
from bid_doctor.core.db import TenantContext
|
||||
from bid_doctor.document.pipeline import text_layer
|
||||
|
||||
EVALS = pathlib.Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(EVALS))
|
||||
import gold # noqa: E402
|
||||
|
||||
OUT = EVALS / "out" / "check"
|
||||
CACHE = EVALS / "out" / "cache"
|
||||
usage = {"calls": 0, "cached": 0, "tokens_in": 0, "tokens_out": 0}
|
||||
|
||||
|
||||
async def cached_run_json(
|
||||
ctx: Any, *, job_type: str, job_id: Any, task: str, prompt_id: str, version: int, user: str
|
||||
) -> dict[str, Any]:
|
||||
del ctx, job_type, job_id
|
||||
p = prompts.get(prompt_id, version)
|
||||
s = get_settings()
|
||||
model = s.ai_models.get(p.capability) or s.ai_model
|
||||
key = hashlib.sha256(f"{model}\n{prompt_id}\n{version}\n{p.body}\n{user}".encode()).hexdigest()[:32]
|
||||
f = CACHE / f"{key}.json"
|
||||
if f.exists():
|
||||
data = json.loads(f.read_text())
|
||||
usage["cached"] += 1
|
||||
else:
|
||||
r = await provider().complete_json(p.body, user, p.capability, task)
|
||||
data = {
|
||||
"data": r.data,
|
||||
"model": r.model,
|
||||
"tokens_in": r.tokens_in,
|
||||
"tokens_out": r.tokens_out,
|
||||
"incomplete": r.incomplete,
|
||||
}
|
||||
f.write_text(json.dumps(data, ensure_ascii=False))
|
||||
usage["calls"] += 1
|
||||
usage["tokens_in"] += data.get("tokens_in") or 0
|
||||
usage["tokens_out"] += data.get("tokens_out") or 0
|
||||
if data.get("incomplete") or near_limit(data.get("tokens_out")):
|
||||
raise OutputIncomplete(task, data["data"])
|
||||
return data["data"]
|
||||
|
||||
|
||||
def checklist(tender: str, run: str) -> dict[UUID, dict[str, Any]]:
|
||||
"""读标评测输出的正式清单 → 查标的清单项(和检查快照的 payload 同样的字段)。"""
|
||||
rows = json.loads((EVALS / "out" / "runs" / run / tender / "items.json").read_text())
|
||||
by_sid = {gold.sid(s): s for s in gold.sentences(tender)}
|
||||
out: dict[UUID, dict[str, Any]] = {}
|
||||
for i, r in enumerate(rows):
|
||||
if r.get("zone") != "formal":
|
||||
continue
|
||||
text = "".join(by_sid[k].text for k in r.get("sentences") or [] if k in by_sid)
|
||||
raw = (r.get("raw") or {}).get("tech_param") or {}
|
||||
spec = {"tech_param": {k: raw[k] for k in ("param", "op", "value", "unit") if raw.get(k)}} if raw else {}
|
||||
out[UUID(int=i + 1)] = {
|
||||
"title": r.get("title"),
|
||||
"requirement_text": text or None,
|
||||
"categories": r.get("categories") or [],
|
||||
"importance": r.get("importance"),
|
||||
"importance_basis": r.get("importance_basis"),
|
||||
"spec": spec,
|
||||
"sentences": r.get("sentences") or [],
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("bid")
|
||||
ap.add_argument("--checklist", default="fix3")
|
||||
ap.add_argument("--name", default="base")
|
||||
args = ap.parse_args()
|
||||
d = OUT / args.bid
|
||||
manifest = json.loads((d / "manifest.json").read_text())
|
||||
items = checklist(manifest["tender"], args.checklist)
|
||||
docs = []
|
||||
for n, pdf in enumerate(sorted(d.glob("v*.pdf")), start=1):
|
||||
pages = text_layer.extract(f"{args.bid}-{pdf.stem}", pdf.read_bytes())
|
||||
docs.append(match.BidDoc(uuid4(), n, pdf.stem, text_layer.body_sentences(pages)))
|
||||
ai_service.run_json = cached_run_json # type: ignore[assignment]
|
||||
|
||||
async def progress(_: float) -> None:
|
||||
return None
|
||||
|
||||
ctx = TenantContext(tenant_id=UUID(int=0), user_id=UUID(int=0))
|
||||
results, stats = await match.run(ctx, uuid4(), items, docs, progress)
|
||||
rows = []
|
||||
for item_id, item in items.items():
|
||||
res = results[item_id]
|
||||
spec = findings.spec(res, item)
|
||||
rows.append(
|
||||
{
|
||||
"title": item["title"],
|
||||
"sentences": item["sentences"],
|
||||
"categories": item["categories"],
|
||||
"importance": item["importance"],
|
||||
"verdict": res.verdict,
|
||||
"layer": res.layer,
|
||||
"evidence_status": res.evidence_status,
|
||||
"reason_code": res.reason_code,
|
||||
"evidence": [{"volume": doc.label, "p": s.page_no, "text": s.text} for doc, s in res.evidence],
|
||||
"detail": res.detail,
|
||||
"finding": {"type": spec.type, "severity": spec.severity, "title": spec.title} if spec else None,
|
||||
}
|
||||
)
|
||||
out = d / "runs" / args.name
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
summary = {"items": len(rows), "usage": usage, "verify_failures": stats.verify_failures, "splits": stats.splits}
|
||||
(out / "results.json").write_text(json.dumps({"summary": summary, "items": rows}, ensure_ascii=False, indent=1))
|
||||
findings_n = sum(r["finding"] is not None for r in rows)
|
||||
print(
|
||||
f"{args.bid}:清单 {len(rows)} 条,问题 {findings_n} 个,模型调用 {usage['calls']} 次(缓存 {usage['cached']}),"
|
||||
f"token {usage['tokens_in']} / {usage['tokens_out']}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
5
evals/check/split.json
Normal file
5
evals/check/split.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"_comment": "查标评测集划分(GUIDELINES §3)。2026-11-04 开工前选定,之后不改。留出集只在第 3 步代码冻结后跑一次。",
|
||||
"dev": {"tenders": ["S01", "S02", "S04", "S05", "S07", "S08", "S10"], "styles": ["A", "B"]},
|
||||
"holdout": {"tenders": ["S03", "S06", "S09"], "styles": ["B", "C"], "real": ["国动小程序投标文件(用户提供)"]}
|
||||
}
|
||||
Reference in New Issue
Block a user