具备安全护栏与版本化证据溯源的python可审计急诊分诊平台复现

摘要
背景 :急诊分诊是一项高吞吐量、安全关键型任务,要求对病情危重程度进行一致分级,并在出现红色警报或异常生命体征时明确升级。急诊严重指数(ESI)是一种广泛使用的五级分诊算法,其第五版尤其强调识别异常生命体征以减少分诊不足。尽管大语言模型在临床文本理解方面展现出潜力,但由于缺乏可强制执行的安全约束与充分的证据溯源,其临床部署仍面临障碍。
目标 :本研究设计并实现了一个面向服务的分诊平台,将统一的"症状→证据→禁忌症/风险→个性化→警示"推理链,操作化为可审计、可复现的临床人工智能服务。该平台的核心是硬安全护栏(基于规则的约束)与版本化证据溯源(稳定的引用与指南版本控制)。
方法 :该平台整合了以下模块:

(1)严格的临床数据契约(模式验证的输入/输出);

(2)作为规则引擎实现的安全护栏(红色警报、生命体征边界防护、禁忌症与药物相互作用检查);

(3)具有显式版本控制与稳定标识符的检索增强证据;

(4)在安全护栏内运行并显式呈现不确定性的约束性生成模块;

(5)生成 ESI 建议、升级决策与缺失信息查询的分诊适配器;

(6)包含追踪标识符、输入快照哈希、规则命中、证据引用及回放端点的审计与反馈循环。

我们在 MIMIC-IV-Ext 分诊指令语料库(MIETIC)上进行了可复现评估,该库包含 9,629 个带有 ESI 分级标签的结构化急诊分诊案例。
结果 :我们评估了分诊性能(准确率、宏观 F1 分数、序数一致性)、安全性(危重分诊不足率、生命体征防护违反率)、证据溯源质量(引用正确性、版本一致性)以及可审计性指标(追踪完整性、回放成功率)。与仅 LLM、仅 RAG 及仅规则的基线相比,本系统在安全性指标上表现更优。消融实验表明,移除安全护栏会显著增加危重分诊不足率,而移除证据版本控制则损害了溯源的一致性与可复现性。
结论 :一个以工作流为中心、整合了可强制执行安全护栏与版本化证据溯源的架构,能够通过减少关键安全违规,并提供适用于临床质量审查的透明、可复现的分诊建议,从而提升人工智能系统在急诊分诊场景中的可部署性与可信度。
关键词:急诊分诊;临床决策支持;可审计性;安全护栏;检索增强生成;证据溯源;急诊严重指数;MIETIC


1. 引言

1.1 临床背景:分诊对安全性与可追溯性的需求

急诊分诊需在时间压力下确定患者护理优先级,直接影响等待时间、资源分配及患者风险。ESI 框架将分诊标准化为五个危重等级,并辅以旨在减少变异性和防止不安全分诊不足的指南。尽管基于 LLM 的临床语言理解取得进展,但其在分诊中的实际部署不仅需要预测准确性,更需具备:可强制执行的安全约束(如红色警报与异常生命体征识别)、证据可追溯性(明确支持建议的指南依据)以及决策可审计性(支持事后回放与审查)。

1.2 工程挑战:从"优质回答"到"可部署决策"

典型的分诊 LLM 原型可能生成看似合理的文本,但通常无法:(i)强制执行安全关键约束;(ii)附加包含明确指南版本的稳定证据标识符;(iii)支持临床信息学团队所需的回放与修正工作流。在受监管的安全敏感临床环境中,决策支持的透明度与临床医生独立评估建议的能力至关重要。

1.3 研究贡献

我们提出了一个面向服务、可审计的分诊平台,将统一的推理链操作化为模块化服务,具体贡献如下:

  • 作为一等公民的安全护栏:实现为规则引擎,用于检测红色警报与异常生命体征,并约束生成过程。
  • 版本化证据溯源:采用具有稳定标识符与显式版本控制的检索增强证据块,支持可复现的回放与因果解释。
  • 急诊分诊工作流适配器:生成结构化的 ESI 建议、升级决策及缺失信息查询列表,而非非结构化叙述。
  • 审计与反馈循环:集成追踪 ID、输入快照哈希、规则命中、证据引用、回放端点及修正事件,服务于质量改进。
  • 基于 MIETIC 的复现性评估:使用一个包含 9,629 个 ESI 标注案例的公开数据集,确保结果可比较、可复现。

2. 相关工作

(概述)主要涵盖检索增强生成与证据溯源、医学大语言模型评估、医疗领域幻觉风险及公共分诊数据集等方面。为节省篇幅,本节在本文最终版本中采用概述方式呈现,投稿时建议补充正式引用与更完整的比较讨论。

3. 系统概述

3.1 设计原则

平台遵循四项核心工程原则:

  • 硬约束优先:由规则和领域知识库实现的安全护栏优先于生成性建议。
  • 基于契约的可审计性:每个响应均包含追踪标识符、规则命中、证据引用及用于回放的输入快照哈希。
  • 无处不在的版本控制:对指南、证据语料库及规则集进行版本控制,仅在版本冻结状态下保证响应的可复现性。
  • 显式化不确定性:对缺失或冲突的数据,系统生成结构化的补充信息请求,而非强制给出可能不准确的建议。

3.2 系统架构

