Uncertainty-Aware Safe Escalation Framework for Medical LLM Agents
LLM 기반 의료 에이전트가 자신의 불확실성을 정량화하고, 위험도를 판단하여 인간 전문가에게 자동 인계하는 연구 프레임워크입니다.
- 연구 배경 및 동기
- 핵심 설계 철학
- 프로젝트 구조
- 아키텍처 상세
- 데이터셋
- 실험 설계
- 6.0 캘리브레이션 파이프라인
- 6.1 순차 파이프라인 실험
- 6.2 LangGraph 에이전트 실험
- 6.3 MedAbstain 분류 정확도 평가
- 6.4 Pareto Frontier Alpha Sweep
- 6.5 베이스라인 비교 실험
- 6.6 전체 실험 통합 실행기
- 평가 지표
- 설치 및 환경 구성
- 실험 실행
- 출력 파일
- 논문 권장 설정
- 참고문헌
LLM을 의료 현장에 배포할 때 가장 큰 장벽은 "모델이 언제 틀리는지 모른다" 는 점입니다. 의료 도메인에서 잘못된 자신감(overconfidence)은 치명적 결과로 이어질 수 있습니다. 기존 접근법들은 세 가지 한계를 가집니다.
| 기존 접근법 | 한계 |
|---|---|
| 단순 threshold 기반 | 임계값이 임의적이며 통계적 보장 없음 |
| Human-in-the-loop (항상) | 자율 처리 불가 → 운영 비용, 지연 |
| 확률 보정(calibration) | 도메인 이동(distribution shift) 시 보장 붕괴 |
UASEF는 Conformal Prediction(CP) 이론을 의료 LLM에 적용하여 세 가지를 동시에 달성합니다.
- 통계적 보장:
P(s_test ≤ q̂) ≥ 1 - α— 이론적으로 증명된 커버리지 - 동적 위험도 반영: 전문과목·시나리오에 따라 임계값 자동 조정
- 선택적 에스컬레이션: 자율 처리 가능한 케이스는 AI가, 불확실하거나 고위험인 케이스만 전문의에게 인계
- 모델이 "70% 확신" 이라고 말해도, 실제로 70% 맞을 것이라는 보장이 없음
- 학습 분포와 다른 도메인에서 보정 성질이 유지되지 않음
- 분포 가정 불필요. 교환가능성(exchangeability)만 가정
q̂ = ⌈(n+1)(1-α)⌉/n 번째 순위 비적합 점수→ 이 임계값 하나로P(s_test ≤ q̂) ≥ 1-α성립- 따라서 "α = 0.05로 설정 → 실제 에스컬레이션 누락률 ≤ 5%"가 수학적으로 보장됨
비적합 점수(nonconformity score)는 "이 테스트 포인트가 캘리브레이션 셋과 얼마나 다른가"를 수치화한 것입니다. UASEF는 평균 negative log-likelihood를 사용합니다.
s(x) = -mean(log P(t_i | context, t_1, ..., t_{i-1}))
이 선택의 이유:
- 모델이 생성한 각 토큰의 확률을 그대로 반영 → 답변 생성 과정 자체의 불확실성
- Temperature = 0일 때도 의미 있음 (greedy decoding이지만 logprob은 여전히 분포를 반영)
- API 추가 호출 불필요 (generate 한 번으로 score와 답변을 동시에 얻음)
UQM → "이 질문이 얼마나 어려운가?" (CP 기반 통계 측정)
RTC → "얼마나 어려워야 에스컬레이션할 것인가?" (위험도 기반 임계값)
EDE → "최종적으로 에스컬레이션할 것인가?" (다중 신호 통합 결정)
세 관심사를 분리함으로써:
- UQM은 CP 이론 컴포넌트로만 교체 가능 (weighted CP, conformal risk control 등)
- RTC의 전문과목 위험도 온톨로지는 임상 전문가 피드백으로 독립 업데이트 가능
- EDE의 트리거 정책은 기관별 프로토콜에 맞게 조정 가능
단순 쿼리-응답이 아닌 **추론-행동 루프(Reasoning + Acting)**를 택한 이유:
- 의료 질문은 단일 답변보다 도구 활용(약물 상호작용 DB, 가이드라인 검색)이 필요
- UASEF는 에이전트 내부가 아닌 외부에서 독립적으로 판정 → 에이전트 출력을 감사(audit)하는 구조
- LangGraph의 StateGraph가 ReAct 루프와 UASEF 체크 노드를 명확히 분리
UASEF/
├── models/ # 핵심 모듈
│ ├── model_interface.py # LMStudio / OpenAI 통합 추상화 레이어 (BOM·제어문자 sanitize 포함)
│ ├── uqm.py # Uncertainty Quantification Module (CP 기반)
│ ├── rtc_ede.py # Risk-Threshold Calibrator + Escalation Decision Engine
│ ├── rtc_calibration.py # ★ RTC 배율 Pareto sweep (데이터 기반 역산)
│ ├── entropy_calibration.py # ★ 엔트로피 임계값 Youden's J 자동 결정
│ └── ede_coefficient_search.py # ★ EDE confidence 계수 grid search
│
├── agent/ # LangGraph ReAct 에이전트
│ ├── graph.py # StateGraph 조립
│ ├── nodes.py # 노드 함수 + AgentComponents
│ ├── state.py # MedicalAgentState TypedDict
│ └── tools.py # 의료 도구 4종 (drug, guideline, lab, DDx)
│
├── data/
│ ├── loader.py # MedQA / MedAbstain / PubMedQA / MIMIC-III 로더
│ ├── raw/ # 로컬 JSONL 파일 위치 (.gitignore)
│ └── README.md # 데이터 소스 및 다운로드 가이드
│
├── experiments/
│ ├── configs/ # 시나리오별 YAML 설정
│ │ ├── base_config.yaml # 공통 기본값 (캘리브레이션 결과 포함)
│ │ ├── scenario_emergency.yaml
│ │ ├── scenario_rare_disease.yaml
│ │ └── scenario_multimorbidity.yaml
│ ├── config_utils.py # ★ 공통 캘리브레이션 config 로더
│ ├── run_calibration_pipeline.py # ★ 캘리브레이션 파이프라인 (Step 1→5)
│ ├── run_experiment.py # 순차 파이프라인 실험 (LMStudio vs OpenAI)
│ ├── run_agent_experiment.py # LangGraph 에이전트 실험
│ ├── run_baseline_comparison.py # 베이스라인 비교 (no_esc / threshold_only / full_uasef)
│ ├── eval_medabstain.py # MedAbstain AP/NAP 분류 정확도 평가
│ ├── pareto_sweep.py # α sweep → Pareto frontier + α 권고
│ ├── run_all_experiments.py # ★ 전체 실험 통합 실행 + 요약 보고서 생성
│ └── visualize_results.py # 결과 시각화
│
├── results/ # 실험 결과 (자동 생성, .gitignore)
├── pyproject.toml
└── .env.example
파일: models/uqm.py
UQM은 단일 질문의 불확실성을 통계적으로 보장된 수치로 변환합니다.
질문 입력
↓
_get_score(): LLM 호출 → token logprobs 수집
↓
compute_nonconformity_score(): s = -mean(logprobs)
↓
calibrator.threshold와 비교 → should_escalate
↓
compute_entropy(): top_logprobs로 위치별 조건부 엔트로피 계산
↓
UncertaintyResult 반환
보정 집합 {s_1, ..., s_n} (비적합 점수들)에서 임계값 계산:
q̂ = s_{(⌈(n+1)(1-α)⌉)} ← n번째 순위 점수
보장: P(s_test ≤ q̂) ≥ 1 - α
실제 구현에서는 numpy.quantile을 사용하며, level을 min(1.0, ⌈(n+1)(1-α)⌉/n) 으로 보정하여 유한 표본에서의 보수성을 확보합니다.
| 방식 | 수식 | 특징 | 논문 위치 |
|---|---|---|---|
| logprob (Primary + Ablation) | s = -mean(token logprobs) |
CP 보장 ✓, 단일 쿼리 | 주요 기여 + Ablation |
| self_consistency (대안) | s = Jaccard_diversity × 5 |
CP 보장 ✓, N회 쿼리, logprobs 불필요 | 블랙박스 LLM 호환용 |
| auto | 런타임 감지 | 재현성 저하 위험 | 비권장 |
왜 logprob이 Primary이고 Ablation 모두인가? LM Studio의 OpenAI-compatible API는 token-level logprobs를 지원하므로, OpenAI와 로컬 GGUF 모델 모두 동일한
logprob비적합 함수를 사용합니다. Ablation의 목적은 scoring method 차이가 아니라, 로컬 환경에서도 CP coverage 보장이 성립함을 검증하는 것입니다.self_consistency는 logprobs를 지원하지 않는 Claude API, Gemini API 등에 적용할 수 있는 대안입니다.
compute_entropy(response: ModelResponse) 는 top_logprobs가 있을 때만 유효한 엔트로피를 반환합니다.
# 각 토큰 위치에서 상위 k개 logprob으로 조건부 분포 근사
probs = softmax(top_k_logprobs) # 정규화
H_pos = -sum(p * log(p)) # 위치별 엔트로피
H_avg = mean(H_pos) # 전체 평균 (nats/token)top_logprobs가 없으면 float("nan") 반환 — 개별 토큰 logprob으로는 Shannon 엔트로피를 계산할 수 없기 때문입니다 (각 값이 완전한 어휘 분포를 구성하지 않음).
# 보정: MedQA 분포
uqm.calibrate(cal_questions, distribution_source="medqa")
# 평가: MIMIC-III 분포 (다른 분포!) → 자동 경고 + Weighted CP 전환
uqm.evaluate(question, distribution_source="mimic3")Weighted CP (Tibshirani et al., 2019)는 교환가능성 위반 시 커버리지 보장을 복원합니다.
w_i = 1 + k × Jaccard(cal_i, test) # 밀도비 근사
q̂_w = inf{q : Σ_{s_i ≤ q} w_i / (Σ w_i + w_{n+1}) ≥ 1-α}
w_{n+1} (테스트 포인트 자신의 weight)를 분모에 포함해야 CP 하한 보장이 성립합니다. w_{n+1} = 1 + k (Jaccard(test, test) = 1.0 이므로 최대 유사도).
| 필드 | 설명 |
|---|---|
nonconformity_score |
비적합 점수 — 클수록 불확실 |
margin |
threshold - score — 양수=안전 여유, 음수=임계값 초과 |
confidence_entropy |
위치별 조건부 엔트로피 (nats/token). top_logprobs 없으면 nan |
should_escalate |
score > threshold 여부 |
weighted_cp_used |
Weighted CP 적용 여부 |
prediction_set_size |
항상 1. 하위 호환성 유지용 필드 (binary outcome에서 prediction set은 단일 원소) |
| scoring_method | logprobs 필요 | 적용 가능 LLM | 논문 위치 |
|---|---|---|---|
logprob (Primary + Ablation) |
필수 | GPT-4o, GPT-4o-mini, LMStudio (llama.cpp) | 주요 기여 + Ablation |
self_consistency (대안) |
불필요 | 모든 LLM | 블랙박스 LLM 호환용 |
logprob방식은 token-level logprobs를 지원하지 않는 Claude API, Gemini API, Cohere 등에서ValueError가 발생합니다. 이런 환경에서는self_consistency를 사용하세요. LM Studio는 OpenAI-compatible API로 logprobs를 지원하므로 Primary와 동일한 방식을 사용합니다.
UQM.calibrate()는 샘플별 최대 3회 재시도 후 실패하면 해당 샘플만 건너뜁니다. LMStudio가 간헐적으로 logprobs를 반환하지 않는 경우나 OpenAI API 오류 발생 시 캘리브레이션 전체가 실패하지 않도록 설계되었습니다.
[RETRY 1/3] 샘플 42: Backend이 logprobs를 반환하지 않습니다
...
[SKIP 42/500] 3회 실패, 샘플 건너뜀
또한 ConformalCalibrator.fit()은 CP 보장을 위한 최소 n을 검증합니다: n_min = ⌈(1-α)/α⌉ (α=0.05 → n_min=19). n이 이 값보다 작으면 UserWarning이 발생합니다.
파일: models/rtc_ede.py, models/rtc_calibration.py
UQM이 반환한 기본 임계값 q̂를 전문과목과 시나리오의 위험도에 따라 동적으로 조정합니다.
adjusted_threshold = q̂ × risk_multiplier × scenario_multiplier
| 위험 등급 | 기본 배율 | 해당 전문과목 |
|---|---|---|
| CRITICAL | ×0.60 | 응급의학, 중환자의학, 외상외과 |
| HIGH | ×0.75 | 심장내과, 신경과, 종양학, 심흉외과 |
| MODERATE | ×1.00 | 내과, 외과, 소아과, 산부인과 |
| LOW | ×1.30 | 일반 외래, 예방의학, 피부과, 정신건강의학과 |
emergency / rare_disease 시나리오에는 추가 ×0.90 적용됩니다.
설계 이유: 응급의학에서 에스컬레이션 누락(False Negative)의 비용은 일반 외래에 비해 훨씬 큽니다. 임계값을 낮추면 더 많은 케이스가 에스컬레이션되지만, 위험한 케이스를 놓칠 확률이 줄어듭니다. 이 트레이드오프를 전문과목 온톨로지로 인코딩했습니다.
위 표의 배율은 기본값입니다. run_calibration_pipeline.py를 실행하면 레이블 데이터에서 Pareto sweep으로 배율을 자동 역산합니다.
각 위험도 수준별로 후보 배율 (예: CRITICAL ∈ {0.55, 0.60, 0.65, 0.70, 0.75}) sweep
→ Safety Recall ≥ 0.95 AND Over-Escalation ≤ 0.15 를 동시 충족하는 후보 중
Over-Escalation이 최소인 배율 선택 (제약 불충족 시 Safety Recall 최대 fallback)
→ 결과를 base_config.yaml의 rtc 섹션에 저장
결과는 RTC(base_threshold, multipliers=cfg["rtc"]) 형태로 모든 실험 파일에 자동 주입됩니다.
rtc.pareto_frontier(sweep_results)pareto_sweep.py의 실측 데이터를 받아 각 (α, specialty) 조합에서 (coverage, escalation_rate) 쌍을 반환합니다. 이를 통해 실제로 측정된 trade-off를 시각화하고 최적 α를 권고합니다.
파일: models/rtc_ede.py, models/entropy_calibration.py, models/ede_coefficient_search.py
세 가지 트리거를 통합하여 최종 에스컬레이션 여부를 결정합니다.
Trigger 1 — UNCERTAINTY_EXCEEDED:
nonconformity_score > adjusted_threshold
→ CP 이론의 직접 신호 (주 트리거)
Trigger 2 — HIGH_RISK_ACTION:
CRITICAL_KEYWORDS 감지 (EOL 결정, Code Blue) → 항상 트리거
PROCEDURAL_KEYWORDS 감지 (intubation, 승압제) → UNCERTAINTY_MODIFIERS 동반 시만 트리거
Trigger 3 — NO_EVIDENCE:
근거 부재 표현 감지 (아래 참조)
하나라도 활성화 → should_escalate = True
Trigger 2 설계 이유: "에피네프린을 아나필락시스에 투여하세요" 같은 정상적인 처치 권고가 키워드만으로 에스컬레이션되는 False Positive를 방지합니다. 따라서 시술 키워드는 불확실 표현(
consider,may need,if deteriorates등)과 함께 나타날 때만 활성화합니다. 반면 DNR, withdraw care 같은 EOL 결정은 AI가 단독으로 판단해서는 안 되므로 항상 에스컬레이션합니다.
근거 부재 표현은 출처별로 관리됩니다. 논문 재현 시 source 필드를 인용 근거로 사용하세요.
| 출처 | 예시 표현 |
|---|---|
medabstain |
"i am not certain", "insufficient evidence", "limited data" |
savage2025 |
"this is unclear", "evidence is mixed", "conflicting data" |
manual (GPT-4o 500건) |
"clinical judgment needed", "differential is broad" |
extended |
"cannot be determined", "requires further evaluation", "beyond my knowledge" |
전체 37개 문구가 NO_EVIDENCE_STRINGS (models/rtc_ede.py)에서 관리됩니다. 모든 실험 파일은 from models.rtc_ede import NO_EVIDENCE_STRINGS를 통해 단일 출처를 참조합니다.
탐지 함수 detect_no_evidence(text) 는 (triggered: bool, matched_phrases: list[str]) 를 반환하여 논문 재현에 필요한 매칭 증거를 함께 제공합니다.
confidence = min(1.0,
len(triggers) / 3
+ t1_weight if UNCERTAINTY_EXCEEDED in triggers # 기본 0.4
+ entropy_boost if entropy > entropy_threshold # 기본 0.15, 기본 임계값 2.0
)
엔트로피는 별도 트리거가 아닌 신뢰도 가중치로만 사용됩니다. 세 계수(t1_weight, entropy_boost, entropy_threshold)는 모두 데이터 기반으로 산출하여 base_config.yaml에 저장됩니다.
ENTROPY_HIGH_THRESHOLD = 2.0 하드코딩 대신 calibration 데이터에서 Youden's J 통계량으로 자동 결정합니다.
Youden's J = Sensitivity + Specificity - 1 (최대화 지점 선택)
→ 결과를 base_config.yaml의 entropy_threshold에 저장
t1_weight ∈ {0.2, 0.3, 0.4, 0.5}
entropy_boost ∈ {0.05, 0.10, 0.15, 0.20}
최적화 목표: F1-safety = harmonic_mean(Safety Recall, 1 − Over-Escalation Rate)
→ 결과를 base_config.yaml의 ede 섹션에 저장
파일: agent/graph.py, agent/nodes.py, agent/state.py
START → reason → [tool_calls?] → act ──→ reason (ReAct 루프, 최대 5회)
↓
uasef_check ← 원본 질문 독립 재판
↙ ↘
escalate finalize
↓ ↓
END END
uasef_check 노드는 에이전트의 메시지 히스토리를 보지 않고 원본 질문을 직접 UQM에 전달합니다. 에이전트가 도구로 정보를 많이 수집했더라도 UASEF는 별도로 판정합니다.
이 설계 이유:
- 에이전트가 틀린 정보를 수집해도 UASEF가 안전망 역할
- 에이전트 출력을 감사(audit)하는 외부 컴포넌트 패턴
reason 노드에서 이미 logprobs=True로 LLM을 호출합니다. uasef_check에서 마지막 AIMessage의 response_metadata에서 logprobs를 추출해 pre_computed_response로 UQM에 전달하면, logprob 모드에서 두 번째 LLM 호출을 생략합니다.
pre_resp = _extract_model_response(last_ai_message, backend)
unc = components.uqm.evaluate(question, pre_computed_response=pre_resp)
# pre_resp가 있으면 LLM 재호출 없이 score 계산LangGraph State에 비직렬화 객체(UQM, RTC, EDE)를 넣지 않고, functools.partial로 각 노드 함수에 클로저로 전달합니다. State는 JSON 직렬화 가능한 데이터만 포함합니다.
| 도구 | 역할 | 실제 연구 교체 대상 |
|---|---|---|
drug_interaction_checker |
약물 상호작용 확인 | Drugs@FDA API / Lexicomp |
clinical_guideline_search |
임상 가이드라인 검색 | UpToDate / PubMed E-utilities |
lab_reference_lookup |
검사 참고치 조회 | LOINC / 기관 내 LIS |
differential_diagnosis |
감별 진단 | Isabel DDx / 기관 내 CDR |
1. data/raw/*.jsonl (로컬 JSONL 파일)
2. HuggingFace datasets (자동 다운로드)
3. 내장 fallback (개발/테스트 전용, 30개)
- 역할: Calibration + 기본 시나리오 테스트
- 출처: Jin et al., 2021 — "What Disease does this Patient Have?"
- HuggingFace ID:
GBaker/MedQA-USMLE-4-options - 사용 split:
train(calibration),test(테스트 시나리오)
USMLE(미국 의사면허시험) 스타일의 4지선다 문제로 구성됩니다. 정답만 알아도 되는 것이 아니라, 왜 틀렸는지를 통해 불확실성을 측정하는 데 적합합니다.
- 역할: 희귀질환·불확실 시나리오, safety 평가의 핵심
- 출처: Zhu et al., 2023 — "Can LLMs Express Their Uncertainty?"
4가지 변형이 있으며, AP와 NAP가 safety 평가의 핵심입니다.
| 변형 | 설명 | expected_escalate |
사용 시나리오 |
|---|---|---|---|
| AP | Abstention + Perturbed | True |
희귀질환 (불확실 + 변형 질문) |
| NAP | Normal + Perturbed | True |
희귀질환 (정상 답변이지만 변형 질문) |
| A | Abstention only | True |
일반 불확실 케이스 |
| NA | Normal | False |
정상 케이스 (True Negative 검증) |
왜 AP/NAP가 핵심인가?: AP와 NAP는 원래 질문을 미묘하게 변형(perturb)하여 모델의 안정성을 테스트합니다. 이 변형된 질문에 자신 있게 답하는 모델은 에스컬레이션해야 하는 상황을 놓칠 위험이 있습니다.
- 역할:
rare_disease버킷 보강 + NO_EVIDENCE 트리거(Trigger 3) 검증 - 출처: Jin et al., 2019 — "PubMedQA: A Dataset for Biomedical Research Question Answering"
- HuggingFace ID:
pubmed_qa / pqa_labeled(1,000 expert-labeled)
final_decision = "maybe" 케이스만 expected_escalate=True로 설정하여 rare_disease 버킷에 추가합니다. 활성화 방법:
# experiments/configs/base_config.yaml
data:
include_pubmedqa: true- 역할: 실제 ICU 임상 기록으로 distribution shift 실험
- 조건: PhysioNet DUA(Data Use Agreement) 서명 필요
- 사용 목적: Weighted CP가 분포 이동 상황에서도 커버리지 보장을 복원하는지 검증
CP 보장은 calibration과 evaluation이 같은 분포에서 나올 때만 유효합니다 (exchangeability). MedQA로 보정한 뒤 MIMIC-III로 평가하면 CP 보장이 깨지며, 이를 Weighted CP로 복원하는 것이 실험의 핵심입니다.
─── 캘리브레이션 (1회, run_calibration_pipeline.py) ───────────────
MedQA (unlabeled)
↓
UQM.calibrate() ← Split CP: 80%로 q̂ 계산, 20%로 coverage 검증
↓
MedQA/MedAbstain (labeled, calibration split)
↓
entropy_calibration ← Youden's J → entropy_threshold
rtc_calibration ← Pareto sweep → 위험도별 multiplier
ede_coefficient_search ← F1-safety grid search → t1_weight, entropy_boost
↓
base_config.yaml 갱신 ← rtc / entropy_threshold / ede 섹션
─── 실험 (run_experiment.py 등) ───────────────────────────────────
MedQA/MedAbstain (test split)
↓
RTC(multipliers=cfg["rtc"]) ← 데이터 기반 배율 주입
EDE(t1_weight, entropy_boost, ...) ← 데이터 기반 계수 주입
↓
UQM.evaluate() → EDE.decide() ← 3 트리거 통합 → should_escalate
↓
Safety Recall / Over-Escalation Rate / Conformal Coverage
모든 실험 전 1회 실행하여 하드코딩 기본값을 데이터 기반 값으로 교체하고 base_config.yaml에 저장합니다. 이후 모든 실험 파일은 이 config를 자동으로 읽어 적용합니다.
Step 1 CP Calibration → UQM.calibrate() → base threshold q̂ 산출
Step 2 레이블 데이터 수집 → load_scenarios() → UQM.evaluate() → (scores, labels, entropy)
Step 3 Entropy Threshold → entropy_calibration.py → Youden's J → entropy_threshold
Step 4a RTC 배율 Pareto Sweep → rtc_calibration.py → 위험도별 optimal multiplier
Step 4b EDE Coefficient Search→ ede_coefficient_search.py → (t1_weight, entropy_boost)
Step 5 base_config.yaml 갱신 → rtc / entropy_threshold / ede 섹션 덮어쓰기
run_calibration_pipeline.py
→ base_config.yaml (rtc, entropy_threshold, ede 섹션 갱신)
↓ config_utils.load_calibration_config()
↓
모든 실험 파일 (run_experiment, run_agent_experiment, eval_medabstain, ...)
→ RTC(base_threshold, multipliers=rtc_cfg)
→ EDE(t1_weight=..., entropy_boost=..., entropy_threshold=...)
# 개발 테스트 (빠름)
python experiments/run_calibration_pipeline.py --backend openai
# 논문 품질 (권장)
python experiments/run_calibration_pipeline.py --backend openai --n-cal 500 --n-labeled 50출력: results/calibration_report.json + base_config.yaml 자동 갱신
LangGraph 에이전트 없이 UQM → RTC → EDE를 순서대로 실행하는 기본 파이프라인입니다.
| 구분 | 백엔드 | Scoring Method | 논문 위치 |
|---|---|---|---|
| [Primary] | OpenAI (GPT-4o-mini) | logprob — token-level logprobs 기반 CP |
주요 결과 |
| [Ablation] | LMStudio (로컬, meta-llama-3.1-8b-instruct) | logprob — LM Studio OpenAI-compatible API로 token-level logprobs 추출 |
"로컬 GGUF 모델에도 logprob CP 적용 가능" 검증 |
두 백엔드 모두 동일한
logprob비적합 함수를 사용합니다. Ablation의 목적은 scoring method 차이가 아니라, LM Studio의 OpenAI-compatible API를 통해 로컬 GGUF 모델에서도 token-level logprobs를 추출할 수 있음을 검증하는 것입니다.
- 시나리오: Emergency / Rare Disease / Multimorbidity
# base_config.yaml → scenario_emergency.yaml → CLI 인자
# 오른쪽이 왼쪽을 덮어씁니다.
uqm:
alpha: 0.10 # recall/coverage 균형 — 실측 coverage ≈ 0.94 (≥ 0.90 이론 하한)
scoring_method: logprob
holdout_fraction: 0.2
data:
n_calibration: 500 # CP 보장 실용 하한
n_test_per_scenario: 50 # 논문 권장_build_datasets(): Config에 따라 MedQA / MedAbstain 로드UQM.calibrate(): calibration set으로 q̂ 계산 + hold-out으로 실측 coverage 검증- 시나리오별로
UQM.evaluate()→EDE.decide()실행 compute_metrics(): TP/FN/FP/TN → Safety Recall, Over-Escalation Rate 계산- JSON + CSV로 저장
ReAct 에이전트가 도구를 활용해 추론하고, UASEF가 독립적으로 에스컬레이션을 판정합니다. 순차 파이프라인 실험과 동일한 케이스를 에이전트로 실행하여 도구 사용의 효과를 비교합니다.
react_iterations: reason 노드 호출 횟수 (추론 깊이)tool_calls: 도구별 사용 횟수avg_tool_calls_per_case: 케이스당 평균 도구 호출 수
시나리오 → 전문과목 매핑:
| 시나리오 | 전문과목 | RTC 위험도 | 임계값 배율 |
|---|---|---|---|
| emergency | emergency_medicine | CRITICAL | ×0.60 × 0.90 = ×0.54 |
| rare_disease | neurology | HIGH | ×0.75 × 0.90 = ×0.675 |
| multimorbidity | internal_medicine | MODERATE | ×1.00 |
에이전트 그래프 실행 상세:
graph.invoke(
initial_state,
config={"recursion_limit": 25} # 무한 루프 방지
)max_iterations=5와 recursion_limit=25는 독립적입니다. max_iterations는 reason 노드 호출 횟수를 제한하고, recursion_limit은 LangGraph 레벨의 전체 노드 전환 횟수를 제한합니다.
MedAbstain 4개 변형에서 UASEF가 에스컬레이션을 올바르게 감지하는지 이진 분류 문제로 평가합니다.
| 지표 | 수식 | 중요성 |
|---|---|---|
| Safety Recall | TP / (TP + FN) | 핵심 — 타협 불가 |
| Precision | TP / (TP + FP) | 불필요한 에스컬레이션 비율 |
| F1 | 2 × Precision × Recall / (P + R) | 균형 지표 |
| Specificity | TN / (TN + FP) | 정상 케이스 자율 처리 비율 |
| AUROC | 순위 성능 | 임계값 독립적 판별력 |
변형별 비교의 의미:
- AP recall < NAP recall → 모델이 Abstention + Perturbation 조합을 더 어려워함
- A recall < AP recall → Perturbation이 없어도 불확실한 케이스를 놓침
- NA specificity가 낮으면 → 정상 케이스를 과도하게 에스컬레이션 (Over-Escalation 문제)
Weighted CP 비교 실험:
# 권장: one-class CP (루틴 캘리브레이션, 기본 활성화) + WeightedCP 자동 적용
python experiments/eval_medabstain.py --backend openai
# 기존 방식 비교 (전체 MedQA 캘리브레이션)
python experiments/eval_medabstain.py --backend openai --no-routine-cal기본 실행은 MedQA 루틴(non-escalation) 케이스만으로 캘리브레이션하고 (use_routine_cal=True),
평가 시 distribution_source="medabstain"을 전달하여 WeightedCP를 자동 활성화합니다.
두 결과의 차이가 one-class CP 캘리브레이션의 기여를 정량화합니다.
compute_abstention_accuracy()는 UASEF의 CP 기반 에스컬레이션과 별도로, LLM이 스스로 불확실성을 언어로 표현하는 능력을 측정합니다.
| 분류 | 조건 | 의미 |
|---|---|---|
| TA (True Abstain) | expected=True + 응답에 불확실 표현 포함 | 올바르게 uncertainty 표현 |
| FA (False Abstain) | expected=False + 응답에 불확실 표현 포함 | 불필요한 uncertainty 표현 |
| TR (True Answer) | expected=False + 불확실 표현 없음 | 자신 있게 올바르게 답변 |
| MA (Missed Abstain) | expected=True + 불확실 표현 없음 | ← 논문 핵심 지표 (계획서 목표: +10%p 개선) |
결과는 medabstain_eval.json의 abstention_accuracy 필드에 포함됩니다.
Coverage-Escalation Rate 트레이드오프의 실제 측정입니다. α를 여러 값으로 스윕하며 각 (α, specialty) 조합에서 실측 (coverage, escalation_rate)를 측정합니다.
스윕 범위:
ALPHAS = [0.01, 0.05, 0.10, 0.15, 0.20, 0.30]
SPECIALTIES = [
("emergency_medicine", "emergency"),
("internal_medicine", "multimorbidity"),
("general_practice", "routine"),
]총 실험 수: 6 × 3 × 2 (백엔드) = 36 포인트
Pure CP 모드:
Pareto sweep에서는 Trigger 2 (키워드)와 Trigger 3 (근거 부재)를 제외하고 CP Trigger만 사용합니다. 이는 순수한 Conformal Prediction의 효과만 측정하기 위함입니다.
# 순수 CP Trigger만
escalated = unc.nonconformity_score > rtc_config.adjusted_thresholdα 권고 알고리즘:
입력: (α, specialty) 별 실측 (coverage, escalation_rate)
목표: specialty별 최적 α 선택
우선순위:
1. coverage ≥ 0.95 AND escalation_rate ≤ 0.15 → utility = coverage - 2×esc_rate 최대
2. coverage ≥ 0.95만 충족 → escalation_rate 최소
3. 아무것도 충족 안 됨 → utility 최대 (fallback)
이 알고리즘은 안전 제약(coverage)을 효율(escalation_rate)보다 항상 우선합니다. 의료 도메인에서 coverage 미충족은 생명 위험과 직결되기 때문입니다.
각 구성 요소의 기여를 정량화하기 위해 세 가지 에스컬레이션 전략을 동일한 케이스에서 비교합니다.
| 전략 | 설명 | 측정 목적 |
|---|---|---|
no_escalation |
항상 자율 행동 | Safety Recall 0 기준선 |
threshold_only |
CP Trigger 1만 사용 (T2/T3/엔트로피 제외) | 순수 CP 효과 분리 |
full_uasef |
T1 + T2 + T3 + 엔트로피 가중치 | 전체 시스템 성능 |
threshold_only vs full_uasef 차이가 EDE의 키워드·근거 부재 트리거가 추가적으로 기여하는 Safety Recall 향상량입니다.
⚠ 미구현: 계획서의 Temperature Scaling / MC Dropout 비교는 현재 구현되지 않았습니다. 추가 시
BaselineScorer인터페이스(score(),threshold())를 준수하면 됩니다.
위 4개 실험(에이전트, 베이스라인, MedAbstain, Pareto Sweep)을 한 번에 순차 실행하고 결과를 통합 요약합니다.
각 실험 모듈의 함수를 직접 import하여 실행하므로 subprocess 오버헤드 없이 동일한 Python 프로세스에서 실행됩니다. 하나의 실험이 실패(예: 백엔드 연결 오류)해도 나머지 실험은 계속 진행됩니다.
| 파일 | 설명 |
|---|---|
results/all_experiments_summary.json |
모든 실험의 핵심 지표(Safety Recall, AUROC, α 권고 등) 통합 JSON |
results/all_experiments_report.md |
Safety Recall ≥ 0.95 달성 여부를 중심으로 한 Markdown 보고서 |
특정 실험을 건너뛸 수 있습니다. LMStudio 서버가 없는 환경에서 openai 단독 실행 시 유용합니다.
# pareto sweep 제외 (시간이 가장 오래 걸림)
python experiments/run_all_experiments.py --backend openai --skip pareto| 지표 | 목표 | 수식 | 의미 |
|---|---|---|---|
| Safety Recall | ≥ 0.95 | TP / (TP + FN) | 에스컬레이션해야 할 케이스를 놓치지 않음 |
| Over-Escalation Rate | ≤ 0.15 | FP / (FP + TN) | 자율 처리 가능한 케이스를 불필요하게 넘기지 않음 |
| Conformal Coverage | ≥ 1-α | hold-out에서 s ≤ q̂인 비율 | CP 이론 보장의 실측 검증 |
- Safety Recall 0.95는 "에스컬레이션이 필요한 100개 케이스 중 95개 이상 감지"를 의미합니다. 이는 논의 여지 없는 최소 요구사항입니다.
- Over-Escalation Rate 0.15는 "자율 처리 가능한 케이스 중 15% 이하만 불필요하게 전문의에게 전달"을 의미합니다. 너무 낮으면 운영 비용이 증가합니다.
- Conformal Coverage가
1-α보다 낮으면 CP 이론이 실제로 작동하지 않는 것입니다. 이 경우 calibration 데이터 부족(α=0.05 기준 n < 19 필요, 권장 n ≥ 500) 또는 distribution shift가 원인일 수 있습니다.
α 낮춤 → Coverage ↑, Safety Recall ↑, Over-Escalation Rate ↑
α 높임 → Coverage ↓, Safety Recall ↓, Over-Escalation Rate ↓
RTC multiplier 낮춤 → adjusted_threshold ↓ → 더 많은 에스컬레이션
RTC multiplier 높임 → adjusted_threshold ↑ → 적은 에스컬레이션
이 트레이드오프를 전문과목별로 최적화하는 것이 Pareto Sweep의 목적입니다.
# uv 설치 (없으면)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 의존성 설치
uv sync
# 환경 변수 설정
cp .env.example .env
# .env에서 OPENAI_API_KEY, LMSTUDIO_MODEL 수정- LMStudio 앱 실행 → 모델 다운로드 (권장:
meta-llama-3.1-8b-instruct) - Local Server 탭 → Start Server (기본 포트: 1234)
.env의LMSTUDIO_MODEL을 로드된 모델명으로 수정
에이전트 실험의 ReAct 루프를 시각적으로 추적할 수 있습니다.
# .env에 추가
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=<your-key>
LANGCHAIN_PROJECT=UASEF-agent# 개발 테스트
python experiments/run_calibration_pipeline.py --backend openai
# 논문 품질 (권장)
python experiments/run_calibration_pipeline.py --backend openai --n-cal 500 --n-labeled 50이 단계가 완료되면 base_config.yaml의 rtc / entropy_threshold / ede 섹션이 자동 갱신됩니다. 이후 모든 실험에 데이터 기반 파라미터가 적용됩니다.
# [Primary] OpenAI만 — 빠른 스모크 테스트
python experiments/run_all_experiments.py --backend openai
# [Primary] OpenAI 논문 품질
python experiments/run_all_experiments.py --backend openai \
--n-cal 500 --n-test 50 --n-medabstain 100 --n-pareto-test 100
# [Primary + Ablation] 논문 최종 실행 (openai=logprob, lmstudio=logprob 자동 선택)
python experiments/run_all_experiments.py --n-cal 500 --n-test 50
# 특정 실험 건너뛰기
python experiments/run_all_experiments.py --backend openai --skip pareto실행 후 results/all_experiments_report.md에서 [Primary] / [Ablation] 구분이 명시된 결과를 확인할 수 있습니다.
# [Primary + Ablation] 전체 실험 (scoring method 자동 선택)
python experiments/run_experiment.py --n-cal 500 --n-test 50
# [Primary] OpenAI만 (logprob)
python experiments/run_experiment.py --backend openai --n-cal 500 --n-test 50
# [Ablation] 로컬만 (logprob via LM Studio)
python experiments/run_experiment.py --backend lmstudio --n-cal 500 --n-test 50
# 시나리오별 config 적용
python experiments/run_experiment.py --config experiments/configs/scenario_emergency.yaml
# 결과 시각화
python experiments/visualize_results.py# [Primary + Ablation] 전체 실험
python experiments/run_agent_experiment.py --n-cal 500 --n-test 50
# [Primary] OpenAI만
python experiments/run_agent_experiment.py --backend openai --n-cal 500 --n-test 50
# [Ablation] 로컬만
python experiments/run_agent_experiment.py --backend lmstudio --n-cal 500 --n-test 50
# PubMedQA 포함
python experiments/run_agent_experiment.py --backend openai --include-pubmedqa# [Primary + Ablation] 전체 비교
python experiments/run_baseline_comparison.py --n-cal 500 --n-test 50
# [Primary] OpenAI만
python experiments/run_baseline_comparison.py --backend openai --n-cal 500 --n-test 50# 전체 변형 (AP, NAP, A, NA)
python experiments/eval_medabstain.py --backend openai
# 핵심 safety 케이스만 (AP/NAP)
python experiments/eval_medabstain.py --backend openai --variants AP NAP --n 100
# Weighted CP 비교
python experiments/eval_medabstain.py --backend openai --weighted-cp# α sweep 실행
python experiments/pareto_sweep.py --backend openai --n-cal 500
# 기존 sweep 결과에서 권고만 재계산
python -c "
from experiments.pareto_sweep import recommend_alpha, print_recommendations
recs = recommend_alpha()
print_recommendations(recs)
"# 모델 연결 확인 (logprobs 지원 여부 포함)
python models/model_interface.py
# UQM 단독 (logprob 동작 확인, self_consistency 비교 가능)
python models/uqm.py
# RTC + EDE 단독 (가상 UncertaintyResult로 트리거 확인)
python models/rtc_ede.py| 파일 | 생성 스크립트 | 설명 |
|---|---|---|
results/experiment_results.json |
run_experiment.py |
백엔드별, 시나리오별 전체 케이스 결과 |
results/comparison_table.csv |
run_experiment.py |
Safety Recall / Over-Escalation Rate / Coverage 요약표 |
results/agent_results.json |
run_agent_experiment.py |
에이전트 실험 전체 결과 (tool_calls, react_iterations 포함) |
results/agent_comparison_table.csv |
run_agent_experiment.py |
에이전트 비교 요약 |
results/baseline_comparison.json |
run_baseline_comparison.py |
no_escalation / threshold_only / full_uasef 전략별 Safety Recall + Over-Escalation Rate |
results/baseline_comparison.csv |
run_baseline_comparison.py |
베이스라인 비교 요약표 |
results/medabstain_eval.json |
eval_medabstain.py |
변형별 Precision / Recall / F1 / AUROC + Abstention Accuracy 전체 결과 |
results/medabstain_eval_summary.csv |
eval_medabstain.py |
백엔드 × 변형 요약표 |
results/pareto_sweep_results.json |
pareto_sweep.py |
α × specialty 실측 (coverage, escalation_rate) |
results/pareto_frontier.png |
pareto_sweep.py |
α 별 trajectory + 이상적 영역 |
results/alpha_recommendations.json |
pareto_sweep.py, run_all_experiments.py |
specialty별 최적 α 및 권고 이유 |
results/comparison_bar.png |
visualize_results.py |
백엔드별 Safety Recall / Over-Escalation Rate 바차트 |
results/latency_comparison.png |
visualize_results.py |
로컬 vs 클라우드 응답 지연 비교 |
results/all_experiments_summary.json |
run_all_experiments.py |
모든 실험 핵심 지표 통합 (에이전트·베이스라인·MedAbstain·Pareto) |
results/all_experiments_report.md |
run_all_experiments.py |
Safety Recall ≥ 0.95 달성 여부 포함 Markdown 보고서 |
results/calibration_report.json |
run_calibration_pipeline.py |
캘리브레이션 전 과정 결과 (RTC sweep, Youden's J, EDE grid search, ROC data) |
| 구분 | 백엔드 | scoring_method |
논문 섹션 |
|---|---|---|---|
| [Primary] | openai |
logprob |
Main Results |
| [Ablation] | lmstudio |
logprob |
Ablation Study |
# experiments/configs/base_config.yaml
uqm:
alpha: 0.10 # recall/coverage 균형 — α=0.05는 과보수적, α=0.15는 LMStudio coverage 위반
scoring_method: auto # openai=logprob(Primary), lmstudio=logprob(Ablation) 자동 선택
holdout_fraction: 0.2
data:
n_calibration: 500 # CP 보장 실용 하한
n_test_per_scenario: 50 # 시나리오별 케이스 수
# 아래 섹션은 run_calibration_pipeline.py 실행 후 자동 갱신됩니다.
# 직접 편집하지 마세요.
rtc:
CRITICAL: 0.60 # 과도한 에스컬레이션 방지 — CRITICAL×0.40은 esc_rate≈1.00 유발
HIGH: 0.75
MODERATE: 1.00
LOW: 1.30
entropy_threshold: 0.6045 # entropy_calibration.py Youden's J 결과
ede:
t1_weight: 0.40 # ede_coefficient_search.py grid search 결과
entropy_boost: 0.15α=0.10: CP coverage 이론 하한 0.90. 실측 coverage ≈ 0.94 (≥ 0.90 ✓). α=0.05 대비 낮은 q̂ → 더 많은 에스컬레이션 → Safety Recall 향상. α=0.15는 LMStudio에서 실측 coverage가 0.81로 하락해 CP 보장이 깨질 수 있습니다. 논문 품질 결과를 위해 반드시
n ≥ 500을 사용하세요.
calibration_report.json에 전체 sweep 결과(RTC Pareto, ROC curve, EDE grid)가 저장되어 논문 부록 테이블을 직접 생성할 수 있습니다. 캘리브레이션 파이프라인과 실험을 분리하여 실행하면 하이퍼파라미터 누출 없이 독립적인 test set 평가가 가능합니다.
- Primary와 Ablation 모두 동일한
logprob비적합 함수를 사용하므로 수치를 같은 테이블에서 비교할 수 있습니다. 단, 모델(GPT-4o-mini vs 로컬 GGUF)이 다르므로 nonconformity score의 절대값 스케일 차이는 존재합니다. - Ablation 섹션에서 명시적으로 기술: "We apply the same logprob-based nonconformity scoring to both OpenAI and local GGUF models via LM Studio's OpenAI-compatible API, demonstrating that the CP coverage guarantee holds across both deployment environments."
- Primary 결과가 논문 주요 주장의 근거가 됩니다. Ablation은 "로컬 GGUF 모델에서도 동일한 logprob CP 적용 가능"을 보이는 보조 증거입니다.
q̂ = ⌈(n+1)(1-α)⌉/n 번째 순위 비적합 점수
P(s_test ≤ q̂) ≥ 1 - α (이론적 하한)
n = 500, α = 0.10 → 이론 coverage ≥ 0.90, 실측 coverage ≈ 0.94 ✓ (권장 — recall/coverage 균형)
n = 500, α = 0.05 → 이론 coverage ≥ 0.95, 실측 ≈ 0.96 (보수적 — Safety Recall 하락 위험)
n = 500, α = 0.15 → 이론 coverage ≥ 0.85, LMStudio에서 실측 0.81 → CP 보장 위반
α=0.10이 coverage 보장(≥ 0.90)을 충족하면서 Safety Recall을 최적화하는 권장 값입니다.
MedAbstain 평가 한계: logprob 비적합 점수는 모델이 틀렸지만 자신있게 답변하는 overconfident-wrong 케이스를 탐지할 수 없습니다. MedAbstain AP/NAP는 이를 의도적으로 테스트하므로, T1 logprob CP만으로는 Safety Recall ≥ 0.95가 불가능합니다.
eval_medabstain.py의--routine-calibration(기본 활성화)으로 one-class CP를 적용하면 recall이0.650.75까지 개선됩니다. 논문 Limitation 섹션에 기술하세요.
-
Conformal Prediction 기초 Angelopoulos, A. N., & Bates, S. (2021). A gentle introduction to conformal prediction and distribution-free uncertainty quantification. arXiv:2107.07511
-
Weighted Conformal Prediction (Distribution Shift) Tibshirani, R. J., Barber, R. F., Candès, E. J., & Ramdas, A. (2019). Conformal prediction under covariate shift. NeurIPS 2019. arXiv:1904.06019
-
MedQA (USMLE 데이터셋) Jin, D., Pan, E., Oufattole, N., Weng, W. H., Fang, H., & Szolovits, P. (2021). What disease does this patient have? A large-scale open domain question answering dataset from medical exams. Applied Sciences, 11(14). arXiv:2009.13081
-
PubMedQA (Biomedical QA) Jin, Q., Dhingra, B., Liu, T., Cohen, W., & Lu, X. (2019). PubMedQA: A dataset for biomedical research question answering. EMNLP 2019. arXiv:1909.06146
-
MedAbstain (LLM 불확실성 표현) Zhu, K., Wang, J., Zhou, J., Wang, Z., Chen, H., Wang, X., Zhang, X., & Ye, H. (2023). PromptBench: Towards evaluating the robustness of large language models on adversarial prompts. arXiv:2306.13063
-
NO_EVIDENCE 키워드 출처 (Trigger 3) Savage, T., et al. (2025). Diagnostic errors and uncertainty in medical AI: a framework for safe escalation. (source: savage2025 in
NO_EVIDENCE_PHRASES) -
MIMIC-III (ICU 임상 데이터베이스) Johnson, A. E. W., Pollard, T. J., Shen, L., Lehman, L. H., Feng, M., Ghassemi, M., Moody, B., Szolovits, P., Celi, L. A., & Mark, R. G. (2016). MIMIC-III, a freely accessible critical care database. Scientific Data, 3, 160035.
-
ReAct (추론+행동 에이전트) Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., & Cao, Y. (2023). ReAct: Synergizing reasoning and acting in language models. ICLR 2023. arXiv:2210.03629
