Skip to main content

Command Palette

Search for a command to run...

법률 AI 검색 실험기 (8) — 1차 아키텍처 확정: 실험에서 운영으로

Updated
6 min read

실험이 끝나는 순간은 생각보다 조용하다. 극적인 성능 점프가 아니라, "더 이상 구조를 바꿔도 의미 있는 차이가 나지 않는다"는 판단이 쌓이면서 자연스럽게 온다. 법률 검색 서비스의 RAG 파이프라인을 약 한 달간 실험한 끝에, 나는 1차 아키텍처를 확정하고 운영 전환 준비에 들어갔다. 이 글은 그 과정에서 내린 결정들과 그 이유를 정리한 기록이다.


최종 아키텍처: 여섯 단계의 파이프라인

확정된 구조는 다음과 같다.

질문
-> PreRewriter (질문 변환)
-> Vector 검색 / Graph 검색 병렬 수행
-> Hybrid Merge
-> 조건부 Rerank (multi_issue만)
-> Answer 생성

일반적인 RAG 시스템이 query -> retrieve -> generate의 세 단계로 설명되는 것과 비교하면, 단계가 더 세분화되어 있다. 업계에서도 프로덕션 RAG 시스템은 단순한 세 단계 구조를 넘어서, 쿼리 변환(query rewriting), 하이브리드 검색(hybrid retrieval), 리랭킹(reranking) 같은 중간 레이어를 독립적으로 관측하고 교체할 수 있는 모듈형 구조로 진화하고 있다. 내가 도달한 구조도 결국 같은 방향이었다.

각 단계를 왜 이렇게 나눴는지 설명한다.

PreRewriter: 질문을 검색용 표현으로 변환

사용자의 자연어 질문을 그대로 벡터 검색에 넣으면 잘 되는 경우도 있지만, 복합 질문에서는 한계가 뚜렷했다. PreRewriter는 질문을 "법적 결론"이 아니라 "검색을 잘하기 위한 표현"으로 바꾸는 역할을 한다.

출력은 단일 구조다. 질문 유형(single, multi_issue, calculation, procedure)을 분류하고, 벡터 검색용 대표 문장과 서브쿼리, 그래프 검색용 키워드와 법률명을 함께 생성한다. 하나의 질문에서 벡터와 그래프가 각각 다른 표현을 받는 셈이다.

다만 운영에서는 PreRewriter의 위치를 "기본값"이 아니라 "조건부 보조"로 잡기로 했다. 실험 결과, 원문 질문(raw query)을 그대로 쓰는 것이 가장 안정적인 baseline이었고, PreRewriter가 항상 baseline을 넘지는 못했기 때문이다. raw query를 완전히 치환하는 것은 금지하고, 병렬 후보 생성용으로 활용하는 방향이 안전하다.

벡터 검색과 그래프 검색의 병렬 구조

검색은 두 개의 독립된 경로(lane)로 동시에 수행된다.

벡터 검색은 pplx-embed-v1-4b 임베딩 모델 기반의 dense + sparse 하이브리드 방식이다. 질문의 의미와 유사한 조문을 넓게 회수하는 역할이고, 1차 anchor를 잡는 기본축이다. 프로덕션 RAG에서도 벡터 검색과 BM25 같은 sparse 검색을 병렬로 돌리고 결과를 합치는 하이브리드 접근이 recall을 높이는 표준적인 방법으로 자리 잡고 있다.

그래프 검색은 Neo4j 기반이다. 조문 간 의미 관계를 Concept 노드와 typed edge(NEXT_STEP, REQUIRES, CALCULATION_INPUT, LIMITS 등)로 표현해서, 벡터 검색이 놓치는 보조 조문을 회수한다. 예를 들어 전세 보증금 관련 질문이 들어오면, 벡터는 대항력 조문을 잡고, 그래프는 우선변제권이나 임차권등기명령처럼 함께 필요한 조문들을 끌어온다.

중요한 것은 그래프를 "벡터의 후처리"가 아니라 "독립적인 retrieval lane"으로 취급했다는 점이다. 벡터가 anchor를 잡고, 그래프가 그 anchor를 보강하는 구조다.

Hybrid Merge: recall을 해치지 않는 합치기

