feat(iit): Implement event-level QC architecture V3.1 with dynamic rule filtering, report deduplication and AI intent enhancement

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 21:22:11 +08:00
parent 45c7b32dbb
commit 7a299e8562
51 changed files with 10638 additions and 184 deletions

View File

@@ -0,0 +1,30 @@
-- CRA 智能质控引擎支持
-- 扩展 IitSkill 表,新增 IitQcReport 表
-- AlterTable: 扩展 skills 表支持 CRA 五大规则体系
ALTER TABLE "iit_schema"."skills" ADD COLUMN "level" TEXT NOT NULL DEFAULT 'normal',
ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 100,
ADD COLUMN "required_tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
ADD COLUMN "rule_type" TEXT NOT NULL DEFAULT 'HARD_RULE';
-- CreateTable: 质控报告存储表
CREATE TABLE "iit_schema"."qc_reports" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"report_type" TEXT NOT NULL DEFAULT 'daily',
"summary" JSONB NOT NULL,
"issues" JSONB NOT NULL,
"llm_report" TEXT NOT NULL,
"generated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3),
CONSTRAINT "qc_reports_pkey" PRIMARY KEY ("id")
);
-- CreateIndex: 报告索引
CREATE INDEX "idx_qc_report_project_type" ON "iit_schema"."qc_reports"("project_id", "report_type");
CREATE INDEX "idx_qc_report_generated" ON "iit_schema"."qc_reports"("generated_at");
-- CreateIndex: Skill 触发类型索引
CREATE INDEX "idx_iit_skill_active_trigger" ON "iit_schema"."skills"("is_active", "trigger_type");

View File