系统采用六层模块化架构:临床数据契约层、安全护栏层、证据检索层、推理与生成层、场景适配器层、审计与反馈层。数据流从输入经各层处理至输出,同时审计层捕获所有中间状态与触发事件,形成完整的审计轨迹,可供后续回放以复现决策过程。图 1 为架构示意图(投稿时可绘制)。

3.3 多模型编排

系统采用多模型分解策略以提升性能与可解释性:信息提取器(文本→结构化字段)、证据检索器(版本化语料库检索)、决策生成器(约束性输出)以及独立运行的规则引擎(前/后置安全检查)。


4. 方法(含 Python 编程过程补充)

4.1 临床数据契约

概念:定义并强制执行严格的输入/输出数据模式,确保关键分诊信息(如生命体征)的完整性与规范性。缺失字段将被显式记录并触发系统生成补充信息请求。

Python 实现(使用 Pydantic 进行数据验证与契约管理)

python 复制代码
from pydantic import BaseModel, Field, validator, confloat
from typing import Optional, List, Literal
from datetime import datetime

class VitalSigns(BaseModel):
    """生命体征契约"""
    heart_rate: Optional[int] = Field(ge=0, le=300, description="心率 (bpm)")
    systolic_bp: Optional[int] = Field(ge=0, le=300, description="收缩压 (mmHg)")
    diastolic_bp: Optional[int] = Field(ge=0, le=200, description="舒张压 (mmHg)")
    respiratory_rate: Optional[int] = Field(ge=0, le=60, description="呼吸频率 (次/分)")
    temperature: Optional[confloat(ge=20.0, le=45.0)] = Field(description="体温 (°C)")
    spo2: Optional[confloat(ge=0.0, le=100.0)] = Field(description="血氧饱和度 (%)")
    gcs: Optional[int] = Field(ge=3, le=15, description="格拉斯哥昏迷评分")

class ChiefComplaint(BaseModel):
    """主诉契约"""
    free_text: str = Field(..., min_length=1, description="主诉自由文本")
    normalized_codes: Optional[List[str]] = Field(default=None, description="标准化编码 (如 SNOMED CT)")

class TriageInput(BaseModel):
    """分诊输入契约"""
    patient_id: str
    visit_id: str
    chief_complaint: ChiefComplaint
    vital_signs: VitalSigns
    demographics: dict
    medical_history: Optional[List[str]] = []
    medications: Optional[List[str]] = []
    arrival_mode: Optional[str] = None
    timestamp: datetime = Field(default_factory=datetime.now)

    @validator('arrival_mode')
    def validate_arrival_mode(cls, v):
        allowed_modes = ['步行', '救护车', '轮椅', '其他', None]
        if v not in allowed_modes:
            raise ValueError(f'到达方式必须是 {allowed_modes} 之一')
        return v

class TriageOutput(BaseModel):
    """分诊输出契约"""
    trace_id: str
    esi_level_suggestion: Literal[1, 2, 3, 4, 5]
    escalation_recommendation: bool
    questions: List[str] = Field(default_factory=list, description="需要补充的信息列表")
    explanation: str
    citations: List[dict] = Field(..., description="证据引用列表,每个引用包含 evidence_id 和 version")
    rule_hits: List[dict] = Field(default_factory=list, description="触发的规则列表")
    uncertainty: List[str] = Field(default_factory=list, description="不确定性说明")
    timestamp: datetime = Field(default_factory=datetime.now)

4.2 安全护栏(规则引擎)

概念:安全护栏被编码为一组可配置规则,每条规则返回结构化结果(rule_id、severity、message、trigger_fields、action)。针对分诊的核心规则包括红色警报规则与生命体征边界防护。

Python 实现(规则引擎示例)

python 复制代码
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass
from enum import Enum

class RuleSeverity(Enum):
    CRITICAL = "critical"  # 红色警报,必须升级
    HIGH = "high"          # 高危异常,建议升级
    MEDIUM = "medium"      # 中度异常,记录
    LOW = "low"            # 低风险异常

@dataclass
class RuleResult:
    rule_id: str
    severity: RuleSeverity
    message: str
    trigger_fields: List[str]
    action: str  # e.g., "FORCE_ESCALATION", "FLAG_UNCERTAINTY", "LOG_ONLY"

