在临床研究质控系统里,SDV、逻辑核查、异常跟进经常被拆在不同模块:一个人看原始资料,一个人跑规则,一个人导出 Excel 跟进 Query。断链之后,问题不是“有没有发现异常”,而是异常是否进入统一任务、是否被分派、是否有审计轨迹、是否能回写到研究数据流程。本文只讨论技术架构与工程流程示例,不提供诊断、治疗、分诊或用药建议;文中的阈值和升级规则均为示例,真实项目应由医疗专业人员和机构规范确认。

质控断链通常发生在哪里

我在做临床研究质控系统复盘时,最常见的断点有三个。

第一,SDV 结果只停留在核对记录里。监查员确认了 CRF 字段与原始资料是否一致,但系统没有把“不一致”“缺失”“无法确认”转成可追踪的问题对象。

第二,逻辑核查只输出批量报表。规则引擎能发现访视日期、入排标准、用药记录、实验室字段之间的矛盾,但如果结果只是 CSV,后续就会依赖人工复制、标注和邮件沟通。

第三,异常识别模型没有业务闭环。AI 可以给出“疑似异常”的分数,但如果缺少证据片段、规则来源、处理人、状态流转和审计日志,最终很难进入正式质控流程。

所以优化目标不是把某个模型做得更复杂,而是把 SDV、规则、异常分数统一到一个“质控任务中心”。

技术目标与边界

本文的实现范围包括三个核心模块:

  • 质控任务中心:统一承接 SDV 差异、逻辑核查问题、异常识别结果。
  • 规则执行器:支持可配置规则,输出结构化 issue。
  • 问题回写机制:将确认后的质控问题回写到业务系统,并保留审计记录。

推荐技术栈可以保持朴素:

  • Python:规则执行、异常检测、任务生成。
  • PostgreSQL:保存质控任务、状态、审计日志。
  • Object Storage:保存原始证据快照、导入文件、核查附件。
  • Audit Service:记录谁在什么时候基于什么证据做了什么操作。

这里的 AI 更适合放在“异常候选生成”和“优先级辅助排序”,不要直接替代质控结论。质控结论仍应通过研究项目 SOP、机构规范和授权人员确认。

联动架构:把三类信号收敛为一个 Issue

一个可落地的架构可以这样拆:

EDC/CTMS/原始资料索引
        |
        v
数据标准化层:subject_id / visit_code / field_code / value / source_ref
        |
        +------------------+
        |                  |
        v                  v
  SDV比对服务        逻辑规则执行器
        |                  |
        +---------+--------+
                  |
                  v
          异常识别与优先级评分
                  |
                  v
             质控任务中心
                  |
        +---------+---------+
        v                   v
   Query/问题回写        Audit审计日志

关键设计点是:无论来源是 SDV、逻辑核查还是异常识别,都要落成统一的 Issue Schema。这样后续分派、跟进、关闭、复核都不需要关心问题来自哪个模块。

一个简化表结构如下:

CREATE TABLE qc_issue (
    issue_id UUID PRIMARY KEY,
    study_id TEXT NOT NULL,
    subject_id TEXT NOT NULL,
    visit_code TEXT,
    field_code TEXT,
    issue_type TEXT NOT NULL,
    severity TEXT NOT NULL,
    source_module TEXT NOT NULL,
    evidence_uri TEXT,
    status TEXT NOT NULL DEFAULT 'open',
    assignee TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT now(),
    updated_at TIMESTAMP NOT NULL DEFAULT now()
);

CREATE TABLE qc_audit_log (
    log_id UUID PRIMARY KEY,
    issue_id UUID NOT NULL,
    actor TEXT NOT NULL,
    action TEXT NOT NULL,
    before_state JSONB,
    after_state JSONB,
    created_at TIMESTAMP NOT NULL DEFAULT now()
);

issue_type 可以是 sdv_mismatchlogic_violationoutlier_candidatesource_module 用来追踪来源,但不应该影响任务中心的状态机。

Python 实现:规则执行到任务生成

下面示例演示一个最小规则执行器:输入标准化后的受试者访视数据,输出可进入任务中心的 issue。示例规则只用于说明工程流程,不代表任何机构标准。

from dataclasses import dataclass, asdict
from datetime import datetime
from uuid import uuid4
from typing import Any, Dict, List, Optional


@dataclass
class QCIssue:
    issue_id: str
    study_id: str
    subject_id: str
    visit_code: Optional[str]
    field_code: Optional[str]
    issue_type: str
    severity: str
    source_module: str
    message: str
    evidence_uri: Optional[str]
    created_at: str


def create_issue(
    row: Dict[str, Any],
    issue_type: str,
    severity: str,
    source_module: str,
    message: str,
    evidence_uri: Optional[str] = None
) -> QCIssue:
    return QCIssue(
        issue_id=str(uuid4()),
        study_id=row["study_id"],
        subject_id=row["subject_id"],
        visit_code=row.get("visit_code"),
        field_code=row.get("field_code"),
        issue_type=issue_type,
        severity=severity,
        source_module=source_module,
        message=message,
        evidence_uri=evidence_uri,
        created_at=datetime.utcnow().isoformat()
    )