@@ -959,19 +959,26 @@ model IitAuditLog {
/// Skill 配置存储表 - 存储质控规则、SOP流程图
/// 支持 webhook/cron/event 三种触发方式
/// V3.0 扩展:支持 CRA 五大规则体系
model IitSkill {
id String @id @default(uuid())
projectId String @map("project_id")
skillType String @map("skill_type") // qc_process | daily_briefing | general_chat | weekly_report | visit_reminder
skillType String @map("skill_type") // qc_process | daily_briefing | general_chat | weekly_report | visit_reminder | variable_qc | inclusion_exclusion | protocol_deviation | ae_monitoring | ethics_compliance
name String // 技能名称
description String? // 技能描述
config Json @db.JsonB // 核心配置 JSONSOP 流程图)
config Json @db.JsonB // 核心配置 JSONSOP 流程图 / 规则定义
isActive Boolean @default(true) @map("is_active")
version Int @default(1)
// V2.9 新增:主动触发能力
triggerType String @default("webhook") @map("trigger_type") // 'webhook' | 'cron' | 'event'
cronSchedule String? @map("cron_schedule") // Cron 表达式,如 "0 9 * * *"
triggerType String @default("webhook") @map("trigger_type") // 'webhook' | 'cron' | 'manual'
cronSchedule String? @map("cron_schedule") // Cron 表达式,如 "0 2 * * *" (每日凌晨2点)
// V3.0 新增CRA 质控引擎支持
ruleType String @default("HARD_RULE") @map("rule_type") // 'HARD_RULE' | 'LLM_CHECK' | 'HYBRID'
level String @default("normal") @map("level") // 'blocking' | 'normal' - 阻断性检查优先
priority Int @default(100) @map("priority") // 执行优先级,数字越小越优先
requiredTags String[] @default([]) @map("required_tags") // 数据依赖标签,如 ['#lab', '#demographics']
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -979,10 +986,41 @@ model IitSkill {
@@unique([projectId, skillType], map: "unique_iit_skill_project_type")
@@index([projectId], map: "idx_iit_skill_project")
@@index([isActive], map: "idx_iit_skill_active")
@@index([isActive, triggerType], map: "idx_iit_skill_active_trigger")
@@map("skills")
@@schema("iit_schema")
}
/// 质控报告存储表 - 存储预生成的 LLM 友好报告
/// 报告驱动模式:后台预计算 → LLM 阅读报告 → 回答用户
model IitQcReport {
id String @id @default(uuid())
projectId String @map("project_id")
// 报告类型
reportType String @default("daily") @map("report_type") // 'daily' | 'weekly' | 'on_demand'
// 统计摘要
summary Json @db.JsonB // { totalRecords, criticalIssues, pendingQueries, passRate }
// 详细问题列表
issues Json @db.JsonB // [{ record, rule, severity, description, evidence }]
// LLM 友好的 XML 报告
llmReport String @map("llm_report") @db.Text
// 报告生成时间
generatedAt DateTime @default(now()) @map("generated_at")
// 报告有效期(过期后需重新生成)
expiresAt DateTime? @map("expires_at")
@@index([projectId, reportType], map: "idx_qc_report_project_type")
@@index([generatedAt], map: "idx_qc_report_generated")
@@map("qc_reports")
@@schema("iit_schema")
}
/// 字段名映射字典表 - 解决 LLM 生成的字段名与 REDCap 实际字段名不一致的问题
model IitFieldMapping {
id String @id @default(uuid())

View File

@@ -0,0 +1,384 @@
/**
* CRA 智能质控引擎 - 默认规则配置 Seed
*
* 五大规则体系:
* 1. 变量质控 (VARIABLE_QC) - 硬规则
* 2. 入排标准 (INCLUSION_EXCLUSION) - LLM 检查
* 3. 方案偏离 (PROTOCOL_DEVIATION) - 混合规则
* 4. AE 监测 (AE_MONITORING) - LLM 检查
* 5. 伦理合规 (ETHICS_COMPLIANCE) - 硬规则
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
/**
* 默认 Skills 配置
*/
const DEFAULT_SKILLS = [
// ============================================================
// 1. 变量质控 - 硬规则 (Level: normal, 实时触发)
// ============================================================
{
skillType: 'variable_qc',
name: '变量质控',
description: '针对每个变量的数据校验,包括空值检查、数值范围、格式验证',
ruleType: 'HARD_RULE',
level: 'normal',
priority: 100,
triggerType: 'webhook',
requiredTags: ['#demographics', '#lab'],
config: {
engine: 'HardRuleEngine',
rules: [
{
id: 'VQ-001',
name: '年龄范围检查',
field: 'age',
formName: 'demographics',
logic: {
and: [
{ '>=': [{ var: 'age' }, 18] },
{ '<=': [{ var: 'age' }, 100] },
],
},
message: '年龄超出合理范围 (18-100岁)',
severity: 'error',
category: 'lab_values',
},
{
id: 'VQ-002',
name: 'BMI 范围检查',
field: 'bmi',
formName: 'demographics',
logic: {
and: [
{ '>': [{ var: 'bmi' }, 10] },
{ '<': [{ var: 'bmi' }, 60] },
],
},
message: 'BMI 值异常 (正常范围 10-60)',
severity: 'warning',
category: 'lab_values',
},
{
id: 'VQ-003',
name: '体重范围检查',
field: 'weight',
formName: 'demographics',
logic: {
and: [
{ '>': [{ var: 'weight' }, 20] },
{ '<': [{ var: 'weight' }, 300] },
],
},
message: '体重值异常 (正常范围 20-300kg)',
severity: 'warning',
category: 'lab_values',
},
{
id: 'VQ-004',
name: '身高范围检查',
field: 'height',
formName: 'demographics',
logic: {
and: [
{ '>': [{ var: 'height' }, 50] },
{ '<': [{ var: 'height' }, 250] },
],
},
message: '身高值异常 (正常范围 50-250cm)',
severity: 'warning',
category: 'lab_values',
},
],
},
},
// ============================================================
// 2. 入排标准 - LLM 检查 (Level: normal, 定时触发)
// ============================================================
{
skillType: 'inclusion_exclusion',
name: '入排标准核查',
description: '判断受试者是否符合研究方案的入组标准和排除标准',
ruleType: 'LLM_CHECK',
level: 'normal',
priority: 200,
triggerType: 'cron',
cronSchedule: '0 2 * * *', // 每日凌晨 2 点
requiredTags: ['#demographics', '#medical_history', '#lab'],
config: {
engine: 'SoftRuleEngine',
model: 'deepseek-v3',
systemPrompt: '你是一个专业的临床研究监查员,负责核查受试者是否符合入组标准。',
checks: [
{
id: 'IE-001',
name: '年龄入组标准',
desc: '检查受试者年龄是否符合研究方案规定的入组年龄范围',
promptTemplate: `请根据以下受试者数据,判断其年龄是否符合入组标准。
受试者年龄: {{age}}
研究方案通常要求受试者年龄在 18-75 岁之间。请判断此受试者是否符合年龄入组标准。`,
requiredTags: ['#demographics'],
category: 'inclusion',
severity: 'critical',
},
{
id: 'IE-002',
name: '确诊时间入组标准',
desc: '检查受试者确诊时间是否在研究方案规定的时间窗口内',
promptTemplate: `请根据以下受试者数据,判断其确诊时间是否符合入组标准。
确诊日期: {{diagnosis_date}}
入组日期: {{enrollment_date}}
研究方案通常要求确诊时间在入组前一定时间内(如 3 个月、6 个月等)。请判断此受试者是否符合确诊时间入组标准。`,
requiredTags: ['#demographics', '#medical_history'],
category: 'inclusion',
severity: 'critical',
},
],
},
},
// ============================================================
// 3. 方案偏离 - 混合规则 (Level: normal, 定时触发)
// ============================================================
{
skillType: 'protocol_deviation',
name: '方案偏离检测',
description: '检测访视超窗、漏做检查、违反用药规定等方案偏离情况',
ruleType: 'HYBRID',
level: 'normal',
priority: 300,
triggerType: 'cron',
cronSchedule: '0 3 * * *', // 每日凌晨 3 点
requiredTags: ['#visits', '#medications'],
config: {
engine: 'HybridEngine',
hardRules: [
{
id: 'PD-001',
name: '访视间隔检查',
field: ['visit_1_date', 'visit_2_date'],
logic: {
'<=': [
{ '-': [{ var: 'visit_2_date' }, { var: 'visit_1_date' }] },
31, // 最大间隔 28+3 天
],
},
message: '访视超窗:访视 2 与访视 1 间隔超过 31 天',
severity: 'warning',
category: 'logic_check',
},
],
softChecks: [
{
id: 'PD-002',
name: '禁用药物检查',
desc: '检查受试者是否使用了研究方案禁止的药物',
promptTemplate: '请检查受试者的用药记录,判断是否存在使用研究方案禁止药物的情况。',
requiredTags: ['#medications'],
category: 'protocol_deviation',
severity: 'warning',
},
],
},
},
// ============================================================
// 4. AE 监测 - LLM 检查 (Level: normal, 定时触发)
// ============================================================
{
skillType: 'ae_monitoring',
name: 'AE 事件监测',
description: '检测未报告的不良事件,包括实验室异常与 AE 记录的一致性检查',
ruleType: 'LLM_CHECK',
level: 'normal',
priority: 250,
triggerType: 'cron',
cronSchedule: '0 4 * * *', // 每日凌晨 4 点
requiredTags: ['#lab', '#ae'],
config: {
engine: 'SoftRuleEngine',
model: 'deepseek-v3',
systemPrompt: '你是一个专业的药物安全监查员,负责核查不良事件报告的完整性和准确性。',
checks: [
{
id: 'AE-001',
name: 'Lab 异常与 AE 一致性',
desc: '检查实验室检查异常值是否已在 AE 表中报告',
promptTemplate: `请对比以下实验室检查数据和不良事件记录,判断是否存在未报告的实验室异常。
重点关注:
1. Grade 3 及以上的实验室异常是否已记录为 AE
2. 持续异常是否已报告
3. 与基线相比显著变化的指标
请给出判断结果,并列出可能遗漏报告的异常。`,
requiredTags: ['#lab', '#ae'],
category: 'ae_detection',
severity: 'critical',
},
{
id: 'AE-002',
name: 'SAE 报告时效性',
desc: '检查严重不良事件是否在规定时间内报告',
promptTemplate: `请检查以下 SAE 记录,判断报告时效是否符合法规要求。
通常要求:
- 致死/危及生命的 SAE: 24 小时内报告
- 其他 SAE: 15 天内报告
请判断各 SAE 的报告时效是否合规。`,
requiredTags: ['#ae'],
category: 'ae_detection',
severity: 'critical',
},
],
},
},
// ============================================================
// 5. 伦理合规 - 硬规则 (Level: blocking, 实时触发)
// ============================================================
{
skillType: 'ethics_compliance',
name: '伦理合规检查',
description: '检查知情同意书签署时间、隐私保护等伦理合规问题',
ruleType: 'HARD_RULE',
level: 'blocking', // 阻断性检查,失败则跳过后续 AI 检查
priority: 10, // 最高优先级
triggerType: 'webhook',
requiredTags: ['#consent', '#demographics'],
config: {
engine: 'HardRuleEngine',
rules: [
{
id: 'EC-001',
name: '知情同意书签署时间',
field: ['icf_date', 'first_visit_date'],
logic: {
'<=': [{ var: 'icf_date' }, { var: 'first_visit_date' }],
},
message: '伦理违规:知情同意书签署日期晚于首次访视日期',
severity: 'error',
category: 'ethics',
},
{
id: 'EC-002',
name: '知情同意书必填检查',
field: 'icf_date',
logic: {
'!!': [{ var: 'icf_date' }],
},
message: '伦理违规:缺少知情同意书签署日期',
severity: 'error',
category: 'ethics',
},
{
id: 'EC-003',
name: '受试者 ID 必填检查',
field: 'record_id',
logic: {
'!!': [{ var: 'record_id' }],
},
message: '数据完整性:缺少受试者 ID',
severity: 'error',
category: 'ethics',
},
],
},
},
];
/**
* 为项目创建默认 Skills
*
* @param projectId 项目 ID
*/
export async function seedDefaultSkills(projectId: string): Promise<void> {
console.log(`[Seed] Creating default skills for project: ${projectId}`);
for (const skillData of DEFAULT_SKILLS) {
try {
// 使用 upsert 避免重复创建
await prisma.iitSkill.upsert({
where: {
projectId_skillType: {
projectId,
skillType: skillData.skillType,
},
},
update: {
name: skillData.name,
description: skillData.description,
ruleType: skillData.ruleType,
level: skillData.level,
priority: skillData.priority,
triggerType: skillData.triggerType,
cronSchedule: skillData.cronSchedule,
requiredTags: skillData.requiredTags,
config: skillData.config,
isActive: true,
updatedAt: new Date(),
},
create: {
projectId,
skillType: skillData.skillType,
name: skillData.name,
description: skillData.description,
ruleType: skillData.ruleType,
level: skillData.level,
priority: skillData.priority,
triggerType: skillData.triggerType,
cronSchedule: skillData.cronSchedule,
requiredTags: skillData.requiredTags,
config: skillData.config,
isActive: true,
},
});
console.log(`[Seed] Created/Updated skill: ${skillData.name}`);
} catch (error: any) {
console.error(`[Seed] Failed to create skill: ${skillData.name}`, error.message);
}
}
console.log(`[Seed] Completed creating default skills for project: ${projectId}`);
}
/**
* 主函数 - 可以直接运行
*/
async function main() {
// 获取命令行参数
const projectId = process.argv[2];
if (!projectId) {
console.error('Usage: npx ts-node prisma/seeds/cra-qc-skills.seed.ts <projectId>');
console.error('Example: npx ts-node prisma/seeds/cra-qc-skills.seed.ts test0102-pd-study');
process.exit(1);
}
// 检查项目是否存在
const project = await prisma.iitProject.findUnique({
where: { id: projectId },
});
if (!project) {
console.error(`Project not found: ${projectId}`);
process.exit(1);
}
await seedDefaultSkills(projectId);
}
// 如果直接运行此文件
main()
.catch(console.error)
.finally(() => prisma.$disconnect());