Project 02
Insurance Workflow
보험금 지급거절 통지서를 받은 사용자가 재심의를 준비할 수 있도록 돕는 AI 워크플로우입니다. 저는 이 시스템에서 Step 3 분석 파이프라인 설계, RAG + Re-ranking 구현, HyDE 쿼리 변형 실험을 담당했습니다.
| 기간 | 2026.02 (약 2주) |
| 팀 규모 | 미니 프로젝트 팀 |
| 기술 스택 | Python 3.12, LangChain, Upstage Solar Pro 2, ChromaDB, Solar Embedding |
| GitHub | upstage-mini-PJT/upstage_workflow_pjt |
Overview
시스템은 어떻게 동작하는가
사용자가 보험 거절 통지서를 업로드하면, 내부적으로 두 단계가 순차 실행됩니다.
Step 1/2 온보딩은 거절 사유를 파싱하고, 약관을 검색하고, 추가 서류를 수집하는 단계입니다. Step 3 분석 파이프라인은 수집된 근거를 바탕으로 쟁점 분석, 갭 분석, 성공 확률 판정, 증거 팩 생성까지 실행합니다.
저는 이 중 Step 3 전체 설계와, Step 1/2 안에서 약관을 검색하는 RAG + Re-ranking 구조, 그리고 검색 품질을 높이기 위한 HyDE 쿼리 변형 실험을 담당했습니다.
Deep Dive 01
Step 3 — 분석 결과를 어떻게 믿을 것인가
처음 부딪힌 문제
Step 3의 목적은 "이 사용자가 재심의에서 이길 수 있는가"에 대한 분석을 내놓는 것이었습니다. 처음에는 단순히 LLM에게 수집된 증거와 거절 사유를 주고 분석 결과를 요청했습니다. 문장은 그럴듯하게 나왔지만, 곧 문제가 보였습니다.
- 출력 구조가 호출마다 달라졌습니다. 어떤 경우엔
success_probability키가 없고, 어떤 경우엔evidence_pack이 빈 배열이었습니다. - 근거 출처가 붙어있지 않은 evidence가 많았습니다. "관련 약관이 있다"는 문장만 있고 실제 조항 링크는 없었습니다.
- 성공 확률 판정이 LOW/MEDIUM/HIGH가 아닌 "높음", "중간 정도" 같은 자유 형식 텍스트로 나왔습니다.
LLM이 "잘 된 것처럼 보이는 결과"를 내놓는다는 게 오히려 위험했습니다. 결과를 자동으로 신뢰할 수 없다면, 평가 체계를 먼저 만들어야 했습니다.
그래서 평가 지표를 설계했습니다
저는 세 가지 지표로 분석 결과를 자동 검증하는 구조를 설계했습니다.
| 지표 | 측정 대상 | 계산 방식 |
|---|---|---|
schema_completeness |
출력 구조 유효성 | 5개 필수 키 중 존재하는 키 수 / 5 (0~1) |
provenance_coverage |
근거 출처 추적률 | evidence_pack 중 provenance 있는 항목 수 / 전체 (0~1) |
band_validity |
성공 확률 판정 유효성 | LOW / MEDIUM / HIGH 중 하나인지 여부 (True/False) |
5개 필수 키는 issue_tree, gap_analysis, recommended_actions, success_probability, evidence_pack입니다. 이 중 하나라도 빠지면 후속 처리에서 KeyError가 납니다.
# metrics.py 핵심 구현
REQUIRED_KEYS = [
"issue_tree", "gap_analysis", "recommended_actions",
"success_probability", "evidence_pack"
]
def schema_completeness(result):
present = sum(1 for key in REQUIRED_KEYS if key in result)
return present / len(REQUIRED_KEYS)
def provenance_coverage(result):
evidence_pack = result.get("evidence_pack", [])
if not evidence_pack:
return 0.0
covered = sum(1 for item in evidence_pack if item.get("provenance"))
return covered / len(evidence_pack)
def band_validity(result):
band = result.get("success_probability", {}).get("band")
return band in {"LOW", "MEDIUM", "HIGH"}
이 지표 설계에서 중요하게 생각한 것
세 지표는 단순히 "출력이 맞는가"를 보는 게 아니었습니다. 각각 서로 다른 실패 유형을 잡아내도록 설계했습니다.
schema_completeness는 LLM이 구조를 빠뜨리는 실패를 잡습니다.provenance_coverage는 "근거 없는 주장"을 잡습니다. 판정을 내렸는데 어떤 약관 조항 기반인지 추적할 수 없으면 쓸 수 없는 결과입니다.band_validity는 성공 확률이 자유 텍스트로 나오는 실패를 잡습니다. 후속 분류 로직이"HIGH"라는 문자열을 기대하는데"높음"이 오면 파이프라인이 조용히 깨집니다.
평가 체계를 먼저 만든 덕분에, 프롬프트를 수정할 때마다 품질이 올라갔는지 내려갔는지 바로 확인할 수 있었습니다.
Deep Dive 02
RAG + Re-ranking — 약관 검색을 어떻게 신뢰할 것인가
왜 단순 벡터 검색으로는 부족했나
약관 검색에서 처음 부딪힌 문제는 언어적 거리였습니다. 사용자의 거절 사유는 일상 언어로 쓰여 있고, 약관 조항은 법적·보험 전문 언어로 쓰여 있었습니다.
예를 들어 사용자가 "미용 목적이라고 거절당했는데 실제로 치료였어요"라고 입력하면, 임베딩 공간에서 이 문장과 "외모 개선을 직접적인 목적으로 하는 치료 행위는 급여 대상에서 제외한다"는 조항의 벡터 거리는 생각보다 멀었습니다. 단순 1회 검색으로는 관련 조항이 top-5 안에 들어오지 않는 경우가 자주 생겼습니다.
멀티 쿼리 + RRF 재랭킹 구조로 해결했습니다
1개 쿼리로 1번 검색하는 대신, 여러 쿼리를 생성해 각각 검색한 뒤 결과를 통합·재랭킹하는 구조를 설계했습니다.
Reciprocal Rank Fusion(RRF)은 여러 검색 결과 목록을 순위 기반으로 통합하는 방법입니다. 각 조항이 여러 쿼리에서 상위권에 반복 등장할수록 최종 점수가 높아지는 구조입니다. 단일 쿼리에서 낮은 순위였던 조항도, 다른 쿼리에서 높은 순위였다면 최종 결과에 올라올 수 있습니다.
임베딩 및 청킹 구조
약관 문서는 Upstage Solar Embedding(solar-embedding-1-large)으로 임베딩하고
ChromaDB에 저장했습니다. 청킹은 2단계로 처리했습니다.
- 1단계 — Markdown 구조 분할:
MarkdownHeaderTextSplitter로 문서(#), 섹션(##), 조항(###) 단위로 먼저 나눕니다. - 2단계 — 길이 분할:
RecursiveCharacterTextSplitter로 청크 크기 500자, 오버랩 30자로 재분할합니다.
각 청크에는 policy_date, policy_title, chunk_id 메타데이터를 붙여서
검색 결과에서 어떤 약관의 어느 조항인지 추적할 수 있게 했습니다.
이 메타데이터가 나중에 provenance_coverage 지표에서 출처 추적 기반이 됩니다.
Deep Dive 03
HyDE 쿼리 변형 — 어떤 검색 전략이 실제로 더 나은가
HyDE란 무엇인가
HyDE(Hypothetical Document Embeddings)는 사용자 질문을 그대로 임베딩하는 대신, "이 질문에 대한 이상적인 답변 문서"를 LLM으로 먼저 생성한 뒤 그것을 임베딩해서 검색하는 방법입니다.
예를 들어 "미용 목적 거절에 대한 반박 근거는?"이라는 질문이 있다면, HyDE는 먼저 "미용 목적 제외 조항의 예외 사유에 해당하는 치료 행위로는 …" 같은 가상 답변을 생성하고, 그 텍스트의 임베딩으로 약관 DB를 검색합니다. 가상 답변이 약관 언어에 더 가깝기 때문에 검색 공간에서 실제 조항과의 거리가 줄어듭니다.
단순히 HyDE를 쓰는 것만으로는 충분하지 않았습니다
HyDE를 적용했는데 오히려 성능이 떨어지는 경우가 생겼습니다. 가상 문서가 실제 약관 언어를 제대로 모방하지 못하면, 오히려 원본 쿼리보다 임베딩이 더 어긋날 수 있었습니다. 어떤 방식이 실제로 더 잘 작동하는지 직접 실험으로 확인해야 했습니다.
4가지 모드를 설계하고 비교했습니다
| 모드 | 방식 |
|---|---|
plain |
원본 쿼리를 그대로 임베딩해서 검색 |
hyde |
가상 정답 문서 생성 후 임베딩 검색 |
reverse_hyde |
거절 통보를 역방향으로 분석한 가상 문서 생성 후 검색 |
hybrid_hyde |
plain + hyde 결과를 RRF로 통합 |
16개 시나리오(실제 보험 거절 케이스 기반)로 각 모드를 돌리고, 점수를 비교했습니다.
| 모드 | 평균 점수 | 비고 |
|---|---|---|
plain |
49.6 | 베이스라인 |
hyde |
48.9 | plain보다 낮음 — 가상 문서가 약관 언어와 어긋난 경우 역효과 |
reverse_hyde |
61.9 | 거절 사유 역방향 분석이 약관 언어에 더 가깝게 매핑됨 |
hybrid_hyde |
66.6 | 최고 성능 — 단일 전략의 약점을 상호 보완 |
실험에서 배운 점
단순 HyDE(hyde 모드)가 plain보다 낮은 점수가 나온 게 흥미로웠습니다. 이유를 분석해보면, 생성된 가상 문서가 "일반적인 의료 설명"처럼 나오는 경우가 있었는데 이는 실제 약관 조항 언어("급여 대상에서 제외", "직접 목적" 같은 표현)와 오히려 더 멀어지는 결과를 냈습니다.
반면 reverse_hyde는 "거절 통보 자체를 역방향으로 해석한 문서"를 생성하기 때문에,
거절 사유에 사용된 약관 언어가 자연스럽게 포함됩니다. 검색 공간에서 실제 조항과 더 가까운 위치에 놓이게 됩니다.
hybrid_hyde가 가장 좋았던 이유는 단일 전략의 약점을 상호 보완했기 때문입니다.
plain이 잡는 직접 키워드 매칭과 hyde가 잡는 의미적 유사성을 RRF로 통합하면,
어느 한 방식이 놓치는 케이스를 다른 방식이 커버합니다.
이 실험을 통해 "더 복잡한 방법이 항상 더 좋은 게 아니다"는 걸 실제 수치로 확인했습니다. hyde가 plain보다 낮게 나온 케이스를 구체적으로 분석해서 reverse_hyde 아이디어로 이어진 것처럼, 실패 케이스를 보는 것이 다음 설계의 출발점이 됐습니다.
Results
결과와 배운 점
| 항목 | 내용 |
|---|---|
| HyDE 실험 | 4가지 모드 × 16 시나리오 비교 — hybrid_hyde 66.6점으로 최고 성능 |
| 평가 지표 | schema_completeness / provenance_coverage / band_validity 3종 자동 검증 체계 |
| 검색 구조 | 멀티 쿼리 + RRF 재랭킹으로 단일 쿼리 대비 검색 커버리지 향상 |
| 출처 추적 | evidence_pack의 모든 항목에 약관 조항 provenance 연결 |
제가 배운 점
- 검색 품질과 생성 품질은 분리해서 봐야 합니다. RAG 시스템에서 최종 출력이 나쁠 때, 원인이 LLM인지 검색인지 구분하기 어렵습니다. 평가 지표를 검색 단계와 생성 단계로 나눠서 측정하면 어디를 고쳐야 할지 명확해집니다.
- 복잡한 방법이 항상 더 좋지는 않습니다. HyDE는 이론적으로 맞지만 실제로 plain보다 낮게 나왔습니다. 실험으로 확인하지 않으면 틀린 방향으로 최적화할 수 있습니다.
- 근거 없는 결과는 결과가 아닙니다. provenance_coverage를 지표로 만든 이유는, 아무리 그럴듯한 분석이라도 어떤 약관 조항에 기반했는지 추적할 수 없으면 실제 재심의에서 쓸 수 없기 때문입니다.