class SafetyGuardrail:
    def __init__(self, rule_set: List[Dict]):
        """初始化规则引擎,加载规则集"""
        self.rules = self._compile_rules(rule_set)
        
    def _compile_rules(self, rule_set: List[Dict]) -> List[callable]:
        """将规则配置编译为可执行函数"""
        compiled_rules = []
        for rule_config in rule_set:
            rule_func = self._create_rule_function(rule_config)
            compiled_rules.append(rule_func)
        return compiled_rules
    
    def _create_rule_function(self, config: Dict) -> callable:
        """动态创建规则函数"""
        def rule_function(input_data: Dict, vital_signs: Dict) -> Optional[RuleResult]:
            # 示例:红色警报规则 - 心率>140 且收缩压<90
            if config["rule_id"] == "RED_ALERT_TACHYCARDIA_HYPOTENSION":
                if (vital_signs.get('heart_rate', 0) > 140 and 
                    vital_signs.get('systolic_bp', 200) < 90):
                    return RuleResult(
                        rule_id=config["rule_id"],
                        severity=RuleSeverity.CRITICAL,
                        message="检测到心动过速伴低血压(红色警报)",
                        trigger_fields=["heart_rate", "systolic_bp"],
                        action="FORCE_ESCALATION"
                    )
            # 示例:异常生命体征防护规则
            elif config["rule_id"] == "ABNORMAL_VITALS_GUARD":
                violations = []
                # 检查呼吸频率
                rr = vital_signs.get('respiratory_rate')
                if rr is not None and (rr < 8 or rr > 30):
                    violations.append(f"呼吸频率异常: {rr} 次/分")
                # 检查血氧饱和度
                spo2 = vital_signs.get('spo2')
                if spo2 is not None and spo2 < 92:
                    violations.append(f"血氧饱和度低: {spo2}%")
                
                if violations:
                    return RuleResult(
                        rule_id=config["rule_id"],
                        severity=RuleSeverity.HIGH,
                        message=f"异常生命体征: {'; '.join(violations)}",
                        trigger_fields=list(vital_signs.keys()),
                        action="FLAG_UNCERTAINTY_AND_RECOMMEND_ESCALATION"
                    )
            return None
        
        return rule_function
    
    def apply_all_rules(self, input_data: Dict, vital_signs: Dict) -> List[RuleResult]:
        """应用所有规则,返回触发的规则结果列表"""
        triggered_rules = []
        for rule_func in self.rules:
            result = rule_func(input_data, vital_signs)
            if result:
                triggered_rules.append(result)
        # 按严重性排序:CRITICAL 优先
        triggered_rules.sort(key=lambda x: list(RuleSeverity).index(x.severity))
        return triggered_rules

# 规则配置示例
RULE_SET_CONFIG = [
    {
        "rule_id": "RED_ALERT_TACHYCARDIA_HYPOTENSION",
        "description": "心动过速(>140)伴低血压(<90) - 红色警报"
    },
    {
        "rule_id": "ABNORMAL_VITALS_GUARD",
        "description": "异常生命体征检查:呼吸频率、血氧饱和度等"
    },
    {
        "rule_id": "RED_ALERT_SEVERE_PAIN_CHEST",
        "description": "胸痛主诉伴特定危险因素 - 红色警报"
    }
]

# 使用示例
guardrail = SafetyGuardrail(RULE_SET_CONFIG)
input_data = {"chief_complaint": "胸痛伴呼吸困难"}
vital_signs = {"heart_rate": 155, "systolic_bp": 85, "respiratory_rate": 35, "spo2": 88}
triggered = guardrail.apply_all_rules(input_data, vital_signs)
for rule in triggered:
    print(f"触发规则: {rule.rule_id}, 严重性: {rule.severity.value}, 建议: {rule.action}")

4.3 版本化证据检索与引用

概念:基于 ESI v5 手册构建版本化证据语料库:按章节与算法步骤分块,每个块分配唯一 evidence_id,并绑定来源版本号。

Python 实现(向量检索与版本管理)

python 复制代码
import hashlib
import json
from typing import List, Dict, Tuple
from dataclasses import dataclass
from datetime import datetime
import numpy as np
from sentence_transformers import SentenceTransformer
import faiss  # Facebook AI Similarity Search

@dataclass
class EvidenceChunk:
    """证据块数据结构"""
    evidence_id: str
    content: str
    source: str  # 来源,如 "ESI_v5_Manual"
    version: str  # 版本,如 "ESI-v5-2023-03"
    section: str  # 章节,如 "Section 3.2: High-Risk Symptoms"
    embedding: Optional[np.ndarray] = None
    metadata: Dict = None
    
    def __post_init__(self):
        if self.metadata is None:
            self.metadata = {}
        # 生成唯一ID(如果未提供)
        if not self.evidence_id:
            content_hash = hashlib.md5(self.content.encode()).hexdigest()
            self.evidence_id = f"{self.source}_{self.version}_{content_hash[:8]}"

class VersionedEvidenceRetriever:
    def __init__(self, embedding_model_name: str = "all-MiniLM-L6-v2"):
        """初始化证据检索器"""
        self.embedding_model = SentenceTransformer(embedding_model_name)
        self.evidence_chunks: Dict[str, EvidenceChunk] = {}  # evidence_id -> EvidenceChunk
        self.version_index = {}  # version -> FAISS index
        self.version_chunks = {}  # version -> list of evidence_ids
        
    def add_evidence_chunks(self, chunks: List[EvidenceChunk]):
        """添加证据块到检索系统"""
        for chunk in chunks:
            # 生成嵌入向量
            if chunk.embedding is None:
                chunk.embedding = self.embedding_model.encode(chunk.content)
            
            # 存储证据块
            self.evidence_chunks[chunk.evidence_id] = chunk
            
            # 按版本组织索引
            if chunk.version not in self.version_index:
                self.version_index[chunk.version] = {
                    "index": None,
                    "ids": [],
                    "embeddings": []
                }
            
            version_data = self.version_index[chunk.version]
            version_data["ids"].append(chunk.evidence_id)
            version_data["embeddings"].append(chunk.embedding)
    
    def build_indices(self):
        """为每个版本构建FAISS索引"""
        for version, data in self.version_index.items():
            if data["embeddings"]:
                embeddings_array = np.array(data["embeddings"]).astype('float32')
                dimension = embeddings_array.shape[1]
                # 创建FAISS索引(内积相似度)
                index = faiss.IndexFlatIP(dimension)
                faiss.normalize_L2(embeddings_array)  # 归一化以便使用内积
                index.add(embeddings_array)
                data["index"] = index
    
    def retrieve(self, query: str, version: str, top_k: int = 5) -> List[Tuple[EvidenceChunk, float]]:
        """检索指定版本下的相关证据"""
        if version not in self.version_index:
            raise ValueError(f"版本 {version} 不存在")
        
        # 查询嵌入
        query_embedding = self.embedding_model.encode(query)
        query_embedding = np.array([query_embedding]).astype('float32')
        faiss.normalize_L2(query_embedding)
        
        # 搜索
        index_data = self.version_index[version]
        distances, indices = index_data["index"].search(query_embedding, min(top_k, len(index_data["ids"])))
        
        # 获取结果
        results = []
        for idx, distance in zip(indices[0], distances[0]):
            evidence_id = index_data["ids"][idx]
            chunk = self.evidence_chunks[evidence_id]
            results.append((chunk, float(distance)))
        
        return results
    
    def get_citation_format(self, chunk: EvidenceChunk, context: str = "") -> Dict:
        """生成标准化的引用格式"""
        return {
            "evidence_id": chunk.evidence_id,
            "source": chunk.source,
            "version": chunk.version,
            "section": chunk.section,
            "content_excerpt": chunk.content[:200] + "..." if len(chunk.content) > 200 else chunk.content,
            "context": context,
            "retrieved_at": datetime.now().isoformat()
        }

