Files
bid-doctor/docs/architecture/ref-bid-engine-analysis.md
YANG JIANKUAN f99bb8b358 feat(server): 参考 BidEngine 补五项:原因码、截断重跑、项目信息逐项表态、占位符、提示词规则对应校验
- 待确认记原因码(extract.FAILURES,存在 ai_values.failures),解析结果按原因计数
  (verify_failures);前端待确认条目显示具体原因
- 相邻分块重叠约 800 字;模型输出被截断或不是合法 JSON 时对半重跑(最多 3 层),
  拆到底仍不完整记 output_incomplete。修复:以前截断时整块条目被当成「没有结果」悄悄丢掉
- 项目基本信息提示词 v2:12 个字段逐项表态 found / ambiguous / not_found,跨分块合并为
  found / absent / needs_review(info_fields)
- 「____元」「××」「( )」「待填」等占位符不算取值
- 提示词 substantive v4,qualification / scoring / schedule / tech_param v3:每条禁止事项
  对应一个原因码,对照表在 prompts/README.md
- 顾问(Fable)深读参考项目的结论补进 ref-bid-engine-analysis.md §5;架构 §13 SK12
- 待确认:国动 4 → 2,S02 13 → 3,S06 57 → 55

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 21:59:12 +08:00

260 lines
26 KiB
Markdown
Raw Permalink 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.

