/** * SoftRuleEngine - 软规则质控引擎 (LLM 推理) * * 功能: * - 调用 LLM 进行复杂的医学逻辑判断 * - 支持入排标准、AE 事件检测、方案偏离等场景 * - 返回带证据链的结构化结果 * * 设计原则: * - 智能推理:利用 LLM 处理模糊规则和复杂逻辑 * - 证据链:每个判断都附带推理过程和证据 * - 三态输出:PASS / FAIL / UNCERTAIN(需人工确认) */ import { PrismaClient } from '@prisma/client'; import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js'; import { logger } from '../../../common/logging/index.js'; import { buildClinicalSlice } from '../services/PromptBuilder.js'; const prisma = new PrismaClient(); // ============================================================ // SoftRuleEngine 实现 // ============================================================ export class SoftRuleEngine { constructor(projectId, config) { this.projectId = projectId; this.model = config?.model || 'deepseek-v3'; this.timeoutMs = config?.timeoutMs || 30000; } /** * 执行软规则检查 * * @param recordId 记录ID * @param data 记录数据 * @param checks 要执行的检查列表 * @returns 检查结果 */ async execute(recordId, data, checks) { const startTime = Date.now(); const results = []; const failedChecks = []; const uncertainChecks = []; logger.info('[SoftRuleEngine] Starting execution', { projectId: this.projectId, recordId, checkCount: checks.length, model: this.model, }); // 逐个执行检查(可以改为并发,但需注意 Token 限制) for (const check of checks) { try { const result = await this.executeCheck(recordId, data, check); results.push(result); if (result.status === 'FAIL') { failedChecks.push(result); } else if (result.status === 'UNCERTAIN') { uncertainChecks.push(result); } } catch (error) { logger.error('[SoftRuleEngine] Check execution failed', { checkId: check.id, error: error.message, }); // 发生错误时标记为 UNCERTAIN const errorResult = { checkId: check.id, checkName: check.name, status: 'UNCERTAIN', reason: `执行出错: ${error.message}`, evidence: {}, confidence: 0, severity: check.severity, category: check.category, }; results.push(errorResult); uncertainChecks.push(errorResult); } } // 计算整体状态 let overallStatus = 'PASS'; if (failedChecks.length > 0) { overallStatus = 'FAIL'; } else if (uncertainChecks.length > 0) { overallStatus = 'UNCERTAIN'; } const duration = Date.now() - startTime; logger.info('[SoftRuleEngine] Execution completed', { recordId, overallStatus, totalChecks: checks.length, failed: failedChecks.length, uncertain: uncertainChecks.length, duration: `${duration}ms`, }); return { recordId, projectId: this.projectId, timestamp: new Date().toISOString(), overallStatus, summary: { totalChecks: checks.length, passed: results.filter(r => r.status === 'PASS').length, failed: failedChecks.length, uncertain: uncertainChecks.length, }, results, failedChecks, uncertainChecks, }; } /** * 执行单个检查 */ async executeCheck(recordId, data, check) { const startTime = Date.now(); // 1. 构建 Prompt const prompt = this.buildCheckPrompt(recordId, data, check); // 2. 调用 LLM const llmAdapter = LLMFactory.getAdapter(this.model); const response = await llmAdapter.chat([ { role: 'system', content: this.getSystemPrompt(), }, { role: 'user', content: prompt, }, ]); const rawResponse = response.content; // 3. 解析响应 const parsed = this.parseResponse(rawResponse, check); const duration = Date.now() - startTime; logger.debug('[SoftRuleEngine] Check executed', { checkId: check.id, status: parsed.status, confidence: parsed.confidence, duration: `${duration}ms`, }); return { checkId: check.id, checkName: check.name, status: parsed.status, reason: parsed.reason, evidence: parsed.evidence, confidence: parsed.confidence, severity: check.severity, category: check.category, rawResponse, }; } /** * 构建检查 Prompt */ buildCheckPrompt(recordId, data, check) { // 使用 PromptBuilder 生成临床数据切片 const clinicalSlice = buildClinicalSlice({ task: check.name, criteria: [check.description || check.name], patientData: data, tags: check.requiredTags, instruction: '请根据以下数据进行判断。', }); // 替换 Prompt 模板中的变量 let userPrompt = check.promptTemplate; // 替换 {{variable}} 格式的占位符 userPrompt = userPrompt.replace(/\{\{(\w+)\}\}/g, (_, key) => { return data[key] !== undefined ? String(data[key]) : `[${key}未提供]`; }); // 替换 {{#tag}} 格式的数据标签 userPrompt = userPrompt.replace(/\{\{#(\w+)\}\}/g, (_, tag) => { // 根据标签筛选相关字段 return JSON.stringify(data, null, 2); }); return `${clinicalSlice}\n\n---\n\n## 检查任务\n\n${userPrompt}`; } /** * 获取系统 Prompt */ getSystemPrompt() { return `你是一个专业的临床研究数据监查员 (CRA),负责核查受试者数据的质量和合规性。 ## 你的职责 1. 仔细分析提供的临床数据 2. 根据检查任务进行判断 3. 给出清晰的判断结果和理由 ## 输出格式要求 请严格按照以下 JSON 格式输出: \`\`\`json { "status": "PASS" | "FAIL" | "UNCERTAIN", "reason": "判断理由的详细说明", "evidence": { "key_field_1": "相关数据值", "key_field_2": "相关数据值" }, "confidence": 0.95 } \`\`\` ## 状态说明 - **PASS**: 检查通过,数据符合要求 - **FAIL**: 检查失败,发现问题 - **UNCERTAIN**: 数据不足或存在歧义,需要人工确认 ## 置信度说明 - 0.9-1.0: 非常确定 - 0.7-0.9: 比较确定 - 0.5-0.7: 有一定把握 - <0.5: 不太确定,建议人工复核 请只输出 JSON,不要有其他内容。`; } /** * 解析 LLM 响应 */ parseResponse(rawResponse, check) { try { // 尝试提取 JSON const jsonMatch = rawResponse.match(/```json\s*([\s\S]*?)\s*```/); const jsonStr = jsonMatch ? jsonMatch[1] : rawResponse; const parsed = JSON.parse(jsonStr.trim()); // 验证状态值 const validStatuses = ['PASS', 'FAIL', 'UNCERTAIN']; const status = validStatuses.includes(parsed.status?.toUpperCase()) ? parsed.status.toUpperCase() : 'UNCERTAIN'; return { status: status, reason: parsed.reason || '未提供理由', evidence: parsed.evidence || {}, confidence: typeof parsed.confidence === 'number' ? Math.min(1, Math.max(0, parsed.confidence)) : 0.5, }; } catch (error) { logger.warn('[SoftRuleEngine] Failed to parse LLM response', { checkId: check.id, rawResponse: rawResponse.substring(0, 500), }); // 解析失败时尝试简单匹配 const lowerResponse = rawResponse.toLowerCase(); if (lowerResponse.includes('pass') || lowerResponse.includes('通过')) { return { status: 'PASS', reason: rawResponse, evidence: {}, confidence: 0.6, }; } else if (lowerResponse.includes('fail') || lowerResponse.includes('失败') || lowerResponse.includes('不符合')) { return { status: 'FAIL', reason: rawResponse, evidence: {}, confidence: 0.6, }; } return { status: 'UNCERTAIN', reason: `无法解析响应: ${rawResponse.substring(0, 200)}`, evidence: {}, confidence: 0.3, }; } } /** * 批量执行检查 * * @param records 记录列表 * @param checks 检查列表 * @returns 所有记录的检查结果 */ async executeBatch(records, checks) { const results = []; for (const record of records) { const result = await this.execute(record.recordId, record.data, checks); results.push(result); } return results; } } // ============================================================ // 工厂函数 // ============================================================ /** * 创建 SoftRuleEngine 实例 * * @param projectId 项目ID * @param config 可选配置 * @returns SoftRuleEngine 实例 */ export function createSoftRuleEngine(projectId, config) { return new SoftRuleEngine(projectId, config); } // ============================================================ // 预置检查模板 // ============================================================ /** * 入排标准检查模板 */ export const INCLUSION_EXCLUSION_CHECKS = [ { id: 'IE-001', name: '年龄入组标准', description: '检查受试者年龄是否符合入组标准', promptTemplate: '请根据受试者数据,判断其年龄是否在研究方案规定的入组范围内。如果年龄字段缺失,请标记为 UNCERTAIN。', requiredTags: ['#demographics'], category: 'inclusion', severity: 'critical', }, { id: 'IE-002', name: '确诊时间入组标准', description: '检查受试者确诊时间是否符合入组标准(通常要求确诊在一定时间内)', promptTemplate: '请根据受试者的确诊日期和入组日期,判断确诊时间是否符合研究方案要求。', requiredTags: ['#demographics', '#medical_history'], category: 'inclusion', severity: 'critical', }, ]; /** * AE 事件检测模板 */ export const AE_DETECTION_CHECKS = [ { id: 'AE-001', name: 'Lab 异常与 AE 一致性', description: '检查实验室检查异常值是否已在 AE 表中报告', promptTemplate: '请对比实验室检查数据和不良事件记录,判断是否存在未报告的实验室异常(Grade 3 及以上)。', requiredTags: ['#lab', '#ae'], category: 'ae_detection', severity: 'critical', }, ]; /** * 方案偏离检测模板 */ export const PROTOCOL_DEVIATION_CHECKS = [ { id: 'PD-001', name: '访视超窗检测', description: '检查访视是否在方案规定的时间窗口内', promptTemplate: '请根据访视日期数据,判断各访视之间的时间间隔是否符合方案规定的访视窗口。', requiredTags: ['#visits'], category: 'protocol_deviation', severity: 'warning', }, ];