From 286e83a86f03c2e513efaf122c9d36746472397d Mon Sep 17 00:00:00 2001 From: Dae Hyeon Kim Date: Mon, 27 Jul 2026 15:55:02 +0900 Subject: [PATCH] =?UTF-8?q?docs(ai):=20agent=20=EC=8A=A4=ED=8E=99=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=9E=AC=EC=A0=95=EB=A6=BD=20=E2=80=94=20?= =?UTF-8?q?01=E2=80=9311=20canonical,=20=EA=B5=AC=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=9B=90=EB=B3=B8=204=ED=8C=8C=EC=9D=BC=20=ED=8C=8C=EA=B8=B0?= =?UTF-8?q?=20(=EC=82=AC=EC=9A=A9=EC=9E=90=20directive=202026-07-27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps/ai-server/prompts/의 11개 에이전트가 최종 구성. docs/ai/agents를 prompts와 1:1 대응하는 01–11 시리즈로 확정: 01 orchestrator / 02 safety_classifier / 03 dialogue / 04 clinical_slot / 05 input_normalizer / 06 stt / 07 ocr / 08 handoff_generator / 09 evidence_verifier / 10 sentiment_analyzer / 11 domain_inference 파기 4파일(wave-6 2026-07-20 번호 재부여의 구번호 원본, 이후 갱신 없는 stale — 최종 커밋 06-19~07-09): 10_handoff_generator.md, 11_evidence_verifier.md, 13_sentiment_analyzer.md, 14_domain_inference.md. 재번호본 4파일의 번호-재부여 배너에 파기 사실 명기. old→new 매핑 표는 docs/ai/agent_collaboration_f1f5.md(병행 세션 소유, 무수정) 참조. --- docs/ai/agents/08_handoff_generator.md | 2 +- docs/ai/agents/09_evidence_verifier.md | 2 +- docs/ai/agents/10_handoff_generator.md | 234 ----------------------- docs/ai/agents/10_sentiment_analyzer.md | 2 +- docs/ai/agents/11_domain_inference.md | 2 +- docs/ai/agents/11_evidence_verifier.md | 223 ---------------------- docs/ai/agents/13_sentiment_analyzer.md | 167 ---------------- docs/ai/agents/14_domain_inference.md | 242 ------------------------ 8 files changed, 4 insertions(+), 870 deletions(-) delete mode 100644 docs/ai/agents/10_handoff_generator.md delete mode 100644 docs/ai/agents/11_evidence_verifier.md delete mode 100644 docs/ai/agents/13_sentiment_analyzer.md delete mode 100644 docs/ai/agents/14_domain_inference.md diff --git a/docs/ai/agents/08_handoff_generator.md b/docs/ai/agents/08_handoff_generator.md index 637986e..cdc7e0e 100644 --- a/docs/ai/agents/08_handoff_generator.md +++ b/docs/ai/agents/08_handoff_generator.md @@ -1,6 +1,6 @@ # Agent 08: Handoff Report Writer Agent -> **번호 재부여 2026-07-20 (wave-6, 사용자 directive):** 구 Agent 10(`10_handoff_generator.md`). +> **번호 재부여 2026-07-20 (wave-6, 사용자 directive):** 구 Agent 10(`10_handoff_generator.md`). 구번호 원본 파일은 2026-07-27 사용자 directive로 파기 — 스펙 번호 재정립: 01–11 = `apps/ai-server/prompts/` 최종 구성과 1:1. > old→new 전체 매핑은 `docs/ai/agent_collaboration_f1f5.md`의 번호 체계 표를 참조. ## 개요 diff --git a/docs/ai/agents/09_evidence_verifier.md b/docs/ai/agents/09_evidence_verifier.md index df4ebf8..29cdf7e 100644 --- a/docs/ai/agents/09_evidence_verifier.md +++ b/docs/ai/agents/09_evidence_verifier.md @@ -1,6 +1,6 @@ # Agent 09: QA & Consistency Checker Agent -> **번호 재부여 2026-07-20 (wave-6, 사용자 directive):** 구 Agent 11(`11_evidence_verifier.md`). +> **번호 재부여 2026-07-20 (wave-6, 사용자 directive):** 구 Agent 11(`11_evidence_verifier.md`). 구번호 원본 파일은 2026-07-27 사용자 directive로 파기 — 스펙 번호 재정립: 01–11 = `apps/ai-server/prompts/` 최종 구성과 1:1. > old→new 전체 매핑은 `docs/ai/agent_collaboration_f1f5.md`의 번호 체계 표를 참조. ## 개요 diff --git a/docs/ai/agents/10_handoff_generator.md b/docs/ai/agents/10_handoff_generator.md deleted file mode 100644 index 1c9d252..0000000 --- a/docs/ai/agents/10_handoff_generator.md +++ /dev/null @@ -1,234 +0,0 @@ -# Agent 10: Handoff Report Writer Agent - -## 개요 - -| 항목 | 내용 | -|---|---| -| **Agent ID** | `10` | -| **Agent Name** | `HandoffGeneratorAgent` | -| **역할** | 의료진용 사전 문진 handoff report 생성 | -| **LLM Routing** | benchmarked (Primary: Upstage Solar Pro 3 / Secondary: LG K-EXAONE / Fallback: SKT A.X K1) | - -## 목적 - -사전 문진 과정에서 수집된 모든 정보를 종합하여 의료진이 진료에 즉시 활용할 수 있는 구조화된 handoff report를 생성한다. 모든 주장(claim)에는 반드시 evidence 인용을 포함한다. **진단적 단정을 하지 않으며**, **치료 지시를 하지 않는다**. - -## Report 12개 섹션 - -PRD Section 6.3 기준 12-section 구조. 모든 필수 섹션은 반드시 존재해야 하며, 데이터가 없는 경우 "해당 정보 없음"으로 포함한다. - -| 번호 | 섹션명 | 설명 | 필수 여부 | -|---|---|---|---| -| 1 | 사용자 기본정보 | 환자 가명 ID, 나이, 성별, 평가 일시 | 필수 | -| 2 | 입력 source 요약 | 자율 대화, 구조화 문진, STT, OCR, 진료기록, 이전 handoff 등 사용된 source 목록 | 필수 | -| 3 | 현재 주요 호소 | chief_complaint + 주요 증상 표현 | 필수 | -| 4 | 정신건강 영역 후보 | domain_candidates (confidence, evidence 포함). 진단 아님 disclaimer 필수 | 필수 | -| 5 | CTRS 기반 위험도 평가 | CTRS level, 위험 근거, 자살/자해/타해/폭력성/환각/공황/물질사용/기능손상 | 필수 | -| 6 | 시행된 구조화 문진 결과 | 척도명, 점수, severity, 시행 일시, 위험 문항 양성 여부 | 조건부 (설문 시행 시 필수) | -| 7 | 기록 기반 근거 | OCR/진료기록/처방기록에서 추출한 진단명, 처방약, 진료과, 검사 결과 | 조건부 (기록 존재 시 필수) | -| 8 | 대화 기반 근거 | 주요 사용자 발화 + **발화별 sentiment 태그** (SentimentAnalyzer(13) 출력), 반복 표현, 정서 변화 흐름, 기능 손상 표현 (원문 인용) | 필수 | -| 9 | 종단적 상태 변화 | 이전 대비 호전/악화/유지, 새로운 증상, 재발 신호, CTRS 변화 | 조건부 (재진 시 필수, 초진 시 "해당 없음") | -| 10 | AI 판단의 한계 | 진단 아님 고지, OCR/STT 오류 가능성, RAG 근거 제한 가능성, 전문가 검토 필요 | 필수 | -| 11 | 권장 다음 조치 | 자가관리, 재평가, 전문가 상담, 정신건강의학과 상담 고려, 위기지원 안내 | 필수 | -| 12 | Evidence Registry | 모든 evidence_id와 source의 매핑 테이블 | 필수 | - -### Section Completeness Tracking - -출력 스키마에 `section_completeness` 필드를 포함하여 12개 섹션 각각의 포함 여부를 추적한다: - -```json -{ - "section_completeness": { - "section_01_patient_info": true, - "section_02_input_sources": true, - "section_03_chief_complaint": true, - "section_04_domain_candidates": true, - "section_05_risk_assessment": true, - "section_06_survey_results": false, - "section_07_record_evidence": false, - "section_08_dialogue_evidence": true, - "section_09_longitudinal": false, - "section_10_limitations": true, - "section_11_recommendations": true, - "section_12_evidence_registry": true - } -} -``` - -## 입력 - -| 필드 | 타입 | 설명 | -|---|---|---| -| `patient_profile` | `object` | 환자 기본 정보 | -| `session_metadata` | `object` | 세션 정보 (일시, 입력 채널, 소요 시간) | -| `clinical_slots` | `object` | ClinicalSlotAgent 추출 결과 | -| `safety_classification` | `object` | SafetyClassifierAgent 결과 | -| `scale_scores` | `object \| null` | 구조화 척도 점수 (rule-based 계산 결과) | -| `temporal_summary` | `object` | TemporalSummaryAgent 출력 | -| `retrieved_context` | `array` | TemporalRetrieverAgent 검색 결과 | -| `dialogue_transcript` | `array` | 대화 전문 | -| `ocr_results` | `array \| null` | OCR 추출 결과 | - -## 출력 - -```json -{ - "report_id": "handoff_20260618_001", - "session_id": "sess_20260618_001", - "patient_id": "pt_12345", - "generated_at": "2026-06-18T14:36:00+09:00", - "model_used": "upstage-solar-pro-3", - "sections": { - "s01_patient_info": { - "patient_id": "pt_12345", - "age": 34, - "sex": "M", - "visit_type": "재진" - }, - "s02_assessment_datetime": { - "start": "2026-06-18T14:25:00+09:00", - "end": "2026-06-18T14:35:00+09:00", - "duration_minutes": 10 - }, - "s03_input_sources": { - "channels": [ - { "type": "text_chat", "turns": 12, "reliability": "high" }, - { "type": "ocr_document", "count": 1, "avg_confidence": 0.89, "reliability": "medium-high" } - ] - }, - "s04_chief_complaint": { - "summary": "2개월간 지속된 우울감과 불면 [ev_msg_001]", - "evidence": [ - { "id": "ev_msg_001", "source_type": "message", "source_ref": "dialogue_turn_3", "text": "두 달 전부터 계속 우울해요" } - ] - }, - "s05_mental_health_domains": { - "disclaimer": "아래는 수집된 증상을 기반으로 한 관련 영역 후보이며, 진단이 아닙니다.", - "candidates": [ - { "domain": "우울 영역", "basis": "지속적 우울감, 수면장애, 식욕감소, 흥미저하 [ev_msg_002][ev_msg_003][ev_msg_004]" }, - { "domain": "불안 영역", "basis": "입면 곤란 동반 [ev_msg_003]" } - ] - }, - "s06_ctrs_risk": { - "ctrs_level": 4, - "label": "중증/주의", - "risk_level": "low", - "basis": "자살/자해 관련 표현 미감지. 위험 키워드 및 LLM 분류 모두 CTRS 5. 과거 위험 이벤트 없음. [ev_risk_001]", - "evidence": [ - { "id": "ev_risk_001", "source_type": "risk_event", "source_ref": "safety_classifier", "text": "keyword: no match, llm: CTRS 5 (conf 0.92)" } - ] - }, - "s07_structured_scales": { - "scales_administered": [ - { "scale": "PHQ-9", "score": 12, "severity": "moderate", "method": "rule-based", "evidence_id": "ev_scale_001" } - ] - }, - "s08_record_based_evidence": { - "items": [ - { "id": "ev_ocr_001", "source_type": "document_block", "source_ref": "ocr_prescription_001", "content": "에스시탈로프람 10mg 1일 1회 (2026-05-20 처방)", "confidence": 0.95 } - ] - }, - "s09_dialogue_based_evidence": { - "items": [ - { "id": "ev_msg_002", "source_type": "message", "source_ref": "dialogue_turn_3", "content": "계속 우울해요, 아무것도 하기 싫어요" }, - { "id": "ev_msg_003", "source_type": "message", "source_ref": "dialogue_turn_5", "content": "잠들기가 너무 어렵고 자다가도 깨요" }, - { "id": "ev_msg_004", "source_type": "message", "source_ref": "dialogue_turn_7", "content": "밥맛도 없고 3키로 빠졌어요" } - ] - }, - "s10_longitudinal_changes": { - "baseline_date": "2026-05-01", - "domains": { - "phq9": { "previous": 16, "current": 12, "direction": "improved" }, - "ctrs": { "previous": 4, "current": 4, "direction": "unchanged" }, - "sleep": { "direction": "improved", "note": "입면 곤란 빈도 감소" } - } - }, - "s11_ai_limitations": { - "low_confidence_items": [ - { "field": "psychosocial_context.occupation", "confidence": 0.55, "note": "간접 언급으로 추정" } - ], - "missing_information": ["집중력 변화", "정신운동 변화", "과거 정신과 병력 상세"], - "known_limitations": [ - "본 보고서는 AI 사전 문진 보조 도구의 출력이며, 의료적 진단이나 치료 결정을 대체하지 않습니다.", - "구조화 척도 점수는 환자 자가보고 기반 rule-based 계산 결과입니다." - ] - }, - "s12_recommended_next_steps": { - "disclaimer": "아래는 추가 평가를 위한 절차적 제안이며, 치료 지시가 아닙니다.", - "suggestions": [ - "GAD-7 미시행 — 불안 영역 추가 평가 고려", - "집중력/정신운동 변화 미확인 — 면담 시 확인 권장", - "과거 정신과 병력 상세 미수집 — 면담 시 확인 권장" - ] - } - }, - "ctrs_level": 4, - "evidence_registry": { - "total_citations": 7, - "citation_ids": ["ev_msg_001", "ev_msg_002", "ev_msg_003", "ev_msg_004", "ev_scale_001", "ev_ocr_001", "ev_risk_001"] - }, - "section_completeness": { - "section_01_patient_info": true, - "section_02_input_sources": true, - "section_03_chief_complaint": true, - "section_04_domain_candidates": true, - "section_05_risk_assessment": true, - "section_06_survey_results": true, - "section_07_record_evidence": true, - "section_08_dialogue_evidence": true, - "section_09_longitudinal": true, - "section_10_limitations": true, - "section_11_recommendations": true, - "section_12_evidence_registry": true - }, - "evidence_count": 7 -} -``` - -## Evidence ID 생성 규칙 - -Report 내 모든 evidence 인용은 `[ev_{source_type}_{3-digit sequence}]` 형식을 사용한다: - -| Source Type | Evidence ID 패턴 | 예시 | -|---|---|---| -| 대화 메시지 | `[ev_msg_NNN]` | `[ev_msg_001]`, `[ev_msg_012]` | -| 구조화 척도 | `[ev_scale_NNN]` | `[ev_scale_001]` | -| OCR 문서 블록 | `[ev_ocr_NNN]` | `[ev_ocr_001]` | -| 위험 이벤트 | `[ev_risk_NNN]` | `[ev_risk_001]` | -| 이전 handoff | `[ev_prior_NNN]` | `[ev_prior_001]` | - -**규칙:** -1. 시퀀스 번호는 source_type 내에서 001부터 순차 증가한다. -2. 모든 `[ev_*]` 인용은 반드시 Section 12 Evidence Registry에 대응하는 항목이 있어야 한다. -3. Evidence Registry에 등록되지 않은 ID를 인용하면 EvidenceVerifier가 dangling reference로 reject한다. - -## CTRS Level 출력 - -출력 스키마에 `ctrs_level: int` 필드를 포함한다. CTRS 1-2인 경우 Section 11(권장 다음 조치)에 즉시 대응 관련 내용이 반드시 포함되어야 한다. - -## 핵심 동작 - -1. **Evidence 인용 필수**: 모든 주장(claim)에 `[ev_{source_type}_{NNN}]` 형태의 evidence ID를 인용한다. 인용 없는 주장은 report에 포함하지 않는다. -2. **12개 섹션 완전 생성**: 해당 데이터가 없는 섹션도 "해당 정보 없음"으로 포함한다. 섹션을 생략하지 않는다. -3. **진단적 단정 금지**: "우울증입니다", "불안장애가 의심됩니다" 등의 진단적 표현을 사용하지 않는다. "우울 영역 관련 증상이 수집되었습니다"로 표현한다. -4. **치료 지시 금지**: "약을 처방하세요", "입원이 필요합니다" 등의 치료 지시를 하지 않는다. "추가 평가 고려" 수준으로만 제안한다. -5. **한계 명시**: Section 11에서 AI 판단의 한계를 투명하게 기술한다. -6. **구조화 점수 검증**: PHQ-9/GAD-7 등의 점수가 rule-based로 계산되었는지 확인하고, 계산 방법을 명시한다. -7. **Source 신뢰도 전파**: OCR/STT에서 온 정보의 신뢰도를 report에 반영한다. - -## 안전 제약 - -1. **AI는 진단하지 않는다.** Section 5(정신건강 영역 후보)에 반드시 disclaimer를 포함한다. -2. **치료 지시를 하지 않는다.** Section 12(권장 다음 조치)는 절차적 제안에 한정한다. -3. **Evidence 없는 주장 금지.** 모든 claim은 evidence registry에 등록된 citation이 있어야 한다. -4. **CTRS level과 권장 조치 정합성**: CTRS 1-2인 경우 권장 조치에 즉시 대응 관련 내용이 반드시 포함되어야 한다. -5. **Low-confidence 항목 명시**: confidence < 0.7인 항목은 Section 11에 나열한다. - -## 실패 시 대응 - -| 실패 유형 | 대응 | -|---|---| -| LLM report 생성 실패 | Secondary → Fallback LLM 시도 | -| 전체 LLM 실패 | 구조화된 slot 데이터를 템플릿에 직접 삽입한 minimal report 생성 | -| Evidence verification 실패 (11번 Agent reject) | 지적 사항 수정 후 재생성 (최대 2회) | -| 필수 섹션 데이터 부재 | 해당 섹션에 "정보 미수집" 표기 후 Section 11에 한계로 기록 | diff --git a/docs/ai/agents/10_sentiment_analyzer.md b/docs/ai/agents/10_sentiment_analyzer.md index adf5a2e..d6228df 100644 --- a/docs/ai/agents/10_sentiment_analyzer.md +++ b/docs/ai/agents/10_sentiment_analyzer.md @@ -1,6 +1,6 @@ # Agent 10: Sentiment Analyzer Agent -> **번호 재부여 2026-07-20 (wave-6, 사용자 directive):** 구 Agent 13(`13_sentiment_analyzer.md`). +> **번호 재부여 2026-07-20 (wave-6, 사용자 directive):** 구 Agent 13(`13_sentiment_analyzer.md`). 구번호 원본 파일은 2026-07-27 사용자 directive로 파기 — 스펙 번호 재정립: 01–11 = `apps/ai-server/prompts/` 최종 구성과 1:1. > old→new 전체 매핑은 `docs/ai/agent_collaboration_f1f5.md`의 번호 체계 표를 참조. > 이번 작업에서 이 번호 재부여보다 앞서 존재하던 stale downstream-agent 참조 2건 > (`TemporalSummaryAgent`, `HandoffGeneratorAgent`)도 함께 정정했다 — 아래 inline note 참조. diff --git a/docs/ai/agents/11_domain_inference.md b/docs/ai/agents/11_domain_inference.md index 71afcfb..7b5262c 100644 --- a/docs/ai/agents/11_domain_inference.md +++ b/docs/ai/agents/11_domain_inference.md @@ -1,6 +1,6 @@ # Agent 11: 정신건강 영역 추론 에이전트 (DomainInferenceAgent) — 단일 RAG 에이전트 -> **번호 재부여 2026-07-20 (wave-6, 사용자 directive):** 구 Agent 14(`14_domain_inference.md`). +> **번호 재부여 2026-07-20 (wave-6, 사용자 directive):** 구 Agent 14(`14_domain_inference.md`). 구번호 원본 파일은 2026-07-27 사용자 directive로 파기 — 스펙 번호 재정립: 01–11 = `apps/ai-server/prompts/` 최종 구성과 1:1. > old→new 전체 매핑은 `docs/ai/agent_collaboration_f1f5.md`의 번호 체계 표를 참조. ## 개요 diff --git a/docs/ai/agents/11_evidence_verifier.md b/docs/ai/agents/11_evidence_verifier.md deleted file mode 100644 index 59b199a..0000000 --- a/docs/ai/agents/11_evidence_verifier.md +++ /dev/null @@ -1,223 +0,0 @@ -# Agent 11: QA & Consistency Checker Agent - -## 개요 - -| 항목 | 내용 | -|---|---| -| **Agent ID** | `11` | -| **Agent Name** | `EvidenceVerifierAgent` | -| **역할** | Handoff report 품질 검증 및 release gate | -| **LLM Routing** | **100% rule-based — 실제로는 LLM을 전혀 호출하지 않는다.** `agents/evidence_verifier.py`에는 router/adapter/prompt_loader 참조가 없다(정규식/패턴 매칭 기반). `agent_model_registry.yaml`은 `benchmarked`로 등록해 두었으나 코드 현실과 어긋난 등록 상태다(알려진 드리프트, 별도 BUG 예정 — 본 미션 범위에서 registry는 수정하지 않음) | - -## 목적 - -HandoffGeneratorAgent(10)가 생성한 report를 배포 전에 검증한다. 모든 검증 항목을 통과해야만 report가 의료진에게 전달된다. 위반 사항 발견 시 **reject하고 재생성을 요청**하며, 최대 2회 재시도 후에도 통과하지 못하면 의료진에게 경고와 함께 전달한다. 이 에이전트가 **release gate** 역할을 수행한다. - -## 검증 항목 (Validation Checklist) — 스펙 대비 구현 현황 - -실제 구현(`agents/evidence_verifier.py`)은 아래 6개 체크 함수만 존재한다: `_check_unsupported_claims`, `_check_diagnosis_violations`, `_check_treatment_violations`, `_check_section_completeness`, `_check_ctrs_action_alignment`, `_check_dangling_references`(각각 정규식/패턴 매칭, LLM 미사용). V-01..V-12 12개 항목 중 **V-04, V-06, V-10, V-11, V-12는 미구현**이다. - -| 번호 | 검증 항목 | 심각도 | 설명 | 구현 상태 | -|---|---|---|---|---| -| V-01 | Evidence citation 완전성 | CRITICAL | 모든 claim에 `[ev_*]` 인용이 있는가 | 구현(`_check_unsupported_claims`) | -| V-02 | 진단적 단정 부재 | CRITICAL | 진단명을 단정하는 표현이 없는가 | 구현(`_check_diagnosis_violations`) | -| V-03 | 치료 지시 부재 | CRITICAL | 치료를 지시하는 표현이 없는가 | 구현(`_check_treatment_violations`) | -| V-04 | 구조화 점수 정확성 | CRITICAL | PHQ-9/GAD-7 등의 점수가 rule-based 계산 결과와 일치하는가 | **미구현** | -| V-05 | CTRS-조치 정합성 | CRITICAL | CTRS level과 권장 조치가 정합한가 (CTRS 1-2 → 즉시 대응 포함) | 구현(`_check_ctrs_action_alignment`) | -| V-06 | OCR/STT low-confidence 표기 | HIGH | OCR/STT confidence < 0.8 항목이 한계 섹션에 명시되어 있는가 | **미구현** | -| V-07 | 누락 위험 신호 + CTRS 일관성 | CRITICAL | Safety classifier가 감지한 위험 신호가 report에 반영되어 있는가. **CTRS level이 위험 지표와 일치하는가** (아래 세부 규칙 참조) | 부분 구현(`_check_ctrs_action_alignment`) | -| V-08 | 12개 섹션 완전성 | HIGH | 모든 섹션이 존재하는가 (데이터 없는 섹션도 "정보 없음"으로 포함) | 구현(`_check_section_completeness`) | -| V-09 | Evidence registry 정합성 | HIGH | report 내 인용된 evidence ID가 모두 registry에 존재하는가 | 구현(`_check_dangling_references`) | -| V-10 | Disclaimer 존재 | HIGH | Section 5, 11, 12에 disclaimer가 포함되어 있는가 | **미구현** | -| V-11 | 종단 비교 근거 충분성 | MEDIUM | 종단 비교에서 "unknown" 아닌 판정에 근거가 있는가 | **미구현** | -| V-12 | 시제/표현 일관성 | LOW | 보고서 내 시제와 표현이 일관적인가 | **미구현** | - -## V-07 CTRS 일관성 검증 세부 규칙 — 스펙 대비 구현 현황 - -**정정:** V-07은 개념상 "CTRS level이 관련 위험 지표와 폭넓게 일관적인지" 교차 검증하는 취지이나, 실제 구현 함수 `_check_ctrs_action_alignment(report: str, ctrs_level: int)`(`agents/evidence_verifier.py:302-337`)은 **report 텍스트와 ctrs_level 정수만 입력받는다.** PHQ-9 점수, safety classifier의 위험 카테고리/`crisis_triggered` 플래그, 자살 계획·수단 언급 여부, 자살시도 진행 여부는 이 함수의 인자에 없고 코드 어디에도 그 값을 참조하는 로직이 없다 — 아래 6개 행 중 실제로 구현된 것은 **마지막 행(섹션 11 키워드 존재 검사)뿐**이며, 이는 위 "검증 항목" 표의 V-07 "부분 구현" 표기와 일치한다. - -| 조건 | 최소 CTRS | 불일치 시 | 구현 상태 | -|---|---|---|---| -| PHQ-9 Q9 >= 1 (자살 사고 문항 양성) | CTRS 3 이하 | CTRS 4-5이면 FAIL | **미구현** (design-intent only — PHQ-9 점수가 함수 입력에 없음) | -| Active suicidal ideation (능동적 자살 사고) 감지됨 | CTRS 2 이하 | CTRS 3-5이면 FAIL | **미구현** (design-intent only — safety classifier 카테고리가 함수 입력에 없음) | -| 구체적 자살 계획/수단 언급 | CTRS 2 이하 | CTRS 3-5이면 FAIL | **미구현** (design-intent only) | -| 자살시도 진행 중 또는 직후 | CTRS 1 | CTRS 2-5이면 FAIL | **미구현** (design-intent only) | -| Safety classifier가 crisis_triggered = true 반환 | CTRS 1-2 | CTRS 3-5이면 FAIL | **미구현** (design-intent only — `crisis_triggered` 플래그가 함수 입력에 없음) | -| 섹션 11(권장 조치)에 CTRS별 필수 키워드 미포함 | CTRS 1: `119\|112\|응급`, CTRS 2: `109\|119\|긴급\|위기상담`, CTRS 3: `정신건강의학과\|109\|위기` | 미포함 시 FAIL | **구현** (`_check_ctrs_action_alignment`) — CTRS 1/2 미포함은 `severity=error`, CTRS 3 미포함은 `severity=warning` | - -**FAIL 판정 시:** 위 마지막 행(구현된 유일한 체크)만 실제로 report를 reject/regenerate로 이끈다. CTRS 1/2 미포함은 `error`로 기록되어 즉시 reject(CRITICAL 위반) 대상이다. CTRS 3 미포함은 `warning`으로 기록되며, 단독으로는 reject를 유발하지 않고 다른 warning과 합산해 3건 이상일 때만 regenerate를 유발한다(`run()`의 `error_count`/`warning_count` 판정, `agents/evidence_verifier.py:133-142`) — 위 "검증 항목" 표의 V-07 CRITICAL 표기는 이 체크가 개념적으로 속한 심각도 분류이며, CTRS 3 행의 코드 레벨 `severity=warning`과는 별개다. 나머지 5개 행(미구현)은 FAIL 판정 자체가 발생하지 않는다 — 해당 조건이 코드에서 평가되지 않기 때문이다. - -## 입력 - -| 필드 | 타입 | 설명 | -|---|---|---| -| `handoff_report` | `object` | HandoffGeneratorAgent가 생성한 report | -| `clinical_slots` | `object` | ClinicalSlotAgent 원본 출력 (점수 검증용) | -| `safety_classification` | `object` | SafetyClassifierAgent 원본 결과 | -| `scale_scores_rule_based` | `object \| null` | Rule-based 계산된 척도 점수 원본 | -| `ocr_results` | `array \| null` | OCR 원본 결과 (confidence 검증용) | -| `retry_count` | `integer` | 현재 재시도 횟수 (0, 1, 2) | - -## 출력 - -```json -{ - "report_id": "handoff_20260618_001", - "verification_result": "PASS", - "retry_count": 0, - "checks": [ - { - "check_id": "V-01", - "name": "Evidence citation 완전성", - "severity": "CRITICAL", - "result": "PASS", - "details": "7개 claim, 7개 citation 확인" - }, - { - "check_id": "V-02", - "name": "진단적 단정 부재", - "severity": "CRITICAL", - "result": "PASS", - "details": "진단적 단정 표현 미발견" - }, - { - "check_id": "V-03", - "name": "치료 지시 부재", - "severity": "CRITICAL", - "result": "PASS", - "details": "치료 지시 표현 미발견" - }, - { - "check_id": "V-04", - "name": "구조화 점수 정확성", - "severity": "CRITICAL", - "result": "PASS", - "details": "PHQ-9 report 12점 = rule-based 12점 일치" - }, - { - "check_id": "V-05", - "name": "CTRS-조치 정합성", - "severity": "CRITICAL", - "result": "PASS", - "details": "CTRS 4, 권장 조치에 즉시 대응 불필요" - }, - { - "check_id": "V-06", - "name": "OCR/STT low-confidence 표기", - "severity": "HIGH", - "result": "PASS", - "details": "OCR confidence < 0.8 항목 1건, Section 11에 표기 확인" - }, - { - "check_id": "V-07", - "name": "누락 위험 신호", - "severity": "CRITICAL", - "result": "PASS", - "details": "Safety classifier 결과와 report CTRS 섹션 일치" - }, - { - "check_id": "V-08", - "name": "12개 섹션 완전성", - "severity": "HIGH", - "result": "PASS", - "details": "12개 섹션 모두 존재" - }, - { - "check_id": "V-09", - "name": "Evidence registry 정합성", - "severity": "HIGH", - "result": "PASS", - "details": "인용된 7개 ID 모두 registry에 존재" - }, - { - "check_id": "V-10", - "name": "Disclaimer 존재", - "severity": "HIGH", - "result": "PASS", - "details": "Section 5, 11, 12 disclaimer 확인" - }, - { - "check_id": "V-11", - "name": "종단 비교 근거 충분성", - "severity": "MEDIUM", - "result": "PASS", - "details": "improved/unchanged 판정 모두 evidence 포함" - }, - { - "check_id": "V-12", - "name": "시제/표현 일관성", - "severity": "LOW", - "result": "PASS", - "details": "일관성 확인" - } - ], - "critical_violations": 0, - "high_violations": 0, - "medium_violations": 0, - "low_violations": 0, - "action": "RELEASE", - "timestamp": "2026-06-18T14:36:30+09:00" -} -``` - -### Reject 시 출력 예시 - -```json -{ - "report_id": "handoff_20260618_001", - "verification_result": "FAIL", - "retry_count": 1, - "checks": [ - { - "check_id": "V-02", - "name": "진단적 단정 부재", - "severity": "CRITICAL", - "result": "FAIL", - "details": "Section 5에서 '주요우울장애가 의심됩니다' 표현 발견. 진단적 단정 금지 위반.", - "location": "sections.s05_mental_health_domains.candidates[0].basis", - "suggested_fix": "'우울 영역 관련 증상이 수집되었습니다'로 변경" - } - ], - "critical_violations": 1, - "action": "REJECT_AND_REGENERATE", - "rejection_instructions": [ - "Section 5의 '주요우울장애가 의심됩니다'를 '우울 영역 관련 증상이 수집되었습니다'로 수정" - ], - "timestamp": "2026-06-18T14:36:30+09:00" -} -``` - -## 핵심 동작 - -1. **부분 구현 검증**: 스펙상 12개 검증 항목 중 6개 체크 함수가 V-01/02/03/05/07/08/09(7개 항목 — `_check_ctrs_action_alignment` 하나가 V-05와 V-07을 함께 커버)를 수행한다. V-04/06/10/11/12는 미구현이다(위 "검증 항목" 표 참조). -2. **CRITICAL 위반 시 즉시 reject**: CRITICAL 심각도 항목이 하나라도 FAIL이면 report를 reject한다. -3. **Reject → 재생성 루프**: reject 시 violation 상세와 수정 지시를 HandoffGeneratorAgent에 전달한다. 최대 2회 재시도. -4. **3회 실패 시 경고 배포**: 2회 재시도 후에도 CRITICAL 위반이 남아있으면, 경고 메시지를 첨부하여 의료진에게 전달한다. report를 차단하지는 않는다 (의료진 판단 우선). -5. **점수 교차 검증 — 미구현(V-04)**: report 내 구조화 척도 점수를 rule-based 계산 원본과 대조하는 로직은 설계 의도이나 현재 구현되어 있지 않다. -6. **진단/치료 표현 패턴 탐지**: 금지 표현 패턴 사전 기반 정규식 매칭으로 report 텍스트를 스캔한다(LLM 미사용). -7. **Evidence trace**: report 내 모든 `[ev_*]` ID가 evidence registry에 존재하고, 원본 데이터와 일치하는지 검증한다. - -## 금지 표현 패턴 (예시) - -| 카테고리 | 패턴 예시 | -|---|---| -| 진단적 단정 | `~장애`, `~증`, `~병`, `의심됩니다`, `진단`, `확진` | -| 치료 지시 | `처방`, `투약`, `입원`, `~해야 합니다`, `~하세요`, `권고합니다` | -| 과도한 확신 | `확실히`, `분명히`, `틀림없이` | - -- 문맥 의존적 예외(예: "이전 진단서에 우울증이 기재되어 있었습니다"는 허용)의 판별 방식은 정규식/패턴 매칭 로직의 세부 구현에 달려 있으며, LLM 기반 판별이 아니다(위 "LLM Routing" 상태 참조). - -## 안전 제약 - -1. **Release gate 역할**: 이 에이전트를 통과하지 않은 report는 의료진에게 전달되지 않는다. -2. **CRITICAL 위반 무시 금지**: CRITICAL 위반은 어떤 경우에도 무시할 수 없다. 3회 실패 시에도 경고를 첨부한다. -3. **자동 수정 금지**: Verifier는 report를 직접 수정하지 않는다. 수정은 HandoffGeneratorAgent가 수행한다. -4. **검증 로그 보존**: 모든 검증 결과(PASS/FAIL)를 로그에 저장한다. 추후 감사(audit)에 활용한다. - -## 실패 시 대응 - -| 실패 유형 | 대응 | -|---|---| -| 3회 연속 CRITICAL 실패 | Report에 상세 경고를 첨부하여 의료진에게 전달. 차단하지 않음 | - -**정정:** 이 에이전트는 LLM을 호출하지 않으므로 "LLM 검증 실패"/"전체 LLM 실패"/"검증 timeout"(LLM 응답 대기 관련) 행은 as-built 상태에 존재하지 않는다(과거 버전 문서의 서술 제거). V-04/06/10/11/12의 미구현은 "실패"가 아니라 해당 체크가 애초에 수행되지 않는 상태이며, 그 항목들은 결과에 나타나지 않는다. diff --git a/docs/ai/agents/13_sentiment_analyzer.md b/docs/ai/agents/13_sentiment_analyzer.md deleted file mode 100644 index b29ed2e..0000000 --- a/docs/ai/agents/13_sentiment_analyzer.md +++ /dev/null @@ -1,167 +0,0 @@ -# Agent 13: Sentiment Analyzer Agent - -## 개요 - -| 항목 | 내용 | -|---|---| -| **Agent ID** | `13` | -| **Agent Name** | `SentimentAnalyzerAgent` | -| **역할** | 발화 단위 감정 분석 + 세션 통합 sentiment 리포트 | -| **LLM Routing** | **Mode A(발화 단위)에만 적용.** benchmarked (Primary: Solar Pro 3 / Secondary: K-EXAONE / Fallback: A.X K1), 프롬프트 pin v2(`agents/sentiment_analyzer.py:40`). **Mode B(세션 통합)는 LLM을 전혀 호출하지 않는 순수 rule-based 집계**(Python `Counter`/산술 연산)다 — 아래 "동작 모드" 절 참조 | - -## 목적 - -환자의 **개별 발화마다** 감정 상태를 분석하고, **세션 전체**에 걸친 통합 sentiment 리포트를 생성한다. 이 출력은 두 가지 downstream agent에서 소비된다: - -1. **TemporalSummaryAgent(09)**: 일자/시간별 sentiment 추이 그래프 생성 -2. **HandoffGeneratorAgent(10)**: 대화 기록 내 발화별 sentiment 태깅 표시 - -Sentiment 분석은 **보조 정보**이며, 임상 척도(PHQ-9, GAD-7)를 대체하지 않는다. - -## 동작 모드 - -### Mode A: 발화 단위 분석 (per-utterance) - -Dialogue 진행 중 매 턴마다 호출되어 해당 발화의 감정을 분류한다. - -**입력:** - -| 필드 | 타입 | 설명 | -|---|---|---| -| `utterance` | `string` | 환자의 단일 발화 텍스트 | -| `turn_index` | `int` | 대화 내 턴 번호 | -| `conversation_context` | `list[dict]` | 직전 2-3턴 (맥락 파악용) | - -**출력:** - -```json -{ - "turn_index": 3, - "emotions": [ - { "label": "anxiety", "intensity": 0.8 }, - { "label": "sadness", "intensity": 0.5 } - ], - "polarity": -0.65, - "arousal": "high", - "evidence_phrase": "요즘 계속 불안하고 잠을 못 자요", - "risk_signal": false -} -``` - -### Mode B: 세션 통합 리포트 (session-level) - -세션 종료(또는 handoff 생성 직전) 시 호출되어, 전체 발화의 sentiment를 종합 분석한다. **LLM을 호출하지 않는다** — Mode A 출력들을 입력으로 받아 감정 빈도 집계, polarity 평균, dominant emotion 산출 등을 순수 rule-based 연산(Python `Counter`/산술)으로 수행한다(`agents/sentiment_analyzer.py:140-` `_analyze_session`). 출력의 `prompt_version: "v1"`은 프롬프트가 로드되지 않는 cosmetic 라벨이다. - -**입력:** - -| 필드 | 타입 | 설명 | -|---|---|---| -| `per_utterance_results` | `list[object]` | Mode A 출력 목록 | -| `conversation_history` | `list[dict]` | 전체 대화 기록 | -| `session_id` | `string` | 세션 식별자 | - -**출력:** - -```json -{ - "session_id": "sess_20260619_001", - "dominant_emotions": ["anxiety", "sadness"], - "emotion_distribution": { - "anxiety": 0.35, - "sadness": 0.28, - "neutral": 0.20, - "hope": 0.10, - "anger": 0.07 - }, - "polarity_trajectory": [ - { "turn": 1, "polarity": -0.3 }, - { "turn": 3, "polarity": -0.7 }, - { "turn": 5, "polarity": -0.8 }, - { "turn": 8, "polarity": -0.5 } - ], - "signal_strength": "moderate", - "emotional_shift_detected": true, - "shift_description": "대화 초반 경도 불안 → 중반 불안 및 슬픔 심화 → 후반 약간 안정", - "repeated_patterns": ["불안 표현 4회 반복", "수면 관련 호소 3회"], - "per_utterance_tags": [ - { "turn": 1, "emotions": ["neutral"], "polarity": -0.1 }, - { "turn": 2, "emotions": ["anxiety"], "polarity": -0.5 }, - { "turn": 3, "emotions": ["anxiety", "sadness"], "polarity": -0.7 } - ], - "timestamp": "2026-06-19T14:30:00+09:00" -} -``` - -## 감정 분류 체계 - -| Label | 한국어 | 설명 | -|---|---|---| -| `anxiety` | 불안 | 걱정, 긴장, 초조, 공포 | -| `sadness` | 슬픔 | 우울, 비탄, 상실감, 눈물 | -| `anger` | 분노 | 짜증, 적대감, 좌절 | -| `despair` | 절망 | 무망감, 무기력, 의미 상실 | -| `fear` | 공포 | 위협감, 회피 | -| `hope` | 희망 | 기대, 의지, 긍정적 기대 | -| `neutral` | 중립 | 감정적 색채 미약 | -| `relief` | 안도 | 완화, 편안함 | - -**polarity**: -1.0 (극도 부정) ~ +1.0 (극도 긍정). 0.0 = 중립. -**arousal**: `low` / `medium` / `high` — 감정의 활성도/강도. -**intensity**: 0.0 ~ 1.0 — 개별 감정의 강도. - -## Downstream 연동 - -### → TemporalSummaryAgent(09) - -``` -session_level_sentiment (Mode B output) - └── polarity_trajectory → 일자별 sentiment 추이 그래프 데이터 - └── dominant_emotions → 시계열 도메인에 sentiment 도메인 추가 - └── signal_strength → 종단적 sentiment 변화 direction 판정 -``` - -TemporalSummary의 `plot_data`에 `sentiment_polarity` 필드가 추가된다: - -```json -{ "date": "2026-06-19", "PHQ-9": 15, "sentiment_polarity": -0.65, "ctrs_level": 4 } -``` - -### → HandoffGeneratorAgent(10) - -``` -per_utterance_tags (Mode B output) - └── Section 8 (대화 기반 근거)에서 발화별 감정 태그 표시 -``` - -Section 8 예시: - -```markdown -| 턴 | 발화 | 감정 태그 | Evidence | -|---|---|---|---| -| 1 | "잠을 못 자서 왔습니다" | 😟 anxiety | [ev_msg_001] | -| 3 | "요즘 죽고 싶다는 생각이..." | 😔 despair, 🚨 risk | [ev_msg_003] | -| 5 | "약을 먹으니 조금 나아졌어요" | 🙂 hope, relief | [ev_msg_005] | -``` - -## 핵심 동작 규칙 - -1. **모든 환자 발화에 실행**: assistant 발화는 분석하지 않는다. -2. **risk_signal 연동**: despair intensity ≥ 0.8 또는 위기 관련 감정 패턴 감지 시 `risk_signal: true` 반환. SafetyClassifier의 판단을 대체하지 않으며 보조 신호로만 사용한다. -3. **문화적 맥락 반영**: 한국어 감정 표현의 특성 반영 (간접적 감정 표현, 축소 표현 등). -4. **Polarity trajectory 필수**: 대화 내 감정 변화 흐름을 시계열로 추적한다. -5. **반복 패턴 감지**: 동일 감정/표현이 3회 이상 반복되면 `repeated_patterns`에 기록한다. - -## 안전 제약 - -1. **AI는 진단하지 않는다.** sentiment 결과를 "우울증", "불안장애" 등 진단명으로 표현하지 않는다. -2. **임상 척도를 대체하지 않는다.** sentiment는 보조 정보이며, PHQ-9/GAD-7 rule-based 점수가 우선한다. -3. **CTRS 판정을 대체하지 않는다.** risk_signal은 SafetyClassifier의 보조 신호일 뿐이다. -4. **환자의 감정을 평가하지 않는다.** "부적절한 감정" 같은 표현 금지. - -## 실패 시 대응 - -| 실패 유형 | 대응 | -|---|---| -| LLM 호출 실패 | `emotions: [{"label": "unknown", "intensity": 0}]`, `polarity: 0.0` 반환. downstream agent는 sentiment 없이 진행 | -| 발화가 너무 짧음 (< 5자) | `neutral` 반환, confidence 낮게 설정 | -| Mode B 입력 비어있음 | 빈 session report 반환 (`dominant_emotions: []`, `signal_strength: "none"`) | diff --git a/docs/ai/agents/14_domain_inference.md b/docs/ai/agents/14_domain_inference.md deleted file mode 100644 index cc3e586..0000000 --- a/docs/ai/agents/14_domain_inference.md +++ /dev/null @@ -1,242 +0,0 @@ -# Agent 14: Mental-Health Domain Inference Agent (DomainInferenceAgent) - -## 개요 - -| 항목 | 내용 | -|---|---| -| **Agent ID** | `14` | -| **Agent Name** | `DomainInferenceAgent` | -| **역할** | RAG 기반 정신건강 영역(domain)/진료과(department) 후보 추론 | -| **LLM Routing** | benchmarked (Primary: Upstage Solar Pro 3 / Secondary: LG K-EXAONE / Fallback: SKT A.X K1) | -| **파이프라인 상태** | Standalone. Orchestrator(01) 11-state 머신에 미연결(`routes/domain.py:1-8`). G-D 게이트가 프로덕션 통합의 전제조건 | -| **인증 상태** | ADR-015 — `llm_only` 경로 인증(조건부), RAG 경로 EXPERIMENTAL (아래 "인증 상태" 절 참조) | - -## 목적 - -수집된 임상 슬롯(F1 `final_slots`)과, 있는 경우 검색된 근거를 바탕으로 정신건강 영역 후보(최대 3개)와 진료과 후보를 제시한다. 환자 대면 응답을 생성하지 않으며 진단을 확정하지 않는다. **위험 관련 표현은 이 에이전트가 산출하는 domain confidence의 근거로 사용되지 않는다** — 위험 판정은 SafetyClassifier(02)의 소관이며, 이 규칙은 프롬프트 문구뿐 아니라 코드 레벨에서도 강제된다(아래 "근거 검증 4계층 방어선" 참조). - -이 에이전트는 2단계(Stage) 파이프라인 중 **Stage 2(LLM 1회 호출)만** 담당한다. Stage 1(코드 레벨 검색)은 호출자의 책임이며, 검증 하네스인 `apps/ai-server/src/f2.py`가 두 단계를 조립한다(`domain_inference.py:1-11`). - -## 2단계 아키텍처 - -``` -F1 conversation.json (final_slots, turns, session_ctrs, ...) - │ - ▼ -[Stage 1] 코드 레벨 검색 (f2.py::run_stage1, rag/retrieval.py 재사용) - │ chief_complaint/HPI/risk_assessment 슬롯 → retrieve_domain_chunks() - │ --no-rag 플래그 / 빈 쿼리 / DB·임베딩 예외 → mode="llm_only"로 강등 - │ (never raises — 상위 호출자에 항상 (mode, chunks) 튜플 반환) - ▼ -[Stage 2] LLM 1회 호출 (DomainInferenceAgent.run, 이 문서의 대상) - │ ModelRouter: primary → 실패 시 fallback 1단계만 시도 - │ 파싱/전송 실패 → 빈 domain_candidates + reason_summary (never crashes) - ▼ -[근거 검증] f2_grounding.filter_domain_candidates (f2.py 하네스 경유 시에만 적용) - │ source-id 화이트리스트 + quote 어휘 대조 + 위험-어휘 필터 - │ → 거부된 evidence 제거, evidence 0개된 후보는 통째로 탈락 - ▼ -산출물 (JSON + report.md, post-cascade 후보 + filter_summary + repro 메타) -``` - -### Stage 1: 코드 레벨 검색 (`f2.py::run_stage1`) - -`retrieve_domain_chunks`(`src/rag/retrieval.py`)를 재사용한다. `chief_complaint`, `history_of_present_illness`, `risk_assessment` 세 슬롯의 값만 쿼리로 사용한다(`f2.py:58` `_STAGE1_QUERY_SLOTS`). - -| 조건 | 결과 | -|---|---| -| `--no-rag` 플래그 | 즉시 `mode="llm_only"`, 빈 chunk 목록 | -| 세 쿼리 슬롯이 모두 비어있음 | `mode="llm_only"`, 빈 chunk 목록 | -| DB 연결/임베딩 등 Stage-1 예외 | `mode="llm_only"`로 강등, 예외를 삼키고 로그만 남김 (`f2.py:139-171`) | -| 정상 검색 | `mode="rag"`, `chunk_id` + 본문 텍스트 포함 chunk 목록 | - -Stage 1은 **어떤 경우에도 예외를 상위로 전파하지 않는다** — 실패는 항상 `llm_only` 강등으로 흡수된다(`f2.py:142-144` docstring). - -### Stage 2: LLM 후보 생성 (`agents/domain_inference.py::DomainInferenceAgent.run`) - -ModelRouter로 primary 모델을 호출하고, 실패 시 fallback 1단계를 시도한다(`domain_inference.py:145-249`). 두 시도 모두 실패하거나 응답 JSON 파싱/스키마 검증에 실패하면 **빈 `domain_candidates` + `reason_summary`에 사유를 담아 반환한다 — 절대 크래시하지 않는다**(`domain_inference.py:196-202`, `217-224`, `229-237`). - -## 입력 (`DomainInferenceInput`, `schemas/domain_inference.py:105-122`) - -| 필드 | 타입 | 설명 | -|---|---|---| -| `final_slots` | `dict[str, str]` | F1 최종 슬롯(12-key, flat) | -| `session_ctrs` | `int (1-5)` | 현재 세션 CTRS level — **참고용, evidence 근거로 사용 금지** | -| `crisis_triggered` | `bool` | Safety 위기 트리거 여부 — 참고용 | -| `crisis_turn` | `int \| null` | 위기 트리거 턴 번호 | -| `is_first_visit` | `bool` | 초진/재진 여부 | -| `turns` | `list[UtteranceTurn]` | F1 환자 발화(turn + patient_message) | -| `prior_handoff` | `string \| null` | 이전 handoff 요약(참고용) | -| `probe_events` | `list[dict]` | Safety probe 이벤트 | -| `scale_scores` | `list[ScaleScore]` | 구조화 척도 점수(1-5) | -| `retrieved_chunks` | `list[RetrievedChunk]` | Stage 1 산출물 — 호출 전 조립되어 전달됨 | -| `retrieval_mode` | `"rag" \| "llm_only"` | Stage 1 결과 모드 | -| `queries` | `list[string]` | Stage 1에 사용된 쿼리 원문 | - -## 출력 (`DomainInferenceOutput`, `schemas/domain_inference.py:125-132`) - -```json -{ - "session_id": "sess_20260618_001", - "model_used": "solar-pro3", - "prompt_version": "v2", - "latency_ms": 0.0, - "reason_summary": "domain/department candidates generated", - "domain_candidates": [ - { - "domain": "anxiety", - "confidence": 0.0, - "evidence": [ - { - "source_type": "utterance", - "source_id": "turn_3", - "quote": "<원문 발화에서 실제로 확인되는 인용>" - } - ], - "recommended_surveys": ["GAD-7"] - } - ], - "department_candidates": [ - { "department": "정신건강의학과", "reason": "<근거 요약>", "domain_ref": "anxiety" } - ], - "summary": "", - "retrieval_meta": { - "mode": "llm_only", - "chunks_returned": 0, - "chunk_ids": [], - "queries": [""] - }, - "additional_questions": ["<추가 확인 질문>"] -} -``` - -| 필드 | 설명 | -|---|---| -| `domain_candidates` | 최대 3개(`max_length=3`). 각 candidate는 evidence를 최소 1개 가져야 함(`min_length=1`) — "근거 없으면 후보 없음" | -| `domain` | Literal 8종: `anxiety \| depression \| alcohol \| substance \| trauma \| sleep \| psychosis \| other` | -| `department_candidates` | `domain_ref`는 생략되거나 `domain_candidates` 중 실제 존재하는 값이어야 함(고아 참조는 검증 대상) | -| `retrieval_meta` | Stage 1 결과를 항상 정직하게 반영(강등되었으면 `mode="llm_only"`로 보고, 은폐하지 않음) | - -## 근거 검증 4계층 방어선 - -Stage 2의 LLM 출력이 산출물에 반영되기까지 네 겹의 독립적 검증을 거친다: - -1. **Pydantic 스키마 경계** — `DomainCandidate.evidence`는 `min_length=1`, `domain_candidates`는 `max_length=3`. 스키마 자체가 "근거 없는 후보"와 "3개 초과 후보"를 원천 차단한다(`schemas/domain_inference.py:40-51, 68, 128`). -2. **`f2_grounding.check_evidence`** — evidence 항목별로 (a) `source_id`가 이번 실행에서 실제로 제공된 chunk_id/turn_id인지(화이트리스트), (b) `quote`가 해당 source의 실제 텍스트에서 어휘적으로 뒷받침되는지, (c) `quote`에 위험-어휘가 포함되어 있지 않은지를 순서대로 검사한다(`f2_grounding.py:227-305`). 위험-어휘 검사는 어휘 대조보다 먼저 수행된다. -3. **거부 캐스케이드 (`filter_domain_candidates`)** — 거부된 evidence 항목은 candidate에서 제거되고, accepted evidence가 0개가 된 candidate는 산출물에서 완전히 탈락한다(`f2_grounding.py:337-398`, ADR-014). -4. **`filter_summary` 감사 델타** — `f2.py` 하네스가 cascade 전/후 candidate 수, 탈락한 domain 목록, 제거된 evidence 수(위험-어휘 사유 별도 집계)를 산출물에 기록한다(`f2.py:250-275`). - -**이 4계층은 `src/f2.py` 검증 하네스를 경유할 때만 전부 적용된다.** `POST /ai/domain/infer` 라우트 단독 호출은 계층 1-2만 거치며(스키마 + 라우트 핸들러 예외 처리), 3-4(캐스케이드 + 감사 델타)는 하네스 전용이다 — 아래 "API 라우트" 절 참조. - -### 위험-어휘 필터 (`f2_grounding._RISK_PHRASES`, ADR-014 코드 강제) - -프롬프트 v1의 절대 규칙 2("위험 표현을 domain confidence의 근거로 사용하지 않는다")가 프롬프트 문구만으로는 라이브에서 지켜지지 않음이 확인되어(VAL-006/REV-008), v2부터 코드 레벨 강제 필터를 추가했다. 자살/자해/죽음-지향 SI-class 다단어 표현을 담은 quote는 소스 정당성이나 어휘적 근거와 무관하게 기계적으로 거부된다. - -- `_RISK_PHRASES` 리스트는 `f2_grounding.py:87-152`에 정의되어 있으며, 확인 결과 (스페이스 유무 변형을 포함해) 37개 다단어 stem으로 구성된다. -- 모두 다단어(multi-word) 구성이다 — 단독 명사(예: 과거 버전의 "손목", "목숨")는 무관한 임상 발화를 과잉 거부한 이력이 있어(BUG-015) 제거되었다. -- **공황 관용구 carve-out**: "죽는 줄 알았", "죽을 것 같" 등 공황발작의 죽음-공포 관용구(`_PANIC_IDIOM_PHRASES`, ISS-046/SM-07a)는 위험-어휘 리스트와 **구조적으로 disjoint**하도록 별도 구성되어 있다 — 런타임 예외 처리가 아니라 리스트 자체가 겹치지 않게 설계되어 있으며, `tests/test_f2_grounding.py::test_risk_and_panic_idiom_lexicons_are_disjoint`가 이를 assert한다(`f2_grounding.py:154-176`). - -| Verdict | 의미 | -|---|---| -| `accepted` | 통과 | -| `rejected_unknown_source` | source_id가 이번 실행에서 제공되지 않음 | -| `rejected_quote_mismatch` | quote가 source 텍스트에서 어휘적으로 뒷받침되지 않음 | -| `rejected_unknown_source_type` | `rag_chunk`/`utterance` 외 값 | -| `rejected_risk_lexicon` | quote에 위험-어휘 stem 포함 | - -## 프롬프트 pin - -현재 라이브 pin은 **v2**(`domain_inference.py:41`, `docs/ai/prompts/domain_inference/v2.system.md`). v1은 롤백 대비용으로 디스크에 유지된다. v2 시스템 프롬프트 파일은 문자 수 기준 2,948자로 확인됨(UTF-8 바이트 수 5,004와는 별개 단위 — 파일 크기를 볼 때 혼동 주의). - -**절대 규칙 5개**(다른 모든 지시보다 우선, `v2.system.md:23-41`): - -| 번호 | 규칙 요지 | -|---|---| -| 1 | 진단하지 않는다 — 영역/진료과는 항상 "후보" | -| 2 | 위험 표현을 domain confidence 근거로 사용하지 않는다(코드 강제 명시, 공황 관용구 예외) | -| 3 | 근거 없는 후보 금지(evidence 최소 1개) | -| 4 | 인용은 실제로 주어진 source_id/quote에서만 | -| 5 | 지정된 출력 필드 외 아무것도 출력하지 않는다 | - -## 라우팅 - -benchmarked (`routing/agent_model_registry.yaml:194-210`) — Primary: `solar-pro3`, Secondary: `k-exaone`, Fallback: `ak-llm`(A.X-K1). 다른 benchmarked 에이전트와 동일한 3-tier 구성이며, 벤치마크를 거쳐 확정된 라우팅이다. - -## API 라우트 - -`POST /ai/domain/infer` (`routes/domain.py:36-60`) — **standalone 라우트다. Orchestrator(01)의 11-state 머신에 연결되어 있지 않다.** 이 라우트는 Stage 2(LLM 후보 생성)만 노출한다 — Stage 1 검색과 근거 화이트리스트 감사(위 4계층 방어선의 3-4단계)는 라우트가 아니라 `src/f2.py` 검증 하네스의 책임이다. **fabrication-0 보장이 필요한 호출자는 이 라우트를 단독으로 사용해서는 안 되며, `f2.py` 하네스를 경유해야 한다**(`routes/domain.py:1-8` 명시). - -## 인증 상태 (ADR-015) - -REV-010(critic, EXP-005 전수 재감사) 채택 결과, 비인증 범위가 부분적으로 해제되었다: - -| 경로 | 상태 | 조건 | -|---|---|---| -| `llm_only` arm | **인증(certified)** | 매 라이브 배치에 대한 상시 수동 taxonomy 감사 필요(어휘 필터는 구조적으로 불완전하다는 전제) | -| RAG arm | **EXPERIMENTAL(비인증 유지)** | 인증 경로: BUG-016/BUG-017 수정 → VP-003 RAG n≥2 클린 재검증 → VAL-010(위험-편향 쿼리) 완화의 상시화 | - -이 인증 판정은 ADR-014의 특정 질문(위험≠도메인 규칙 준수 여부)에 국한되며, 그 자체로 프로덕션 통합(orchestrator.py 연동)을 승인하지 않는다 — 프로덕션 통합은 별도 전제조건(스키마 통합, orchestrator.py 배선)에 묶인 G-D 게이트 소관이다. - -## 알려진 결함 - -| ID | 요지 | 심각도 | 상태 | -|---|---|---|---| -| BUG-016 | `domain_inference.py:75,77`의 프롬프트 문구("chunk_id=" 라벨)를 모델이 `source_id`에 그대로 echo하는 경우가 있어, `f2_grounding.py:247-254`의 정확-일치 조회가 실패 → 실제로는 유효한 `rag_chunk` evidence가 `rejected_unknown_source`로 과잉 거부됨. 방향은 보수적(허위 근거 채택이 아닌 정당한 근거의 누락)이며 위험은 evidence가 이 항목 하나뿐인 candidate가 캐스케이드로 통째 탈락할 수 있다는 점 | major | **resolved** — 코드 레벨 정규화(`_normalize_rag_chunk_source_id`) 라이브 확인(n=32, 0/32 과잉거부, `EXP-006`) | -| BUG-017 | RAG 모드 LLM 출력이 파싱/스키마 검증에 실패하는 사례(`domain_inference.py:117-123` 파싱, `:128-143` `_call`) — `max_tokens=1536` 고정값이 RAG 모드의 더 긴 프롬프트에서 절단을 유발했을 가능성이 유력 가설이나, `finish_reason`/`usage`가 로그·출력 스키마 어디에도 기록되지 않아 현재 아티팩트만으로는 확증/반증이 불가능함 | major | 진단된 절단 메커니즘은 해소(`max_tokens` 1536→4096, `finish_reason="length"` 0/16 post-fix) — 실질 증상은 `BUG-019`로 잔존, BUG 자체는 open | -| BUG-019 | `RetrievedChunk.source_type`(DB 테이블 출처: `case_card`/`qa`)와 `DomainEvidence.source_type`(스키마 provenance enum: `rag_chunk`/`utterance`)가 필드명을 공유해, 프롬프트의 청크 나열 형식이 모델에게 이를 시각적으로 혼동시킴 — `qa`-테이블 청크를 인용할 때 간헐적으로 `source_type`에 `qa`/`qa:NNN` 같은 잘못된 값이 채워져 schema validation 실패(`finish_reason=stop`, 절단 아님, `BUG-017`과 별개 메커니즘) | major | **근본원인 진단·수정·코드검증 완료**(`EXP-007` 라이브 진단, `_normalize_source_type_collision`, qa `GATE:PASS`) — 라이브 clean VP-003/VP-001 RAG n=2 재검증 미실시로 **open 유지** | - -세 결함 모두 실패 시 빈 출력 또는 evidence 탈락으로 **보수적으로(fabrication 방향이 아닌 방향으로) 저하**되며, 크래시나 허위 근거 채택을 유발하지 않는다(위 "Stage 2" 절의 never-crash 설계와 일치). - -## AI 예상질환(AI-predicted-disease) 엔티티 — 별도 컨테이너, sibling key (PLAN-2026-W28-H Track B, 2026-07-09) - -이 에이전트(14)의 RAG top-5 disease 후보를 기반으로 하는 새 비진단·비임상 필드다. `f2.py`가 이를 기존 `domain_candidates` 출력에 **병합하지 않고** sibling key로 배선한다 — 즉 F2 산출물에 나란히 존재하는 별개 컨테이너다. - -- **스키마:** `AIPredictedDiseaseCandidate`(disease + `similarity_score`) 목록 + `AIPredictedDiseaseOutput`(`is_diagnostic: Literal[False]`, 고정 `disclaimer_ko`). -- **라벨링(`REV-013` §4, binding):** `similarity_score`("유사도 점수") 외 라벨 금지(`probability`/`확률`/`가능성(%)`/`confidence` 전부 금지). top-5 간 softmax 정규화 없음. -- **구조적 격리:** 04(`ClinicalSlotAgent`)의 12개 슬롯이나 `HandoffInput`의 어떤 typed field에도 병합되지 않는다 — qa의 9-테스트 adversarial suite(`tests/test_hpi_isolation.py`)가 `AgentInput.extra`/`state.conversation_history` 두 채널을 포함해 이를 확인했다(`REV-013` §3 조건 1 충족). 상세: `docs/ai/agents/04_clinical_slot.md`. -- **상태:** 컨테이너만 구축됨(`mode="experimental_unpopulated"`) — 라이브 실채움은 이 에이전트의 RAG-arm 인증(아래 "인증 상태" 절)에 게이트된다. RAG arm이 EXPERIMENTAL/UNCERTIFIED로 잔류하는 한 실채움 대상 라이브 데이터가 없다. -- 근거: `discussion.md` PLAN-2026-W28-H Track B, REV-013 §3/§4; `error.md` BUG-019 "Track B" subsection; `development_report.md` DR-010 §3. - -## 핵심 동작 - -1. **2단계 분리**: Stage 1(코드 검색)과 Stage 2(LLM 1회)는 서로 다른 실행 단위다. 이 에이전트 클래스는 Stage 2만 구현한다. -2. **위험≠도메인 이중 강제**: 프롬프트 절대 규칙 2 + 코드 레벨 위험-어휘 필터. 프롬프트 문구 단독 강제는 이전 버전(v1)에서 라이브 위반이 확인되었다(VAL-006/REV-008). -3. **근거 없는 후보 미출력**: 스키마(`min_length=1`) + 캐스케이드(0개 남으면 탈락) 이중 보장. -4. **정직한 강등 보고**: Stage 1이 `llm_only`로 강등되어도 `retrieval_meta.mode`는 실제 값을 그대로 보고한다 — RAG로 위장 보고하지 않는다. -5. **Never-crash 원칙**: Stage 1/Stage 2 모두 실패를 삼키고 명시적 사유와 함께 저하된 출력을 반환한다. 예외를 상위로 전파하지 않는다(라우트 핸들러의 500 응답은 별개 — 에이전트 내부 로직 자체는 크래시하지 않는다는 의미). - -## 안전 제약 - -1. **AI는 진단하지 않는다.** domain/department는 항상 "후보"로만 제시하며 확정 진단을 산출하지 않는다. -2. **위험 표현은 domain evidence로 사용하지 않는다.** 프롬프트 규칙과 코드 레벨 필터(`f2_grounding.check_evidence`) 이중 강제. Safety 판정을 대체하거나 우회하지 않는다. -3. **화이트리스트 미통과 근거는 산출물에 남지 않는다.** source_id/quote가 실제 제공 자료와 일치하지 않으면 거부되고, evidence가 소진된 후보는 산출물에서 제거된다. -4. **standalone 라우트의 한계를 명시한다.** `POST /ai/domain/infer`를 단독 호출하는 경우 캐스케이드/감사 델타가 적용되지 않는다는 점을 호출자에게 알려야 한다. -5. **RAG arm은 EXPERIMENTAL이다.** ADR-015 조건 충족 전까지 RAG 경로의 결과를 인증된 것으로 취급하지 않는다. - -## 실패 시 대응 - -| 실패 유형 | 대응 | -|---|---| -| Stage 1 DB/임베딩 실패 | `mode="llm_only"`로 강등, 예외를 삼키고 로그만 기록. Stage 2는 정상 진행 | -| Stage 2 LLM primary 실패 | fallback 1단계 시도 | -| Stage 2 primary+fallback 모두 실패 | 빈 `domain_candidates` + `reason_summary`에 사유 기록. 크래시하지 않음 | -| LLM 응답 JSON 파싱/스키마 검증 실패 | 빈 `domain_candidates` + `reason_summary`에 파싱 실패 사유 기록 | -| evidence 화이트리스트 전량 거부 | 해당 candidate가 산출물에서 탈락(다른 candidate에는 영향 없음) | -| 라우트 레벨 미처리 예외 | HTTP 500, "Domain inference failed" (`routes/domain.py:52-54`) | - -## 관련 파일 - -| 파일 | 위치 | -|---|---| -| 에이전트 구현 | `apps/ai-server/src/agents/domain_inference.py` | -| 스키마 | `apps/ai-server/src/schemas/domain_inference.py` | -| 검증 하네스 | `apps/ai-server/src/f2.py` | -| 근거 화이트리스트/위험-어휘 필터 | `apps/ai-server/src/eval/f2_grounding.py` | -| Stage 1 검색 프리미티브 | `apps/ai-server/src/rag/retrieval.py` (`retrieve_domain_chunks`) | -| API 라우트 | `apps/ai-server/src/routes/domain.py` | -| 시스템 프롬프트 | `docs/ai/prompts/domain_inference/v1.system.md`(롤백 대비), `v2.system.md`(라이브) | -| 라우팅 설정 | `apps/ai-server/src/routing/agent_model_registry.yaml` (`domain_inference` 항목) | -| 관련 결정/리뷰 | ADR-014, ADR-015, ADR-016, ADR-017, REV-006/007/008/009/010/011/012/013, VAL-006, VAL-010, BUG-016(resolved), BUG-017(open), BUG-019(open) (`discussion.md`, `error.md`) | -| AI 예상질환 스키마 (Track B) | `apps/ai-server/src/schemas/ai_predicted_disease.py` | -| HPI 격리 adversarial 테스트 (Track B, qa) | `apps/ai-server/tests/test_hpi_isolation.py` | -| continuous_test.py 하네스 (Track C) | `apps/ai-server/src/continuous_test.py` |