# 参考项目分析:标擎 BidEngine
> 对象:`opensource-ref/bid-engine/`(github.com/panviviplus/bid-engine),招投标 AI 工作台,约 16 万行。阅读日期:2026-11-03。
> 方法:按三个方向只读走查源码,关键结论对照源码抽查过。只看了代码,没有部署运行;标「推测」的未经运行验证。
> 目的:为「技术架构设计」阶段提供参考和避坑清单。
> **许可证**:PolyForm Noncommercial 1.0.0,只允许非商业使用。本文只记录**做法层面**的结论,不得把它的代码、提示词、SQL、文案搬进本项目(见 AGENTS.md「参考开源项目」)。本文的结论只是讨论素材,正式结论以 decisions.md / analysis / PRD 为准。
下文的代码路径默认相对于 `opensource-ref/bid-engine/bid-engine-backend/pkg/`;`smart-bid.sql` 指 `opensource-ref/bid-engine/docs/sql/smart-bid.sql`。
## 0. 总评
它在「防止 AI 乱引用原文」上做得扎实,值得学:证据编号白名单由服务端核对,审核侧还会把引文和原文逐字比对。但租户隔离、密钥管理、模型路由和数据贯通都比较粗糙,而这几处恰好是我们的产品红线。它没有一份贯穿全流程的「检查清单」,正好反证了我们以项目检查清单为核心的设计方向是对的。
| 方向 | 值得参考 | 需要避坑 |
|---|---|---|
| 技术架构 | 原文块带页码和坐标;按页检查解析是否完整;AI 值和人工值分开存;每次解析是一次独立运行 | 登录密钥写死在源码里;租户隔离靠手写;API Key 明文存库;自研队列会丢任务;接口文件过大,没有测试 |
| 提示词工程 | 证据编号白名单;引文逐字核对;固定字段逐项表态;结构化输出逐级降级;输出被截断后二分重跑 | 只核对编号不核对内容;审核结论的证据不校验;领域知识缺位;写标书时诱导编造;没有版本号和评测集 |
| 模型路由与业务 | 只对临时错误重试;错误分类;按模型的上下文长度分块;调用流水账;记录模型配置快照 | 用户可以随意填模型地址;只有部分环节记调用账;三个模块之间靠复制数据衔接;重跑会丢人工确认;评分预估是机械折算 |
## 1. 技术架构
### 1.1 现状
- 后端是 Go 1.23 + Gin + GORM(用 gorm.io/gen 生成代码),MySQL 8 有 102 张表;任务队列和进度事件用 Redis 7,对象存储用 MinIO,文档解析用 Docling,另有一个 doc→PDF 转换服务。
- 前端是 Next.js 14(App Router)+ Chakra UI,富文本编辑用 TipTap 3。前端通过 `app/api/[...path]` 同源代理到后端。
- 采集服务是 Python 写的(FastAPI + Playwright + trafilatura),单独部署,不连数据库。
- 后端是单体:HTTP 服务和所有后台 Worker(解析、蓝图、生成、审核、情报采集,外加回收卡死任务的扫描器)在同一个进程里启动(`bid-engine-backend/cmd/server/main.go:89-178`)。
- 分层名义上是 handler / repo / service,实际业务逻辑基本写在 handler 里:`pkg/service` 只有 325 行,`handler/bidanalysisv3` 直接操作数据库约 107 处。
### 1.2 值得参考
1. **原文切块、带页码和坐标入库**:表结构见 `smart-bid.sql:205-231`,坐标处理见 `handler/bidanalysisv3/document.go:544-557`。坐标兼容了左下角原点的情况,并统一缩放到 0–1,前端直接叠一层高亮。
→ 它只能高亮到 Docling 切出的整块,我们要求高亮到原句,需要切到行或字符级。
2. **按页检查解析是否完整**(`document.go:391-454`):逐页核对 Docling 解析出的页数;平均每页少于 40 字时,自动加 OCR 重跑。
→ 可以直接用来判断扫描件和「解析失败」,对应红线 9「解析失败不扣点」。
3. **AI 值和人工值分开存**(`smart-bid.sql:340-358`、`handler/bidanalysisv3/api.go:1108-1121`):每个值有两个维度,来源分为 AI / 人工,状态分为生效 / 建议 / 历史。人工改值不覆盖 AI 原值,改完要求补出处。
→ 正好满足红线 1「全程留痕」。
4. **每次解析是一次独立运行**:运行、阶段、子任务、控制信号分四张表存。
- 任务开始时比对运行编号,过时的结果直接丢弃(`handler/bidanalysisv3/pipeline.go:56-73`)。
- 只有整轮成功才切换成当前结果,晚到的结果会被拒绝(`repo/bidanalysisv3/repository.go:372-407`)。
- 续跑前先清掉该阶段的残留数据(`pipeline.go:103`),重跑某一阶段时按运行清理下游数据(`repo/bidanalysisv3/recovery.go:240-260`)。
5. **模型调用流水账**(`smart-bid.sql:396-415`):记录功能、模型、请求和响应的哈希、token 数、耗时、状态,可以作为额度计量的底账。
### 1.3 需要避坑
1. **鉴权**
- 登录令牌的签名密钥写死在源码里(`middleware/jwt.go:13`),知道这个值就能伪造任何用户的令牌。
- cookie 没开 secure,有效期 30 天,没有吊销机制。
- 只要设了 `LOCAL_DEV=true`,就直接以超级管理员身份放行(`middleware/auth.go:21-30`),配错一次就是后门。
- 开放接口只靠一个全局令牌,验证通过后同样拿到超级管理员身份(`auth.go:111-160`)。
2. **租户隔离靠每个接口手写条件**
- 查询项目时用户编号传 0 就不过滤(`repo/bidanalysisv3/repository.go:179-184`),每个接口都要自己记得传。
- 表里有公司字段,但不参与隔离;没有统一的查询过滤,也没有数据库行级策略。
- 管理员权限是在各个接口里自己调用检查函数(`handler/tenderintel/admin_notices.go:85`)。漏写一处就会越权。
→ 我们要在数据访问层统一强制按租户过滤,不依赖每个接口自觉。
3. **大模型 API Key 明文存库**:建表注释里直接写着「API Key(明文)」(`smart-bid.sql:1722`),只在接口返回时打码。
4. **自研 Redis 队列会丢任务**
- 先把任务从队列里取出来,下一步才标记「处理中」(`repo/taskqueue/worker.go:105-131`),进程在这两步之间崩溃,任务就丢了。
- 回收卡死任务的扫描器每 30 秒把所有任务键扫一遍(`worker.go:338`)。
- 服务没有优雅退出,发版时只能等扫描器去回收任务。
5. **暂停和取消信号靠轮询数据库**:每个阶段开一个协程,每 300ms 查一次(`pipeline.go:323`),并发项目一多,数据库压力就上来了。
6. **代码组织**
- 接口层文件过大:`api.go` 1542 行,前端 `panels.tsx` 3201 行。
- 后端有两套响应格式混用。
- 前端状态全靠 useState 加定时轮询;PDF 预览装了两套库。
7. **工程基础薄弱**:表结构只有一份导出的 SQL,没有迁移工具;后端没有一个测试文件;前端只测了纯函数。
## 2. 提示词工程
### 2.1 现状
- 共约 35 条提示词:招标解析 11 条,标书生成约 8 条外加一组共享写作规范,审核和素材 OCR 约 9 条,情报站 3 条。
- 全部写死在 Go 常量或字符串模板里,按业务模块分文件存放。送给模型的内容统一是「大写标签 + JSON」格式。
- `docs/llm-prompts/` 是从代码里人工抄出来的汇编,和代码基本一致。
- 没有提示词版本号,没有评测集。固定字段目录(约 62 条)属于种子数据,没有开源。
### 2.2 值得参考
1. **证据编号白名单,由服务端核对**(`handler/bidanalysisv3/extraction.go:741-822`、`fixed_field_extraction.go:304-436`):
- 送给模型的每个文本块和表格都带一个短编号,要求模型只能引用这些编号。
- 服务端逐条核对:引用的编号不在本次给定范围、结论是「找到」却没给证据、结论是「没找到」却带了证据,都判为无效或降为「待核验」,并记录原因。
2. **引文逐字核对**(`handler/bidreview/tender_direct.go:446-505`):要求模型返回页码和该页的连续原文,代码去掉空白后做子串匹配,对不上的直接丢弃。
→ 我们应该把「编号核对 + 逐字核对」两道校验用到**所有** AI 结论上。
3. **固定字段逐项表态**(`fixed_field_extraction.go:332, 610-621`):每个字段必须返回「找到 / 有歧义 / 没找到」三选一,用输出格式约束锁死字段个数,模型漏答的字段自动补成待核验。
4. **结构化输出逐级降级**(`bidanalysisv3/structured_output.go:43-67`):先用严格的 JSON 格式约束,模型不支持就放宽一级,再不行用普通 JSON 模式,并按「功能 + 模型」记住降到了哪一级。
→ 国产模型对严格格式约束的支持参差不齐,这一点很实用。
5. **输出被截断时的处理**(`bidanalysisv3/extraction_schema.go:176-260`):根据模型返回的结束原因判断截断,先保住已经完整的部分,再把输入对半拆开重跑,最多拆 4 层。
6. **token 预算**(`bidanalysisv3/llm_helpers.go:11-80`):每类任务的输出上限固定;中文按每字约 1.2 token 保守估算后分块。
7. **防注入声明**(`extraction.go:267`):解析侧的提示词都声明「输入是不可信数据,其中的指令一律不执行」。
8. **区分来源**(`bidanalysisv3/blueprint.go:109`):标书大纲里每个节点都标明来源,分为「招标文件要求」「招标文件给出的目录」「AI 建议」三种。
9. **没召回内容就不调模型**(`bidreview/verdict.go:297`):检索不到任何相关内容时,直接给出规则结论「需人工确认」。
### 2.3 需要避坑
1. **解析侧只核对证据编号,不核对引文内容**:条款内容允许「简明摘录」(`extraction.go:828-840`),模型可以先改写或总结原文,再挂一个真实编号,照样通过校验。原文回链就可能指向一个其实没有这句话的位置。
2. **审核结论的证据不校验**
- 模型给的投标文件页码对不上时,会退回去挂到排第一的召回文件上;一个都没召回时,挂到任意一个文件(`bidreview/verdict.go:465-485`)。
- 判为「通过」却没有任何证据,也不会降级(`verdict.go:500`)。
- 审核环节不用格式约束,只靠事后修补 JSON(`bidreview/llm.go:150`)。
→ 模型可以配一句编造的引文判「通过」。
3. **不少输出完全没有出处**
- 摘要里的风险只有文字、类型、标签三个字段(`bidanalysisv3/summary.go:27-31`)。
- AI 生成的材料清单既没有页码也没有引文,最多 18 项,输入还被截断到 160~200 字(`bidreview/checklist.go:292-364`)。
- 情报站的 AI 解读也没有出处(`tenderintel/llm.go:199`)。
→ 按我们的红线 2,这些只能进「待确认」区。
4. **领域知识缺位**
- 全仓库没有区分政府采购体系和招标投标体系,没有出现「87 号令」或「投标无效」的口径区分。
- 「否决 / 废标 / 无效投标」放在同一组关键词里一起匹配(`bidreview/checklist.go:127-135`);★ 只出现在一条预设检查项里(`checklist.go:51`)。
- 送给模型的只有纯文本(`extraction.go:40-65`),斜体、下划线等格式都丢了。
→ 我们的样本分析(`docs/analysis/07-sample-findings.md` F1)已经确认,有的实质性条款只靠斜体加下划线标注,丢了格式就会全部漏掉。
5. **写标书时会诱导编造**
- 写作规范要求「上下文没有数据时,改写为承诺做法与保障机制」,并且「禁止降低我方响应程度」(`bidgen/writing_rules.go:229-251`),模型因此倾向于全部正偏离、过度承诺。
- 设了 300 字的最低字数,不够就自动扩写(`bidgen/topup.go:68`);生成温度 0.7。
- 生成后不检查数字和资质;不标注「初稿」;没有禁止生成报价,也没有报价占位符。
→ 这些都违反我们的红线 1、5、6。
6. **没有版本管理和评测**:调用记录(`extraction.go:690`)只存了模型名和输入的哈希,没有提示词版本,而且系统提示词不参与哈希,改了提示词也看不出来。
7. **防注入不统一**
- 情报站把抓来的公告原文直接拼进提示词,没有任何隔离(`tenderintel/llm.go:122, 199`);审核判定的提示词也没有隔离声明。
- 打标时让模型自己把预算换算成「元」,结果没有核对(`tenderintel/llm.go:137`)。
## 3. 大模型路由与业务设计
### 3.1 路由现状
- **按「用户 × 业务模块」路由**(`repo/llm/user_config.go:43-75, 162-245`)
- 每个功能写死对应到四个业务模块之一(解析、生成、审核、素材),任务类型不参与路由。
- 查找顺序:先找这个模块的配置,再找用户的通用配置,都没有就报「未配置」,不会回落到平台配置。
- base_url、API Key、模型名都由用户自己填。
- **平台配置只给后台任务用**,比如情报站(`repo/sysllm/sysllm.go:157-216`):候选模型按顺序排列,失败了换下一个,列表为空时回落到配置文件里写死的 DeepSeek。
- **多模态没有用视觉模型**:扫描件靠 Docling 的 OCR;签章只是审核清单里的一条文本项,让模型读文字来判断。
- **整个系统没有额度和计费**:只留了一个空接口(`bidgen/generation_worker.go:27`)。
### 3.2 路由:值得参考
1. **只对临时错误重试**(`repo/llm/openai_client.go:50-54, 150-187`):只在限流、服务端 5xx 和网络抖动时重试,最多 3 次,间隔 2 秒、8 秒。
2. **错误分类**
- 错误统一归成 8 类,前端只显示友好文案(`repo/llm/errors.go:47-77`)。
- 解析环节还区分「基础设施故障」和「内容质量问题」,前者直接让这一阶段失败(`bidanalysisv3/llm_helpers.go:93-105`)。
→ 正好可以作为我们判断「是否扣点」的依据。
3. **按模型能力分块**:每类任务的输出上限固定,输入按模型的上下文长度自动切块。换成上下文小的模型只是多调几次,不会报错。
4. **记录模型配置快照**(`bidanalysisv3/api.go:109`):每次解析开始时,把当时用的模型配置存进这轮运行的记录里。
5. **并发控制**
- 解析用进程内信号量,默认 8 路(`bidanalysisv3/service.go:59-75`);审核默认 4 路。
- 生成按用户在 Redis 里占槽位(`bidgen/generation_worker.go:404-411`)。
### 3.3 路由:需要避坑
1. **用户可以随意填模型地址**:招标文件会被发到任意地址,没有地址白名单,还允许关闭证书校验(`openai_client.go:29`)。
→ 和我们「数据不出境」的约束(T1)根本冲突。
2. **配置连通性测试的设计有风险**:如果提交上来的 Key 是打码后的值,会先从库里还原真实 Key,再发往这次新填的 base_url(`handler/llmconfig/test_connection.go:51-76`)。目前只能用到本人自己的 Key,但这种做法本身危险。
3. **调用账不全**:只有解析环节记了完整的调用账,审核只记了模型和耗时,生成不记。全系统没有提示词版本号;供应商一律记成 `"user"`(`repo/llm/llm.go:136`),审计时分不清是谁家的模型。
4. **用户级调用失败后不会换模型**:只有平台后台任务才有候选列表可以降级。
### 3.4 业务:现状
- **解析流水线实际分 6 个阶段**(`repo/bidanalysisv3/repository.go:40-47`):预处理 → 文档解析 → 摘要 → 章节识别 → 事实抽取(含固定字段、动态条款、证据、解读)→ 归并。标书蓝图不在这条流水线里,单独生成。
- **解析 → 生成**:把蓝图转成标书目录,再附一份字段快照(`bidgen/parse_handler.go:162`)。
- **解析 → 审核**:把解析结果冻结成一份 JSON 快照,再从快照重建审核清单(`bidreview/snapshot.go:388-412`)。没有关联解析项目时,审核侧会自己重新抽取一遍招标依据(`bidreview/tender_direct.go:23`)。
- **结论**:三个模块之间靠复制数据衔接,没有一个共享的「要求项」实体,审核结果也连不回生成的章节。
### 3.5 业务:值得参考
1. **审核项两侧都存证据**:每条审核结论同时存招标文件和投标文件两侧的证据(页码、引文、坐标)。
2. **人工确认留痕**:确认时记录确认人和确认时间,另有操作日志(`bidreview/handlers_items.go:95-118`)。
3. **审核清单项有 4 种来源**:通用预设、解析依据、企业规则库、暗标版式检查。
### 3.6 业务:需要避坑
1. **各模块各存一套数据**
- 原文块和证据在解析、审核两个模块里各存一套。
- 复制成审核清单时只保留了页码和引文,丢了坐标(`bidreview/checklist.go:141-149`)。
- 已经不用的旧审核表还留在库里。
2. **重跑会丢人工确认**
- 重建审核清单时会删掉「通用预设」和「解析依据」两类检查项(`checklist.go:75`),上面的人工确认跟着消失。
- 操作日志写失败时错误被直接忽略,也不和状态更新放在同一个事务里;确认时备注不是必填。
3. **「已处理」状态按列表顺序号存**(`bidanalysisv3/api.go:1137-1183`):风险的「已处理」状态按它在列表里的位置记录,摘要一重新生成,状态就对不上了。
4. **高风险条款截断不提示**:识别靠关键词加重要程度,超过 24 条直接截掉,界面不提示(`checklist.go:127-135`)。
5. **报价、签章、密封全靠模型读文本判断**:报价没有确定性的金额比对,签章没有任何视觉层面的检查。
6. **「竞争力预估得分」是机械折算**(`bidreview/excel.go:76`、`bidreview/scoring.go`):由「通过 / 警告」的数量直接折算成分数,很容易被当成确定结论。
→ 和我们红线 5「数据不足宁可不展示」相反。
7. **审核快照可能把旧版解析的条款也读进来(推测)**:读取条款时只按项目过滤,不按本次解析过滤(`bidreview/snapshot.go:231-237`,这一点代码里已核实)。如果重新解析时不清理旧条款,快照就会把多次解析的条款都读进来;这一点没有实际运行验证。
## 4. 对标博士的落地建议
以下建议留给「技术架构设计」阶段讨论,定下来的写入 `docs/decisions.md` 或 ADR。
1. **把「项目检查清单」做成真正的实体**
- 每条要求项有稳定编号;原文块、证据、AI 结论、人工确认都挂在要求项上。
- 解读、写标、查标三个阶段共用这一份,不复制快照。
- 重新解析时,按要求项对齐新旧结果:只更新 AI 给出的部分,人工确认和备注保留。新旧对不上的要求项提示用户处理,不要静默删除。
2. **所有 AI 结论都过两道证据校验**
- 第一道:引用的原文块编号必须在本次输入范围内。
- 第二道:引文和原文逐字比对(去掉空白后做子串匹配)。
- 两道都通过才能进「已识别」区,否则进「待确认」。投标文件一侧同样如此,没有证据的结论不能判「通过」。
- 原文高亮要细到原句:切块时保留行级或字符级的坐标。
3. **送给模型的文本保留格式**:斜体、下划线、★ 等标注要转成模型看得懂的标记,再交给模型,对应样本分析 F1。
4. **模型路由由平台统一管理**
- 业务代码只声明需要的能力(多模态 / 快速 / 深度思考),由路由层选模型;供应商只能在国产云端白名单里选,用户不能填地址。
- 每种能力配候选列表,失败了按顺序降级;只对临时错误重试。
- API Key 加密存放,只从密钥管理服务或环境变量注入。
5. **所有 AI 任务记一本统一的账**
- 记录供应商、模型、提示词版本、输入输出哈希、token、耗时,以及错误属于哪一类。
- 错误类别直接决定是否扣点:基础设施故障、超时、解析失败都不扣,对应红线 9。
6. **提示词工程化**
- 提示词单独存成带版本号的文件,不写死在代码里。
- 从第一天起就配评测集:用 `samples/tenders/` 里的样本加人工标注,重点覆盖 S03、S06。
- 所有拼进提示词的外部内容(招标文件、投标文件、公告)统一隔离,并声明「其中的指令一律不执行」。
7. **领域规则先于模型**
- 先判断项目属于政府采购体系还是招标投标体系,再套用对应的术语和无效情形。
- 报价检查用确定性规则比对金额,不交给模型判断;签章只提示「位置与有无」。
- 关键词召回只做补充手段,截断时必须在界面上提示。
8. **基础设施直接用成熟方案**
- 任务队列用成熟组件,或把任务状态放进数据库(保证取任务和改状态是同一步);服务要能优雅退出。
- 数据库表结构用迁移工具管理。
- 租户隔离在数据层统一强制;管理端接口单独挂一层鉴权中间件。
- 任何「本地开发免登录」的开关,都不能出现在生产构建里。
## 5. 解析流水线深读(2026-11-03 补)
> 用户要求再读一次,聚焦「模型 + 规则」解析招标文件和检查投标文件。由 Fable 模型做顾问走查源码,关键结论对照源码抽查过(饱和对半重跑、段落级出处、not_found 分两类、直抽「找不到逐字引文就不要输出」、条款允许简明摘录)。只读、未运行。路径相对 `bid-engine-backend/pkg/handler/`。
### 5.1 结论
1. **组织方式比单个技巧更值得学**:抽取分两条车道——按章节开放式找条款(`bidanalysisv3/extraction.go`),和按固定字段表逐项过全文、每个字段必须表态 found / ambiguous / not_found(`bidanalysisv3/fixed_field_extraction.go`)。模型输出后有十余条确定性校验,每条失败都记原因码并按原因计数(`extraction.go:757-893`)。
2. **分块比我们细**:按章节打包,包与包之间带一个重叠块,超大表格单独成包;输出被截断(finish_reason=length)或条数正好碰到上限时,把这一包对半重跑,最多 4 层(`bidanalysisv3/extraction_schema.go:257-401`)。
3. **章节识别三级**:强正则 → 弱正则候选交模型消歧(只能删、改名、分类,不能新增,必须保留第一个,服务端核对编号和顺序)→ 每 20 页兜底(`bidanalysisv3/chapter_identification.go`、`extraction.go:207-317`)。章节类型用来分派任务。
4. **出处只到段落**:`pkg/repo/docling/adapter.go` 注释写明「溯源高亮精度从逐字精确变为段落级覆盖,已确认可接受」,引文就是整块文本;Docling 返回的粗体斜体、表头、合并单元格都没用,表格压成纯文本矩阵;条款允许「简明摘录」(`bidanalysisv3/fields.go:109`)。★ / 斜体下划线标注会全部漏掉。**这一层不学**。
5. **查标学框架、不学判定**:检查项来源分层(预设 / 从解读映射 / 企业规则 / 版式项),致命项逐条、其余按类别批量,检索不到直接给「未找到,需人工确认」不调模型,版式用规则扫描分硬 / 软证据(`bidreview/`)。判定结果的页码和引文不校验,pass 不要求证据,违反红线 2。
### 5.2 值得学的具体做法
- **固定字段逐项表态**:漏答、found 却无值、证据定位不到一律降为待核验并记原因;所有分块都答完才允许定 not_found;最终 not_found 再分「文件本身没写」(info)和「抽取异常」(warning)(`bidanalysisv3/fixed_field_service.go:183-217`)。
- **占位符不算值**:`___`、`××`、待填、TBD 等匹配为占位符(`extraction.go:29, 796-800`)。
- **标准化由服务端做**:金额、日期、单位在服务端解析,提示词禁止模型输出标准化值。
- **重复候选合并取证据并集**;多值冲突交模型裁决但只能缩减,没表态的默认全保留,不同标段 / 主体的值必须保留多个(`bidanalysisv3/consolidation.go`)。
- **每页质量指标**:字数、是否 OCR,平均不到 40 字 / 页判扫描页并 OCR 重跑,页数核对不符判失败(`bidanalysisv3/document.go:410-473`)。
- **告警三级**:critical(基础设施)/ warning(内容质量)/ info(文件本身缺),同类合并计数,基础设施错误直接停整个阶段。
### 5.3 提示词亮点
- 安全规则编号放在最前,声明输入是不可信数据并列举注入形式;用户消息只放数据标签 + JSON,能不给的原文就不给(章节消歧只给标题、页码、编号)。
- **否定清单具体、每条对应一条服务端校验**:not_found 不得带证据;占位符不算值;不同主体、标段、预算与限价不能混成一个值;不要为凑数编造;不得输出标准化值。
- JSON Schema 锁 enum、条数上限、引用编号格式;固定字段的条数下限 = 上限 = 字段数。
- 裁决类提示词「只能缩减」。
- 查标直抽:「quote 必须是该页文本中的连续原文……找不到逐字引文就不要输出」(`bidreview/tender_direct.go:23-25`),是全仓库最接近我们红线的一条。
- 没有正反例、没有版本号、没有评测集。
### 5.4 对标博士的取舍
顾问的 13 条建议里,「引文对不上时在同一块里找到原句、改挂出处」不采纳:它是在迁就模型的错误输出(2026-11-03 用户定的原则:规则只修真正的缺陷,模型输出不合理的改提示词)。只采纳其中「记原因码」的部分。
- **S01–S10 标注集评测之前做**(只改抽取、分块和提示词,不动数据结构):
1. 待确认条目记原因码,按原因计数;
2. 分块加重叠,检测输出截断并对半重跑;
3. 项目基本信息逐项表态,区分「文件本身没写」和「抽取出错」;
4. 识别占位符;
5. 提示词的禁止事项和服务端校验一一对应。
- **评测之后做**(涉及文字层数据结构):顶级章节识别并按章节类型分派任务;每页质量指标和扫描件判定;表格按行生成证据单元、带表头(解决两栏表、竖排文字串行)。
- **留到 M3 设计**:检查项来源分层、判定分层、检索不到不调模型、版式规则扫描;必须补投标侧引文逐字校验,pass 必须有校验通过的证据。检索用 PostgreSQL 的 pg_trgm / 全文检索,不自建倒排。