docs: 查标实现计划 v0.2,确认 C1–C11、SK1–SK2;参考项目查标深读

- m3-plan.md:请 Fable 当顾问深读参考项目的查标实现、对照读标教训复评后的计划。
  先建评测集再写规则(金标准是虚构投标文件的响应清单);每条清单项留带证据的结论;
  成本靠按章节打批、硬预算和复查复用控制;认不出、没查的都写进检查覆盖范围。
- decisions.md:C1–C11、SK1–SK2 全部按建议确认;用户的真实国动投标文件作为留出集,第 3 步冻结代码后再用。
- ref-bid-engine-analysis.md:新增 §6 查标深读;§5.4 的检索方案改为内存 BM25。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-04 11:48:10 +08:00
parent ad34b0cb23
commit 6ab42959df
3 changed files with 328 additions and 1 deletions

View File

@@ -0,0 +1,263 @@
# 查标(M3)实现计划 v0.2(2026-11-04)
> 范围:FR-M3-01~09,加上查标依赖的 FR-M6-08 最小资质库。
> 依据:`docs/analysis/03-functional-requirements.md`、架构 §3.2 / §3.6~§3.9 / §3.12、`data-model.md` §4.3 / §5.5~§5.6、`api-design.md` §3~§4、原型 `bid-check.html` `results.html` `report.html` `qualifications.html`。
> v0.2:请 Fable 做顾问,深读参考项目的查标实现(`ref-bid-engine-analysis.md` §6),对照读标优化的教训(`extract-status.md`),重新评估了 v0.1 的待定事项。关键结论抽查过源码。
## 进度
| 步骤 | 状态 | 说明 |
|---|---|---|
| 第 0 步 评测集 | 未开始 | 第 2 步写规则之前要定稿问题分类表、响应清单格式和留出集 |
| 第 1 步 链路打通 | ✅ 2026-11-04 | 见下方「第 1 步落地记录」 |
| 第 2~6 步 | 未开始 | |
**第 1 步落地记录**(和本文设计不一致、或留到后面的,记在这里):
- 后端 `check` 模块六个阶段都已接上,`rules` 阶段还是空的;问题由 `check_item_result` 生成,状态沿用、计费三种方式、健全性检查(投标文件读不出文字按 `input` 失败、不扣点)都有集成测试。
- 响应匹配目前只有第三层:检索用内存 BM25,分词先用汉字 2-gram + 「数字 + 单位」+ 英文型号,**jieba 留到第 3 步按评测决定**;批量先按 8 条清单项一批,**按投标章节打批、token 预算、复查复用、重复条目分组、致命项重问留到第 3 步**;`cache_key`、`group_key` 列已建好、还没写入。
- 提示词 `check.match` v1;假模型按字面重合判断,只用来测链路。
- 投标分册加了 `volume_kind` 列(迁移 0005),同一分册重新上传按 `lineage_id` 递增版本;电子投标平台加密格式先只认 `.kedt`(苏采云)和条目全部加密的压缩包,见过新的再补。
- 前端:上传投标文件并检查页、查标页(进度、覆盖范围、按级别筛选、逐条处理和行内说明、撤销、原文抽屉、已处理和本次已解决),项目头抽成公共组件,加了「查标」标签页;键盘操作、批量处理、判断有误、需人工核对、导出报告留到第 6 步。
- 用真实模型(DeepSeek Flash)在虚构的 tender-mini + bid-mini 上走了一遍:漏响应的 ★ 条款查出为致命「未找到响应」,另两条判为已响应且出处核对通过;补上响应的 v2 复查后该问题「本次已解决」。一次检查一次模型调用(输入 1,552 token、输出 147 token)。这只说明链路通了,**不代表效果**,效果按第 0 步的评测集评。
## 0. 结论速览
- 表结构和接口契约大部分已有:`check_run` `finding` `finding_citation` `manual_check` `report` `qualification` 在基线迁移里,`openapi.yaml` 里查标相关的 20 多个操作都已定义。要做的是 `check`、`rules`、`report`、`qualification` 四个后端模块、投标文件的解析和结构识别,以及 4 个前端页面。
- 和 v0.1 相比,三处改动最大:
1. **先建评测集,再写第一条规则**。金标准是构造出来的:虚构投标文件时同时写一份「响应清单」,记下每条要求在哪里响应、故意埋了什么问题,按清单打分,不再用 AI 裁定。
2. **每条清单项都留一条带证据的结论,「已响应」也不例外**。新增 `check_item_result` 表,问题只为没响应好的条目生成。
3. **成本靠结构控制,不靠一个上限数字**:按投标文件的章节打批,同一段正文只送一次;每次检查有 token 预算,超出预算没查的条目列在报告的「检查覆盖范围」里;24 小时内复查时,没变的条目复用上次结果。
- 原则:**认不出、查不了、没查的,都写进「检查覆盖范围」(`coverage`),不静默跳过。** 读标里章识别静默失效的教训。
- 分 7 步交付,第 0 步(评测集)在第 2 步(规则)之前完成。
- §7 是 11 个待确认事项的最终建议(含 SK1、SK2)。
## 1. 现状盘点
| 项 | 已有 | 要做 |
|---|---|---|
| 表结构 | 基线迁移 `06_check.sql`、`07_…sql`;`project.latest_check_run_id` `open_fatal_count` `last_charged_check_at` | ORM 模型;新增 `check_item_result`(C9);清单快照里的重复组标记(C5) |
| 接口契约 | `check-preview`、`check-runs`、`findings` 的 resolve / reopen / bulk-resolve、`manual-checks`、`reports`、`qualifications`、`feedback` | 实现;补「每条清单项的检查结论」的读取接口。改了契约就 `pnpm gen:api` |
| 文件上传 | 分片上传、按文件头识别格式;`role = bid_volume` 能挂到项目上 | 分册类型和版本;**电子投标平台加密格式的识别还没做**(C8) |
| 文件解析 | 解析流水线和 `ParseJob` 绑在一起(`runner.py`),阶段函数基本是纯函数 | 拆出阶段函数给检查批次复用;新增投标侧结构识别 `bid_structure` |
| 清单 | 五类清单项,技术参数带 `op / value / unit`;确认时生成快照 | 检查前生成 `kind = check` 快照;**「报价」类清单项还没有抽取任务**(C7) |
| 额度 | 预扣 / 结算 / 退回、幂等键、余额行锁 | 检查的三种计费方式和免扣流水 |
| 模型调用 | 分块并发、截断检测(`near_limit`)、对半重跑、原因码 | **并发上限还是每个任务各算各的**(`extract.py:36` 的 TODO)。解析和检查同时跑会把供应商的每分钟请求数打满,查标上线前要做「供应商 × 租户」统一限流 |
| 事件流 | Valkey stream、`Last-Event-ID` 续读、`resync` | 加 `check_run`、`report_job` 的进度;发布用的客户端设短超时(SK1) |
| CI | PostgreSQL、Valkey 服务容器 | 加 Gotenberg(投标样例有 Word);数据库角色改为直接跑 `init-roles.sh`(SK2) |
| OCR | 未做(技术验证 S5) | 扫描件先按 C10 兜底;S5 排在 M3 之后 |
| 样本 | 10 份真实招标文件、S01–S10 标注集 | **没有投标文件**(C1) |
## 2. 检查流水线
`POST /projects/{id}/check-runs` 在同一个事务里:算计费方式 → 预扣(或记免扣)→ 生成清单快照 → 建 `check_run` → 入队。worker 按六个阶段跑,每个阶段写检查点、开始前清掉本阶段残留的产物,失败后从失败的阶段续跑(架构 §3.4)。
| 阶段 | 做什么 | 产出 |
|---|---|---|
| `prepare_bid` | 每个投标分册版本跑解析流水线的前五个阶段(格式核对、转换、文字层、OCR、结构识别),不抽取;同一版本只解析一次。然后做**投标侧结构识别**:偏离表、开标一览表、分项报价表、格式文件(投标函、授权书、声明函)、资格证明附件。表格沿用读标的按单元格切句(F8),偏离表直接从单元格里读。**健全性检查**:文字层为空或字数过少、全是图片页时,按 `input` 失败,不扣点、不生成问题 | 投标侧文字层;结构索引;每页有没有文字层 |
| `match` | 对每条参与检查的清单项找响应,见 §2.1 | `check_item_result` |
| `rules` | 确定性规则,见 §2.4 | 规则问题 |
| `verify` | 投标侧三道校验(编号、原文、依据)。**没有校验通过的证据,不能判为「已响应」** | 校验状态和原因码 |
| `carry_over` | 和上一次检查对齐,算出新增 / 沿用 / 本次已解决(FR-M3-08,对齐方式见 C5) | `presence` |
| `summarize` | 由 `check_item_result` 生成问题;写 `counts`、`coverage`;更新就绪状态和项目状态;汇总本次 `ai_call` 的 token 和费用;结算额度;发通知 | 检查完成 |
结构识别认不出(比如没找到偏离表)时,改用全文检索,并在 `coverage` 里写明「没有识别出偏离表」。
### 2.1 响应匹配
一份清单有 216~1348 条正式条目(参考项目每个项目只判 90 条左右,它的成本做法不能当依据)。分三层,能在前一层解决的就不进下一层:
1. **偏离表直取**。偏离表的行和清单项要先对齐:投标人抄过来的要求常被截断、改写、重新编号,所以用去掉编号后的文本相似度、参数名和顺序做确定性对齐,设置信阈值,对不上的行进第三层。MVP 以偏离表为准,「偏离表写无偏离,正文却相反」不查,写进 `coverage`。
2. **规则比较**。技术参数要求有 `op / value / unit` 的,用 Decimal 和单位换算比较读到的响应值,覆盖区间、枚举、支持 / 不支持、版本号。响应只写「满足」「响应」、没给具体值的,结论是「未给出具体值」(提示级),不是「疑似不满足」。模型只负责找出响应值在哪,满不满足由规则判断;模型给出的值必须出现在它引用的句子里。★ 参数判为「疑似不满足」时,问题里要同时列出两边的值、单位和比较依据,防止因为单位换算判错出致命误报。
3. **检索 + 模型判断**。见 §2.2。批量方式:
- **按投标文件的章节打批**:一段投标正文只送一次,带上候选句落在这段里的所有清单项。参考项目按清单项打批,同一页正文在一批里重复送好几遍,是反例。
- 每条清单项都必须有结论,漏答的记原因码 `missing_answer`,结论记「需人工核对」。
- 模型引用**句子编号**,不写页码和自由引文,这样三道校验能原样复用。
- 沿用读标的输出上限、`near_limit` 截断检测和对半重跑;每批之间检查取消标记;并发走统一限流。
- 部分批次因为模型的临时错误失败时,不拖垮整个批次:失败的条目记「未判定(模型错误)」,写进 `coverage`。
- 致命级条目的证据没通过校验时,允许重问一次:只把模型指的那一页作为候选,提示词不变。重问之后仍不通过,按 C3 处理。
- 提示词 `check.match` v1 的例子一律虚构,不取自评测集里的投标文件。
**token 预算**:每次检查有一个硬预算。超出后剩下的条目按 提示 → 重要 → 致命 的顺序放弃(先放弃提示级),没查的条目在 `coverage` 里逐条列出。预算按实测定(C2),正常情况下不应触发。
### 2.2 检索
用内存里的 BM25,保留架构 §6.2 的做法。`ref-bid-engine-analysis.md` §5.4 写的「用 PostgreSQL 的 pg_trgm / 全文检索」改掉,理由:
- 句子放对象存储、不进数据库(DM1)。用 PostgreSQL 检索就要为投标文字层另建一张句子表;
- 检索只在 worker 里、一次检查之内用。一个项目的投标文件最多几百页,在内存里建索引用不了几秒;
- 中文全文检索要装 zhparser 之类的第三方扩展,托管数据库和私有化部署都麻烦;pg_trgm 没有 IDF;
- 内存索引是纯函数,单元测试和离线评测都能直接用。
实现要点:
- 索引单位是带编号的句子和单元格,出处能回链;
- 分词用 jieba 加字 2-gram / 3-gram,「数字 + 单位」(≥32GB)和英文型号(10GE)保留成整词;
- 查询按类别构造:技术参数用参数名、值和单位,资格条件用材料名;
- 给模型的候选按句子窗口取上下文,不送整页;
- 排序带结构先验:技术参数先找技术偏离表,商务先找商务偏离表,资格先找资格证明分册。分册类型只用来排序,不限制检索范围(C8)。
### 2.3 问题级别
级别从清单项算出来,不让模型定(参考项目让模型定级别,模型可以把高危项降成低危):
| 清单项 | 级别 |
|---|---|
| `importance = substantive`(★ 条款、法规列明、原文写明无效),或类别含资格条件、无效 / 废标 | 致命 |
| ▲ 参数、评分项缺证明材料 | 重要 |
| 一般参数、其他 | 提示 |
问题上带 `importance_basis`,界面把「法规列明」和「文件写明」的无效分开显示(决策 Q4)。`explanation` 只写事实和出处,**不写整改建议**:参考项目让模型给每条结论写建议,用在报价类问题上就会写出报价数字(红线 6)。
### 2.4 规则(`rules` 模块)
每条规则都有编号,规则集有版本,写进 `check_run.rule_set_version`。每条规则都要有单元测试,至少配一个反例(不该报的写法)。
| 规则组 | 内容 |
|---|---|
| `PRICE.*` | 超出限价:总价、采购包、分项三层(分项见 C7);开标一览表和分项表的合计是否一致(苏采云范本写明「以开标一览表为准」,定为「重要」,说明里引用原文);大写和小写金额是否一致;是否只给了一个报价。金额只从投标文件读,`detail` 里存读到的金额、原文写法和出处 |
| `FORM.*` | 格式文件齐不齐;**格式文件里该填的地方没填**(C4);签章位置有没有图像(C4);落款日期是否合理;**出现了别的项目的名称、编号或采购人**:以本项目的名称、编号、采购人为准做比对,重点扫页眉页脚(照搬旧标书最常留在那里);泛泛匹配公司名只作弱证据;格式文件的固定文字有没有被改动:依赖招标侧识别出格式章,识别不出就不查,写进 `coverage` |
| `QUAL.*` | 证书在投标截止日是否已过期;等级能比较时看够不够;资格条件和评分项要求的材料有没有覆盖 |
| `REGIME.*` | 按法律体系切换术语(「投标无效」/「否决投标」)和无效情形清单;体系识别不出时用中性措辞。招标投标体系没有样本,`coverage` 里写「规则未经样本验证」 |
「需人工核对」的条目(份数、装订密封、递交方式、CA 加密)由规则按清单项生成 `manual_review`,勾选结果按项目存在 `manual_check`,复查时沿用(P2)。
### 2.5 检查覆盖范围(`coverage`)
第 1 步就定下字段,报告首页、查标页顶部都从这里读:
- 查了哪些分册和版本;哪些页没有文字层(扫描件);
- 哪些条目没查,以及原因:在待确认区、不在所投的包、超出预算、模型错误、扫描件;
- 结构识别结果:偏离表、开标一览表、格式文件,哪些认出来了;
- 法律体系,以及这套体系的规则是否经过样本验证;
- 清单是否已确认(G7);
- 规则集版本、提示词版本;本次 token 和费用;证据没通过校验的条目数(按原因码计)。
## 3. 计费
按 `data-model.md` §4.3 实现:
| 情况 | `check_run.billing` | 流水 |
|---|---|---|
| 专业版,24 小时窗口外 | `charged` | 在解析额度上预扣 −1;成功结算,失败、取消退回;成功后更新 `project.last_charged_check_at` |
| 专业版,24 小时窗口内 | `waived_recheck` | 免扣一行 0,`reason = recheck_24h`,关联本次检查 |
| 免费版 | `free_plan` | 不记流水 |
`check-preview` 和 `POST check-runs` 共用同一个计费函数;`expected_charge` 对不上时返回 409 `billing_changed`,不扣点。投标文件的解析算在本次检查里,不另外扣点。
**复查复用结果**:第三层(模型判断)的结论按「清单项内容哈希 + 投标分册版本哈希 + 提示词版本」缓存。没变的条目直接复用上次的 `check_item_result`,不再调用模型。否则一次扣点可能被拿来跑三五次全量。用户改了清单项,或者换了分册版本,相关条目就会重新判断。
## 4. 评测集(第 0 步)
没有任何真实投标文件,先自己造。读标的三个坑这样堵:
| 读标的坑 | 查标的做法 |
|---|---|
| AI 标注、AI 裁定,结果偏宽 | 投标文件是按「响应清单」写出来的,问题在构造时就已知,**响应清单就是金标准**,不需要 AI 裁定 |
| 评分脚本照搬抽取规则 | 评分只对照响应清单,不用检索和偏离表对齐的逻辑来判对错。准确率分两列报:对上响应清单的,和其他 |
| 规则照着样本写法调 | 至少两种写法风格,按招标文件整体留出,开工前选定 |
做法:
1. **问题分类表先定**:从 05 §5 的 R01–R20、07 F6 / F7 和 7 种问题类型派生,不少于 15 类,每类至少 3 种写法。例如:缺行、值不满足、单位不同、写在错的分册里、偏离表写「无偏离」正文却否定、页眉留着旧项目名、大小写金额不一致、证书过期、该填的地方没填。
2. **用招标文件自己的格式章写投标文件**:投标函、开标一览表、偏离表、声明函的模板,S01–S10 里都有。每份招标文件写两份投标文件,一份「规矩的」、一份「马虎的」;至少两种写法风格,其中一种由另一套提示词或另一个人来写。
3. **响应清单**:每条清单项记响应在哪(分册、页码 + 片段),以及怎么响应的(偏离表的行、正文句子、附件);没响应的记问题类型。
4. **留出集**:按招标文件整体划分,开工前选定,其中至少包括 S06(有分项限价)或 S10(多包、有单价上限)中的一份。
5. **裁定政策**:开发集上查出了响应清单里没有的问题,由人裁定一次,再改响应清单。投标文件里的问题是客观可查的,这是唯一允许「看过输出再改标注」的情况。留出集只记录,不改。
6. **人工复核**只看一件事:写出来的投标文件里,有没有无意中留下的问题(占位符没填、复制错位)。
7. 指标:致命级问题的召回率(漏报比误报严重);问题准确率;证据没通过校验的问题占致命问题的比例(目标 < 10%,否则会成为查标的「待确认区噪音」);每次检查的 token 和费用。
8. 按真实招标文件写出的投标文件,和招标文件一样不进 git。
## 5. 分步交付
每一步都要通过后端检查和前端检查(AGENTS.md「工程」一节);新接口都要有「A 企业的令牌访问 B 企业的资源,返回 404」的用例。
### 第 0 步:评测集
按 §4。问题分类表、响应清单格式、留出集划分,**在写第一条规则之前定稿**。投标文件可以和第 1 步并行写。
### 第 1 步:链路打通(假模型)
- 投标分册:类型和版本;加密格式识别(C8)。
- 拆 `ParseJob` 的阶段函数,`prepare_bid` 复用;健全性检查。
- `check` 模块:`check-preview`、发起 / 取消 / 查询检查、六个阶段的骨架、计费三个分支、状态沿用、`check_item_result`(C9)、`coverage` 字段(§2.5)。
- 处理问题:resolve(忽略要写原因;致命问题「确认无误」也要写说明,同时写一条 `feedback`)、reopen、只能批量处理「提示」级、只能处理最近一次检查的问题;同一个事务里更新 `open_fatal_count`。沿用「已确认无误」的致命问题时,如果证据所在的分册换了版本,标出「证据所在文件已更新」。
- SK1、SK2 的补丁:Valkey 发布客户端设短超时,SSE 连接池设上限;CI 加 Gotenberg、直接跑 `init-roles.sh`;架构 §8 改掉 testcontainers 的写法。
- 前端:上传投标文件和发起检查页、查标页的最小版本。
验收:在虚构样例上走完「上传投标文件 → 检查 → 处理问题 → 复查后状态沿用」;扣点、免扣、免费版、失败退回、409 `billing_changed`、健全性检查失败不扣点,都有测试。
### 第 2 步:投标侧结构识别和规则
- `bid_structure`:偏离表、开标一览表、分项报价表、格式文件、资格证明附件;认不出时走兜底,并写进 `coverage`。
- `rules` 模块:§2.4 的四组规则;需人工核对的条目和 `manual-checks` 接口。
- 「报价」类清单项(C7)。
- 只在开发集上调,每条结构规则配一个开发集以外的反例。
### 第 3 步:AI 响应匹配
- §2.1 三层、§2.2 检索、`check.match` 提示词、投标侧三道校验、致命项重问一次。
- 「供应商 × 租户」统一限流(解决 `extract.py:36` 的 TODO)。
- token 预算、复查复用结果、重复条目分组(C5)、扫描件兜底(C10)。
- 在开发集上调完后冻结代码,留出集跑一次,结果写进 `docs/architecture/check-eval.md`,按实测定 C2 的元数。
### 第 4 步:资质库和资质匹配
- `qualification` 模块:资质的增删改、附件(企业级数据密钥,DM4);前端资质库页。
- `QUAL.*`:过期、等级不足、未覆盖。资格证明是扫描件时,按名称回到资质库里匹配(C10)。资质数据只来自用户自己的资质库和投标文件(红线 5)。
### 第 5 步:检查报告
- HTML 模板 → Gotenberg 的 Chromium 转 PDF,嵌入思源宋体、思源黑体;后台生成,完成后发 `report.ready`。
- 内容按 FR-M3-09:首页(项目信息、投标文件版本、检查时间、规则版本、检查覆盖范围、免责声明「辅助检查结果,仅供投标前自查参考」)、汇总、问题明细、需人工核对附录。另加一页「证据索引」,每条证据的文件、页码、引文单独成行,方便逐条翻原文。
- 报告生成后不再改,存哈希;重复导出就生成新报告。CI 检查模板文案,不能出现「通过 / 合格 / 保证」;logo 不当作结论章使用。
### 第 6 步:前端补齐
- 查标页:对照布局(招标原文 / 投标原文)、键盘操作(含 U 键撤销)、批量处理、「反馈:判断有误」、需人工核对、`coverage` 摘要。
- 读标清单上显示每条清单项的检查结论(「已响应 · 第 N 页」),已响应的也能反馈判断有误。
- 报告页、项目概览的检查记录、工作台的就绪状态和致命问题数。致命问题没处理完时显示「未就绪」,但不限制导出和递交(红线 4)。
## 6. 红线对照
| 红线 | 落在哪里 |
|---|---|
| 1 辅助定位 | 每条清单项的检查结论都留痕,包括「已响应」;处理问题记录处理人和时间 |
| 2 可复核 | 招标侧出处必有;投标侧出处要过三道校验,没通过的不判「已响应」;「未找到」的写明找过哪些分册、哪些章节;规则问题没有清单项时挂规则编号 |
| 3 不用绝对化表述 | 只称「辅助检查 / 风险提示」;没查的写进覆盖范围;报告模板做 CI 文案检查 |
| 4 不阻断用户 | 就绪状态只是提示 |
| 5 不编造 | 资质只来自资质库和投标文件;不给整改建议 |
| 6 报价 | 只读金额、不生成金额,问题说明里不写建议 |
| 7 签章 | 只提示位置和有无,写明「不鉴定真伪;电子签章可能在投标平台加盖」 |
| 9 额度 | 失败、取消、健全性检查不过都不扣点;免扣也记一行;复查复用结果 |
## 7. 待确认事项:最终建议(2026-11-04 全部确认,见 decisions.md)
| # | 事项 | 建议 |
|---|---|---|
| C1 | 投标文件样本 | 按 §4 自己写:用招标文件自己的格式章,响应清单就是金标准,按招标文件整体留出,人工只裁定开发集上「响应清单里漏记的问题」。**用户有一份真实的国动小程序投标文件**,作为留出集里唯一的真实样本:第 3 步冻结代码后再要来跑一次,之前不看、不按它调 |
| C2 | 单次检查成本 | 不先定元数。设计时按 token 预算:模型输入总量不超过投标正文字数的 3 倍加上清单文字。另加三条:按投标文件章节打批、每次检查有硬预算且超出部分写进覆盖范围、复查复用结果。第 3 步在 S06 规模的样本上实测,再定元数(一起为 D2 提供依据) |
| C3 | 模型说已响应,但证据没通过校验 | 不判已响应。问题类型记「未找到响应」,级别不降,`detail` 记证据状态和原因码,说明里写「模型认为第 N 页有响应,引文未能核对」,界面可以按原因码筛。致命项允许重问一次(只给模型指的那一页)。评测单列这类问题的比例,目标是占致命问题的 10% 以内 |
| C4 | 签章 | 只提示位置和有无。主要检查是**格式文件里该填的地方有没有填**:投标人名称、法定代表人或授权代表姓名、落款日期(「年 月 日」)还空着。「(盖单位章)」「(签字)」这类括注在盖章后通常仍然留着,括注本身不算问题。图像检测只看签章位置附近有没有图像,结论只到「提示」。Word 稿也转成了规范化 PDF,两种稿走同一套检查 |
| C5 | 重复条目 | 检查时分组:只在同类别、同一个包里,按保守的文本相似度聚组(阈值记成规则,配反例)。问题挂在组里编号最小的清单项上,其他成员的出处作为附加的招标侧出处,`detail.group_members` 记全组;复查对齐时,组内任一成员对上就算同一个问题。分组结果记在检查快照里,界面显示「此要求出现在 3 处」。读标阶段合并以后再做 |
| C6 | 是否全查 | 正式清单全查。没查的写进覆盖范围:待确认区的条数、不在所投包的条目、超出预算的条目 |
| C7 | 分项限价 | 补 `extract.price`,规则优先(「限价 / 最高限价 / 单价不超过」+ 金额 + 所在的表行),模型只做结构化;用户也能在清单里手工新增「报价」类条目。10 份里只有 S06、S10 两份有分项限价或单价上限,不让它卡住总价和采购包两层 |
| C8 | 投标分册类型 | 固定选项:整本(默认)/ 资格证明 / 商务 / 技术 / 报价 / 其他,再加一个自填的分册名称。类型只用来排序检索候选和填写「找过哪些分册」,不限制检索范围。电子投标平台的加密格式按文件头和扩展名维护一张表,422 写明是哪个平台的格式 |
| 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 写法 |
| SK1 | 进度推送用 Valkey stream | 确认。补两处:发布用的客户端设短超时(现在 `valkey()` 没设,Valkey 挂掉时会卡住 worker,而不是只记日志);SSE 连接池设上限 |
| SK2 | 集成测试连 Compose 的 PostgreSQL | 确认。补三处:CI 直接跑 `init-roles.sh`,不再单独写一份建角色的 SQL;CI 加 Gotenberg;架构 §8 改掉 testcontainers 的写法 |
## 8. 读标教训在本计划里的对应
| 读标的教训 | 本计划 |
|---|---|
| 评测集建得太晚,在同一批样本上既调又评 | 第 0 步在规则之前;按招标文件整体留出;冻结代码后留出集只跑一次 |
| AI 标注、AI 裁定偏宽 | 响应清单就是金标准 |
| 评分脚本照搬抽取规则 | 只对照响应清单打分,准确率分两列 |
| 提示词例子取自测试文件 | `check.match` 的例子一律虚构 |
| 输出临近上限时条目静默丢失 | 沿用截断检测和对半重跑;每条必答,漏答记原因码 |
| 待确认区噪音 | 证据没通过的问题记原因码、单列指标、可筛选 |
| 章识别静默失效 | 认不出就兜底,并写进覆盖范围 |
| 规则只修真正的缺陷 | 投标侧引文对不上不在附近找句子改挂;致命项重问是重试,不放宽校验 |

