법률 AI 검색 실험기 (10) — Query Prep 마무리: 무엇을 남기고 무엇을 버렸나
전처리 파이프라인을 "마무리"한다는 것
RAG 파이프라인에서 전처리(query preparation)는 사용자 질문과 검색 엔진 사이의 번역 계층이다. 질문을 그대로 벡터 검색에 넣는 것과, 질문을 구조화하고 어떤 소스를 열지 먼저 정하는 것은 검색 품질에서 체감할 수 있는 차이를 만든다.
이 프로젝트에서는 법률 QA를 다루고 있고, 검색 대상이 조문, 판례, 유권해석, 행정심판 등 8개 소스 레인에 걸쳐 있다. 그만큼 전처리 단계가 감당해야 할 범위가 넓었다. 처음에는 세 축을 세웠다.
- prerewriter: 질문을 구조화하고 검색 힌트를 생성
- source-router: 어떤 소스 레인을 활성화할지 결정
- filter: payload 기준으로 검색 범위를 추가로 좁힘
세 축 모두를 운영 가능한 상태로 올리는 것이 원래 목표였다. 하지만 결론적으로 두 축만 남기고 하나는 보류했다. 이 글은 그 결정의 과정과 근거, 그리고 "마무리"라는 것이 실제로 무엇을 의미하는지를 정리한다.
최종 운영 구조: prerewriter + source-router
현재 query-prep의 운영 구조는 다음과 같다.
질문
-> prerewriter
-> source-router
-> query-prep handoff
-> retrieval
-> merge / rerank
-> answer
prerewriter는 raw 질문을 대체하지 않으면서, retrieval에 필요한 구조화된 힌트를 만든다. queryType, 벡터 검색용 searchQuery와 subQueries, 키워드, 그래프 검색용 법률명 등이 여기서 나온다. 원래 질문을 버리지 않는다는 점이 중요한데, 전처리가 잘못될 경우에도 원본이 살아 있으므로 fallback이 가능하다.
source-router는 8개 소스 레인 중 어떤 것을 활성화할지 정한다. recall-priority 기준까지 반영한 실험을 거쳐 v1_6이 최종 선택됐다. 예를 들어 "국가공무원법상 징계 관련 판례"라는 질문이 들어오면, 조문 레인과 판례 레인을 동시에 열되 해석례나 위원회 결정은 열지 않는 식의 판단을 한다.
이 두 축의 조합만으로도 검색 단계에 넘길 계획(retrieval plan)은 충분히 만들어진다. 중요한 것은 downstream이 prerewriter와 router의 내부 구현을 알 필요가 없다는 점이다. 다음 단계는 buildQueryPrepHandoff라는 하나의 진입점만 보면 된다.
Filter를 보류한 이유
세 번째 축인 filter는 보류했다. "아직 안 만들었다"가 아니라 "만들 수 있지만 지금은 빼는 것이 낫다"는 판단이었다. 이유는 크게 세 가지다.
1. 실제 payload와 설계가 맞지 않았다
filter의 원래 역할은 Qdrant payload filter로 이어지는 것이었다. 처음 설계할 때는 lawNames, regions, institutions 같은 공통 필드를 모든 소스에 걸쳐 사용할 수 있으리라 가정했다.
하지만 실제 payload를 까보니 현실은 달랐다.
- 조문(
law_articles)은lawName필드가 있어 자연스럽게 filter 가능 - 조례(
ordinance_articles)는 지역이 중요하지만 payload에region필드 자체가 없음 - 판례(
precedents)는courtName,caseType,referenceLaws가 더 자연스러운 filter 후보 - 유권해석, 행정심판, 헌재결정 등은
title과summary정도만 있어 filter를 걸 근거가 부족
8개 레인에 공통 스키마를 씌우려 했지만, 실제로는 소스마다 쓸 수 있는 필드가 완전히 다른 상황이었다. 추상 스키마를 먼저 만들고 payload를 나중에 보는 순서가 거꾸로였던 셈이다.
2. 잘못된 hard filter는 recall을 직접 떨어뜨린다
법률 검색에서 filter는 양날의 검이다. 정확한 filter는 noise를 줄여주지만, 잘못된 hard filter는 정답 문서를 아예 검색 결과에서 빼버린다. 특히 판례, 유권해석, 행정심판처럼 메타데이터가 빈약한 소스에서 hard filter를 거는 것은 retrieval miss를 직접 만드는 행위다.
2025년 이후의 RAG 파이프라인 설계에서도 이 점은 공통적으로 언급된다. query expansion이나 다중 paraphrase를 통해 검색 범위를 넓히는 접근이 일반적인 추세이고, filter로 범위를 좁히는 것은 충분한 recall이 확보된 이후에 정밀하게 적용하는 것이 권장된다. 잘못된 전처리가 RAG 실패의 주요 원인이라는 분석도 여러 연구에서 반복적으로 나온다.
3. router가 이미 1차 필터 역할을 하고 있다
source-router가 레인을 선택하는 것 자체가 넓은 의미의 filtering이다. 8개 레인 전부를 여는 것이 아니라, 질문에 맞는 레인만 활성화하므로 불필요한 소스에서의 noise는 이미 상당 부분 걸러진다. 여기에 payload filter까지 추가하면 이득보다 위험이 더 크다고 봤다.
결론적으로, filter를 보류한 것은 "filter가 불필요하다"는 판단이 아니라 "지금 상태에서 안전하게 붙일 수 있는 기반이 갖춰지지 않았다"는 판단이다. 나중에 다시 시작한다면 law_articles의 lawName filter부터, 레인별로 하나씩 시작하는 것이 맞다.
인계 기준 설계: 모듈형 handoff
query-prep을 마무리하면서 가장 신경 쓴 부분은 downstream과의 경계를 어떻게 그을 것인가였다. 문서로만 "이 단계는 끝났다"고 적어두면, 다음 단계에서 결국 내부 구조를 다시 들여다보게 된다.
그래서 buildQueryPrepHandoff라는 함수를 실제 진입점으로 만들었다. 이 함수는 내부적으로 prerewriter와 source-router를 실행하고, 그 결과를 하나의 handoff 객체로 합쳐서 반환한다.
downstream이 받는 계약은 이것이다.
queryType: 질문 유형vector.searchQuery,vector.subQueries,vector.keywords: 벡터 검색 힌트graph.keywords,graph.lawNames: 그래프 검색 힌트sourceHints: 활성화할 소스 레인 목록confidence: 전처리 신뢰도
filterPlan은 계약에 optional로 존재하지만 기본적으로 포함하지 않는다.
이 구조의 핵심은 모듈 merge가 아니라 출력 계약 merge라는 점이다. prerewriter와 router는 내부적으로 여전히 분리되어 있고, 각각의 버전을 독립적으로 교체할 수 있다. 하지만 downstream은 그 내부 구조를 알 필요 없이 handoff 하나만 보면 된다.
"완료"의 기준은 무엇인가
소프트웨어에서 "완료"라는 단어는 항상 조심스럽다. 특히 실험 기반 프로젝트에서는 더 그렇다. 여기서 query-prep의 "완료"는 다음을 의미한다.
완료인 것:
- prerewriter와 source-router의 운영 버전 선정
- downstream에 넘길 출력 계약 고정
- handoff 모듈의 코드 구현
- filter 보류 결정과 그 근거 문서화
완료가 아닌 것:
- filter를 영구적으로 폐기한 것
- prerewriter나 router를 다시는 건드리지 않겠다는 것
- 전처리 파이프라인 전체의 최적화가 끝난 것
즉, "이 단계에서 더 실험하는 것보다 다음 단계로 넘어가는 것이 전체 시스템에 더 이롭다"는 판단이 완료의 기준이었다. query-prep 내부를 계속 다듬는 것보다, retrieval과 answer까지의 end-to-end 흐름을 먼저 검증하는 편이 병목을 더 빠르게 찾을 수 있다.
교훈: 완벽보다 운영 가능한 구조
이 과정에서 몇 가지 배운 것을 정리한다.
첫째, payload를 먼저 보고 설계해야 한다. filter 설계를 추상 스키마에서 시작한 것이 가장 큰 실수였다. "어떤 필드를 추출할지"보다 "실제로 어떤 필드가 존재하고 filter로 쓸 수 있는지"를 먼저 봤어야 했다. 설계가 코드보다 앞서가면, 나중에 코드가 설계를 못 따라간다.
둘째, 빼는 것도 결정이다. filter를 보류한 것은 소극적인 선택처럼 보일 수 있다. 하지만 실제로는 "recall risk를 감수하면서 불완전한 filter를 유지하는 것"과 "filter 없이 넓게 회수하는 것" 사이의 능동적 선택이었다. 특히 법률 도메인에서 검색 누락은 답변 품질에 치명적이므로, recall 확보가 precision보다 우선이라는 판단에는 지금도 변함이 없다.
셋째, 인계 가능한 상태가 완료의 진짜 기준이다. 문서만 있고 코드가 없으면 다음 단계에서 다시 내부를 파야 한다. 반대로 코드만 있고 계약이 명확하지 않으면 통합할 때 혼란이 생긴다. handoff 모듈과 출력 계약 문서를 함께 만든 것이 이번 마무리에서 가장 유용한 작업이었다.
넷째, 실험 프로젝트에서의 "완료"는 snapshot이다. 지금 시점에서 가장 합리적인 고정점을 찍은 것이지, 영구적인 결론을 내린 것이 아니다. filter는 source별 payload enrichment가 진행되면 다시 열릴 것이고, prerewriter나 router도 downstream 검증 결과에 따라 조정될 수 있다.
다음은 어디로
query-prep을 마무리한 이상, 다음은 이 결과를 실제로 쓰는 쪽이다. retrieval에 handoff를 연결하고, merge와 rerank를 거쳐 answer까지 이어지는 end-to-end 흐름을 검증해야 한다.
전처리를 오래 다듬는 것보다, 전체 파이프라인을 한 번이라도 끝까지 돌려보는 것이 지금 시점에서는 더 가치 있다. 부분 최적화에 매몰되면 전체 시스템의 병목을 놓치기 쉽다.
결국 query-prep은 "질문 구조화 + source lane 결정"이라는 역할로 정리됐다. 크지 않은 역할처럼 보일 수 있지만, 이 두 가지가 안정적으로 동작한다는 확신이 있어야 그 뒤의 모든 단계가 의미를 갖는다. 기초가 흔들리면 그 위에 무엇을 쌓아도 불안하다.
Sources:

