# **技术设计文档:工具 B \- 病历结构化机器人 (The AI Structurer)** | 文档类型 | Technical Design Document (TDD) | | :---- | :---- | | **对应 PRD** | **PRD\_工具B\_病历结构化机器人\_V2.md** | | **版本** | **V2.0** (架构升级:双模型交叉验证) | | **状态** | Draft | | **核心目标** | 构建一个高可信度的医疗文本结构化引擎,通过**双模型(DeepSeek & Qwen)并发提取**与**自动交叉验证**,解决 AI 幻觉问题。 | ## **1\. 总体架构设计 (Architecture Overview)** 系统架构从“单线性流水线”升级为 **“Y型并发流水线”**。数据进入后,分发给两个不同的 LLM 模型并行处理,结果汇聚到“冲突检测引擎”进行比对,最后输出到人工验证网格。 ### **1.1 系统架构图** graph TD Client\[React 前端 (Grid & Drawer UI)\] subgraph API\_Server \[Fastify API 服务\] JobAPI\[任务与模版接口\] VerifyAPI\[全景网格接口\] end subgraph Async\_Cluster \[后台 Worker 集群\] BullMQ\[BullMQ 任务队列\] Orchestrator\[任务编排器\] PII\_Engine\[隐私脱敏引擎\] subgraph Dual\_LLM\_Engine \[双盲提取引擎\] ClientA\[DeepSeek 客户端\] ClientB\[Qwen 客户端\] end CrossValidator\[交叉验证/冲突检测器\] end subgraph Storage \[数据存储\] PG\[(PostgreSQL \- 业务数据)\] VectorDB\[(pgvector \- 可选,用于语义比对)\] Redis\[(Redis \- 队列)\] end Client \--1.上传&体检--\> JobAPI JobAPI \--2.创建并发任务--\> BullMQ BullMQ \--3.消费--\> Orchestrator Orchestrator \--4.脱敏--\> PII\_Engine PII\_Engine \--5.并行调用--\> ClientA & ClientB ClientA & ClientB \--6.返回JSON--\> CrossValidator CrossValidator \--7.计算一致性--\> PG Client \--8.拉取网格数据--\> VerifyAPI VerifyAPI \--9.人工裁决--\> PG ## **2\. 技术选型 (Tech Stack)** | 层级 | 技术组件 | 选型理由 | | :---- | :---- | :---- | | **后端框架** | **Fastify 5.x** | 高性能异步 I/O,适合处理高并发模型调用。 | | **模型接入** | **LangChain.js** | 统一封装 DeepSeek 和 Qwen 的调用接口,便于切换模型。 | | **任务队列** | **BullMQ** | 核心组件。V2 需要利用 Flow 功能或手动编排来实现“等待两个模型都返回”的逻辑。 | | **冲突检测** | **Lodash (基础) \+ Dice Coefficient (进阶)** | 用于比对两个 JSON 对象的字段差异。文本相似度可使用简单的 Dice 系数或 Levenshtein 距离,暂不需要重型向量库。 | | **数据库** | **PostgreSQL 15** | 存储 JSONB 格式的双模型结果。 | | **前端交互** | **React \+ TanStack Table** | V2 改为全景网格,数据量大时需要 TanStack Table (Headless) 配合虚拟滚动。 | ## **3\. 核心流程设计 (Core Logic)** ### **3.1 智能体检 (Health Check Logic)** * **触发时机:** 用户在前端选择“文本列”的瞬间。 * **执行逻辑:** 1. 后端读取该列的前 100 行(不读全量)。 2. 计算统计指标: * emptyRate: 空值 / 总行数。 * avgLength: 非空行的平均字符数。 3. **拦截策略:** 若 emptyRate \> 0.8 或 avgLength \< 10,返回 status: 'BAD'。 4. **Token 预估:** totalRows \* avgLength \* 1.5 (粗略估算)。 ### **3.2 双盲提取与交叉验证 (Double-Blind & Validation)** 这是 V2 的心脏。 #### **A. 提示词工程 (Prompt Engineering)** 为了方便比对,必须强制两个模型输出**完全一致的 JSON 结构**。 * **System Prompt:** "You are a medical structural extraction assistant..." * **Constraint:** "Output strictly in JSON format. Keys must be: \['tumor\_size', 'lymph\_node', ...\]." * **Temperature:** 设为 0,追求最大确定性。 #### **B. 交叉验证算法 (The Judge)** 当 Model A (DeepSeek) 和 Model B (Qwen) 返回结果后,执行比对: function validate(jsonA, jsonB) { const conflicts \= \[\]; const keys \= Object.keys(jsonA); for (const key of keys) { const valA \= normalize(jsonA\[key\]); // 归一化:去除空格、转小写、半角化 const valB \= normalize(jsonB\[key\]); // 1\. 精确匹配 if (valA \=== valB) continue; // 2\. 数值归一化匹配 (如 "3cm" vs "3.0cm") if (isNumber(valA) && isNumber(valB) && parse(valA) \=== parse(valB)) continue; // 3\. (可选) 语义相似度匹配 // if (similarity(valA, valB) \> 0.95) continue; conflicts.push(key); } return conflicts.length \=== 0 ? 'CLEAN' : 'CONFLICT'; } ## **4\. 数据库设计 (Database Schema)** V2 需要存储两份 AI 结果以及用户的裁决结果。 ### **Prisma Schema 更新** // 任务表 model ExtractionJob { id String @id @default(uuid()) // ...其他字段 diseaseType String // 疾病类型 (肺癌) reportType String // 报告类型 (病理) targetFields Json // 目标字段定义 \[{name: "肿瘤大小", desc: "..."}\] } // 单行记录表 model ExtractionItem { id String @id @default(uuid()) jobId String originalText String @db.Text // V2 核心字段 resultA Json? // DeepSeek 结果 { "size": "3cm" } resultB Json? // Qwen 结果 { "size": "3.0 cm" } // 冲突检测结果 status ItemStatus // PENDING, CLEAN, CONFLICT, RESOLVED conflictFields String\[\] // \["size"\] 记录哪些字段冲突了 // 最终采纳结果 (用户裁决后写入,或者一致时自动写入) finalResult Json? } ## **5\. 接口设计 (API Endpoints)** ### **5.1 模版与配置** * GET /api/templates: 获取预设的疾病和报告模版列表。 * POST /api/jobs: 创建任务,Payload 中需包含 diseaseType 和 reportType,便于后端组装 Prompt。 ### **5.2 网格验证 (Grid Verification)** * GET /api/jobs/:id/rows: 分页获取验证数据。 * **Response:** 返回 originalText, resultA, resultB, conflictFields。 * POST /api/items/:id/resolve: 单行裁决。 * **Payload:** { field: "tumor\_size", chosenValue: "3cm" }。 * **Logic:** 更新 finalResult,如果该行所有冲突字段都已解决,将 status 更新为 RESOLVED。 ## **6\. 前端详细设计 (Frontend)** ### **6.1 全景验证网格 (Verification Grid)** * **组件选型:** 依然推荐 **TanStack Table** (逻辑层) \+ **UI 组件库** (渲染层)。 * **冲突单元格渲染:** * 当 conflictFields.includes(column.id) 时,单元格渲染为**对比模式**。 * 显示两个小按钮:\[DS: 3cm\] 和 \[QW: 3.0cm\]。 * 用户点击任一按钮,触发 resolve API,前端乐观更新(Optimistic Update)为选中状态。 ### **6.2 侧边栏原文 (Context Drawer)** * **触发:** 点击表格行的空白处或“查看原文”图标。 * **功能:** 展示 originalText。 * **高亮优化:** 简单实现 String.indexOf 查找当前字段的值并标黄。 ## **7\. 风险控制与性能优化** | 潜在风险 | 解决方案 | | :---- | :---- | | **双倍 Token 成本** | 1\. 默认使用 DeepSeek (极低成本) \+ Qwen (低成本) 组合。 2\. 在“体检”阶段严格拦截无效数据。 | | **处理速度慢** | 两个模型必须 **并发调用 (Promise.all)**,而不是串行。整体耗时取决于最慢的那个模型。 | | **模型格式不听话** | Prompt 中增加 Few-Shot (少样本) 示例,明确展示 JSON 格式。如果 JSON 解析失败,自动重试 1 次。 | | **前端网格卡顿** | 如果数据超过 1000 条,开启 Virtual Scrolling (虚拟滚动)。 |