View File

@@ -256,4 +256,46 @@
4. 识别占位符;
5. 提示词的禁止事项和服务端校验一一对应。
- **评测之后做**(涉及文字层数据结构):顶级章节识别并按章节类型分派任务;每页质量指标和扫描件判定;表格按行生成证据单元、带表头(解决两栏表、竖排文字串行)。
- **留到 M3 设计**:检查项来源分层、判定分层、检索不到不调模型、版式规则扫描;必须补投标侧引文逐字校验,pass 必须有校验通过的证据。检索用 PostgreSQL 的 pg_trgm / 全文检索,不自建倒排。
- **留到 M3 设计**:检查项来源分层、判定分层、检索不到不调模型、版式规则扫描;必须补投标侧引文逐字校验,pass 必须有校验通过的证据。~~检索用 PostgreSQL 的 pg_trgm / 全文检索,不自建倒排。~~(2026-11-04 改:句子不进库(DM1),中文全文检索要第三方扩展,改用内存 BM25,见 §6.4 和 `m3-plan.md` §2.2。)
## 6. 查标深读(2026-11-04 补)
> 为 M3 设计再读一次,聚焦 `handler/bidreview/`(18 个文件,约 7100 行)。由 Fable 做顾问走查,关键结论对照源码抽查过(预设清单项、按清单项重复送页面文本、投标侧证据不校验)。只读、未运行。路径相对 `bid-engine-backend/pkg/handler/bidreview/`。采纳情况见 `m3-plan.md`。
### 6.1 规模不同,成本做法不能照搬
它每个项目只有 14 条预设项、最多 24 条否决条款、若干资格字段和评分行、最多 18 条材料清单(`checklist.go:42-59, 133-135, 292-300`),大约 90 条。「技术方案对招标需求全覆盖」只算一条预设项(`checklist.go:57`),技术参数不逐条查。我们的正式清单有 216~1348 条。
### 6.2 值得学的做法
| 做法 | 位置 | 我们怎么用 |
|---|---|---|
| 检查项来源分层,每条有稳定的键;重建清单时只删非人工来源的检查项 | `checklist.go:97-124`;`verdict.go:147-150` | 对应 `match_key`。它的评分项键带序号(`checklist.go:214`),重抽后排序一变键就变,我们用清单项编号 |
| 召回为空就不调模型,直接「需人工确认」;批量结果里漏答的条目默认给 warning,不给 pass | `verdict.go:296-306, 375-394, 498-509` | 每条必答,漏答记 `missing_answer` |
| 判定阶段部分失败不拖垮整体,失败的条目可以单独复检 | `verdict.go:237-245`;`handlers_items.go:122-201` | 失败条目记「未判定(模型错误)」,写进覆盖范围 |
| 所有清单项都召回不到候选时,整个阶段失败并提示检查投标文件 | `pipeline.go:387-389` | `prepare_bid` 之后的健全性检查,按 `input` 失败、不扣点 |
| 版式扫描分强 / 弱证据;没有文字层的页面占比过高时,出「需人工核对」并列出页码 | `format_scan.go:100-176, 235-278` | 「别的项目名称」以本项目名称、编号、采购人为准做强证据;没有文字层的页码写进覆盖范围 |
| 页眉页脚单独落块,版式检查专门扫它 | `parse.go:194-211`;`format_scan.go:178-233` | 我们文字层已把页眉页脚单独成句(SK10),查别的项目名称时重点扫 |
| 报告附一张证据索引:每条证据的文件、页码、引文单独成行 | `excel.go:162-179` | 报告加「证据索引」页 |
| 每个功能的输出上限写死在代码里;模型并发用一个信号量,流水线和人工复检共用 | `llm.go:22-29, 68-91` | `check.match` 固定输出上限;解析和检查共用统一限流 |
### 6.3 需要避坑
| 问题 | 位置 | 对照 |
|---|---|---|
| 报价、签章、截止日都是「把要求写成文字,让模型读投标文件判断」:限价没有确定性的金额比对,签章没有任何图像层面的检查 | `checklist.go:46-50, 102-106`;`verdict.go:254` | 红线 6、7 |
| 投标侧证据不校验:模型给的页码和引文原样入库,页码对不上就挂到第一个召回文件;pass 没有证据也照样 pass;批量判定也一样 | `verdict.go:322-327, 397-402, 428-434, 465-486` | 红线 2 |
| 问题级别由模型定,模型可以把高危项降成低危 | `verdict.go:321, 396, 498-522` | 级别从清单项算 |
| 模型给每条结论写「整改建议」 | `verdict.go:72, 96, 113` | 报价类问题会写出数字(红线 6);我们不写建议 |
| 检索:每块只索引前 240 个词,后半块静默丢失;候选页截到 1400 字;一批里同一页正文给每条清单项各送一遍 | `parse.go:436-438`;`retrieval.go:75`;`verdict.go:345-353` | 召回静默损失,成本翻倍 |
| 投标文件的表格不做结构解析,偏离表、报价表被压成纯文本 | `pipeline.go:273`;`parse.go:113-116` | 偏离表直取靠单元格结构 |
| 解析不出页时把全文每 3000 字切成「伪页」;转 PDF 失败就直接解析原文件 | `parse.go:214-231` | 页码失真;我们按 `input` 失败、不扣点 |
| 输出截断不检测:JSON 修补失败就整批丢掉 | `llm.go:151-157` | 沿用 `near_limit` 和对半重跑 |
| 否决条款最多映射 24 条,超出的静默截掉 | `checklist.go:127-135` | 没查的都写进覆盖范围 |
| 报告用「通过 / 高风险 / 竞争力预估得分」 | `excel.go:18-24, 76`;`scoring.go:77-82` | 红线 3、5 |
| 复检后清单项上的人工确认状态不重置,用户确认的是旧结论(推测) | `verdict.go:411-448`;`handlers_items.go:96-101` | 证据所在分册换了版本时提示 |
| 企业规则的适用条件存了但从不求值,每条规则都进清单 | `checklist.go:237-251` | V1 做规则库时条件要真求值 |
### 6.4 检索方案
保留架构 §6.2 的内存 BM25 + jieba,不用 PostgreSQL 检索:句子放对象存储、不进库(DM1);检索只在 worker 里、一次检查内用,几百页建索引用不了几秒;中文全文检索要装 zhparser 之类的第三方扩展,pg_trgm 没有 IDF;内存索引是纯函数,便于测试和离线评测。它把 2-gram / 3-gram 倒排放进 MySQL 的代价(只索引前 240 词、没有 IDF)就是反例。

