# 查标(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(`evals/check/GUIDELINES.md`,Fable 审查后修改)和留出集划分(`evals/check/split.json`)已定;渲染、离线查标脚本已写。下一步先做 S02、S04、S10 六份跑通 | | 第 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 写法 | | 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 的写法 | ## 8. 读标教训在本计划里的对应 | 读标的教训 | 本计划 | |---|---| | 评测集建得太晚,在同一批样本上既调又评 | 第 0 步在规则之前;按招标文件整体留出;冻结代码后留出集只跑一次 | | AI 标注、AI 裁定偏宽 | 响应清单就是金标准 | | 评分脚本照搬抽取规则 | 只对照响应清单打分,准确率分两列 | | 提示词例子取自测试文件 | `check.match` 的例子一律虚构 | | 输出临近上限时条目静默丢失 | 沿用截断检测和对半重跑;每条必答,漏答记原因码 | | 待确认区噪音 | 证据没通过的问题记原因码、单列指标、可筛选 | | 章识别静默失效 | 认不出就兜底,并写进覆盖范围 | | 规则只修真正的缺陷 | 投标侧引文对不上不在附近找句子改挂;致命项重问是重试,不放宽校验 |