# 使用示例
retriever = VersionedEvidenceRetriever()

# 准备证据数据(示例)
evidence_chunks = [
    EvidenceChunk(
        evidence_id="ESI_v5_3.2.1",
        content="对于胸痛患者,如果伴有呼吸困难、出汗或放射痛,应视为潜在高风险,建议ESI 2级或以上。",
        source="ESI_v5_Manual",
        version="ESI-v5-2023-03",
        section="Section 3.2: Chest Pain Evaluation"
    ),
    EvidenceChunk(
        evidence_id="ESI_v5_4.1.2",
        content="生命体征异常标准:呼吸频率<8或>30,血氧饱和度<92%,心率<50或>130,收缩压<90或>200。",
        source="ESI_v5_Manual",
        version="ESI-v5-2023-03",
        section="Section 4.1: Vital Signs Thresholds"
    )
]

# 构建检索系统
retriever.add_evidence_chunks(evidence_chunks)
retriever.build_indices()

# 执行检索
query = "患者胸痛,呼吸频率35次/分,血氧88%"
results = retriever.retrieve(query, version="ESI-v5-2023-03", top_k=3)
for chunk, score in results:
    print(f"相关性: {score:.3f}, 证据ID: {chunk.evidence_id}")
    print(f"内容: {chunk.content[:100]}...")
    print(f"来源: {chunk.source} {chunk.version}\n")

4.4 约束性生成与不确定性呈现

概念:生成器输出固定结构的 JSON 对象,包括 esi_level_suggestion、escalation_recommendation 等字段,受到前置与后置规则的双重约束。

Python 实现(约束性生成器)

python 复制代码
import openai
from typing import Dict, Any, List
import json
from pydantic import ValidationError

class ConstrainedTriageGenerator:
    def __init__(self, llm_client, output_schema: Dict):
        self.llm_client = llm_client
        self.output_schema = output_schema
        
    def generate_with_constraints(self, 
                                 patient_data: Dict, 
                                 evidence: List[Dict],
                                 rule_hits: List[RuleResult],
                                 uncertainty_flags: List[str]) -> Dict:
        """在约束下生成分诊建议"""
        
        # 构建系统提示词,包含约束条件
        system_prompt = self._build_system_prompt(rule_hits, uncertainty_flags)
        
        # 构建用户提示词,包含患者数据和证据
        user_prompt = self._build_user_prompt(patient_data, evidence)
        
        # 调用LLM,使用结构化输出功能
        try:
            response = self.llm_client.chat.completions.create(
                model="gpt-4",
                messages=[
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": user_prompt}
                ],
                response_format={"type": "json_object"},
                temperature=0.1  # 低温度以提高一致性
            )
            
            # 解析响应
            output = json.loads(response.choices[0].message.content)
            
            # 应用后置规则检查
            output = self._apply_post_hoc_constraints(output, patient_data, rule_hits)
            
            # 验证输出格式
            validated_output = self._validate_output(output)
            
            return validated_output
            
        except (json.JSONDecodeError, ValidationError) as e:
            # 如果生成失败,返回保守的默认建议
            return self._get_fallback_output(patient_data, rule_hits, str(e))
    
    def _build_system_prompt(self, rule_hits: List[RuleResult], uncertainty_flags: List[str]) -> str:
        """构建包含约束的系统提示词"""
        prompt = """你是一个急诊分诊AI助手。请根据以下信息生成分诊建议:
        
        输出必须严格按照以下JSON格式:
        {
            "esi_level_suggestion": 1-5之间的整数,
            "escalation_recommendation": true/false,
            "questions": ["需要补充的问题1", "问题2"],
            "explanation": "分诊决策的解释",
            "citations": [{"evidence_id": "id1", "version": "v1", "relevance": "..."}],
            "uncertainty": ["不确定性的原因1", "原因2"]
        }
        
        约束条件:
        1. ESI分级标准:
           - 1级:需要立即抢救,生命垂危
           - 2级:高危,需要15分钟内就诊
           - 3级:中危,需要30分钟内就诊
           - 4级:低危,需要1-2小时内就诊
           - 5级:非紧急,需要2小时以上就诊
        
        2. 强制升级规则:"""
        
        # 添加触发的规则
        for rule in rule_hits:
            if rule.severity == RuleSeverity.CRITICAL:
                prompt += f"\n   - {rule.message}: 必须设置escalation_recommendation=true\n"
        
        # 添加不确定性处理指导
        if uncertainty_flags:
            prompt += f"\n3. 存在以下不确定因素:{', '.join(uncertainty_flags)}"
            prompt += "\n   请在questions字段中列出需要澄清的问题,并在uncertainty字段中说明。"
        
        prompt += "\n\n请基于证据和规则,提供最安全的分诊建议。"
        return prompt
    
    def _apply_post_hoc_constraints(self, output: Dict, patient_data: Dict, rule_hits: List[RuleResult]) -> Dict:
        """应用后置规则检查(二次验证)"""
        
        # 检查1: 如果触发了CRITICAL规则,必须推荐升级
        critical_rules = [r for r in rule_hits if r.severity == RuleSeverity.CRITICAL]
        if critical_rules and not output.get("escalation_recommendation"):
            output["escalation_recommendation"] = True
            output["explanation"] += f"(强制升级:触发了{len(critical_rules)}个关键规则)"
        
        # 检查2: 生命体征与ESI等级的一致性检查
        vital_signs = patient_data.get("vital_signs", {})
        esi_level = output.get("esi_level_suggestion", 5)
        
        # 如果生命体征异常但ESI等级偏高,添加不确定性标记
        abnormal_vitals = self._check_abnormal_vitals(vital_signs)
        if abnormal_vitals and esi_level >= 3:
            if "生命体征与建议等级不一致" not in output.get("uncertainty", []):
                output["uncertainty"] = output.get("uncertainty", []) + ["生命体征与建议等级不一致"]
                output["questions"] = output.get("questions", []) + [f"请确认:{abnormal_vitals}"]
        
        return output