두 lane의 결과를 병합할 때 가장 조심한 원칙은 recall 보존이었다. 특정 lane 하나가 나머지를 압도하지 않도록, weighted RRF(Reciprocal Rank Fusion) 계열 방식에 graph reserve를 결합했다. 그래프가 찾아온 보조 조문이 tail에서 완전히 사라지지 않게 하는 것이 핵심이었다.

이 단계는 실험 과정에서 과적합 위험이 가장 컸다. 특정 질문 하나를 살리려고 수치를 조정하면 다른 질문에서 깨지는 패턴이 반복됐기 때문에, 특정 문제 해결용 튜닝 대신 일반화 가능한 구조 정책만 채택했다.

조건부 Rerank: 필요한 곳에만 쓴다

Rerank를 모든 질문에 적용하는 것이 아니라, multi_issue 유형에만 적용하기로 확정했다. 이 결정의 근거는 명확했다.

  • single, scenario, tax 유형: retrieval만으로 이미 충분히 안정적. rerank를 걸어도 이득이 거의 없고 latency만 늘어남
  • multi_issue 유형: 여러 쟁점의 facet coverage를 균형 있게 맞추는 후단 판단이 필요. rerank로 top10, top20 순위를 당기는 효과가 뚜렷함

조건부 rerank라는 선택은 비용 대비 효과를 극대화하는 실용적 판단이었다.


프롬프트 설계 결정

RAG 시스템에서 프롬프트는 결국 파이프라인의 각 단계가 제 역할을 하도록 만드는 인터페이스다. 이번에 확정한 프롬프트 설계에서 가장 중요했던 원칙 두 가지가 있다.

첫째, PreRewriter 프롬프트의 "절대 금지" 규칙이다. 법적 판단이나 결론을 내리지 않고, 질문에 없는 구체적 조문번호나 확정적 결론을 추가하지 않는다. 법률명도 질문에 직접 언급되었거나 키워드로 유일하게 특정되는 경우에만 출력한다. 이 제약이 없으면 PreRewriter가 "추측"을 하기 시작하고, 검색 품질이 오히려 떨어진다.

둘째, Answer 생성에서 retrieval용 topK와 answer용 usedReferenceIds를 분리한 것이다. topK는 넓은 후보군이고, usedReferenceIds는 실제로 답변을 뒷받침하는 최소 근거다. answer 모델이 자기가 실제로 쓴 근거 ID를 명시적으로 돌려주게 함으로써, "검색은 됐지만 답변에 안 쓴 조문"과 "실제로 근거가 된 조문"을 구분할 수 있게 했다.

모델 선택도 역할별로 분리했다. retrieval과 PreRewriter에는 gemini-2.5-flash-lite, answer 생성에는 gemini-2.5-flash를 사용한다. answer 문장 품질과 보고서형 응답 구조의 안정성이 더 높은 모델이 필요했기 때문이다.


평가 기준 확정

아키텍처를 확정하려면 "무엇이 더 나은가"를 판단할 기준이 먼저 있어야 한다. 최종 평가 기준은 질문 유형별 topK recall이었다.

확정 시점의 주요 결과를 보면:

  • direct, scenario: K10부터 K50까지 전 문항 정답 회수 (20/20)
  • tax: K10부터 K50까지 전 문항 회수 (17/17)
  • multi_issue: K10에서 24/31, K40에서 전 문항 회수 (31/31)

multi_issue에서 K10과 K40 사이의 격차가 가장 컸다. 이것이 rerank를 multi_issue에만 적용하기로 한 실증적 근거이기도 하다. single 유형은 top10만으로도 충분하지만, multi_issue는 더 넓은 후보에서 추려야 한다.

한 가지 중요했던 에피소드는 benchmark 정의 자체를 수정한 경우다. multi-2 문항에서 원래 정답으로 잡았던 민법 제766조(소멸시효)를 제756조(사용자책임)로 정정했다. 질문의 직접 쟁점과 정답 정의가 불일치하는 문제를 바로잡은 것이지, 성능 수치를 올리기 위한 조작이 아니었다. 평가 기준을 스스로 검증하고 정정하는 것도 실험 프로세스의 일부다.

