feat(iit): QC deep fix + V3.1 architecture plan + project member management
QC System Deep Fix: - HardRuleEngine: add null tolerance + field availability pre-check (skipped status) - SkillRunner: baseline data merge for follow-up events + field availability check - QcReportService: record-level pass rate calculation + accurate LLM XML report - iitBatchController: legacy log cleanup (eventId=null) + upsert RecordSummary - seed-iit-qc-rules: null/empty string tolerance + applicableEvents config V3.1 Architecture Design (docs only, no code changes): - QC engine V3.1 plan: 5-level data structure (CDISC ODM) + D1-D7 dimensions - Three-batch implementation strategy (A: foundation, B: bubbling, C: new engines) - Architecture team review: 4 whitepapers reviewed + feedback doc + 4 critical suggestions - CRA Agent strategy roadmap + CRA 4-tool explanation doc for clinical experts Project Member Management: - Cross-tenant member search and assignment (remove tenant restriction) - IIT project detail page enhancement with tabbed layout (KB + members) - IitProjectContext for business-side project selection - System-KB route access control adjustment for project operators Frontend: - AdminLayout sidebar menu restructure - IitLayout with project context provider - IitMemberManagePage new component - Business-side pages adapt to project context Prisma: - 2 new migrations (user-project RBAC + is_demo flag) - Schema updates for project member management Made-with: Cursor
This commit is contained in:
@@ -1,23 +1,25 @@
|
||||
# IIT Manager Agent模块 - 当前状态与开发指南
|
||||
|
||||
> **文档版本:** v3.0
|
||||
> **文档版本:** v3.1
|
||||
> **创建日期:** 2026-01-01
|
||||
> **维护者:** IIT Manager开发团队
|
||||
> **最后更新:** 2026-02-26 **前端架构调整 + Web Chat 上线**
|
||||
> **最后更新:** 2026-03-01 **质控引擎 V3.1 架构升级计划定稿(五级数据结构 + 多维报告)**
|
||||
> **重大里程碑:**
|
||||
> - **2026-03-01:质控引擎 V3.1 架构设计完成!** 五级数据结构(CDISC ODM)+ D1-D7 多维报告 + 三批次落地计划
|
||||
> - **2026-03-01:架构团队评审完成!** 采纳 InstanceID/规则分类/状态冒泡/LLM 三不原则,暂缓 SDV/自动映射/GCP 全量报表
|
||||
> - **2026-02-26:前端架构调整完成!** 运营管理端恢复 IIT 项目管理 + 业务端精简为日常使用 + Web AI 对话页面上线
|
||||
> - **2026-02-26:CRA Agent V3.0 P0+P1 全部完成!** 自驱动质控流水线 + ChatOrchestrator + LLM Function Calling + E2E 54/54 通过
|
||||
> - **2026-02-25:CRA Agent V3.0 开发计划定稿**(替代 CRA 定位 + 报告驱动架构 + 4 语义化工具 + 统一驾驶舱)
|
||||
> - ✅ 2026-02-25:QC 系统深度修复(null tolerance + baseline merge + record-level pass rate)
|
||||
> - ✅ 2026-02-08:事件级质控架构 V3.1 完成(record+event 独立质控 + 规则动态过滤 + 报告去重)
|
||||
> - ✅ 2026-02-08:质控驾驶舱 UI 开发完成(驾驶舱页面 + 热力图 + 详情抽屉)
|
||||
> - ✅ 2026-02-07:实时质控系统开发完成(pg-boss防抖 + 质控日志 + 录入汇总 + 管理端批量操作)
|
||||
> - ✅ 2026-02-02:REDCap 生产环境部署完成(ECS + RDS + HTTPS)
|
||||
> - ✅ 2026-01-04:混合检索架构实现(REDCap实时数据 + Dify文档知识库)
|
||||
> **文档目的:** 反映模块真实状态,记录开发历程
|
||||
> **V3.0 核心文档:**
|
||||
> - [CRA Agent V3.0 开发计划](./04-开发计划/V3.0全新开发计划/V3.0全新开发计划.md)
|
||||
> - [统一数字 CRA 质控平台 PRD](./04-开发计划/V3.0全新开发计划/统一数字%20CRA%20质控平台产品需求文档(PRD).md)
|
||||
> - [UI 原型](./04-开发计划/V3.0全新开发计划/Final%20CRA质控平台V3.html)
|
||||
> **V3.1 核心文档(质控引擎架构升级):**
|
||||
> - [质控引擎 V3.1 开发计划(唯一开发文档)](./04-开发计划/V3.0全新开发计划/质控引擎V3.1架构升级-五级数据结构与多维报告开发计划.md)
|
||||
> - [CRA Agent 质控体系全景技术路径(策略评审稿)](./00-系统设计/CRA%20Agent%20质控体系全景技术路径(策略评审稿).md)
|
||||
> - [五层数据架构方案评审反馈](./09-技术评审报告/五层数据架构方案评审反馈.md)
|
||||
> - [CRA 四大工具工作原理说明](./08-对外输出报告/CRA智能质控Agent-四大工具工作原理说明.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -53,24 +55,22 @@ CRA Agent 是一个**替代 CRA 岗位的自主 AI Agent**,而非辅助 CRA
|
||||
- AI能力:DeepSeek/Qwen + 自研 RAG(pgvector)+ LLM Tool Use
|
||||
|
||||
### 当前状态
|
||||
- **开发阶段**:**V3.0 P0 + P1 已完成,E2E 测试 54/54 通过**
|
||||
- **P0 已完成**(自驱动质控流水线):
|
||||
- 变量清单导入 + 可视化
|
||||
- 规则配置增强(4 类规则 + AI 辅助建议)
|
||||
- 定时质控 + 报告生成 + eQuery 闭环 + 重大事件归档
|
||||
- 统一质控驾驶舱(健康分 + 趋势图 + 风险热力图)+ AI Stream Timeline
|
||||
- **P1 已完成**(对话层 Tool Use 改造):
|
||||
- LLM Adapter 原生 Function Calling(DeepSeek / GPT-5 / Claude-4.5)
|
||||
- 4 语义化工具:`read_report` / `look_up_data` / `check_quality` / `search_knowledge`
|
||||
- ChatOrchestrator 轻量 ReAct(max 3 轮 Function Calling loop)
|
||||
- ChatService (1,442行) 已废弃,替换为 ChatOrchestrator (~160行)
|
||||
- **前端架构调整已完成**(2026-02-26):
|
||||
- 运营管理端:恢复 IIT 项目管理(项目列表 + 配置 + 驾驶舱),供运营团队使用
|
||||
- 业务端 CRA 质控:精简为 5 个日常页面(驾驶舱 / 报告 / AI 工作流水 / AI 对话 / eQuery),供 PI/CRC 使用
|
||||
- 新增 Web AI 对话页面(`/iit/chat`)+ 后端 `POST /api/v1/iit/chat` 端点
|
||||
- **开发阶段**:**V3.0 P0 + P1 已完成 → 正在规划 V3.1 质控引擎架构升级**
|
||||
- **V3.0 已完成**:
|
||||
- P0 自驱动质控流水线 + P1 对话层 Tool Use 改造 + E2E 54/54 通过
|
||||
- QC 系统深度修复(null tolerance + baseline merge + record-level pass rate + LLM 报告修正)
|
||||
- 前端架构调整(运营管理端 + 业务端 + Web AI 对话)
|
||||
- 项目成员管理(跨租户添加成员 + 项目知识库)
|
||||
- CRA 四大工具工作原理文档输出
|
||||
- **V3.1 规划中**(质控引擎架构升级):
|
||||
- **核心升级**:三级数据结构 → CDISC ODM 五级结构(Record → Event → Form → Instance → Field)
|
||||
- **报告维度**:从单一通过率 → D1-D7 七维质控报告(入排/完整性/准确性/Query/安全性/方案偏离/药物管理)
|
||||
- **触发模式**:手动 + 定时 Cron + REDCap DET 实时触发
|
||||
- **关键机制**:状态冒泡 + eQuery 自动闭环 + Record-Level Context + Event-Aware 完整性
|
||||
- **落地策略**:三批次落地(A 数据地基 → B 聚合冒泡 → C 新维度引擎)
|
||||
- **开发计划文档**:[质控引擎 V3.1 开发计划](./04-开发计划/V3.0全新开发计划/质控引擎V3.1架构升级-五级数据结构与多维报告开发计划.md)
|
||||
- **产品定位明确**:运营团队配置项目(白手套服务) → 终端用户使用质控平台
|
||||
- **待开发**:P1-2 对话体验优化 / P2 可选功能
|
||||
- **代码规模**:后端 ~14,000+ 行(净减 ~1,100 行)/ 20 张表(iit_schema)
|
||||
- **代码规模**:后端 ~14,000+ 行 / 20 张表(iit_schema)
|
||||
|
||||
#### ✅ 已完成功能(基础设施)
|
||||
- ✅ 数据库Schema创建(iit_schema,20个表 = 原5个 + 4质控表 + 2新增(equery/critical_events) + 9其他)
|
||||
@@ -108,6 +108,17 @@ CRA Agent 是一个**替代 CRA 岗位的自主 AI Agent**,而非辅助 CRA
|
||||
- 运营管理端(`/admin/iit-projects`)→ 运营团队:创建项目、配置 REDCap、设规则
|
||||
- 业务端(`/iit/`)→ PI/CRC:驾驶舱、报告、AI 工作流水、AI 对话、eQuery
|
||||
|
||||
#### ✅ 已完成功能(QC 系统深度修复 + 架构评审 - 2026-02-25 ~ 03-01)
|
||||
- ✅ **QC 规则修复**(seed-iit-qc-rules.ts:null/空字符串容忍 + applicableEvents 配置)
|
||||
- ✅ **HardRuleEngine 增强**(字段可用性预检查 + skipped 状态)
|
||||
- ✅ **SkillRunner 基线数据合并**(随访事件自动合并基线数据)
|
||||
- ✅ **QcReportService 修正**(Record-Level 通过率计算 + LLM XML 报告准确性)
|
||||
- ✅ **批量质控修复**(iitBatchController:legacy log 清理 + upsert RecordSummary)
|
||||
- ✅ **CRA 四大工具原理文档**(面向临床专家的工具工作原理说明文档)
|
||||
- ✅ **质控引擎 V3.1 架构设计**(五级数据结构 + D1-D7 维度 + 三批次落地计划)
|
||||
- ✅ **架构团队评审完成**(4 份白皮书评审 + 评审反馈文档 + 4 条关键落地建议采纳)
|
||||
- ✅ **V3.0 → V3.1 计划迁移**(V3.1 为唯一开发文档,V3.0 存档供参考)
|
||||
|
||||
#### ✅ 已完成功能(实时质控系统 - 2026-02-07)
|
||||
- ✅ **质控数据库表**(iit_qc_logs + iit_record_summary + iit_qc_project_stats + iit_field_metadata)
|
||||
- ✅ **pg-boss 防抖机制**(WebhookController + singletonKey)
|
||||
@@ -145,18 +156,37 @@ CRA Agent 是一个**替代 CRA 岗位的自主 AI Agent**,而非辅助 CRA
|
||||
- ✅ **AI 意图识别增强**(支持"严重违规有几项"等自然语言查询)
|
||||
- ✅ **Bug 修复**(formatPatientData 500 错误 + 记录数统计 + 报告限制移除)
|
||||
|
||||
#### ⏳ V3.0 开发计划(2026-02-25 定稿)
|
||||
#### ✅ V3.0 开发计划(已完成)
|
||||
|
||||
> 详见:[CRA Agent V3.0 开发计划](./04-开发计划/V3.0全新开发计划/V3.0全新开发计划.md)
|
||||
|
||||
| 阶段 | 内容 | 预估 | 状态 |
|
||||
| 阶段 | 内容 | 状态 |
|
||||
|------|------|------|
|
||||
| **P0-1** | ChatOrchestrator + ToolsService 重构 | ✅ 已完成 |
|
||||
| **P0-2** | read_report + search_knowledge 实现 | ✅ 已完成 |
|
||||
| **P0-3** | 定时质控 + 报告生成 + eQuery 闭环 | ✅ 已完成 |
|
||||
| **P0-4** | 统一质控驾驶舱 | ✅ 已完成 |
|
||||
| **P1** | 对话层 Tool Use + E2E 测试 54/54 | ✅ 已完成 |
|
||||
|
||||
#### ✅ QC 系统深度修复(2026-02-25)
|
||||
|
||||
| 修复项 | 说明 |
|
||||
|--------|------|
|
||||
| Null Tolerance | 质控规则增加 null/空字符串容忍,未填数据不再误判为 FAIL |
|
||||
| Baseline Data Merge | SkillRunner 合并基线事件数据,解决随访期规则缺字段问题 |
|
||||
| Record-Level Pass Rate | QcReportService 改为按受试者最严重状态计算通过率 |
|
||||
| Legacy Log Cleanup | 批量质控前清理 eventId 为 null 的历史脏数据 |
|
||||
| LLM 报告修正 | XML 报告准确反映真实质控结果(从 0% → 92.9%) |
|
||||
|
||||
#### ⏳ V3.1 质控引擎架构升级(规划中)
|
||||
|
||||
> 详见:[质控引擎 V3.1 开发计划(唯一开发文档)](./04-开发计划/V3.0全新开发计划/质控引擎V3.1架构升级-五级数据结构与多维报告开发计划.md)
|
||||
|
||||
| 批次 | 内容 | 预估 | 状态 |
|
||||
|------|------|------|------|
|
||||
| **P0-1** | ChatOrchestrator + ToolsService 重构(4 语义化工具 + LLM Tool Use) | 2 天 | 待开始 |
|
||||
| **P0-2** | report 工具补全(read_report + search_knowledge 实现) | 1.5 天 | 待开始 |
|
||||
| **P0-3** | 定时质控 + 报告生成 + eQuery 闭环(pg-boss cron + 全量/增量报告 + eQuery 状态机) | 3.5 天 | 待开始 |
|
||||
| **P0-4** | 统一质控驾驶舱(概览 + AI Stream + eQuery 管理 + 报告归档 + 重大事件库) | 2.5 天 | 待开始 |
|
||||
| **P1-1** | 企业微信主动推送(日报/告警/eQuery 通知) | 2 天 | 待开始 |
|
||||
| **P1-2** | 管理端配置增强(规则模板 + 多项目切换) | 2 天 | 待开始 |
|
||||
| **Batch A** | 数据地基(五级状态表 DDL + QcExecutor + InstanceID 标准化 + D1/D3 迁移) | ~5 天 | 待开始 |
|
||||
| **Batch B** | 聚合冒泡(状态冒泡 + 多维报告 + Cron 灵活配置 + 驾驶舱升级) | ~5 天 | 待开始(依赖 A) |
|
||||
| **Batch C** | 新维度引擎(D2 完整性 + D5 安全性 + D6 方案偏离 + 沙盒测试) | ~5 天 | 待开始(依赖 B + 临床专家确认) |
|
||||
| **P2** | 长期可选(PII 脱敏 / SDV 视觉核对 / AutoMapper / 数据响应质量评级) | - | 待开始 |
|
||||
|
||||
**P0 合计:约 9.5 天 / P0+P1 合计:约 13.5 天(1 人)**
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
# CRA Agent 质控体系全景技术路径(策略评审稿)
|
||||
|
||||
> **文档性质**:策略与路径评审文档,供团队内部和临床研究专家讨论可行性
|
||||
> **版本**:V1.0 | 2026-03-01
|
||||
> **目标读者**:技术团队、产品负责人、临床研究顾问
|
||||
> **下一步**:经评审认可后,基于本文档形成具体开发计划
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与核心问题
|
||||
|
||||
### 1.1 CRA 做什么
|
||||
|
||||
临床监查员(CRA)的工作本质上是一个**多维度的数据质量守门人**。参照行业标准(ICH-GCP E6 R2/R3),CRA 在常规监查阶段的工作可以分解为以下 7 个维度:
|
||||
|
||||
| 维度 | CRA 具体工作 | 输出物 |
|
||||
|------|------------|--------|
|
||||
| **D1 入组质量** | 核查入排标准、知情同意时序、筛选流程 | 筛选与入组日志 |
|
||||
| **D2 数据完整性** | 检查缺失字段、录入时效、分支逻辑完整性 | 数据缺失率报表 |
|
||||
| **D3 变量准确性** | 极值校验、时序逻辑、跨表单关联 | 变量质控清单 |
|
||||
| **D4 数据质疑管理** | 生成 Query、跟踪回复、二次复核、关闭 | eQuery 清单 |
|
||||
| **D5 安全性监测** | AE/SAE 识别、编码、分级、因果判定、时效监管 | AE/SAE 追踪表 |
|
||||
| **D6 方案偏离侦测** | 访视超窗、剂量偏差、操作遗漏、CAPA 跟踪 | 方案偏离记录 |
|
||||
| **D7 药物管理** | 发药/回收核算、依从性计算、批号追踪 | IP 管理明细 |
|
||||
|
||||
以上 7 个维度最终汇聚为一张 **项目健康度评分卡**,供 PI 和项目管理层决策。
|
||||
|
||||
### 1.2 我们现在能做什么
|
||||
|
||||
当前 CRA Agent 系统已实现的能力:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ CRA 工作全景(7 维度) │
|
||||
│ │
|
||||
已实现 ──────► │ ■ D1 入组质量(入排标准规则已有) │
|
||||
│ □ D2 数据完整性 │
|
||||
部分实现 ────► │ ■ D3 变量准确性(基础范围检查) │
|
||||
│ □ D4 数据质疑管理(eQuery 表已建) │
|
||||
│ □ D5 安全性监测 │
|
||||
│ □ D6 方案偏离侦测 │
|
||||
未涉及 ─────► │ □ D7 药物管理 │
|
||||
│ │
|
||||
│ □ 项目健康度评分卡 │
|
||||
└──────────────────────────────────────────┘
|
||||
|
||||
■ 已实现 ■ 部分实现 □ 未涉及
|
||||
```
|
||||
|
||||
**覆盖率估算**:约 20-25%(仅 D1 部分 + D3 基础),距离替代 CRA 70-80% 工作的目标差距明显。
|
||||
|
||||
### 1.3 差距的根源
|
||||
|
||||
差距不仅是"规则数量不够",而是**架构层面的三个结构性问题**:
|
||||
|
||||
| 问题 | 影响 |
|
||||
|------|------|
|
||||
| **规则体系缺乏分类** | 所有规则混在一起,无法按 CRA 工作维度组织和统计 |
|
||||
| **数据结构粒度不足** | 只有记录级状态,缺少事件级和变量级独立状态表 |
|
||||
| **报告缺乏维度** | 只有一张扁平的"通过/不通过"报告,无法反映 7 个维度的各自状况 |
|
||||
|
||||
---
|
||||
|
||||
## 二、核心认知:一条完整的链路
|
||||
|
||||
### 2.1 从方案到报告的 1:1 映射链
|
||||
|
||||
```
|
||||
研究方案(Protocol)
|
||||
│
|
||||
│ "年龄 16-35 岁、已签知情同意、无排除标准..."
|
||||
│
|
||||
▼
|
||||
变量清单(Data Dictionary)
|
||||
│
|
||||
│ age(数值型)、consent_date(日期型)、exclusion_criteria(选择型)...
|
||||
│
|
||||
▼
|
||||
质控规则(QC Rules)──── 按 7 大维度分类
|
||||
│
|
||||
│ D1: { "and": [{">=": ["age", 16]}, {"<=": ["age", 35]}] }
|
||||
│ D3: { "<": ["consent_date", "screening_date"] }
|
||||
│ D6: { "<=": [{"abs_diff": ["visit_date", "target_date"]}, 3] }
|
||||
│
|
||||
▼
|
||||
三级质控状态(变量级 → 事件级 → 记录级)
|
||||
│
|
||||
│ record_3 / baseline / exclusion_criteria → FAIL (D1:排除标准)
|
||||
│ record_3 / baseline → FAIL
|
||||
│ record_3 → FAIL
|
||||
│
|
||||
▼
|
||||
多维质控报告(7 个维度各自独立汇总)
|
||||
│
|
||||
│ D1 入组质量: 13/14 通过 (92.9%)
|
||||
│ D3 变量准确性: 98.5% 合格率
|
||||
│ D5 安全性: 0 例未报告 AE
|
||||
│ ...
|
||||
│
|
||||
▼
|
||||
项目健康度评分(加权综合)
|
||||
│
|
||||
│ 综合评分: 87/100
|
||||
│ 建议: 关注 3 号受试者入组资格
|
||||
│
|
||||
▼
|
||||
行动(eQuery 派发 / 告警推送 / 监查报告)
|
||||
```
|
||||
|
||||
**关键洞察**:这条链路中的每一环都需要"维度(category)"这根线串起来。变量清单中的字段属于哪个表单→ 规则属于哪个维度→ 质控结果按维度汇总→ 报告按维度呈现。
|
||||
|
||||
### 2.2 IIT 场景的特殊性
|
||||
|
||||
我们的目标用户是 IIT(研究者发起的试验),与注册临床试验有显著区别:
|
||||
|
||||
| 特征 | 注册试验 | IIT 项目 |
|
||||
|------|---------|---------|
|
||||
| 有无专职 CRA | 有,全职 CRA 团队 | 通常没有,或仅兼职 |
|
||||
| 监管严格度 | 极高(FDA/NMPA 审查) | 中等(伦理委员会审查) |
|
||||
| EDC 系统 | 商业 EDC(Medidata 等) | 多用 REDCap(免费) |
|
||||
| 预算 | 充足 | 有限 |
|
||||
| AI 替代的意义 | 从 100% 提升到 110%(锦上添花) | **从 0% 提升到 70%(雪中送炭)** |
|
||||
|
||||
**这意味着**:
|
||||
- 我们不需要追求注册试验级别的完美覆盖
|
||||
- 优先覆盖 IIT 中**最常出问题、最容易被忽视**的维度
|
||||
- 先做"有胜于无"的基础覆盖,再做精细化提升
|
||||
|
||||
---
|
||||
|
||||
## 三、技术路径:三阶段演进
|
||||
|
||||
### 阶段一:数据质量基石(当前 → 近期)
|
||||
|
||||
**目标**:把已有的能力做扎实,建立正确的数据基础架构。
|
||||
|
||||
#### 核心工作
|
||||
|
||||
```
|
||||
1. 三级质控数据结构
|
||||
├── 变量级状态表(qc_field_status)
|
||||
├── 事件级状态表(qc_event_status)
|
||||
└── 记录级状态表(record_summary 改造)
|
||||
|
||||
2. 规则分类体系
|
||||
└── 每条规则标注所属维度(D1-D7)
|
||||
|
||||
3. 多模式触发
|
||||
├── 实时触发(REDCap DET Webhook,已有代码)
|
||||
├── 定时触发(项目级 Cron 配置,取代全局硬编码)
|
||||
└── 手动触发(一键全量质控 + AI 对话)
|
||||
|
||||
4. 报告结构升级
|
||||
└── 从扁平报告 → 多维度报告框架
|
||||
```
|
||||
|
||||
#### 覆盖的 CRA 维度
|
||||
|
||||
| 维度 | 阶段一目标 |
|
||||
|------|-----------|
|
||||
| D1 入组质量 | 已有入排规则,补充知情同意时序检查 |
|
||||
| D3 变量准确性 | 已有范围检查,补充时序逻辑和跨表单关联 |
|
||||
| D4 数据质疑 | 已有 eQuery 表,补充闭环状态机 |
|
||||
|
||||
#### 需要确认的问题
|
||||
|
||||
> **问临床专家**:
|
||||
> 1. IIT 项目中,入排标准检查和变量范围检查是否是最高优先级?
|
||||
> 2. 知情同意日期 < 筛选日期 < 基线日期的时序校验,在 IIT 中是否必须?
|
||||
> 3. 跨表单校验(如性别 vs 妊娠检查)在 IIT 中常见吗?
|
||||
|
||||
---
|
||||
|
||||
### 阶段二:方案合规覆盖(中期)
|
||||
|
||||
**目标**:覆盖 CRA 日常监查中最高频的工作——数据完整性检查和方案偏离侦测。
|
||||
|
||||
#### 核心工作
|
||||
|
||||
```
|
||||
1. 数据完整性引擎(D2)
|
||||
├── 按事件/表单统计应填字段 vs 实际已填
|
||||
├── 分支逻辑感知(排除不应填的字段)
|
||||
├── 录入时效统计(REDCap 审计日志 → 计算延迟天数)
|
||||
└── 缺失率阈值告警
|
||||
|
||||
2. 方案偏离侦测引擎(D6)
|
||||
├── 访视窗口检查(目标日 ± 允许天数)
|
||||
├── 操作遗漏检查(Schedule of Assessment 对照)
|
||||
└── 偏离严重性自动分级(Major / Minor)
|
||||
|
||||
3. 报告增强
|
||||
├── 新增"数据完整性"章节
|
||||
├── 新增"方案偏离"章节
|
||||
└── 健康度评分初版(D1+D2+D3+D6 加权)
|
||||
```
|
||||
|
||||
#### 覆盖的 CRA 维度
|
||||
|
||||
| 维度 | 阶段二目标 |
|
||||
|------|-----------|
|
||||
| D2 数据完整性 | **新增**:缺失率、录入时效、分支逻辑计算 |
|
||||
| D6 方案偏离 | **新增**:超窗检测、操作遗漏、偏离分级 |
|
||||
|
||||
#### 需要确认的问题
|
||||
|
||||
> **问临床专家**:
|
||||
> 1. IIT 项目中最常见的方案偏离类型是什么?(超窗?漏检?用药偏差?)
|
||||
> 2. 访视窗口 ±N 天的 N 值,一般如何确定?是每个方案不同还是有通用标准?
|
||||
> 3. 数据缺失率到多少算"红灯"?IIT 行业有没有通用标准(如 3%、5%)?
|
||||
> 4. 分支逻辑的处理——REDCap 中的分支逻辑字段在 Data Dictionary 中是否完整可读?
|
||||
|
||||
#### 技术前提
|
||||
|
||||
- **依赖阶段一**的三级数据结构和规则分类体系
|
||||
- 需要从 REDCap 拉取 Schedule of Assessment(表单-事件映射 + 必填字段清单)
|
||||
- 需要从 REDCap 审计日志(Logging API)获取录入时间信息
|
||||
|
||||
---
|
||||
|
||||
### 阶段三:安全性与药物管理(远期)
|
||||
|
||||
**目标**:覆盖 CRA 工作中专业性最强的部分——不良事件监测和药物管理。
|
||||
|
||||
#### 核心工作
|
||||
|
||||
```
|
||||
1. AE/SAE 安全性引擎(D5)
|
||||
├── AE 识别触发(实验室异常自动触发 AE 检查)
|
||||
├── 严重程度分级(CTCAE 标准内嵌)
|
||||
├── 时序逻辑(AE 起止时间 vs 用药时间)
|
||||
├── SAE 时效监控(发现后 24h 上报 deadline)
|
||||
└── LLM 辅助:因果关系初步评估(概率性判断)
|
||||
|
||||
2. 药物依从性引擎(D7)
|
||||
├── 发药/回收核算
|
||||
├── 依从性 = (发药量 - 回收量) / 预期量 × 100%
|
||||
├── 80%-120% 合格区间判定
|
||||
└── 批号有效期交叉比对
|
||||
|
||||
3. 实验室预警增强
|
||||
├── 绝对值 vs 正常范围
|
||||
├── 基线变化率监测(连续升高趋势)
|
||||
└── DILI(药物性肝损伤)早期预警模型
|
||||
|
||||
4. 合并用药核查
|
||||
├── 方案禁忌药物比对
|
||||
├── 新增用药 ↔ AE 关联推断
|
||||
└── 洗脱期计算
|
||||
|
||||
5. 完整健康度评分
|
||||
└── 7 维度加权综合评分 + AI 建议后续行动
|
||||
```
|
||||
|
||||
#### 覆盖的 CRA 维度
|
||||
|
||||
| 维度 | 阶段三目标 |
|
||||
|------|-----------|
|
||||
| D5 安全性监测 | **新增**:AE/SAE 追踪、时效监控、因果评估 |
|
||||
| D7 药物管理 | **新增**:依从性计算、批号追踪 |
|
||||
|
||||
#### 需要确认的问题
|
||||
|
||||
> **问临床专家**:
|
||||
> 1. IIT 项目中,AE/SAE 的管理是由 PI 团队自行负责还是有外部安全委员会?
|
||||
> 2. IIT 项目中是否普遍使用 CTCAE 分级?还是只在肿瘤相关 IIT 中常用?
|
||||
> 3. 药物依从性在 IIT 中的重要性如何?是否每个 IIT 都涉及试验药物管理?
|
||||
> 4. AI 做因果关系"初步评估"(而非最终判定),这在临床上能否被接受?
|
||||
> 5. 合并用药的禁忌药物清单,是每个方案自定义还是有标准数据库可以引用?
|
||||
|
||||
#### 技术前提
|
||||
|
||||
- 依赖阶段一、二的基础设施
|
||||
- AE/SAE 需要 REDCap 中有对应的 CRF 表单(不是所有 IIT 项目都有)
|
||||
- 因果关系评估涉及 LLM 概率性推理(SoftRuleEngine),需要人类确认机制
|
||||
- 药物管理需要 IWRS 或 REDCap 药物管理模块的数据接口
|
||||
|
||||
---
|
||||
|
||||
## 四、三级数据架构概要
|
||||
|
||||
三个阶段共用同一套数据架构,一次设计到位:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ qc_field_status │
|
||||
│ (变量级,保持最新) │
|
||||
│ │
|
||||
│ project × record × event × field │
|
||||
│ + status + rule_category │
|
||||
│ + actual_value + expected_value │
|
||||
└───────────────┬──────────────────┘
|
||||
│ 聚合
|
||||
┌───────────────▼──────────────────┐
|
||||
│ qc_event_status │
|
||||
│ (事件级,保持最新) │
|
||||
│ │
|
||||
│ project × record × event │
|
||||
│ + status (所有变量中最严重的) │
|
||||
│ + 各维度计数 (d1_issues, d2_...) │
|
||||
└───────────────┬──────────────────┘
|
||||
│ 聚合
|
||||
┌───────────────▼──────────────────┘
|
||||
│ record_summary │
|
||||
│ (记录级,保持最新) │
|
||||
│ │
|
||||
│ project × record │
|
||||
│ + overall_status (所有事件最严重) │
|
||||
│ + events_total / passed / failed │
|
||||
└───────────────┬──────────────────┘
|
||||
│ 聚合
|
||||
┌───────────────▼──────────────────┐
|
||||
│ 多维度质控报告 │
|
||||
│ │
|
||||
│ D1 入组质量: 92.9% │
|
||||
│ D2 数据完整性: --(阶段二) │
|
||||
│ D3 变量准确性: 98.5% │
|
||||
│ D4 eQuery: 3 Open / 12 Closed │
|
||||
│ D5 安全性: --(阶段三) │
|
||||
│ D6 方案偏离: --(阶段二) │
|
||||
│ D7 药物管理: --(阶段三) │
|
||||
│ ───────────────────────── │
|
||||
│ 健康度评分: 87 / 100 │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键设计决策**:`rule_category` 字段贯穿整个体系,阶段一只用到 `inclusion`/`exclusion`/`variable`,阶段二加入 `completeness`/`protocol_deviation`,阶段三加入 `ae_safety`/`ip_compliance`。**表结构不需要改,只是规则类别在扩展**。
|
||||
|
||||
---
|
||||
|
||||
## 五、规则类型与引擎匹配
|
||||
|
||||
不同维度的规则,其"确定性"不同,需要匹配不同的执行引擎:
|
||||
|
||||
| 维度 | 规则性质 | 适用引擎 | 举例 |
|
||||
|------|---------|---------|------|
|
||||
| D1 入组质量 | 确定性 100% | HardRuleEngine (JSON Logic) | age ∈ [16,35] |
|
||||
| D2 数据完整性 | 确定性 100% | 专用 CompletenessEngine | 必填字段 A 为空 |
|
||||
| D3 变量准确性 | 确定性 90%+ | HardRuleEngine | consent_date < screening_date |
|
||||
| D4 数据质疑 | 管理流程 | 状态机 (eQuery Workflow) | Open→Answered→Closed |
|
||||
| D5 安全性 | 确定性 60-80% | HardRule + SoftRuleEngine (LLM) | CTCAE 分级确定,因果关系需 LLM |
|
||||
| D6 方案偏离 | 确定性 95% | HardRuleEngine + 日历引擎 | visit_date 超出窗口 ±3 天 |
|
||||
| D7 药物管理 | 确定性 100% | HardRuleEngine | 依从性 = 85% ∈ [80,120] |
|
||||
|
||||
**核心原则**:
|
||||
- **确定性规则用硬引擎**(HardRuleEngine / JSON Logic)——结果可审计、可复现
|
||||
- **概率性判断用软引擎**(SoftRuleEngine / LLM)——必须附带"AI 建议 + 人类确认"机制
|
||||
- **D4 不是规则引擎问题**,而是流程管理问题(状态机)
|
||||
|
||||
---
|
||||
|
||||
## 六、覆盖率演进预期
|
||||
|
||||
```
|
||||
100%─┬─────────────────────────────────────────────
|
||||
│ ┌───────
|
||||
80%─┤ ┌────────┤ 阶段三
|
||||
│ │ D5+D7 │ 70-80%
|
||||
60%─┤ ┌───────────┤ │
|
||||
│ │ D2+D6 │ 阶段二 │
|
||||
40%─┤ │ │ 50-60% │
|
||||
│ ┌──────────┤ │ │
|
||||
20%─┤ │ D1+D3+D4 │ 阶段一 │ │
|
||||
│ │ │ 25-35% │ │
|
||||
0%─┴──┴──────────┴───────────┴────────┴─────────
|
||||
现状 阶段一 阶段二 阶段三
|
||||
```
|
||||
|
||||
**说明**:百分比是估算值,表示对 CRA 可替代工作(70-80%)的覆盖程度。阶段三完成后,系统应能覆盖 CRA 可替代工作的绝大部分。
|
||||
|
||||
---
|
||||
|
||||
## 七、与现有四大工具的关系
|
||||
|
||||
当前对话层的 4 个工具(`read_report`、`look_up_data`、`check_quality`、`search_knowledge`)在三个阶段中保持稳定,**工具不变,内涵在扩展**:
|
||||
|
||||
| 工具 | 阶段一 | 阶段二 | 阶段三 |
|
||||
|------|--------|--------|--------|
|
||||
| `read_report` | D1+D3 报告 | + D2+D6 章节 | + D5+D7 章节 + 健康度评分 |
|
||||
| `look_up_data` | 原始数据 + 质控标注 | + 缺失率标注 | + AE/ConMed 数据 |
|
||||
| `check_quality` | 入排 + 变量检查 | + 完整性 + 偏离检查 | + AE + 依从性检查 |
|
||||
| `search_knowledge` | 研究方案/CRF 搜索 | + Schedule of Assessment | + IB(研究者手册) |
|
||||
|
||||
**这是一个很好的设计**——对 LLM 来说,工具数量和接口不变,它不需要"学习"新工具。内部的规则引擎和报告内容在持续丰富,对 LLM 透明。
|
||||
|
||||
---
|
||||
|
||||
## 八、风险与约束
|
||||
|
||||
### 8.1 数据源约束
|
||||
|
||||
| 数据需求 | 来源 | 约束 |
|
||||
|---------|------|------|
|
||||
| 变量值 | REDCap Record API | ✅ 已打通 |
|
||||
| 变量定义(Data Dictionary) | REDCap Metadata API | ✅ 已打通 |
|
||||
| 表单-事件映射 | REDCap Form-Event API | ✅ 已打通 |
|
||||
| 录入时间(审计日志) | REDCap Logging API | ✅ 已实现(exportLogging) |
|
||||
| 分支逻辑 | Data Dictionary.branching_logic | ⚠️ 需解析 REDCap 分支逻辑语法 |
|
||||
| Schedule of Assessment | 研究方案 PDF + 人工配置 | ⚠️ 需要项目管理员手动配置 |
|
||||
| AE/SAE 表单 | REDCap CRF | ⚠️ 取决于项目是否建了 AE 表单 |
|
||||
| 药物管理数据 | REDCap CRF 或外部 IWRS | ❌ 可能不在 REDCap 中 |
|
||||
|
||||
### 8.2 IIT 场景的现实约束
|
||||
|
||||
- **不是所有 IIT 都有 AE 表单**:观察性研究可能没有不良事件采集
|
||||
- **不是所有 IIT 都涉及试验药物**:非药物干预研究(如手术方案对比)无 D7
|
||||
- **方案复杂度差异大**:简单的 IIT 可能只有 2 个访视,复杂的可能有 20+
|
||||
- **REDCap 配置标准化程度低**:不同机构的 REDCap 项目结构差异很大
|
||||
|
||||
**应对策略**:系统必须支持"维度可选"——管理员在项目配置时勾选本项目适用的维度,未勾选的维度不执行、不展示。
|
||||
|
||||
### 8.3 LLM 在临床决策中的边界
|
||||
|
||||
| 场景 | LLM 角色 | 人类角色 |
|
||||
|------|---------|---------|
|
||||
| 数值范围检查 | 直接判定 | 不需要 |
|
||||
| 时序逻辑检查 | 直接判定 | 不需要 |
|
||||
| 数据缺失统计 | 直接计算 | 不需要 |
|
||||
| AE 严重程度分级 | 根据 CTCAE 标准建议分级 | PI 确认 |
|
||||
| AE 因果关系 | **初步评估(建议)** | **PI 必须确认** |
|
||||
| 方案偏离严重性 | 按规则初分 Major/Minor | PI 可修正 |
|
||||
| 综合健康度建议 | 提供建议行动 | PI 决策 |
|
||||
|
||||
**铁律**:凡涉及医学判断的结论,AI 只提供建议,必须经人类确认后才能写入正式记录。
|
||||
|
||||
---
|
||||
|
||||
## 九、待讨论的关键问题
|
||||
|
||||
以下问题需要在技术评审中与临床研究专家确认:
|
||||
|
||||
### 优先级与范围
|
||||
|
||||
1. **IIT 项目中,以下 7 个维度的重要性排序是什么?** 哪些是"必须有",哪些是"有了更好"?
|
||||
2. **D5(安全性)和 D7(药物管理)在 IIT 中的覆盖率有多高?** 是不是很多 IIT 项目并不涉及?
|
||||
3. **三个阶段的划分是否合理?** 是否有维度需要提前或推迟?
|
||||
|
||||
### 规则可行性
|
||||
|
||||
4. **时序逻辑的通用性**:知情同意日 < 筛选日 < 基线日,这个时序在所有 IIT 中都成立吗?
|
||||
5. **访视窗口**:IIT 中 ±N 天的窗口一般怎么定?是写在方案里的还是有行业惯例?
|
||||
6. **数据缺失率的"红线"**:IIT 行业是否有通用的缺失率阈值标准?
|
||||
7. **跨表单校验的实用性**:性别 vs 妊娠检查这类跨表单逻辑在 IIT 中常见吗?
|
||||
|
||||
### 临床接受度
|
||||
|
||||
8. **AI 做因果关系"初步评估"**,PI 是否愿意参考?还是更倾向完全自行判断?
|
||||
9. **自动生成的 eQuery 语气和内容**,是否需要 PI 先审核再发给 CRC?
|
||||
10. **健康度评分**:PI 能否接受一个 AI 打分?评分维度和权重怎么定才合理?
|
||||
|
||||
### 数据源
|
||||
|
||||
11. **REDCap 中 Schedule of Assessment 的信息**是否足够完整?还是需要额外配置?
|
||||
12. **AE 表单在 IIT REDCap 项目中的标准化程度如何?** 字段命名是否有规范?
|
||||
|
||||
---
|
||||
|
||||
## 十、总结
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 研究方案 ──→ 变量清单 ──→ 质控规则 ──→ 三级质控状态 ──→ 多维报告 │
|
||||
│ (7大维度) (变量/事件/记录) (7大章节) │
|
||||
│ │
|
||||
│ 阶段一:D1 入组 + D3 变量 + D4 eQuery ── 数据质量基石 │
|
||||
│ 阶段二:D2 完整性 + D6 方案偏离 ── 方案合规覆盖 │
|
||||
│ 阶段三:D5 安全性 + D7 药物管理 ── 全维度覆盖 │
|
||||
│ │
|
||||
│ 确定性规则 → HardRuleEngine(可审计、可复现) │
|
||||
│ 概率性判断 → SoftRuleEngine + LLM(AI 建议 + 人类确认) │
|
||||
│ │
|
||||
│ 工具层不变(4 工具),内涵逐步扩展 │
|
||||
│ 数据架构一次设计到位(三级状态表 + rule_category 字段) │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**本文档的目的不是确定"怎么做",而是确定"做什么、按什么顺序做、做到什么程度"。** 经团队和临床专家评审认可后,再基于此形成具体的开发计划和技术设计。
|
||||
@@ -0,0 +1,442 @@
|
||||
# CRA Agent V3.0 — 用户权限与多租户三阶段实施计划
|
||||
|
||||
> **版本:** v1.0
|
||||
> **日期:** 2026-02-28
|
||||
> **状态:** 待实施
|
||||
> **前置依赖:** P0 + P1 已完成(质控流水线 + ChatOrchestrator)
|
||||
> **关联文档:**
|
||||
> - [V3.0 全新开发计划](./V3.0全新开发计划.md)
|
||||
> - [统一数字 CRA 质控平台 PRD](./统一数字%20CRA%20质控平台产品需求文档(PRD).md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题背景
|
||||
|
||||
### 1.1 当前系统存在的三个核心问题
|
||||
|
||||
**问题 A:业务端 CRA 质控平台无法运行**
|
||||
|
||||
- 所有业务端页面(驾驶舱、eQuery、报告、AI 对话)从 URL 参数 `?projectId=xxx` 获取项目 ID
|
||||
- 没有任何机制把登录用户与 IIT 项目关联,导致页面显示"请先选择一个项目"
|
||||
- 侧边栏项目信息为硬编码假数据("IIT-2026-001")
|
||||
|
||||
**问题 B:运营管理端 IIT 项目管理定位不当**
|
||||
|
||||
- "IIT 项目管理"与"Prompt 管理""租户管理""用户管理"混在同一菜单中
|
||||
- 普通项目运营人员不应看到平台级管理功能
|
||||
- 药企/医院客户的项目管理人员不应看到其他企业的项目
|
||||
|
||||
**问题 C:用户-项目关联机制缺失**
|
||||
|
||||
- `IitUserMapping.systemUserId` 存储的是随意字符串(当前值为 `"FengZhiBo"`),未关联平台 `User` 表
|
||||
- `IitProject` 没有 `tenantId` 字段,无法实现租户隔离
|
||||
- IIT API 路由没有认证中间件,无访问控制
|
||||
|
||||
### 1.2 当前数据库实际状态
|
||||
|
||||
```
|
||||
-- iit_schema.projects(1 条记录)
|
||||
id: test0102-pd-study | name: test0207 | status: active
|
||||
|
||||
-- iit_schema.user_mappings(1 条记录)
|
||||
system_user_id: FengZhiBo | wecom_user_id: FengZhiBo | role: PI
|
||||
|
||||
-- public.users(1 条记录)
|
||||
id: user-mock-001 | name: 测试用户 | role: user
|
||||
(注:此用户为占位假数据,非真实用户)
|
||||
```
|
||||
|
||||
两张表之间没有任何外键关联。
|
||||
|
||||
### 1.3 当前角色体系
|
||||
|
||||
**平台级角色(public.users.role):**
|
||||
|
||||
| 角色 | 说明 | 当前使用 |
|
||||
|------|------|---------|
|
||||
| SUPER_ADMIN | 平台超级管理员 | 可访问运营管理端全部功能 |
|
||||
| PROMPT_ENGINEER | Prompt 工程师 | 可访问运营管理端 |
|
||||
| HOSPITAL_ADMIN | 医院管理员 | 已定义,未使用 |
|
||||
| PHARMA_ADMIN | 药企管理员 | 已定义,未使用 |
|
||||
| DEPARTMENT_ADMIN | 科室管理员 | 已定义,未使用 |
|
||||
| USER | 普通用户 | 默认角色 |
|
||||
|
||||
**IIT 项目级角色(iit_schema.user_mappings.role):**
|
||||
|
||||
| 角色 | 说明 | 当前使用 |
|
||||
|------|------|---------|
|
||||
| PI | 主要研究者 | 已使用 |
|
||||
| Sub-I | 次要研究者 | 已定义 |
|
||||
| CRC | 临床研究协调员 | 已定义 |
|
||||
| CRA | 临床监查员 | 已定义 |
|
||||
| DM | 数据管理员 | 已定义 |
|
||||
| Statistician | 统计师 | 已定义 |
|
||||
| Other | 其他 | 已定义 |
|
||||
|
||||
**缺失的角色:**
|
||||
|
||||
| 角色 | 层级 | 说明 |
|
||||
|------|------|------|
|
||||
| **PM** | IIT 项目级 | 项目管理员,负责项目配置和管理 |
|
||||
| **IIT_OPERATOR** | 平台级 | IIT 项目运营,负责为客户创建和配置项目(Phase 2 实现) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 三阶段实施计划
|
||||
|
||||
```
|
||||
Phase 1(立即) Phase 2(近期) Phase 3(中期)
|
||||
让业务端能跑 用户-项目关联 多租户隔离
|
||||
───────────────── → ──────────────────── → ─────────────────
|
||||
自动选中活跃项目 systemUserId 关联 User IitProject 加 tenantId
|
||||
Provider 注入上下文 /my-projects API 项目按租户过滤
|
||||
管理端侧边栏分组 项目角色权限矩阵 PHARMA_ADMIN 自助管理
|
||||
配置功能补全 IIT 路由加认证
|
||||
IIT_OPERATOR 平台角色
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 1:让业务端能跑(预估 0.5 天)
|
||||
|
||||
### 3.1 目标
|
||||
|
||||
- CRA 质控平台业务端页面能正常加载和显示数据
|
||||
- 运营管理端 IIT 项目管理菜单位置合理
|
||||
- 项目配置功能补全(定时质控、变量清单)
|
||||
|
||||
### 3.2 方案
|
||||
|
||||
#### 3.2.1 创建 IitProjectContext Provider
|
||||
|
||||
**设计原则:** 通用方案,适用于用户关联 0 / 1 / N 个项目的所有场景。
|
||||
|
||||
**核心逻辑:**
|
||||
|
||||
```
|
||||
用户进入 /iit → IitProjectProvider 初始化
|
||||
→ 调用 GET /api/v1/admin/iit-projects 获取项目列表
|
||||
→ 过滤 status = 'active'
|
||||
→ 0 个 → 显示空状态页:"暂无关联的 IIT 项目"
|
||||
→ 1 个 → 自动选中,不弹选择器
|
||||
→ N 个 → 恢复 localStorage 上次选择;若无记录,默认选第一个
|
||||
→ 用户可随时通过侧边栏顶部下拉选择器切换项目
|
||||
→ 切换后写入 localStorage,子页面自动刷新
|
||||
→ 通过 useIitProject() hook 暴露:
|
||||
{ projectId, project, projects, loading, switchProject(id) }
|
||||
```
|
||||
|
||||
**项目选择器 UI:**
|
||||
|
||||
放在 IitLayout 侧边栏顶部(替换当前硬编码的项目名称),具体表现:
|
||||
|
||||
- 1 个项目:显示项目名称(纯文本,无下拉箭头)
|
||||
- N 个项目:显示当前项目名 + 下拉箭头,点击弹出 Select 选择器
|
||||
- 选择器选项:项目名称 + 项目编号(如 "原发性痛经队列研究 / IIT-2026-001")
|
||||
- 切换项目后,所有子页面通过 Context 自动获取新 projectId 并刷新数据
|
||||
|
||||
**改动文件:**
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `frontend-v2/src/modules/iit/context/IitProjectContext.tsx` | 新建 Context + Provider + useIitProject hook |
|
||||
| `frontend-v2/src/modules/iit/IitLayout.tsx` | 集成 Provider,侧边栏顶部改为项目选择器 |
|
||||
| `frontend-v2/src/modules/iit/pages/DashboardPage.tsx` | 删除 URL 参数读取,改用 `useIitProject()` |
|
||||
| `frontend-v2/src/modules/iit/pages/EQueryPage.tsx` | 同上 |
|
||||
| `frontend-v2/src/modules/iit/pages/ReportsPage.tsx` | 同上 |
|
||||
| `frontend-v2/src/modules/iit/pages/AiStreamPage.tsx` | 同上 |
|
||||
| `frontend-v2/src/modules/iit/pages/AiChatPage.tsx` | 同上 |
|
||||
|
||||
#### 3.2.2 管理端侧边栏三区重组(方案 B:逻辑拆分)
|
||||
|
||||
**决策背景:** 概念上存在三种管理职能(平台管理、项目运营、商务运营),但当前阶段团队规模小,同一人可能跨职能操作。采用**方案 B(逻辑拆分)**:保留单一 `/admin` 入口,侧边栏按职能分为三个带标题的菜单组,通过 RBAC 控制每个角色看到哪些组。未来用户群体分化后可平滑升级为物理拆分(独立路由 + 独立 Layout)。
|
||||
|
||||
**侧边栏结构(SUPER_ADMIN 视角,看到全部):**
|
||||
|
||||
```
|
||||
┌─ 平台管理 ─────────────┐
|
||||
│ 运营概览 │
|
||||
│ Prompt 管理 │
|
||||
│ 系统知识库 │
|
||||
│ LLM 配置 │
|
||||
│ 系统设置 │
|
||||
├─ 项目运营 ─────────────┤
|
||||
│ IIT 项目管理 │
|
||||
├─ 商务运营 ─────────────┤
|
||||
│ 租户管理 │
|
||||
│ 用户管理 │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
**各角色可见性:**
|
||||
|
||||
| 菜单组 | SUPER_ADMIN | PROMPT_ENGINEER | IIT_OPERATOR | PHARMA_ADMIN |
|
||||
|--------|:-----------:|:---------------:|:------------:|:------------:|
|
||||
| 平台管理 | 全部 | Prompt 管理 + 系统知识库 | - | - |
|
||||
| 项目运营 | 全部项目 | - | 全部项目 | 本租户项目 |
|
||||
| 商务运营 | 全部 | - | - | - |
|
||||
|
||||
**改动文件:** `frontend-v2/src/framework/layout/AdminLayout.tsx`
|
||||
|
||||
#### 3.2.3 项目配置功能补全
|
||||
|
||||
**A. 定时质控配置 UI**
|
||||
|
||||
数据库 `iit_schema.projects` 已有 `cron_enabled` 和 `cron_expression` 字段,前端未暴露。
|
||||
|
||||
在 `IitProjectDetailPage.tsx` 的"REDCap 配置"Tab 中增加:
|
||||
- 定时质控开关(Switch,绑定 `cronEnabled`)
|
||||
- Cron 表达式输入(Input + 预设选项:"每天 8:00" / "每周一 9:00" / 自定义)
|
||||
|
||||
**B. 变量清单 Tab**
|
||||
|
||||
后端 `GET /:id/field-metadata` API 已存在。在 `IitProjectDetailPage.tsx` 新增第 5 个 Tab"变量清单",展示 REDCap 变量表格。
|
||||
|
||||
**C. PM 角色新增**
|
||||
|
||||
在 `iitUserMappingService.ts` 的 `getRoleOptions()` 中添加:
|
||||
```
|
||||
{ value: 'PM', label: '项目管理员 (PM)' }
|
||||
```
|
||||
|
||||
### 3.3 Phase 1 不做的事
|
||||
|
||||
- 不改数据库 Schema
|
||||
- 不做用户-项目关联(systemUserId 改造)
|
||||
- 不加认证中间件
|
||||
- 不做租户隔离
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 2:用户-项目关联 + 角色权限(预估 2 天)
|
||||
|
||||
### 4.1 目标
|
||||
|
||||
- 登录用户自动看到自己关联的 IIT 项目
|
||||
- IIT 项目角色对应明确的权限
|
||||
- IIT API 路由有认证保护
|
||||
- 运营管理端按角色控制菜单可见性
|
||||
- 新增 IIT_OPERATOR 平台角色,使项目运营人员无需 SUPER_ADMIN 权限即可创建和配置 IIT 项目
|
||||
|
||||
### 4.2 数据模型改动
|
||||
|
||||
#### 4.2.1 IitUserMapping 增强
|
||||
|
||||
```prisma
|
||||
model IitUserMapping {
|
||||
// ... 现有字段 ...
|
||||
systemUserId String @map("system_user_id") // 改为必须是 User.id
|
||||
// 新增:
|
||||
userId String? @map("user_id") // 平台 User 表外键(可选,渐进式关联)
|
||||
|
||||
// 新增关联
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
}
|
||||
```
|
||||
|
||||
**迁移策略:**
|
||||
- 新增 `userId` 字段(nullable),不破坏现有数据
|
||||
- 新建用户映射时要求填写真实 `userId`
|
||||
- 旧数据逐步补齐
|
||||
|
||||
#### 4.2.2 IitProject 增强(为 Phase 3 预留)
|
||||
|
||||
```prisma
|
||||
model IitProject {
|
||||
// ... 现有字段 ...
|
||||
// 新增:
|
||||
tenantId String? @map("tenant_id") // 租户归属(Phase 3 正式启用)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 API 改动
|
||||
|
||||
#### 4.3.1 新增接口
|
||||
|
||||
```
|
||||
GET /api/v1/iit/my-projects
|
||||
```
|
||||
|
||||
逻辑:
|
||||
1. 从 JWT Token 获取当前用户 `userId`
|
||||
2. 查询 `IitUserMapping WHERE userId = :userId`
|
||||
3. 返回关联的项目列表 + 用户在每个项目中的角色
|
||||
|
||||
响应示例:
|
||||
```json
|
||||
{
|
||||
"projects": [
|
||||
{
|
||||
"id": "test0102-pd-study",
|
||||
"name": "test0207",
|
||||
"status": "active",
|
||||
"myRole": "PI",
|
||||
"description": "原发性痛经队列研究"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3.2 IIT 路由加认证
|
||||
|
||||
所有 `/api/v1/admin/iit-projects` 路由添加 `authenticate` 中间件。
|
||||
|
||||
#### 4.3.3 项目级权限检查
|
||||
|
||||
```
|
||||
中间件:requireProjectRole(projectId, allowedRoles)
|
||||
→ 查 IitUserMapping WHERE projectId AND userId
|
||||
→ 检查 role 是否在 allowedRoles 中
|
||||
```
|
||||
|
||||
### 4.4 项目角色权限矩阵
|
||||
|
||||
| 功能 | PM | PI | CRA | CRC | DM | Sub-I | Statistician |
|
||||
|------|----|----|-----|-----|----|-------|-------------|
|
||||
| 查看驾驶舱 | 读 | 读 | 读 | 读 | 读 | 读 | 读 |
|
||||
| 查看/处理 eQuery | 读写 | 读 | 读写 | 读写 | 读 | 读 | - |
|
||||
| 查看报告 | 读 | 读 | 读 | 读 | 读 | 读 | 读 |
|
||||
| AI 对话 | 是 | 是 | 是 | 是 | 是 | 是 | 是 |
|
||||
| 项目配置(REDCap/规则/通知/知识库) | 读写 | - | - | - | - | - | - |
|
||||
| 查看变量清单 | 读 | 读 | 读 | 读 | 读 | 读 | 读 |
|
||||
| 管理质控规则 | 读写 | - | 读 | - | 读 | - | - |
|
||||
|
||||
### 4.5 管理端菜单权限(承接 Phase 1 三区分组)
|
||||
|
||||
Phase 1 已完成侧边栏三区分组的 UI 结构,Phase 2 加入真正的 RBAC 控制:
|
||||
|
||||
| 角色 | 可进入 /admin | 平台管理 | 项目运营 | 商务运营 |
|
||||
|------|:------------:|:-------:|:-------:|:-------:|
|
||||
| SUPER_ADMIN | 是 | 全部 | 全部项目 | 全部 |
|
||||
| PROMPT_ENGINEER | 是 | Prompt + 知识库 | - | - |
|
||||
| IIT_OPERATOR(本阶段新增) | 是 | - | 全部项目 | - |
|
||||
| PHARMA_ADMIN | 是 | - | 本租户项目 | - |
|
||||
| HOSPITAL_ADMIN | 是 | - | 本租户项目 | - |
|
||||
| USER | 否 | - | - | - |
|
||||
|
||||
**实现方式:**
|
||||
- 路由守卫:`/admin` 入口检查 `user.role` 是否在允许列表中
|
||||
- 侧边栏渲染:根据 `user.role` 过滤 `menuItems`,角色看不到的组直接不渲染
|
||||
- API 层:每个管理端 API 添加 `authenticate` + `requireRole(allowedRoles)` 中间件
|
||||
|
||||
### 4.6 前端改动
|
||||
|
||||
- `IitProjectContext` 改为调用 `/api/v1/iit/my-projects`
|
||||
- 根据 `myRole` 控制业务端 Tab 可见性(PM 看到更多设置入口)
|
||||
- `AdminLayout.tsx` 按 `user.role` 过滤侧边栏菜单组可见性(Phase 1 已分组,此处加权限判断)
|
||||
- `/admin` 路由入口添加角色守卫
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 3:多租户隔离(预估 1.5 天)
|
||||
|
||||
### 5.1 目标
|
||||
|
||||
- IIT 项目按租户隔离,药企只能看到自己的项目
|
||||
- PHARMA_ADMIN 可在运营管理端管理自己租户的 IIT 项目
|
||||
- SUPER_ADMIN 可跨租户查看所有项目
|
||||
|
||||
### 5.2 数据模型
|
||||
|
||||
启用 Phase 2 预留的 `tenantId` 字段,设为必填:
|
||||
|
||||
```prisma
|
||||
model IitProject {
|
||||
tenantId String @map("tenant_id")
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 API 改动
|
||||
|
||||
所有项目查询接口添加租户过滤:
|
||||
|
||||
```sql
|
||||
-- 非 SUPER_ADMIN:
|
||||
SELECT * FROM iit_schema.projects WHERE tenant_id = :currentUserTenantId
|
||||
|
||||
-- SUPER_ADMIN:
|
||||
SELECT * FROM iit_schema.projects -- 无限制
|
||||
```
|
||||
|
||||
### 5.4 项目创建流程
|
||||
|
||||
```
|
||||
Phase 2 完成后:
|
||||
SUPER_ADMIN 或 IIT_OPERATOR 创建项目 → 手动指定 tenantId(选择为哪个客户创建)
|
||||
|
||||
Phase 3 完成后(客户自助):
|
||||
PHARMA_ADMIN 创建项目 → 自动绑定自己的 tenantId
|
||||
```
|
||||
|
||||
### 5.5 前端改动
|
||||
|
||||
- 运营管理端项目列表:SUPER_ADMIN 看到全部 + 租户筛选器;PHARMA_ADMIN 只看到自己的
|
||||
- 项目创建表单:SUPER_ADMIN 需选择租户;PHARMA_ADMIN 自动绑定
|
||||
|
||||
---
|
||||
|
||||
## 6. 实施优先级总结
|
||||
|
||||
| 阶段 | 核心内容 | 预估工时 | 数据库改动 | 前置条件 |
|
||||
|------|---------|---------|-----------|---------|
|
||||
| **Phase 1** | Provider + 管理端分组 + 配置补全 | 0.5 天 | 无 | 无 |
|
||||
| **Phase 2** | userId 关联 + /my-projects + 角色权限 + 认证 + IIT_OPERATOR 角色 | 2 天 | 新增 userId 列、tenantId 列、IIT_OPERATOR 角色枚举 | Phase 1 |
|
||||
| **Phase 3** | 租户隔离 + PHARMA_ADMIN 自助 | 1.5 天 | tenantId 改为必填 | Phase 2 |
|
||||
|
||||
### Phase 1 立即可执行的原因
|
||||
|
||||
- 0 数据库改动
|
||||
- 只改前端代码
|
||||
- 当前只有 1 个活跃项目 + 1 个测试用户,自动选中即可
|
||||
- 让业务端立即可演示
|
||||
|
||||
### Phase 2 的触发条件
|
||||
|
||||
- 有真实用户需要登录系统
|
||||
- 需要区分不同用户看到不同项目
|
||||
- 需要保护 API 安全
|
||||
|
||||
### Phase 3 的触发条件
|
||||
|
||||
- 有多个客户(药企/医院)同时使用系统
|
||||
- 需要租户级别的数据隔离
|
||||
|
||||
---
|
||||
|
||||
## 7. 附录:改动文件清单
|
||||
|
||||
### Phase 1
|
||||
|
||||
| 文件 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `frontend-v2/src/modules/iit/context/IitProjectContext.tsx` | 新建 | 项目上下文 Provider |
|
||||
| `frontend-v2/src/modules/iit/IitLayout.tsx` | 修改 | 集成 Provider + 真实项目信息 |
|
||||
| `frontend-v2/src/modules/iit/pages/DashboardPage.tsx` | 修改 | 改用 useIitProject() |
|
||||
| `frontend-v2/src/modules/iit/pages/EQueryPage.tsx` | 修改 | 改用 useIitProject() |
|
||||
| `frontend-v2/src/modules/iit/pages/ReportsPage.tsx` | 修改 | 改用 useIitProject() |
|
||||
| `frontend-v2/src/modules/iit/pages/AiStreamPage.tsx` | 修改 | 改用 useIitProject() |
|
||||
| `frontend-v2/src/modules/iit/pages/AiChatPage.tsx` | 修改 | 改用 useIitProject() |
|
||||
| `frontend-v2/src/framework/layout/AdminLayout.tsx` | 修改 | 侧边栏分组 |
|
||||
| `frontend-v2/src/modules/admin/pages/IitProjectDetailPage.tsx` | 修改 | 定时质控配置 + 变量清单 Tab |
|
||||
| `backend/src/modules/admin/iit-projects/iitUserMappingService.ts` | 修改 | 添加 PM 角色 |
|
||||
|
||||
### Phase 2
|
||||
|
||||
| 文件 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `backend/prisma/schema.prisma` | 修改 | IitUserMapping 加 userId、IitProject 加 tenantId |
|
||||
| `backend/src/modules/iit-manager/routes/index.ts` | 修改 | 加 /my-projects 路由 + authenticate 中间件 |
|
||||
| `backend/src/common/auth/auth.middleware.ts` | 修改 | 新增 requireProjectRole 中间件 |
|
||||
| `backend/prisma/schema.prisma` (Role enum) | 修改 | 新增 IIT_OPERATOR 角色枚举值 |
|
||||
| `frontend-v2/src/modules/iit/context/IitProjectContext.tsx` | 修改 | 改调 /my-projects |
|
||||
| `frontend-v2/src/framework/layout/AdminLayout.tsx` | 修改 | 按角色控制菜单组可见性(IIT_OPERATOR 只看项目运营) |
|
||||
|
||||
### Phase 3
|
||||
|
||||
| 文件 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `backend/prisma/schema.prisma` | 修改 | tenantId 改为必填 |
|
||||
| `backend/src/modules/admin/iit-projects/iitProjectService.ts` | 修改 | 查询加租户过滤 |
|
||||
| `frontend-v2/src/modules/admin/pages/IitProjectListPage.tsx` | 修改 | 租户筛选器 |
|
||||
| `frontend-v2/src/modules/admin/pages/IitProjectDetailPage.tsx` | 修改 | 创建时选择租户 |
|
||||
@@ -0,0 +1,920 @@
|
||||
# 质控引擎 V3.1 架构升级 — 五级数据结构与多维报告开发计划
|
||||
|
||||
> **版本**:V1.1(合并架构团队二次评审 4 条关键建议)
|
||||
> **日期**:2026-03-01
|
||||
> **定位**:基于架构团队评审意见,在 V3.0 三级数据结构基础上升级为 CDISC ODM 五级结构,分三批次落地
|
||||
> **前置文档**:
|
||||
> - [五层数据架构方案评审反馈](../../09-技术评审报告/五层数据架构方案评审反馈.md)(采纳/暂缓清单)
|
||||
> - [CRA Agent 质控体系全景技术路径(策略评审稿)](../../00-系统设计/CRA%20Agent%20质控体系全景技术路径(策略评审稿).md)
|
||||
> - [V3.0 三级数据结构技术设计](./质控引擎架构升级-三级数据结构与多模式触发技术设计.md)(历史版本,供参考)
|
||||
> - 架构团队:核心数据架构与业务落地白皮书 / 核心转换机制白皮书 / Skill 化配置架构技术设计 / CRA 质控报告自动化生成与 LLM 友好型设计规范
|
||||
>
|
||||
> **V1.1 变更记录**:
|
||||
> - 新增 3.3 节:状态优先级与 SKIPPED 处理
|
||||
> - 新增 3.8 节:Record-Level Context(跨表单质控上下文全拉取)
|
||||
> - 新增 3.9 节:eQuery 自动闭环(State Transition Hook)
|
||||
> - 修正 6.3 节:D2 缺失率增加 Event-Aware 时序过滤
|
||||
> - 修正 3.4 节:聚合防抖粒度从项目级细化为受试者级
|
||||
|
||||
---
|
||||
|
||||
## 1. V3.0 → V3.1 升级概要
|
||||
|
||||
### 1.1 为什么从三级升到五级
|
||||
|
||||
V3.0 设计的三级结构是 Record → Event → Field,足以覆盖常规表单。但架构团队指出了一个关键盲区:**REDCap 重复表单(Repeating Instruments)**。
|
||||
|
||||
一个患者可以在同一个访视下填写多条 AE(不良事件)、多次合并用药。没有 Form 层和 Instance 层,就无法精确定位"3 号受试者 → 随访 2 → AE 表 → 第 2 条 AE → 事件名称字段"。
|
||||
|
||||
### 1.2 五级坐标体系
|
||||
|
||||
```
|
||||
Record(受试者)
|
||||
└── Event(访视/事件)
|
||||
└── Form(表单) ← 新增
|
||||
└── Instance(实例) ← 新增(核心突破)
|
||||
└── Field(变量/字段)
|
||||
```
|
||||
|
||||
每一个质控状态、每一条 eQuery,都必须绑定在这个五维坐标上,坐标不完整不落盘。
|
||||
|
||||
### 1.3 与 V3.0 的主要差异
|
||||
|
||||
| 维度 | V3.0 | V3.1 |
|
||||
|------|------|------|
|
||||
| 数据层级 | 3 级(Record → Event → Field) | 5 级(+ Form + Instance) |
|
||||
| `qc_field_status` 唯一键 | project × record × event × field | project × record × event × form × instance × field |
|
||||
| 规则分类 | `inclusion`/`exclusion`/`lab_values`/`logic_check` | D1-D7 七大维度 |
|
||||
| 字段语义化 | 无 | `IitFieldMapping.semanticLabel`(反向映射) |
|
||||
| 冒泡机制 | 应用层逐级 UPDATE | 异步防抖聚合(避免并发死锁) |
|
||||
| 报告结构 | 扁平单章 | 按 D1-D7 分章节 |
|
||||
| REDCap InstanceID | 未处理 | `RedcapAdapter` 层强制标准化 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库设计
|
||||
|
||||
### 2.1 新增表:`qc_field_status`(变量级质控状态 — 五级坐标)
|
||||
|
||||
每个 **project × record × event × form × instance × field** 唯一一行,反映最新质控状态。
|
||||
|
||||
```sql
|
||||
CREATE TABLE iit_schema.qc_field_status (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id TEXT NOT NULL,
|
||||
record_id TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL,
|
||||
form_name TEXT NOT NULL, -- 表单名(如 ae_log, conmed_log)
|
||||
instance_id INT NOT NULL DEFAULT 1, -- 实例编号(非重复表单 = 1)
|
||||
field_name TEXT NOT NULL,
|
||||
|
||||
-- 质控结果
|
||||
status TEXT NOT NULL, -- 'PASS' | 'FAIL' | 'WARNING'
|
||||
rule_id TEXT,
|
||||
rule_name TEXT,
|
||||
rule_category TEXT, -- 'D1' | 'D2' | 'D3' | 'D5' | 'D6' | 'D7'
|
||||
severity TEXT, -- 'critical' | 'warning' | 'info'
|
||||
message TEXT,
|
||||
actual_value TEXT,
|
||||
expected_value TEXT,
|
||||
|
||||
-- 溯源
|
||||
source_qc_log_id TEXT,
|
||||
triggered_by TEXT NOT NULL, -- 'webhook' | 'cron' | 'manual'
|
||||
last_qc_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- 唯一约束:五级坐标
|
||||
CONSTRAINT uq_field_status
|
||||
UNIQUE (project_id, record_id, event_id, form_name, instance_id, field_name),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 高频查询索引
|
||||
CREATE INDEX idx_fs_record ON iit_schema.qc_field_status (project_id, record_id);
|
||||
CREATE INDEX idx_fs_event ON iit_schema.qc_field_status (project_id, record_id, event_id);
|
||||
CREATE INDEX idx_fs_fail ON iit_schema.qc_field_status (project_id, status) WHERE status IN ('FAIL', 'WARNING');
|
||||
CREATE INDEX idx_fs_cat ON iit_schema.qc_field_status (project_id, rule_category);
|
||||
```
|
||||
|
||||
### 2.2 新增表:`qc_event_status`(事件级质控状态)
|
||||
|
||||
每个 **project × record × event** 唯一一行,由 `qc_field_status` 聚合。
|
||||
|
||||
```sql
|
||||
CREATE TABLE iit_schema.qc_event_status (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id TEXT NOT NULL,
|
||||
record_id TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL,
|
||||
event_label TEXT,
|
||||
|
||||
-- 聚合状态
|
||||
status TEXT NOT NULL, -- 最严重的子级状态
|
||||
fields_total INT NOT NULL DEFAULT 0,
|
||||
fields_passed INT NOT NULL DEFAULT 0,
|
||||
fields_failed INT NOT NULL DEFAULT 0,
|
||||
fields_warning INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- 维度计数(方便多维报告直接读取)
|
||||
d1_issues INT NOT NULL DEFAULT 0,
|
||||
d2_issues INT NOT NULL DEFAULT 0,
|
||||
d3_issues INT NOT NULL DEFAULT 0,
|
||||
d5_issues INT NOT NULL DEFAULT 0,
|
||||
d6_issues INT NOT NULL DEFAULT 0,
|
||||
d7_issues INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- 表单级摘要
|
||||
forms_checked TEXT[] DEFAULT '{}',
|
||||
top_issues JSONB DEFAULT '[]',
|
||||
|
||||
triggered_by TEXT NOT NULL,
|
||||
last_qc_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_event_status
|
||||
UNIQUE (project_id, record_id, event_id),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_es_record ON iit_schema.qc_event_status (project_id, record_id);
|
||||
CREATE INDEX idx_es_fail ON iit_schema.qc_event_status (project_id, status) WHERE status IN ('FAIL', 'WARNING');
|
||||
```
|
||||
|
||||
### 2.3 改造表:`record_summary`
|
||||
|
||||
在现有 `IitRecordSummary` 上新增聚合字段:
|
||||
|
||||
```sql
|
||||
ALTER TABLE iit_schema.record_summary
|
||||
ADD COLUMN IF NOT EXISTS events_total INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS events_passed INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS events_failed INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS events_warning INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fields_total INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fields_passed INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fields_failed INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS d1_issues INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS d2_issues INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS d3_issues INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS d5_issues INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS d6_issues INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS d7_issues INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS top_issues JSONB DEFAULT '[]';
|
||||
```
|
||||
|
||||
### 2.4 改造表:`IitQcLog` + `IitEquery` 增加 Instance 层
|
||||
|
||||
```sql
|
||||
ALTER TABLE iit_schema.qc_logs
|
||||
ADD COLUMN IF NOT EXISTS form_name TEXT,
|
||||
ADD COLUMN IF NOT EXISTS instance_id INT DEFAULT 1;
|
||||
|
||||
ALTER TABLE iit_schema.iit_equeries
|
||||
ADD COLUMN IF NOT EXISTS instance_id INT DEFAULT 1;
|
||||
```
|
||||
|
||||
### 2.5 改造表:`IitFieldMapping` 增加反向语义标签
|
||||
|
||||
```sql
|
||||
ALTER TABLE iit_schema.field_mapping
|
||||
ADD COLUMN IF NOT EXISTS semantic_label TEXT, -- 中文语义标签(如"谷丙转氨酶(ALT)")
|
||||
ADD COLUMN IF NOT EXISTS form_name TEXT, -- 所属表单
|
||||
ADD COLUMN IF NOT EXISTS rule_category TEXT; -- 所属维度 D1-D7
|
||||
```
|
||||
|
||||
### 2.6 保留不变:`qc_logs`(审计日志)
|
||||
|
||||
继续作为追加型审计日志,每次质控执行新增一行,永不删改。新增 `form_name` 和 `instance_id` 字段后,日志也具备完整的五级坐标。
|
||||
|
||||
### 2.7 完整数据模型
|
||||
|
||||
```
|
||||
REDCap 原始数据(5 层结构)
|
||||
│
|
||||
│ 质控引擎执行
|
||||
▼
|
||||
┌─────────────┐ ┌──────────────────┐
|
||||
│ qc_logs │ │ qc_field_status │
|
||||
│ (审计日志) │←ref─│ (变量级, 5层坐标) │
|
||||
│ 追加型 │ │ + rule_category │
|
||||
└─────────────┘ └────────┬─────────┘
|
||||
│ 异步防抖聚合
|
||||
┌────────▼─────────┐
|
||||
│ qc_event_status │
|
||||
│ (事件级) │
|
||||
│ + d1..d7_issues │
|
||||
└────────┬─────────┘
|
||||
│ 异步防抖聚合
|
||||
┌────────▼─────────┐
|
||||
│ record_summary │
|
||||
│ (记录级) │
|
||||
│ + d1..d7_issues │
|
||||
└────────┬─────────┘
|
||||
│ 聚合
|
||||
┌────────▼─────────┐ ┌──────────────┐
|
||||
│ qc_project_stats │────►│ qc_reports │
|
||||
│ (项目级) │ 生成 │ (LLM 报告) │
|
||||
└──────────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键工程设计
|
||||
|
||||
### 3.1 REDCap InstanceID 标准化(RedcapAdapter 层洗线)
|
||||
|
||||
**问题**:REDCap API 对 `redcap_repeat_instance` 的返回值不一致:
|
||||
- 非重复表单:字段不存在或为空字符串
|
||||
- 重复表单第一行:有时为空字符串,有时为 `"1"`
|
||||
- 重复表单后续行:`"2"`, `"3"`, ...
|
||||
|
||||
**解法**:在 `RedcapAdapter` 返回数据之前,强制标准化所有记录:
|
||||
|
||||
```typescript
|
||||
// RedcapAdapter.ts — 新增 normalizeInstance() 方法
|
||||
|
||||
private normalizeInstances(records: RedcapRecord[]): NormalizedRecord[] {
|
||||
return records.map(record => {
|
||||
const formName = record.redcap_repeat_instrument || this.inferFormName(record);
|
||||
let instanceId: number;
|
||||
|
||||
if (!record.redcap_repeat_instrument) {
|
||||
// 非重复表单:强制 instanceId = 1
|
||||
instanceId = 1;
|
||||
} else {
|
||||
// 重复表单:空或无效值强制为 1,否则取实际值
|
||||
const raw = record.redcap_repeat_instance;
|
||||
instanceId = (raw && !isNaN(Number(raw)) && Number(raw) > 0)
|
||||
? Number(raw)
|
||||
: 1;
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
_normalized: {
|
||||
recordId: String(record.record_id),
|
||||
eventId: record.redcap_event_name || 'default',
|
||||
formName,
|
||||
instanceId,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**原则**:`RedcapAdapter` 之后的所有下游(引擎、状态表、报告)都使用标准化后的五级坐标,无需二次处理。
|
||||
|
||||
### 3.2 QcExecutor 重构(统一执行入口)
|
||||
|
||||
将分散在 `SkillRunner`、`iitBatchController`、`QcReportService` 中的执行逻辑统一收归:
|
||||
|
||||
```typescript
|
||||
class QcExecutor {
|
||||
|
||||
/**
|
||||
* 单记录单事件质控(实时触发 / AI 调用)
|
||||
*/
|
||||
async executeSingle(
|
||||
projectId: string,
|
||||
recordId: string,
|
||||
eventId: string,
|
||||
options?: { triggeredBy: 'webhook' | 'manual' }
|
||||
): Promise<void> {
|
||||
// 1. RedcapAdapter 拉取并标准化(含 InstanceID 洗线)
|
||||
// 2. 加载适用规则(按 applicableEvents + applicableForms 过滤)
|
||||
// 3. 逐 Form × Instance × Field 执行规则
|
||||
// 4. 写入 qc_logs(追加审计)
|
||||
// 5. upsert qc_field_status(五级坐标)
|
||||
// 6. 标记需聚合(推入防抖队列,不立即冒泡)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量质控(定时 / 手动 / 一键全量)
|
||||
*/
|
||||
async executeBatch(
|
||||
projectId: string,
|
||||
options?: { triggeredBy: 'cron' | 'manual' }
|
||||
): Promise<BatchResult> {
|
||||
// 1. RedcapAdapter 拉取全量并标准化
|
||||
// 2. 基线数据合并
|
||||
// 3. 逐 record × event 复用 executeSingle 核心逻辑
|
||||
// 4. 批量完成后触发一次聚合(而非逐条)
|
||||
// 5. 刷新 qc_reports
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步防抖聚合(解决冒泡并发死锁问题)
|
||||
*/
|
||||
async aggregateDeferred(projectId: string): Promise<void> {
|
||||
// 详见 3.3 节
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 状态优先级与 SKIPPED 处理
|
||||
|
||||
聚合时始终取"最严重的状态":
|
||||
|
||||
```
|
||||
FAIL (3) > WARNING (2) > UNCERTAIN (1) > PASS (0)
|
||||
```
|
||||
|
||||
- **事件状态** = 其下所有变量状态中最严重的
|
||||
- **记录状态** = 其下所有事件状态中最严重的
|
||||
|
||||
**SKIPPED 处理**:当某个变量在某个事件中不存在时(如随访期没有 age 字段),**不写入 `qc_field_status`**,不参与聚合计算。只有被规则实际检查过的变量才写入。
|
||||
|
||||
### 3.4 状态冒泡:异步防抖聚合方案
|
||||
|
||||
**问题**:如果批量执行 1000 条规则,每次 Field 变更都同步 UPDATE 上级,会导致数据库行锁冲突甚至死锁。
|
||||
|
||||
**解法**:冒泡不做同步级联,改为"批量完成后统一聚合"。
|
||||
|
||||
```
|
||||
执行阶段(高频写入,无锁竞争):
|
||||
HardRuleEngine 执行 1000 条规则
|
||||
↓ 逐条 upsert
|
||||
qc_field_status(各自独立行,无锁冲突)
|
||||
|
||||
聚合阶段(受试者级防抖,避免项目级锁竞争):
|
||||
executeSingle 完成后
|
||||
↓ 推入 pg-boss 防抖队列
|
||||
singletonKey: `aggregate_${projectId}_${recordId}`
|
||||
↓ 只重算该受试者的 event_status 和 record_summary
|
||||
|
||||
executeBatch 完成后
|
||||
↓ 直接调用 aggregateDeferred(projectId)
|
||||
↓ 一次性 SQL 聚合全项目
|
||||
|
||||
项目级统计(qc_project_stats)
|
||||
↓ 由独立的低频定时任务刷新(如每 5 分钟)
|
||||
↓ 或在 aggregateDeferred 末尾追加一次
|
||||
```
|
||||
|
||||
```typescript
|
||||
async aggregateDeferred(projectId: string): Promise<void> {
|
||||
// 1. 事件级聚合:一条 SQL 搞定
|
||||
await prisma.$executeRaw`
|
||||
INSERT INTO iit_schema.qc_event_status
|
||||
(id, project_id, record_id, event_id, status,
|
||||
fields_total, fields_passed, fields_failed, fields_warning,
|
||||
d1_issues, d2_issues, d3_issues, d5_issues, d6_issues, d7_issues,
|
||||
triggered_by, last_qc_at, created_at, updated_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
fs.project_id, fs.record_id, fs.event_id,
|
||||
-- 最严重状态
|
||||
CASE
|
||||
WHEN COUNT(*) FILTER (WHERE fs.status = 'FAIL') > 0 THEN 'FAIL'
|
||||
WHEN COUNT(*) FILTER (WHERE fs.status = 'WARNING') > 0 THEN 'WARNING'
|
||||
ELSE 'PASS'
|
||||
END,
|
||||
COUNT(*),
|
||||
COUNT(*) FILTER (WHERE fs.status = 'PASS'),
|
||||
COUNT(*) FILTER (WHERE fs.status = 'FAIL'),
|
||||
COUNT(*) FILTER (WHERE fs.status = 'WARNING'),
|
||||
-- 维度计数
|
||||
COUNT(*) FILTER (WHERE fs.rule_category = 'D1' AND fs.status = 'FAIL'),
|
||||
COUNT(*) FILTER (WHERE fs.rule_category = 'D2' AND fs.status = 'FAIL'),
|
||||
COUNT(*) FILTER (WHERE fs.rule_category = 'D3' AND fs.status = 'FAIL'),
|
||||
COUNT(*) FILTER (WHERE fs.rule_category = 'D5' AND fs.status = 'FAIL'),
|
||||
COUNT(*) FILTER (WHERE fs.rule_category = 'D6' AND fs.status = 'FAIL'),
|
||||
COUNT(*) FILTER (WHERE fs.rule_category = 'D7' AND fs.status = 'FAIL'),
|
||||
'manual', NOW(), NOW(), NOW()
|
||||
FROM iit_schema.qc_field_status fs
|
||||
WHERE fs.project_id = ${projectId}
|
||||
GROUP BY fs.project_id, fs.record_id, fs.event_id
|
||||
ON CONFLICT (project_id, record_id, event_id)
|
||||
DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
fields_total = EXCLUDED.fields_total,
|
||||
fields_passed = EXCLUDED.fields_passed,
|
||||
fields_failed = EXCLUDED.fields_failed,
|
||||
fields_warning = EXCLUDED.fields_warning,
|
||||
d1_issues = EXCLUDED.d1_issues,
|
||||
d2_issues = EXCLUDED.d2_issues,
|
||||
d3_issues = EXCLUDED.d3_issues,
|
||||
d5_issues = EXCLUDED.d5_issues,
|
||||
d6_issues = EXCLUDED.d6_issues,
|
||||
d7_issues = EXCLUDED.d7_issues,
|
||||
updated_at = NOW()
|
||||
`;
|
||||
|
||||
// 2. 记录级聚合:同理从 qc_event_status 聚合到 record_summary
|
||||
// 3. 项目级聚合:从 record_summary 聚合到 qc_project_stats
|
||||
}
|
||||
```
|
||||
|
||||
**核心优势**:
|
||||
- 执行阶段只写 `qc_field_status`,各行互不冲突,可高并发
|
||||
- 聚合阶段用 SQL `INSERT ... ON CONFLICT` 一次性完成,无应用层循环
|
||||
- 实时触发用受试者级防抖(`singletonKey: aggregate_${projectId}_${recordId}`),多 CRC 同时录入不同受试者时互不干扰
|
||||
- 项目级统计(`qc_project_stats`)独立刷新,避免单行频繁锁竞争
|
||||
|
||||
### 3.5 规则分类体系(D1-D7 维度枚举)
|
||||
|
||||
扩展 `QCRule.category` 和 `IitSkill.skillType`:
|
||||
|
||||
| 维度代码 | 含义 | 对应规则类型 | 当前状态 |
|
||||
|---------|------|------------|---------|
|
||||
| `D1` | 入排合规性 | HardRule | 已有 `inclusion`/`exclusion`,需重新标注 |
|
||||
| `D2` | 数据完整性 | CompletenessEngine(批次 C) | 未实现 |
|
||||
| `D3` | 变量准确性 | HardRule | 已有 `lab_values`/`logic_check`,需重新标注 |
|
||||
| `D4` | 数据质疑管理 | 状态机(eQuery 流转) | 已有 eQuery 表 |
|
||||
| `D5` | 安全性监测 | HardRule + SoftRule(批次 C) | SoftRuleEngine 已有框架 |
|
||||
| `D6` | 方案偏离 | HardRule + SoftRule(批次 C) | 未实现 |
|
||||
| `D7` | 药物管理 | HardRule(批次 C) | 未实现 |
|
||||
|
||||
**改造点**:
|
||||
- `QCRule` 接口的 `category` 字段从 4 种值改为 D1-D7
|
||||
- 种子规则 `seed-iit-qc-rules.ts` 重新标注所有现有规则的维度
|
||||
- `qc_field_status.rule_category` 存储维度代码,支持按维度聚合
|
||||
|
||||
### 3.6 字段语义化(IitFieldMapping 反向增强)
|
||||
|
||||
当前 `IitFieldMapping` 只用于 LLM 输入方向(alias→actual)。增强为双向:
|
||||
|
||||
```
|
||||
LLM 输入方向(已有): "年龄" → age (LLM 对话中说"年龄",系统查 age)
|
||||
LLM 输出方向(新增): age → "年龄(岁)" (报告中展示"年龄(岁)"而非 age)
|
||||
```
|
||||
|
||||
**数据来源**:REDCap Data Dictionary 的 `field_label` 字段天然就是中文语义标签。项目初始化时自动同步:
|
||||
|
||||
```typescript
|
||||
async function syncSemanticLabels(projectId: string) {
|
||||
const metadata = await redcapAdapter.exportMetadata();
|
||||
for (const field of metadata) {
|
||||
await prisma.iitFieldMapping.upsert({
|
||||
where: { projectId_aliasName: { projectId, aliasName: field.field_name } },
|
||||
update: { semanticLabel: field.field_label, formName: field.form_name },
|
||||
create: {
|
||||
projectId,
|
||||
aliasName: field.field_name,
|
||||
actualName: field.field_name,
|
||||
semanticLabel: field.field_label,
|
||||
formName: field.form_name,
|
||||
fieldType: field.field_type,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用场景**:`QcReportService` 生成 LLM XML 时,用 `semanticLabel` 替代物理字段名。
|
||||
|
||||
### 3.7 LLM 三不原则(正式化为设计规范)
|
||||
|
||||
所有 LLM-facing 输出必须遵循:
|
||||
|
||||
| 原则 | 含义 | 实现方式 |
|
||||
|------|------|---------|
|
||||
| **不喂全量** | 只传 FAIL/WARNING 的切片 | `QcReportService` 从 `qc_field_status` WHERE status IN ('FAIL','WARNING') |
|
||||
| **不喂物理字段** | 字段名用中文语义 | 查 `IitFieldMapping.semanticLabel` 替换 |
|
||||
| **不让 LLM 算数** | 百分比、天数差等由 Node.js 预算 | 所有数值结论以"标签"形式传入(如"超窗 5 天"而非让 LLM 算日期差) |
|
||||
|
||||
### 3.8 Record-Level Context:跨表单质控上下文全拉取
|
||||
|
||||
**问题**:REDCap DET Webhook 推送的 payload 只包含**当前保存的表单**数据。如果 CRC 保存了"实验室检查表",Webhook 只带这一个表单。但 D5 规则(如"ALT 异常但没报 AE")需要同时查看 AE 表的数据——拿不到就会误判。
|
||||
|
||||
**设计原则**:在 IIT 场景下,单个患者的全量数据通常只有几十到几百行(几 KB),不存在性能问题。因此,**不论 Webhook 传来了什么表单,`QcExecutor` 一律拉取该患者的全量数据**。
|
||||
|
||||
```typescript
|
||||
// QcExecutor.executeSingle() 中的数据拉取逻辑
|
||||
async executeSingle(projectId: string, recordId: string, eventId: string, ...) {
|
||||
// ⚠️ 关键:不论 Webhook 传来的 instrument 是什么,都拉全量
|
||||
// 这样跨表单规则(D5 AE 漏报、D6 合并用药禁忌)才能正确执行
|
||||
const allRecords = await redcapAdapter.exportRecords({ records: [recordId] });
|
||||
|
||||
// 标准化五级坐标(含 InstanceID 洗线)
|
||||
const normalized = redcapAdapter.normalizeInstances(allRecords);
|
||||
|
||||
// 按 event 分组,但在内存中保持全量,供跨表单规则访问
|
||||
const patientContext = this.buildPatientContext(normalized);
|
||||
|
||||
// 执行规则时传入完整上下文
|
||||
for (const rule of applicableRules) {
|
||||
const result = engine.executeRule(rule, patientContext);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键区别**:
|
||||
- 旧设计(V3.0):`getRecordById()` 拉全量但 **merge 成扁平对象**,丢失五级结构
|
||||
- 新设计(V3.1):`exportRecords()` 拉全量且 **保持原始多行结构**,每行 = 一个 event × form × instance
|
||||
|
||||
### 3.9 eQuery 自动闭环:State Transition Hook
|
||||
|
||||
**问题**:当质控发现 Field 为 FAIL 时会生成 eQuery。但当 CRC 修正数据后,Webhook 再次触发质控,Field 变回 PASS——此时**没有任何机制自动关闭之前的 eQuery**。系统变成了"只开 Query 不销账"的半成品。
|
||||
|
||||
**设计**:在 `QcExecutor` 的 `upsert qc_field_status` 逻辑中,比较新旧状态,触发自动闭环。
|
||||
|
||||
```typescript
|
||||
// QcExecutor 内部:upsert 前先读取旧状态
|
||||
async upsertFieldStatus(data: FieldStatusData): Promise<void> {
|
||||
const oldRecord = await prisma.qcFieldStatus.findUnique({
|
||||
where: {
|
||||
uq_field_status: {
|
||||
project_id: data.projectId,
|
||||
record_id: data.recordId,
|
||||
event_id: data.eventId,
|
||||
form_name: data.formName,
|
||||
instance_id: data.instanceId,
|
||||
field_name: data.fieldName,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const oldStatus = oldRecord?.status;
|
||||
|
||||
// 执行 upsert
|
||||
await prisma.qcFieldStatus.upsert({ ... });
|
||||
|
||||
// ===== State Transition Hook =====
|
||||
// FAIL → PASS:自动关闭关联的 eQuery
|
||||
if (oldStatus === 'FAIL' && data.status === 'PASS') {
|
||||
await prisma.iitEquery.updateMany({
|
||||
where: {
|
||||
projectId: data.projectId,
|
||||
recordId: data.recordId,
|
||||
eventId: data.eventId,
|
||||
formName: data.formName,
|
||||
instanceId: data.instanceId,
|
||||
fieldName: data.fieldName,
|
||||
status: { in: ['pending', 'reopened'] },
|
||||
},
|
||||
data: {
|
||||
status: 'auto_closed', // 区分于人工 'closed'
|
||||
respondedAt: new Date(),
|
||||
responseText: 'AI 自动复核通过:数据已修正,质控结果变为 PASS',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PASS → FAIL:如果之前有 auto_closed 的 eQuery,自动重开
|
||||
if (oldStatus === 'PASS' && data.status === 'FAIL') {
|
||||
// 新建 eQuery(不重开旧的,保持审计链清晰)
|
||||
await equeryService.create({ ... });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**设计决策**:
|
||||
- 自动关闭使用 `auto_closed` 状态(而非复用 `closed`),便于审计区分"AI 自动销账"和"CRC 回复后人工关闭"
|
||||
- `IitEquery` 的 status 枚举需扩展:`pending | responded | reviewing | closed | reopened | auto_closed`
|
||||
- 回退场景(PASS → FAIL)创建新 eQuery 而非重开旧的,保持审计链完整
|
||||
|
||||
---
|
||||
|
||||
## 4. 批次 A:数据底座加固(预估 1.5-2 周)
|
||||
|
||||
### 4.1 任务清单
|
||||
|
||||
| # | 任务 | 工作量 | 依赖 |
|
||||
|---|------|--------|------|
|
||||
| A1 | Prisma Schema 升级 + Migration | 1 天 | 无 |
|
||||
| | - 新建 `QcFieldStatus` model(五级坐标) | | |
|
||||
| | - `IitQcLog` 加 `formName`, `instanceId` | | |
|
||||
| | - `IitEquery` 加 `instanceId` | | |
|
||||
| | - `IitFieldMapping` 加 `semanticLabel`, `formName`, `ruleCategory` | | |
|
||||
| A2 | `RedcapAdapter.normalizeInstances()` | 0.5 天 | 无 |
|
||||
| | - InstanceID 幽灵状态洗线 | | |
|
||||
| | - 非重复表单强制 `instanceId: 1` | | |
|
||||
| | - 重复表单第一行强制 `1` | | |
|
||||
| A3 | `QcExecutor` 核心服务 | 2.5 天 | A1, A2 |
|
||||
| | - `executeSingle()` 方法(含 Record-Level Context 全量拉取) | | |
|
||||
| | - `executeBatch()` 方法 | | |
|
||||
| | - 五级坐标 upsert `qc_field_status` | | |
|
||||
| | - State Transition Hook:FAIL→PASS 自动关闭 eQuery | | |
|
||||
| | - `IitEquery.status` 枚举扩展 `auto_closed` | | |
|
||||
| | - 改造 `iitBatchController` 调用 `QcExecutor` | | |
|
||||
| A4 | 规则维度重新标注 | 0.5 天 | A1 |
|
||||
| | - `seed-iit-qc-rules.ts` 所有规则加 `ruleCategory: 'D1'|'D3'` | | |
|
||||
| | - `HardRuleEngine.QCRule.category` 枚举扩展 | | |
|
||||
| A5 | 字段语义同步 + 报告语义化 | 1 天 | A1 |
|
||||
| | - `syncSemanticLabels()` 从 REDCap Metadata 自动填充 | | |
|
||||
| | - `QcReportService` 生成报告时查 `semanticLabel` 替换字段名 | | |
|
||||
| A6 | 验证脚本 | 0.5 天 | A3, A4, A5 |
|
||||
| | - 验证 `qc_field_status` 五级坐标正确性 | | |
|
||||
| | - 验证 LLM 报告字段名已语义化 | | |
|
||||
| | - 回归测试:现有 D1 + D3 功能不退化 | | |
|
||||
|
||||
### 4.2 验收标准
|
||||
|
||||
- [ ] 执行一键全量质控后,`qc_field_status` 包含完整的 project × record × event × form × instance × field 数据
|
||||
- [ ] 非重复表单的 `instance_id` 统一为 1
|
||||
- [ ] 重复表单(如 AE)的多个 Instance 各自独立记录
|
||||
- [ ] LLM XML 报告中字段名为中文语义(如"年龄"而非"age")
|
||||
- [ ] 每条 `qc_field_status` 记录的 `rule_category` 正确标注为 D1-D7
|
||||
- [ ] 跨表单规则可正确执行(如 D5 规则需要同时访问实验室表和 AE 表)
|
||||
- [ ] Field 从 FAIL 变 PASS 时,关联的 eQuery 自动变为 `auto_closed`
|
||||
- [ ] 现有的入排标准检查 + 变量范围检查功能不回归
|
||||
|
||||
---
|
||||
|
||||
## 5. 批次 B:聚合层与冒泡机制(预估 1.5-2 周)
|
||||
|
||||
### 5.1 任务清单
|
||||
|
||||
| # | 任务 | 工作量 | 依赖 |
|
||||
|---|------|--------|------|
|
||||
| B1 | Prisma Schema:新建 `QcEventStatus`,改造 `RecordSummary` | 0.5 天 | 批次 A 完成 |
|
||||
| B2 | `QcExecutor.aggregateDeferred()` | 1.5 天 | B1 |
|
||||
| | - SQL 聚合 field_status → event_status | | |
|
||||
| | - SQL 聚合 event_status → record_summary | | |
|
||||
| | - SQL 聚合 record_summary → project_stats | | |
|
||||
| | - 批量执行后触发一次聚合(防抖) | | |
|
||||
| | - 单条执行后推入 pg-boss 防抖队列 | | |
|
||||
| B3 | 多维报告框架 | 1 天 | B2 |
|
||||
| | - `QcReportService` 按 D1-D7 分章节生成 LLM XML | | |
|
||||
| | - 新增 `<event_overview>` 章节 | | |
|
||||
| | - 新增 `<dimension_summary>` 章节(各维度通过率) | | |
|
||||
| B4 | 定时质控灵活配置 | 1 天 | 批次 A 完成 |
|
||||
| | - 后端:`registerProjectCrons()` 从全局硬编码改为读取项目 `cronExpression` | | |
|
||||
| | - 前端:可视化配置面板(每天/每周/每N小时/高级 Cron) | | |
|
||||
| | - Cron 表达式参考:`0 8 * * *`(每天8:00) / `0 9 * * 1`(每周一) / `0 8 * * 1,3,5`(一三五) | | |
|
||||
| B5 | 前端:受试者×表单热力图原型 | 1 天 | B2 |
|
||||
| | - 从 `qc_event_status` 读取数据 | | |
|
||||
| | - 行 = 受试者,列 = 事件/表单,颜色 = 状态 | | |
|
||||
| B6 | 实时质控激活 | 0.5 天 | B2 |
|
||||
| | - `WebhookController` 接入 `QcExecutor.executeSingle()` | | |
|
||||
| | - 执行后推入防抖聚合队列 | | |
|
||||
| B7 | 端到端验证 | 0.5 天 | B2-B6 |
|
||||
|
||||
### 5.2 验收标准
|
||||
|
||||
- [ ] `qc_field_status` FAIL 后,对应 `qc_event_status` 自动为 FAIL
|
||||
- [ ] `qc_event_status` FAIL 后,对应 `record_summary.latestQcStatus` 自动为 FAIL
|
||||
- [ ] 维度计数正确:`qc_event_status.d1_issues` 等于该事件下 D1 类别 FAIL 的数量
|
||||
- [ ] 批量 1000 条规则执行无死锁(聚合在执行完成后统一进行)
|
||||
- [ ] 不同项目可独立配置定时质控策略
|
||||
- [ ] REDCap 保存表单后 30 秒内三级状态表更新(实时触发)
|
||||
- [ ] 热力图正确展示红/黄/绿状态
|
||||
- [ ] LLM 报告包含维度分章节 + 事件概览
|
||||
- [ ] 报告生成时间 < 2 秒(100 条记录、500 个变量规模)
|
||||
|
||||
---
|
||||
|
||||
## 6. 批次 C:新维度引擎(按需,依赖临床专家输入)
|
||||
|
||||
### 6.1 前提条件
|
||||
|
||||
- 批次 A + B 已稳定运行至少 1 周
|
||||
- 临床专家已确认各维度规则的可行性和优先级
|
||||
- 有真实 IIT 项目数据可供验证
|
||||
|
||||
### 6.2 任务清单(按优先级排序)
|
||||
|
||||
| # | 任务 | 工作量 | 说明 |
|
||||
|---|------|--------|------|
|
||||
| C1 | D2 CompletenessEngine(简化版) | 2 天 | 仅统计 required=true 且无 branching_logic 的绝对必填字段缺失率 |
|
||||
| C2 | D6 方案偏离引擎 | 2 天 | 访视超窗检测(目标日 ± N 天) |
|
||||
| C3 | D5 AE 漏报侦测 | 2-3 天 | SoftRule + RAG,实验室异常 → 检查 AE 表有无匹配 |
|
||||
| C4 | 项目健康度评分 | 1 天 | D1-D7 加权综合,可视化展示 |
|
||||
| C5 | 沙盒测试机制 | 1 天 | 历史数据回放 + 结果导出 Excel |
|
||||
|
||||
### 6.3 D2 缺失率的折中过渡法(V1.1 修正:增加时序过滤)
|
||||
|
||||
**问题 1(字段维度)**:在完整的 Branching Logic 解析器实现之前,简单的"总字段数 - 实填数"会严重高估缺失率(把因分支逻辑隐藏的字段也算作缺失)。
|
||||
|
||||
**问题 2(时序维度,V1.1 新增)**:如果一个项目有 V1-V10 共 10 次访视,患者昨天刚入组(当前在 V1),系统去算缺失率会把 V2-V10 的必填字段全部算作"已缺失",导致新入组患者缺失率高达 90%。这是**致命的临床逻辑错误**——那些访视根本还没发生。
|
||||
|
||||
**解法**:双重过滤——字段过滤 + 时序过滤。
|
||||
|
||||
```typescript
|
||||
async function calculateMissingRate(
|
||||
projectId: string,
|
||||
recordId: string
|
||||
): Promise<{ rate: number; denominator: number; numerator: number }> {
|
||||
|
||||
// 1. 字段过滤:只统计绝对必填字段(required=y 且无 branching_logic)
|
||||
const metadata = await redcapAdapter.exportMetadata();
|
||||
const absoluteRequired = metadata.filter(
|
||||
f => f.required_field === 'y' && !f.branching_logic
|
||||
);
|
||||
|
||||
// 2. 时序过滤(V1.1 关键补丁):
|
||||
// 找出该患者在 REDCap 中有实质数据的事件列表
|
||||
const patientRecords = await redcapAdapter.exportRecords({ records: [recordId] });
|
||||
const activeEvents = new Set(
|
||||
patientRecords
|
||||
.filter(r => hasSubstantiveData(r)) // 排除只有 record_id 的空行
|
||||
.map(r => r.redcap_event_name)
|
||||
);
|
||||
|
||||
// 3. 只统计已到达事件中的绝对必填字段
|
||||
const formEventMapping = await redcapAdapter.getFormEventMapping();
|
||||
let denominator = 0;
|
||||
let filled = 0;
|
||||
|
||||
for (const field of absoluteRequired) {
|
||||
// 该字段所属的表单,在哪些事件中出现
|
||||
const fieldEvents = formEventMapping
|
||||
.filter(m => m.form === field.form_name)
|
||||
.map(m => m.unique_event_name);
|
||||
|
||||
// 只计算患者已到达的事件
|
||||
for (const event of fieldEvents) {
|
||||
if (!activeEvents.has(event)) continue; // 未来事件,跳过
|
||||
denominator++;
|
||||
const record = patientRecords.find(
|
||||
r => r.redcap_event_name === event
|
||||
);
|
||||
if (record && record[field.field_name] != null && record[field.field_name] !== '') {
|
||||
filled++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const numerator = denominator - filled;
|
||||
const rate = denominator > 0 ? Math.round((numerator / denominator) * 1000) / 10 : 0;
|
||||
return { rate, denominator, numerator };
|
||||
}
|
||||
|
||||
function hasSubstantiveData(record: Record<string, any>): boolean {
|
||||
// 排除只有 record_id / redcap_event_name 等元数据的空行
|
||||
const metaFields = ['record_id', 'redcap_event_name', 'redcap_repeat_instrument', 'redcap_repeat_instance'];
|
||||
return Object.entries(record).some(
|
||||
([key, val]) => !metaFields.includes(key) && val != null && val !== ''
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**总结**:
|
||||
- **字段过滤**:`required=y` 且无 `branching_logic` → 排除条件字段
|
||||
- **时序过滤**:只统计患者已有数据的事件 → 排除未来访视
|
||||
- 两层过滤后,分子分母才是临床上无争议的
|
||||
|
||||
### 6.4 沙盒测试机制
|
||||
|
||||
**问题**:临床专家不懂代码,怎么验证新规则的准确性?
|
||||
|
||||
**解法**:开发"历史数据回放"功能:
|
||||
|
||||
```
|
||||
1. 管理员配置新的 D5/D6 规则(状态设为"草稿",不在生产环境生效)
|
||||
2. 点击"沙盒测试"按钮
|
||||
3. 系统拿该项目所有历史患者数据,用草稿规则跑一遍
|
||||
4. 结果不写入 qc_field_status(只存临时表或内存)
|
||||
5. 导出 Excel:"AI 抓出了这 5 个疑似漏报 AE,请主任确认"
|
||||
6. 专家确认 OK → 规则状态改为"已发布",正式生效
|
||||
```
|
||||
|
||||
**实现要点**:
|
||||
- `IitSkill` 增加 `status` 字段:`draft` | `published` | `archived`
|
||||
- `QcExecutor` 加载规则时只取 `status = 'published'` 的
|
||||
- 沙盒执行复用 `QcExecutor` 核心逻辑,但结果写入临时存储
|
||||
|
||||
---
|
||||
|
||||
## 7. LLM 报告升级(批次 B 产出)
|
||||
|
||||
### 7.1 多维报告 XML 结构
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<qc_context project_id="xxx" project_name="test0207" generated="2026-03-15T08:00:00Z">
|
||||
|
||||
<!-- 1. 宏观统计 -->
|
||||
<summary>
|
||||
- 通过率: 92.9%(14 条记录,13 通过,1 失败)
|
||||
- 事件覆盖: 42 个事件已质控,39 通过
|
||||
- 严重问题: 1 | 警告: 2
|
||||
</summary>
|
||||
|
||||
<!-- 2. 维度概览(各维度独立通过率)-->
|
||||
<dimension_summary>
|
||||
- D1 入排合规: 13/14 通过 (92.9%) — 1 条排除标准违规
|
||||
- D3 变量准确: 550/556 通过 (98.9%) — 6 条极值异常
|
||||
- D2 数据完整: --(尚未启用)
|
||||
- D5 安全性: --(尚未启用)
|
||||
- D6 方案偏离: --(尚未启用)
|
||||
</dimension_summary>
|
||||
|
||||
<!-- 3. 事件维度统计 -->
|
||||
<event_overview>
|
||||
- 筛选期: 14/14 通过 (100%)
|
||||
- 基线期: 13/14 通过 (92.9%)
|
||||
- 随访1: 12/12 通过 (100%)
|
||||
</event_overview>
|
||||
|
||||
<!-- 4. 严重问题详情(按受试者 × 事件 × 表单 × 实例定位)-->
|
||||
<critical_issues count="1">
|
||||
<record id="3">
|
||||
<event name="基线期">
|
||||
<form name="入排标准表" instance="1">
|
||||
1. [D1][exc_001] **排除标准检查**: 排除标准第1项 = **1**(标准: 0)
|
||||
</form>
|
||||
</event>
|
||||
</record>
|
||||
</critical_issues>
|
||||
|
||||
<!-- 5. 警告问题 -->
|
||||
<warnings count="2">
|
||||
<record id="7">
|
||||
<event name="随访2">
|
||||
<form name="实验室检查" instance="1">
|
||||
1. [D3][lab_003] **谷丙转氨酶(ALT)**: 当前值 **52 U/L**(正常上限: 50 U/L)
|
||||
</form>
|
||||
</event>
|
||||
</record>
|
||||
</warnings>
|
||||
|
||||
</qc_context>
|
||||
```
|
||||
|
||||
**对比 V3.0**:
|
||||
- 新增 `<dimension_summary>` —— 各维度独立呈现
|
||||
- 新增 `<event_overview>` —— 事件级统计
|
||||
- 问题定位从 `record → field` 精确到 `record → event → form → instance → field`
|
||||
- 每条问题标注维度代码(如 `[D1]`、`[D3]`)
|
||||
|
||||
---
|
||||
|
||||
## 8. 与四大工具的集成变更
|
||||
|
||||
| 工具 | 批次 A 变更 | 批次 B 变更 |
|
||||
|------|------------|------------|
|
||||
| `read_report` | 报告字段名语义化 | 新增 `section=dimension_summary` / `section=event_overview` |
|
||||
| `look_up_data` | 无变更 | 可附带每个字段的 `qc_status`(从 `qc_field_status` 读取) |
|
||||
| `check_quality` | 调用 `QcExecutor` 替代旧逻辑 | 自动触发聚合,结果写入三级状态表 |
|
||||
| `search_knowledge` | 无变更 | 无变更 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与缓解
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|---------|
|
||||
| REDCap InstanceID 返回值不一致 | 高 | 五级坐标不完整 | `RedcapAdapter.normalizeInstances()` 强制标准化(3.1 节) |
|
||||
| 跨表单规则拿不到完整上下文 | 高 | D5 规则误判 | Record-Level Context 全量拉取(3.8 节) |
|
||||
| eQuery 只开不关 | 高 | 系统不可信 | State Transition Hook 自动闭环(3.9 节) |
|
||||
| 批量质控冒泡导致死锁 | 中 | 数据库卡顿 | 受试者级防抖聚合(3.4 节) |
|
||||
| D2 缺失率被高估(无分支逻辑) | 高 | 临床不信任 | 绝对必填字段 + 时序过滤(6.3 节) |
|
||||
| D2 缺失率计入未来访视 | 高 | 新入组患者 90% 缺失 | Event-Aware 过滤,只统计已到达事件(6.3 节) |
|
||||
| 新规则上线后误报过多 | 中 | 影响用户信心 | 沙盒测试 + 专家确认后再发布(6.4 节) |
|
||||
| Migration 影响现有数据 | 低 | 数据丢失 | 新增表/列均为追加型,不修改现有数据 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 关键决策记录
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 数据层级 | 五级(Record → Event → Form → Instance → Field) | 对齐 CDISC ODM,解决重复表单定位 |
|
||||
| InstanceID 标准化位置 | `RedcapAdapter` 层统一处理 | 下游无需关心 REDCap 返回值不一致 |
|
||||
| 状态优先级 | FAIL(3) > WARNING(2) > UNCERTAIN(1) > PASS(0) | 聚合时始终取最严重状态 |
|
||||
| SKIPPED 变量处理 | 不写入 `qc_field_status` | 减少无意义数据,简化聚合逻辑 |
|
||||
| 实时质控数据拉取范围 | Record-Level(全量拉取该患者所有事件/表单) | 跨表单规则必须有完整上下文 |
|
||||
| eQuery 闭环策略 | State Transition Hook(FAIL→PASS 自动关闭) | 避免"只开不关",状态用 `auto_closed` 区分人工关闭 |
|
||||
| 冒泡防抖粒度 | 实时触发用受试者级,批量触发用项目级 | 多 CRC 同时录入不同受试者时互不干扰 |
|
||||
| D2 缺失率过渡方案 | 绝对必填字段 + Event-Aware 时序过滤 | 排除条件字段和未来访视,分子分母临床无争议 |
|
||||
| 规则发布流程 | draft → sandbox test → published | 临床专家可验证准确性 |
|
||||
| 规则分类 | D1-D7 七大维度 | 对齐 CRA 工作维度,支撑多维报告 |
|
||||
| 字段语义化数据源 | REDCap Data Dictionary `field_label` | 已有数据,自动同步,DM 可微调 |
|
||||
| 自动映射 vs 半自动映射 | 半自动(系统建议 + DM 确认) | 全自动 LLM 映射准确率不可控 |
|
||||
| V3.0 文档处置 | 保留为历史版本 | 记录从三级到五级的设计演进 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 时间线总览
|
||||
|
||||
```
|
||||
批次 A 批次 B 批次 C
|
||||
(数据底座加固) (聚合与冒泡) (新维度引擎)
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||||
│ 1.5-2 周 │ │ 1.5-2 周 │ │ 按需 │
|
||||
│ │ │ │ │ │
|
||||
Week 1-2 │ A1 Schema 升级 │ │ │ │ │
|
||||
│ A2 InstanceID 洗线│ │ │ │ │
|
||||
│ A3 QcExecutor │ │ │ │ │
|
||||
│ A4 规则维度标注 │ │ │ │ │
|
||||
Week 2-3 │ A5 语义化同步 │ │ │ │ │
|
||||
│ A6 验证 │ │ │ │ │
|
||||
└────────┬─────────┘ │ │ │ │
|
||||
│ 验收通过 │ │ │ │
|
||||
Week 3-4 └──────────────►│ B1 Event 表 │ │ │
|
||||
│ B2 防抖聚合 │ │ │
|
||||
│ B3 多维报告 │ │ │
|
||||
Week 4-5 │ B4 定时配置 │ │ │
|
||||
│ B5 热力图 │ │ │
|
||||
│ B6 实时质控 │ │ │
|
||||
│ B7 端到端验证 │ │ │
|
||||
└────────┬─────────┘ │ │
|
||||
│ 验收通过 + │ │
|
||||
│ 临床专家确认 │ │
|
||||
Week 6+ └──────────────►│ C1 D2 缺失率 │
|
||||
│ C2 D6 偏离 │
|
||||
│ C3 D5 AE │
|
||||
│ C4 健康度 │
|
||||
│ C5 沙盒测试 │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **一句话总结**:五级坐标让每个质控结论都有精确的 GPS 定位,维度分类让报告从扁平变为多维,异步防抖聚合让冒泡机制可靠落地——这三者共同构成 V3.1 质控引擎的架构基石。先把底座做对(批次 A),再把聚合做稳(批次 B),最后把覆盖做广(批次 C)。
|
||||
@@ -0,0 +1,697 @@
|
||||
# 质控引擎架构升级 — 三级数据结构与多模式触发技术设计
|
||||
|
||||
> **版本:** V1.0
|
||||
> **日期:** 2026-03-01
|
||||
> **定位:** 质控引擎核心架构升级,解决当前数据粒度不足、调度僵化、无法精准溯源的根本问题
|
||||
> **关联文档:**
|
||||
> - [V3.0 全新开发计划](./V3.0全新开发计划.md)
|
||||
> - [CRA 智能质控 Agent 四大工具工作原理说明](../../08-对外输出报告/CRA智能质控Agent-四大工具工作原理说明.md)
|
||||
> - [CRA AI 替代工作梳理](../../09-技术评审报告/CRA%20AI%20替代工作梳理2.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心认知:研究方案 → 变量清单 → 质控规则 → 质控报告
|
||||
|
||||
在 IIT 临床研究中,质控体系不是一堆孤立的模块,而是一条**从研究方案到质控报告的完整链条**:
|
||||
|
||||
```
|
||||
研究方案(Protocol)
|
||||
│ 定义了:纳入/排除标准、访视窗口、用药规范、AE 监测要求
|
||||
▼
|
||||
变量清单(Data Dictionary / CRF 变量)
|
||||
│ 将方案中的每一条要求,具体化为 EDC 系统中可采集的字段
|
||||
│ 例:方案规定"年龄 16-35 岁" → CRF 中 age 字段(数值型,范围 16-35)
|
||||
▼
|
||||
质控规则(QC Rules)
|
||||
│ 将变量的合规要求,编码为机器可执行的逻辑
|
||||
│ 例:{ "and": [{ ">=": ["age", 16] }, { "<=": ["age", 35] }] }
|
||||
▼
|
||||
质控报告(QC Report)
|
||||
│ 将规则执行的结果,聚合为人和 AI 可理解的报告
|
||||
│ 例:通过率 92.9%,1 条严重违规(3 号受试者排除标准不合规)
|
||||
▼
|
||||
行动(eQuery / 告警 / 监查报告)
|
||||
将报告中的问题,转化为具体的跟进动作
|
||||
```
|
||||
|
||||
**这四者是 1:1:1:1 的映射关系**——方案中的每一条要求,对应变量清单中的具体字段,对应一条或多条质控规则,最终体现在质控报告中的一条具体结论。
|
||||
|
||||
**当前问题**:我们的质控引擎在"规则执行"层面已经成熟(HardRuleEngine + SkillRunner),但在**数据存储粒度**和**调度灵活性**上存在显著不足,导致报告聚合困难、历史追溯低效、调度僵化。
|
||||
|
||||
---
|
||||
|
||||
## 2. 问题诊断
|
||||
|
||||
### 2.1 数据结构粒度不足
|
||||
|
||||
REDCap 中的数据天然具有三级结构:
|
||||
|
||||
```
|
||||
Record(受试者记录)
|
||||
└── Event(访视事件,如:筛选期、基线期、随访 1、随访 2...)
|
||||
└── Variable(变量/字段,如:age、gender、lab_alt、consent_date...)
|
||||
```
|
||||
|
||||
**但当前质控数据结构只有 1.5 级**:
|
||||
|
||||
| 数据层级 | 当前是否有独立状态表 | 问题 |
|
||||
|---------|------------------|------|
|
||||
| **记录级(Record)** | `record_summary` 表,有 `latestQcStatus` | 有,但缺乏事件维度详情 |
|
||||
| **事件级(Event)** | **无独立表** | 事件状态藏在 `qc_logs` 日志里,查询需 `DISTINCT ON` 聚合,效率低、易出错 |
|
||||
| **变量级(Variable)** | **无独立表** | 变量级结果藏在 `qc_logs.issues` 的 JSON 数组里,无法直接 SQL 查询 |
|
||||
|
||||
**实际后果**:
|
||||
|
||||
1. 之前出现的"通过率为 0%"的 Bug,根本原因之一就是缺少事件级状态表,报告聚合时把不同事件的旧数据混入计算
|
||||
2. 无法回答"3 号受试者在基线期的 age 字段质控状态是什么"这样的精确查询
|
||||
3. 每次生成报告都要从日志中做复杂的 SQL 聚合,性能差且容易遗漏
|
||||
|
||||
### 2.2 定时质控调度僵化
|
||||
|
||||
当前定时质控**全局硬编码**为每天 08:00:
|
||||
|
||||
```typescript
|
||||
// 当前代码(iit-manager/index.ts)
|
||||
await jobQueue.schedule('iit_daily_qc', '0 0 * * *', {}, { tz: 'Asia/Shanghai' });
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 所有项目共享一个全局调度,无法按项目独立配置
|
||||
- 不支持"每周一三五"、"每 6 小时"等灵活策略
|
||||
- 数据库已预留 `cronEnabled` / `cronExpression` 字段,但代码未使用
|
||||
|
||||
### 2.3 实时质控未激活
|
||||
|
||||
系统**已实现**了 REDCap DET(Data Entry Trigger)Webhook 接收端,但:
|
||||
- REDCap 端是否配置了 DET 取决于部署环境
|
||||
- Webhook 处理逻辑写入的也是旧的 `qc_logs` 结构,同样存在粒度不足问题
|
||||
|
||||
---
|
||||
|
||||
## 3. 设计目标
|
||||
|
||||
### 3.1 三级质控状态体系
|
||||
|
||||
建立与 REDCap 数据结构 **1:1 对齐**的三级质控状态:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Record 10 │
|
||||
│ 记录级状态: FAIL(取所有事件中最严重的) │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Event A: 筛选期 │ │
|
||||
│ │ 事件级状态: PASS │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ age = 28 → QC: PASS (规则: 16-35岁) │ │ │
|
||||
│ │ │ gender = 1 → QC: PASS (规则: 非空) │ │ │
|
||||
│ │ │ consent = 1 → QC: PASS (规则: 已签署) │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Event B: 基线期 │ │
|
||||
│ │ 事件级状态: FAIL │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ exclusion = 1 → QC: FAIL (规则: 应为0) │ │ │
|
||||
│ │ │ lab_alt = 5.2 → QC: PASS (规则: <40) │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Event C: 随访 1 │ │
|
||||
│ │ 事件级状态: PASS │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ followup_date = 2026-03-01 → QC: PASS │ │ │
|
||||
│ │ │ vitals_bp = 120/80 → QC: PASS │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 多模式触发体系
|
||||
|
||||
| 触发模式 | 描述 | 粒度 | 延迟 |
|
||||
|---------|------|------|------|
|
||||
| **实时触发** | REDCap DET Webhook,录入即质控 | 单记录 × 单事件 × 单表单 | 秒级 |
|
||||
| **定时触发** | 按项目独立配置 Cron 策略 | 全量记录 × 全量事件 | 分钟级 |
|
||||
| **手动触发** | 用户点击"一键全量质控"或 AI 调用 | 全量或指定记录 | 分钟级 |
|
||||
|
||||
### 3.3 自底向上的聚合链
|
||||
|
||||
```
|
||||
qc_logs(审计日志,追加型,不删不改,保留完整历史)
|
||||
↓ 每次质控后 upsert ↓
|
||||
qc_field_status(变量级,保持最新状态)
|
||||
↓ 自底向上聚合 ↓
|
||||
qc_event_status(事件级,保持最新状态)
|
||||
↓ 自底向上聚合 ↓
|
||||
record_summary(记录级,保持最新状态)
|
||||
↓ 自底向上聚合 ↓
|
||||
qc_project_stats(项目级统计)
|
||||
↓ 格式化生成 ↓
|
||||
qc_reports(LLM 友好报告 + 人类可读报告)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库设计
|
||||
|
||||
### 4.1 新增表:`qc_field_status`(变量级质控状态)
|
||||
|
||||
> 每个 **project × record × event × field** 唯一一行,始终反映该变量的**最新**质控状态。
|
||||
|
||||
```sql
|
||||
CREATE TABLE iit_schema.qc_field_status (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id TEXT NOT NULL,
|
||||
record_id TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL, -- REDCap event_name
|
||||
field_name TEXT NOT NULL, -- 变量名(REDCap field_name)
|
||||
|
||||
-- 质控结果
|
||||
status TEXT NOT NULL, -- 'PASS' | 'FAIL' | 'WARNING' | 'SKIPPED'
|
||||
rule_id TEXT, -- 触发的规则 ID
|
||||
rule_name TEXT, -- 规则名称(冗余,查询方便)
|
||||
severity TEXT, -- 'critical' | 'warning' | 'info'
|
||||
message TEXT, -- 质控结论描述
|
||||
actual_value TEXT, -- 实际值
|
||||
expected_value TEXT, -- 期望值/标准
|
||||
|
||||
-- 溯源
|
||||
source_qc_log_id TEXT, -- 关联到 qc_logs 的具体记录
|
||||
triggered_by TEXT NOT NULL, -- 'webhook' | 'cron' | 'manual'
|
||||
last_qc_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- 唯一约束:每个变量只保留最新状态
|
||||
CONSTRAINT unique_field_status
|
||||
UNIQUE (project_id, record_id, event_id, field_name),
|
||||
|
||||
-- 时间戳
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_field_status_record
|
||||
ON iit_schema.qc_field_status (project_id, record_id);
|
||||
CREATE INDEX idx_field_status_event
|
||||
ON iit_schema.qc_field_status (project_id, record_id, event_id);
|
||||
CREATE INDEX idx_field_status_fail
|
||||
ON iit_schema.qc_field_status (project_id, status)
|
||||
WHERE status IN ('FAIL', 'WARNING');
|
||||
CREATE INDEX idx_field_status_rule
|
||||
ON iit_schema.qc_field_status (project_id, rule_id);
|
||||
```
|
||||
|
||||
**关键设计决策**:
|
||||
|
||||
- `actual_value` 和 `expected_value` 用 `TEXT` 存储(非 JSONB),因为变量值可能是数字、日期、字符串等多种类型,统一为文本便于展示和比较
|
||||
- `source_qc_log_id` 指向 `qc_logs` 表,支持从状态追溯到具体的质控执行记录
|
||||
- `SKIPPED` 状态用于"字段在该事件中不存在"的情况(如随访期没有 age 字段)
|
||||
|
||||
### 4.2 新增表:`qc_event_status`(事件级质控状态)
|
||||
|
||||
> 每个 **project × record × event** 唯一一行,状态 = 其下所有变量的最严重状态。
|
||||
|
||||
```sql
|
||||
CREATE TABLE iit_schema.qc_event_status (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id TEXT NOT NULL,
|
||||
record_id TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL, -- REDCap event_name
|
||||
event_label TEXT, -- 事件显示名(如"筛选期")
|
||||
|
||||
-- 聚合状态
|
||||
status TEXT NOT NULL, -- 'PASS' | 'FAIL' | 'WARNING'
|
||||
fields_total INT NOT NULL DEFAULT 0, -- 已检查的变量总数
|
||||
fields_passed INT NOT NULL DEFAULT 0,
|
||||
fields_failed INT NOT NULL DEFAULT 0,
|
||||
fields_warning INT NOT NULL DEFAULT 0,
|
||||
fields_skipped INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- 问题摘要(方便快速查询,不需要 JOIN field_status)
|
||||
top_issues JSONB DEFAULT '[]', -- 最严重的 N 条问题摘要
|
||||
|
||||
-- 关联的表单列表
|
||||
forms_checked TEXT[] DEFAULT '{}', -- 该事件中参与质控的表单
|
||||
|
||||
-- 溯源
|
||||
triggered_by TEXT NOT NULL,
|
||||
last_qc_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- 唯一约束
|
||||
CONSTRAINT unique_event_status
|
||||
UNIQUE (project_id, record_id, event_id),
|
||||
|
||||
-- 时间戳
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_event_status_record
|
||||
ON iit_schema.qc_event_status (project_id, record_id);
|
||||
CREATE INDEX idx_event_status_fail
|
||||
ON iit_schema.qc_event_status (project_id, status)
|
||||
WHERE status IN ('FAIL', 'WARNING');
|
||||
```
|
||||
|
||||
### 4.3 改造表:`record_summary`(记录级质控状态)
|
||||
|
||||
> 在现有 `record_summary` 基础上新增事件维度统计字段:
|
||||
|
||||
```sql
|
||||
-- 新增列(在已有表上 ALTER)
|
||||
ALTER TABLE iit_schema.record_summary
|
||||
ADD COLUMN IF NOT EXISTS events_total INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS events_passed INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS events_failed INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS events_warning INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fields_total INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fields_passed INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fields_failed INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS top_issues JSONB DEFAULT '[]';
|
||||
```
|
||||
|
||||
### 4.4 保留表:`qc_logs`(审计日志,不变)
|
||||
|
||||
`qc_logs` 继续作为**追加型审计日志**,每次质控执行都新增一行,永不删除、永不修改。它是完整的历史记录,用于:
|
||||
|
||||
- 趋势分析(对比不同时间点的质控结果)
|
||||
- 审计追踪(谁在什么时间触发了什么质控)
|
||||
- 回溯调查(某个问题是何时首次被发现的)
|
||||
|
||||
### 4.5 完整数据模型关系图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ iit_schema │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
|
||||
│ │ qc_logs │ │ qc_field_ │ │ qc_event_ │ │
|
||||
│ │ (审计日志) │◄────│ status │─────►│ status │ │
|
||||
│ │ │ ref │ (变量级最新) │ 聚合 │ (事件级最新) │ │
|
||||
│ │ 追加型 │ │ │ │ │ │
|
||||
│ │ 不删不改 │ │ project_id │ │ project_id │ │
|
||||
│ │ │ │ record_id │ │ record_id │ │
|
||||
│ │ 每次质控 │ │ event_id │ │ event_id │ │
|
||||
│ │ 一行记录 │ │ field_name │ │ │ │
|
||||
│ │ │ │ status │ │ status │ │
|
||||
│ │ │ │ rule_id │ │ fields_total │ │
|
||||
│ │ │ │ actual_value │ │ fields_passed │ │
|
||||
│ │ │ │ expected_val │ │ fields_failed │ │
|
||||
│ └─────────────┘ └──────────────┘ └───────┬───────┘ │
|
||||
│ │ 聚合 │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ qc_reports │◄────│ qc_project_ │◄────│ record_ │ │
|
||||
│ │ (LLM报告) │ 生成 │ stats │ 聚合 │ summary │ │
|
||||
│ │ │ │ (项目级统计) │ │ (记录级最新) │ │
|
||||
│ │ llm_report │ │ │ │ │ │
|
||||
│ │ summary │ │ total_records │ │ latestQcStatus│ │
|
||||
│ │ issues │ │ passed_records│ │ events_total │ │
|
||||
│ │ │ │ passRate │ │ events_passed │ │
|
||||
│ └──────────────┘ └───────────────┘ └───────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 多模式触发设计
|
||||
|
||||
### 5.1 实时触发(REDCap DET Webhook)
|
||||
|
||||
**已有基础**:`WebhookController.ts` 已实现 DET 接收端。
|
||||
|
||||
**升级点**:Webhook 处理完毕后,不仅写 `qc_logs`,还需同步更新三级状态表。
|
||||
|
||||
```
|
||||
REDCap 保存表单
|
||||
↓ DET POST
|
||||
/api/v1/iit/webhooks/redcap
|
||||
↓ 立即返回 200,异步处理
|
||||
WebhookController.processWebhookAsync()
|
||||
↓
|
||||
pg-boss 队列(iit_quality_check,5 分钟防抖)
|
||||
↓
|
||||
QcExecutor.executeSingleRecord(projectId, recordId, eventId, formName)
|
||||
├── 1. RedcapAdapter 拉取该记录该事件的最新数据
|
||||
├── 2. HardRuleEngine 逐规则执行
|
||||
├── 3. 结果写入 qc_logs(追加,审计)
|
||||
├── 4. upsert qc_field_status(逐变量更新最新状态)
|
||||
├── 5. upsert qc_event_status(聚合该事件所有变量状态)
|
||||
├── 6. upsert record_summary(聚合该记录所有事件状态)
|
||||
└── 7. 刷新 qc_project_stats 和 qc_reports 缓存
|
||||
```
|
||||
|
||||
### 5.2 定时触发(项目级 Cron 配置)
|
||||
|
||||
**改造方案**:
|
||||
|
||||
#### 5.2.1 数据库配置
|
||||
|
||||
`IitProject` 表已有 `cronEnabled` 和 `cronExpression` 字段,直接使用。
|
||||
|
||||
#### 5.2.2 前端配置界面
|
||||
|
||||
在项目管理页面新增"定时质控"配置面板:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 定时质控配置 │
|
||||
│ │
|
||||
│ ○ 关闭定时质控 │
|
||||
│ ● 开启定时质控 │
|
||||
│ │
|
||||
│ 频率: │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ ○ 每天 时间 [08:00]│ │
|
||||
│ │ ○ 每周指定日期 │ │
|
||||
│ │ ☑ 周一 ☐ 周二 ☑ 周三 │ │
|
||||
│ │ ☐ 周四 ☑ 周五 ☐ 周六 ☐ 周日│ │
|
||||
│ │ 时间 [08:00] │ │
|
||||
│ │ ○ 每隔 N 小时 [6] 小时 │ │
|
||||
│ │ ○ 高级(Cron 表达式) │ │
|
||||
│ │ [0 8 * * 1,3,5] │ │
|
||||
│ │ ⓘ Cron 表达式说明 │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ 下次执行时间:2026-03-03 周一 08:00 │
|
||||
│ │
|
||||
│ [保存配置] │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Cron 表达式说明**(对临床专家的通俗解释):
|
||||
|
||||
| 配置需求 | 对应表达式 | 含义 |
|
||||
|---------|-----------|------|
|
||||
| 每天 8:00 | `0 8 * * *` | 每天早上 8 点执行 |
|
||||
| 每周一 9:00 | `0 9 * * 1` | 每周一早上 9 点 |
|
||||
| 每周一三五 8:00 | `0 8 * * 1,3,5` | 周一、周三、周五早上 8 点 |
|
||||
| 每 6 小时 | `0 */6 * * *` | 每天 0:00, 6:00, 12:00, 18:00 |
|
||||
| 工作日每天 9:00 | `0 9 * * 1-5` | 周一到周五每天 9 点 |
|
||||
| 每天 8:00 和 20:00 | `0 8,20 * * *` | 早晚各一次 |
|
||||
|
||||
> **说明**:大多数用户通过可视化界面选择即可,系统自动生成 Cron 表达式。只有高级用户需要直接填写表达式。
|
||||
|
||||
#### 5.2.3 后端调度改造
|
||||
|
||||
```typescript
|
||||
// 改造后的逻辑(伪代码)
|
||||
async function registerProjectCrons() {
|
||||
const projects = await prisma.iitProject.findMany({
|
||||
where: { cronEnabled: true, status: 'active' },
|
||||
select: { id: true, cronExpression: true },
|
||||
});
|
||||
|
||||
for (const project of projects) {
|
||||
const cronExpr = project.cronExpression || '0 0 * * *'; // 默认每天 08:00
|
||||
await jobQueue.schedule(
|
||||
`iit_qc_${project.id}`, // 每个项目独立的任务名
|
||||
cronExpr,
|
||||
{ projectId: project.id },
|
||||
{ tz: 'Asia/Shanghai' }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 手动触发
|
||||
|
||||
- **前端入口**:质控驾驶舱"一键全量质控"按钮
|
||||
- **AI 入口**:`check_quality` 工具(用户对话中说"帮我跑一下质控")
|
||||
- **执行流程**:与定时触发相同,复用 `QcExecutor.executeBatch()`
|
||||
|
||||
---
|
||||
|
||||
## 6. 质控执行器重构(QcExecutor)
|
||||
|
||||
### 6.1 核心流程
|
||||
|
||||
将当前分散在 `SkillRunner`、`iitBatchController`、`QcReportService` 中的质控执行逻辑,统一收归到 `QcExecutor` 服务中:
|
||||
|
||||
```typescript
|
||||
class QcExecutor {
|
||||
|
||||
/**
|
||||
* 单记录质控(实时触发 / AI 调用)
|
||||
*/
|
||||
async executeSingleRecord(
|
||||
projectId: string,
|
||||
recordId: string,
|
||||
eventId: string,
|
||||
options?: { triggeredBy: 'webhook' | 'manual' }
|
||||
): Promise<void> {
|
||||
// 1. 从 REDCap 拉取该 record × event 的最新数据
|
||||
// 2. 加载适用于该事件的质控规则
|
||||
// 3. 逐规则执行 → 收集变量级结果
|
||||
// 4. 写入 qc_logs(追加审计日志)
|
||||
// 5. upsert qc_field_status(逐变量)
|
||||
// 6. upsert qc_event_status(聚合事件)
|
||||
// 7. upsert record_summary(聚合记录)
|
||||
// 8. 更新 qc_project_stats
|
||||
// 9. 标记 qc_reports 缓存过期
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量质控(定时触发 / 手动触发)
|
||||
*/
|
||||
async executeBatch(
|
||||
projectId: string,
|
||||
options?: { triggeredBy: 'cron' | 'manual' }
|
||||
): Promise<BatchResult> {
|
||||
// 1. 从 REDCap 拉取全量记录(按事件分开)
|
||||
// 2. 基线数据合并(将基线事件数据合并到后续事件)
|
||||
// 3. 逐 record × event 调用 executeSingleRecord 逻辑
|
||||
// 4. 汇总批量结果
|
||||
// 5. 强制刷新 qc_reports
|
||||
// 6. 推送通知(企微等)
|
||||
}
|
||||
|
||||
/**
|
||||
* 自底向上聚合(内部方法)
|
||||
* field_status → event_status → record_summary → project_stats
|
||||
*/
|
||||
private async aggregateUpward(
|
||||
projectId: string,
|
||||
recordId: string,
|
||||
eventId: string
|
||||
): Promise<void> {
|
||||
// 事件级:从 qc_field_status 聚合
|
||||
// status = 所有变量中最严重的状态
|
||||
// fields_total = COUNT(*)
|
||||
// fields_passed = COUNT(status='PASS')
|
||||
// fields_failed = COUNT(status='FAIL')
|
||||
|
||||
// 记录级:从 qc_event_status 聚合
|
||||
// latestQcStatus = 所有事件中最严重的状态
|
||||
// events_total = COUNT(*)
|
||||
// events_passed = COUNT(status='PASS')
|
||||
|
||||
// 项目级:从 record_summary 聚合
|
||||
// passRate = passed_records / total_records * 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 状态优先级
|
||||
|
||||
聚合时始终取"最严重的状态":
|
||||
|
||||
```
|
||||
FAIL (3) > WARNING (2) > UNCERTAIN (1) > PASS (0)
|
||||
```
|
||||
|
||||
规则:
|
||||
- **事件状态** = 其下所有变量状态中最严重的
|
||||
- **记录状态** = 其下所有事件状态中最严重的
|
||||
- 只要有一个变量 FAIL,事件就是 FAIL
|
||||
- 只要有一个事件 FAIL,记录就是 FAIL
|
||||
|
||||
### 6.3 SKIPPED 处理
|
||||
|
||||
当某个变量在某个事件中不存在(如随访期没有 age 字段):
|
||||
- `qc_field_status` 中不写入该变量的记录(而非写 SKIPPED)
|
||||
- 该变量不参与事件级聚合计算
|
||||
- 只有被规则实际检查过的变量才写入 `qc_field_status`
|
||||
|
||||
---
|
||||
|
||||
## 7. 报告生成优化
|
||||
|
||||
### 7.1 当前问题
|
||||
|
||||
`QcReportService.aggregateStats()` 目前直接从 `qc_logs` 做 `DISTINCT ON` 聚合,效率低、逻辑复杂。
|
||||
|
||||
### 7.2 优化后
|
||||
|
||||
有了三级状态表后,报告生成变得简单高效:
|
||||
|
||||
```typescript
|
||||
// 报告概览 — 直接从 record_summary 读取
|
||||
const records = await prisma.iitRecordSummary.findMany({
|
||||
where: { projectId },
|
||||
});
|
||||
const totalRecords = records.length;
|
||||
const passedRecords = records.filter(r => r.latestQcStatus === 'PASS').length;
|
||||
const passRate = (passedRecords / totalRecords * 100).toFixed(1);
|
||||
|
||||
// 严重问题列表 — 直接从 qc_field_status 读取
|
||||
const criticalIssues = await prisma.qcFieldStatus.findMany({
|
||||
where: { projectId, status: 'FAIL', severity: 'critical' },
|
||||
orderBy: { lastQcAt: 'desc' },
|
||||
});
|
||||
|
||||
// 事件级统计 — 直接从 qc_event_status 读取
|
||||
const eventStats = await prisma.qcEventStatus.findMany({
|
||||
where: { projectId },
|
||||
});
|
||||
```
|
||||
|
||||
**对比**:
|
||||
|
||||
| 维度 | 改造前 | 改造后 |
|
||||
|------|-------|-------|
|
||||
| 聚合方式 | `DISTINCT ON` 从 qc_logs 实时聚合 | 直接 SELECT 状态表 |
|
||||
| 查询复杂度 | 复杂 SQL + JSON 解析 | 简单 WHERE 过滤 |
|
||||
| 性能 | 随日志增长线性下降 | 恒定(状态表大小固定) |
|
||||
| 正确性 | 容易被旧日志污染 | 始终是最新状态 |
|
||||
|
||||
### 7.3 LLM 报告增强
|
||||
|
||||
三级数据结构让 LLM 报告可以新增"事件维度"章节:
|
||||
|
||||
```xml
|
||||
<qc_context project_id="xxx" project_name="test0207">
|
||||
|
||||
<summary>
|
||||
- 通过率: 92.9%(14 条记录,13 条通过,1 条失败)
|
||||
- 事件覆盖: 42 个事件已质控,39 个通过
|
||||
- 严重问题: 1 | 警告: 0
|
||||
</summary>
|
||||
|
||||
<critical_issues>
|
||||
<record id="3">
|
||||
<event name="基线期">
|
||||
1. [exc_001] **排除标准检查**: 当前值 **1** (标准: 0)
|
||||
</event>
|
||||
</record>
|
||||
</critical_issues>
|
||||
|
||||
<!-- 新增:事件维度统计 -->
|
||||
<event_overview>
|
||||
- 筛选期: 14/14 通过 (100%)
|
||||
- 基线期: 13/14 通过 (92.9%)
|
||||
- 随访1: 12/12 通过 (100%) <!-- 2条记录尚未到达随访1 -->
|
||||
</event_overview>
|
||||
|
||||
</qc_context>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 与四大工具的集成
|
||||
|
||||
### 8.1 `read_report` — 直接受益
|
||||
|
||||
报告数据来源从"日志聚合"变为"状态表直查",响应更快、数据更准确。
|
||||
|
||||
新增支持按事件维度查询:
|
||||
- `section=event_overview`:各事件的通过率统计
|
||||
- `section=record_detail&record_id=3`:某个记录的三级完整状态
|
||||
|
||||
### 8.2 `look_up_data` — 可增加质控标注
|
||||
|
||||
查询原始数据时,可附带每个字段的质控状态:
|
||||
|
||||
```json
|
||||
{
|
||||
"record_id": "3",
|
||||
"age": { "value": "28", "qc_status": "PASS" },
|
||||
"exclusion": { "value": "1", "qc_status": "FAIL", "message": "排除标准应为0" }
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 `check_quality` — 执行后更新三级状态
|
||||
|
||||
调用 `QcExecutor`,结果自动写入三级状态表。
|
||||
|
||||
### 8.4 `search_knowledge` — 不受影响
|
||||
|
||||
知识库检索与质控数据结构无关,不需要改动。
|
||||
|
||||
---
|
||||
|
||||
## 9. 实施计划
|
||||
|
||||
### Phase 1:三级数据结构(2 天)
|
||||
|
||||
| 任务 | 工作量 | 说明 |
|
||||
|------|--------|------|
|
||||
| 编写 Prisma Schema + Migration | 0.5 天 | 新增 `qc_field_status`、`qc_event_status`,改造 `record_summary` |
|
||||
| 实现 `QcExecutor` 服务 | 1 天 | 统一质控执行 + 三级状态更新逻辑 |
|
||||
| 改造 `iitBatchController` | 0.5 天 | 调用 `QcExecutor` 替代原有分散逻辑 |
|
||||
|
||||
### Phase 2:定时质控灵活配置(1 天)
|
||||
|
||||
| 任务 | 工作量 | 说明 |
|
||||
|------|--------|------|
|
||||
| 后端:项目级 Cron 调度改造 | 0.5 天 | 从全局硬编码改为读取项目配置 |
|
||||
| 前端:定时质控配置面板 | 0.5 天 | 可视化选择 + 高级 Cron 输入 |
|
||||
|
||||
### Phase 3:报告生成优化 + 工具集成(1 天)
|
||||
|
||||
| 任务 | 工作量 | 说明 |
|
||||
|------|--------|------|
|
||||
| 改造 `QcReportService` | 0.5 天 | 从状态表直查替代日志聚合 |
|
||||
| 改造 `ToolsService` | 0.5 天 | `read_report` 和 `check_quality` 适配新结构 |
|
||||
|
||||
### Phase 4:实时质控激活 + 验证(1 天)
|
||||
|
||||
| 任务 | 工作量 | 说明 |
|
||||
|------|--------|------|
|
||||
| 改造 Webhook Worker | 0.5 天 | 处理逻辑接入 `QcExecutor` |
|
||||
| 端到端验证脚本 | 0.5 天 | 覆盖三种触发模式 + 三级数据一致性验证 |
|
||||
|
||||
**总计:约 5 天**
|
||||
|
||||
---
|
||||
|
||||
## 10. 验收标准
|
||||
|
||||
| 验收项 | 标准 |
|
||||
|--------|------|
|
||||
| 三级数据一致性 | 执行质控后,`qc_field_status` → `qc_event_status` → `record_summary` 的聚合结果一致 |
|
||||
| 变量级查询 | 能直接查询"3 号受试者在基线期的 age 字段质控状态" |
|
||||
| 事件级查询 | 能直接查询"3 号受试者基线期的整体质控状态" |
|
||||
| 定时质控配置 | 不同项目可配置不同的 Cron 策略,互不影响 |
|
||||
| 实时质控 | REDCap 保存表单后,30 秒内三级状态表更新 |
|
||||
| 报告准确性 | 报告中的通过率与 `record_summary` 中的统计一致 |
|
||||
| 历史追溯 | `qc_logs` 保留完整历史,可追溯任意时间点的质控结果 |
|
||||
| 性能 | 报告生成时间 < 2 秒(100 条记录、500 个变量规模) |
|
||||
|
||||
---
|
||||
|
||||
## 11. 关键决策记录
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 变量级状态是否独立建表 | 是,`qc_field_status` | 支持精准查询、避免 JSON 解析、支持索引 |
|
||||
| 事件级状态是否独立建表 | 是,`qc_event_status` | 消除日志聚合的复杂性和出错风险 |
|
||||
| `qc_logs` 是否保留 | 保留,追加型不变 | 审计追踪和趋势分析不可或缺 |
|
||||
| 聚合方向 | 自底向上(变量→事件→记录→项目) | 与 REDCap 数据结构一致,逻辑清晰 |
|
||||
| SKIPPED 变量处理 | 不写入 `qc_field_status` | 减少无意义数据,简化聚合逻辑 |
|
||||
| Cron 配置粒度 | 项目级 | 不同项目节奏不同(I 期 vs III 期) |
|
||||
| 前端 Cron 配置 | 可视化优先 + 高级模式 | 临床团队无需学习 Cron 语法 |
|
||||
| 实时质控防抖 | 5 分钟(pg-boss singleton) | 避免快速连续保存时重复执行 |
|
||||
| `QcExecutor` 统一入口 | 三种触发模式共用一个执行器 | 确保三级数据更新逻辑一致 |
|
||||
|
||||
---
|
||||
|
||||
> **一句话总结**:研究方案定义了"查什么",变量清单定义了"查哪些字段",质控规则定义了"怎么查",三级数据结构记录了"查的结果",质控报告汇总了"结论是什么"——这条链路从头到尾 1:1 对齐,是 CRA Agent 准确运行的根基。
|
||||
@@ -0,0 +1,357 @@
|
||||
# CRA 智能质控 Agent — 四大工具工作原理说明
|
||||
|
||||
> **文档用途**:面向临床专家、PI(主要研究者)及项目管理人员,说明 CRA 智能质控 Agent 背后的四个核心工具是如何工作的。
|
||||
> **最后更新**:2026-02-25
|
||||
|
||||
---
|
||||
|
||||
## 一、整体工作方式
|
||||
|
||||
CRA 智能质控 Agent 是一个基于大语言模型(LLM)的 AI 助手,专门用于 IIT 临床研究项目的质量监控。当用户在对话框中提问时,AI **不是凭空回答**,而是通过调用 4 个专用工具获取真实数据后,再基于数据生成回答。
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
用户提问(如:"当前通过率是多少?")
|
||||
↓
|
||||
AI 分析问题,自动选择合适的工具
|
||||
↓
|
||||
工具执行,获取真实数据
|
||||
↓
|
||||
AI 基于工具返回的数据,生成自然语言回答
|
||||
↓
|
||||
用户看到回答(如:"当前整体通过率为 92.9%,14 条记录中 13 条通过...")
|
||||
```
|
||||
|
||||
### 核心原则
|
||||
|
||||
- **所有回答必须基于真实数据**,AI 不会编造临床数据
|
||||
- **只读不写**:AI 不会修改任何临床数据,如果被要求修改数据会拒绝
|
||||
- **优先使用报告**:80% 的问题通过预计算的质控报告直接回答,保证速度和一致性
|
||||
|
||||
---
|
||||
|
||||
## 二、四个工具详解
|
||||
|
||||
### 工具 1:`read_report` — 质控报告查阅
|
||||
|
||||
**一句话说明**:查阅预先生成好的质控报告,是最常用的工具。
|
||||
|
||||
#### 什么时候使用?
|
||||
|
||||
用户问以下类型的问题时,AI 会调用此工具:
|
||||
- "目前通过率是多少?"
|
||||
- "有哪些严重问题?"
|
||||
- "3 号受试者有什么质控问题?"
|
||||
- "哪个表单通过率最低?"
|
||||
- "最近质控发现了什么趋势?"
|
||||
|
||||
#### 报告包含哪些内容?
|
||||
|
||||
| 报告章节 | 包含信息 |
|
||||
|---------|---------|
|
||||
| **概览(summary)** | 总记录数、已完成记录数、通过率、严重问题数、警告数、最后质控时间 |
|
||||
| **严重问题(critical_issues)** | 每个有问题的受试者具体违反了哪条规则、实际值是什么、标准应该是什么 |
|
||||
| **警告(warning_issues)** | 非致命性的数据异常提醒 |
|
||||
| **表单统计(form_stats)** | 每张 CRF 表单的通过率 |
|
||||
| **问题排名(trend)** | 哪些规则被违反最多,影响了多少人 |
|
||||
|
||||
#### 报告示例
|
||||
|
||||
以下是 AI 实际读取的报告格式(修复后的真实报告):
|
||||
|
||||
```xml
|
||||
<qc_context project_id="xxx" project_name="test0207" generated="2026-02-25T...">
|
||||
|
||||
<summary>
|
||||
- 状态: 1/14 记录存在严重违规 (7% Fail)
|
||||
- 通过率: 92.9%
|
||||
- 严重问题: 1 | 警告: 0
|
||||
- Top 1 问题:
|
||||
1. 排除标准检查 (1人)
|
||||
</summary>
|
||||
|
||||
<critical_issues record_count="1" issue_count="1">
|
||||
<record id="3">
|
||||
**严重违规 (1项)**:
|
||||
1. [exc_001] **排除标准检查**: 当前值 **1** (标准: 应为0)
|
||||
</record>
|
||||
</critical_issues>
|
||||
|
||||
</qc_context>
|
||||
```
|
||||
|
||||
#### 报告数据从哪里来?
|
||||
|
||||
```
|
||||
质控报告 ← 数据库中的质控日志(qc_logs 表)
|
||||
↓
|
||||
每个受试者 × 每个访视事件,取最新一条质控记录
|
||||
↓
|
||||
按受试者+规则去重,避免重复计数
|
||||
↓
|
||||
通过率 = 按受试者级别计算(每个受试者取所有访视中最严重的状态)
|
||||
```
|
||||
|
||||
#### 缓存机制
|
||||
|
||||
报告生成后会缓存 **24 小时**。在缓存有效期内,多次提问不会重复计算,保证了响应速度。执行"一键全量质控"后会自动刷新报告缓存。
|
||||
|
||||
#### 常见问题
|
||||
|
||||
**Q:报告里只有质控问题,不包含录入的原始数据吗?**
|
||||
A:对。`read_report` 只包含质控结论(通过/不通过、违规详情)。如果要看具体患者的原始录入数据(如年龄、实验室指标),AI 会自动调用下面的 `look_up_data` 工具。
|
||||
|
||||
---
|
||||
|
||||
### 工具 2:`look_up_data` — 查询原始临床数据
|
||||
|
||||
**一句话说明**:从 REDCap 系统实时拉取患者的原始录入数据。
|
||||
|
||||
#### 什么时候使用?
|
||||
|
||||
用户问以下类型的问题时,AI 会调用此工具:
|
||||
- "3 号受试者的年龄是多少?"
|
||||
- "帮我查一下 5 号患者的实验室检查结果"
|
||||
- "这个患者的知情同意日期是什么时候?"
|
||||
|
||||
#### 工作流程
|
||||
|
||||
```
|
||||
AI 调用 look_up_data(受试者编号=3, 查询字段=[年龄, 性别])
|
||||
↓
|
||||
系统将中文字段名翻译为 REDCap 内部字段名(如:年龄 → age)
|
||||
↓
|
||||
通过 REDCap API 实时查询该患者数据
|
||||
↓
|
||||
如果是纵向研究(多次访视),自动合并所有访视的数据
|
||||
↓
|
||||
返回给 AI:{ record_id: "3", age: "28", gender: "1" }
|
||||
↓
|
||||
AI 组织成自然语言回答用户
|
||||
```
|
||||
|
||||
#### 关键特性
|
||||
|
||||
- **实时查询**:每次调用都从 REDCap 获取最新数据,不使用缓存
|
||||
- **支持中文查询**:用户可以说"帮我查年龄",系统会自动翻译为 REDCap 字段名 `age`
|
||||
- **纵向数据自动合并**:同一患者在不同访视(筛选期、基线、随访等)中录入的数据会自动合并为一条完整记录
|
||||
- **只读操作**:只查询数据,绝不修改
|
||||
|
||||
#### 数据来源
|
||||
|
||||
直接通过 REDCap REST API 从 REDCap 数据库实时获取,数据路径为:
|
||||
|
||||
```
|
||||
AI → REDCap API(http://REDCap服务器/api/)→ REDCap 数据库 → 返回记录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工具 3:`check_quality` — 即时质控检查
|
||||
|
||||
**一句话说明**:对患者数据立即执行质控规则检查,可以检查单个患者或全部患者。
|
||||
|
||||
#### 什么时候使用?
|
||||
|
||||
用户**明确要求重新检查**时,AI 才会调用此工具:
|
||||
- "帮我重新检查一下 3 号受试者"
|
||||
- "现在执行一次全量质控"
|
||||
- "数据刚更新了,帮我跑一下质控"
|
||||
|
||||
> **注意**:如果用户只是问"通过率是多少"这样的查询性问题,AI 会使用 `read_report` 而不是 `check_quality`,因为报告中已有预计算的结果。
|
||||
|
||||
#### 两种检查模式
|
||||
|
||||
##### 模式 A:单患者检查
|
||||
|
||||
```
|
||||
AI 调用 check_quality(record_id="3")
|
||||
↓
|
||||
从 REDCap 拉取 3 号患者的最新数据
|
||||
↓
|
||||
加载项目配置的所有质控规则(如:年龄范围检查、排除标准检查等)
|
||||
↓
|
||||
逐条规则执行:
|
||||
- 字段缺失?→ 跳过该规则(不算失败)
|
||||
- 字段有值?→ 用 JSON Logic 规则引擎判定通过/失败
|
||||
↓
|
||||
返回结果:
|
||||
整体状态: FAIL
|
||||
通过 8 / 9 条规则
|
||||
违规: 排除标准检查 — 当前值为 1(应为 0)
|
||||
```
|
||||
|
||||
##### 模式 B:全量批量检查
|
||||
|
||||
```
|
||||
AI 调用 check_quality()(不传受试者编号)
|
||||
↓
|
||||
从 REDCap 拉取所有患者的所有访视数据
|
||||
↓
|
||||
基线数据合并:将基线访视的数据(如年龄、性别)合并到后续访视中
|
||||
↓
|
||||
逐个 受试者 × 访视 执行规则引擎
|
||||
↓
|
||||
结果写入数据库(追加新记录,不覆盖历史)
|
||||
↓
|
||||
返回汇总:
|
||||
总计: 42 条检查
|
||||
通过: 39 条
|
||||
未通过: 3 条
|
||||
通过率: 92.9%
|
||||
问题受试者: [3号 - 排除标准检查违规]
|
||||
```
|
||||
|
||||
#### 质控规则引擎说明
|
||||
|
||||
系统使用 **JSON Logic 规则引擎** 执行质控检查。目前配置的规则包括:
|
||||
|
||||
| 规则类别 | 示例规则 | 说明 |
|
||||
|---------|---------|------|
|
||||
| **纳入标准** | 年龄范围检查(16-35岁) | 不在范围内则标记为严重问题 |
|
||||
| **纳入标准** | 知情同意检查 | 必须已签署知情同意 |
|
||||
| **排除标准** | 排除标准符合性检查 | 符合任何排除标准则标记为严重问题 |
|
||||
|
||||
**缺失数据处理策略**(V3.2 修复后):
|
||||
- 如果某个字段在当前访视中没有录入(如随访期没有"年龄"字段),该规则**自动跳过**,不算失败
|
||||
- 只有字段有值但不符合规则时,才判定为失败
|
||||
|
||||
---
|
||||
|
||||
### 工具 4:`search_knowledge` — 知识库检索
|
||||
|
||||
**一句话说明**:在项目文档(研究方案、CRF、伦理批件等)中搜索信息。
|
||||
|
||||
#### 什么时候使用?
|
||||
|
||||
用户问以下类型的问题时,AI 会调用此工具:
|
||||
- "纳入标准是什么?"
|
||||
- "研究方案中对随访间隔是怎么规定的?"
|
||||
- "主要疗效指标是什么?"
|
||||
- "知情同意的要求有哪些?"
|
||||
|
||||
#### 工作流程
|
||||
|
||||
```
|
||||
AI 调用 search_knowledge(query="纳入标准是什么")
|
||||
↓
|
||||
查找该项目关联的知识库
|
||||
↓
|
||||
将用户问题转化为向量(语义表示)
|
||||
↓
|
||||
在知识库中进行语义相似度搜索(不是简单的关键词匹配)
|
||||
↓
|
||||
返回最相关的 5 个文档片段:
|
||||
1. 研究方案V2.0.pdf(相关度 87.3%)
|
||||
内容:"纳入标准:1. 年龄16-35岁 2. 确诊为..."
|
||||
2. ICF知情同意书.pdf(相关度 72.1%)
|
||||
内容:"..."
|
||||
↓
|
||||
AI 综合这些文档片段,生成回答
|
||||
```
|
||||
|
||||
#### 关键特性
|
||||
|
||||
- **语义搜索**:不是简单的关键词匹配,而是理解问题含义后搜索。比如问"入组条件"也能找到"纳入标准"的内容
|
||||
- **文档来源标注**:每个搜索结果都标注来自哪个文档,方便追溯
|
||||
- **相关度评分**:只返回相关度 ≥ 30% 的结果,过滤无关内容
|
||||
- **支持多种文档**:研究方案、CRF 说明、伦理批件、操作手册等上传到知识库的文档均可搜索
|
||||
|
||||
#### 知识库管理
|
||||
|
||||
知识库中的文档需要通过项目管理界面上传。上传后,系统会自动:
|
||||
1. 解析文档内容(支持 PDF、Word 等格式)
|
||||
2. 将文档分块(每块约 500-1000 字)
|
||||
3. 为每个文档块生成向量嵌入(用于语义搜索)
|
||||
4. 存入向量数据库
|
||||
|
||||
---
|
||||
|
||||
## 三、工具协作示例
|
||||
|
||||
以下是一个典型的多工具协作场景:
|
||||
|
||||
### 场景:用户问"3 号受试者有什么问题?详细说明一下"
|
||||
|
||||
```
|
||||
第 1 轮:AI 调用 read_report(section="critical_issues", record_id="3")
|
||||
→ 获取质控报告中 3 号受试者的问题列表
|
||||
→ 发现:排除标准检查失败,实际值为 1
|
||||
|
||||
第 2 轮:AI 调用 look_up_data(record_id="3", fields=["exclusion_criteria"])
|
||||
→ 从 REDCap 获取原始数据确认
|
||||
→ 确认 exclusion_criteria 字段值确实为 1
|
||||
|
||||
AI 最终回答:
|
||||
"3 号受试者存在 1 项严重违规:排除标准检查未通过。
|
||||
该受试者的排除标准字段值为 1(应为 0,即不符合任何排除标准)。
|
||||
建议核实该受试者是否确实符合入组条件。"
|
||||
```
|
||||
|
||||
### 场景:用户问"纳入标准中年龄范围是多少?有没有超龄的患者?"
|
||||
|
||||
```
|
||||
第 1 轮:AI 调用 search_knowledge(query="纳入标准年龄范围")
|
||||
→ 从研究方案中找到:"纳入标准第1条:年龄16-35岁"
|
||||
|
||||
第 2 轮:AI 调用 read_report(section="critical_issues")
|
||||
→ 检查是否有年龄相关的质控问题
|
||||
→ 当前无年龄违规记录
|
||||
|
||||
AI 最终回答:
|
||||
"根据研究方案,纳入标准规定年龄范围为 16-35 岁。
|
||||
当前质控报告显示,所有已录入受试者的年龄均在合规范围内,
|
||||
未发现超龄问题。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、总结对比
|
||||
|
||||
| 工具 | 核心用途 | 数据来源 | 实时性 | 使用频率 |
|
||||
|------|---------|---------|--------|---------|
|
||||
| **read_report** | 查阅质控报告 | 数据库(预计算报告) | 缓存 24 小时 | ~80% |
|
||||
| **look_up_data** | 查询原始数据 | REDCap 实时 API | 实时 | ~10% |
|
||||
| **check_quality** | 执行质控检查 | REDCap + 规则引擎 | 实时执行 | ~5% |
|
||||
| **search_knowledge** | 搜索文档知识 | 项目知识库(向量搜索) | 准实时 | ~5% |
|
||||
|
||||
### 设计理念
|
||||
|
||||
**报告优先,工具兜底**(Report-first, Tools-fallback)
|
||||
|
||||
- 绝大多数质控问题通过预计算的报告直接回答,保证响应速度(秒级)
|
||||
- 只在需要查看具体原始数据或重新执行质控时,才调用其他工具
|
||||
- AI 会根据问题类型自动选择最合适的工具,无需用户干预
|
||||
|
||||
---
|
||||
|
||||
## 附录:技术架构简图
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 用户对话界面 │
|
||||
│ (Web / 微信公众号) │
|
||||
└──────────────────────┬───────────────────────────────────────────┘
|
||||
│ 用户提问
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ ChatOrchestrator(对话编排器) │
|
||||
│ │
|
||||
│ System Prompt(角色定义 + 工具选择策略) │
|
||||
│ ↓ │
|
||||
│ LLM(DeepSeek-V3)← Function Calling 循环(最多 3 轮) │
|
||||
│ ↓ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌───────────┐ │
|
||||
│ │ read_report │ │look_up_data │ │check_quality │ │search_ │ │
|
||||
│ │ │ │ │ │ │ │knowledge │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬───────┘ └─────┬─────┘ │
|
||||
└─────────┼───────────────┼───────────────┼───────────────┼────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
|
||||
│ QcReport │ │ REDCap │ │ REDCap │ │ pgvector │
|
||||
│ Service │ │ REST API │ │ REST API │ │ 向量搜索 │
|
||||
│ (数据库) │ │ (实时) │ │ + 规则引擎 │ │ (知识库) │
|
||||
└────────────┘ └────────────┘ └────────────┘ └────────────┘
|
||||
```
|
||||
198
docs/03-业务模块/IIT Manager Agent/09-技术评审报告/CRA AI 替代工作梳理2.md
Normal file
198
docs/03-业务模块/IIT Manager Agent/09-技术评审报告/CRA AI 替代工作梳理2.md
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,106 @@
|
||||
# **CRA 质控报告自动化生成与 LLM 友好型设计规范**
|
||||
|
||||
**文档版本**:V2.1 (5 层架构剪枝与 LLM 实战版)
|
||||
|
||||
**设计目标**:利用“五层数据坐标剪枝”与“语义组装”,形成对大模型(LLM)极度友好、绝不产生幻觉的 Payload。
|
||||
|
||||
## **核心设计原则:LLM 友好性 (LLM Readiness)**
|
||||
|
||||
要让 LLM 稳定输出,必须遵循“三不”原则:
|
||||
|
||||
1. **不喂全量数据**:依托 5 层架构执行 JSON 树剪枝,只提取 status \= FAIL 的切片。
|
||||
2. **不喂物理字段**:所有的 lb\_alt\_v 必须经过 AutoMapper 映射为 谷丙转氨酶(ALT)。
|
||||
3. **不让 LLM 算数**:日期差、缺失率由 Node.js 算好,直接把“结论标签”喂给 LLM。
|
||||
|
||||
## **报告生成链路与 LLM 模拟实战 (基于 5 层坐标)**
|
||||
|
||||
### **场景一:数据质疑管理 (eQuery Log) \- D3 规则**
|
||||
|
||||
**【业务场景】** D3 逻辑发现异常,生成一条像资深 CRA 写的质疑描述。
|
||||
|
||||
**【剪枝后的 5层 LLM 友好输入片段 ✅】**
|
||||
|
||||
{
|
||||
"system\_instruction": "你是一个严谨的临床数据经理。请根据下方异常数据,生成一条专业的电子质疑(eQuery)。指出矛盾点并给建议,100字以内。",
|
||||
"clinical\_context": {
|
||||
"受试者ID": "P005",
|
||||
"访视阶段": "随访2 (Week 4)",
|
||||
"触发表单": "实验室检查",
|
||||
"数据实例": "Instance 1"
|
||||
},
|
||||
"anomaly\_data": {
|
||||
"异常指标": "谷丙转氨酶 (ALT)",
|
||||
"当前录入值": "150 U/L",
|
||||
"HardRuleEngine\_计算结论": "超出正常上限 (50 U/L) 的 3 倍",
|
||||
"跨表核查结果": "查询《不良事件(AE)表》所有 Instance,当前记录数为 0"
|
||||
}
|
||||
}
|
||||
|
||||
**【LLM 输出示例】**
|
||||
|
||||
“检测到该受试者在「随访2」的「实验室检查」中,谷丙转氨酶(ALT)为 150 U/L,已超出正常上限 3 倍。经跨表核查,未发现对应的《不良事件(AE)》记录。请核实检验结果是否录入无误;若无误,请评估是否需要补充填报 AE。”
|
||||
|
||||
### **场景二:不良事件风险评估 (AE Log) \- D5 规则**
|
||||
|
||||
**【业务场景】** 提取录入的 AE,结合患者的其他重复表单(如多次服药),提示风险。
|
||||
|
||||
**【应对重复表单 (Repeating Forms) 的完美 JSON 结构 ✅】**
|
||||
|
||||
系统精确拉取 AE\_Log 和 ConMed\_Log 下的不同 InstanceID:
|
||||
|
||||
{
|
||||
"system\_instruction": "作为临床安全评估助手,请分析患者新发的 AE,结合既往病史和用药,提示潜在风险关联。你无权下定论,仅供 PI 参考。",
|
||||
"patient\_context": {
|
||||
"受试者": "P012",
|
||||
"合并用药表 (ConMed\_Log)": \[
|
||||
{"instance\_id": 1, "药物名称": "阿司匹林"},
|
||||
{"instance\_id": 2, "药物名称": "布洛芬"}
|
||||
\]
|
||||
},
|
||||
"不良事件表 (AE\_Log)": \[
|
||||
{"instance\_id": 3, "事件名称": "消化道出血", "严重程度": "CTCAE 3级"}
|
||||
\],
|
||||
"rag\_knowledge\_retrieval": "方案规定:非甾体抗炎药(NSAIDs)如布洛芬、阿司匹林联用会显著增加消化道出血风险。"
|
||||
}
|
||||
|
||||
**【LLM 输出示例】**
|
||||
|
||||
“该受试者新发 3 级「消化道出血」(Instance 3)。系统监测到患者正在同时服用「阿司匹林」(Instance 1)与「布洛芬」(Instance 2)。结合方案提示,非甾体抗炎药联用是高危出血因素。建议 PI 重点关注该 AE 是否与合并用药具有因果关系。”
|
||||
|
||||
### **场景三:方案偏离记录 (PD Log) \- D6 规则**
|
||||
|
||||
**【喂给 LLM 的切片 ✅】**
|
||||
|
||||
{
|
||||
"system\_instruction": "请根据硬逻辑引擎计算出的偏差事实,撰写连贯的方案偏离描述,并提出纠正预防措施(CAPA)。",
|
||||
"deviation\_facts": {
|
||||
"坐标": "Record:P002 \-\> Event:随访3 \-\> Form:访视表 \-\> Instance:1",
|
||||
"偏离类型": "访视超窗",
|
||||
"HardRuleEngine\_计算依据": {
|
||||
"方案允许窗口": "2026-02-15 ±3天",
|
||||
"实际发生日期": "2026-02-25",
|
||||
"计算结果": "延误 10 天"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
### **场景四:数据缺失率总结 (Missing Rate Log) \- D2 规则**
|
||||
|
||||
**【特殊说明:此报告尽量不用 LLM】**
|
||||
|
||||
数据缺失率表严禁使用 LLM 计算百分比!完全由 Node.js CompletenessEngine 根据 Instance 和分支逻辑动态计算完成。LLM 仅根据最终聚合数值生成执行摘要。
|
||||
|
||||
**【喂给 LLM 的切片 ✅】**
|
||||
|
||||
{
|
||||
"report\_type": "数据完整性摘要",
|
||||
"total\_missing\_rate": "5.0%",
|
||||
"top\_missing\_forms": \[
|
||||
{"form": "SF-36 生活质量问卷", "missing\_rate": "40%"}
|
||||
\],
|
||||
"task": "基于以上统计数据,写两句话的执行摘要。"
|
||||
}
|
||||
|
||||
**技术收益总结**:
|
||||
|
||||
五层架构(特别是 InstanceID 实例层)的引入,让发给 LLM 的 JSON 数据结构具备了完美的数组映射能力,彻底消灭了多行数据相互覆盖的 Bug,并最大程度防范了 AI 幻觉!
|
||||
99
docs/03-业务模块/IIT Manager Agent/09-技术评审报告/Skill化配置架构技术设计.md
Normal file
99
docs/03-业务模块/IIT Manager Agent/09-技术评审报告/Skill化配置架构技术设计.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# **Skill 化配置架构与业务流转技术设计文档**
|
||||
|
||||
**文档版本**:V2.1 (5层架构与语义解耦全景版)
|
||||
|
||||
**核心理念**:规则必须脱离底层代码;质控逻辑必须基于“医学语义”而非“物理字段”进行配置,以适应不同的 IIT 项目。
|
||||
|
||||
## **第一章 架构总览:从“硬编码”到“Skill 驱动”**
|
||||
|
||||
我们将数据、规则与报告的生成链路设计为高度解耦的三段式架构:
|
||||
|
||||
\[阶段一:基建与抽象\] \[阶段二:Skill 编排配置\] \[阶段三:执行与映射\]
|
||||
(项目前置准备) (DM 规则配置) (引擎自动计算)
|
||||
|
||||
1\. REDCap 字典同步 1\. 选择 Skill 模板 1\. 监听数据变化 (Webhook)
|
||||
2\. 知识库向量化 (RAG) \-\> 2\. 绑定语义标签 \-\> 2\. 提取 5 层级联坐标
|
||||
3\. LLM 语义映射对齐 3\. 设定阈值与报警级别 3\. 引擎执行逻辑 (硬/软)
|
||||
(AutoMapper) (赋能 D1-D7 维度) 4\. 结果落入 5层数据底座
|
||||
5\. 自动组装 5大 GCP 报告
|
||||
|
||||
## **第二章 配置前置准备 (Project Initialization)**
|
||||
|
||||
在配置具体的质控 Skill 前,系统必须完成项目的“数字孪生”构建:
|
||||
|
||||
### **2.1 结构化数据基建:REDCap 元数据同步**
|
||||
|
||||
* **动作**:调用 exportMetadata 获取字典,包含分支逻辑(Branching Logic)。这为 D2 (缺失率) 计算打下基石。
|
||||
|
||||
### **2.2 非结构化知识基建:建立专属 RAG 大脑**
|
||||
|
||||
* **动作**:上传方案 Protocol、ICF,进行向量化。为 D5/D6 的 SoftRule 模糊推理提供依据。
|
||||
|
||||
### **2.3 核心解耦:AutoMapper 语义映射 (重中之重)**
|
||||
|
||||
* **动作**:利用 LLM,将 REDCap 毫无规律的物理变量名(如 ie\_01, ae\_term),映射为**系统内置标准医学语义标签**(如 \[入选标准\_年龄\], \[不良事件\_名称\])。
|
||||
* **价值**:DM 面向通用语义配置规则,无需关心物理表名,实现通用 Skill 的跨项目复用。
|
||||
|
||||
## **第三章 Skill 核心数据模型设计 (The Skill Schema)**
|
||||
|
||||
### **3.1 数据库表结构 (iit\_skills)**
|
||||
|
||||
model IitSkill {
|
||||
id String @id @default(cuid())
|
||||
projectId String // 绑定到具体项目
|
||||
name String // e.g., "ALT 重度异常监测"
|
||||
|
||||
category String // 关键!挂载到 7 大维度: D1, D2, D3, D5, D6
|
||||
// 质控深度,全面升级支持 5 层架构
|
||||
targetLevel String // RECORD, EVENT, FORM, INSTANCE, FIELD
|
||||
|
||||
engineType String // HARD\_RULE (JSON Logic) 或 SOFT\_RULE (LLM)
|
||||
triggerType String // WEBHOOK (实时) 或 CRON (定时)
|
||||
|
||||
configuration Json // JSON 配置体
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
### **3.2 Configuration 字段设计 (配置解析)**
|
||||
|
||||
**A. HardRule (硬规则 \- 适用于 D1, D3, D7)**
|
||||
|
||||
采用 JSON Logic 语法,实现毫秒级绝对判定。
|
||||
|
||||
{
|
||||
"semantic\_tags": \["实验室\_谷丙转氨酶"\],
|
||||
"logic": {
|
||||
"\>": \[{"var": "实验室\_谷丙转氨酶"}, 150\]
|
||||
},
|
||||
"on\_fail\_message": "谷丙转氨酶(ALT)达到重度异常标准,触发安全预警。",
|
||||
"severity": "CRITICAL"
|
||||
}
|
||||
|
||||
**B. SoftRule (软规则 \- 适用于 D5, D6)**
|
||||
|
||||
采用 Prompt 模板,供 LLM 结合 RAG 在各个 Instance 间进行推理。
|
||||
|
||||
{
|
||||
"semantic\_tags": \["不良事件\_症状描述", "合并用药\_药物名称"\],
|
||||
"prompt\_template": "患者主诉发生了 {不良事件\_症状描述},请检索知识库禁忌药目录,评估这是否与患者填报的第 {InstanceID} 行用药 {合并用药\_药物名称} 存在禁忌冲突?",
|
||||
"rag\_enabled": true,
|
||||
"severity": "WARNING"
|
||||
}
|
||||
|
||||
## **第四章 配置工作流与执行链路 (How it works in action)**
|
||||
|
||||
### **4.1 DM 配置工作流 (UI 交互层)**
|
||||
|
||||
1. **选择维度**:DM 选择配置【D1 入排标准】。
|
||||
2. **选择模板**:选择“数值范围检验”。
|
||||
3. **绑定语义**:下拉选择 \[入选标准\_年龄\]。
|
||||
4. **设置条件**:\>= 18 且 \<= 75。保存发布。
|
||||
|
||||
### **4.2 引擎自动化执行链路 (Execution Pipeline)**
|
||||
|
||||
1. **触发与拦截**:REDCap Webhook 抵达。
|
||||
2. **提取五层坐标**:解析出 Record:P005 \-\> Event:V2 \-\> Form:AE\_Log \-\> Instance:2 \-\> Field:ae\_term。
|
||||
3. **加载 Skills**:查询匹配的 active Skills。
|
||||
4. **装填数据**:按 AutoMapper 映射装填数值。
|
||||
5. **引擎计算**:调起 HardRuleEngine 或 SoftRuleEngine。
|
||||
6. **状态回写**:若失败,将原因写入 qc\_field\_status,携带 Category (如 D5),红灯沿五层坐标向上一路冒泡至 Record 级。
|
||||
99
docs/03-业务模块/IIT Manager Agent/09-技术评审报告/五层数据架构方案评审反馈.md
Normal file
99
docs/03-业务模块/IIT Manager Agent/09-技术评审报告/五层数据架构方案评审反馈.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 五层数据架构方案评审反馈
|
||||
|
||||
> **评审对象**:架构团队提交的 4 份技术文档
|
||||
> 1. 核心数据架构与业务落地白皮书
|
||||
> 2. 核心转换机制白皮书:数据·规则·报告链路设计
|
||||
> 3. Skill 化配置架构技术设计
|
||||
> 4. CRA 质控报告自动化生成与 LLM 友好型设计规范
|
||||
>
|
||||
> **评审日期**:2026-03-01
|
||||
> **评审结论**:方向认可,分层落地
|
||||
|
||||
---
|
||||
|
||||
## 一、总体评价
|
||||
|
||||
方案方向正确、视野开阔,5 层数据架构和 7 维度报告体系在理论上完整且符合 CDISC 标准。但它是一份**理想态蓝图**,与系统实际现状有较大距离,需要区分"方向性采纳"和"现阶段过度设计"。
|
||||
|
||||
---
|
||||
|
||||
## 二、值得借鉴的建议(采纳)
|
||||
|
||||
| # | 建议 | 采纳理由 | 系统现状差距 |
|
||||
|---|------|---------|-------------|
|
||||
| 1 | **InstanceID 实例层(第 4 层)** | AE、合并用药等重复表单必须精确到"第几条"才能定位问题,这是架构方案中**最有价值的贡献** | `IitQcLog`、`IitEquery` 均缺少 `instanceId` 字段 |
|
||||
| 2 | **规则按 D1-D7 维度分类** | 多维报告的前提,也是上轮内部讨论的共识 | 当前 `QCRule.category` 仅 4 种值,未对齐 D1-D7 |
|
||||
| 3 | **状态冒泡机制** | Field → Instance → Form → Event → Record 的级联逻辑清晰,便于前端热力图展示和报告聚合 | 仅有记录级 `IitRecordSummary`,无独立的事件级/变量级状态表 |
|
||||
| 4 | **LLM 三不原则** | "不喂全量、不喂物理字段、不让 LLM 算数"与我们已有实践高度一致,值得正式化为设计规范 | 已有剪枝和预计算,但字段语义化尚未实现 |
|
||||
| 5 | **AutoMapper 语义映射** | REDCap 字段名(如 `ie_01`、`ae_term`)对 LLM 不友好,反向语义化是必要的 | `IitFieldMapping` 表已有 alias→actual 映射,缺 actual→semantic 方向 |
|
||||
| 6 | **优先级调整:D2 提至 P0** | IIT 中数据缺失率是最普遍的问题,D2 重要性应高于 D6/D7 | 我们原规划将 D2 放在阶段二,可考虑提前 |
|
||||
|
||||
---
|
||||
|
||||
## 三、暂不采纳的建议
|
||||
|
||||
| # | 建议 | 暂不采纳的理由 |
|
||||
|---|------|---------------|
|
||||
| 1 | **D8 核心数据 SDV(AI 多模态视觉比对)** | 这是一个独立产品功能(前端上传 + OCR + 比对),不是质控引擎的架构问题。技术难度极高,且 IIT 场景 SDV 比率不高,应单独立项 |
|
||||
| 2 | **AutoMapper 全自动 LLM 映射** | 用 LLM 自动将物理变量名映射为医学语义标签,准确率不可控。建议改为**半自动**:系统基于 Data Dictionary 的 `field_label` 提供建议,DM 人工确认 |
|
||||
| 3 | **5 张 GCP 报表同时规划** | 系统连 D2(缺失率)都还未实现,一次性规划 5 张完整 GCP 报表脱离现实。数据底座做好后,报告生成是顺水推舟的事 |
|
||||
| 4 | **Branching Logic 解析器(阶段一)** | REDCap 分支逻辑语法是非标准格式,解析器开发成本高。阶段一先用简单统计替代,分支逻辑解析推迟到 D2 专项实现时 |
|
||||
|
||||
---
|
||||
|
||||
## 四、落地策略:分 3 个批次
|
||||
|
||||
**不建议一次性修复。** 原因:改动范围涉及 schema + 引擎 + 报告服务 + 前端,回归测试压力大;D5/D6/D7 的规则尚无临床专家输入,空设架构无意义。
|
||||
|
||||
### 批次 A:数据底座加固(1-2 周)
|
||||
|
||||
让现有功能的数据粒度正确。
|
||||
|
||||
- `IitQcLog` + `IitEquery` 增加 `instanceId` 字段
|
||||
- `QCRule.category` 扩展为 D1-D7 维度枚举
|
||||
- `IitFieldMapping` 增加 `semanticLabel`(反向语义化)
|
||||
- 新建 `qc_field_status` 表(5 层坐标 + category + status)
|
||||
- `HardRuleEngine` 执行结果写入 `qc_field_status`
|
||||
- `QcReportService` 基于 `qc_field_status` 生成语义化报告
|
||||
|
||||
**验收标准**:一键全量质控后 `qc_field_status` 有正确的 5 层坐标数据;LLM 报告中字段名为中文语义。
|
||||
|
||||
### 批次 B:聚合层与冒泡机制(1-2 周)
|
||||
|
||||
完成五级聚合,支撑多维报告和前端热力图。
|
||||
|
||||
- 新建 `qc_event_status` 表(由 `field_status` 聚合)
|
||||
- 改造 `IitRecordSummary`(由 `event_status` 聚合)
|
||||
- 实现状态冒泡逻辑(field → instance → form → event → record)
|
||||
- 多维报告框架(按 D1-D7 分章节)
|
||||
- 前端:受试者×表单热力图原型
|
||||
|
||||
**验收标准**:字段 FAIL 后对应 event 和 record 自动变红;报告按维度分章节。
|
||||
|
||||
### 批次 C:新维度引擎(按需)
|
||||
|
||||
扩展质控覆盖面,**依赖临床专家确认规则可行性**。
|
||||
|
||||
- D2 CompletenessEngine(先简单统计,后续加分支逻辑解析)
|
||||
- D6 方案偏离引擎(访视超窗检测)
|
||||
- D5 SoftRule + RAG 联动(AE 漏报侦测)
|
||||
- 项目健康度评分模型
|
||||
|
||||
**前提条件**:临床专家已确认各维度规则;有真实 IIT 项目数据验证;批次 A+B 已稳定运行。
|
||||
|
||||
---
|
||||
|
||||
## 五、架构团队二次评审补充建议(4 条,全部采纳)
|
||||
|
||||
| # | 建议 | 采纳结论 | 落地位置 |
|
||||
|---|------|---------|---------|
|
||||
| 1 | **跨表单"上下文断层"**:Webhook 只带单表单数据,跨表规则(D5 AE 漏报)无法执行 | 完全采纳。`QcExecutor.executeSingle()` 一律拉取该患者全量数据(Record-Level Context) | 开发计划 3.7 节 |
|
||||
| 2 | **eQuery 自动闭环缺失**:Field 从 FAIL 变 PASS 时无人关闭关联 eQuery | 完全采纳。新增 State Transition Hook,用 `auto_closed` 状态区分人工关闭 | 开发计划 3.8 节 |
|
||||
| 3 | **D2 缺失率"未来时空"陷阱**:新入组患者未来访视的必填字段被算作缺失 | 完全采纳。增加 Event-Aware 时序过滤,只统计已到达事件的必填字段 | 开发计划 6.3 节 |
|
||||
| 4 | **聚合防抖锁粒度**:项目级聚合在多 CRC 并发录入时有行锁竞争 | 部分采纳。实时触发用受试者级防抖,项目级统计独立低频刷新 | 开发计划 3.3 节 |
|
||||
|
||||
---
|
||||
|
||||
## 六、一句话总结
|
||||
|
||||
> 5 层数据架构方向正确,**InstanceID** 和 **rule_category** 是我们最缺的两块拼图,必须尽快补上。表结构可以一次设计到位,但填充内容要一层一层来——先底座(批次 A),再聚合(批次 B),最后扩维度(批次 C)。
|
||||
111
docs/03-业务模块/IIT Manager Agent/09-技术评审报告/核心数据架构与业务落地白皮书.md
Normal file
111
docs/03-业务模块/IIT Manager Agent/09-技术评审报告/核心数据架构与业务落地白皮书.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# **纯数字 CRA 平台:五层数据底座与核心引擎技术白皮书**
|
||||
|
||||
**文档版本**:V2.1 (CDISC ODM 国际标准 5 层全景版)
|
||||
|
||||
**核心理念**:以 CDISC ODM 国际标准为底座,彻底解决重复表单问题,打造对人类透明、符合 GCP 规范、对 LLM 友好的临床质控体系。
|
||||
|
||||
## **第一章 核心数据架构:五层状态级联树 (The 5-Layer Hierarchy)**
|
||||
|
||||
为了支撑前端的“受试者 × 表单”热力图,并彻底解决重复表单(Repeating Forms,如多次发生的 AE、多次服药记录)的数据覆盖与追踪问题,我们严格对齐 CDISC ODM 标准,采用 **5 层状态级联架构**作为系统的影子质控底座。
|
||||
|
||||
### **1.1 数据实体定义与 GPS 坐标**
|
||||
|
||||
系统中的任何一个质控状态、任何一条 eQuery,都必须唯一且绝对精确地绑定在这个五维坐标上:
|
||||
|
||||
1. **RecordID (受试者级)**:例如 P005。反映该患者的整体健康度。
|
||||
2. **EventID (访视/事件级)**:例如 Visit\_2(随访2)。反映特定时间点的数据质量。
|
||||
3. **FormID (表单级)**:例如 AE\_Log(不良事件表)。**(前端风险热力图的 X 轴核心来源)**
|
||||
4. **InstanceID (实例/行号级)**:例如 1, 2, 3。**(核心突破!用于区分同一患者填写的第 1 个 AE、第 2 个 AE。非重复表单默认值为 1)**
|
||||
5. **FieldID (字段/变量级)**:例如 ae\_name(不良事件名称)。最底层的原子数据。
|
||||
|
||||
### **1.2 状态级联与冒泡机制 (State Bubbling)**
|
||||
|
||||
当后台的 HardRuleEngine 或 SoftRuleEngine 发现数据异常时:
|
||||
|
||||
1. **精准打标**:系统首先在最底层的 qc\_field\_status 表中,将坐标 P005 \-\> Visit\_2 \-\> AE\_Log \-\> Instance\_2 \-\> ae\_name 标记为 FAIL,并生成一条 eQuery。
|
||||
2. **状态冒泡 (自动向上传递)**:
|
||||
* 包含该字段的实例 Instance\_2 (第2条不良事件) 状态变为 FAIL。
|
||||
* 包含该实例的表单 AE\_Log 的状态自动变为 FAIL(热力图该格子变红)。
|
||||
* 包含该表单的访视 Visit\_2 自动变为 FAIL。
|
||||
* 受试者 P005 的全局状态自动变为 FAIL。
|
||||
3. **闭环消警**:当 CRC 在 REDCap 修改了该实例 Instance\_2 的值,引擎复核通过,底层 Field 变绿,绿灯逐级向上点亮,直至受试者恢复健康状态。
|
||||
|
||||
## **第二章 CRA 质控工作优先级策略**
|
||||
|
||||
面对庞杂的临床规则,我们应遵循\*\*“高频易错、AI 擅长、IIT 痛点”\*\*的原则排定工作优先级:
|
||||
|
||||
### **📌 优先级一:P0 级(系统上线必须具备的核心基石)**
|
||||
|
||||
这些是 IIT 试验中最容易崩盘,且传统 CRC 最头疼的硬逻辑工作。AI 处理这些具有 100% 的准确性和碾压级效率。
|
||||
|
||||
1. **D2 数据完整性 (Completeness)**:扫描 Form、Instance 和 Field 级数据,基于 REDCap 的分支逻辑(Branching Logic)计算真实的缺失率。
|
||||
2. **D3 逻辑准确性 (Accuracy \- Edit Checks)**:时序校验(知情同意 \< 筛选 \< 首次用药)、数值极值校验、跨表单关联必填项校验。
|
||||
3. **D1 入排合规性 (Eligibility)**:对入选/排除标准表单进行强逻辑逆向校验。
|
||||
|
||||
### **📌 优先级二:P1 级(建立系统护城河的亮点功能)**
|
||||
|
||||
1. **D8 核心数据 SDV 强制核对 (AI Vision)**:针对主要研究终点(如肿瘤大小、关键生化指标),强制要求 CRC 在前端上传原始影像/化验单。AI 通过多模态视觉比对。
|
||||
2. **D5 安全性事件漏报侦测 (Safety)**:跨表单与跨实例关联。AI 发现化验单达到 3 级毒性,但 AE\_Log 表中没有任何一个 Instance 记录该事件,立刻触发告警。
|
||||
|
||||
### **📌 优先级三:P2 级(锦上添花的拓展功能)**
|
||||
|
||||
* **D6 方案偏离 (超窗检测)** 与 **D7 试验药物依从性管理**。由于 IIT 很多是非药物研究且随访不严,可作为可选模块后期加入。
|
||||
|
||||
## **第三章 以终为始:多维 CRA 质控报告体系设计**
|
||||
|
||||
结合系统化的 SaaS 管理看板与极其严谨的 GCP 规范要求,我们将最终输出的质控报告划分为两大层级:**“宏观管理驾驶舱层”与“核心业务报表层”**。
|
||||
|
||||
### **3.1 宏观管理层 (Executive Dashboard)**
|
||||
|
||||
用于系统前台实时展示,提供全局项目健康度:
|
||||
|
||||
* **项目综合健康度评分**(基于 D1\~D6 加权计算)。
|
||||
* **受试者风险热力图**(直观展示哪位受试者的哪个表单异常)。
|
||||
* **CRC 数据录入绩效**(eQuery 平均关闭时间、录入时效)。
|
||||
|
||||
### **3.2 核心业务报表层 (GCP 规范自动化日志)**
|
||||
|
||||
这是数字 CRA 真正替代人工的交付物。系统基于“五层数据底座”结合 AI 判定,**自动生成以下 5 张标准化的临床记录表**:
|
||||
|
||||
#### **表 1:筛选与入选登记表 (对应 D1)**
|
||||
|
||||
* **涵盖字段**:筛选编号、性别/年龄、诊断、纳入/排除标准是否符合(AI 判定)、筛选结果(入组/失败)、失败原因(AI 自动抓取不符条款)、电子签名。
|
||||
|
||||
#### **表 2:数据录入率 & 缺失率记录表 (对应 D2)**
|
||||
|
||||
* **涵盖字段**:表格名称、应录入总字段数(基于 Branching Logic 动态计算)、实际已录入字段数、**数据录入率 & 缺失率 %**、缺失变量与原因、审核时间戳。
|
||||
|
||||
#### **表 3:数据质疑管理跟踪表 (eQuery Log) (对应 D3/D4)**
|
||||
|
||||
* **涵盖字段**:质疑编号、5层精确坐标定位、AI 质疑描述(客观描述矛盾点)、发现日期/发现人(AI)、CRC 解释与回复、二次复核状态、关闭日期。
|
||||
|
||||
#### **表 4:AE 不良事件记录与监查表 (对应 D5 \- 核心合规文件)**
|
||||
|
||||
* **涵盖字段**:受试者信息、AE 名称/症状、起止日期、严重程度、**与干预的因果关系(AI 提示禁忌,PI 最终定性)**、处理措施与转归、SAE 时效监控、**⚠️ AI 挖掘出的疑似漏报事件高亮**。
|
||||
|
||||
#### **表 5:方案偏离记录表 (PD Log) (对应 D6)**
|
||||
|
||||
* **涵盖字段**:偏离编号、发生日期、**偏离类型(AI 自动分类:超窗/禁忌用药/未采集等)**、偏离描述、对安全性影响评估(AI 初判,人工复核)、纠正预防措施(CAPA)。
|
||||
|
||||
## **第四章 架构的 LLM 友好性设计与优化 (LLM Readiness)**
|
||||
|
||||
我们的 5 层级联树状架构,天生就是为了喂给 LLM 而设计的最佳结构。采用四大 LLM 优化策略:
|
||||
|
||||
### **优化 1:语义化映射网关 (AutoMapper) —— 解决 LLM 看不懂**
|
||||
|
||||
* **痛点**:REDCap 导出的变量名叫 lb\_dt, ae\_yn。
|
||||
* **优化**:通过 iit\_field\_mapping 表,将其转换为具有临床语义的键:{"采样日期": "2026-02-25", "是否发生不良事件": "否"}。
|
||||
|
||||
### **优化 2:基于实例的剪枝 (Instance-Level Pruning) —— 解决 Token 溢出**
|
||||
|
||||
* **痛点**:写报告时没必要把 95% 正确的数据喂给模型。
|
||||
* **优化**:执行 JSON 树剪枝。只 query 出 qc\_field\_status \= 'FAIL' 的那一小块实例分支:
|
||||
{"受试者\_P005": {"不良事件表": \[{"Instance\_2": {"事件": "发烧", "质控": "未填严重程度"}}\]}}。
|
||||
|
||||
### **优化 3:XML 提示词封装 —— 解决上下文幻觉**
|
||||
|
||||
* 利用 \<record\>, \<event\>, \<form\>, \<instance\> 严格包裹数据,先进的大模型对 XML 层级的理解极佳,能完美规避张冠李戴。
|
||||
|
||||
### **优化 4:计算下放 (CPU over GPU) —— 解决算术薄弱**
|
||||
|
||||
* LLM 绝对不做日期和数值计算(如算缺失率百分比、算超窗天数)。所有硬核计算由 Node.js HardRuleEngine 算好,直接把“结论标签”传给 LLM 组装文字。
|
||||
@@ -0,0 +1,72 @@
|
||||
# **核心转换机制白皮书:从五层数据底座到多维质控报告的生成逻辑**
|
||||
|
||||
**文档定位**:系统设计核心枢纽,连接底层数据库、规则引擎与前台业务报表。
|
||||
|
||||
**阅读对象**:后端架构师、规则配置工程师(DM)、产品经理
|
||||
|
||||
## **核心理念:质控的本质是“坐标映射与计算”**
|
||||
|
||||
在我们的系统中,没有任何一份报告是凭空“写”出来的。所有的报告,其本质都是:
|
||||
|
||||
1. 取出特定 **五层坐标 (Record \-\> Event \-\> Form \-\> Instance \-\> Field)** 上的数据。
|
||||
2. 放入特定的 **规则引擎 (D1\~D7)** 进行计算。
|
||||
3. 将计算结果(PASS/FAIL/数值)**组装并投影**到 5 张标准化 GCP 报告的单元格中。
|
||||
|
||||
## **逐表拆解:5大核心报告的生成机制与数据链路**
|
||||
|
||||
### **报告一:筛选与入选登记表 (对应 D1 规则)**
|
||||
|
||||
**业务目的**:证明患者入组是完全合规的,没有放错人进来。
|
||||
|
||||
| 报告显示的列 (微小内容) | 依赖的 5 层数据坐标 (查什么) | 调用的质控规则与逻辑 (算什么) | 执行引擎 |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| **筛选日期** | Event=筛选期 \-\> Form=知情同意表 \-\> Instance=1 \-\> Field=签署日期 | 提取知情书的签署日期。 | 基础查询 |
|
||||
| **纳入/排除是否符合** | Event=基线 \-\> Form=入排标准表 \-\> Instance=1 \-\> Field=所有入排项 | **\[D1 逻辑\]** 验证纳入标准是否全为“是”,排除标准是否全为“否”。 | HardRuleEngine |
|
||||
| **时序合规校验** | Form=知情表 vs Form=基线表 | **\[D1 逻辑\]** 校验 知情签署日期 \<= 筛选检查发生日期。违规即阻断。 | HardRuleEngine |
|
||||
| **失败原因** | qc\_field\_status \= 'FAIL' 的具体 Field | 提取亮红灯的 FieldID 对应的字典标签(如:排除标准3不符)。 | AutoMapper |
|
||||
|
||||
### **报告二:数据录入率 & 缺失率记录表 (对应 D2 规则)**
|
||||
|
||||
**业务目的**:证明数据的完整性。最考验系统工程能力的一张表。
|
||||
|
||||
| 报告显示的列 | 依赖的 5 层数据坐标 (查什么) | 调用的质控规则与逻辑 (算什么) | 执行引擎 |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| **应录入总字段数** | REDCap 字典 & Branching Logic | **\[D2 逻辑 \- 极度核心\]** 代入患者其他字段值,解析分支逻辑。例如:如果 性别=男,则该患者所有 妊娠检查 相关的 Instance 应录入数 \= 0。 | CompletenessEngine |
|
||||
| **实际已录入字段数** | 遍历当前 Form 下所有 Instance 的所有 Field | 计算非空 (value \!= null) 的字段总数。 | 基础查询 |
|
||||
| **数据缺失率 (%)** | 上述两个计算结果 | 公式:(应录入 \- 实录入) / 应录入 \* 100%。 | CPU 计算 |
|
||||
|
||||
### **报告三:数据质疑管理跟踪表 (eQuery Log) (对应 D3/D4 规则)**
|
||||
|
||||
**业务目的**:记录逻辑矛盾和笔误的闭环过程。
|
||||
|
||||
| 报告显示的列 | 依赖的 5 层数据坐标 (查什么) | 调用的质控规则与逻辑 (算什么) | 执行引擎 |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| **质疑编号 & 定位** | 取 status \= 'FAIL' 的 5 层全路径 | 拼接坐标:\[Record\]-\[Event\]-\[Form\]-\[Instance\]-\[Field\]。 | 基础查询 |
|
||||
| **问题描述 (AI 生成)** | 提取异常 Field 的 actual\_value,预期规则 | **\[D3 逻辑\]** 极值/关联校验。 **LLM组装**:将硬规则警报抛给 LLM,翻译成严谨的人类句子。 | HardRule \+ SoftRule |
|
||||
| **当前状态 & 解决时间** | pending\_actions 表流转状态 | **\[D4 逻辑\]** 监听该 Instance 的数据变更。复核通过,状态从 OPEN 变 CLOSED。 | 状态机 |
|
||||
|
||||
### **报告四:AE 不良事件记录表 (对应 D5 规则 \- 核心亮点)**
|
||||
|
||||
**业务目的**:展现 AI 作为“数字 CRA”挖掘隐患的强大能力。
|
||||
|
||||
| 报告显示的列 | 依赖的 5 层数据坐标 (查什么) | 调用的质控规则与逻辑 (算什么) | 执行引擎 |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| **显性 AE 记录** | Form=不良事件表 的所有 Instance | 遍历每个独立 Instance(第 1 次 AE, 第 2 次 AE),提取名称、开始/结束日期。 | 基础查询 |
|
||||
| **⚠️ 疑似漏报 AE 挖掘** | 遍历 Form=实验室检查 等表单的 Field | **\[D5 逻辑\]** 发现化验数值达到 2 级及以上异常,跨表检索 AE表 所有 Instance,若无匹配记录,触发告警。 | HardRule |
|
||||
| **与干预的因果关系** | Form=不良事件表 \+ Form=合并用药表(所有Instance) | 提取患者服用的所有药物。**LLM知识库比对**,若属禁忌,提示 PI 关注。 | SoftRule (RAG) |
|
||||
|
||||
### **报告五:方案偏离记录表 (PD Log) (对应 D6 规则)**
|
||||
|
||||
**业务目的**:抓取不按方案日历和标准操作的违规行为。
|
||||
|
||||
| 报告显示的列 | 依赖的 5 层数据坐标 (查什么) | 调用的质控规则与逻辑 (算什么) | 执行引擎 |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| **偏离类型:访视超窗** | Event=本次随访 实际日期 vs Event=基线 日期 | **\[D6 逻辑\]** 依据方案 SoA,计算天数差。若超窗则判定为偏离。 | HardRule |
|
||||
| **偏离类型:禁忌药** | Form=合并用药表 \-\> 所有 Instance \-\> 药物名称 | **\[D6 逻辑\]** 将各 Instance 录入药物名与方案知识库比对。 | SoftRule (RAG) |
|
||||
| **偏离描述与分级建议** | 综合上述计算结果 | LLM 将“超窗 5 天”总结为连贯描述,并根据规则库预判 Major/Minor 偏离。 | SoftRule (LLM) |
|
||||
|
||||
## **终极总结:这三者是如何协同工作的?(The Orchestration)**
|
||||
|
||||
1. **底层地基 (数据坐标)**:CDISC 5层架构提供了极其精确的 GPS 定位,彻底搞定了重复表单数据覆盖问题。
|
||||
2. **流水线加工 (规则引擎)**:当 Webhook 带着 InstanceID 涌入时,系统调取 D1\~D7 规则。算缺失率用 D2 解析分支;查超窗用 D6 算日期;查漏报 AE 用 D5 跨 Instance 匹配。
|
||||
3. **顶层组装 (质控报告与 LLM)**:引擎产生带标签的坐标点(如 P005-V2-AE\_Log-Instance\_3: 严重程度未填)。系统将这些剪枝后的碎片喂给 LLM,LLM “翻译”并填入 5 张标准的 GCP 报表。
|
||||
Reference in New Issue
Block a user