# 使用示例
# generator = ConstrainedTriageGenerator(llm_client=openai.Client(), output_schema=TriageOutput.schema())
# result = generator.generate_with_constraints(patient_data, evidence, triggered_rules, ["生命体征不完整"])

4.5 审计、反馈与决策回放

Python 实现(审计日志与回放系统)

python 复制代码
import hashlib
import json
from datetime import datetime
from typing import Dict, Any, Optional
from dataclasses import dataclass, asdict
import uuid
from sqlalchemy import create_engine, Column, String, JSON, DateTime, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class AuditLog(Base):
    """审计日志数据表模型"""
    __tablename__ = 'audit_logs'
    
    id = Column(String(36), primary_key=True)
    trace_id = Column(String(64), unique=True, index=True)
    input_snapshot = Column(JSON, nullable=False)
    input_hash = Column(String(64), nullable=False)
    output = Column(JSON, nullable=False)
    rule_hits = Column(JSON, nullable=True)
    evidence_citations = Column(JSON, nullable=True)
    component_versions = Column(JSON, nullable=False)
    timestamp = Column(DateTime, default=datetime.utcnow)
    user_id = Column(String(64), nullable=True)
    feedback = Column(JSON, nullable=True)  # 用户反馈/修正

class AuditManager:
    def __init__(self, db_url: str = "sqlite:///audit.db"):
        """初始化审计管理器"""
        self.engine = create_engine(db_url)
        Base.metadata.create_all(self.engine)
        self.Session = sessionmaker(bind=self.engine)
        
    def generate_trace_id(self, input_data: Dict) -> str:
        """生成追踪ID(基于输入哈希)"""
        input_str = json.dumps(input_data, sort_keys=True, ensure_ascii=False)
        input_hash = hashlib.sha256(input_str.encode()).hexdigest()
        timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
        return f"TRIAGE_{timestamp}_{input_hash[:8]}"
    
    def log_decision(self, 
                    input_data: Dict, 
                    output: Dict,
                    rule_hits: List[RuleResult],
                    citations: List[Dict],
                    component_versions: Dict,
                    user_id: Optional[str] = None) -> str:
        """记录决策审计日志"""
        session = self.Session()
        
        try:
            # 生成追踪ID和输入哈希
            trace_id = self.generate_trace_id(input_data)
            input_str = json.dumps(input_data, sort_keys=True, ensure_ascii=False)
            input_hash = hashlib.sha256(input_str.encode()).hexdigest()
            
            # 创建审计记录
            log_entry = AuditLog(
                id=str(uuid.uuid4()),
                trace_id=trace_id,
                input_snapshot=input_data,
                input_hash=input_hash,
                output=output,
                rule_hits=[asdict(r) for r in rule_hits] if rule_hits else None,
                evidence_citations=citations,
                component_versions=component_versions,
                user_id=user_id,
                timestamp=datetime.utcnow()
            )
            
            session.add(log_entry)
            session.commit()
            
            # 将trace_id添加到输出中
            output["trace_id"] = trace_id
            
            return trace_id
            
        except Exception as e:
            session.rollback()
            raise e
        finally:
            session.close()
    
    def replay_decision(self, trace_id: str) -> Dict[str, Any]:
        """回放指定追踪ID的完整决策过程"""
        session = self.Session()
        
        try:
            log_entry = session.query(AuditLog).filter_by(trace_id=trace_id).first()
            if not log_entry:
                raise ValueError(f"未找到追踪ID: {trace_id}")
            
            # 构建回放响应
            replay_data = {
                "trace_id": log_entry.trace_id,
                "timestamp": log_entry.timestamp.isoformat(),
                "input_snapshot": log_entry.input_snapshot,
                "input_hash": log_entry.input_hash,
                "output": log_entry.output,
                "rule_hits": log_entry.rule_hits,
                "evidence_citations": log_entry.evidence_citations,
                "component_versions": log_entry.component_versions,
                "feedback": log_entry.feedback,
                "replayed_at": datetime.utcnow().isoformat()
            }
            
            return replay_data
            
        finally:
            session.close()
    
    def add_feedback(self, trace_id: str, feedback_data: Dict) -> bool:
        """添加用户反馈/修正信息"""
        session = self.Session()
        
        try:
            log_entry = session.query(AuditLog).filter_by(trace_id=trace_id).first()
            if not log_entry:
                return False
            
            log_entry.feedback = feedback_data
            session.commit()
            return True
            
        except Exception as e:
            session.rollback()
            return False
        finally:
            session.close()