holdout 세트에서는 domain 수준 일반화가 확인되었으나(21/21), 조문 단위 full recall 평가는 main benchmark만큼 정밀하지 않았다. 이것은 운영 전환 이후의 과제로 남겨두었다.


운영 전환 체크포인트

아키텍처가 확정되었다고 해서 바로 운영에 넣을 수 있는 것은 아니다. 실험 코드와 운영 코드 사이에는 drift가 있고, 이를 메우는 작업이 필요하다.

내가 정리한 운영 전환 체크포인트는 다음과 같다.

검색 인프라 정비. 실험에서는 8개 컬렉션(law_articles, ordinance_articles, precedents, legal_interpretations, ministry_interpretations, administrative_trials, constitutional_decisions, committee_decisions)을 한 덩어리로 다뤘지만, 운영에서는 source별 병렬 lane 파이프라인으로 분리해야 한다. 조문은 직접 근거, 판례와 해석례는 보강 근거로 역할이 다르기 때문이다.

컬렉션 네이밍 분리. 연구용 eval_* 이름을 운영용으로 정리해야 한다. 연구 코드와 운영 코드가 같은 컬렉션을 바라보면 사고가 난다.

Qdrant 결과를 최종 원문으로 믿지 않기. 대용량 문서는 chunk로 분할되어 저장되므로, Qdrant payload는 "검색용 대표 텍스트"이지 "최종 표시용 원문"이 아니다. 검색 결과의 sourceCollection과 documentId를 기준으로 원문 저장소에서 다시 읽는 흐름이 필요하다.

chunking의 검색 의미론 이해. 긴 문서는 여러 point로 쪼개어 저장되고, 검색 시에는 documentId 기준으로 collapse한다. 판례처럼 긴 문서가 많은 source에서는 "전체 문서가 골고루 맞는지"보다 "어떤 chunk 하나가 강하게 맞는지"에 더 민감해진다. 이 특성을 전제로 랭킹을 봐야 한다.

Top-K를 너무 일찍 줄이지 않기. K20과 K50 사이 차이가 실제로 컸다. 운영 초기에는 1차 회수를 넉넉하게 가져가고, 후단 merge와 selection에서 정리하는 방향이 안전하다.

sourceType 기반 공통 reference 스키마 도입. 조문, 판례, 해석례, 결정례를 한 answer 파이프라인에서 다루려면, 검색 결과를 referenceId, sourceType, sourceCollection, documentId, title, displayText, score 같은 공통 구조로 통일해야 한다.

구현 우선순위는 source별 fan-out 검색기부터 시작해서, lane별 retrieval unit 정리, 공통 reference 스키마 도입, 원문 재조회 흐름 구축, 조문 anchor + 보조 source merge 규칙 확정, 조건부 rerank 연결, 마지막으로 graph lane 연결 순서로 잡았다.


1차 마무리 회고

한 달간의 실험을 마무리하며 느낀 것들이 몇 가지 있다.

구조 선택이 모델 선택보다 중요했다. 임베딩 모델을 바꾸는 것보다, 벡터와 그래프를 병렬 lane으로 분리하고 hybrid merge 정책을 잡는 것이 성능에 더 큰 영향을 줬다. 업계 연구에서도 chunking 전략이 임베딩 모델 선택보다 retrieval 정확도에 더 큰 제약이 된다는 결과가 있는데, 내 경험도 비슷했다. 개별 컴포넌트의 품질보다 컴포넌트 간 연결 방식이 전체 성능을 결정한다.

"항상 좋은" 전략은 없었다. PreRewriter가 대표적이다. 구조적으로 의미 있는 시도였지만, 모든 질문에서 항상 이득을 주지는 않았다. rerank도 마찬가지다. 전체에 걸면 latency만 늘고, multi_issue에만 걸면 효과적이었다. 결국 "언제 쓸 것인가"를 결정하는 것이 "무엇을 쓸 것인가"만큼 중요했다.

실험 코드와 운영 코드의 drift는 불가피하다. 실험 중에는 빠르게 검증하기 위해 코드를 자주 바꾸고, 결과적으로 "최종 선택"과 "현재 코드 상태"가 일치하지 않는 구간이 생긴다. 재실행했을 때 rerank latency가 0으로 찍히는 것을 보고, 코드가 곧 사양이 아니라는 점을 확인했다. 그래서 이 문서가 단순 실험 기록이 아니라 구현 기준 사양 문서로서의 역할을 한다.

