- 新增 docs/architecture/extract-status.md:指标两种读法(报告值 / 只按人工标注)、 已确认的进展、泛化风险(Fable 过拟合评估)、P0–P3 遗留问题、找样本和建留出集的流程 - extract-eval.md、AGENTS.md、架构 SK13、evals/README.md 改正达标说法并指向新文档 - 读标召回率优化暂停,后续专项优化从 extract-status.md 接着做 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
74 KiB
技术架构设计 v0.3
依据:PRD v0.9.1 第 7 章(非功能需求)、
docs/analysis/(功能需求、领域模型、样本结论)、docs/decisions.md(T1 及各阶段决策)、参考项目分析ref-bid-engine-analysis.md。 状态:v0.3。v0.2 的选型已全部确认;v0.3 新增 §3.14:MVP 的表结构先按多人、多登录方式建,给以后的团队版、企业版、飞书 / 企业微信登录留结构(AR11、AR12,已确认)。物理数据模型见data-model.md(DM1–DM7,已确认);接口契约见api-design.md+openapi.yaml(API1–API5,已确认)。
- 已确认(2026-11-03):T2–T5;AR1、AR3、AR5–AR9 按建议;AR2 改为 Vue 3 + Vite。
- 已确认:AR4 修订后的建议(引入 Valkey,只存可以丢的数据,§6.1);AR10 不引入带样式的 UI 框架(§4.3)。至此选型全部确认。
- 确认后写入
docs/decisions.md;构建、运行、测试命令等工程骨架搭好后回填到 AGENTS.md。 范围:MVP(M2 + M3 + M6 的 P0 部分),同时给 V1 的标书生成、资产库,P2 的私有化、信创、桌面客户端留出扩展点。
0. 结论速览
| 层 | 选型 | 状态 |
|---|---|---|
| 后端语言 | Python 单语言:业务接口和后台任务是同一套代码、两种进程 | T2 已确认 |
| 后端框架 | FastAPI + Pydantic v2 + SQLAlchemy 2 + Alembic;包管理用 uv | 建议 |
| 架构形态 | 模块化单体:按业务模块分包,按负载拆成几个独立部署的进程;不做微服务 | AR1 已确认 |
| 前端 | Vue 3 + TypeScript + Vite 单页应用,pdf.js | AR2 已确认 |
| UI 组件 | 不引入带样式的 UI 框架:视觉自己写(tokens.css + Vue 单文件组件),复杂交互用无样式组件库 Reka UI 打底 | AR10 已确认 |
| 数据库 | PostgreSQL(16 以上) | AR3 已确认 |
| 任务队列 | 用 PostgreSQL 做队列(Procrastinate):任务状态以数据库为准 | AR4 已确认 |
| 缓存、限流、消息推送 | Valkey(Redis 的 BSD 许可分支),只存丢了也没关系的数据 | AR4 已确认 |
| 文件存储 | S3 协议的对象存储(SaaS 用云厂商托管服务)+ 本地磁盘驱动(单机私有化);不用 MinIO | AR5 已确认 |
| 文档转换 | Gotenberg(封装了 LibreOffice 和 Chromium):Word 转 PDF、报告 HTML 转 PDF | 建议 |
| OCR | PaddleOCR 独立服务;多模态大模型作补充 | 建议,需验证 |
| 大模型 | 自建模型路由层,按能力声明路由,只接白名单内的国产模型(T1) | 已定方向 |
| 部署 | 同一批镜像,两种编排:Docker Compose 和 Helm(K8s)。SaaS 内测期用 Compose,商用后切到托管 K8s | T4 已确认 |
| 云厂商 | 暂不定:只依赖 PostgreSQL、S3、OpenAI 兼容接口三类标准接口 | T5 已确认 |
| 桌面客户端 | 连服务端的客户端外壳,界面和网页版同一套;倾向 Electron,到时再定 | T3、AR6 已确认 |
1. 设计输入
1.1 业务负载特点
这些特点决定了架构不需要很重:
- 请求量低、单个任务耗时长:B 端投标团队,并发用户少。重活是解析和检查:一份 300 页的文件目标 10 分钟内完成(FR-M2-02),中间要调用几十到上百次大模型。
- 文件大、页数多:单份文件几十 MB、几百页,前端要能流畅翻页并高亮原文。
- 强审计:额度流水只追加,人工确认全程留痕,检查批次要能复现当时的判断依据(04 §4)。
- 两种部署形态共用一套代码:SaaS 多租户;私有化部署一套就是一个租户(04 §4.5),信创环境作为可选项(PRD 7)。
- 数据敏感:标书里有报价策略、资质、业绩,日志和监控里也不能出现文档内容。
1.2 来自产品红线的硬约束
| 红线 | 对架构的要求 | 落在哪一节 |
|---|---|---|
| 可复核(2) | 出处精确到原句和坐标;AI 结论必须通过证据校验,否则进待确认区 | §3.5、§3.6 |
| 全程留痕(1) | 审计事件和业务修改在同一个事务里写入;AI 产出记录模型和提示词版本 | §3.6、§3.10 |
| 额度即货币(9) | 预扣和建任务在同一个事务里;按错误类别决定是否退回;流水只追加 | §3.9 |
| 报价不由系统生成(6) | 报价检查只用确定性规则,不交给模型 | §3.7 |
| 数据敏感(10) | 按租户强制隔离,加密存储,可以彻底删除,日志脱敏 | §3.3、§3.11 |
| T1 国产模型 | 业务代码只声明能力,供应商白名单由运维配置,用户不能填模型地址 | §3.6 |
1.3 从参考项目吸取的教训
详见 ref-bid-engine-analysis.md,下表是对照结果:
| 标擎的问题 | 本项目的做法 |
|---|---|
| 解析、审核、生成各存一份数据,靠复制快照衔接 | 一份项目检查清单贯穿全流程,要求项编号稳定(§3.8) |
| 只核对证据编号,不核对引文内容;审核证据不校验 | 两道证据校验,适用于所有 AI 结论(§3.6) |
| 用户可以填任意模型地址;API Key 明文存库 | 供应商白名单由运维配置;密钥加密存储(§3.6、§3.11) |
| 自研 Redis 队列,取出任务和标记状态分两步,会丢任务 | 任务状态以数据库为准,入队和建任务在同一个事务里(§3.4) |
| 租户隔离靠每个接口手写条件,传 0 就不过滤 | 数据访问层强制带租户上下文,再加数据库行级策略兜底(§3.3) |
| 登录密钥写死在源码里,开发开关能直接拿到超级管理员身份 | 密钥只从环境注入;调试开关在生产环境下拒绝启动(§3.11) |
| 重跑后人工确认丢失;已处理状态按列表顺序号存 | 人工确认挂在稳定编号上,重跑时按编号对齐(§3.8) |
| 提示词写死在代码里,没有版本号,没有评测集 | 提示词单独存成带版本号的文件,有评测集,CI 跑回归(§3.6) |
| 接口文件 1500 行、组件 3200 行;没有测试 | 分层约定、文件规模上限、测试是合并的前提(§8) |
2. 总体架构
2.1 为什么不做微服务
用户要求重点考虑微服务。结论是 MVP 不拆微服务,做模块化单体,理由有三点:
- 团队小:微服务带来的服务间调用、分布式事务、链路排查、版本兼容,都是纯成本。
- 私有化交付:每多一个服务,客户现场就多一份部署、升级、排障的负担。政企客户的运维往往只会 Docker。
- 核心流程要求强一致:「预扣额度 + 建任务 + 入队」、「改清单 + 写审计」都必须在一个数据库事务里完成,拆开后就得上分布式事务。
模块化单体不等于一个进程:代码按业务模块划分边界(§3.2),按负载特性拆成几个进程独立部署和扩容(§2.2)。以后真要拆服务时,模块边界就是拆分线。
2.2 进程与部署单元
flowchart LR
subgraph client[客户端]
B[浏览器]
D[桌面客户端外壳<br/>以后]
end
subgraph edge[入口]
N[web<br/>nginx:静态页面 + 反向代理]
end
subgraph app[应用(同一个 Python 镜像)]
A[api<br/>FastAPI]
W[worker<br/>解析 / 检查 / 报告]
end
subgraph aux[辅助服务]
G[converter<br/>Gotenberg]
O[ocr<br/>PaddleOCR]
end
subgraph data[数据]
P[(PostgreSQL<br/>业务数据 + 任务队列)]
S[(对象存储 / 本地磁盘)]
V[(Valkey<br/>限流 / 推送 / 缓存)]
end
L[[大模型<br/>国产云端 API / 客户自部署]]
B --> N
D --> N
N --> A
A --> P
A --> S
A --> V
W --> P
W --> S
W --> V
W --> G
W --> O
W --> L
A -. SSE 进度 .-> N
| 单元 | 镜像 | 说明 | 扩容方式 |
|---|---|---|---|
| web | bid-doctor-web |
nginx 托管前端静态文件,并把 /api 反向代理到 api |
几乎不需要扩 |
| api | bid-doctor-app |
接口服务,只做请求处理和短事务,不跑重活 | 按请求量扩 |
| worker | bid-doctor-app(同一个镜像,启动命令不同) |
跑解析、检查、报告等后台任务;可以按队列拆成 worker-parse 和 worker-light |
按排队长度扩 |
| converter | 官方 Gotenberg 镜像 + 字体层 | Word 转 PDF、HTML 转 PDF;不开网络出口 | 按转换量扩 |
| ocr | bid-doctor-ocr |
PaddleOCR 推理服务,有 CPU 版和 GPU 版两种镜像 | 按扫描件量扩 |
| PostgreSQL | 托管服务或官方镜像 | 业务数据、会话、任务队列都在这里,是唯一的权威数据源 | 纵向扩,加只读副本 |
| Valkey | 托管 Redis 兼容服务或官方镜像 | 限流计数、验证码、进度推送、模型调用并发控制、缓存;不持久化、不备份,清空后系统照常运行 | 几乎不需要扩 |
| 对象存储 | 托管 OSS / 本地磁盘 | 原始文件、规范化 PDF、文字层、报告 | — |
单机私有化最少只要 7 个容器:web、api、worker、converter、ocr、postgres、valkey。文件存在本地磁盘上,不需要对象存储,也不需要 K8s。
3. 后端
3.1 语言与基础库(T2)
- Python 3.12 以上,用 uv 管理依赖和锁文件,用 ruff 做代码检查和格式化,用 pyright 的严格模式做类型检查,测试用 pytest。
- FastAPI:接口层。用 Pydantic v2 定义请求和响应模型,自动生成 OpenAPI 文档,前端再据此生成类型和客户端(§4.2)。
- SQLAlchemy 2 + Alembic:数据库访问层和版本化的迁移脚本;不用自动建表。
- httpx:调用大模型和内部服务,统一设置超时和重试策略。
- 选 Python 的代价是私有化交付会暴露源码。缓解办法见 AR7:用 Nuitka 把核心包编译成二进制,再加上授权文件校验。
3.2 模块划分
按业务划分模块,每个模块自己管自己的表,模块之间只能通过对方公开的服务接口调用,不能直接读写别的模块的表。
| 模块 | 职责 | 对应需求 |
|---|---|---|
identity |
人、登录方式、企业主体(租户)、成员身份、会话、短信验证码、登录风控、权限点(§3.14) | FR-M6-04 |
billing |
套餐、订阅、额度账户、额度流水 | FR-M6-03、05 |
project |
项目、分包、状态流、就绪标记 | FR-M6-01、02 |
document |
文件上传、格式识别、转换、文字层、OCR、出处定位 | FR-M2-01、06 |
checklist |
项目检查清单、清单项、待确认区、版本、补遗合并 | FR-M2-03~10 |
check |
检查批次、问题、人工确认、复查沿用 | FR-M3-01~08 |
rules |
确定性规则:报价、日期、法律体系、形式要件 | FR-M3-04、06 |
ai |
模型路由、提示词注册表、调用流水账、证据校验 | T1 |
report |
检查报告的生成与归档 | FR-M3-09 |
qualification |
最小资质库 | FR-M6-08 |
audit |
审计事件 | FR-M6-07 |
ops |
运营后台接口、误报 / 漏报反馈 | FR-M6-09 |
notification |
站内通知(解析 / 检查完成、额度提醒、开通) | FR-M2-02、FR-M6-03 |
每个模块内部分三层:
api:路由、请求校验、调用服务,一个接口通常不超过 30 行。service:业务规则和事务边界。repo:数据访问,必须带租户上下文。
模块之间的依赖方向用 import-linter 在 CI 里检查。
3.3 租户隔离
用三层防护:
- 数据访问层强制带租户:所有仓储方法的第一个参数都是
TenantContext,没有「传 0 就查全部」这种写法。跨租户查询只有运营后台能用,而且必须写审计事件。 - PostgreSQL 行级安全策略(RLS)兜底:每个事务开始时设置当前租户,业务表的策略按
tenant_id过滤。即使代码漏了条件,数据库也不会把别的租户的数据返回出去。 - 测试:每个接口都跑一条「用 A 租户的令牌访问 B 租户资源」的用例,返回必须是 404。
私有化部署只有一个租户,代码不需要改。如果客户的数据库是信创数据库、不支持行级安全策略,可以关掉第 2 层(AR3)。
3.4 异步任务与流水线
原则:任务状态以 PostgreSQL 为准,队列只负责派发。
- 建任务和入队在同一个事务里完成:「预扣额度 → 建解析任务 → 入队」在一个数据库事务内提交。所以不会出现「扣了点但任务没进队列」或者「任务跑了但没扣点」。这是选用 PostgreSQL 做队列的主要理由(AR4)。
- 队列库用 Procrastinate:基于 PostgreSQL 的成熟开源库,支持锁、重试、定时任务和队列优先级;不自己写队列。它靠 LISTEN/NOTIFY 及时发现新任务,所以 worker 要直连数据库,或者经过会话模式的连接池,不能走事务模式的连接池。
- 流水线分阶段、可续跑:解析任务分为格式识别 → 转换 → 文字层 → OCR 补全 → 结构识别 → 抽取 → 证据校验 → 清单合并。每个阶段完成后把检查点写入数据库,失败后从失败的那个阶段续跑。每个阶段都要幂等:开始前先清掉这个阶段的残留产物。
- 过时结果直接丢弃:任务带运行编号,写结果前比对;发现已经有更新的一次运行,就直接丢弃本次结果。
- 取消:取消标记写在数据库里(权威),同时通过 Valkey 发一条消息,让 worker 立即知道。worker 在阶段之间、每批模型调用之间检查这个标记,不需要每 300ms 轮询数据库。即使消息丢了,worker 到下一个检查点也会读到数据库里的标记。
- 优雅退出:worker 收到 SIGTERM 后不再领取新任务,等手头这个阶段跑完再退出;超时没跑完的任务由队列的租约机制自动交还给别的 worker。
- 进度推送:worker 把进度写进数据库,再通过 Valkey 的发布 / 订阅通知;api 用 SSE 推给前端。多个 api 实例都能收到这条通知。前端断线后用轮询兜底,轮询读的是数据库里的进度。
- 并发:worker 内部用 asyncio 并发调用大模型,每个供应商、每个租户都有并发上限(§3.6)。
3.5 文档处理流水线
这是产品的核心能力,也是技术风险最大的地方,§10 列出了需要先验证的事项。
上传 → 格式识别(看文件头,不看扩展名)
→ 预处理(旧版 DOC / WPS 先转 DOCX;修正被错标成 SYMBOL 字符集的中文字体,见技术验证 S1)
→ 转换(DOCX → PDF,用 Gotenberg + LibreOffice + 按宋体行高调整过的开源字体包)
→ 转换质量检查(大段 OpenSymbol 字体、抽出的文字大量重复同一个字 → 判为转换异常,不扣点)
→ 规范化 PDF(只生成一次,存哈希,以后不再重新生成;它是出处页码的唯一依据)
→ 文字层(逐字的坐标、字体、斜体、下划线;按行、句、块切分,给出稳定编号)
→ 逐页检查覆盖:文字太少的页交给 OCR(PaddleOCR),OCR 结果并入文字层
→ 结构识别(章节、表格、分包、「标注规则说明」)
→ 交给 ai 模块抽取 → 三道校验(编号、原文、依据)→ 标注类条款规则兜底 → 并入检查清单
关键设计:
- 文字层是一份带编号的数据:每一句都有编号(文档 + 页 + 序号),并记录它在原文中的字符范围和坐标框。模型只能引用句子编号;高亮时按坐标框画,直接满足「原句高亮」(FR-M2-06)。
- 格式必须保留:PDF 里的斜体靠字体名或字形倾斜判断,下划线靠文字下方的线条判断,加粗靠字体粗细判断。转成模型的输入时,这些格式变成文本标记(例如
<u><i>…</i></u>),对应样本结论 F1。DOCX 原文件里的格式信息可以用来交叉校验。 - PDF 库选择:用 pypdfium2(Apache / BSD)和 pdfplumber(MIT)。不用 PyMuPDF,它是 AGPL 许可,或者要买商业授权。
- 布局分析:S2 已验证,不用 Docling 做主解析器:它从 DOCX 解析能保留格式但没有坐标,从 PDF 解析有坐标但丢了格式,CPU 上 199 页要 96 秒。文字层自己从规范化 PDF 提取(格式 + 坐标都有)。Docling 的表格结构识别可以作为表格切句的候选。MinerU 历史上用 AGPL 许可,选用前要核实它当前的许可证。
- DOCX 交叉校验:读 DOCX 文字时要同时读
<w:t>和<w:sym>(WPS 文档转换后有一部分正文是符号字符),并处理样式继承(S1、S2)。 - 字体与页码:Word 转 PDF 的分页取决于字体。招标文件常用宋体、仿宋、黑体,这些是商业字体,不能随镜像分发。替代字体会导致分页和用户在 Word 里看到的不同。处理办法:
- 出处页码统一按规范化 PDF 计,界面上说明「页码以系统转换后的 PDF 为准」。平台导出的 DOCX(例如苏采云,由 ONLYOFFICE 生成)本来就没有记录 Word 里的页数,用户看到的分页取决于他们自己的软件,所以这条是必需的。
- S1 结论:分页偏差的主因是行高。用 Noto CJK 克隆出行高 1.0em 的「BD Song / BD Hei」(OFL,改名后仍按 OFL 分发),加 fontconfig 替换规则,有真值的 5 个数据点偏差都在 ±3% 内;默认配置偏差 +17%~+37%。
- 私有化时,客户如果有正版字体,可以挂载进去。
- 沙箱:转换服务不开网络出口,不运行宏,限制 CPU、内存和处理时长;上传入口要拦截 zip 炸弹和超大文件。
3.6 AI 层(模型路由、提示词、证据校验)
模型路由
- 业务代码只声明能力和任务,例如
ai.run(task="extract.qualification", capability=Capability.REASONING, ...)。路由表决定实际用哪个供应商、哪个模型。 - 能力分类沿用 T1:
vision(多模态)、fast(快速)、reasoning(深度思考);以后加embedding。 - 供应商都走 OpenAI 兼容协议:DeepSeek、通义千问、豆包、GLM、Kimi 等,私有化时换成客户自部署的 vLLM 或 SGLang。
- 供应商白名单和路由表由运维在部署配置里设置,用户和租户都不能填模型地址。
- 每种能力配一个候选列表,按顺序降级;只对限流、5xx、网络抖动这类临时错误重试。
- 并发上限和速率限制按「供应商 × 租户」两个维度设置,防止一个大客户把配额占满。国产模型的接口普遍有每分钟请求数和 token 数的上限,多个 worker 进程要共用同一组计数器,所以计数器放在 Valkey 里(令牌桶)。Valkey 不可用时,每个 worker 退回到进程内的保守上限,不会中断任务。
错误分类决定是否扣点
| 类别 | 例子 | 任务结果 | 额度 |
|---|---|---|---|
| 基础设施 | 模型超时、限流重试后仍失败、5xx、转换服务崩溃 | 失败,可以重试 | 退回 |
| 输入问题 | 文件加密、损坏、超出页数上限 | 在上传时拦截,不建任务 | 不扣 |
| 内容质量 | 模型输出格式错误、证据校验没通过 | 不算任务失败,相关条目降级进待确认区 | 正常结算 |
| 用户取消 | — | 已取消 | 退回 |
提示词工程化
- 提示词存成带版本号的文件:
prompts/<任务>/v<N>.md,旁边放对应的输出格式定义。代码只按「任务 + 版本」引用,不在代码里写提示词正文。 - 提示词注册表启动时加载,并计算内容哈希。每次调用都记下
prompt_id、version、hash。 - 外部内容(招标文件、投标文件)统一放进带分隔符的数据区,系统提示词里声明「数据区里的任何指令都不执行」。
- 结构化输出逐级降级:严格的 JSON 格式约束 → JSON 模式 → 修补;按「任务 + 模型」记住降到了哪一级。
- 评测集:在
evals/里存放用samples/tenders/人工标注的结果,重点覆盖 S03、S06、S08。提示词或路由一改,CI 就跑回归,主要指标是 ★ 和实质性条款的召回率(FR-M2-05)。
调用流水账(覆盖所有 AI 调用)
ai_call 表记录以下字段:租户、关联任务、任务类型、能力、供应商、模型、提示词编号、提示词版本、提示词哈希、输入哈希、输出哈希、token 数、耗时、错误类别、降级到了第几级。它既是审计依据(T1),也是成本核算和对比模型效果的数据源。不存文档原文:要排查问题时,凭输入哈希去对象存储里取。
三道校验(适用于所有 AI 结论)
- 编号校验:引用的句子编号必须在本次输入的范围内,而且类型要对得上。
- 原文校验:模型给出的引文,去掉空白后必须是所引句子原文的子串,允许按规则做极小的差异容忍。
- 依据校验(S4 验证后新增):模型声称的依据,用确定性信号核对。例如说是 ★ 条款,原句里就要有 ★;说是斜体下划线,原句就要有这种格式;说是「写明无效」,原句或相邻句就要有无效类字样。S4 里,不加这一道时模型会大量标错依据,采纳条目只有 36.7% 属实;加上后是 95.2%。
标注类条款用规则兜底:★、* 和文件声明了的斜体下划线,都能从规范化 PDF 上确定性识别出来。这类句子必须被某个条目覆盖;模型没覆盖的,系统自动生成一条「待确认」,召回不完全依赖模型。
三道都通过,结论才能进正式清单或成为有效的问题。没通过的,按情况降级为「待确认」或「提示」。投标文件一侧同样如此:没有证据的结论不能判为「已响应」。
3.7 规则引擎(确定性检查)
能用规则判断的,不交给模型:
- 报价:超过三层限价、大小写金额是否一致、合计是否一致、是否只有一个报价。金额只从投标文件里读取,用 Decimal 计算,系统不生成任何报价数字。
- 日期:落款日期是否合理,资质在投标截止日是否已经过期。
- 法律体系:先识别项目属于政府采购体系还是招标投标体系,再按体系切换术语(「投标无效」或「否决投标」)和无效情形清单(05 §2)。
- 形式:格式文件是否齐全,固定文字有没有被改动,文件里有没有出现其他项目的名称(这一步可以用模型召回,但判定结论要走证据校验)。
规则写成 Python 代码,每条规则有编号;规则集有版本,检查批次会记下当时用的规则版本(FR-M3-02)。
3.8 检查清单与重跑对齐
- 清单项有稳定编号,出处(Citation)单独建表,清单项和问题都引用它(04 §2)。
- 重新解析时不删清单:新结果先和原有清单项对齐(按类别、所属包、原文句子编号、文本相似度)。
- 对上的:只更新 AI 给出的字段,人工确认、修改和备注都保留。
- 对不上的旧项:标为「本次解析未出现」,交给用户处理,不静默删除。
- 新出现的项:进待确认区或正式清单。
- 人工值和 AI 值分开存:用户修改不覆盖 AI 原值,留着用来做误报分析。
- 清单有版本:确认清单时生成一个不可变快照,检查批次引用的就是这个版本(04 §4.2)。
- 复查沿用:问题按「清单项编号 + 问题类型」匹配(FR-M3-08),不用列表顺序号。
3.9 额度
- 额度流水只追加,余额由流水汇总得出;另存一个缓存余额,和流水在同一个事务里更新,并定期对账。
- 每笔流水都带幂等键(任务编号 + 动作),重复执行也不会重复扣点。
- 任务的三种结局(成功结算、失败退回、取消退回)都由任务状态机统一触发,退不退回按 §3.6 的错误分类表决定。
- 24 小时免扣(决策 P1):每个项目记一个「上次扣点检查时间」,是否免扣由规则判断。
3.10 审计
- 审计事件和业务修改在同一个事务里写入,审计写失败,整个操作就失败;不能像标擎那样忽略写入错误。
- 字段:操作人、租户、动作、对象类型、对象编号、修改前、修改后、时间、请求编号。
- 审计表只追加;可以加哈希链(每条记录带上一条的哈希),让篡改可以被发现,作为可选项。
- 删除项目时,按 G10 只保留不含业务内容的审计记录:「修改前 / 修改后」里的业务字段要能单独抹掉。
3.11 安全
- 会话(AR8):不用长期有效的 JWT,改用服务端会话。
- 令牌是 256 位随机串,只发给客户端一次;数据库里只存它的 SHA-256 哈希,数据库泄露也拿不到可用的令牌。
- 会话表
user_session放在 PostgreSQL 里,字段包括:令牌哈希、用户、租户、设备信息、IP、创建时间、最近活跃时间、过期时间、吊销时间。 - 每个请求按令牌哈希做一次主键查询,耗时不到 1 毫秒。「最近活跃时间」最多每 5 分钟更新一次,避免每个请求都写库。
- 放在 PostgreSQL 而不是 Redis,理由见 §6.1:能持久化、能按用户列出所有会话(用来实现「同一账号异地多设备登录时提示并限制」,FR-M6-04),删除租户时也能级联清理。
- 网页端用 HttpOnly + Secure cookie,桌面端用 Bearer 令牌并存进系统钥匙串。
- 验证码和登录限流:短信验证码、发送频率、登录失败次数都放在 Valkey 里,带过期时间。这些数据丢了,最多是用户重新获取一次验证码。
- 身份来源可插拔:SaaS 用手机号 + 短信验证码;私有化用账号密码,并预留 OIDC、LDAP 接入(P2 的 SSO);飞书、企业微信登录按同一接口扩展(§3.14)。
- 密钥:只从环境变量或密钥管理服务注入,代码和镜像里一律不放。启动时如果检测到默认密钥或弱密钥,在生产环境下拒绝启动。
- 调试开关:「跳过登录」之类的开关只在测试配置里存在;生产环境检测到这类开关会直接退出。
- 加密:
- 传输:全链路 TLS。
- 存储:云盘和 OSS 开服务端加密。文件另做信封加密,每个项目一把数据密钥(资质附件用企业级密钥,DM4),删除时销毁密钥,备份里的数据也就随之不可读,用来落实 G10 的「彻底删除」。
- 模型供应商的 API Key 加密存储。
- 运营后台:独立应用、独立登录,加 IP 白名单和二次验证,所有操作都写审计。
- 日志脱敏:日志和监控里只出现编号和哈希,不出现文档内容、引文、金额;有专门的脱敏中间件,加上测试。
3.12 报告
- 报告用 HTML 模板渲染,再交给 Gotenberg 里的 Chromium 转成 PDF。字体嵌入思源宋体和思源黑体(OFL)。
- 报告生成后不再修改,存哈希;重新导出就生成一份新报告(04 §2)。
- 模板在 CI 里做文案检查:不出现「通过 / 合格 / 保证」这类字样(FR-M3-09,红线 3)。
3.13 文档组件:参考标擎的做法
用户要求在 Office 解析、PDF 解析、标书组装和格式转换这三块参考开源项目。下面是标擎的做法(2026-11-03 阅读源码,路径相对于 opensource-ref/bid-engine/)和我们的取舍。只借鉴做法,不复制代码(许可证限制,见 ref-bid-engine-analysis.md)。
Office / Word 文件
标擎的做法
- 转换服务单独一个容器(
bid-engine-backend/Dockerfile.converter、cmd/converter/),主后端镜像里不装 LibreOffice。 - 转换服务的工程细节做得扎实:
- 每次转换用独立的 LibreOffice 配置目录(
-env:UserInstallation),避免并发转换时互相锁住; - 超时后杀掉整个进程组,避免残留
soffice.bin孤儿进程; - 并发上限默认 2;服务端超时(240s)短于客户端超时(300s),保证错误先由服务端明确返回;
- 上传文件时流式发送,200MB 级的文档也不会整份读进内存。
- 每次转换用独立的 LibreOffice 配置目录(
- LibreOffice 版本有坑:Dockerfile 注释写明,LibreOffice 7.3 和 24.2 处理部分含内嵌 OLE 对象的公文类 docx 时会崩溃,换成 26.2 才正常,所以用了 ubuntu:26.04 作基础镜像。
- 字体用
fonts-noto-cjk;转换失败时依次回退到 soffice、unoconv(pkg/repo/pdf/pdf_impl.go:235-300)。 - DOC 先转成 DOCX,再用 gooxml 按样式识别标题,用来生成标书大纲(
lib/docx/docx.go)。 - 不保留文字格式:解析链路里没有处理斜体和下划线。
我们的取舍
- 采纳:转换服务独立部署、不开网络出口、有并发上限、整组清理进程、分层超时、流式上传。这些 Gotenberg 已经内置。
- 要核实:Gotenberg 镜像里的 LibreOffice 版本。S1 要专门用含 OLE 对象的公文类 docx 做测试;如果版本过旧导致崩溃,就自建转换镜像,沿用上面的工程做法,并锁定 LibreOffice 版本。
- 补上标擎没做的:DOCX 原文件用 python-docx(MIT)读取文字和格式(斜体、下划线、加粗、样式),和规范化 PDF 的文字层交叉校验(§3.5,对应样本结论 F1)。
- 字体:Noto CJK 只解决「字能显示」,解决不了「分页一致」:宋体和 Noto 的字宽不同,会导致分页偏移。所以 S1 要比较几种替代字体的分页偏差,标擎在这一点上没有处理。
PDF 文件
标擎的做法
- 用 Docling 做解析服务:单独一个容器
docling-servev1.16.1(MIT)。- 请求参数:输出格式选 json、md、html、text,表格识别选精确模式,OCR 可开关(引擎默认是 easyocr,可选 rapidocr)(
pkg/repo/docling/docling_impl.go:176-231)。 - 从结果里取文本块的坐标框和表格单元格。
- 请求参数:输出格式选 json、md、html、text,表格识别选精确模式,OCR 可开关(引擎默认是 easyocr,可选 rapidocr)(
- 按页检查覆盖率:平均每页文字太少,就强制开 OCR 重新解析一次(
pkg/handler/bidanalysisv3/document.go:391-454)。 - PDF 拆页用 pdfcpu(Apache 2.0)。
- 另有一个不开源的「PDF 服务」:通过
pdf_base_url加应用编号和密钥调用(pkg/repo/pdf/pdf.go),负责解析、生成 HTML 和字符索引。这部分能力离开它就跑不起来,参考价值有限。 - 注意:compose 里给 docling 镜像写死了
platform: linux/amd64。
我们的取舍
- 采纳:
- 解析服务化,主应用不装模型类重型依赖;
- 按页检查覆盖率,不够就用 OCR 补;
- 从解析结果里保留坐标框。
- 调整:
- 坐标粒度从「块」细化到「句」(§3.5)。
- 文字层自己用 pypdfium2 和 pdfplumber 提取,字体、斜体、下划线信息由我们自己掌握。
- Docling 只作为版面和表格识别的候选,在 S2 里和自研方案对比。
- OCR 用 PaddleOCR:中文效果更好,坐标也更精细。
- 信创风险:如果用 Docling,要确认它的镜像或依赖有 arm64 版本,标擎写死 amd64 就是一个信号。S2 要顺带验证。
标书的组装、转码和格式转换(V1 的 M4,现在先定方向)
标擎的做法
- 编辑与生成:
- 富文本编辑器用 TipTap 3。
- 前端用
docx(npm 包,MIT)把编辑器内容拼成 DOCX,包括封面、目录和样式(bid-engine-frontend/components/file-gen/docx-export.ts)。 - 导出 PDF 时,后端再把 DOCX 交给 LibreOffice 转换(
pkg/handler/bidgen/export.go)。
- 两遍导出回填目录页码:
- 前端拼 DOCX 时算不出分页,所以第一遍先把 DOCX 转成 PDF,在正文区按章节标题回读每章的起始页码(
pkg/handler/bidgen/page_map.go:30); - 第二遍再把真实页码写进目录。
- 前端拼 DOCX 时算不出分页,所以第一遍先把 DOCX 转成 PDF,在正文区按章节标题回读每章的起始页码(
- 模板导入:上传 DOC 模板时先转成 DOCX,再按样式拆出大纲(
template_outline.go:74)。 - 审核报告用 excelize 导出为多个工作表的 Excel。
- 许可证风险:前端依赖 TipTap Pro 的私有源(
.npmrc),Pro 扩展是收费商业组件。
我们的方向
- 组装放在服务端(Python),不放前端。理由有三:
- 标书要套用招标文件自带的格式文件(投标函、授权书、开标一览表),本质是「按模板填充 + 合并章节 + 附上资质扫描件」,服务端更好控制;
- 网页版和桌面版的输出能保证一致;
- 组装结果还要回过头来过 M3 的检查。
- 组件候选(都是宽松许可证,V1 立项时再做验证):
- python-docx(MIT):读写 DOCX;
- docxtpl(LGPL-2.1):在 DOCX 里用模板语法填充,许可证要评审;不行就自己基于 python-docx 写填充;
- docxcompose(MIT):合并多个 DOCX,并保留样式和编号;
- pypdf(BSD):合并 PDF,例如在标书末尾附上资质扫描件。
- 目录页码:标擎的「两遍导出」可以用,但更好的做法是:
- 在 DOCX 里写入真正的目录域;
- 转 PDF 时让 LibreOffice 先更新索引再导出,DOCX 和 PDF 的页码由此一致;
- 用户用 Word 打开 DOCX 时也能自己更新目录。
- 这个做法要验证;不行再退回两遍导出。
- 编辑器:TipTap 核心是 MIT 许可,但不依赖它的 Pro 扩展;V1 立项时再和其他编辑器对比。
- 报价:模板里的报价位置只放空白填充位,由用户自己填写(红线 6)。
- OFD 格式:标擎没有处理国家标准的 OFD 版式文件(GB/T 33190)。部分政府采购平台和电子公文会用到 OFD,需要在种子用户访谈时确认是否需要支持;需要的话再评估开源的 OFD 解析和转换库。
3.14 账号、企业与权限:给以后的版本留结构
以后会上团队版和企业版:一个企业多个席位,按角色和项目分权限;也会支持飞书、企业微信登录,以及私有化部署的 SSO。这些功能早期版本都不做。这一节只定一件事:MVP 的表结构和代码分层先按多人、多登录方式来建,以后上这些功能时只加功能,不用重构。
原设计的问题
原设计的租户隔离(§3.3)和建在企业上的额度池(§3.9)不用改。会导致以后要重构的有三处:
- 用户表直接带所属企业(04 §2)。以后一个人要能有多种登录方式、要能离职、要能加入别的企业,就得拆表、迁移数据。
- 权限只有租户这一层。以后加项目级权限,要回头给每个接口补检查,还要改所有列表查询。
- 记录「谁」的字段没约定指向哪张表。如果指向会被删除的账号,人离职以后,留痕就断了。
MVP 就按这个结构建
| 内容 | MVP 的样子 | 以后接入时 |
|---|---|---|
user_account:一个自然人,全局表,不带 tenant_id |
注册时建一条 | 不变 |
login_identity:登录方式,字段有来源、外部组织编号、外部用户编号;唯一键是「来源 + 外部组织 + 外部用户」 |
只有「手机号」一种来源 | 飞书、企业微信、账号密码、OIDC、LDAP 都只是新的来源 |
membership:人在企业里的成员身份,字段有企业角色、状态 |
每个企业一条,角色是「所有者」;「一人只属于一个企业」(FR-M6-04)在服务层检查,不写进表结构 | 加邀请、停用,放开一人多企业 |
user_session 绑定 membership_id |
当前租户从成员身份上取 | 切换企业就是换一个会话 |
project_member:项目、用户、项目角色 |
建项目的人自动成为负责人 | 加项目成员管理、可见范围 |
| 订阅带「席位数」 | 写 1 | 按席位计费 |
权限点枚举 + service 层统一调用 authz.require(ctx, 权限点, 对象) |
所有者拥有全部权限点,检查总是通过 | 加角色、改角色和权限点的对应关系;接口代码不用改 |
| 列表查询走 repo 层统一的可见范围过滤 | 过滤条件只有租户 | 加「只有项目成员可见」的条件 |
IdentityProvider 接口:授权地址 → 回调换取用户信息 → 统一的外部身份 |
只有手机号验证码一个实现 | 每种新登录方式加一个实现 |
约定:
- 业务表里记录「谁」的字段(创建人、确认人、处理人)都指向
user_account。人不做物理删除,离职只停用成员身份。 - 额度流水和审计事件都记操作人;审计再存一份操作人姓名快照,人离职后,报告和留痕上仍显示当时确认的人。
user_account和login_identity是 §3.3 唯一的例外:全局表,不受行级安全策略约束,只能由identity模块读写,在 import-linter 规则里写明。- 行级安全策略只管租户这一层,项目级权限在应用层判断。
这些在 MVP 里多出来的工作只是两三张表和一层检查调用,界面上看不出任何区别。
接入时再定的事
下面这些不影响表结构,到对应版本立项时再设计:
- 团队版、企业版的具体角色和权限点划分(PRD 写的是管理员 / 编标 / 只读);
- 项目可见范围的默认值、分工指派;
- 飞书、企业微信的应用形态,外部组织怎么绑定到企业,新成员是否需要批准,账号怎么合并;
- 桌面端的第三方登录回调方式;
- 个人注销和企业注销的拆分(MVP 只有一个成员,两者是同一个操作)。
4. 前端
4.1 Vue 3 单页应用(AR2,已确认)
- 为什么是单页应用、不做服务端渲染:
- 桌面客户端:单页应用打包后就是一堆静态文件,Electron 或 Tauri 直接加载即可;服务端渲染要多带一个 Node 进程。
- 私有化交付:静态文件由 nginx 托管就行,客户现场少一个 Node 服务要运维。
- 不需要 SEO:工作台在登录之后。以后的官网是独立站点,可以用 Astro 生成静态页。
- 为什么选 Vue 3(用户决定):政企项目的前端人才和现成案例以 Vue 为主,组件库生态成熟,私有化客户的二次开发和运维人员也更熟悉。
4.2 技术选型
| 用途 | 选型 | 说明 |
|---|---|---|
| 构建 | Vite + TypeScript(严格模式)+ vue-tsc | |
| 写法 | 单文件组件 + <script setup lang="ts"> + 组合式 API |
不用选项式 API |
| 路由 | Vue Router 4 | |
| 客户端状态 | Pinia | 只放界面状态(当前选中的清单项、筛选条件等) |
| 服务端数据 | TanStack Query for Vue(@tanstack/vue-query) |
缓存、重试、轮询兜底;服务端数据不放进 Pinia |
| 接口类型 | openapi-typescript + openapi-fetch | 由后端的 OpenAPI 文档生成,不手写类型 |
| UI 组件 | 自己写(packages/ui),不引入带样式的 UI 框架(AR10,见 §4.3) |
样式只来自 tokens.css 和 components.css |
| 交互基础组件 | Reka UI(原 Radix Vue,无样式) | 对话框、下拉选择、日期选择、弹出层、轻提示等的键盘操作和无障碍 |
| 表格 | 自己写样式;需要排序、筛选时用 TanStack Table,长列表用 TanStack Virtual | 都是无样式库 |
| 样式 | docs/design/system/tokens.css 的变量 + 单文件组件的 scoped CSS |
不引入 Tailwind,避免和组件库、设计规范形成第三套样式体系 |
| 图标 | Lucide(lucide-vue-next,U3) |
不用组件库自带的图标集,全站统一 |
| pdf.js(pdfjs-dist,Apache 2.0)+ 自研高亮层 | 按页虚拟滚动,按坐标框高亮原句 | |
| 表单校验 | Zod + 自写的 useForm 组合式函数 |
校验时机按设计规范 7.13 |
| 测试 | Vitest + Vue Test Utils(单元测试)+ Playwright(端到端测试) | |
| 代码检查 | ESLint(flat config)+ Prettier |
4.3 要不要引入 UI 框架(AR10)
结论:不引入 Element Plus 这类带样式的 UI 框架。 视觉全部由我们自己用 Vue 单文件组件和样式表实现;只在交互复杂、无障碍要求高的少数几类组件上,用无样式交互组件库 Reka UI 打底。
为什么不用带样式的 UI 框架
带样式的框架适合「直接用它自带外观」的项目,比如内部系统。我们有一套自己的高保真设计规范,用它反而吃亏,有三个原因:
-
核心组件它根本不提供:出处批注、问题行、清单项、原文查看器、参数对照表、项目行、额度、任务进度(设计规范 7.2~7.11),都是产品专属组件,不管用不用框架都得自己写。框架能覆盖的只是外围的基础件。
-
基础件的默认行为和规范冲突,几乎每一个都要改造:
设计规范的要求 带样式框架的典型默认行为 没有红色危险按钮(7.1) 提供 danger 按钮,删除确认框默认用红色 加载中按钮把文字换成「正在…」,宽度不变(7.1) 在按钮里加转圈图标,宽度会变 必填项在标签后写「(必填)」,不用红色星号(7.13) 默认显示红色星号 错误提示:赭色边框 + 下方赭色小字 + 「!」图标(7.13) 红色边框,错误文字的位置和结构是固定的 轻提示 4 秒后消失,悬停暂停;页内提醒用左边线样式(7.12) 自带一套提示样式和图标 级别靠实心、描边、无框来区分(3.3) 标签组件只有几种固定的视觉变体 改造的办法是覆盖框架的内部样式(例如
:deep(.el-input__wrapper)),要和框架的选择器比优先级。框架一升级、内部结构一变,这些覆盖就可能悄悄失效。 -
两套样式体系并存:框架有自己的变量、尺寸档位和间距,和
tokens.css并存,开发时要不断判断「该用谁的」,很容易偏离设计稿。
为什么也不完全手写
纯手写对外观没问题,但少数几类组件的交互和无障碍很难写对,这类代码写得不对,就会在键盘操作、屏幕阅读器、浮层定位上不断出边角问题:
| 组件 | 难点 | 用在哪里 |
|---|---|---|
| 对话框 | 焦点锁定、关闭后焦点回到原处、Esc 关闭、背景禁止滚动 | 删除确认、忽略原因(7.14) |
| 下拉选择、可搜索的下拉框 | 键盘上下选择、输入即搜索、浮层定位和翻转 | 分包选择、资质类型 |
| 日期 / 日期时间选择 | 日历键盘导航、中文本地化、日期范围 | 关键日期修改、资质有效期 |
| 弹出层、提示气泡、下拉菜单 | 定位、碰撞检测、悬停延迟、外部点击关闭 | 出处预览、行内更多操作 |
| 轻提示 | 队列、悬停暂停、读屏播报 | 操作结果(7.12) |
| 标签页、复选框、单选、开关 | 方向键切换、ARIA 状态 | 清单分类、批量勾选 |
中间方案:无样式交互组件库(headless)
这类库只提供行为、键盘操作和无障碍属性,不带任何样式;外观完全由我们自己写,和设计规范没有冲突。
| 候选 | 最新版本 | 最近发版 | npm 周下载 | 许可证 | 判断 |
|---|---|---|---|---|---|
| Reka UI(原 Radix Vue) | 2.11.0 | 2026-10-05 | 约 285 万 | MIT | 推荐:组件最全(对话框、下拉选择、可搜索下拉框、日期选择、弹出层、菜单、轻提示、标签页等),Nuxt UI 和 shadcn-vue 都基于它 |
| Ark UI(Vue 版) | 5.40.0 | 2026-10-29 | 约 4 万 | MIT | 备选:基于状态机,质量好,但 Vue 社区规模小 |
| Headless UI(Vue 版) | 1.7.23 | 2024-09-09 | — | MIT | 不选:两年没有发版,组件也少 |
(2026-11-03 查询 npm。shadcn-vue 也基于 Reka UI,但它依赖 Tailwind,和我们「只用 tokens.css」的约定冲突,所以不用。)
最终方案
packages/ui/
tokens.css、base.css ← 来自 docs/design/system/,作为唯一的样式来源
基础件(自己写,纯 Vue + 样式表)
Button、Tag、SeverityTag、Card、Alert、Input、Textarea、Field、
Table、Pagination、Empty、Skeleton、Progress、Steps……
交互件(Reka UI 打底,外观自己写)
Dialog、Select、Combobox、DatePicker、Popover、Tooltip、
DropdownMenu、Toast、Tabs、Checkbox、RadioGroup、Switch
业务组件(自己写)
Citation(出处)、FindingRow(问题行)、ChecklistItem、
PdfViewer(pdf.js + 高亮层)、ParamCompareTable、ProjectRow、
QuotaMeter、TaskProgress、AiMark……
其余配套库也都选无样式的:
- 表格:项目列表、清单、问题列表都是自己写的表格样式。需要排序、筛选、列宽调整时,用 TanStack Table(无样式);长列表用 TanStack Virtual 做虚拟滚动。
- 表单:用 Zod 定义校验规则,配一个自写的
useForm组合式函数,实现设计规范 7.13 的校验时机(离开输入框时校验、提交时汇总)。表单变复杂后再评估 VeeValidate。 - 图标:Lucide(
lucide-vue-next,U3)。 - 浮层定位:Reka UI 内置了 Floating UI,不用另外引入。
工作量评估
- 设计规范第 7 节和原型
docs/prototype/已经把样式和结构定好了,components.css可以直接拿来用。所以基础件和交互件主要是「把现有的样式封装成 Vue 组件」,预计 2~3 人周,并且可以和页面开发并行。 - 用 Element Plus 起步看似更快,但要改造的组件和要写的覆盖样式加起来,加上后续升级的维护,不会更省;而业务组件那部分工作量,两种方案完全一样。
- 质量保证:每个组件写一个预览页,继承现有的
components.html,作为视觉对照和回归检查;交互件补上键盘操作的单元测试。
运营后台:不在这一阶段讨论,等做运营后台时再定。它是独立应用(apps/admin),用不用带样式的框架都不影响用户端。
4.4 应用划分
apps/web:用户端(工作台、读标、查标、额度、资质库)。apps/admin:运营后台,单独构建、单独部署,私有化时不交付;技术选型等做到时再定。packages/ui:基础件、交互件(Reka UI 打底)和业务组件,样式只来自设计规范,见 §4.3。packages/api-client:生成的接口客户端。packages/platform:平台能力的抽象层,见 §5.2。
4.5 代码规模约定
吸取参考项目的教训:单文件组件超过 400 行要拆分,页面组件只负责组合;服务端数据由 TanStack Query 管理,不在页面里堆一堆 ref 和定时器。
5. 桌面客户端(T3:连服务端的客户端外壳)
5.1 形态
桌面端 = 本地外壳 + 随安装包附带的同一套前端静态文件 + 连接 SaaS 或私有化服务器。外壳不内嵌后端、OCR 和模型。
外壳只额外提供几样本地能力:
- 选择文件或文件夹、拖入大文件;
- 系统通知;
- 报告存到本地并打开;
- 登录令牌存进系统钥匙串;
- 自动更新(SaaS)或离线升级包(私有化);
- 首次启动时选择服务器地址(SaaS 或私有化服务器)。
5.2 现在就要守住的约束(保证以后打包成本低)
- 前端不能依赖服务端渲染,必须是纯静态构建产物(§4.1)。
- 接口地址可配置:不能写死「同源
/api」,从运行时配置里读取。网页版默认同源,桌面版用用户选择的服务器地址。 - 鉴权同时支持 cookie 和 Bearer 令牌(§3.11)。桌面外壳以自定义协议加载页面时,跨域 cookie 不可靠。
- 所有平台能力都走
packages/platform抽象层:文件选择、下载保存、通知、打开外部链接、剪贴板。现在只实现网页版,以后加桌面版实现,业务代码不用改。 - 不依赖浏览器专有能力:PDF 预览用 pdf.js 自己渲染,不用浏览器内置的 PDF 查看器;字体全部自托管(已经这样做了);不依赖第三方 CDN。
- 路由在
file://或自定义协议下也能用:路由基础路径可配置。 - 大文件上传:用分片上传、支持断点续传,接口设计成和平台无关的样子。
5.3 外壳选型(AR6,到时再定)
| Electron | Tauri 2 | |
|---|---|---|
| 渲染引擎 | 自带 Chromium,各平台显示一致,和网页版测试的 Chrome 一样 | 用系统的 WebView:Windows 上是 WebView2,macOS 上是 WebKit,Linux 上是 WebKitGTK,各平台有差异 |
| 安装包大小 | 约 100MB | 约 10MB |
| 信创 Linux(UOS / 麒麟) | 有 arm64 构建;LoongArch 要靠社区构建,需要实测 | 依赖系统自带的 WebKitGTK 版本,需要实测 |
| 本地能力开发 | Node.js | Rust |
倾向 Electron:读标和查标高度依赖 pdf.js 渲染和精确高亮,渲染一致性比安装包大小更重要;团队也不必再学 Rust。如果到时信创环境对体积敏感,或者 WebKitGTK 实测没问题,再考虑 Tauri。因为有 §5.2 的约束,两者切换的成本都不高。
估计工作量:外壳 PoC 约 1 周,加上代码签名(Windows EV 证书、Apple 公证)、自动更新和信创打包,正式版约 3~4 周。
6. 配套组件
许可证策略:MIT、BSD、Apache 2.0、MPL、OFL 可以直接用。AGPL、SSPL、BSL 以及商业许可要先评审,因为私有化交付属于「分发」。CI 里做依赖许可证扫描,并生成 SBOM(软件物料清单)。
| 组件 | 选型 | 许可证 | SaaS | 私有化 | 不选什么、为什么 |
|---|---|---|---|---|---|
| 数据库 | PostgreSQL 16 以上 | PostgreSQL License | 云厂商托管实例 | 官方镜像,或客户已有的实例 | MySQL:行级安全策略、JSONB、LISTEN/NOTIFY 都不如 PostgreSQL;而且主流信创数据库(人大金仓、openGauss / GaussDB、瀚高)多数兼容 PostgreSQL |
| 任务队列 | Procrastinate(基于 PostgreSQL) | MIT | 同左 | 同左 | Celery + Redis:入队和数据库事务无法原子提交;自研队列:标擎的教训 |
| 缓存、限流、推送 | Valkey 8(§6.1) | BSD | 托管的 Redis 兼容服务 | 官方镜像,不持久化 | Redis:7.4 以后改了许可证(RSAL / SSPL,Redis 8 又加了 AGPL),私有化分发有风险;Valkey 是 Linux 基金会维护的分支,协议兼容 |
| 对象存储 | S3 协议;本地磁盘驱动 | — | 云厂商的 OSS / COS / OBS | 单机用本地磁盘;集群用客户已有的 S3,或 SeaweedFS(Apache 2.0) | MinIO 社区版:2025-10 起不再发布镜像和二进制,随后停止维护,许可证还是 AGPL |
| 文档转换 | Gotenberg(LibreOffice + Chromium) | MIT(LibreOffice 是 MPL) | 同左 | 同左 | 自己封装 LibreOffice:并发和稳定性要自己处理。如果 Gotenberg 里的 LibreOffice 版本太旧,就按标擎的做法自建转换镜像(§3.13) |
| Office 读写 | python-docx、docxcompose;V1 再评估 docxtpl | MIT、MIT、LGPL | — | — | 标擎在前端用 docx 拼文件:模板填充和网页版、桌面版输出的一致性都不好控制(§3.13) |
| OCR | PaddleOCR | 历史上是 Apache 2.0,以仓库为准 | CPU 版,量大时上 GPU;也可以用多模态大模型 | CPU 或 GPU 版 | 云 OCR 接口:私有化时用不了 |
| PDF 处理 | pypdfium2、pdfplumber | Apache / BSD、MIT | — | — | PyMuPDF:AGPL |
| 反向代理 | nginx | BSD | 同左,前面再放云负载均衡 | 同左 | Caddy 也行,但政企运维更熟悉 nginx |
| 监控 | OpenTelemetry + Prometheus + Grafana + Loki | Apache / AGPL | 自建或用云厂商的监控 | 不随包交付:只暴露指标接口和日志文件,由客户接入自己的监控 | — |
| 错误追踪 | GlitchTip 或云厂商服务 | MIT | 内部使用 | 不交付 | Sentry 自部署版是 FSL 许可,内部使用没问题,但不分发 |
6.1 要不要引入 Redis(AR4 复议)
v0.1 建议「MVP 不引入 Redis」。用户追问了两点:会话令牌存在哪里?不引入 Redis 到底能省多少?重新评估后的结论是:引入 Valkey,但只让它存丢了也没关系的数据。
先把两件事分开。 v0.1 的建议里混了两个决定:
- 任务状态和队列放在 PostgreSQL:「预扣额度 + 建任务 + 入队」在一个事务里原子提交。这是额度红线的保障,保留不变。
- 整个系统不要 Redis:这一条才是复议的对象。
第 1 条的收益和有没有 Redis 无关。把两者分开看,「不要 Redis」本身能省下的东西很少。
不引入 Redis 能省多少:
| 方面 | 能省下的 | 实际分量 |
|---|---|---|
| SaaS 成本 | 一个小规格的托管 Redis 实例,每月几百元 | 小 |
| SaaS 运维 | 托管服务基本免运维 | 很小 |
| 私有化 | 少一个容器(内存约 100MB);只要不让它存权威数据,就不用备份,也不用做高可用 | 小 |
| 代码 | 少一个客户端库 | 很小 |
不引入 Redis 要付出的代价(MVP 里确实会遇到的需求):
| 需求 | 只用 PostgreSQL 怎么做 | 问题 |
|---|---|---|
| 模型接口限速:国产模型普遍限制每分钟请求数和 token 数,多个 worker 要共用计数器 | 用表加行锁或咨询锁模拟令牌桶 | 高频写入,锁竞争重,实现别扭 |
| 短信防刷、登录限流 | 计数表按时间窗口 upsert | 攻击流量会直接打到主库上 |
| SSE 进度推送到多个 api 实例 | LISTEN/NOTIFY | 每个实例要单独占一条长连接;云厂商的托管 PostgreSQL 前面常有连接池代理,事务模式下用不了 LISTEN |
| 验证码、短期缓存 | 带过期时间字段的表,再加定时清理 | 能做,但要自己处理过期 |
结论:不引入 Redis 省下的只有「一个轻量容器」;而限速、防刷、多实例推送这三件 MVP 必做的事,用 PostgreSQL 做都很别扭。值得为它付出一个容器的代价,前提是守住下面的边界。
Valkey 的使用边界:
| 放在 Valkey | 放在 PostgreSQL(权威) |
|---|---|
| 模型接口的限速计数器、并发信号量 | 用户、租户、会话 |
| 短信验证码、发送频率、登录失败计数 | 项目、清单、问题、审计 |
| 进度和取消的推送通知 | 任务状态、进度、取消标记 |
| 短期缓存(例如规则配置) | 额度账户和流水 |
| 任务队列(Procrastinate) |
判断标准:Valkey 被清空后,系统照常运行,最多是用户重新获取一次验证码、限速计数从零开始。所以 Valkey 不开持久化、不做备份;私有化时就是一个普通容器。
会话为什么放在 PostgreSQL 而不是 Valkey(回答「令牌存在哪里」):
- 持久:Valkey 一重启,所有人就都掉线了;放在数据库里没有这个问题。
- 要做关系查询:「同一账号异地多设备登录时提示并限制」要按用户列出所有会话;运营后台要查登录记录;删除租户时要级联清理。这些都是关系型操作。
- 性能不是问题:每个请求一次主键查询,不到 1 毫秒;B 端的并发量远远够用。真要优化,再在 Valkey 里加一层短期缓存,并在吊销会话时主动清掉。
具体设计见 §3.11。
6.2 不引入的组件
不引入的组件和理由:
- Elasticsearch:投标文件里的检索范围只是单个项目的几百页,在内存里用 BM25 加 jieba 分词就够了。
- 独立向量库:MVP 不需要,V1 的知识库先用 pgvector。
- Kafka、RabbitMQ:消息量太小,用不上。
7. 部署与运维
7.1 一套镜像,三种形态
| 形态 | 编排 | 数据库、存储 | 用在 |
|---|---|---|---|
| SaaS 内测(T4) | Docker Compose,1~2 台云服务器 | 托管 PostgreSQL + 托管对象存储 | 种子用户阶段;顺带打磨私有化的 Compose 交付 |
| SaaS 商用 | 托管 K8s + Helm chart | 托管 PostgreSQL(高可用)+ 托管对象存储 | 公测以后 |
| 私有化 | 小客户用 Compose 单机;有 K8s 的客户用 Helm | 自带 PostgreSQL + 本地磁盘 / 客户的 S3 | P2 企业版 |
- Compose 和 Helm 用同一批镜像、同一份配置结构。配置项定义只写一份,用来生成
.env模板和 Helm 的values.yaml。 - CI 每次都要验证两种编排:在 CI 里用 Compose 起一整套服务跑冒烟测试;Helm chart 在 kind 集群里做安装测试。
- 镜像从第一天起就同时构建 amd64 和 arm64:鲲鹏、飞腾这类信创服务器都是 arm64。
7.2 配置与发布
- 配置全部来自环境变量,启动时用 Pydantic Settings 校验,缺项或者值不合法就直接退出。
- 数据库迁移作为一个一次性任务,在新版本上线前执行;采用「先扩后缩」的方式(先加列,再切换代码,最后删旧列),支持滚动发布。
- 健康检查分两种:存活检查(进程活着)和就绪检查(数据库能连上、迁移版本一致)。
7.3 私有化交付包
- 离线安装包:所有镜像(
docker save导出)+ Compose 文件或 Helm chart + 安装脚本 + 校验和 + 签名。字体、OCR 模型都打进镜像,安装时不需要联网。 - 授权文件:用 Ed25519 签名,内容包括客户名称、有效期、可用功能;离线校验,过期后系统改为只读。
- 代码保护(AR7):私有化镜像里的 Python 核心包用 Nuitka 编译成二进制,提示词文件加密打包。
- 大模型:路由层改指向客户自部署的服务(vLLM、SGLang、Xinference 上跑的 Qwen、DeepSeek 等),或者客户批准使用的云端接口。
- 运维脚本:备份(
pg_dump+ 文件目录)、恢复、升级(含迁移)、一键导出诊断包(日志、版本、配置,自动脱敏)。
7.4 信创适配(企业版可选,P2)
- CPU 和操作系统:鲲鹏、飞腾(arm64)的镜像从第一天就构建。龙芯(LoongArch)要按需评估,难点是 Python 依赖在 loong64 上有没有现成的 wheel 包(龙芯有自己的 PyPI 镜像,需要实测)。操作系统是麒麟或 UOS,容器运行时用 Docker 或 iSulad。
- 数据库:核心 SQL 只用 PostgreSQL 的常见子集,不依赖小众扩展,确保能迁到人大金仓、openGauss、瀚高。行级安全策略、LISTEN/NOTIFY、Procrastinate 在这些数据库上的兼容性,等第一个信创客户出现时专门验证(AR3)。
- 中间件:如果客户要求用东方通、宝兰德这类国产 Web 中间件,只把 nginx 那一层换掉。
7.5 可观测性
- 用 OpenTelemetry 打通 api → worker → 大模型调用的完整链路。
- 核心指标:
- 各解析阶段的耗时,P95 对照 10 分钟的目标;
- 队列长度;
- 各模型供应商的错误率、耗时和 token 用量;
- 证据校验的通过率,它下降意味着提示词或模型出了问题;
- 额度对账的差异。
- 日志统一用 JSON 格式,带请求编号、租户编号、任务编号,不带文档内容(§3.11)。
7.6 备份与容灾
- SaaS:托管 PostgreSQL 开时间点恢复(PITR);对象存储开版本控制。RPO、RTO 的目标在商用前确定。
- 删除项目时同步清理对象存储,并销毁对应的数据密钥(§3.11),保证备份里的数据也无法再读取。
8. 代码仓库与工程规范
采用单一仓库(monorepo),前端用 pnpm workspace,后端用 uv workspace:
apps/
web/ # 用户端单页应用
admin/ # 运营后台
desktop/ # 桌面外壳(以后再建)
packages/
ui/ # 设计规范组件库
api-client/ # 由 OpenAPI 文档生成的接口客户端
platform/ # 平台能力抽象层(网页版 / 桌面版)
server/
src/bid_doctor/
identity/ billing/ project/ document/ checklist/ check/
rules/ ai/ report/ qualification/ audit/ ops/
core/ # 配置、数据库、租户上下文、错误、日志
migrations/ # Alembic 迁移脚本
tests/
services/
ocr/ # PaddleOCR 服务封装
prompts/ # 带版本号的提示词与输出格式定义
evals/ # 评测集(标注数据 + 评测脚本)
deploy/
compose/ # Docker Compose
helm/ # Helm chart
offline/ # 私有化离线包构建脚本
docs/ # 现有文档
- 合并代码的前提:类型检查通过、单元测试通过、租户隔离用例通过;改了提示词或路由的,还要跑评测回归。
- 测试分层:
- 规则引擎和证据校验用单元测试覆盖,要求接近 100%;
- 接口用集成测试,跑在真实的 PostgreSQL 上,用 testcontainers;
- 关键流程用端到端测试:上传 → 解析 → 检查 → 导出。
- 大模型在测试里用录制回放,CI 不调用真实模型;评测回归单独跑,需要真实模型。
9. 关键数据设计要点
物理建模在下一步细化,这里先定原则:
- 主键用 UUIDv7:按时间有序、不可猜测;私有化环境和 SaaS 之间迁移数据时也不会冲突。
- 所有业务表都带
tenant_id、created_at、updated_at;时间一律用timestamptz。例外只有user_account和login_identity两张全局表(§3.14)。 - 业务表里记录「谁」的字段(创建人、确认人、处理人)都指向
user_account,不指向成员身份;人不做物理删除。 - 出处(Citation)存:文档编号、规范化 PDF 的哈希、页码、句子编号、字符范围、坐标框数组(
quads)。 - AI 值和人工值分开存(§3.8);状态字段用枚举,不用自由文本。
- 文字层(逐字坐标、格式、句子切分)存在对象存储里,按页分片;句子不进数据库(DM1),数据库只存页摘要和出处快照(原句、坐标框)。
- 完整的表结构见
data-model.md和schema-draft.sql。
10. 编码前要做的技术验证
| # | 验证内容 | 通过标准 | 影响 |
|---|---|---|---|
| S1 | Word 转 PDF 的保真度:用 10 份样本,加上含内嵌 OLE 对象的公文类 docx,比较替代字体下的分页偏差,并核实 Gotenberg 里的 LibreOffice 版本 | 转换不崩溃;分页偏差可接受,或者有可用的字体替换表 | §3.5、§3.13 |
| S2 | 在规范化 PDF 里识别斜体和下划线,并和 DOCX 原文件的格式交叉校验;Docling 和自研方案对比,同时确认 Docling 有没有 arm64 版本 | S06、S08 的实质性条款能全部识别出来 | FR-M2-05、07 F1 |
| S3 | 原句级的坐标切分和高亮 | 抽查 50 条,位置都对 | FR-M2-06 |
| S4 | 国产模型的结构化输出和证据校验:在 S01–S10 上测 ★ 和实质性条款的召回率,同时测成本 | 召回率目标在验证时定;同时给出 D2 额度定价所需的单次成本 | FR-M2-05、D2 |
| S5 | PaddleOCR 处理扫描件:中文准确率、坐标精度、CPU 上的耗时 | 300 页在 10 分钟内 | FR-M2-02 |
| S6 | 用多模态模型检测签章「位置与有无」 | 给出准确率,决定 G12 怎么做 | FR-M3-04 |
| S7 | Procrastinate 的稳定性:worker 被杀掉、并发领取任务、优雅退出 | 不丢任务、不重复执行 | §3.4 |
S1–S4 风险最高,建议先做;S8(桌面外壳 PoC)等桌面版立项时再做。
S1–S4 已于 2026-11-03 完成,详见 tech-validation.md,代码在 spikes/。结论:
| # | 结论 | 关键数字 |
|---|---|---|
| S1 | 可行,但要加两步:修正 WPS 错标的字体字符集,换成按宋体行高调整过的开源字体。Gotenberg 有 arm64 镜像,自带 LibreOffice 26.8.1;10 份文件都没有崩溃 | 默认配置偏差 +17%~+37%,S07 正文空白;调整后有真值的 5 个数据点都在 ±3% 内。本批样本没有含 OLE 嵌入对象的文件,未测 |
| S2 | 可行,在规范化 PDF 上识别格式,不需要 Docling | 斜体且下划线:4 份文件按字符 100% 一致 |
| S3 | 正文可行;表格内和页眉页脚带要改进切句 | 几何回读 300/300;文本保真:正文 97.5%,表格 78% |
| S4 | 可行,加上第三道依据校验和规则兜底 | ★ 召回 93.5%(扣除银标准噪音后约 96.6%),斜体下划线 99.8%;采纳条目 95.2% 属实;单项抽取每份约 0.15 元(DeepSeek Flash,高峰价) |
据此建议:FR-M2-05 标注类条款召回 ≥ 99%(规则兜底 + 模型),采纳条目准确率 ≥ 95%;FR-M2-06 出处页码正确率 100%,原句高亮准确率正文 ≥ 98%、表格 ≥ 95%(tech-validation.md §5)。
11. 架构决策清单
| # | 事项 | 结论 | 状态 |
|---|---|---|---|
| AR1 | 架构形态 | 模块化单体 + 按负载拆进程,不拆微服务(§2.1) | 已确认 |
| AR2 | 前端框架 | Vue 3 + TypeScript + Vite 单页应用,不做服务端渲染(§4.1) | 已确认(用户改为 Vue 3) |
| AR3 | 数据库 | PostgreSQL,并以兼容 PostgreSQL 的信创数据库作为迁移路线;行级安全策略作为第二层隔离,在不支持它的信创数据库上可以关掉 | 已确认 |
| AR4 | 任务队列与 Redis | 任务状态和队列放在 PostgreSQL(Procrastinate);引入 Valkey,只存丢了也没关系的数据(限速、验证码、推送、缓存);会话放在 PostgreSQL(§6.1) | 已确认(修订后) |
| AR5 | 对象存储 | S3 协议 + 本地磁盘驱动;不用 MinIO | 已确认 |
| AR6 | 桌面外壳 | 倾向 Electron,桌面版立项时再定;现在守住 §5.2 的 7 条约束 | 已确认 |
| AR7 | 私有化代码保护 | 用 Nuitka 编译核心包 + 签名授权文件 + 提示词加密 | 已确认 |
| AR8 | 会话机制 | 服务端会话(可吊销),令牌只存哈希;网页端用 cookie,桌面端用 Bearer 令牌 | 已确认 |
| AR9 | 仓库组织 | 单一仓库(monorepo) | 已确认 |
| AR10 | UI 组件 | 不引入带样式的 UI 框架:视觉自己写,复杂交互用 Reka UI(无样式)打底;表格、表单也用无样式方案(§4.3)。运营后台另议 | 已确认 |
| AR11 | 身份模型 | 拆成人(user_account)、登录方式(login_identity)、成员身份(membership)三层,MVP 就按这个结构建;「一人只属于一个企业」在服务层限制(§3.14) |
已确认 |
| AR12 | 权限检查点 | MVP 就建 project_member 表,service 层统一调用 authz.require,列表查询统一按可见范围过滤;具体角色划分到团队版再定(§3.14) |
已确认 |
12. 下一步
- 做技术验证 S1–S4,结论写进本文件,并据此确定 FR-M2-05、FR-M2-06 的目标值。
物理数据建模:✅ 已完成,见data-model.md、schema-draft.sql(DM1–DM7 已确认)。定接口契约:✅ 已完成,见api-design.md、openapi.yaml(API1–API5 已确认)。搭工程骨架:✅ 已完成,见 §13 和 AGENTS.md「工程」一节。
13. 工程骨架的落地记录(2026-11-03)
搭骨架时按代码实际情况做了下面这些调整或补充。SK1、SK2 改了上文的写法,待确认;其余是实现细节,记在这里备查。
| # | 事项 | 做法 | 状态 |
|---|---|---|---|
| SK1 | 进度推送(§3.4) | 用 Valkey stream 代替发布 / 订阅:一个结构同时解决实时推送和断线补发(按 Last-Event-ID 续读),多个 api 实例都能读到。Valkey 丢了也只是没有推送,前端轮询兜底 |
待确认 |
| SK2 | 接口集成测试(§8) | 不用 testcontainers,改为连 Compose 起的 PostgreSQL,每次测试会话重建 bid_doctor_test 库;CI 里用 GitHub Actions 的服务容器。更快,本地和 CI 走同一套配置 |
待确认 |
| SK3 | 建任务和入队在同一个事务里(§3.4) | Procrastinate 的 defer 用的是它自己的连接,进不了我们的事务,所以直接调用它的 SQL 函数 procrastinate_defer_jobs_v1(core/queue.py);它的表放在 public schema。pyproject 锁定了大版本,升级时要核对 |
已实现 |
| SK4 | 上传会话(§5.2 第 7 条) | 上传会话的元数据放在 Valkey(24 小时过期),分片按企业级数据密钥加密后写进对象存储;挂到项目上时换成项目密钥 | 已实现 |
| SK5 | ORM 模型 | 表结构以迁移为准,ORM 模型只做列映射、不声明外键,所以写入顺序要自己 flush 保证(测试里踩到过两次) |
编码约定 |
| SK6 | 文字层 | 有的 PDF(如 Chromium 生成的)把汉字映射成康熙部首码位(「目」成了 U+2F6C),引文校验会对不上;文字层把康熙部首和部首补充区换回常用汉字,其他字符不动 | 已实现 |
| SK7 | 斜体下划线(07 F1) | 只有文件的「标注规则说明」声明过斜体下划线标注,才按它认定实质性条款;没声明的文件里,标题、表头也常是斜体或下划线(S02 实测踩到) | 已实现 |
| SK8 | 登录验证码 | 登录和注册共用一个验证码;会话建好之后才作废,用户确认挤下线(API3)或发现「还没注册」时不用重新获取 | 已实现 |
| SK9 | 读标抽取(2026-11-03 补) | 五类清单(资格、无效 / 实质性、评分、日期与递交、技术参数)加项目基本信息各一个抽取任务、一份提示词(prompts/extract.*),分块并发调用;每类都过三道校验,第三道按类别核对:实质性条款核对标注或「无效」字样,其他类别核对日期、分值、参数值、金额在原文里找得到,是 / 否字段核对说法。各任务抽到同一条要求时按首句合并 | 已实现 |
| SK10 | 文字层 v2 与结构识别 | 页眉页脚带(页码、各页重复的行)单独成句,不并进正文;标记写到页尾的句子,和下一页首句接续成一条要求(出处两页各一条);最小结构识别:十进制分级编号和冒号引出的列表找上级条款,上级条款写明无效情形的,下面的条目照样认定。文字层 v1、v2 的句子编号不同,规范化 PDF 不变 | 已实现 |
| SK11 | 待确认审计后的修正(2026-11-03) | 国动样本 73 条待确认里多数是好结果被校验误拦,逐条审计后按「规则只修真正的缺陷、不迁就模型的错误输出;模型输出不合理的改提示词」处理。文字层 v3:两端接竖线的横线是表格框线,不算下划线(功能表整格被误认成斜体下划线条款);句号后紧跟的右括号、右引号并进同一句(切句 s3-v3,句子编号和 v2 不同)。结构识别:中文编号小节(「三、……否则投标无效」)下的 1. 2.,以冒号结尾的编号条目下的句子;依据核对沿上级链往上找。校验:比对引文前去掉我们自己加进输入的格式标记和〔…〕附注;不足 8 个字的短句整句复制时句末标点可省;取值核对认带量词的中文数字、同一行的单元格和评分因素标题;补「投标(响应)无效」「不能通过资格审查」「没有实质性响应」「拒收」等写法。引文最短 8 个字、编号必须对、证据要全部通过,这些不放宽。提示词:substantive v3、qualification / scoring / schedule / tech_param v2,统一证据规则(引文复制整句、并列多项不拆、跨页只引上一行、数字写阿拉伯数字),除实质性条款外输入改成纯文本;资格不收附件范本和价格扣除条件,日期不收合同条款,技术参数一句一条。模型路由:BD_AI_MODELS 按提示词声明的能力选模型,没配的用 BD_AI_MODEL。离线评测工具 evals/run_extract.py | 已实现 |
| SK12 | 参考 BidEngine 的五项改进(2026-11-03) | 见 ref-bid-engine-analysis.md §5。原因码:没通过校验的结果记具体原因(extract.FAILURES,存在清单项 ai_values.failures),解析结果按原因计数(verify_failures),提示词规则和原因码的对照表在 prompts/README.md。分块:相邻分块重叠约 800 字;模型输出被截断(finish_reason=length)或不是合法 JSON 时,把分块对半重跑,最多 3 层,拆到底仍不完整的记 output_incomplete(以前当成「这一块没有结果」,审计发现 deepseek-flash 有输出正好停在 8192 token、整块条目悄悄丢掉的情况)。项目基本信息:提示词 v2,12 个字段每个都要表态 found / ambiguous / not_found,跨分块合并后分 found / absent(文件本身没写)/ needs_review(抽取有问题),记在解析结果 info_fields。占位符:「____元」「××」「( )」「待填」不算取值。提示词:substantive v4,qualification / scoring / schedule / tech_param v3,每条禁止事项对应一个原因码。三份样本的待确认:国动 4 → 2,S02 13 → 3,S06 57 → 55(S06 有 4 次对半重跑,截断的分块都救回来了) | 已实现 |
| SK13 | S01–S10 标注集评测后的修正(2026-11-03) | 见 extract-eval.md §7。规则:合并时待确认条目不再「借道」正式条目(pending_categories),表后说明、章识别、★ 节标题、输出接近上限按截断处理、格式范本和整表概括句不收;提示词 substantive v5、qualification v4、schedule v4、tech_param v4;WPS 旧版 DOC 直接修正字体表字符集;表格按单元格切句(切句 s3-v4)。口径 Q1–Q4 见 decisions.md。报告分数有过拟合,只按人工标注算未全部达标,泛化未测,见 extract-status.md | 已实现 |
SK11 修正前后(同一模型 deepseek-chat,即 deepseek-flash 不思考模式)的待确认数:国动 46 → 4,S02 17 → 13,S06 174 → 57;同一句被拆成多条的情况没有了。剩下的待确认多数是表格两栏或竖排文字串行(要按单元格切句才能解决)。实质性条款任务上比较了 flash 不思考、flash 思考(deepseek-reasoner)、deepseek-v4-pro:抽到的条款大体一致、互有多少,后两者慢 15~45 倍、输出 token 多 7~12 倍,没有看出明显更准;选型等标注集评测出结果再定。
用国动便民小程序(71 页)、S02、S06 三份文件实测(真实模型 DeepSeek):单份 33~87 秒,清单 216~1348 条,正式清单里的条目都带页码和原句。发现并已修复:标注规则说明的碎片被当成条款、页码混进正文、跨页拆成两条、「做无效响应处理」等写法漏认、上级标题下列出的无效情形被合成一条或漏掉、「无效质疑」被误当成投标无效、「是否接受联合体」取错证据。
还没做、留了 TODO 的:转换镜像的字体包(S1)、OCR(S5)、分包对应到清单项、模型路由的候选列表、降级和限流、查标全部。S01–S10 标注集评测和修复见 extract-eval.md。