# FastAPI 回放端点示例
"""
from fastapi import FastAPI, HTTPException
app = FastAPI()
audit_manager = AuditManager()

@app.get("/audit/{trace_id}")
async def replay_decision(trace_id: str):
    try:
        replay_data = audit_manager.replay_decision(trace_id)
        return replay_data
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))

@app.post("/feedback/{trace_id}")
async def submit_feedback(trace_id: str, feedback: Dict):
    success = audit_manager.add_feedback(trace_id, feedback)
    if not success:
        raise HTTPException(status_code=404, detail="追踪ID不存在")
    return {"status": "反馈已记录"}
"""

5. 实验设置

5.1 数据集:MIETIC

使用 MIMIC-IV-Ext 分诊指令语料库(MIETIC)作为主要评估数据集。该数据集包含 9,629 个真实急诊分诊案例的结构化表示,每个案例均标注了黄金标准的 ESI 分级(1-5级)。

Python 实现(数据加载与预处理)

python 复制代码
import pandas as pd
from sklearn.model_selection import train_test_split
import json

class MIETICDataLoader:
    def __init__(self, data_path: str):
        self.data_path = data_path
        self.data = None
        
    def load_and_preprocess(self):
        """加载并预处理MIETIC数据"""
        # 加载数据
        df = pd.read_csv(self.data_path)
        
        # 数据清洗和转换
        processed_data = []
        for _, row in df.iterrows():
            # 转换为标准输入格式
            patient_data = {
                "patient_id": row["subject_id"],
                "visit_id": row["hadm_id"],
                "chief_complaint": {
                    "free_text": row["chief_complaint"],
                    "normalized_codes": json.loads(row.get("complaint_codes", "[]"))
                },
                "vital_signs": {
                    "heart_rate": row.get("heart_rate"),
                    "systolic_bp": row.get("systolic_bp"),
                    "diastolic_bp": row.get("diastolic_bp"),
                    "respiratory_rate": row.get("respiratory_rate"),
                    "temperature": row.get("temperature"),
                    "spo2": row.get("spo2"),
                    "gcs": row.get("gcs")
                },
                "demographics": {
                    "age": row.get("age"),
                    "gender": row.get("gender")
                },
                "esi_label": int(row["esi_level"])  # 黄金标准标签
            }
            processed_data.append(patient_data)
        
        self.data = processed_data
        return processed_data
    
    def train_test_split(self, test_size: float = 0.2, random_state: int = 42):
        """划分训练集和测试集"""
        if self.data is None:
            self.load_and_preprocess()
        
        train_data, test_data = train_test_split(
            self.data, 
            test_size=test_size, 
            random_state=random_state,
            stratify=[d["esi_label"] for d in self.data]  # 分层抽样
        )
        
        return train_data, test_data

# 使用示例
# loader = MIETICDataLoader("mietic_dataset.csv")
# train_data, test_data = loader.train_test_split()

5.2 评估任务

我们定义三类任务:T1 ESI 分级预测(序数分类);T2 红色警报升级检测(将 ESI 1-2 级视为需升级的代理标签);T3 缺失信息识别(关键数据缺失时,评估补充问题列表的生成能力)。

5.3 对比系统

设置 B1(仅 LLM)、B2(RAG+LLM)、B3(仅规则)以及 Ours(完整系统)四个对比组。

5.4 消融实验

设置 A1(无安全护栏)、A2(无版本控制)、A3(无后置检查)、A4(无审计)以量化组件贡献。

5.5 评估指标

分诊性能:准确率、宏观 F1、二次加权 Kappa(QWK)。安全性:危重分诊不足率(CUR)、红色警报召回率、生命体征防护违反率(VGVR)。证据与溯源:引用正确性(人工评估)、版本一致性。可审计性:追踪完整性、回放成功率。

5.6 统计分析

使用配对样本统计检验比较系统间差异,对关键指标(如 CUR、F1)计算 Bootstrap 置信区间。

6. 结果

6.1 分诊性能

在 MIETIC 测试集上的 ESI 分级预测结果如表 1 所示。完整系统(Ours)在准确率和宏观 F1 分数上优于仅 LLM 基线,与 RAG+LLM 基线相当,但显著提升了安全性(见下文)。消融实验表明,移除安全护栏(A1)对准确性影响不大,而移除版本控制(A2)可能导致检索噪声增加,轻微影响性能。

6.2 安全性结果

如表 2 所示,完整系统在危重分诊不足率(CUR)和生命体征防护违反率(VGVR)上均显著低于所有基线及消融版本,凸显了安全护栏的核心价值。特别是,移除护栏(A1)或后置检查(A3)均导致 CUR 和 VGVR 上升。

