Project 02

Insurance Workflow

보험금 지급거절 통지서를 받은 사용자가 재심의를 준비할 수 있도록 돕는 AI 워크플로우입니다. 저는 이 시스템에서 Step 3 분석 파이프라인 설계, RAG + Re-ranking 구현, HyDE 쿼리 변형 실험을 담당했습니다.

RAG Re-ranking HyDE LangChain Upstage Solar ChromaDB
기간 2026.02 (약 2주)
팀 규모 미니 프로젝트 팀
기술 스택 Python 3.12, LangChain, Upstage Solar Pro 2, ChromaDB, Solar Embedding
GitHub upstage-mini-PJT/upstage_workflow_pjt

Overview

시스템은 어떻게 동작하는가

사용자가 보험 거절 통지서를 업로드하면, 내부적으로 두 단계가 순차 실행됩니다.

graph LR INPUT["거절 통지서 업로드"] --> STEP12["Step 1/2\n온보딩"] STEP12 --> STEP3["Step 3\n분석 파이프라인"] STEP3 --> OUTPUT["재심의 전략 출력"] style STEP3 fill:#dbeafe,stroke:#2563eb,stroke-width:2px

Step 1/2 온보딩은 거절 사유를 파싱하고, 약관을 검색하고, 추가 서류를 수집하는 단계입니다. Step 3 분석 파이프라인은 수집된 근거를 바탕으로 쟁점 분석, 갭 분석, 성공 확률 판정, 증거 팩 생성까지 실행합니다.

저는 이 중 Step 3 전체 설계와, Step 1/2 안에서 약관을 검색하는 RAG + Re-ranking 구조, 그리고 검색 품질을 높이기 위한 HyDE 쿼리 변형 실험을 담당했습니다.

Deep Dive 01

Step 3 — 분석 결과를 어떻게 믿을 것인가

처음 부딪힌 문제

Step 3의 목적은 "이 사용자가 재심의에서 이길 수 있는가"에 대한 분석을 내놓는 것이었습니다. 처음에는 단순히 LLM에게 수집된 증거와 거절 사유를 주고 분석 결과를 요청했습니다. 문장은 그럴듯하게 나왔지만, 곧 문제가 보였습니다.

LLM이 "잘 된 것처럼 보이는 결과"를 내놓는다는 게 오히려 위험했습니다. 결과를 자동으로 신뢰할 수 없다면, 평가 체계를 먼저 만들어야 했습니다.

그래서 평가 지표를 설계했습니다

저는 세 가지 지표로 분석 결과를 자동 검증하는 구조를 설계했습니다.

graph LR RUN["run()"] --> SC["schema_completeness()"] RUN --> PC["provenance_coverage()"] RUN --> BV["band_validity()"]
지표 측정 대상 계산 방식
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"}

이 지표 설계에서 중요하게 생각한 것

세 지표는 단순히 "출력이 맞는가"를 보는 게 아니었습니다. 각각 서로 다른 실패 유형을 잡아내도록 설계했습니다.

평가 체계를 먼저 만든 덕분에, 프롬프트를 수정할 때마다 품질이 올라갔는지 내려갔는지 바로 확인할 수 있었습니다.

Deep Dive 02

RAG + Re-ranking — 약관 검색을 어떻게 신뢰할 것인가

왜 단순 벡터 검색으로는 부족했나

약관 검색에서 처음 부딪힌 문제는 언어적 거리였습니다. 사용자의 거절 사유는 일상 언어로 쓰여 있고, 약관 조항은 법적·보험 전문 언어로 쓰여 있었습니다.

예를 들어 사용자가 "미용 목적이라고 거절당했는데 실제로 치료였어요"라고 입력하면, 임베딩 공간에서 이 문장과 "외모 개선을 직접적인 목적으로 하는 치료 행위는 급여 대상에서 제외한다"는 조항의 벡터 거리는 생각보다 멀었습니다. 단순 1회 검색으로는 관련 조항이 top-5 안에 들어오지 않는 경우가 자주 생겼습니다.

멀티 쿼리 + RRF 재랭킹 구조로 해결했습니다

1개 쿼리로 1번 검색하는 대신, 여러 쿼리를 생성해 각각 검색한 뒤 결과를 통합·재랭킹하는 구조를 설계했습니다.

graph LR Q["사용자 입력\n(거절 사유)"] --> GEN["쿼리 변형 생성\n(HyDE 포함)"] GEN --> V1["검색 결과 1"] GEN --> V2["검색 결과 2"] GEN --> V3["검색 결과 3"] V1 --> RRF["Reciprocal Rank Fusion\n(재랭킹)"] V2 --> RRF V3 --> RRF RRF --> TOP["최종 상위 조항"]

Reciprocal Rank Fusion(RRF)은 여러 검색 결과 목록을 순위 기반으로 통합하는 방법입니다. 각 조항이 여러 쿼리에서 상위권에 반복 등장할수록 최종 점수가 높아지는 구조입니다. 단일 쿼리에서 낮은 순위였던 조항도, 다른 쿼리에서 높은 순위였다면 최종 결과에 올라올 수 있습니다.

임베딩 및 청킹 구조

약관 문서는 Upstage Solar Embedding(solar-embedding-1-large)으로 임베딩하고 ChromaDB에 저장했습니다. 청킹은 2단계로 처리했습니다.

각 청크에는 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 연결

제가 배운 점