feat(iit): Implement real-time quality control system
Summary: - Add 4 new database tables: iit_field_metadata, iit_qc_logs, iit_record_summary, iit_qc_project_stats - Implement pg-boss debounce mechanism in WebhookController - Refactor QC Worker for dual output: QC logs + record summary - Enhance HardRuleEngine to support form-based rule filtering - Create QcService for QC data queries - Optimize ChatService with new intents: query_enrollment, query_qc_status - Add admin batch operations: one-click full QC + one-click full summary - Create IIT Admin management module: project config, QC rules, user mapping Status: Code complete, pending end-to-end testing Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
# IIT Manager Agent模块 - 当前状态与开发指南
|
||||
|
||||
> **文档版本:** v1.6
|
||||
> **文档版本:** v2.1
|
||||
> **创建日期:** 2026-01-01
|
||||
> **维护者:** IIT Manager开发团队
|
||||
> **最后更新:** 2026-01-04 🎉 **Dify知识库集成完成 - 混合检索实现!**
|
||||
> **重大里程碑:** ✅ 混合检索架构实现(REDCap实时数据 + Dify文档知识库)!
|
||||
> **最后更新:** 2026-02-07 🎉 **实时质控系统核心功能开发完成!**
|
||||
> **重大里程碑:**
|
||||
> - ✅ 2026-02-07:**实时质控系统开发完成**(pg-boss防抖 + 质控日志 + 录入汇总 + 管理端批量操作)
|
||||
> - ✅ 2026-02-05:**V2.9.1 完整开发计划发布**(双脑架构 + 三层记忆 + 主动性增强 + 隐私合规)
|
||||
> - ✅ 2026-02-02:**REDCap 生产环境部署完成**(ECS + RDS + HTTPS)
|
||||
> - ✅ 2026-01-04:混合检索架构实现(REDCap实时数据 + Dify文档知识库)
|
||||
> **文档目的:** 反映模块真实状态,记录开发历程
|
||||
|
||||
---
|
||||
@@ -27,70 +31,65 @@
|
||||
IIT Manager Agent(研究者发起试验管理助手)是一个基于企业微信的主动式AI Agent产品,为IIT(研究者发起试验)提供智能化管理能力。
|
||||
|
||||
### 架构设计
|
||||
- **核心理念**:Native Orchestration(原生编排)+ Dify RAG + Shadow State(影子状态)
|
||||
- **核心理念**:**双脑架构(V2.9.1)** - SOP 状态机 + ReAct 引擎 + 三层记忆 + 隐私合规
|
||||
- **双脑路由**:
|
||||
- **左脑(SOP 引擎)**:结构化任务执行,写操作必经
|
||||
- **右脑(ReAct 引擎)**:模糊查询,只读不写
|
||||
- **技术栈**:
|
||||
- 后端:Node.js (Fastify) + PostgreSQL (Prisma) + pg-boss
|
||||
- 前端:微信小程序 (Taro 4.x)
|
||||
- 前端:企业微信 + 微信小程序 (Taro 4.x)
|
||||
- 数据源:REDCap (EDC系统)
|
||||
- 通知:企业微信
|
||||
- AI能力:Dify RAG + DeepSeek/Qwen
|
||||
- AI能力:DeepSeek/Qwen + 自研 RAG
|
||||
|
||||
### 当前状态
|
||||
- **开发阶段**:🎉 **Phase 1.5完成 - 混合检索架构实现!**
|
||||
- **整体完成度**:65%(Day 1-3 + Phase 1.5完成 + Dify集成完成)
|
||||
- **已完成功能**:
|
||||
- ✅ 数据库Schema创建(iit_schema,5个表)
|
||||
- ✅ Prisma Schema编写(223行类型定义)
|
||||
- ✅ 模块目录结构创建
|
||||
- ✅ 企业微信应用注册和配置
|
||||
- ✅ 企业微信Access Token获取成功
|
||||
- ✅ **企业微信可信域名配置成功**(devlocal.xunzhengyixue.com)
|
||||
- ✅ **REDCap本地Docker环境部署成功**(15.8.0)
|
||||
- ✅ **REDCap对接技术方案确定**(DET + REST API)
|
||||
- ✅ **REDCap测试项目创建**(test0102, PID 16, 11条记录)
|
||||
- ✅ **REDCap实时集成完成**(DET + REST API + WebhookController + SyncManager)
|
||||
- ✅ **企业微信推送服务完成**(WechatService, 314行)
|
||||
- ✅ **企业微信回调处理完成**(WechatCallbackController, 501行)
|
||||
- ✅ **natapp内网穿透配置成功**(https://devlocal.xunzhengyixue.com + 公司备案域名)
|
||||
- ✅ **RedcapAdapter API适配器完成**(271行,7个API方法)
|
||||
- ✅ **WebhookController完成**(327行,<10ms响应)
|
||||
- ✅ **SyncManager完成**(398行,增量+全量同步)
|
||||
- ✅ **Worker注册完成**(iit_quality_check, iit_redcap_poll)
|
||||
- ✅ **质控Worker完善**(质控逻辑 + 企业微信推送 + 审计日志)
|
||||
- ✅ **🎯 端到端测试通过**(REDCap → Node.js → 企业微信,<2秒延迟)
|
||||
- ✅ **🎯 MVP闭环完全打通**(100%消息成功率)
|
||||
- ✅ **🚀 Phase 1.5: AI对话集成完成**(2026-01-03 & 2026-01-04)
|
||||
- ✅ ChatService集成(485行)
|
||||
- ✅ SessionMemory(120行,上下文记忆)
|
||||
- ✅ REDCap数据查询集成(queryRedcapRecord, countRedcapRecords)
|
||||
- ✅ **Dify知识库集成**(queryDifyKnowledge)
|
||||
- ✅ **混合检索实现**(REDCap实时数据 + Dify文档知识库)
|
||||
- ✅ **智能路由**(根据意图自动选择数据源)
|
||||
- ✅ 意图识别优化(扩充医学关键词库)
|
||||
- ✅ 数据注入LLM(基于真实数据,不编造)
|
||||
- ✅ 即时反馈("正在查询")
|
||||
- ✅ **Dify Dataset关联**(test0102 → b49595b2-bf71-4e47-9988-4aa2816d3c6f)
|
||||
- ✅ **Bug修复**(Dify API字段路径错误修正)
|
||||
- ✅ 测试通过(5个场景:REDCap查询、Dify文档查询、混合查询)
|
||||
- **未开发功能**:
|
||||
- ⏳ Function Calling(LLM自主决策)- Phase 2
|
||||
- ⏳ 多项目支持(项目切换)- Phase 2
|
||||
- ⏳ 文档API上传(自动化上传到Dify)- Phase 2
|
||||
- ⏳ 数据质量Agent(AI质控逻辑)- Phase 2
|
||||
- ⏳ 任务驱动引擎 - Phase 2
|
||||
- ⏳ 患者随访Agent - Phase 2
|
||||
- ⏳ 微信小程序前端 - Phase 3
|
||||
- ⏳ REDCap双向回写 - Phase 2
|
||||
- **部署状态**:✅ AI对话正常运行,支持REDCap实时数据 + Dify文档查询
|
||||
- **开发阶段**:🎉 **实时质控系统核心功能开发完成!待端到端测试**
|
||||
- **整体完成度**:
|
||||
- **基础设施**:85%(REDCap + 企业微信 + AI 对话 + 实时质控)
|
||||
- **架构设计**:100%(V2.9.1 完整开发计划发布)
|
||||
- **代码实现**:45%(实时质控系统已实现)
|
||||
|
||||
#### ✅ 已完成功能(基础设施)
|
||||
- ✅ 数据库Schema创建(iit_schema,9个表 = 原5个 + 新增4个质控表)
|
||||
- ✅ Prisma Schema编写(扩展至 ~350 行类型定义)
|
||||
- ✅ 企业微信应用注册和配置
|
||||
- ✅ **REDCap 生产环境部署完成**(ECS + RDS + HTTPS)
|
||||
- ✅ **REDCap实时集成完成**(DET + REST API)
|
||||
- ✅ **企业微信推送服务完成**(WechatService)
|
||||
- ✅ **端到端测试通过**(REDCap → Node.js → 企业微信)
|
||||
- ✅ **AI对话集成完成**(ChatService + SessionMemory)
|
||||
|
||||
#### ✅ 已完成功能(实时质控系统 - 2026-02-07)
|
||||
- ✅ **质控数据库表**(iit_qc_logs + iit_record_summary + iit_qc_project_stats + iit_field_metadata)
|
||||
- ✅ **pg-boss 防抖机制**(WebhookController + singletonKey)
|
||||
- ✅ **Worker 双产出**(质控日志 + 录入汇总,一次执行两个产出)
|
||||
- ✅ **HardRuleEngine 增强**(按表单过滤规则)
|
||||
- ✅ **QcService 创建**(质控查询服务,6 个核心方法)
|
||||
- ✅ **ChatService 优化**(优先查询汇总表/质控表,新增录入/质控意图识别)
|
||||
- ✅ **管理端批量操作**(一键全量质控 + 一键全量汇总,前后端完整实现)
|
||||
|
||||
#### ✅ 已完成功能(架构设计 V2.9.1)
|
||||
- ✅ **双脑架构设计**(SOP 状态机 + ReAct 引擎)
|
||||
- ✅ **三层记忆系统设计**(流水账 + 热记忆 + 历史书)
|
||||
- ✅ **主动性增强设计**(Cron Skill + 用户画像 + 反馈循环)
|
||||
- ✅ **隐私合规设计**(PII 脱敏中间件 + 审计日志)
|
||||
- ✅ **自动化工具设计**(AutoMapper REDCap Schema 对齐)
|
||||
- ✅ **模块化开发文档**(6 份专项文档)
|
||||
|
||||
#### ⏳ 待实施功能(按 Phase 规划)
|
||||
| Phase | 内容 | 优先级 | 状态 |
|
||||
|-------|------|--------|------|
|
||||
| **Phase 1** | 基础工具层(ToolsService + HardRuleEngine + AutoMapper) | P0 | ✅ 部分完成(HardRuleEngine 已实现) |
|
||||
| **Phase 1.5** | 隐私安全(AnonymizerService + PII 脱敏) | **P0 合规必需** | 待开始 |
|
||||
| **Phase 2** | 实时质控系统(QC Worker + QcService + 批量操作) | P0 | ✅ **已完成** |
|
||||
| **Phase 3** | ReAct 引擎 + 流水账(ReActEngine + 反馈循环) | P0 | 待开始 |
|
||||
| **Phase 4** | 调度系统(SchedulerService + Cron Skill + ProfilerService) | P1 | 待开始 |
|
||||
| **Phase 5** | 智能路由(IntentService + 多意图处理) | P1 | 待开始 |
|
||||
| **Phase 6** | 视觉能力(VisionService) | 延后 | 待开始 |
|
||||
|
||||
- **部署状态**:✅ REDCap 生产环境运行中(https://redcap.xunzhengyixue.com/)
|
||||
- **已知问题**:无
|
||||
- **临时措施**:
|
||||
- ⚠️ 使用关键词匹配识别意图(Phase 2升级为Function Calling)
|
||||
- ⚠️ SessionMemory基于内存(Phase 2改为Redis)
|
||||
- ⚠️ 默认查询第一个active项目(Phase 2支持项目选择)
|
||||
- ⚠️ Dify文档通过Web界面手动上传(Phase 2开发API自动上传)
|
||||
- ⚠️ 单项目单知识库(Phase 2支持多知识库)
|
||||
- ⚠️ UserID从环境变量获取(`WECHAT_TEST_USER_ID`)- Phase 2改进
|
||||
- ⚠️ 定时轮询暂时禁用(REDCap DET已足够)- Phase 2添加
|
||||
- **开发计划文档**:[V2.9.1 综合开发计划](./04-开发计划/IIT%20Manager%20Agent%20V2.6%20综合开发计划.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -105,6 +104,8 @@ IIT Manager Agent(研究者发起试验管理助手)是一个基于企业微
|
||||
| **Day 2:REDCap拉取** | ✅ 已完成 | 2026-01-02 | RedcapAdapter(271行) + WebhookController(327行) + SyncManager(398行) |
|
||||
| **Day 3:企微推送** | ✅ 已完成 | 2026-01-03 | WechatService(314行) + WechatCallbackController(501行) + 质控Worker |
|
||||
| **Phase 1.5:AI对话** | ✅ 已完成 | 2026-01-03 & 2026-01-04 | ChatService(485行) + SessionMemory(120行) + Dify集成 |
|
||||
| **IIT 管理端配置** | ✅ 已完成 | 2026-02-07 | 运营管理端 IIT 项目管理(REDCap配置 + 质控规则 + 用户映射 + 知识库) |
|
||||
| **实时质控系统** | ✅ 已完成 | 2026-02-07 | 4个新表 + pg-boss防抖 + Worker双产出 + QcService + 管理端批量操作 |
|
||||
| **Day 6-7:小程序** | ⏳ 待开始 | - | Taro前端 + 审批界面 |
|
||||
| **Day 8:回写+集成** | ⏳ 待开始 | - | REDCap回写 + 端到端测试 |
|
||||
| **Day 9-10:完善+测试** | ⏳ 待开始 | - | 错误处理 + 日志 + 性能优化 |
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
> **更新日期:** 2026-02-05
|
||||
> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md)
|
||||
>
|
||||
> **V2.9 更新**:
|
||||
> **V2.9.1 更新**:
|
||||
> - 扩展 `iit_skills` 表支持 Cron Skill(主动提醒)
|
||||
> - 扩展 `iit_conversation_history` 表增加反馈字段
|
||||
> - 更新 `iit_project_memory` 内容结构(用户画像)
|
||||
> - **新增 `iit_pii_audit_log` 表**:PII 脱敏审计日志(合规必需)
|
||||
|
||||
---
|
||||
|
||||
@@ -17,6 +18,7 @@
|
||||
|------|------|-------|--------|
|
||||
| `iit_skills` | Skill 配置存储 | 1 | P0 |
|
||||
| `iit_field_mapping` | 字段名映射字典 | 1 | P0 |
|
||||
| `iit_pii_audit_log` | PII 脱敏审计日志 | 1.5 | P0 |
|
||||
| `iit_task_run` | SOP 任务执行记录 | 2 | P0 |
|
||||
| `iit_pending_actions` | 待处理的违规记录 | 2 | P0 |
|
||||
| `iit_conversation_history` | 对话历史(流水账) | 2 | P1 |
|
||||
@@ -159,6 +161,64 @@ INSERT INTO iit_field_mapping (project_id, alias_name, actual_name) VALUES
|
||||
|
||||
---
|
||||
|
||||
## 2.5 Phase 1.5:隐私安全表(P0 合规必需)
|
||||
|
||||
### 2.5.1 iit_pii_audit_log - PII 脱敏审计日志
|
||||
|
||||
> **重要**:临床数据包含大量患者隐私信息(姓名、身份证、手机号),在调用第三方 LLM 之前**必须脱敏**。
|
||||
> 此表用于存储脱敏记录,便于事后合规审计。
|
||||
|
||||
```prisma
|
||||
model IitPiiAuditLog {
|
||||
id String @id @default(uuid())
|
||||
projectId String
|
||||
userId String // 操作者
|
||||
sessionId String // 会话 ID(关联 conversation_history)
|
||||
|
||||
// 脱敏内容(加密存储)
|
||||
originalHash String // 原始内容的 SHA256 哈希(不存明文)
|
||||
maskedPayload String @db.Text // 脱敏后发送给 LLM 的内容
|
||||
maskingMap String @db.Text // 加密存储的映射表 { "[PATIENT_1]": "张三", ... }
|
||||
|
||||
// 元数据
|
||||
piiCount Int // 检测到的 PII 数量
|
||||
piiTypes String[] // 检测到的 PII 类型 ['name', 'id_card', 'phone']
|
||||
llmProvider String // 'qwen' | 'deepseek' | 'openai'
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([projectId, userId])
|
||||
@@index([sessionId])
|
||||
@@index([createdAt])
|
||||
@@map("iit_pii_audit_log")
|
||||
@@schema("iit_schema")
|
||||
}
|
||||
```
|
||||
|
||||
**PII 类型说明**:
|
||||
|
||||
| PII 类型 | 正则模式 | 脱敏示例 |
|
||||
|----------|----------|----------|
|
||||
| `name` | 中文姓名(2-4字) | 张三 → [PATIENT_1] |
|
||||
| `id_card` | 身份证号(18位) | 420101... → [ID_CARD_1] |
|
||||
| `phone` | 手机号(11位) | 13800138000 → [PHONE_1] |
|
||||
| `mrn` | 病历号 | MRN123456 → [MRN_1] |
|
||||
|
||||
**脱敏流程**:
|
||||
|
||||
```
|
||||
用户输入: "张三(身份证420101199001011234)今天血压偏高"
|
||||
↓ AnonymizerService.mask()
|
||||
LLM 收到: "[PATIENT_1](身份证[ID_CARD_1])今天血压偏高"
|
||||
↓ 同时写入 iit_pii_audit_log
|
||||
↓ LLM 处理
|
||||
LLM 返回: "[PATIENT_1] 的血压需要关注..."
|
||||
↓ AnonymizerService.unmask()
|
||||
用户看到: "张三 的血压需要关注..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 2:SOP 执行与记忆表
|
||||
|
||||
### 3.1 iit_task_run - SOP 任务执行记录
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
> **更新日期:** 2026-02-05
|
||||
> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md)
|
||||
>
|
||||
> **V2.9 更新**:
|
||||
> **V2.9.1 更新**:
|
||||
> - 新增 `ProfilerService` 用户画像服务
|
||||
> - `ChatService` 增加反馈循环功能
|
||||
> - `SchedulerService` 支持 Cron Skill 触发
|
||||
> - **新增 `AnonymizerService`**:PII 脱敏中间件(P0 合规必需)
|
||||
> - **新增 `AutoMapperService`**:REDCap Schema 自动对齐工具
|
||||
|
||||
---
|
||||
|
||||
@@ -16,6 +18,8 @@
|
||||
| 服务 | 职责 | Phase |
|
||||
|------|------|-------|
|
||||
| `ToolsService` | 统一工具管理(字段映射 + 执行) | 1 |
|
||||
| `AnonymizerService` | **PII 脱敏中间件(P0 合规必需)** | 1.5 |
|
||||
| `AutoMapperService` | **REDCap Schema 自动对齐** | 1 |
|
||||
| `ChatService` | 消息路由(双脑入口)+ 反馈收集 | 2 |
|
||||
| `IntentService` | 意图识别(混合路由) | 5 |
|
||||
| `MemoryService` | 记忆管理(V2.8 架构) | 2-3 |
|
||||
@@ -315,6 +319,460 @@ export class ToolsService {
|
||||
|
||||
---
|
||||
|
||||
## 2.5 AnonymizerService - PII 脱敏中间件(P0 合规必需)
|
||||
|
||||
> **文件路径**: `backend/src/modules/iit-manager/services/AnonymizerService.ts`
|
||||
>
|
||||
> **⚠️ 重要**:临床数据包含大量患者隐私信息,在调用第三方 LLM 之前**必须脱敏**!
|
||||
|
||||
### 2.5.1 核心职责
|
||||
|
||||
- 识别文本中的 PII(个人身份信息)
|
||||
- 发送 LLM 前脱敏(Masking)
|
||||
- 接收 LLM 回复后还原(Unmasking)
|
||||
- 记录脱敏审计日志
|
||||
|
||||
### 2.5.2 PII 识别正则库
|
||||
|
||||
```typescript
|
||||
// PII 类型定义
|
||||
const PII_PATTERNS = {
|
||||
// 中文姓名(2-4字,排除常见非姓名词)
|
||||
name: /(?<![a-zA-Z\u4e00-\u9fa5])[\u4e00-\u9fa5]{2,4}(?![a-zA-Z\u4e00-\u9fa5])/g,
|
||||
|
||||
// 身份证号(18位)
|
||||
id_card: /\d{6}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]/g,
|
||||
|
||||
// 手机号(11位,1开头)
|
||||
phone: /1[3-9]\d{9}/g,
|
||||
|
||||
// 病历号(字母+数字组合)
|
||||
mrn: /(?:MRN|HN|病案号)[::\s]?([A-Z0-9]{6,12})/gi
|
||||
};
|
||||
|
||||
// 非姓名词排除列表(提高准确率)
|
||||
const NAME_EXCLUSIONS = [
|
||||
'患者', '医生', '护士', '主任', '教授', '方案', '访视',
|
||||
'入组', '排除', '标准', '剂量', '疗程', '周期', '疗效'
|
||||
];
|
||||
```
|
||||
|
||||
### 2.5.3 完整实现
|
||||
|
||||
```typescript
|
||||
import { prisma } from '../../common/prisma';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
interface MaskingResult {
|
||||
maskedText: string;
|
||||
maskingMap: Record<string, string>; // { "[PATIENT_1]": "张三" }
|
||||
piiCount: number;
|
||||
piiTypes: string[];
|
||||
}
|
||||
|
||||
interface UnmaskingContext {
|
||||
maskingMap: Record<string, string>;
|
||||
}
|
||||
|
||||
export class AnonymizerService {
|
||||
private encryptionKey: string;
|
||||
|
||||
constructor() {
|
||||
this.encryptionKey = process.env.PII_ENCRYPTION_KEY || 'default-key-change-me';
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏:发送 LLM 前调用
|
||||
*/
|
||||
async mask(
|
||||
text: string,
|
||||
context: { projectId: string; userId: string; sessionId: string }
|
||||
): Promise<MaskingResult> {
|
||||
const maskingMap: Record<string, string> = {};
|
||||
const piiTypes: string[] = [];
|
||||
let maskedText = text;
|
||||
let counter = { name: 0, id_card: 0, phone: 0, mrn: 0 };
|
||||
|
||||
// 按优先级处理(先处理身份证,再处理姓名,避免误识别)
|
||||
|
||||
// 1. 身份证号
|
||||
maskedText = maskedText.replace(PII_PATTERNS.id_card, (match) => {
|
||||
counter.id_card++;
|
||||
const placeholder = `[ID_CARD_${counter.id_card}]`;
|
||||
maskingMap[placeholder] = match;
|
||||
if (!piiTypes.includes('id_card')) piiTypes.push('id_card');
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 2. 手机号
|
||||
maskedText = maskedText.replace(PII_PATTERNS.phone, (match) => {
|
||||
counter.phone++;
|
||||
const placeholder = `[PHONE_${counter.phone}]`;
|
||||
maskingMap[placeholder] = match;
|
||||
if (!piiTypes.includes('phone')) piiTypes.push('phone');
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 3. 病历号
|
||||
maskedText = maskedText.replace(PII_PATTERNS.mrn, (match, mrn) => {
|
||||
counter.mrn++;
|
||||
const placeholder = `[MRN_${counter.mrn}]`;
|
||||
maskingMap[placeholder] = mrn;
|
||||
if (!piiTypes.includes('mrn')) piiTypes.push('mrn');
|
||||
return placeholder.padEnd(match.length);
|
||||
});
|
||||
|
||||
// 4. 中文姓名(需要更精细的判断)
|
||||
maskedText = maskedText.replace(PII_PATTERNS.name, (match) => {
|
||||
// 排除非姓名词
|
||||
if (NAME_EXCLUSIONS.includes(match)) return match;
|
||||
// 排除已被其他规则处理的部分
|
||||
if (Object.values(maskingMap).includes(match)) return match;
|
||||
|
||||
counter.name++;
|
||||
const placeholder = `[PATIENT_${counter.name}]`;
|
||||
maskingMap[placeholder] = match;
|
||||
if (!piiTypes.includes('name')) piiTypes.push('name');
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
const piiCount = Object.keys(maskingMap).length;
|
||||
|
||||
// 记录审计日志
|
||||
if (piiCount > 0) {
|
||||
await this.saveAuditLog({
|
||||
projectId: context.projectId,
|
||||
userId: context.userId,
|
||||
sessionId: context.sessionId,
|
||||
originalHash: this.hashText(text),
|
||||
maskedPayload: maskedText,
|
||||
maskingMap: this.encrypt(JSON.stringify(maskingMap)),
|
||||
piiCount,
|
||||
piiTypes
|
||||
});
|
||||
}
|
||||
|
||||
return { maskedText, maskingMap, piiCount, piiTypes };
|
||||
}
|
||||
|
||||
/**
|
||||
* 还原:接收 LLM 回复后调用
|
||||
*/
|
||||
unmask(text: string, context: UnmaskingContext): string {
|
||||
let result = text;
|
||||
|
||||
// 将占位符替换回原始值
|
||||
for (const [placeholder, original] of Object.entries(context.maskingMap)) {
|
||||
result = result.replace(new RegExp(this.escapeRegex(placeholder), 'g'), original);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== 辅助方法 =====
|
||||
|
||||
private hashText(text: string): string {
|
||||
return crypto.createHash('sha256').update(text).digest('hex');
|
||||
}
|
||||
|
||||
private encrypt(text: string): string {
|
||||
const cipher = crypto.createCipheriv(
|
||||
'aes-256-gcm',
|
||||
crypto.scryptSync(this.encryptionKey, 'salt', 32),
|
||||
crypto.randomBytes(16)
|
||||
);
|
||||
return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
|
||||
}
|
||||
|
||||
private escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
private async saveAuditLog(data: {
|
||||
projectId: string;
|
||||
userId: string;
|
||||
sessionId: string;
|
||||
originalHash: string;
|
||||
maskedPayload: string;
|
||||
maskingMap: string;
|
||||
piiCount: number;
|
||||
piiTypes: string[];
|
||||
}): Promise<void> {
|
||||
await prisma.iitPiiAuditLog.create({
|
||||
data: {
|
||||
...data,
|
||||
llmProvider: process.env.LLM_PROVIDER || 'qwen'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5.4 集成到 ChatService
|
||||
|
||||
```typescript
|
||||
// ChatService.ts 中的使用
|
||||
export class ChatService {
|
||||
private anonymizer: AnonymizerService;
|
||||
|
||||
async handleMessage(userId: string, message: string): Promise<string> {
|
||||
const projectId = await this.getUserProject(userId);
|
||||
const sessionId = this.sessionMemory.getSessionId(userId);
|
||||
|
||||
// ⚠️ 调用 LLM 前脱敏
|
||||
const { maskedText, maskingMap, piiCount } = await this.anonymizer.mask(
|
||||
message,
|
||||
{ projectId, userId, sessionId }
|
||||
);
|
||||
|
||||
if (piiCount > 0) {
|
||||
console.log(`[Anonymizer] 检测到 ${piiCount} 个 PII,已脱敏`);
|
||||
}
|
||||
|
||||
// 使用脱敏后的文本调用 LLM
|
||||
const llmResponse = await this.llm.chat(maskedText, ...);
|
||||
|
||||
// ⚠️ 收到 LLM 回复后还原
|
||||
const unmaskedResponse = this.anonymizer.unmask(llmResponse, { maskingMap });
|
||||
|
||||
return unmaskedResponse;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2.6 AutoMapperService - REDCap Schema 自动对齐
|
||||
|
||||
> **文件路径**: `backend/src/modules/iit-manager/services/AutoMapperService.ts`
|
||||
>
|
||||
> **目的**:大幅减少 `iit_field_mapping` 表的人工配置工作量
|
||||
|
||||
### 2.6.1 核心职责
|
||||
|
||||
- 解析 REDCap Data Dictionary(CSV/JSON)
|
||||
- 使用 LLM 进行语义映射
|
||||
- 提供管理后台确认界面
|
||||
|
||||
### 2.6.2 完整实现
|
||||
|
||||
```typescript
|
||||
import { parse } from 'papaparse';
|
||||
import { LLMFactory } from '../../common/llm/adapters/LLMFactory';
|
||||
import { prisma } from '../../common/prisma';
|
||||
|
||||
interface FieldDefinition {
|
||||
variableName: string;
|
||||
fieldLabel: string;
|
||||
fieldType: string;
|
||||
choices?: string;
|
||||
}
|
||||
|
||||
interface MappingSuggestion {
|
||||
redcapField: string;
|
||||
redcapLabel: string;
|
||||
suggestedAlias: string[];
|
||||
confidence: number;
|
||||
status: 'pending' | 'confirmed' | 'rejected';
|
||||
}
|
||||
|
||||
export class AutoMapperService {
|
||||
private llm = LLMFactory.create('qwen');
|
||||
|
||||
// 系统标准字段列表
|
||||
private readonly STANDARD_FIELDS = [
|
||||
{ name: 'age', aliases: ['年龄', 'age', '岁数'] },
|
||||
{ name: 'gender', aliases: ['性别', 'sex', 'gender', '男女'] },
|
||||
{ name: 'ecog', aliases: ['ECOG', 'PS评分', '体力状态'] },
|
||||
{ name: 'visit_date', aliases: ['访视日期', '就诊日期', 'visit date'] },
|
||||
{ name: 'height', aliases: ['身高', 'height', 'ht'] },
|
||||
{ name: 'weight', aliases: ['体重', 'weight', 'wt'] },
|
||||
{ name: 'bmi', aliases: ['BMI', '体质指数'] },
|
||||
{ name: 'consent_date', aliases: ['知情同意日期', 'ICF日期', 'consent date'] },
|
||||
{ name: 'enrollment_date', aliases: ['入组日期', 'enrollment date', '入选日期'] }
|
||||
];
|
||||
|
||||
/**
|
||||
* 解析 REDCap Data Dictionary
|
||||
*/
|
||||
async parseDataDictionary(fileContent: string, format: 'csv' | 'json'): Promise<FieldDefinition[]> {
|
||||
if (format === 'csv') {
|
||||
const result = parse(fileContent, { header: true });
|
||||
return result.data.map((row: any) => ({
|
||||
variableName: row['Variable / Field Name'] || row['variable_name'],
|
||||
fieldLabel: row['Field Label'] || row['field_label'],
|
||||
fieldType: row['Field Type'] || row['field_type'],
|
||||
choices: row['Choices, Calculations, OR Slider Labels'] || row['choices']
|
||||
}));
|
||||
} else {
|
||||
return JSON.parse(fileContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 LLM 生成映射建议
|
||||
*/
|
||||
async generateMappingSuggestions(
|
||||
projectId: string,
|
||||
fields: FieldDefinition[]
|
||||
): Promise<MappingSuggestion[]> {
|
||||
const prompt = `你是一个临床研究数据专家。请将以下 REDCap 字段与系统标准字段进行语义匹配。
|
||||
|
||||
## 系统标准字段
|
||||
${this.STANDARD_FIELDS.map(f => `- ${f.name}: ${f.aliases.join(', ')}`).join('\n')}
|
||||
|
||||
## REDCap 字段列表
|
||||
${fields.slice(0, 50).map(f => `- ${f.variableName}: ${f.fieldLabel}`).join('\n')}
|
||||
|
||||
请返回 JSON 格式的映射建议:
|
||||
{
|
||||
"mappings": [
|
||||
{
|
||||
"redcapField": "nl_age",
|
||||
"suggestedAlias": ["age", "年龄"],
|
||||
"confidence": 0.95
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
注意:
|
||||
1. 只返回有把握的映射(confidence >= 0.7)
|
||||
2. 如果不确定,不要强行映射
|
||||
3. 一个 REDCap 字段可以有多个别名`;
|
||||
|
||||
const response = await this.llm.chat([
|
||||
{ role: 'user', content: prompt }
|
||||
]);
|
||||
|
||||
try {
|
||||
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
||||
const result = JSON.parse(jsonMatch?.[0] || '{"mappings":[]}');
|
||||
|
||||
return result.mappings.map((m: any) => ({
|
||||
redcapField: m.redcapField,
|
||||
redcapLabel: fields.find(f => f.variableName === m.redcapField)?.fieldLabel || '',
|
||||
suggestedAlias: m.suggestedAlias,
|
||||
confidence: m.confidence,
|
||||
status: 'pending' as const
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('[AutoMapper] LLM 返回解析失败', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量确认映射
|
||||
*/
|
||||
async confirmMappings(
|
||||
projectId: string,
|
||||
confirmations: Array<{
|
||||
redcapField: string;
|
||||
aliases: string[];
|
||||
confirmed: boolean;
|
||||
}>
|
||||
): Promise<{ created: number; skipped: number }> {
|
||||
let created = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const conf of confirmations) {
|
||||
if (!conf.confirmed) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const alias of conf.aliases) {
|
||||
try {
|
||||
await prisma.iitFieldMapping.upsert({
|
||||
where: {
|
||||
projectId_aliasName: { projectId, aliasName: alias }
|
||||
},
|
||||
create: {
|
||||
projectId,
|
||||
aliasName: alias,
|
||||
actualName: conf.redcapField
|
||||
},
|
||||
update: {
|
||||
actualName: conf.redcapField
|
||||
}
|
||||
});
|
||||
created++;
|
||||
} catch (e) {
|
||||
console.error(`[AutoMapper] 创建映射失败: ${alias} -> ${conf.redcapField}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { created, skipped };
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键导入流程
|
||||
*/
|
||||
async autoImport(
|
||||
projectId: string,
|
||||
fileContent: string,
|
||||
format: 'csv' | 'json'
|
||||
): Promise<{
|
||||
suggestions: MappingSuggestion[];
|
||||
message: string;
|
||||
}> {
|
||||
// 1. 解析 Data Dictionary
|
||||
const fields = await this.parseDataDictionary(fileContent, format);
|
||||
console.log(`[AutoMapper] 解析到 ${fields.length} 个字段`);
|
||||
|
||||
// 2. 生成 LLM 建议
|
||||
const suggestions = await this.generateMappingSuggestions(projectId, fields);
|
||||
console.log(`[AutoMapper] 生成 ${suggestions.length} 个映射建议`);
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
message: `已解析 ${fields.length} 个 REDCap 字段,生成 ${suggestions.length} 个映射建议,请在管理后台确认。`
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6.3 管理后台 API
|
||||
|
||||
```typescript
|
||||
// routes/autoMapperRoutes.ts
|
||||
|
||||
router.post('/auto-mapper/import', async (req, res) => {
|
||||
const { projectId, fileContent, format } = req.body;
|
||||
|
||||
const result = await autoMapperService.autoImport(projectId, fileContent, format);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
suggestions: result.suggestions,
|
||||
message: result.message
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/auto-mapper/confirm', async (req, res) => {
|
||||
const { projectId, confirmations } = req.body;
|
||||
|
||||
const result = await autoMapperService.confirmMappings(projectId, confirmations);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
created: result.created,
|
||||
skipped: result.skipped,
|
||||
message: `已创建 ${result.created} 个映射,跳过 ${result.skipped} 个`
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2.6.4 效率对比
|
||||
|
||||
| 配置方式 | 100 个字段耗时 | 准确率 |
|
||||
|----------|---------------|--------|
|
||||
| 手动逐条配置 | 2-4 小时 | 100%(人工保证) |
|
||||
| LLM 猜测 + 人工确认 | 15-30 分钟 | 95%(LLM猜测)→ 100%(人工确认) |
|
||||
|
||||
---
|
||||
|
||||
## 3. IntentService - 意图识别
|
||||
|
||||
> **文件路径**: `backend/src/modules/iit-manager/services/IntentService.ts`
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
> **更新日期:** 2026-02-05
|
||||
> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md)
|
||||
>
|
||||
> **V2.9 更新**:
|
||||
> **V2.9.1 更新**:
|
||||
> - **新增 Phase 1.5**:隐私安全与自动化工具(P0 合规必需)
|
||||
> - Phase 1 新增 AutoMapperService 任务
|
||||
> - Phase 3 新增反馈循环任务
|
||||
> - Phase 4 新增 ProfilerService 和 Cron Skill 任务
|
||||
> - Phase 5 新增多意图处理任务
|
||||
@@ -14,15 +16,15 @@
|
||||
## 1. 开发阶段总览
|
||||
|
||||
```
|
||||
Phase 1 Phase 2 Phase 3 Phase 4 Phase 5 Phase 6
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 基础工具层 │ ──▶ │ SOP 引擎 │ ──▶ │ ReAct 引擎 │ ──▶ │ 调度系统 │ ──▶ │ 智能路由 │ ──▶ │ 视觉能力 │
|
||||
│ │ │ + 记忆L2 │ │ + 记忆L1 │ │ + 记忆L3 │ │ │ │ (延后) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||
▼ ▼ ▼ ▼ ▼ ▼
|
||||
ToolsService SopEngine ReActEngine SchedulerService IntentService VisionService
|
||||
FieldMapping HotMemory FlowMemory WeeklyReports MixedRouting (Postponed)
|
||||
HardRuleEngine SoftRuleEngine AgentTrace ReportService StreamingFB
|
||||
Phase 1 Phase 1.5 Phase 2 Phase 3 Phase 4 Phase 5 Phase 6
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│基础工具层│ ──▶│ 隐私安全 │ ──▶│ SOP 引擎 │ ──▶│ReAct 引擎│ ──▶│ 调度系统 │ ──▶│ 智能路由 │ ──▶│ 视觉能力 │
|
||||
│ │ │ P0必需 │ │ + 记忆L2 │ │ + 记忆L1 │ │ + 记忆L3 │ │ │ │ (延后) │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
|
||||
▼ ▼ ▼ ▼ ▼ ▼ ▼
|
||||
ToolsService Anonymizer SopEngine ReActEngine Scheduler IntentService VisionService
|
||||
FieldMapping AutoMapper HotMemory FlowMemory WeeklyReports MixedRouting (Postponed)
|
||||
HardRule PII Audit SoftRule AgentTrace ReportService StreamingFB
|
||||
```
|
||||
|
||||
---
|
||||
@@ -46,17 +48,72 @@ Phase 1 Phase 2 Phase 3 Phase 4
|
||||
| P1-05 | 实现 `search_protocol` 工具 | 高 | 待开始 | P1-03 |
|
||||
| P1-06 | 实现 `HardRuleEngine` | 高 | 待开始 | - |
|
||||
| P1-07 | 集成字段映射到 ToolsService | 中 | 待开始 | P1-02, P1-03 |
|
||||
| P1-08 | 单元测试覆盖 | 中 | 待开始 | P1-01~P1-07 |
|
||||
| P1-08 | **[V2.9.1]** 实现 `AutoMapperService` | 中 | 待开始 | P1-02 |
|
||||
| P1-09 | **[V2.9.1]** 实现 REDCap Data Dictionary 解析器 | 中 | 待开始 | P1-08 |
|
||||
| P1-10 | **[V2.9.1]** 实现 LLM 语义映射 Job | 中 | 待开始 | P1-08 |
|
||||
| P1-11 | **[V2.9.1]** 实现管理后台映射确认 UI | 低 | 待开始 | P1-10 |
|
||||
| P1-12 | 单元测试覆盖 | 中 | 待开始 | P1-01~P1-11 |
|
||||
|
||||
### 2.3 验收标准
|
||||
|
||||
- [ ] 工具可通过名称调用
|
||||
- [ ] 字段映射正确生效(LLM 用 "年龄" → 实际调用 "dem_age")
|
||||
- [ ] 硬规则拦截生效
|
||||
- [ ] **[V2.9.1]** AutoMapper 可解析 REDCap Data Dictionary
|
||||
- [ ] **[V2.9.1]** LLM 可生成字段映射建议
|
||||
- [ ] 测试覆盖率 > 80%
|
||||
|
||||
---
|
||||
|
||||
## 2.5 Phase 1.5: 隐私安全与自动化工具(P0 合规必需)
|
||||
|
||||
> **⚠️ 重要**:此阶段必须在 Phase 2 调用 LLM 之前完成!
|
||||
> 临床数据包含患者隐私信息,未脱敏直接发送给 LLM 将违反数据保护法规。
|
||||
|
||||
### 2.5.1 目标
|
||||
|
||||
- 实现 PII 脱敏中间件
|
||||
- 建立脱敏审计日志
|
||||
- 确保合规性
|
||||
|
||||
### 2.5.2 任务清单
|
||||
|
||||
| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 |
|
||||
|--------|----------|--------|------|----------|
|
||||
| P1.5-01 | 创建 `iit_pii_audit_log` 表 | **P0** | 待开始 | - |
|
||||
| P1.5-02 | 实现 PII 识别正则库 | **P0** | 待开始 | - |
|
||||
| P1.5-03 | 实现 `AnonymizerService.mask()` | **P0** | 待开始 | P1.5-01, P1.5-02 |
|
||||
| P1.5-04 | 实现 `AnonymizerService.unmask()` | **P0** | 待开始 | P1.5-03 |
|
||||
| P1.5-05 | 实现脱敏映射加密存储 | 高 | 待开始 | P1.5-03 |
|
||||
| P1.5-06 | 集成到 ChatService 调用链 | **P0** | 待开始 | P1.5-04 |
|
||||
| P1.5-07 | 单元测试:各类 PII 识别 | 高 | 待开始 | P1.5-02 |
|
||||
| P1.5-08 | 端到端测试:脱敏还原完整流程 | 高 | 待开始 | P1.5-01~P1.5-06 |
|
||||
|
||||
### 2.5.3 验收标准
|
||||
|
||||
- [ ] 身份证号正确识别并脱敏(18位)
|
||||
- [ ] 手机号正确识别并脱敏(11位)
|
||||
- [ ] 中文姓名正确识别并脱敏(2-4字)
|
||||
- [ ] 病历号正确识别并脱敏
|
||||
- [ ] LLM 收到的 Payload 不包含任何 PII
|
||||
- [ ] LLM 回复正确还原占位符
|
||||
- [ ] 审计日志正确记录(加密存储)
|
||||
|
||||
### 2.5.4 PII 脱敏流程
|
||||
|
||||
```
|
||||
用户输入: "张三(身份证420101199001011234)今天血压偏高"
|
||||
↓ AnonymizerService.mask()
|
||||
LLM 收到: "[PATIENT_1](身份证[ID_CARD_1])今天血压偏高"
|
||||
↓ 同时写入 iit_pii_audit_log(加密存储映射表)
|
||||
↓ LLM 处理
|
||||
LLM 返回: "[PATIENT_1] 的血压需要关注..."
|
||||
↓ AnonymizerService.unmask()
|
||||
用户看到: "张三 的血压需要关注..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 2: SOP 引擎 + 热记忆
|
||||
|
||||
### 3.1 目标
|
||||
@@ -260,6 +317,8 @@ gantt
|
||||
| **[V2.9]** 用户多意图混乱 | 任务遗漏 | ReAct Prompt 多意图拆分 | ✅ |
|
||||
| **[V2.9]** 回复不符用户偏好 | 体验差 | 反馈循环 + 用户画像 | ✅ |
|
||||
| **[V2.9]** 主动提醒打扰用户 | 用户投诉 | 最佳通知时间 + 个性化 | ✅ |
|
||||
| **[V2.9.1]** 患者隐私泄露给 LLM | **法律风险** | PII 脱敏中间件 + 审计日志 | ✅ |
|
||||
| **[V2.9.1]** 字段映射配置繁琐 | 效率低 | AutoMapper LLM 语义匹配 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
|
||||
883
docs/03-业务模块/IIT Manager Agent/04-开发计划/06-实时质控系统开发计划.md
Normal file
883
docs/03-业务模块/IIT Manager Agent/04-开发计划/06-实时质控系统开发计划.md
Normal file
@@ -0,0 +1,883 @@
|
||||
# IIT Manager Agent 实时质控系统开发计划
|
||||
|
||||
> **文档版本:** v1.1
|
||||
> **创建日期:** 2026-02-07
|
||||
> **最后更新:** 2026-02-07
|
||||
> **参考文档:**
|
||||
> - `docs/02-通用能力层/Postgres-Only异步任务处理指南.md`
|
||||
> - `docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.9 补充:业务规则与数据治理细则.md`
|
||||
|
||||
---
|
||||
|
||||
## 📋 概述
|
||||
|
||||
### 背景
|
||||
|
||||
当前 IIT Manager Agent 的质控功能存在以下问题:
|
||||
1. **无持久化**:每次查询需要实时调用 REDCap API 并执行质控规则
|
||||
2. **规则管理繁琐**:需要手动配置所有质控规则
|
||||
3. **缺乏审计信息**:不知道数据谁录入、何时录入
|
||||
4. **AI响应慢**:每次都要实时查询和计算
|
||||
|
||||
### 目标
|
||||
|
||||
建立一套完整的实时质控系统:
|
||||
1. **字段自动同步**:从 REDCap 元数据自动同步字段并生成默认规则
|
||||
2. **实时质控存储**:每次数据录入后自动执行质控并存储结果
|
||||
3. **录入汇总跟踪**:同步记录入组时间、录入人、表单完成状态
|
||||
4. **分级告警**:严重问题立即推送,非紧急问题汇总推送
|
||||
5. **AI快速响应**:AI 直接查询质控结果表,无需实时计算
|
||||
|
||||
---
|
||||
|
||||
## 🎯 核心设计原则
|
||||
|
||||
### 双模式质控策略(Skill 分层配置)
|
||||
|
||||
针对 "录了3张表" vs "录了10张表" 的不同质控需求,采用 **Skill 分层配置策略**,无需配置复杂的前置条件。
|
||||
|
||||
#### 策略 A:单表实时质控 (Real-time / Form-based)
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| **触发** | Webhook (DET) |
|
||||
| **上下文** | `instrument` 参数限制了范围 |
|
||||
| **逻辑** | 系统自动只加载 `formName` 匹配的规则,无需人工配置 preconditions |
|
||||
| **场景** | 刚录完"人口学",系统只检查"年龄",**绝不会报"实验室检查缺失"的错** |
|
||||
|
||||
```typescript
|
||||
// 实时质控:只加载当前表单的规则
|
||||
async executeFormQc(projectId: string, recordId: string, instrument: string) {
|
||||
// ✅ 根据 instrument 自动过滤规则
|
||||
const rules = await getRulesByForm(projectId, instrument);
|
||||
|
||||
// 只质控这张表单涉及的字段
|
||||
return executeRules(rules, record);
|
||||
}
|
||||
```
|
||||
|
||||
#### 策略 B:全案定时质控 (Daily Batch / Holistic)
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| **触发** | Cron Job (每日凌晨) |
|
||||
| **上下文** | 全量数据 |
|
||||
| **逻辑** | 加载跨表规则(Cross-Form Logic) |
|
||||
| **依赖处理** | **逻辑守卫 (Logic Guard)**:规则逻辑内部判断,未录入则静默跳过 |
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:逻辑守卫模式
|
||||
// 不要在 JSON 里配 dependencies: ['lab_test']
|
||||
// 而是直接在规则逻辑里写判断
|
||||
const crossFormRule = {
|
||||
id: 'age_lab_consistency',
|
||||
name: '年龄与实验室结果一致性检查',
|
||||
logic: {
|
||||
"if": [
|
||||
// 逻辑守卫:如果 lab_test 表单未完成,直接跳过(返回 true = 通过)
|
||||
{ "!": { "var": "lab_test_complete" } },
|
||||
true, // 未录入 = 跳过,不报错
|
||||
// 否则执行实际检查
|
||||
{ "and": [
|
||||
{ ">=": [{ "var": "age" }, 18] },
|
||||
{ "<=": [{ "var": "creatinine" }, 1.5] }
|
||||
]}
|
||||
]
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 质控日志记录策略:仅新增,不覆盖
|
||||
|
||||
**原则**:每次质控都 **新增记录**,而不是覆盖。
|
||||
|
||||
**原因**:
|
||||
|
||||
1. **审计轨迹 (Audit Trail)**
|
||||
- 调出周一的 Log,发现当时 `age` 字段是空的(规则没触发)
|
||||
- 周五补录了 `16`(触发了规则)
|
||||
- **这能证明系统是清白的**
|
||||
|
||||
2. **趋势分析**
|
||||
- P001 患者在入组初期有 10 个错误
|
||||
- 经过 3 次修改后,错误降为 0
|
||||
- **这展示了数据质量提升的过程**
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:每次质控都新增记录
|
||||
await prisma.iitQcLog.create({
|
||||
data: {
|
||||
projectId,
|
||||
recordId,
|
||||
formName: instrument,
|
||||
status,
|
||||
issues,
|
||||
ruleVersion,
|
||||
triggeredBy,
|
||||
createdAt: new Date(), // 每次都是新记录
|
||||
}
|
||||
});
|
||||
|
||||
// ❌ 错误:覆盖模式会丢失历史
|
||||
await prisma.iitQcLog.upsert({ ... }); // 不要用 upsert
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 整体架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ CRC 在 REDCap 录入数据 │
|
||||
└─────────────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼ DET Webhook (含 instrument 参数)
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ 同步层 (WebhookController) │
|
||||
│ - 接收 HTTP 请求 │
|
||||
│ - 校验签名 │
|
||||
│ - 防抖检查 (pg-boss singletonKey) │
|
||||
│ - 推入队列(携带 instrument 参数) │
|
||||
│ - 立即返回 200 OK (< 10ms) │
|
||||
└─────────────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼ pg-boss 队列
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ 异步层 (QcWorker) - 一次处理,两个产出 │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐│
|
||||
│ │ 1. 拉取记录数据 (RedcapAdapter) ││
|
||||
│ └──────────────────────────────────────────────────────────────────────┘│
|
||||
│ │ │
|
||||
│ ┌─────────────┴─────────────┐ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────┐ ┌─────────────────────┐ │
|
||||
│ │ 2a. 更新录入汇总表 │ │ 2b. 执行单表质控 │ │
|
||||
│ │ iit_record_summary │ │ (只评估当前表单规则) │ │
|
||||
│ │ - 入组时间 │ │ 根据 instrument 过滤 │ │
|
||||
│ │ - 录入人 │ └─────────────────────┘ │
|
||||
│ │ - 表单完成状态 │ │ │
|
||||
│ └─────────────────────┘ ▼ │
|
||||
│ │ ┌─────────────────────┐ │
|
||||
│ │ │ 2c. 新增质控日志 │ │
|
||||
│ │ │ iit_qc_logs │ │
|
||||
│ │ │ (仅新增,不覆盖) │ │
|
||||
│ │ └─────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ └─────────────┬─────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ 3. 主动干预 ││
|
||||
│ │ - 🔴 RED: 立即推送企业微信 ││
|
||||
│ │ - 🟡 YELLOW: 存入每日摘要队列 ││
|
||||
│ │ - 🟢 GREEN: 仅记录日志 ││
|
||||
│ └─────────────────────────────────────────────────────────────┘│
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────┴─────────────────────────┐
|
||||
▼ ▼
|
||||
┌───────────────────────────────┐ ┌───────────────────────────────┐
|
||||
│ 每日定时任务 (Cron Job) │ │ AI Agent 查询层 │
|
||||
│ - 全案定时质控(跨表规则) │ │ - 查询 iit_qc_logs(毫秒级) │
|
||||
│ - 逻辑守卫模式处理依赖 │ │ - 查询 iit_record_summary │
|
||||
│ - 生成每日摘要报告 │ │ - 查询 iit_qc_project_stats │
|
||||
└───────────────────────────────┘ └───────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 数据库设计
|
||||
|
||||
### 新增表
|
||||
|
||||
#### 1. iit_qc_logs(质控日志表)
|
||||
|
||||
质控结果存储,**仅新增,不覆盖**,保留完整审计轨迹。
|
||||
|
||||
```prisma
|
||||
model IitQcLog {
|
||||
id String @id @default(uuid())
|
||||
projectId String @map("project_id")
|
||||
recordId String @map("record_id")
|
||||
eventId String? @map("event_id")
|
||||
|
||||
// 质控类型
|
||||
qcType String @map("qc_type") // 'form' | 'holistic'
|
||||
formName String? @map("form_name") // 单表质控时记录表单名
|
||||
|
||||
// 核心结果
|
||||
status String // 'PASS' | 'FAIL' | 'WARNING'
|
||||
|
||||
// 字段级详情 (JSONB)
|
||||
// 格式: [{ field: "age", rule: "range_check", level: "RED", message: "..." }, ...]
|
||||
issues Json @default("[]")
|
||||
|
||||
// 规则统计
|
||||
rulesEvaluated Int @default(0) @map("rules_evaluated") // 实际评估的规则数
|
||||
rulesSkipped Int @default(0) @map("rules_skipped") // 逻辑守卫跳过的规则数
|
||||
rulesPassed Int @default(0) @map("rules_passed")
|
||||
rulesFailed Int @default(0) @map("rules_failed")
|
||||
|
||||
// 规则版本(用于历史追溯)
|
||||
ruleVersion String @map("rule_version")
|
||||
|
||||
// 入排标准检查(全案质控时填充)
|
||||
inclusionPassed Boolean? @map("inclusion_passed")
|
||||
exclusionPassed Boolean? @map("exclusion_passed")
|
||||
|
||||
// 审计信息
|
||||
triggeredBy String @map("triggered_by") // 'webhook' | 'manual' | 'batch'
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
// 索引 - 支持历史查询和趋势分析
|
||||
@@index([projectId, recordId, createdAt]) // 查询某记录的质控历史
|
||||
@@index([projectId, status, createdAt]) // 查询某状态的质控记录
|
||||
@@index([projectId, qcType, createdAt]) // 按质控类型查询
|
||||
@@map("iit_qc_logs")
|
||||
@@schema("iit_schema")
|
||||
}
|
||||
```
|
||||
|
||||
**设计说明**:
|
||||
- **无唯一约束**:同一 `projectId + recordId` 可以有多条记录
|
||||
- **`qcType`**:区分单表实时质控 (`form`) 和全案定时质控 (`holistic`)
|
||||
- **`rulesSkipped`**:逻辑守卫跳过的规则数,用于分析数据完整度
|
||||
|
||||
#### 2. iit_record_summary(录入汇总表)
|
||||
|
||||
**一次处理,两个产出**:与质控同时更新,记录入组和录入进度。
|
||||
|
||||
```prisma
|
||||
model IitRecordSummary {
|
||||
id String @id @default(uuid())
|
||||
projectId String @map("project_id")
|
||||
recordId String @map("record_id")
|
||||
|
||||
// 入组信息
|
||||
enrolledAt DateTime? @map("enrolled_at") // 首次录入时间 = 入组时间
|
||||
enrolledBy String? @map("enrolled_by") // 入组录入人(REDCap username)
|
||||
|
||||
// 最新录入信息
|
||||
lastUpdatedAt DateTime @map("last_updated_at")
|
||||
lastUpdatedBy String? @map("last_updated_by")
|
||||
lastFormName String? @map("last_form_name") // 最后更新的表单
|
||||
|
||||
// 表单完成状态 (JSONB)
|
||||
// 格式: { "demographics": 2, "baseline": 1, "visit1": 0 }
|
||||
// 0=未开始, 1=进行中, 2=完成
|
||||
formStatus Json @default("{}") @map("form_status")
|
||||
|
||||
// 数据完整度
|
||||
totalForms Int @default(0) @map("total_forms")
|
||||
completedForms Int @default(0) @map("completed_forms")
|
||||
completionRate Float @default(0) @map("completion_rate") // 0-100%
|
||||
|
||||
// 最新质控状态(冗余存储,查询更快)
|
||||
latestQcStatus String? @map("latest_qc_status") // 'PASS' | 'FAIL' | 'WARNING'
|
||||
latestQcAt DateTime? @map("latest_qc_at")
|
||||
|
||||
// 更新次数(用于趋势分析)
|
||||
updateCount Int @default(0) @map("update_count")
|
||||
|
||||
// 时间戳
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
// 唯一约束 - 每个记录只有一条汇总
|
||||
@@unique([projectId, recordId])
|
||||
@@index([projectId, enrolledAt])
|
||||
@@index([projectId, latestQcStatus])
|
||||
@@index([projectId, completionRate])
|
||||
@@map("iit_record_summary")
|
||||
@@schema("iit_schema")
|
||||
}
|
||||
```
|
||||
|
||||
**设计说明**:
|
||||
- **使用 upsert**:与 `iit_qc_logs` 不同,汇总表使用覆盖模式
|
||||
- **`enrolledAt`**:首次录入时自动设置,后续不更新(入组时间锁定)
|
||||
- **`formStatus`**:JSONB 存储各表单完成状态,便于前端展示进度条
|
||||
|
||||
#### 3. iit_qc_project_stats(项目级汇总表)
|
||||
|
||||
用于 Dashboard 快速展示,避免每次 COUNT。
|
||||
|
||||
```prisma
|
||||
model IitQcProjectStats {
|
||||
id String @id @default(uuid())
|
||||
projectId String @unique @map("project_id")
|
||||
|
||||
// 汇总统计
|
||||
totalRecords Int @default(0) @map("total_records")
|
||||
passedRecords Int @default(0) @map("passed_records")
|
||||
failedRecords Int @default(0) @map("failed_records")
|
||||
warningRecords Int @default(0) @map("warning_records")
|
||||
|
||||
// 入排标准统计
|
||||
inclusionMet Int @default(0) @map("inclusion_met")
|
||||
exclusionMet Int @default(0) @map("exclusion_met")
|
||||
|
||||
// 录入进度统计
|
||||
avgCompletionRate Float @default(0) @map("avg_completion_rate")
|
||||
|
||||
// 更新时间
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("iit_qc_project_stats")
|
||||
@@schema("iit_schema")
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. iit_field_mapping(字段映射表)- 增强
|
||||
|
||||
```prisma
|
||||
model IitFieldMapping {
|
||||
id String @id @default(uuid())
|
||||
projectId String @map("project_id")
|
||||
|
||||
// REDCap 字段信息
|
||||
fieldName String @map("field_name")
|
||||
fieldLabel String @map("field_label")
|
||||
fieldType String @map("field_type")
|
||||
formName String @map("form_name")
|
||||
|
||||
// 验证规则(从 REDCap 元数据提取)
|
||||
validation String? @map("validation")
|
||||
validationMin String? @map("validation_min")
|
||||
validationMax String? @map("validation_max")
|
||||
choices String? @map("choices")
|
||||
required Boolean @default(false)
|
||||
|
||||
// 别名(LLM 友好名称)
|
||||
alias String?
|
||||
|
||||
// 规则来源标记
|
||||
ruleSource String? @map("rule_source") // 'auto' | 'manual'
|
||||
|
||||
// 时间戳
|
||||
syncedAt DateTime @default(now()) @map("synced_at")
|
||||
|
||||
// 唯一约束
|
||||
@@unique([projectId, fieldName])
|
||||
@@index([projectId])
|
||||
@@index([projectId, formName]) // 按表单查询字段
|
||||
@@map("iit_field_mapping")
|
||||
@@schema("iit_schema")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 核心模块设计
|
||||
|
||||
### 1. 字段同步服务 (FieldSyncService)
|
||||
|
||||
```typescript
|
||||
// backend/src/modules/iit-manager/services/FieldSyncService.ts
|
||||
|
||||
export class FieldSyncService {
|
||||
/**
|
||||
* 从 REDCap 同步字段元数据
|
||||
*
|
||||
* 流程:
|
||||
* 1. 调用 RedcapAdapter.exportMetadata()
|
||||
* 2. 解析元数据,提取字段信息(含 formName)
|
||||
* 3. 更新 iit_field_mapping 表
|
||||
* 4. 自动生成默认质控规则(标记 ruleSource = 'auto')
|
||||
*/
|
||||
async syncFields(projectId: string): Promise<SyncResult> {
|
||||
const adapter = await getRedcapAdapter(projectId);
|
||||
const metadata = await adapter.exportMetadata();
|
||||
|
||||
for (const field of metadata) {
|
||||
await prisma.iitFieldMapping.upsert({
|
||||
where: { projectId_fieldName: { projectId, fieldName: field.field_name } },
|
||||
create: {
|
||||
projectId,
|
||||
fieldName: field.field_name,
|
||||
fieldLabel: field.field_label,
|
||||
fieldType: field.field_type,
|
||||
formName: field.form_name, // ⭐ 关键:用于单表质控规则匹配
|
||||
validation: field.text_validation_type_or_show_slider_number,
|
||||
validationMin: field.text_validation_min,
|
||||
validationMax: field.text_validation_max,
|
||||
choices: field.select_choices_or_calculations,
|
||||
required: field.required_field === 'y',
|
||||
},
|
||||
update: { /* ... */ },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动生成默认质控规则
|
||||
*
|
||||
* 根据 REDCap 元数据自动生成:
|
||||
* - 范围规则(from validation_min/max)
|
||||
* - 必填规则(from required)
|
||||
* - 枚举规则(from choices)
|
||||
*
|
||||
* 规则携带 formName,用于单表质控时过滤
|
||||
*/
|
||||
async generateDefaultRules(projectId: string): Promise<GeneratedRule[]> {
|
||||
const fields = await prisma.iitFieldMapping.findMany({ where: { projectId } });
|
||||
const rules: GeneratedRule[] = [];
|
||||
|
||||
for (const field of fields) {
|
||||
// 范围规则
|
||||
if (field.validationMin || field.validationMax) {
|
||||
rules.push({
|
||||
id: `${field.fieldName}_range`,
|
||||
name: `${field.fieldLabel}范围检查`,
|
||||
formName: field.formName, // ⭐ 规则绑定到表单
|
||||
source: 'auto',
|
||||
logic: { /* JSON Logic */ },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 质控 Worker (QcWorker)
|
||||
|
||||
**关键改动**:根据 `instrument` 参数过滤规则,一次处理更新两个表。
|
||||
|
||||
```typescript
|
||||
// backend/src/modules/iit-manager/workers/qcWorker.ts
|
||||
|
||||
import { jobQueue } from '@/common/jobs';
|
||||
import { logger } from '@/common/logging';
|
||||
|
||||
interface QcJob {
|
||||
projectId: string;
|
||||
recordId: string;
|
||||
eventId?: string;
|
||||
instrument?: string; // ⭐ Webhook 携带的表单名
|
||||
username?: string; // ⭐ REDCap 录入人
|
||||
triggeredBy: 'webhook' | 'manual' | 'batch';
|
||||
}
|
||||
|
||||
export function registerQcWorker() {
|
||||
logger.info('[QcWorker] Registering worker');
|
||||
|
||||
jobQueue.process<QcJob>('iit_quality_check', async (job) => {
|
||||
const { projectId, recordId, eventId, instrument, username, triggeredBy } = job.data;
|
||||
|
||||
logger.info('[QcWorker] Processing', { jobId: job.id, recordId, instrument });
|
||||
|
||||
try {
|
||||
// 1. 从 REDCap 获取记录数据
|
||||
const adapter = await getRedcapAdapter(projectId);
|
||||
const record = await adapter.getRecordById(recordId);
|
||||
|
||||
// ========== 产出1:更新录入汇总表 ==========
|
||||
await updateRecordSummary({
|
||||
projectId,
|
||||
recordId,
|
||||
username,
|
||||
formName: instrument,
|
||||
record,
|
||||
});
|
||||
|
||||
// ========== 产出2:执行单表质控 ==========
|
||||
// ⭐ 策略 A:根据 instrument 过滤规则
|
||||
const rules = instrument
|
||||
? await getRulesByForm(projectId, instrument) // 只加载当前表单的规则
|
||||
: await getAllRules(projectId); // batch 任务加载全部规则
|
||||
|
||||
const ruleEngine = createHardRuleEngine(rules);
|
||||
const qcResult = ruleEngine.execute(recordId, record);
|
||||
|
||||
// 3. 新增质控日志(仅新增,不覆盖)
|
||||
await prisma.iitQcLog.create({
|
||||
data: {
|
||||
projectId,
|
||||
recordId,
|
||||
eventId,
|
||||
qcType: instrument ? 'form' : 'holistic',
|
||||
formName: instrument,
|
||||
status: qcResult.overallStatus,
|
||||
issues: qcResult.results.filter(r => r.status !== 'PASS'),
|
||||
rulesEvaluated: qcResult.summary.totalRules,
|
||||
rulesSkipped: qcResult.summary.skipped || 0,
|
||||
rulesPassed: qcResult.summary.passed,
|
||||
rulesFailed: qcResult.summary.failed,
|
||||
ruleVersion: await getRuleVersion(projectId),
|
||||
triggeredBy,
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 更新汇总统计
|
||||
await updateProjectStats(projectId);
|
||||
|
||||
// 5. 主动干预
|
||||
await handleAlerts(projectId, recordId, qcResult);
|
||||
|
||||
logger.info('[QcWorker] ✅ Job completed', { jobId: job.id });
|
||||
|
||||
return { success: true, recordId, status: qcResult.overallStatus };
|
||||
} catch (error: any) {
|
||||
logger.error('[QcWorker] ❌ Job failed', {
|
||||
jobId: job.id,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('[QcWorker] ✅ Worker registered: iit_quality_check');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新录入汇总表
|
||||
*/
|
||||
async function updateRecordSummary(params: {
|
||||
projectId: string;
|
||||
recordId: string;
|
||||
username?: string;
|
||||
formName?: string;
|
||||
record: Record<string, any>;
|
||||
}) {
|
||||
const { projectId, recordId, username, formName, record } = params;
|
||||
|
||||
// 获取现有汇总
|
||||
const existing = await prisma.iitRecordSummary.findUnique({
|
||||
where: { projectId_recordId: { projectId, recordId } }
|
||||
});
|
||||
|
||||
// 计算表单完成状态
|
||||
const formStatus = calculateFormStatus(record);
|
||||
const totalForms = Object.keys(formStatus).length;
|
||||
const completedForms = Object.values(formStatus).filter(s => s === 2).length;
|
||||
|
||||
await prisma.iitRecordSummary.upsert({
|
||||
where: { projectId_recordId: { projectId, recordId } },
|
||||
create: {
|
||||
projectId,
|
||||
recordId,
|
||||
enrolledAt: new Date(), // 首次录入 = 入组时间
|
||||
enrolledBy: username,
|
||||
lastUpdatedAt: new Date(),
|
||||
lastUpdatedBy: username,
|
||||
lastFormName: formName,
|
||||
formStatus,
|
||||
totalForms,
|
||||
completedForms,
|
||||
completionRate: (completedForms / totalForms) * 100,
|
||||
updateCount: 1,
|
||||
},
|
||||
update: {
|
||||
// 入组时间不更新
|
||||
lastUpdatedAt: new Date(),
|
||||
lastUpdatedBy: username,
|
||||
lastFormName: formName,
|
||||
formStatus,
|
||||
completedForms,
|
||||
completionRate: (completedForms / totalForms) * 100,
|
||||
updateCount: { increment: 1 },
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 每日定时质控服务 (DailyQcService)
|
||||
|
||||
**策略 B:全案定时质控,使用逻辑守卫处理依赖。**
|
||||
|
||||
```typescript
|
||||
// backend/src/modules/iit-manager/services/DailyQcService.ts
|
||||
|
||||
export class DailyQcService {
|
||||
/**
|
||||
* 每日凌晨执行全案质控
|
||||
*
|
||||
* 特点:
|
||||
* - 加载所有规则,包括跨表规则
|
||||
* - 使用逻辑守卫处理依赖表单未完成的情况
|
||||
*/
|
||||
async executeHolisticQc(projectId: string) {
|
||||
const records = await redcapAdapter.exportRecords();
|
||||
|
||||
for (const record of records) {
|
||||
// 推入队列,分批处理
|
||||
await jobQueue.push('iit_quality_check', {
|
||||
projectId,
|
||||
recordId: record.record_id,
|
||||
triggeredBy: 'batch',
|
||||
// 不传 instrument,加载全部规则
|
||||
}, {
|
||||
singletonKey: `batch-qc-${projectId}-${record.record_id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 跨表规则示例:使用逻辑守卫
|
||||
const crossFormRules = [
|
||||
{
|
||||
id: 'age_lab_consistency',
|
||||
name: '年龄与实验室结果一致性检查',
|
||||
formName: null, // 跨表规则,不绑定单一表单
|
||||
logic: {
|
||||
"if": [
|
||||
// 逻辑守卫:检查依赖表单是否完成
|
||||
{ "!": { "var": "lab_test_complete" } },
|
||||
{ "__skip": true }, // 返回特殊标记,表示跳过
|
||||
// 实际检查逻辑
|
||||
{ "and": [
|
||||
{ ">=": [{ "var": "age" }, 18] },
|
||||
{ "<=": [{ "var": "creatinine" }, 1.5] }
|
||||
]}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 4. 告警服务 (AlertService)
|
||||
|
||||
```typescript
|
||||
// backend/src/modules/iit-manager/services/AlertService.ts
|
||||
|
||||
export class AlertService {
|
||||
/**
|
||||
* 处理告警分级
|
||||
*/
|
||||
async handleAlerts(projectId: string, recordId: string, qcResult: QCResult) {
|
||||
const redIssues = qcResult.results.filter(r => r.severity === 'error');
|
||||
const yellowIssues = qcResult.results.filter(r => r.severity === 'warning');
|
||||
|
||||
// 🔴 RED: 立即推送
|
||||
if (redIssues.length > 0) {
|
||||
await this.sendImmediateAlert(projectId, recordId, redIssues);
|
||||
}
|
||||
|
||||
// 🟡 YELLOW: 存入每日摘要队列
|
||||
if (yellowIssues.length > 0) {
|
||||
await this.addToDailyDigest(projectId, recordId, yellowIssues);
|
||||
}
|
||||
|
||||
// 🟢 GREEN: 只记录日志,不推送
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即推送企业微信告警
|
||||
*/
|
||||
private async sendImmediateAlert(
|
||||
projectId: string,
|
||||
recordId: string,
|
||||
issues: QCRuleResult[]
|
||||
) {
|
||||
const message = this.formatAlertMessage(recordId, issues);
|
||||
const piUserId = await this.getProjectPiUserId(projectId);
|
||||
await wechatService.sendTextMessage(piUserId, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日摘要定时发送(17:00)
|
||||
*/
|
||||
async sendDailyDigest(projectId: string) {
|
||||
// 由定时任务触发
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 📋 开发阶段
|
||||
|
||||
### Phase 1: 字段同步与自动规则(预计 2 天)
|
||||
|
||||
| 序号 | 任务 | 优先级 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 1.1 | 创建 `iit_field_mapping` 表迁移(含 `formName` 字段) | P0 | ⬜ |
|
||||
| 1.2 | 实现 `FieldSyncService.syncFields()` | P0 | ⬜ |
|
||||
| 1.3 | 实现 `FieldSyncService.generateDefaultRules()`(规则绑定 formName) | P0 | ⬜ |
|
||||
| 1.4 | 在运营端添加"同步字段"按钮 | P1 | ⬜ |
|
||||
| 1.5 | 规则中添加 `source` 和 `formName` 字段 | P1 | ⬜ |
|
||||
|
||||
### Phase 2: 实时质控与录入汇总(预计 3 天)
|
||||
|
||||
| 序号 | 任务 | 优先级 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 2.1 | 创建 `iit_qc_logs` 表迁移(支持仅新增模式) | P0 | ⬜ |
|
||||
| 2.2 | 创建 `iit_record_summary` 表迁移 | P0 | ⬜ |
|
||||
| 2.3 | 创建 `iit_qc_project_stats` 表迁移 | P0 | ⬜ |
|
||||
| 2.4 | 重构 `WebhookController`:传递 `instrument` 和 `username` 参数 | P0 | ⬜ |
|
||||
| 2.5 | 实现 `QcWorker`(一次处理两个产出) | P0 | ⬜ |
|
||||
| 2.6 | 实现 `updateRecordSummary()` 函数 | P0 | ⬜ |
|
||||
| 2.7 | 实现 `getRulesByForm()` 函数(根据表单过滤规则) | P0 | ⬜ |
|
||||
| 2.8 | 实现 `updateProjectStats()` 函数 | P1 | ⬜ |
|
||||
| 2.9 | 添加 JSONB 索引优化查询性能 | P1 | ⬜ |
|
||||
|
||||
### Phase 3: AI Agent 集成(预计 2 天)
|
||||
|
||||
| 序号 | 任务 | 优先级 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 3.1 | 新增 `queryQcLogs()` 方法(查询质控历史) | P0 | ⬜ |
|
||||
| 3.2 | 新增 `getRecordSummary()` 方法(查询录入汇总) | P0 | ⬜ |
|
||||
| 3.3 | 新增 `getProjectQcStats()` 方法(查询项目汇总) | P0 | ⬜ |
|
||||
| 3.4 | 修改 `ChatService`:优先查询质控表和汇总表 | P0 | ⬜ |
|
||||
| 3.5 | 支持"哪些记录有问题"等汇总查询 | P1 | ⬜ |
|
||||
| 3.6 | 支持"记录10质控历史"等趋势查询 | P1 | ⬜ |
|
||||
| 3.7 | 支持"最近入组的患者"等录入查询 | P1 | ⬜ |
|
||||
|
||||
### Phase 4: 每日定时质控(预计 2 天)
|
||||
|
||||
| 序号 | 任务 | 优先级 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 4.1 | 实现 `DailyQcService.executeHolisticQc()` | P0 | ⬜ |
|
||||
| 4.2 | 实现跨表规则的逻辑守卫模式 | P0 | ⬜ |
|
||||
| 4.3 | 添加定时任务(凌晨 2:00 执行全案质控) | P0 | ⬜ |
|
||||
| 4.4 | 实现每日摘要报告生成 | P1 | ⬜ |
|
||||
|
||||
### Phase 5: 主动干预(预计 2 天)
|
||||
|
||||
| 序号 | 任务 | 优先级 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 5.1 | 实现 `AlertService.handleAlerts()` | P0 | ⬜ |
|
||||
| 5.2 | 实现紧急告警立即推送 | P0 | ⬜ |
|
||||
| 5.3 | 实现每日摘要队列 | P1 | ⬜ |
|
||||
| 5.4 | 添加定时任务(17:00 发送每日摘要) | P1 | ⬜ |
|
||||
| 5.5 | 告警级别可配置化 | P2 | ⬜ |
|
||||
|
||||
### Phase 6: 批量回溯(预计 1 天)
|
||||
|
||||
| 序号 | 任务 | 优先级 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 6.1 | 实现 `BatchQcJob`(批量质控任务) | P1 | ⬜ |
|
||||
| 6.2 | 分片执行(每批 50 条,流控) | P1 | ⬜ |
|
||||
| 6.3 | 运营端添加"重新质控"按钮 | P2 | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 安全规范
|
||||
|
||||
### 1. 幂等性保证
|
||||
|
||||
**质控日志表(仅新增模式)**:通过 pg-boss 防抖保证不重复执行。
|
||||
|
||||
```typescript
|
||||
// ✅ 质控日志:仅新增,不需要 upsert
|
||||
// 幂等性由 pg-boss singletonKey 保证
|
||||
await prisma.iitQcLog.create({
|
||||
data: { projectId, recordId, ... }
|
||||
});
|
||||
```
|
||||
|
||||
**录入汇总表(覆盖模式)**:使用 upsert 确保重试安全。
|
||||
|
||||
```typescript
|
||||
// ✅ 录入汇总:使用 upsert
|
||||
await prisma.iitRecordSummary.upsert({
|
||||
where: { projectId_recordId: { projectId, recordId } },
|
||||
create: { enrolledAt: new Date(), ... },
|
||||
update: { lastUpdatedAt: new Date(), ... },
|
||||
});
|
||||
```
|
||||
|
||||
### 2. pg-boss 防抖
|
||||
|
||||
```typescript
|
||||
// WebhookController.ts
|
||||
await jobQueue.push('iit_quality_check',
|
||||
{ projectId, recordId, triggeredBy: 'webhook' },
|
||||
{
|
||||
singletonKey: `qc-${projectId}-${recordId}`,
|
||||
singletonSeconds: 300, // 5分钟防抖
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Payload 精简
|
||||
|
||||
```typescript
|
||||
// ✅ 只存 ID,不存大数据
|
||||
await jobQueue.push('iit_quality_check', {
|
||||
projectId: 'xxx',
|
||||
recordId: '10',
|
||||
triggeredBy: 'webhook',
|
||||
});
|
||||
|
||||
// ❌ 禁止存大数据
|
||||
await jobQueue.push('iit_quality_check', {
|
||||
recordData: { ... }, // 禁止!
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 任务过期时间
|
||||
|
||||
```typescript
|
||||
// 质控任务 15 分钟过期
|
||||
await jobQueue.push('iit_quality_check', data, {
|
||||
expireInSeconds: 15 * 60,
|
||||
});
|
||||
|
||||
// 批量任务 1 小时过期
|
||||
await jobQueue.push('iit_batch_qc', data, {
|
||||
expireInSeconds: 60 * 60,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 性能预期
|
||||
|
||||
| 指标 | 当前 | 优化后 | 改善 |
|
||||
|------|------|--------|------|
|
||||
| Webhook 响应时间 | 2-5秒 | < 50ms | ✅ -99% |
|
||||
| AI 查询响应时间 | 3-5秒(调REDCap) | < 200ms(查本地表) | ✅ -95% |
|
||||
| 批量质控 1000条 | 不支持 | ~5分钟(分片) | ✅ 新增 |
|
||||
| 并发 Webhook | 可能重复执行 | 防抖去重 | ✅ 修复 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 检查清单
|
||||
|
||||
### 数据库
|
||||
- [ ] `iit_field_mapping` 表已创建(含 `formName` 字段)
|
||||
- [ ] `iit_qc_logs` 表已创建(支持仅新增模式)
|
||||
- [ ] `iit_record_summary` 表已创建
|
||||
- [ ] `iit_qc_project_stats` 表已创建
|
||||
- [ ] JSONB 索引已添加
|
||||
|
||||
### 双模式质控
|
||||
- [ ] 单表质控:根据 `instrument` 过滤规则
|
||||
- [ ] 全案质控:逻辑守卫模式处理依赖
|
||||
- [ ] 质控规则绑定 `formName`
|
||||
|
||||
### 异步处理
|
||||
- [ ] `QcWorker` 已注册
|
||||
- [ ] 队列名称使用下划线(`iit_quality_check`)
|
||||
- [ ] Worker 在 `jobQueue.start()` 之后注册
|
||||
- [ ] 防抖配置已添加
|
||||
|
||||
### 安全规范
|
||||
- [ ] 质控日志:仅新增,不覆盖
|
||||
- [ ] 录入汇总:使用 `upsert`
|
||||
- [ ] Payload:只存 ID
|
||||
- [ ] 错误处理:直接 `throw error`
|
||||
|
||||
### 功能完整性
|
||||
- [ ] 字段同步功能可用
|
||||
- [ ] 实时质控存储正常
|
||||
- [ ] 录入汇总更新正常
|
||||
- [ ] AI 查询质控表正常
|
||||
- [ ] AI 查询录入汇总正常
|
||||
- [ ] 告警推送正常
|
||||
- [ ] 每日定时质控正常
|
||||
|
||||
### 趋势分析
|
||||
- [ ] 可查询记录的质控历史
|
||||
- [ ] 可分析数据质量提升过程
|
||||
- [ ] 审计轨迹完整
|
||||
|
||||
---
|
||||
|
||||
**维护者**: IIT Manager Agent 开发团队
|
||||
**最后更新**: 2026-02-07
|
||||
**文档状态**: ✅ 已完成(v1.1 更新双模式质控策略)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
# IIT Manager Agent V2.6 综合开发计划
|
||||
|
||||
> **版本:** V2.9(极简架构 + SOP状态机 + 双脑路由 + 三层记忆 + 主动性增强)
|
||||
> **版本:** V2.9.1(极简架构 + SOP状态机 + 双脑路由 + 三层记忆 + 主动性增强 + 隐私合规)
|
||||
> **日期:** 2026-02-05
|
||||
> **团队规模:** 2人
|
||||
> **预估周期:** 6周
|
||||
> **核心目标:** 实现数据质控 Agent 的完整闭环 + 智能化交互 + 长期记忆 + 主动提醒 + 个性化
|
||||
> **核心目标:** 实现数据质控 Agent 的完整闭环 + 智能化交互 + 长期记忆 + 主动提醒 + 个性化 + **隐私合规**
|
||||
|
||||
---
|
||||
|
||||
@@ -63,8 +63,32 @@
|
||||
| **V2.4 SOP状态机** | 粗粒度 SOP 节点 + 节点内 ReAct | ✅ 认可 |
|
||||
| **V2.8 记忆系统** | 三层记忆(流水账 + 热记忆 + 历史书) | ✅ 认可 |
|
||||
| **V2.9 主动性增强** | Cron Skill + 用户画像 + 反馈循环 | ✅ 认可 |
|
||||
| **V2.9.1 隐私合规** | PII 脱敏中间件 + REDCap Schema 自动对齐 | ✅ 认可 |
|
||||
|
||||
### 1.0 V2.9 核心增强(新)
|
||||
### 1.0 V2.9.1 隐私合规增强(P0 必需)
|
||||
|
||||
> **⚠️ 重要**:临床数据包含大量患者隐私信息,在调用第三方 LLM 之前**必须脱敏**!
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ V2.9.1 隐私合规能力 │
|
||||
├─────────────────────────────────┬───────────────────────────────────┤
|
||||
│ 🔒 AnonymizerService │ 🔧 AutoMapperService │
|
||||
│ PII 脱敏中间件 │ REDCap Schema 自动对齐 │
|
||||
│ - 身份证号脱敏 │ - Data Dictionary 解析 │
|
||||
│ - 手机号脱敏 │ - LLM 语义映射 │
|
||||
│ - 中文姓名脱敏 │ - 人工确认 UI │
|
||||
│ - 审计日志加密存储 │ - 效率提升 8-16x │
|
||||
└─────────────────────────────────┴───────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 能力 | 实现方式 | 价值 |
|
||||
|------|----------|------|
|
||||
| **PII 脱敏** | `AnonymizerService.mask/unmask()` + 正则库 | 合规必需,防止隐私泄露 |
|
||||
| **审计日志** | `iit_pii_audit_log` 表 + 加密存储 | 事后合规审计 |
|
||||
| **Schema 自动对齐** | `AutoMapperService` + LLM 语义匹配 | 字段配置效率提升 8-16x |
|
||||
|
||||
### 1.1 V2.9 核心增强
|
||||
|
||||
> **目标**:让 Agent 从"被动应答"进化为"主动协作",同时根据用户反馈持续优化
|
||||
|
||||
@@ -325,3 +349,4 @@ backend/src/modules/iit-manager/
|
||||
| V2.6.1 | 2026-02-05 | 整合团队风险审查建议;拆分为多个专项文档 |
|
||||
| V2.6.2 | 2026-02-05 | 简化表结构:删除 `iit_user_preferences` 和 `iit_patient_notes`(合并到 `project_memory`) |
|
||||
| V2.9 | 2026-02-05 | 主动性增强:Cron Skill、用户画像、反馈循环、多意图处理 |
|
||||
| V2.9.1 | 2026-02-05 | 隐私合规:PII 脱敏中间件、REDCap Schema 自动对齐工具 |
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
# **IIT Manager Agent V2.9 补充:业务规则与数据治理细则**
|
||||
|
||||
**文档性质:** 业务逻辑落地方案
|
||||
|
||||
**评审结论:** 4 点建议逻辑清晰,完全可行。本方案在原有基础上进行了 Postgres 原生特性的优化。
|
||||
|
||||
## **1\. 质控规则来源:自动化与人工的完美结合**
|
||||
|
||||
**评价**:非常合理。这是降低实施成本的关键。如果全部靠人工配,运营会累死;如果全部靠自动,医学逻辑会缺失。
|
||||
|
||||
**落地优化建议**:
|
||||
|
||||
我们在 iit\_skills 表的 JSON 配置中,引入 **source** 字段来标记规则来源,方便管理。
|
||||
|
||||
// qc\_skill.json (最终生成的配置文件)
|
||||
{
|
||||
"hard\_rules": \[
|
||||
// \=== 自动生成区 (Auto-Generated) \===
|
||||
{
|
||||
"field": "age",
|
||||
"logic": { "\>=": \[{ "var": "age" }, 18\] },
|
||||
"message": "年龄必须 \>= 18",
|
||||
"source": "meta\_validation\_min", // 标记来源
|
||||
"level": "warning"
|
||||
},
|
||||
{
|
||||
"field": "informed\_consent\_date",
|
||||
"logic": { "\!=": \[{ "var": "informed\_consent\_date" }, ""\] },
|
||||
"message": "知情同意日期必填",
|
||||
"source": "meta\_required",
|
||||
"level": "error"
|
||||
},
|
||||
|
||||
// \=== 人工配置区 (Manual) \===
|
||||
{
|
||||
"field": "visit\_date",
|
||||
"logic": { "\<=": \[{ "var": "visit\_date" }, { "var": "today" }\] },
|
||||
"message": "访视日期不能是未来",
|
||||
"source": "manual\_config",
|
||||
"level": "error"
|
||||
}
|
||||
\]
|
||||
}
|
||||
|
||||
**实施流程**:
|
||||
|
||||
1. **Sync**: RedcapAdapter 拉取 Metadata。
|
||||
2. **Generate**: 代码遍历 Metadata,生成一个 draft\_rules 数组。
|
||||
3. **Merge**: 将 draft\_rules 与数据库里已有的 manual\_rules 合并。
|
||||
4. **Confirm**: 在管理端展示,人工确认后保存。
|
||||
|
||||
## **2\. 质控数据存储:利用 JSONB 简化分层**
|
||||
|
||||
**评价**:分层思路是对的,但**建议简化物理表结构**。
|
||||
|
||||
如果每个字段的错误都存一行记录(iit\_qc\_results),当项目有 1000 个病人 x 400 个变量时,这张表会瞬间爆炸(千万级行数),查询变慢。
|
||||
|
||||
**Postgres 优化方案**:
|
||||
|
||||
利用 Postgres 强大的 **JSONB** 能力,将(1)和(2)合并,保留(3)。
|
||||
|
||||
### **优化后的 Schema:**
|
||||
|
||||
**(1) \+ (2) 合并为:iit\_qc\_logs (记录级 \+ 字段级详情)**
|
||||
|
||||
model IitQcLog {
|
||||
id String @id @default(uuid())
|
||||
projectId String
|
||||
recordId String
|
||||
eventId String
|
||||
|
||||
// 核心结果
|
||||
status String // 'PASS' | 'FAIL' | 'WARNING'
|
||||
|
||||
// 字段级详情,直接存 JSONB
|
||||
// 格式: \[{ field: "age", error: "范围越界", level: "RED" }, { ... }\]
|
||||
// 优势: Postgres 支持对 JSONB 内部字段建立索引,查询速度一样快,但表行数少 100 倍
|
||||
issues Json
|
||||
|
||||
ruleVersion String // 对应问题4的解决方案
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index(\[projectId, recordId\])
|
||||
@@map("iit\_qc\_logs")
|
||||
}
|
||||
|
||||
**(3) 保留:iit\_qc\_project\_stats (每日汇总)**
|
||||
|
||||
* 用于 Dashboard 快速展示,避免每次都 COUNT(\*) 几百万行日志。
|
||||
|
||||
## **3\. 主动干预分级:防打扰机制**
|
||||
|
||||
**评价**:非常棒的\*\*“抗疲劳设计”\*\*。如果不分级,PI 一天收 50 条推送,第二天就会把机器人拉黑。
|
||||
|
||||
**落地实现**:
|
||||
|
||||
在 WechatService 中实现一个 **Notification Filter**。
|
||||
|
||||
// NotificationFilter.ts
|
||||
|
||||
async function handleAlert(projectId, logs) {
|
||||
// 1\. 红色:立即发送
|
||||
const redIssues \= logs.filter(i \=\> i.level \=== 'RED');
|
||||
if (redIssues.length \> 0\) {
|
||||
await sendWechat("🚨 紧急报警: 发现 SAE 或严重违规...");
|
||||
await sendSms("..."); // 可选
|
||||
}
|
||||
|
||||
// 2\. 黄色:存入每日摘要队列 (Redis/DB),不立即发
|
||||
const yellowIssues \= logs.filter(i \=\> i.level \=== 'YELLOW');
|
||||
if (yellowIssues.length \> 0\) {
|
||||
await addToDailyDigest(projectId, yellowIssues);
|
||||
}
|
||||
|
||||
// 3\. 绿色:只记录日志,完全不发
|
||||
}
|
||||
|
||||
## **4\. 潜在问题与解决方案:技术细节补全**
|
||||
|
||||
你提出的解决方案都很到位,我从技术落地角度做一点补充。
|
||||
|
||||
### **(1) 规则版本管理**
|
||||
|
||||
* **你的方案**:增加 rule\_version。✅
|
||||
* **补充**:iit\_skills 表本身应该有一个 version 字段(自增 Int)。每次生成 IitQcLog 时,把这个 version 抄进去。这样以后回溯时,就知道当时是按哪套法律判的案。
|
||||
|
||||
### **(2) 性能问题**
|
||||
|
||||
* **你的方案**:Webhook 只查单条,全量用异步。✅
|
||||
* **补充**:这就是我们 V2.9 架构的天然优势。
|
||||
* **实时**:WebhookController \-\> SopEngine (单条)。
|
||||
* **全量**:pg-boss 定时任务 \-\> BatchQcJob (批量)。
|
||||
|
||||
### **(3) 重复质控 (防抖)**
|
||||
|
||||
* **你的方案**:幂等性检查(5分钟)。✅
|
||||
* **技术落地**:利用 pg-boss 的 **Debounce** 功能。
|
||||
// WebhookController.ts
|
||||
// 如果 5 分钟内来了同一个 record\_id 的 Webhook,只执行最后一次
|
||||
await boss.send('qc-job', { recordId }, {
|
||||
singletonKey: \`qc-${projectId}-${recordId}\`, // 唯一键
|
||||
singletonSeconds: 300 // 300秒防抖窗口
|
||||
});
|
||||
|
||||
这样根本不需要写复杂的 Redis 锁,pg-boss 帮你搞定。
|
||||
|
||||
### **(4) 字段映射变更**
|
||||
|
||||
* **你的方案**:重新同步。✅
|
||||
* **补充**:增加一个 **"变更检测"**。
|
||||
* 每次 Webhook 数据来了,检查一下 payload 里的字段名是否在我们的 iit\_field\_mapping 里。如果不认识,说明 REDCap 改了,Agent 自动触发一次 Metadata Sync 任务。
|
||||
|
||||
## **5\. 异步架构落地场景 (Asynchronous Implementation)**
|
||||
|
||||
为了保证高并发下的系统稳定性,本方案在以下 4 个关键环节强制使用异步处理(基于 pg-boss):
|
||||
|
||||
### **5.1 核心质控执行 (Core Execution)**
|
||||
|
||||
* **场景**:CRC 在 REDCap 点击保存,触发 DET Webhook。
|
||||
* **机制**:
|
||||
* **同步层 (Node.js)**:接收 HTTP 请求 ![][image1] 校验签名 ![][image1] 写入 pg-boss 队列 ![][image1] 立即返回 200 OK (耗时 \< 10ms)。
|
||||
* **异步层 (Worker)**:后台 Worker 从队列获取 record\_id ![][image1] 拉取数据 ![][image1] 执行 Engine A/B ![][image1] 写入日志。
|
||||
* **价值**:确保 EDC 前端操作零卡顿,彻底解耦录入与质控。
|
||||
|
||||
### **5.2 批量全量回溯 (Batch Processing)**
|
||||
|
||||
* **场景**:规则变更后(如入排标准修改),需要重新检查历史数据;或每日定时巡检。
|
||||
* **机制**:
|
||||
* **调度器**:创建 BatchQcJob。
|
||||
* **分片执行**:将 1000 条记录拆分为 20 个子任务(每批 50 条),并行推入队列。
|
||||
* **流控**:Worker 控制并发数,避免瞬间打爆 REDCap API。
|
||||
|
||||
### **5.3 "黄色"报警汇总 (Delayed Notifications)**
|
||||
|
||||
* **场景**:发现非紧急的逻辑矛盾或缺失值(黄色/绿色级别)。
|
||||
* **机制**:
|
||||
* **写入**:质控 Worker 发现问题 ![][image1] 写入 iit\_qc\_logs ![][image1] 标记为 pending\_digest。
|
||||
* **发送**:每日下午 17:00 定时任务触发 ![][image1] 聚合当日所有黄色问题 ![][image1] 生成一份简报发送给 PI。
|
||||
* **价值**:防消息轰炸,提升用户体验。
|
||||
|
||||
### **5.4 规则自动同步 (Metadata Sync)**
|
||||
|
||||
* **场景**:项目初始化或 REDCap 字段变更。
|
||||
* **机制**:
|
||||
* **触发**:用户点击"同步字段"按钮。
|
||||
* **执行**:前端收到"任务已提交" ![][image1] 后台 Worker 调用 exportMetadata (耗时较长) ![][image1] 解析并更新 iit\_field\_mapping ![][image1] 更新任务状态。
|
||||
* **价值**:防止长连接超时。
|
||||
|
||||
## **6\. 总结**
|
||||
|
||||
这 4 个建议是**完全成熟的生产级方案**。
|
||||
|
||||
* 它们解决了 **"规则从哪来"** (1)。
|
||||
* 解决了 **"数据怎么存"** (2)。
|
||||
* 解决了 **"怎么发通知"** (3)。
|
||||
* 解决了 **"异常怎么办"** (4)。
|
||||
|
||||
配合之前的 **V2.9 极简架构**(Engine \+ Tools \+ Skill),以及本补充文档中的 **异步处理规范**,这套系统已经具备了极高的商业交付价值。建议直接纳入 PRD 开发。
|
||||
|
||||
[image1]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAXCAYAAADpwXTaAAAAX0lEQVR4XmNgGAWjYCQAeXn50+hiZAOgYU/QxcgGcnJy2kA8HV2cbAB03SwgDkIXB0lIkomnAfF1oBHM1DBsERCfRzGMHCCPy5ukAmgETEAXJwvIUzFpMMpTM9GOcAAAmV0cRTlI2MMAAAAASUVORK5CYII=>
|
||||
@@ -0,0 +1,93 @@
|
||||
# **IIT Manager Agent:智能化临床研究管理解决方案**
|
||||
|
||||
## **—— 策略汇报与核心机制讨论稿**
|
||||
|
||||
**汇报对象:** 临床研究负责人(PI)、申办方策略官、临床方法学专家
|
||||
|
||||
**汇报目标:** 确认 Agent 的行为准则、记忆逻辑与风险控制策略
|
||||
|
||||
**日期:** 2026-02-05
|
||||
|
||||
## **1\. 核心愿景:我们要打造什么样的 AI 助手?**
|
||||
|
||||
我们开发的不仅仅是一个问答机器人,而是一个**24/7 在线的、具备“长期记忆”与“合规意识”的虚拟项目经理**。
|
||||
|
||||
它旨在解决 IIT(研究者发起的临床研究)中的三大痛点:
|
||||
|
||||
1. **数据质控滞后**:往往等到数据锁库前才发现录入错误,修正成本极高。
|
||||
2. **项目记忆断层**:CRC/CRA 人员流动导致对患者情况、历史决策的记忆丢失。
|
||||
3. **执行偏差**:方案(Protocol)执行细节依赖个人经验,难以标准化。
|
||||
|
||||
## **2\. 策略架构:严谨与智能的“双脑”平衡**
|
||||
|
||||
为了适应临床研究既要“死扣方案”又要“灵活应变”的特点,我们设计了\*\*“双脑协同”\*\*模型:
|
||||
|
||||
### **🧠 左脑(严谨执行者)—— 对应“SOP 质控引擎”**
|
||||
|
||||
* **角色**:像一位铁面无私的质控员(QC)。
|
||||
* **职责**:执行入排标准、访视窗口、不良事件(AE)逻辑检查。
|
||||
* **特点**:**零容忍**。它不依赖 AI 的“猜测”,而是基于既定的医学逻辑规则。如果方案规定年龄必须 \<75岁,76岁的患者绝对无法通过。
|
||||
* **价值**:确保合规性,规避审计风险。
|
||||
|
||||
### **🎨 右脑(智能助理)—— 对应“ReAct 推理引擎”**
|
||||
|
||||
* **角色**:像一位经验丰富的 CRC 组长。
|
||||
* **职责**:回答模糊问题(“查一下最近发烧的病人”)、生成周报、解读复杂方案。
|
||||
* **特点**:**灵活**。它能理解自然语言,综合多维度信息给出建议,并具备主动性(如主动提醒访视)。
|
||||
* **价值**:提高效率,降低沟通成本。
|
||||
|
||||
## **3\. 核心议题:记忆系统(Memory System)的策略设置**
|
||||
|
||||
**这是需要方法学团队重点讨论的部分。**
|
||||
|
||||
临床试验周期长达 1-3 年,普通的 AI 聊几句就“忘事”。我们设计了仿生的\*\*“三层记忆体系”\*\*,让 Agent 能够陪伴项目全周期。
|
||||
|
||||
### **3.1 记忆分层与业务含义**
|
||||
|
||||
| 记忆层级 | 对应业务场景 | 策略价值 |
|
||||
| :---- | :---- | :---- |
|
||||
| **L1:短期流水账** *(Working Memory)* | **“刚才说了什么”** 如:刚才提到的患者 ID 是多少? | 保证对话连贯性,像人一样交流,无需重复上下文。 |
|
||||
| **L2:项目热记忆** *(Active Context)* | **“当前关注焦点”** 如:P003 患者依从性差需重点盯防;PI 偏好简报。 | **个性化与主动性**。Agent 知道每个人的角色偏好,也知道当前项目的“风险点”,不再是冷冰冰的机器。 |
|
||||
| **L3:项目历史书** *(Project Archive)* | **“项目大事记”** 如:3个月前为什么修改了入排标准?上周的 SAE 判定结论是什么? | **对抗人员流动**。即使 CRC 换人,Agent 依然记得项目的所有历史决策和关键事件,实现“无缝交接”。 |
|
||||
|
||||
### **3.2 待讨论的策略问题(请方法学团队决策)**
|
||||
|
||||
**Q1:什么样的信息值得进入“历史书”(L3)?**
|
||||
|
||||
* *现状*:我们设定周报自动归档。
|
||||
* *讨论*:是否需要将每一次“违规录入”都永久记录?还是只记录“经人工确认的违规”?这涉及到未来审计的痕迹管理。
|
||||
|
||||
**Q2:Agent 的“主动性”边界在哪里(基于 L2 热记忆)?**
|
||||
|
||||
* *场景*:Agent 发现某患者有脱落风险(基于过往对话)。
|
||||
* *讨论*:是仅仅在周报中提示?还是每天早上发消息提醒 CRC?还是直接向 PI 发出预警?我们需要定义“打扰预算”。
|
||||
|
||||
## **4\. 安全与合规:隐私保护策略**
|
||||
|
||||
针对临床数据的敏感性,我们在技术底层执行了严格的\*\*“隐私隔离策略”\*\*(Phase 1.5 重点任务):
|
||||
|
||||
1. **PII 信息物理隔离**:
|
||||
* Agent 的大脑(大模型)**永远看不到**患者的真实姓名、身份证号或手机号。
|
||||
* 所有敏感信息在发送给 AI 前,都会被替换为代号(如 \[PATIENT\_001\]),AI 处理完逻辑后,再由本地系统还原显示给医生。
|
||||
2. **数据所有权**:
|
||||
* 所有核心临床数据(EDC数据)只存储在私有数据库中,不会用于训练公有模型。
|
||||
|
||||
## **5\. 开发路线图与里程碑**
|
||||
|
||||
我们计划用 **6周** 时间完成 MVP(最小可行性产品)交付:
|
||||
|
||||
* **第 1-2 周(地基阶段)**:完成隐私脱敏中间件、基础质控规则(左脑)。
|
||||
* *交付物*:能自动检查入排标准和逻辑错误的机器人。
|
||||
* **第 3-4 周(记忆觉醒)**:上线三层记忆系统,实现周报自动生成。
|
||||
* *交付物*:一个能记住项目历史、每周向 PI 汇报进度的助手。
|
||||
* **第 5-6 周(智能进化)**:上线右脑推理与主动提醒功能。
|
||||
* *交付物*:能回答复杂问题、主动管理任务的完整 Agent。
|
||||
|
||||
## **6\. 总结与下一步**
|
||||
|
||||
**IIT Manager Agent** 不是在替代 CRC,而是在为临床研究团队配备一个\*\*“永远不睡觉、永远不遗忘、永远守规矩”\*\*的超级助理。
|
||||
|
||||
**下一步行动:**
|
||||
|
||||
1. **方法学团队**:请针对“记忆策略”中的 Q1、Q2 给出指导意见。
|
||||
2. **技术团队**:立即启动 Phase 1 开发,优先部署隐私安全模块。
|
||||
38
docs/03-业务模块/IIT Manager Agent/05-测试文档/Phase 1 补充任务清单.md
Normal file
38
docs/03-业务模块/IIT Manager Agent/05-测试文档/Phase 1 补充任务清单.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# **Phase 1.5 补充任务:隐私安全与自动化工具**
|
||||
|
||||
**优先级:** P0 (必须在正式处理患者数据前完成)
|
||||
|
||||
**目的:** 解决合规性风险,降低项目部署的人力成本
|
||||
|
||||
## **1\. PII 数据脱敏中间件 (Anonymizer Middleware)**
|
||||
|
||||
在 SoftRuleEngine 和 ChatService 调用 LLM 之前,必须对文本进行处理。
|
||||
|
||||
* \[ \] **实现 PII 识别正则库**:
|
||||
* 识别身份证号、手机号、中文姓名(2-4字)、MRN 号。
|
||||
* \[ \] **实现脱敏/还原逻辑**:
|
||||
* **发送前 (Masking)**: 张三 (ID: 420101...) \-\> \[PATIENT\_NAME\_1\] (ID: \[ID\_CARD\_1\])
|
||||
* **接收后 (Unmasking)**: 将 LLM 回复中的 \[PATIENT\_NAME\_1\] 还原为 张三 显示给前端。
|
||||
* \[ \] **安全审计日志**:
|
||||
* 记录所有发送给 LLM 的原始 Payload(加密存储),用于事后合规审计。
|
||||
|
||||
## **2\. Redcap Schema 自动对齐工具 (Auto-Mapper)**
|
||||
|
||||
减少 iit\_field\_mapping 的人工配置工作量。
|
||||
|
||||
* \[ \] **Data Dictionary 解析器**:
|
||||
* 读取 Redcap 导出的 Data Dictionary (CSV/JSON)。
|
||||
* 提取所有字段的 Variable Name 和 Field Label。
|
||||
* \[ \] **LLM 语义映射 Job**:
|
||||
* 输入:系统标准字段列表(如 age, gender, visit\_date)。
|
||||
* 输入:Redcap 字段列表(如 nl\_age, sex\_v2, d\_visit)。
|
||||
* Prompt: "请将以下 Redcap 字段与系统标准字段进行语义匹配,返回 JSON 映射表。"
|
||||
* \[ \] **人工确认 UI**:
|
||||
* 在管理后台提供一个界面,显示 LLM 猜测的映射关系,管理员点击 "Confirm" 后写入数据库。
|
||||
|
||||
## **3\. 错误处理与熔断机制**
|
||||
|
||||
* \[ \] **ReAct 循环熔断**:
|
||||
* 设置 SoftRuleEngine 最大重试次数为 3。
|
||||
* 设置 ReActEngine 最大 Step 为 5。
|
||||
* 超过限制时,返回固定的 Fallback 回复:"抱歉,该任务过于复杂或数据不足,请人工介入。"
|
||||
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/原发性痛经队列研究方案.pdf
Normal file
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/原发性痛经队列研究方案.pdf
Normal file
Binary file not shown.
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/原发性痛经队列研究病例报告表.pdf
Normal file
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/原发性痛经队列研究病例报告表.pdf
Normal file
Binary file not shown.
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/变量设置范围.docx
Normal file
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/变量设置范围.docx
Normal file
Binary file not shown.
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/模拟数据_10例.xlsx
Normal file
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/模拟数据_10例.xlsx
Normal file
Binary file not shown.
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/知情同意书.docx
Normal file
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/知情同意书.docx
Normal file
Binary file not shown.
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/纳入排除标准.docx
Normal file
BIN
docs/03-业务模块/IIT Manager Agent/05-测试文档/测试案例/纳入排除标准.docx
Normal file
Binary file not shown.
243
docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-02-07-实时质控系统开发记录.md
Normal file
243
docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-02-07-实时质控系统开发记录.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# 2026-02-07 实时质控系统开发记录
|
||||
|
||||
> **开发日期:** 2026-02-07
|
||||
> **开发者:** AI Assistant
|
||||
> **开发时长:** 约 4 小时
|
||||
> **主要内容:** 实时质控系统核心功能实现
|
||||
|
||||
---
|
||||
|
||||
## 一、开发背景
|
||||
|
||||
根据 [06-实时质控系统开发计划.md](../04-开发计划/06-实时质控系统开发计划.md) 的设计,今天完成了实时质控系统的核心功能开发,包括:
|
||||
|
||||
- 数据库表创建
|
||||
- Webhook 防抖机制
|
||||
- Worker 双产出改造
|
||||
- AI 查询优化
|
||||
- 管理端批量操作功能
|
||||
|
||||
---
|
||||
|
||||
## 二、完成的开发任务
|
||||
|
||||
### 2.1 数据库表创建(Phase 1 & 2)
|
||||
|
||||
新增 4 个质控相关表到 `iit_schema`:
|
||||
|
||||
| 表名 | Prisma 模型 | 用途 |
|
||||
|------|-------------|------|
|
||||
| `field_metadata` | `IitFieldMetadata` | REDCap 字段元数据同步 |
|
||||
| `qc_logs` | `IitQcLog` | 质控日志(仅新增,审计轨迹) |
|
||||
| `record_summary` | `IitRecordSummary` | 录入汇总(upsert,最新状态) |
|
||||
| `qc_project_stats` | `IitQcProjectStats` | 项目级统计(Dashboard 用) |
|
||||
|
||||
**核心设计原则:**
|
||||
- `qc_logs`:**仅新增**,不覆盖,保留完整审计轨迹
|
||||
- `record_summary`:**upsert**,每个记录只有一条汇总
|
||||
- 支持 `formName` 字段,用于单表质控规则过滤
|
||||
|
||||
### 2.2 WebhookController 重构
|
||||
|
||||
**文件:** `backend/src/modules/iit-manager/controllers/WebhookController.ts`
|
||||
|
||||
**改动:**
|
||||
1. 移除旧的 `checkDuplicate()` 方法
|
||||
2. 使用 pg-boss `singletonKey` 实现防抖
|
||||
3. Payload 精简:移除 `records` 数据,改在 Worker 中获取
|
||||
|
||||
**关键代码:**
|
||||
```typescript
|
||||
await jobQueue.push('iit_quality_check', {
|
||||
projectId: projectConfig.id,
|
||||
recordId: payload.record,
|
||||
instrument: payload.instrument,
|
||||
eventId: payload.redcap_event_name,
|
||||
triggeredBy: 'webhook',
|
||||
// pg-boss 防抖参数
|
||||
__singletonKey: `qc-${projectConfig.id}-${payload.record}`,
|
||||
__singletonSeconds: 300, // 5分钟
|
||||
__expireInSeconds: 15 * 60, // 15分钟过期
|
||||
});
|
||||
```
|
||||
|
||||
### 2.3 PgBossQueue 增强
|
||||
|
||||
**文件:** `backend/src/common/jobs/PgBossQueue.ts`
|
||||
|
||||
**改动:**
|
||||
- 支持通过 `data` 中的特殊字段传递 pg-boss options
|
||||
- 新增字段:`__singletonKey`, `__singletonSeconds`, `__expireInSeconds`
|
||||
|
||||
### 2.4 Worker 双产出改造
|
||||
|
||||
**文件:** `backend/src/modules/iit-manager/index.ts`
|
||||
|
||||
**改动:**
|
||||
- 一次 Worker 执行,两个产出:
|
||||
- 产出1: `iit_qc_logs`(仅新增,审计轨迹)
|
||||
- 产出2: `iit_record_summary`(upsert,最新状态)
|
||||
- 分级干预:只有 FAIL 状态才发企业微信通知
|
||||
|
||||
### 2.5 HardRuleEngine 增强
|
||||
|
||||
**文件:** `backend/src/modules/iit-manager/engines/HardRuleEngine.ts`
|
||||
|
||||
**改动:**
|
||||
- `createHardRuleEngine()` 新增可选参数 `formName`
|
||||
- 支持按表单名过滤规则(用于单表实时质控)
|
||||
|
||||
### 2.6 QcService 创建
|
||||
|
||||
**文件:** `backend/src/modules/iit-manager/services/QcService.ts`(新建)
|
||||
|
||||
**功能:**
|
||||
- `queryQcLogs()` - 查询质控日志
|
||||
- `getRecordQcSummary()` - 获取记录质控摘要
|
||||
- `getRecordSummary()` - 获取录入汇总
|
||||
- `getProjectStats()` - 获取项目统计
|
||||
- `getProblematicRecords()` - 获取问题记录
|
||||
- `getQcTrend()` - 获取质控趋势
|
||||
|
||||
### 2.7 ChatService 优化
|
||||
|
||||
**文件:** `backend/src/modules/iit-manager/services/ChatService.ts`
|
||||
|
||||
**改动:**
|
||||
1. 新增意图识别:
|
||||
- `query_enrollment` - 录入进度查询
|
||||
- `query_qc_status` - 质控状态查询
|
||||
2. 优先查询汇总表/质控表,而不是每次都调用 REDCap
|
||||
3. `qcSingleRecord()` 优先返回缓存的质控结果(1小时内)
|
||||
|
||||
### 2.8 管理端批量操作功能
|
||||
|
||||
#### 后端 API
|
||||
|
||||
**新增文件:**
|
||||
- `backend/src/modules/admin/iit-projects/iitBatchController.ts`
|
||||
- `backend/src/modules/admin/iit-projects/iitBatchRoutes.ts`
|
||||
|
||||
**API 端点:**
|
||||
- `POST /api/v1/admin/iit-projects/:projectId/batch-qc` - 一键全量质控
|
||||
- `POST /api/v1/admin/iit-projects/:projectId/batch-summary` - 一键全量数据汇总
|
||||
|
||||
#### 前端 UI
|
||||
|
||||
**修改文件:**
|
||||
- `frontend-v2/src/modules/admin/pages/IitProjectDetailPage.tsx`
|
||||
- `frontend-v2/src/modules/admin/api/iitProjectApi.ts`
|
||||
|
||||
**功能:**
|
||||
- 在 IIT 项目详情页顶部添加两个按钮
|
||||
- ⚡ **一键全量质控** - 对所有记录执行质控
|
||||
- 📊 **一键全量汇总** - 同步所有记录的录入状态
|
||||
|
||||
---
|
||||
|
||||
## 三、修改文件清单
|
||||
|
||||
| 文件路径 | 操作 | 说明 |
|
||||
|---------|------|------|
|
||||
| `backend/prisma/schema.prisma` | 修改 | 新增 4 个质控相关表 |
|
||||
| `backend/src/modules/iit-manager/controllers/WebhookController.ts` | 修改 | pg-boss 防抖 |
|
||||
| `backend/src/common/jobs/PgBossQueue.ts` | 修改 | 支持自定义 singletonKey |
|
||||
| `backend/src/modules/iit-manager/index.ts` | 修改 | Worker 双产出 |
|
||||
| `backend/src/modules/iit-manager/engines/HardRuleEngine.ts` | 修改 | 按表单过滤规则 |
|
||||
| `backend/src/modules/iit-manager/services/QcService.ts` | **新建** | 质控查询服务 |
|
||||
| `backend/src/modules/iit-manager/services/ChatService.ts` | 修改 | 意图识别优化 |
|
||||
| `backend/src/modules/admin/iit-projects/iitBatchController.ts` | **新建** | 批量操作 Controller |
|
||||
| `backend/src/modules/admin/iit-projects/iitBatchRoutes.ts` | **新建** | 批量操作路由 |
|
||||
| `backend/src/modules/admin/iit-projects/index.ts` | 修改 | 导出新模块 |
|
||||
| `backend/src/index.ts` | 修改 | 注册批量操作路由 |
|
||||
| `frontend-v2/src/modules/admin/pages/IitProjectDetailPage.tsx` | 修改 | 添加批量操作按钮 |
|
||||
| `frontend-v2/src/modules/admin/api/iitProjectApi.ts` | 修改 | 添加批量操作 API |
|
||||
|
||||
---
|
||||
|
||||
## 四、数据库变更
|
||||
|
||||
### 数据库备份
|
||||
|
||||
```bash
|
||||
# 备份位置(Docker 容器内)
|
||||
/tmp/backup_iit_schema_20260207.dump
|
||||
```
|
||||
|
||||
### 执行命令
|
||||
|
||||
```bash
|
||||
npx prisma db push --accept-data-loss
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、待测试内容
|
||||
|
||||
1. **端到端测试**:REDCap 录入 → Webhook → Worker → 质控日志 + 录入汇总
|
||||
2. **批量操作测试**:一键全量质控、一键全量汇总
|
||||
3. **AI 查询测试**:验证优先查询汇总表的效果
|
||||
4. **防抖测试**:5分钟内重复 Webhook 不重复执行
|
||||
|
||||
---
|
||||
|
||||
## 六、架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 实时质控系统架构 │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌───────────────────┐ ┌──────────────────────────┐ │
|
||||
│ │ REDCap │───▶│ WebhookController │───▶│ pg-boss Queue │ │
|
||||
│ │ (DET) │ │ (singletonKey防抖)│ │ (iit_quality_check) │ │
|
||||
│ └─────────────┘ └───────────────────┘ └────────────┬─────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ QC Worker │ │
|
||||
│ │ (一次执行,双产出) │ │
|
||||
│ └────────────┬─────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────────┼────────────┐ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────┐ │
|
||||
│ │ iit_qc_logs │ │iit_record_summary│ │ 企微 │ │
|
||||
│ │ (仅新增,审计) │ │ (upsert,汇总) │ │ 通知 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └──────┘ │
|
||||
│ ▲ ▲ │
|
||||
│ │ │ │
|
||||
│ └──────────────┬─────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ ChatService / QcService │ │
|
||||
│ │ (优先查询汇总表,而非每次调 REDCap) │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
│ ▲ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────────────┴──────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────┐ ┌─────────────┐ │
|
||||
│ │ 企业微信端 │ │ 管理端 │ │
|
||||
│ │ (AI 问答) │ │ (批量操作) │ │
|
||||
│ └─────────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、下一步计划
|
||||
|
||||
1. 重启前后端服务进行端到端测试
|
||||
2. 验证质控闭环功能
|
||||
3. 验证 AI 回答问题的准确性提升
|
||||
4. 考虑实现日批量质控(Cron Job)
|
||||
|
||||
---
|
||||
|
||||
**记录人:** AI Assistant
|
||||
**记录时间:** 2026-02-07
|
||||
Reference in New Issue
Block a user