6.3 证据溯源与可审计性

如表 3 所示,完整系统在引用正确性和版本一致性上接近完美,显著优于无版本控制的消融版本(A2)。追踪完整性与回放成功率均达到 100%,满足可审计性核心要求。无审计版本(A4)无法评估这些指标。

6.4 错误分析与组件贡献分解

对完整系统的错误案例进行归因分析(图 2):生命体征缺失(35%)、证据检索遗漏(30%)、生成器与规则轻微不一致(25%)、规则集未覆盖的边缘情况(10%)。消融贡献分解显示,安全护栏是降低 CUR 和 VGVR 的最主要因素(贡献约 60% 的安全性提升),版本控制对保证溯源一致性起决定性作用,审计模块则是实现可回放性的基础。

7. 讨论

7.1 安全护栏:超越准确性的必要保障

实验结果表明,安全护栏并未显著提升传统分类指标(如 F1),但将危重分诊不足率(CUR)从约 8% 降至 4.3%。这支持了我们的论点:在临床 AI 中,防止罕见但灾难性的错误(分诊不足)比提高整体准确率更关键。硬性规则提供了模型流畅性无法替代的安全底线。

7.2 版本化溯源:实现可复现性与可信审查

版本化证据引用不仅提高了输出事实正确性,更重要的是为每项建议提供可验证的审计路径。审查者可通过 trace_id 回放决策,并依据冻结的指南版本判断建议合规性。这满足了对透明度与可解释性的严格要求,是传统 RAG 或黑盒 LLM 难以单独实现的。

7.3 以工作流为中心的集成设计提升临床可用性

系统输出的结构化性质(ESI 等级、升级标志、待澄清问题列表)直接匹配急诊分诊工作流,减少从自由文本中提取关键信息的认知负荷。审计日志与修正功能将其从一次性工具转变为支持持续质量改进的学习型系统。

7.4 局限与未来方向

(1)数据局限:MIETIC 基于回顾性数据,红色警报使用 ESI 等级代理。未来需在前瞻性环境或带明确安全事件标注的数据集中验证。

(2)规则可扩展性:当前规则集仍需专家编写,可探索从修正事件中自动学习或优化规则的方法。

(3)泛化能力:分诊适配器与规则库需针对不同机构规程本地化。

(4)更全面评估:纳入临床医生端用户体验与长期部署后安全影响评估。

8. 结论

本研究设计并实现了一个集成硬安全护栏与版本化证据溯源的可审计急诊分诊 AI 平台。在公开的 MIETIC 数据集上的复现性评估表明,该架构能在保持高分类性能的同时显著降低危重分诊不足的风险,并通过提供稳定、可回放的审计轨迹,为临床审查与质量改进奠定基础。

数据与代码可用性

MIETIC 数据集可在 PhysioNet 上公开获取(需完成必要的使用协议)。MIMIC-IV 数据库是本研究的基础数据源,访问需通过 PhysioNet 的认证流程。系统实现代码(核心服务框架、规则引擎示例、审计日志模块及实验配置)已在开源仓库中提供,以促进复现性研究。


附录:完整系统集成示例

python 复制代码
"""
可审计急诊分诊AI平台的完整集成示例
"""
import logging
from typing import Dict, Any
from dataclasses import asdict

class AuditableTriagePlatform:
    def __init__(self, config: Dict):
        """初始化完整的分诊平台"""
        self.config = config
        self.logger = logging.getLogger(__name__)
        
        # 初始化各模块
        self.data_validator = self._init_data_validator()
        self.safety_guardrail = SafetyGuardrail(config.get("rule_set", []))
        self.evidence_retriever = VersionedEvidenceRetriever()
        self.generator = ConstrainedTriageGenerator(
            llm_client=config.get("llm_client"),
            output_schema=config.get("output_schema")
        )
        self.audit_manager = AuditManager(config.get("db_url", "sqlite:///triage_audit.db"))
        
        # 加载证据库
        self._load_evidence_base()
        
        # 组件版本信息(用于审计)
        self.component_versions = {
            "platform": "1.0.0",
            "rule_set": config.get("rule_set_version", "v1.0"),
            "evidence_base": config.get("evidence_version", "ESI-v5-2023-03"),
            "model": config.get("model_version", "gpt-4-2024-01")
        }
    
    def triage(self, input_data: Dict, user_id: Optional[str] = None) -> Dict:
        """执行完整的可审计分诊流程"""
        try:
            # 1. 数据验证与契约执行
            validated_input = self.data_validator.validate_input(input_data)
            
            # 2. 应用安全护栏(前置规则)
            rule_hits = self.safety_guardrail.apply_all_rules(
                validated_input, 
                validated_input.get("vital_signs", {})
            )
            
            # 3. 检索版本化证据
            evidence_results = []
            for query in self._generate_evidence_queries(validated_input):
                evidence = self.evidence_retriever.retrieve(
                    query=query,
                    version=self.component_versions["evidence_base"],
                    top_k=3
                )
                evidence_results.extend(evidence)
            
            # 4. 生成带约束的建议
            uncertainty_flags = self._detect_uncertainty(validated_input, rule_hits)
            
            output = self.generator.generate_with_constraints(
                patient_data=validated_input,
                evidence=evidence_results,
                rule_hits=rule_hits,
                uncertainty_flags=uncertainty_flags
            )
            
            # 5. 格式化和添加引用
            citations = [
                self.evidence_retriever.get_citation_format(chunk, f"相关性: {score:.3f}")
                for chunk, score in evidence_results[:5]  # 只引用前5个最相关的
            ]
            output["citations"] = citations
            output["rule_hits"] = [asdict(r) for r in rule_hits]
            
            # 6. 记录审计日志
            trace_id = self.audit_manager.log_decision(
                input_data=validated_input,
                output=output,
                rule_hits=rule_hits,
                citations=citations,
                component_versions=self.component_versions,
                user_id=user_id
            )
            
            # 7. 返回结果(已包含trace_id)
            return {
                "success": True,
                "trace_id": trace_id,
                "triage_output": output,
                "timestamps": {
                    "received": validated_input.get("timestamp"),
                    "completed": datetime.now().isoformat()
                }
            }
            
        except Exception as e:
            self.logger.error(f"分诊流程失败: {str(e)}", exc_info=True)
            
            # 返回安全失败响应
            return {
                "success": False,
                "error": str(e),
                "trace_id": None,
                "triage_output": {
                    "esi_level_suggestion": 2,  # 保守建议:高危
                    "escalation_recommendation": True,
                    "questions": ["系统错误,请人工复核"],
                    "explanation": f"系统处理失败: {str(e)}",
                    "citations": [],
                    "rule_hits": [],
                    "uncertainty": ["系统错误"]
                },
                "fallback_mode": True
            }
    
    def replay(self, trace_id: str) -> Dict:
        """回放指定决策的完整过程"""
        return self.audit_manager.replay_decision(trace_id)
    
    def submit_feedback(self, trace_id: str, feedback: Dict) -> bool:
        """提交用户反馈/修正"""
        return self.audit_manager.add_feedback(trace_id, feedback)

