📌 Description
OCR로 rawText/좌표는 잘 가져오는데, 이를 {약이름, 1회량, 1일횟수, 총일수} 구조로 바꾸는
파싱이 서식마다 깨진다. OcrParser(정규식+좌표)는 알려진 서식만 커버해 새 서식마다 코드를
추가해야 하고, 뭉개진 약 이름(지스로먹스장 → 지스로맥스정)은 교정할 수 없다.
파싱 단계를 LLM 추출로 검증하고, 측정 결과에 맞는 도입 방식을 정한다.
전제: #96(회귀 하네스), #99(파서 고도화) 머지. fixture 9종이 "정답"을 정의.
🎯 목표
- fixture 9종으로 파서 vs LLM(gpt-4o) 약 단위 정확도 측정
- 측정 결과에 맞는 도입 방식 결정 및 구현
- 응답 스키마(
OcrResultResponse.parsedDrugs) 유지 → 프론트 영향 0
- 키 미설정 시 LLM 자동 비활성 → 기존(파서 전용)과 동일 동작
🧪 측정 결과 (gpt-4o + 좌표 + 프롬프트 개선)
프롬프트 1차 버그: "제형어 빼라"→아모잘탄정을 아모잘탄으로 잘림.
→ "제형 접미사는 이름의 일부, 절대 떼지 마라"로 수정.
| 서식 |
파서 |
gpt-4o |
| 병원 처방전 (표 서식) |
4/4 |
0/4 (좌표 텍스트로 표 숫자 매핑 실패) |
| 약봉투 (별표/압축) |
4/4 · 9/9 |
3~4/4 · 9/9 |
| grid 영수증 |
4/5 |
5/5 (파서가 놓친 약을 잡음) |
| scattered 영수증 |
3/8 |
이름 8/8 |
→ 처방전은 파서(좌표 알고리즘), 약봉투·영수증은 LLM이 유리. 하이브리드로 결정.
🛠️ 작업 범위
하이브리드 라우팅
OCR → 병원 처방전인가? (isPrescription)
├─ YES → 파서 (좌표 알고리즘)
└─ NO → 약봉투/영수증/그 외 → gpt-4o
↓ LLM 실패·약 0개
파서 폴백
isPrescription — 서식별 고유 문구로 구분 (발행처가 약국/병원/약국으로 달라 겹치지 않음):
| 서식 |
고유 문구 |
| 처방전 |
"처방전" 제목 / "처방 의약품" / "교부번호"·"교부일" / 보험코드 줄(\d{8,10}\s+약이름) |
| 약봉투 |
"복약안내" / *약이름 별표 / "N정씩N회N일분" |
| 영수증 |
"약제비" / "계산서" / "본인부담금" |
isPrescription = 처방전 신호 있음 AND 약봉투 신호 없음 AND 영수증 신호 없음
보험코드 줄만 보면 코드 미인쇄 EMR에서 처방전을 놓쳐서, 여러 신호를 OR로 묶고 약봉투·영수증 신호로 배제.
1. DrugExtractor + LlmDrugExtractor
List<ParsedOcrData> extract(fields) — 실패 시 예외 대신 빈 리스트
- 키 미설정·API 오류 → 빈 리스트 (폴백 유도)
- sanity check:
timesPerDay 16, totalDays 190 벗어나면 null
2. OpenAiClient
- fields →
"텍스트 @(x,y)" 목록(y→x) → gpt-4o temperature 0 json_object 25s
- 프롬프트: 행 해석, 제형 접미사 유지, 용량 표기·성분명·제형어 제외, "교부일로부터 N일" 제외
- 새 의존성 없음 (
WebClient)
3. OcrParser.isPrescription(fields) public
4. OcrCommandService 라우팅 + method 로깅
5. application.yml — openai.api-key: ${OPENAI_API_KEY:} 등
6. 테스트
OcrCommandServiceTest (키 불필요) — 처방전→파서(LLM 미호출), 약봉투→LLM, LLM 실패→파서 폴백
LlmDrugExtractorComparisonTest — @Tag("integration") + OPENAI_API_KEY 있을 때만. 파서 vs LLM vs 하이브리드 리포트
✅ To-do
📏 완료 조건 (DoD)
- 키 있는 로컬: 약봉투 → LLM 경로, 처방전 → 파서 경로로
parsedDrugs 채워짐
- 키 없음/LLM 실패 → 파서로 기존과 동일 동작
OcrResultResponse 필드 변화 없음
./gradlew test 통과
📒 기타 · 알려진 한계
- LLM 비결정성:
temperature 0이어도 gpt-4o가 run마다 이름 형식 미세하게 다름. 사용자 확인 UI가 최종 방어선.
- 비용: gpt-4o 호출당
1520원. 볼륨 커지면 gpt-4o-mini + 프롬프트 강화 또는 캐싱.
- 이름 정규화(
지스로먹스장→지스로맥스정)는 gpt-4o도 못 함 → ES 대조 = 다음 이슈 (druginfo 4,745건).
- 이미지→비전 LLM은 별도 검토.
- 로컬 테스트용 컨트롤러
userId 하드코딩은 커밋 금지.
📌 Description
OCR로
rawText/좌표는 잘 가져오는데, 이를{약이름, 1회량, 1일횟수, 총일수}구조로 바꾸는파싱이 서식마다 깨진다.
OcrParser(정규식+좌표)는 알려진 서식만 커버해 새 서식마다 코드를추가해야 하고, 뭉개진 약 이름(
지스로먹스장→지스로맥스정)은 교정할 수 없다.파싱 단계를 LLM 추출로 검증하고, 측정 결과에 맞는 도입 방식을 정한다.
전제: #96(회귀 하네스), #99(파서 고도화) 머지. fixture 9종이 "정답"을 정의.
🎯 목표
OcrResultResponse.parsedDrugs) 유지 → 프론트 영향 0🧪 측정 결과 (gpt-4o + 좌표 + 프롬프트 개선)
프롬프트 1차 버그: "제형어 빼라"→
아모잘탄정을아모잘탄으로 잘림.→ "제형 접미사는 이름의 일부, 절대 떼지 마라"로 수정.
→ 처방전은 파서(좌표 알고리즘), 약봉투·영수증은 LLM이 유리. 하이브리드로 결정.
🛠️ 작업 범위
하이브리드 라우팅
isPrescription— 서식별 고유 문구로 구분 (발행처가 약국/병원/약국으로 달라 겹치지 않음):\d{8,10}\s+약이름)*약이름별표 / "N정씩N회N일분"보험코드 줄만 보면 코드 미인쇄 EMR에서 처방전을 놓쳐서, 여러 신호를 OR로 묶고 약봉투·영수증 신호로 배제.
1.
DrugExtractor+LlmDrugExtractorList<ParsedOcrData> extract(fields)— 실패 시 예외 대신 빈 리스트timesPerDay16,90 벗어나면 nulltotalDays12.
OpenAiClient"텍스트 @(x,y)"목록(y→x) →gpt-4otemperature 0json_object25sWebClient)3.
OcrParser.isPrescription(fields)public4.
OcrCommandService라우팅 +method로깅5.
application.yml—openai.api-key: ${OPENAI_API_KEY:}등6. 테스트
OcrCommandServiceTest(키 불필요) — 처방전→파서(LLM 미호출), 약봉투→LLM, LLM 실패→파서 폴백LlmDrugExtractorComparisonTest—@Tag("integration")+OPENAI_API_KEY있을 때만. 파서 vs LLM vs 하이브리드 리포트✅ To-do
DrugExtractor+LlmDrugExtractor(sanity check)OpenAiClient(좌표 텍스트, 프롬프트, gpt-4o)OcrParser.isPrescription(서식별 고유 문구)OcrCommandService하이브리드 라우팅application.ymlopenai 설정OcrCommandServiceTest,LlmDrugExtractorComparisonTestdocs/kangcheolung/issue-100-ocr-llm-hybrid.md📏 완료 조건 (DoD)
parsedDrugs채워짐OcrResultResponse필드 변화 없음./gradlew test통과📒 기타 · 알려진 한계
temperature 0이어도 gpt-4o가 run마다 이름 형식 미세하게 다름. 사용자 확인 UI가 최종 방어선.1520원. 볼륨 커지면 gpt-4o-mini + 프롬프트 강화 또는 캐싱.지스로먹스장→지스로맥스정)는 gpt-4o도 못 함 → ES 대조 = 다음 이슈 (druginfo4,745건).userId하드코딩은 커밋 금지.