def run_logic_checks(rows: List[Dict[str, Any]]) -> List[QCIssue]:
    issues = []

    for row in rows:
        # 示例规则1:访视日期不能早于入组日期,真实项目需按方案和机构规则确认
        if row.get("visit_date") and row.get("enroll_date"):
            if row["visit_date"] < row["enroll_date"]:
                issues.append(create_issue(
                    row=row,
                    issue_type="logic_violation",
                    severity="major",
                    source_module="rules_engine",
                    message="示例规则:访视日期早于入组日期,请复核日期录入或访视归属。"
                ))

        # 示例规则2:SDV标记为不一致时生成统一质控任务
        if row.get("sdv_result") == "mismatch":
            issues.append(create_issue(
                row=row,
                issue_type="sdv_mismatch",
                severity="major",
                source_module="sdv_service",
                message="SDV发现字段值与原始资料记录不一致,需人工确认。",
                evidence_uri=row.get("source_ref")
            ))

        # 示例规则3:异常分数超过可配置阈值时进入候选任务
        # 阈值0.85仅为示例,不是行业标准
        if row.get("anomaly_score", 0) >= 0.85:
            issues.append(create_issue(
                row=row,
                issue_type="outlier_candidate",
                severity="minor",
                source_module="anomaly_detector",
                message="异常识别模型给出较高候选分数,建议结合上下文复核。"
            ))

    return issues


if __name__ == "__main__":
    demo_rows = [
        {
            "study_id": "STUDY_DEMO",
            "subject_id": "S001",
            "visit_code": "V02",
            "field_code": "visit_date",
            "enroll_date": "2026-04-10",
            "visit_date": "2026-04-01",
            "sdv_result": "matched",
            "anomaly_score": 0.31
        },
        {
            "study_id": "STUDY_DEMO",
            "subject_id": "S002",
            "visit_code": "V01",
            "field_code": "dose_record",
            "enroll_date": "2026-04-11",
            "visit_date": "2026-04-12",
            "sdv_result": "mismatch",
            "source_ref": "s3://qc-evidence/STUDY_DEMO/S002/V01/dose_record.json",
            "anomaly_score": 0.91
        }
    ]

    for issue in run_logic_checks(demo_rows):
        print(asdict(issue))

这个示例刻意没有把 AI 结果直接写成“错误”。outlier_candidate 的语义是候选异常,进入任务中心后还需要质控人员判断、补充说明或关闭。

状态机:避免问题发现后没人处理

任务中心最容易被低估的是状态机。一个建议的最小状态流如下:

open -> assigned -> in_review -> resolved -> closed
  |          |            |
  |          |            +-> rejected
  |          |
  +----------+-> duplicated

每次状态变化都应写审计日志。不要只记录最终状态,否则后续复盘时无法回答这些问题:

  • 问题由哪个模块生成?
  • 哪位人员确认过?
  • 依据了哪个证据文件?
  • 是否被合并、驳回或升级?
  • 回写到哪个外部系统对象?

如果要做性能优化,可以把规则执行和任务入库解耦。规则执行器批量产生 issue event,任务中心异步消费,做去重、合并、分派和审计写入。这样在批量导入数据时,不会因为外部回写接口慢而拖垮规则执行。

去重、合并与优先级排序

SDV、逻辑核查、异常识别可能命中同一字段。比如某受试者某访视的日期字段,既出现 SDV 不一致,又触发逻辑规则,还被模型判为异常。如果直接生成三条任务,质控人员会重复处理。

可以用以下维度生成指纹:

study_id + subject_id + visit_code + field_code + issue_type_group

其中 issue_type_group 可以把相近问题归为一组,例如 date_consistency。如果新 issue 命中已有未关闭任务,则把新证据追加到 evidence 列表,而不是新建任务。

优先级可以由多因素计算:

  • 规则严重程度:由机构规则配置。
  • 是否影响关键字段:由研究项目配置。
  • 是否存在 SDV 证据:有证据的问题优先进入人工核查。
  • 异常分数:仅作为排序参考,不作为最终判断。

这些都是示例策略,真实项目中需要与质控计划、研究方案、SOP 和数据管理要求对齐。

工程踩坑与优化建议

第一,证据要快照化。不要只保存一个外部系统 URL,因为原始数据可能后续被修改。更稳妥的做法是把触发规则时的字段值、来源引用、文件版本写入对象存储。

第二,规则版本要入库。某条规则在 5 月触发的问题,不能用 6 月改过的规则去解释。建议保存 rule_idrule_versionrule_params

第三,AI 输出要可解释。至少保存输入字段、异常分数、触发原因或特征摘要。否则质控人员只能看到“高风险”标签,很难判断是否需要跟进。

第四,回写要幂等。外部系统接口超时后重试很常见,必须用 issue_id 或业务唯一键保证不会重复创建 Query。

第五,权限和审计不要后补。临床研究质控场景里,谁看过、谁改过、谁关闭了问题,本身就是系统能力的一部分。

结论

AI 提升临床研究质控效率,不是把 SDV、逻辑核查、异常识别分别做一个漂亮面板,而是把它们收敛到统一的任务闭环。工程上要抓住四件事:统一 Issue Schema、规则版本化、证据快照化、状态与审计可追踪。

发现异常不等于完成质控。只有当异常被分派、复核、回写、关闭,并且全流程可追溯时,SDV、逻辑核查和异常识别才真正形成联动。

本文文献检索、文献挖掘以及文献翻译采用的是【超能文献| AI文献检索|AI文档翻译】

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