# 使用示例
if __name__ == "__main__":
    # 配置平台
    config = {
        "rule_set": RULE_SET_CONFIG,
        "rule_set_version": "v1.2",
        "evidence_version": "ESI-v5-2023-03",
        "model_version": "gpt-4",
        "llm_client": openai.Client(api_key="your-api-key"),
        "db_url": "sqlite:///triage_audit.db"
    }
    
    # 创建平台实例
    platform = AuditableTriagePlatform(config)
    
    # 模拟分诊输入
    sample_input = {
        "patient_id": "P12345",
        "visit_id": "V67890",
        "chief_complaint": {
            "free_text": "胸痛伴呼吸困难2小时,出汗明显"
        },
        "vital_signs": {
            "heart_rate": 145,
            "systolic_bp": 88,
            "respiratory_rate": 32,
            "spo2": 89
        },
        "demographics": {
            "age": 65,
            "gender": "男"
        }
    }
    
    # 执行分诊
    result = platform.triage(sample_input, user_id="clinician_001")
    
    if result["success"]:
        print(f"分诊完成!追踪ID: {result['trace_id']}")
        print(f"ESI建议等级: {result['triage_output']['esi_level_suggestion']}")
        print(f"是否建议升级: {result['triage_output']['escalation_recommendation']}")
        print(f"解释: {result['triage_output']['explanation']}")
        
        # 后续可以回放决策
        replay_data = platform.replay(result['trace_id'])
        print(f"\n回放数据可用,包含 {len(replay_data['evidence_citations'])} 条证据引用")
        
        # 提交反馈示例
        feedback = {
            "clinician_id": "dr_smith",
            "corrected_esi_level": 2,
            "comments": "同意建议,已收治入院",
            "timestamp": datetime.now().isoformat()
        }
        platform.submit_feedback(result['trace_id'], feedback)
    else:
        print(f"分诊失败: {result['error']}")

运行环境要求

txt 复制代码
python>=3.8
pydantic>=2.0
openai>=1.0
sentence-transformers>=2.2
faiss-cpu>=1.7
sqlalchemy>=2.0
scikit-learn>=1.3
pandas>=2.0
fastapi>=0.100  # 如需Web服务
uvicorn>=0.23   # 如需Web服务

部署建议

  1. 临床数据契约层应作为API的第一道防线
  2. 规则引擎需要定期更新以符合最新临床指南
  3. 证据库版本应与机构使用的ESI手册版本保持一致
  4. 审计数据库应定期备份并设置访问权限控制
  5. 生产环境应添加限流、监控和告警机制

致谢 :本研究基于MIT许可的开源组件构建,感谢所有相关开源项目的贡献者。

相关推荐
彩妙不是菜喵2 小时前
c++:初阶/初始模版
开发语言·c++
专注于大数据技术栈2 小时前
java学习--LinkedList
java·开发语言·学习
weixin199701080162 小时前
安家 GO item_get - 获取安家详情数据接口对接全攻略:从入门到精通
java·大数据·python·golang
Wzx1980122 小时前
自研开发的前后端项目部署流程
vue.js·python
凌~风2 小时前
014-计算机操作系统实验报告之C 程序的编写!
c语言·开发语言·实验报告
洛豳枭薰2 小时前
Java常用开发工具
java·开发语言·python
爱蛙科技2 小时前
如何精准选择太阳光模拟器?
安全
NewCarRen2 小时前
汽车安全威胁分析与风险评估技术及缓解方法
网络·安全·web安全
西红市杰出青年2 小时前
crawl4ai------AsyncPlaywrightCrawlerStrategy使用教程
开发语言·python·架构·正则表达式·pandas