Files
bid-doctor/docs/architecture/m3-plan.md
YANG JIANKUAN 54cbb556d0
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled
feat(ui): 查标第 6 步前端补齐
- 读标页:每条清单项写最近一次检查的结论,点投标侧出处看投标原文;
  「已响应」也能反馈判断有误(漏报);证书要求写资质库里的情况
  (新接口 GET /projects/{id}/qualification-matches,资质库空时不写)
- 概览:检查记录卡(历次检查、各级问题数、报告下载)、文件卡
- 工作台:「致命」列、统计可点筛选、待处理的致命问题卡(?f= 定位到问题)
- 日期输入加日历弹层(DateInput,Reka UI Calendar)
- 查标页:? 键显示快捷键说明;反馈对话框抽成 FeedbackDialog 共用
- 扫描件提示去掉多余空格

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 13:39:19 +08:00

271 lines
32 KiB
Markdown
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.

# 查标(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 步 评测集 | 试跑完成,继续扩充 | 规范 v0.2、留出集划分已定,留出集词库和风格 C 已封存;生成、注入、渲染、离线查标、评分工具已写;开发集 7 份招标文件、14 份投标文件第一版跑完,结果和发现见 `check-eval.md` §2 |
| 第 1 步 链路打通 | ✅ 2026-11-04 | 见下方「第 1 步落地记录」 |
| 第 2 步 规则 | 基本完成 | 响应方式(C12)、文件要在、扫描件兜底、规则检查 `rules.py`(报价形式、形式要件、响应表)、需人工核对(`manual.py`、`manual-checks` 接口)、「报价」类清单项和分项上限(C7)。还差分项和采购需求逐项对照(P13)、散文写法的单价上限。见 `check-eval.md` §4–§7 |
| 第 3 步 AI 响应匹配 | 进行中 | 整行上下文、提示词 v2、偏离栏直读、响应错位、复查复用、硬预算;送给模型的同一行按单元格、重复条目只合并显示(C5 修订);致命项重问按数据暂不做。见 `check-eval.md` §8–§9 |
| 第 4 步 资质库 | ✅ 2026-11-04 | 方案和落地记录见 `qualification-plan.md`(决策 QL1–QL7)。`qualification` 模块(增删改、附件按企业级密钥加密、`late_for` 到期提醒、审计)和前端资质库页;`check/qualification.py`:证书类别词表(16 类)、规则 `QUAL.EXPIRED / LEVEL_LOW / NOT_COVERED`,投标文字层按清单项定级、只有资质库记录的降一级,规则集 2026.11-r4。评测见 `check-eval.md` §13 |
| 第 5 步 检查报告 | ✅ 2026-11-04 | `report` 模块:导出 → worker 用 Gotenberg(Chromium)把 HTML 转成 A4 PDF → 按项目数据密钥加密存对象存储 → `report`(不可改,存哈希);`report_job` 记生成过程(迁移 0009),完成推 `report.ready`、发通知。首页有项目信息、投标文件版本、检查依据、规则版本、导出人、检查覆盖范围、汇总、免责声明;问题明细带两侧出处和处理人;附录是需人工核对的勾选结果;页眉 logo、页脚「本报告为辅助检查结果,仅供投标前自查参考」+ 页码;测试检查模板文字不出现「通过 / 合格 / 保证」。前端「报告」标签页:选项、导出、内嵌预览、历次报告下载。「证据索引」页没有单独做(出处已在问题明细里逐条列出)。同时补了项目状态流转(递交 / 撤销递交 / 归档 / 撤销归档,FR-M6-02):致命问题未处理也能递交,确认框说明;递交后只读,撤销归档回到归档前的状态,都写审计 |
| 第 6 步 前端补齐 | ✅ 2026-11-05 | 查标页:对照布局、键盘(J / K / O / 1 / 2 / 3 / X / U / ?)、「提示」批量确认、判断有误反馈、从工作台带 `?f=` 定位到问题。读标页:每条写最近一次检查的结论,点投标侧出处看投标原文,「已响应」也能反馈判断有误(漏报);资格、评分里的证书要求写资质库的情况(`GET /projects/{id}/qualification-matches`,资质库空时不写)。概览:检查记录卡(历次检查、各级问题数、报告下载)、文件卡(招标方文件、投标分册最新版本,追加补遗挪到这里)。工作台:「致命」列、统计可点筛选、待处理的致命问题卡。日期输入加日历弹层(`DateInput`,Reka UI Calendar,周一开始、非本月留空)。反馈对话框抽成 `FeedbackDialog` 两页共用 |
**第 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 步:投标侧结构识别和规则
- 读标新增「响应方式」抽取(C12):类别 × 方式 × 出处,规则优先;查标按响应方式决定「未找到响应」报不报、报什么级别。
- `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 写法 |
| C12 | **每条要求怎么才算「响应了」**(2026-11-04 新增,已按建议确认,decisions.md C12) | 招标文件对响应方式的规定按类别不同(`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 的写法 |
## 8. 读标教训在本计划里的对应
| 读标的教训 | 本计划 |
|---|---|
| 评测集建得太晚,在同一批样本上既调又评 | 第 0 步在规则之前;按招标文件整体留出;冻结代码后留出集只跑一次 |
| AI 标注、AI 裁定偏宽 | 响应清单就是金标准 |
| 评分脚本照搬抽取规则 | 只对照响应清单打分,准确率分两列 |
| 提示词例子取自测试文件 | `check.match` 的例子一律虚构 |
| 输出临近上限时条目静默丢失 | 沿用截断检测和对半重跑;每条必答,漏答记原因码 |
| 待确认区噪音 | 证据没通过的问题记原因码、单列指标、可筛选 |
| 章识别静默失效 | 认不出就兜底,并写进覆盖范围 |
| 规则只修真正的缺陷 | 投标侧引文对不上不在附近找句子改挂;致命项重问是重试,不放宽校验 |