언제 쓰나
여러 시점의 의무기록을 시간순으로 읽어 사건(재발)이 언제 어디서 일어났는지 판정할 때. 그리고 더 넓게는 — LLM 추출 정확도를 제대로 측정하는 방법이 필요할 때. 평가 설계 부분은 재발이 아닌 어떤 추출 과제에도 그대로 쓴다.
정본 코드
~/Documents/IMOK/LungCancerRecurrence/
run_batch_analysis.py 배치 실행
analyze_gt_accuracy.py ★ GT 대비 정확도 (정본)
analyze_mismatches.py / analyze_all_mismatches.py 불일치 분석
analyze_patient_detail.py 개별 환자 추적
scripts_tmp_eval_full.py, scripts_tmp_modelcompare.py, scripts_tmp_parallel_run.py
model_comparison_260617.md ★ 모델 비교 결과 기록
backup_prompts_260526/, backup_prompts_260611/ 프롬프트 BEFORE/AFTER 스냅샷
docker-compose.yml + app.db(SQLite)바로 쓰는 코드
python3 scripts/score.py --gt gt.json --pred pred.json --spec spec.jsonscripts/score.py — 네 개 파일에 재구현돼 있던 채점 코드를 합친 것. jaccard(둘 다 비면 채점 제외) · date_diff_days · Scorer(지표별 n 분리 누적) · majority(self-consistency 다수결) · report(min_n)(표본 부족 경고). 재발 전용이 아니라 어떤 LLM 추출 채점에도 쓴다.
판정 구조
환자별 기록 타임라인
→ 상위 분석 (기록에서 재발 관련 사실 추출)
→ final_decision 재발 여부 + 재발일 + 부위 코드상위 분석과 최종 판단을 나눈다. 모델을 바꿔 비교할 때 상위 분석은 재사용하고 최종 판단만 다시 돌린다 — 비교 비용이 크게 준다(model_comparison_260617.md의 "동일 상위분석 재사용").
채점을 제대로 설계해라 — 이 skill의 핵심
정확도를 "맞다/틀리다"로만 재면 개선이 안 보인다. 이 프로젝트가 쓰는 4지표:
| 지표 | 이유 |
|---|---|
| 날짜 정확일치 | 엄격 기준 |
| 날짜 ±30일 | 재발일은 확진일·영상일·기록일이 며칠씩 다르다. 완전일치만 재면 실제 성능이 안 보인다 |
| Local 부위 Jaccard | 부위는 여러 개일 수 있다 → 부분점수 |
| Distant 부위 Jaccard | 위와 동일 |
부분점수(Jaccard)와 허용오차(±30일)가 있어야 프롬프트 개선이 지표에 나타난다. 완전일치만 보면 "3개 중 2개 맞춤"이 0점이라 개선 방향을 못 잡는다.
비교 전에 정규화가 필요하다 — normalize_llm_location()이 LLM이 뱉은 부위 표현을 GT 코드 체계로 매핑한다. 정규화 없이 문자열 비교하면 실제보다 훨씬 낮게 나온다.
parse_gt_sites(gt_distant_site, gt_local_site, gt_mixed_site) — GT가 여러 컬럼에 흩어져 있으면 파싱을 한 군데로 모아라.
홀드아웃 + 표본 수를 같이 적어라
홀드아웃 40명, 프로젝트 19
날짜 n=38~39 / Distant n=32~37 / Local n=3~7 ← Local은 신뢰도 낮음n을 지표 옆에 반드시 적는다. model_comparison_260617.md는 "Local 92.9%"에 n=7을 붙이고 본문에 "신뢰도 낮음"을 명시했다. n을 안 적으면 표본 7건짜리 92.9%가 결론을 뒤집는다.
모델 비교에서 배운 것
| 방식 | 날짜 정확 | Distant(J) |
|---|---|---|
| gpt-oss:20b 단일 | 31.6% | 73.8% |
| gpt-oss:20b 다수결 3회 | 34.2% | 76.3% |
| llama3.3:70b 단일 | 41.0% | 51.9% |
- self-consistency(같은 입력 3회 호출 후 다수결): +2~3%p 일관 개선. 비용 3배지만 저위험.
- 단일 최강 모델은 없다. 날짜는 llama가, 부위는 gpt-oss가 낫다 → 항목별 하이브리드가 현실적.
- 실패도 기록해라:
qwen3.5:35b-a3b는 서버에서 사실상 무응답(간단 호출도 12초+ 타임아웃), 72분에 10명도 처리 못 해 제외. 왜 제외했는지 남기지 않으면 다음 사람이 또 시도한다.
프롬프트 변경은 스냅샷으로 관리
backup_prompts_YYYYMMDD/ 폴더에 변경 대상 파일의 *_BEFORE.* / *_AFTER.*를 함께 넣고 README에 원본 경로와 롤백 명령을 적는다:
final_decision_BEFORE.py → src/final_decision.py
ground_truth_api_BEFORE.py → src/backend/controllers/ground_truth_api.py프롬프트를 고치면 지표가 오르내린다. 되돌릴 수 있어야 실험을 과감하게 한다. git이 있어도 "이 지표는 어느 프롬프트에서 나온 건지"를 한 폴더로 묶어두는 게 훨씬 빠르다.
불일치를 봐야 개선된다
analyze_mismatches.py가 LLM 예측(extract_llm_prediction)과 분석 근거(extract_analysis_details)를 뽑아 GT와 다른 케이스를 정리한다. analyze_patient_detail.py로 개별 환자를 파고든다.
틀린 케이스를 안 읽고 프롬프트를 고치는 건 추측이다. 지표는 어디가 틀렸는지 안 알려준다.
함정
- 오래 걸리는 배치는 재시도 스크립트를 따로 둔다(
scripts_tmp_rerun_errors.py). 전체 재실행은 최후 수단. - 병렬 실행(
scripts_tmp_parallel_run.py) 시 온프렘 서버 부하를 확인해라 — 위 qwen 사례가 그 결과다. app.db-shm/app.db-wal이 남아 있다. SQLite WAL 모드이므로 DB를 복사할 때 세 파일을 함께 옮겨야 한다.
연관 skill
tnm-staging-extract·pathology-llm-extract(추출 대상), excel-case-validator, llm-provider-switch