# **🎯 终极架构审查与研发红线规范:工具 3 全文智能提取 V2.0** **文档性质:** 架构定稿与研发执行标准 **审查基准:** 《Postgres-Only 异步任务处理指南 (v1.1)》、《通用能力层清单 (v2.4)》 **适用对象:** 后端研发、前端研发、测试工程师 **核心宗旨:** 确保工具 3 在分布式环境下的高可用性,彻底对齐平台现有的 Postgres-Only 规范,消除并发死锁、状态撕裂与算力浪费。 ## **🚨 核心研发红线 (Sprint 1 强制执行)** 在进入代码编写前,所有研发人员必须对齐以下三条底层红线: 1. **Payload 绝对轻量化:** pg-boss 的 Job Data 中,**绝对不允许**传入 PDF 文件的 Base64 或 extractedText 全文。只能传递 { taskId, resultId, pkbDocumentId }。所需的大文本必须在 Worker 启动后,通过 ID 实时从 DB 或 OSS 拉取。 2. **严格的计算卸载:** Node.js 进程绝对不碰任何文档实体的解析计算(pymupdf4llm 或 MinerU)。所有解析动作必须通过 HTTP 路由给独立的 Python 微服务 (extraction\_service) 执行。 3. **过期时间兜底:** 由于大模型提取长文本耗时较长,在推送 asl\_extraction\_child 任务时,expireInMinutes 强制设置为 **30 分钟**,防止任务被系统意外判死。 ## **🛠️ 后端架构排雷与对齐方案** ### **1\. 废弃单机并发控制,拥抱全局队列限流** * **❌ 错误做法:** 在 ExtractionService 内部使用 P-Queue 控制 MinerU 并发。在多实例(Pods)部署下,这会导致真实的 API 请求量翻倍,瞬间引发 429 熔断和重试风暴。 * **✅ 标准解法:** 把并发控制权交还给数据库。针对昂贵的 MinerU 解析,单独拆分一个子队列 asl\_mineru\_extract,配置严格的全局并发数: // 全局只允许同时有 2 个 MinerU 解析任务在跑,跨所有 Node.js 实例生效 jobQueue.process('asl\_mineru\_extract', { teamConcurrency: 2 }, async (job) \=\> { ... }) ### **2\. Fan-out 扇出模式下的并发写入安全** * **❌ 错误做法:** 查询父任务 \-\> successCount \+ 1 \-\> 更新父任务。100 个子任务并发完成时,会导致严重的计数丢失(Race Condition)。 * **✅ 标准解法 (原子递增):** 所有聚合数据的回写,必须 100% 使用 Prisma 的原子操作,并结合**幂等性**检查: // 1\. 幂等性检查 const existing \= await prisma.aslExtractionResult.findUnique({ where: { id: resultId }}); if (existing.status \=== 'completed') return { success: true }; // 2\. 事务内的原子递增 await prisma.$transaction(\[ prisma.aslExtractionResult.update({ where: { id: resultId }, data: { status: 'completed' } }), prisma.aslExtractionTask.update({ where: { id: taskId }, data: { successCount: { increment: 1 }, totalTokens: { increment: tokens } } }) \]); ### **3\. 错误处理边界:永久失败 vs 自动重试** * **业务痛点:** 严格遵循《Postgres-Only 指南》规范1(直接 throw error),会导致在遇到“PKB源文件被删除”等不可逆错误时,pg-boss 盲目重试 3 次,白白消耗资源。 * **✅ 标准解法 (异常分级路由):** 在 Worker 中必须明确区分“可恢复错误”与“致命错误”: try { await doExtraction(); } catch (error) { if (error instanceof PkbDocumentNotFoundError || error.name \=== 'PdfCorruptedError') { // 致命错误:更新业务状态为 error,直接 return success 欺骗 pg-boss 停止重试 await prisma.aslExtractionResult.update({ where: { id: resultId }, data: { status: 'error', errorMessage: error.message } }); return { success: false, reason: 'Permanent Failure, aborted retry.' }; } // 临时错误 (429/网络抖动):直接 throw,让 pg-boss 自动指数退避重试 throw error; } ### **4\. 极致落实 Clean Data 缓存机制** * **业务痛点:** MinerU 表格解析极度昂贵且耗时。 * **✅ 标准解法:** 必须前置检查 OSS 缓存,避免重复计算。 const cleanDataKey \= \`pkb/${kbId}/${docId}\_mineru\_clean.html\`; try { const html \= await storage.download(cleanDataKey); // 优先读取缓存 (\<1秒) return html; } catch (e) { const html \= await callPythonMinerUService(pdfKey); // Fallback: 真正调用 await storage.upload(cleanDataKey, Buffer.from(html)); // 同步存入 Clean Data return html; } ## **💻 前端架构排雷与对齐方案** ### **1\. 通信机制的优雅混合 (React Query \+ SSE)** * **业务痛点:** 前端到底是采用 SSE 维持连接,还是用 React Query 轮询?混用会导致状态撕裂。 * **✅ 标准解法:** * **主业务流控制 (Step 进度、成功/失败跳页):** 严格遵守《Postgres-Only 指南》步骤5。使用 useTaskStatus (React Query) 的 refetchInterval 进行串行稳健轮询。 * **视觉反馈增强 (终端日志流):** 引入 SSE 单向通道,仅用于给 \ 组件灌入实时的打字机日志流。即使 SSE 意外断开,也不会阻断主线业务流。 ### **2\. 人机协作 (HITL) 抽屉的死锁解套** * **业务痛点:** 若 AI 提取的 Quote 模糊匹配失败(置信度 \< 0.8),前端标红警告。但如果医生强行认为 AI 提取的没错,系统没有提供放行的交互,导致数据卡在 Pending 状态。 * **✅ 标准解法:** 抽屉内的错误警告框必须配套两个处理按钮: 1. \[强制认可\]:消除警告,在 payload 中标记 quote\_force\_accepted: true。 2. \[手动修改数值\]:医生直接修改 Input 框,系统自动给旧的错误 Quote 画上删除线,并提示“已转为人工干预,原文引用取消强绑定”。 ### **3\. 规避签名 URL 过期导致的 403 报错** * **业务痛点:** 医生复核 50 篇文献需要很长时间,预签名的 OSS PDF 链接容易过期。 * **✅ 标准解法:** 绝对禁止在加载列表时批量生成并缓存签名 URL。采用**懒加载**:仅当医生点击某行文献的“复核提单”并展开右侧抽屉时,前端才实时请求获取一个有效期为 10 分钟的临时 URL 赋给 iframe。