언제 쓰나
텍스트 한 덩어리(임상시험 제목, 논문 제목, 공고문)를 암 여부 → 암종으로 나눠야 할 때. 담도암 환자에게 어떤 임상시험이 있는지 찾는 것 같은 조합 작업의 핵심 부품이다.
조합 예: ctrial-collect로 임상시험을 긁고 → 이 skill로 암종을 붙이고 → 담도암(isBileDuctCancer)만 필터 → regimen-extract로 레지멘 대조.
정본 코드
~/Documents/IMOK/ctrial-auto/app/
services/clinical_classification_service.py 296줄 메인 로직
utils/oncology_constants.py SHEET_MAPPING, CANCER_FIELDS
prompt/oncology_detection.md 1단계: 암 여부
prompt/cancer_typing.md 2단계: 암종 분류
prompt/apply_feedback.md 3단계: 피드백 보정바로 쓰는 코드
scripts/feedback_guard.py — 피드백 보정이 분류를 전부 지우는 사고를 막는다. parse_json_result · cosine · find_similar(임계 0.6, 상위 3건) · apply_feedback(전면삭제 원복 + Rule 9 의도적 비움 예외).
from feedback_guard import apply_feedback
result, rolled_back, why = apply_feedback(original, improved, CANCER_FIELDS)3단 하이브리드
LLM을 언제 안 부를지가 설계의 절반이다.
1) 하드 필터 대상질환 카테고리 ∈ {Oncology, Hemato-oncology} → 암 확정, GPT 호출 생략
2) LLM 1단계 그 외에만 oncology_detection 프롬프트로 암 여부 판정
3) LLM 2단계 암일 때만 cancer_typing으로 전체 암종 JSON
4) 피드백 보정 유사 과거 사례를 찾아 apply_feedback으로 수정공공데이터 API가 이미 TRGT_DISS_CD_NM으로 암 여부를 알려주는데 그걸 다시 LLM에 묻는 건 낭비다. 하드필터로 걸러진 건은 GPT 1단계를 통째로 건너뛴다.
입력을 풍부하게 넣어라
제목만 주면 분류가 흔들린다. _build_input_block()이 있는 것만 골라 붙인다:
Clinical trial title (Korean): ...
English title: ...
Target disease name: ...
Target disease category (MFDS hint, may be inaccurate): ...
Research purpose: ... (500자로 자름)카테고리 힌트에 "may be inaccurate"를 명시한다. 안 쓰면 LLM이 부정확한 MFDS 카테고리를 맹신한다. 연구목적은 길어서 500자로 자른다.
출력 형식: [판정, 확신도, 근거]
각 암종 필드가 3원소 배열이다.
{"isBileDuctCancer": ["1", "0.9", "담도암 환자 대상 2상 시험"],
"isLungCancer": ["0", "1.0", "암 관련이 아님"]}근거를 같이 받는 게 핵심이다. 의료 데이터에서 분류만 있고 근거가 없으면 검수를 못 한다. 확신도는 낮은 것만 사람이 보게 정렬하는 데 쓴다.
최종적으로 SHEET_MAPPING(영문 필드 → 한글)으로 "위암;담도암" 형태 문자열을 만들어 DB에 넣는다. 이 매핑은 분류·내보내기가 공유하는 단일 소스여야 한다 — 두 군데 두면 시트명이 어긋난다.
피드백 RAG 자가개선
사람이 분류를 고치면 change_history 테이블에 (제목, 변경사유, 이전분류, 새분류, 제목 임베딩)을 남긴다. 다음 분류 때:
현재 제목 임베딩 → 저장된 임베딩과 코사인 유사도
→ 0.6 초과만 채택, 상위 3건
→ apply_feedback 프롬프트에 유사사례로 주입임베딩은 저장 시점 것을 재사용한다 — 매번 다시 만들면 느리고, 저장·질의가 다른 임베딩 모델을 쓰면 유사도가 무의미해진다. 그래서 _embed()를 공용 헬퍼 하나로 일원화했다.
가드가 필수다
피드백이 분류를 전부 0으로 지워버리는 사고가 실제로 났다. 그래서:
피드백 적용 전 양성(1) 암종 스냅샷 저장
→ 적용 후 양성이 하나도 안 남았고
→ 사유에 "진단/이식/표식/supportive/의료기기/신약 아님/관찰 연구" 키워드가 없으면
→ 스냅샷으로 원복 + WARNING 로그키워드가 있으면 의도적 비움(진단용·기기 평가 등 신약 연구가 아닌 경우)이라 그대로 둔다. LLM 보정 루프를 넣을 때는 "보정이 모든 결과를 지울 수 있다"를 항상 가정하고 되돌림 경로를 만들어라.
현재 상태 주의
정본에서 피드백 단계가 꺼져 있다:
sims = [] # 피드백 비활성화 (복원: sims = _find_similar_feedbacks(trial_title))복원하려면 이 한 줄을 되돌리면 된다. clinical_classification_service.py.bak_feedback_off 백업 파일도 같은 디렉토리에 있다.
함정
- JSON 파싱:
_parse_json_result()가`json블록과 생 JSON을 모두 처리하고 실패 시None을 반환한다.or {}로 받아 빈 dict 폴백. LLM 응답을json.loads한 번으로 끝내려 하지 마라. temperature=0고정: 분류는 재현성이 전부다.- 암 아님으로 판정되면 2·3단계를 건너뛴다: 비용의 대부분이 여기서 줄어든다.
연관 skill
ctrial-collect(입력 공급), regimen-extract, lit-relevance-classify(같은 피드백 RAG 패턴), llm-provider-switch