View File

@@ -4,6 +4,28 @@
---
## C1–C11、SK1–SK2 · 2026-11-04 · 查标(M3)实现计划的待定事项:全部按建议,C1 补充真实样本
依据 `docs/architecture/m3-plan.md` v0.2 §7。v0.2 是请 Fable 做顾问,深读参考项目的查标实现(`ref-bid-engine-analysis.md` §6),对照读标优化的教训重新评估后的结果。
| # | 决策 | 理由 |
|---|---|---|
| C1 | 评测集按 `m3-plan.md` §4 自己写:用招标文件自己的格式章写虚构投标文件,同时写「响应清单」作为金标准,按招标文件整体留出,人工只裁定开发集上响应清单漏记的问题。**补充**:用户有一份真实的国动小程序投标文件,作为留出集里唯一的真实样本,第 3 步冻结代码后再要来跑一次,之前不看、不按它调 | 读标的三个坑(AI 标注偏宽、评分照搬规则、规则照样本调)分别由响应清单、只对照响应清单打分、多写法 + 留出来堵。真实样本只有一份,用来检验泛化最有价值 |
| C2 | 不先定元数。按 token 设计预算;按投标文件章节打批,同一段正文只送一次;每次检查有硬预算,超出没查的条目写进覆盖范围;24 小时内复查时按「清单项内容哈希 + 分册版本哈希 + 提示词版本」复用结果。第 3 步实测后再定元数 | 参考项目每个项目只判约 90 条,我们 216~1348 条,量级不能照搬;可见的覆盖范围比一个上限数字更诚实 |
| C3 | 模型说已响应,但证据没通过校验:不判已响应,记「未找到响应」,级别不降,记原因码,界面可筛;致命项允许重问一次(只给模型指的那一页);这类问题占致命问题的比例单列指标,目标 < 10% | 红线 2;原因码和单列指标来自读标待确认区噪音的教训 |
| C4 | 签章只提示位置和有无。主要检查格式文件里该填的地方(投标人名称、签字人姓名、落款日期)有没有填;「(盖单位章)」这类括注不算问题;图像检测只看签章位置附近有没有图像,结论只到「提示」;Word 稿和 PDF 稿都走规范化 PDF,同一套检查 | 电子签章常在投标平台里加盖,稿子里本来没有章;括注盖章后通常仍保留 |
| C5 | 重复条目在检查时分组:同类别、同一个包、保守相似度;问题挂在组里编号最小的清单项上,其他成员的出处附上;复查对齐时组内任一成员对上就算同一个问题;分组记在检查快照里。读标阶段合并以后再做 | 读标同一要求常抽成三条,直接查会出三个一样的问题 |
| C6 | 正式清单全查;待确认区、不在所投包、超出预算的条目写进覆盖范围 | 「全部都查,或者明确说没查哪些」 |
| C7 | 补 `extract.price`,规则优先,模型只做结构化;允许手工新增「报价」类条目;不卡住总价和采购包两层 | 10 份里只有 S06、S10 两份有分项限价或单价上限 |
| C8 | 分册类型:整本(默认)/ 资格证明 / 商务 / 技术 / 报价 / 其他,加自填名称;类型只用来排序检索候选,不限制范围;电子投标平台加密格式维护一张识别表 | 很多投标人一个文件装全部;用户常标错类型 |
| C9 | 新增 `check_item_result`(检查批次 × 清单项的结论、所走的层、投标侧出处、校验状态、原因码、token);问题仍只为没响应好的条目生成 | 「已响应」也是 AI 结论,按红线 1、2 要能复核、能反馈;复用结果和覆盖范围也靠它 |
| C10 | 扫描件:候选分册里图片页占比超过阈值的条目不出「未找到响应」,改出「请人工核对」;资格类按名称回到资质库匹配;覆盖范围列出没有文字层的页码。OCR(S5)排在 M3 之后 | 真实投标的资格证明多是扫描件,不处理会出一大片致命级「未找到」 |
| C11 | 检索用内存 BM25 + jieba,不用 PostgreSQL 的 pg_trgm / 全文检索 | 句子不进库(DM1);中文全文检索要第三方扩展;内存索引便于测试和评测 |
| SK1 | 确认:进度推送用 Valkey stream。补:发布用的客户端设短超时,SSE 连接池设上限 | 现在 `valkey()` 没设超时,Valkey 挂掉时会卡住 worker |
| SK2 | 确认:集成测试连 Compose 起的 PostgreSQL。补:CI 直接跑 `init-roles.sh`、加 Gotenberg;架构 §8 同步 | CI 里的建角色 SQL 和部署脚本是两份,会漂移;投标样例有 Word |
- **影响**:`m3-plan.md` 按此执行;`data-model.md` 加 `check_item_result`(迁移在第 1 步);架构 §8、§13 同步;`ref-bid-engine-analysis.md` §5.4 的检索写法已改。
## Q1–Q4 · 2026-11-03 · 实质性条款的四个口径:全部按建议
依据 `docs/architecture/extract-eval.md` §5(S01–S10 标注时遇到、规范里没定的情形)。