남은 것은 연구가 아니라 구현이다. 아키텍처, 모델, 프롬프트, 평가 기준이 모두 확정되었다. 이제 해야 할 일은 이 사양을 안정적인 운영 코드로 옮기는 것이다. PreRewriter 결과를 서비스 요청 경로에 연결하고, 벡터/그래프 병렬 retrieval을 서비스 로직으로 분리하고, hybrid merge와 조건부 rerank를 연결하고, answer 생성과 reference selection을 붙이는 일이 남았다.

1차 아키텍처 확정은 끝이 아니라 운영이라는 다음 단계의 시작이다. 실험에서 확인한 것들이 실제 서비스에서도 동일하게 작동하는지, 그것을 증명하는 과정이 이제부터 시작된다.

More from this blog

법률 AI 검색 실험기 (10) — Query Prep 마무리: 무엇을 남기고 무엇을 버렸나

전처리 파이프라인을 "마무리"한다는 것 RAG 파이프라인에서 전처리(query preparation)는 사용자 질문과 검색 엔진 사이의 번역 계층이다. 질문을 그대로 벡터 검색에 넣는 것과, 질문을 구조화하고 어떤 소스를 열지 먼저 정하는 것은 검색 품질에서 체감할 수 있는 차이를 만든다. 이 프로젝트에서는 법률 QA를 다루고 있고, 검색 대상이 조문, 판례, 유권해석, 행정심판 등 8개 소스 레인에 걸쳐 있다. 그만큼 전처리 단계가 감당해야 ...

Apr 28, 20265 min read3

법률 AI 검색 실험기 (9) — Source Router: 8개 컬렉션을 지능적으로 라우팅하기

법률 QA 시스템을 만들면서 가장 먼저 부딪힌 현실이 있다. 우리가 다루는 법률 데이터는 하나의 벡터 DB에 넣고 검색하면 끝나는 구조가 아니라는 것이다. 법령 조문, 판례, 공식 법령해석, 부처 실무 해석, 행정심판 재결례, 헌재 결정, 위원회 결정, 지자체 조례까지 --- 성격이 완전히 다른 8개 컬렉션, 총 300만 건 이상의 문서가 Qdrant에 올라가 있다. 이 글에서는 사용자 질문 하나가 들어왔을 때 어떤 컬렉션을 열고, 어떤 컬렉션...

Apr 26, 20266 min read31

법률 AI 검색 실험기 (7) — Graph RAG 도입기: Neo4j로 조문 간 의미 관계 구축

법률 QA 시스템을 만들면서 벡터 검색의 근본적인 한계에 부딪혔다. 사용자가 "부당해고 당했는데 어떻게 하나요?"라고 물으면 근로기준법 28조(구제신청)는 잘 찾는데, 함께 알아야 하는 23조(해고제한)와 26조(해고예고)는 Top-50에도 들어오지 않았다. 벡터 유사도만으로는 풀 수 없는 문제였고, 그래프 기반 확장이 필요했다. 이 글은 Neo4j를 도입해 법률 조문 간 의미 관계 그래프를 구축하고, 실제로 놓친 조문을 회수하기까지의 과정을 ...

Apr 18, 20267 min read5

법률 AI 검색 실험기 (6) — 하이브리드 검색과 쿼리 분해 실험기

벡터 검색만으로 단답형 질문은 거의 100% recall을 달성했다. 그런데 "부당해고 구제절차와 관련 판례"처럼 정답이 여러 개인 복수정답 질문에서는 벡터 검색이 한계를 드러냈다. 정답 조문 중 일부만 Top-10에 들어오고, 나머지는 빠지는 문제가 반복됐다. 자연스럽게 다음 질문이 떠올랐다. 벡터만으로 안 되면 키워드를 섞으면 어떨까? 업계의 하이브리드 검색 흐름 하이브리드 검색은 RAG 파이프라인에서 이미 표준에 가까운 접근법이 됐다. D...

Apr 16, 20265 min read18
D

Dongjun's Blog

22 posts