QueryRouter를 구현하고(이전 글 ) 30문항짜리 벤치마크를 처음 돌렸습니다. EX(Execution Accuracy) 66.67%, 30문항 중 20개. 네 번의 측정-개선 루프로 80%까지 올린 과정입니다.

방법은 단순했습니다. 30문항을 한 번 돌리고, 틀린 쿼리를 유형별로 나눈 다음 영향이 큰 부분만 손봤습니다. 그리고 다시 돌렸습니다. 이 과정을 네 번 반복했습니다.

출발점: 모든 쿼리를 LLM이 직접 처리하는 구조

Phase 0.5에서 Vanna에 DDL과 Glossary YAML(25개 비즈니스 용어)을 학습시킨 상태였습니다. PoC DB는 civilian7/sql-tutorial 레포에서 가져온 유통 샘플 SQLite DB입니다. 테이블 21개, 뷰 18개, 행 약 23만 개 규모입니다. 여기에 맞춰 30문항 테스트셋을 만들고 EX를 처음 측정했더니 66.67%가 나왔습니다. 30개 중 20개입니다.

막상 돌려보니 한 가지 문제가 바로 드러났습니다. 쉬운 쿼리든 어려운 쿼리든 전부 LLM이 SQL을 생성했습니다. “이번 달 총 매출은?” 같은 단순 집계도, “분기별 YoY 성장률을 구해줘” 같은 복잡한 쿼리도 둘 다 Vanna가 처음부터 끝까지 만들었습니다. LLM이 만든 SQL은 같은 질문에도 결과가 조금씩 달라질 수 있습니다. 단순 집계처럼 패턴이 뻔한 쿼리까지 LLM을 태울 이유는 없었습니다.

그래서 QueryRouter를 만들었습니다.

사이클 1: QueryRouter 도입

QueryRouter는 쿼리를 세 가지로 분류합니다.

  • DETERMINISTIC: 패턴이 정해진 쿼리. “총 매출 합계”, “카테고리별 주문 건수"처럼 SQL 템플릿으로 풀 수 있는 것들. LLM을 안 거치고 바로 SQL을 만듭니다
  • HYBRID: glossary 용어가 포함되어 있으면서 조건이 복잡한 쿼리. 규칙으로 SQL 초안을 만들고 LLM이 검증합니다
  • PROBABILISTIC: 위 두 가지에 해당하지 않는 자유 형식 쿼리. Vanna에 그대로 위임합니다

MVP라서 분류기를 거창하게 만들지 않았습니다. 키워드 매칭과 정규식으로 분기합니다. “합계”, “총”, “평균” 같은 키워드가 있으면 DETERMINISTIC 후보로 보고, glossary에 등록된 용어가 쿼리에 포함되어 있는지 체크합니다.

30문항을 돌렸더니 EX 70%. +3.33%p. 숫자만 보면 미미하지만, 안을 들여다보면 의미 있는 변화가 두 가지 있었습니다.

하나는 동의어 인식률이 33%에서 67%로 뛴 것입니다. “매출"을 “Revenue"로 물어봤을 때 이전에는 못 알아듣던 걸 Router가 glossary의 동의어 목록을 보고 잡아냅니다.

다른 하나는 P95 응답시간(전체 쿼리 중 느린 쪽 5% 구간의 값)이 26초에서 3.3초로 87% 줄어든 것입니다. DETERMINISTIC 경로는 LLM을 안 거치니까 SQL 템플릿을 바로 실행합니다. 비용도 줄고 속도도 빠릅니다.

근데 여기서 예상 못한 문제가 나왔습니다.

“상위 10명” 함정: Fake Determinism

DETERMINISTIC 경로로 분류된 쿼리가 20개였는데, 그중 7개가 틀렸습니다. 35%입니다.

대표적인 케이스가 “쿠폰 사용 상위 10명 고객"입니다. Router는 “상위"를 보고 계층 탐색 템플릿으로 보냈습니다. 하지만 실제 의도는 ORDER BY coupon_count DESC LIMIT 10이었습니다.

같은 단어지만 의미가 다릅니다. “상위 카테고리"는 계층이고, “상위 10명"은 랭킹입니다.

이런 상태가 오히려 더 위험할 수 있습니다. LLM은 맥락으로 보정할 여지가 있습니다. 하지만 DETERMINISTIC으로 분류되면 템플릿이 그대로 실행됩니다. 틀린 결과가 그대로 나갑니다.

사이클 2: top-N 패치 + sanity check

세 가지를 같이 수정했습니다.

우선 top-N 패턴을 정규식으로 잡아서 랭킹 표현이 먼저 매칭되면 HIERARCHY로 보내지 않도록 했습니다. 이 경우 HYBRID나 PROBABILISTIC으로 넘깁니다.

TOP_N_PATTERNS = [
    r"상위\s*\d+",       # "상위 10명"
    r"top\s*\d+",        # "top 5"
    r"가장\s*(많|높|큰)", # "가장 많은"
]

이 규칙을 추가하니 Fake Det Rate가 35%에서 20%까지 내려갔습니다.

다음은 sanity check입니다. DETERMINISTIC 경로로 SQL을 실행한 뒤 결과가 비정상이면 LLM fallback으로 한 번만 재시도합니다. “비정상"의 기준은 두 개, 결과가 0건이거나 NULL 비율이 70%를 넘는 경우입니다.

if det_result.row_count == 0 or det_result.null_ratio > 0.7:
    return await self._execute_probabilistic(query)  # 1회만 재시도

무한 재귀에 빠지면 안 되니까 fallback은 딱 한 번만 합니다. PROBABILISTIC 결과가 나오면 그게 맞든 틀리든 그대로 반환합니다.

마지막으로 few-shot exemplar 보강입니다. 틀린 쿼리들의 오류 유형을 분류해보니 wrong_mapping(테이블/컬럼 매핑 오류)이 6건, wrong_formula(계산식 오류)가 2건이었습니다. 각 유형에 맞는 SQL 예시를 Vanna 학습 데이터에 추가했습니다. “이번 달 매출은?“이라는 질문에 DATE_TRUNC 패턴을 쓰는 SQL 예시, “순매출"을 구할 때 할인과 반품을 빼는 공식 예시 같은 것들입니다.

결과는 EX 76.67%. +6.67%p. HYBRID 분기가 1건에서 6건으로 늘었습니다. Router가 “확신 없으면 LLM에 넘긴다"는 방향으로 움직인 것입니다. 설계한 의도대로 동작한 결과입니다.

사이클 3: is_active 함정, 그리고 정답이 틀릴 수 있다는 것

wrong_mapping 중에 한 패턴이 계속 잡혔습니다. 고객 테이블을 조회할 때 is_active=true 필터를 안 넣는 겁니다. 비활성 고객(탈퇴, 휴면)이 포함되면 집계가 달라집니다. few-shot으로 예시를 여러 번 줘도 안 먹혔습니다.

여기서는 접근을 바꿨습니다. 이건 지식 문제가 아니라 정책입니다. “활성 고객만 집계한다"는 건 비즈니스 규칙이고, LLM이 추론할 영역이 아닙니다. 그래서 sqlparse로 SQL을 파싱해서, is_active 컬럼이 있는 테이블이 쿼리에 포함되면 WHERE 조건을 강제로 붙이도록 했습니다.

단순 문자열 치환이 아니라 SQL AST(Abstract Syntax Tree, SQL 문법 구조를 파싱한 트리) 레벨에서 처리했습니다. subquery 안에는 건드리지 않고, GROUP BY 뒤에 조건이 붙는 실수도 생기지 않습니다. DET/HYBRID/PROB 모든 경로에서 SQL 생성 직후, 실행 직전에 적용됩니다.

is_active rule을 넣고 벤치마크를 돌렸습니다. EX가 56.67%로 떨어졌습니다.

문제는 다른 데 있었습니다. gold SQL(정답 SQL)에 is_active=true 조건이 없었습니다. 시스템은 비즈니스 정책에 맞게 필터를 넣었는데, 정답지가 그 정책 없이 작성되어 있으니 strict 비교에서 불일치로 판정된 것입니다.

시스템의 오류가 아니라 판단 기준의 문제였습니다. BIRD 벤치마크에서도 비슷한 사례가 있는데, strict scoring이 인간 전문가 판단과 62%만 일치하고 나머지 38%는 맞는 SQL을 틀렸다고 판정하는 false negative였습니다.

사이클 3.1: gold SQL 수정 → 80%

작업은 크게 어렵지 않았습니다. gold SQL 8건에 is_active=true 조건을 추가했습니다. 시스템이 생성하는 SQL과 정답 SQL의 정책이 일치하도록 맞춘 것입니다.

재측정 결과 EX 80.00%. 목표로 잡았던 80%에 도달했습니다.

Phase 0.5부터 여기까지의 흐름을 표로 정리하면 이렇습니다.

버전EX핵심 변화틀린 건수
Phase 0.566.67%Vanna + Glossary RAG10/30
v170.00%QueryRouter 추가9/30
v276.67%top-N 수정 + sanity check + few-shot7/30
v3.180.00%is_active hard rule + Gold SQL 수정 및 동기화6/30

+13.33%p 누적 개선. easy 난이도는 100%, Fake Det Rate는 35%에서 13.3%까지 내려갔습니다. hard 난이도는 37.5%로 변화 없는데, formula/JOIN 복잡도 문제라 DataHub synonym 확장이나 exemplar 보강이 필요한 영역입니다.

이 과정에서 배운 것들

이번 케이스에서 분명해진 게 있습니다. is_active 필터는 LLM이 해결할 문제가 아니었습니다. few-shot을 여러 번 넣어도 해결되지 않았습니다. 정책은 규칙으로 강제하는 게 훨씬 빠르고 확실했습니다.

평가 기준도 설계 대상입니다. gold SQL이 정책을 반영하지 않으면, 정상 동작이 오히려 오답이 됩니다. 이번에는 8건이었지만, 테스트셋이 수백 개가 되면 나중에 잡기가 훨씬 어려워집니다. 초기에 gold SQL이 정책을 반영하는지부터 맞춰야 합니다.

변수를 하나씩 고정해서 본 게 컸습니다. Router를 넣을 때, is_active를 넣을 때, 매번 같은 30문항으로 돌려서 delta를 봤습니다. 이렇게 하지 않았으면 is_active 문제에서 “EX가 떨어졌으니 rule이 잘못됐다"고 판단했을 수 있습니다. 실제로는 rule은 제대로 동작했고 gold SQL이 문제였습니다. baseline을 고정하고 한 번에 하나만 바꾸는 게 지루해 보여도, 나중에 원인을 분리할 수 있는 유일한 방법입니다.

Fake Determinism은 규칙 분류기의 구조적 한계입니다. top-N 패턴 같은 예외 규칙으로 대응했지만, 이런 예외가 늘어나면 유지보수가 어려워집니다. Phase 2에서 LLM 분류기를 검토해야 할 수 있습니다. 당장은 보류입니다.

오류 유형 분류가 없었으면 다음 액션이 안 보였을 것입니다. “틀림"으로 뭉뚱그리지 않고 wrong_mapping, wrong_formula, wrong_aggregation으로 나눴기 때문에 “매핑 exemplar를 보강하자”, “계산식을 glossary에 명시하자” 식으로 구체적으로 움직일 수 있었습니다.

80%는 시작입니다

80%는 MVP 최소 기준이고, Phase 2 목표는 90%입니다. 그 사이에 아직 안 쓴 카드가 여러 장 남아 있습니다.

지금 EX 80%는 로컬 YAML 25개 용어 + 규칙 기반 Router + hard rule로 달성한 수치입니다. 남은 실패 사례 6건은 전부 hard 난이도로, formula/JOIN 복잡도 문제입니다. 지금 구조에서는 추가 튜닝으로 올릴 수 있는 폭이 크지 않습니다.

DataHub 연동 (v4) 이 바로 다음입니다. synonym을 확장하고 linked_columns로 테이블/컬럼 매핑을 강화하면, 현재 wrong_mapping 4건 중 일부가 해결됩니다. 동의어 인식률은 현재 67%에서 90-95%까지 올라갈 것으로 봅니다. EX 기준으로는 +3-5%p 영역입니다.

Few-shot 확대 는 hard 쿼리에 직접 영향을 줍니다. 지금 학습 데이터는 30문항 테스트셋 + 추가 예시 쿼리 수준입니다. 윈도우 함수, CTE, 다중 JOIN에 맞는 예시 쿼리를 충분히 추가하면 hard EX 37.5%가 올라갈 여지가 큽니다.

Tool Memory 자동 학습 은 Phase 1.5에서 활성화 예정입니다. 사용자 질문 → SQL 실행 성공 → Q-SQL 쌍을 자동 학습하므로, 쓰면 쓸수록 정확도가 올라가는 구조입니다. 수동 few-shot 추가와 달리 스케일이 됩니다.

LLM 분류기 전환 은 Phase 2 과제입니다. 현재 Router의 키워드 매칭 방식은 Fake Det Rate를 13.3%까지 내렸지만 구조적 한계가 있습니다. LLM 분류기로 전환하면 자연어 중의성 처리가 근본적으로 개선됩니다.

Apache AGE 그래프 쿼리 는 Phase 1.5-2 범위입니다. 현재 그래프 DB가 없어서 Multi-hop 추론이 안 됩니다. “A 공장 이슈가 B 제품 공급망에 미친 영향은?” 같은 관계 탐색 쿼리는 지금 구조로는 처리가 불가합니다. AGE가 붙으면 RCQ(Relationship CQ) 영역이 열리고, Router의 DETERMINISTIC 경로에 Cypher 템플릿이 추가됩니다.

PRD가 그리는 경로를 정리하면 이렇습니다.

접근법EX현재 상태
GPT-4o 단독~51%참고용
RAG 추가 (Vanna)~70-75%완료
온톨로지 + RAG≥ 80% (MVP)완료 (YAML)
+ DataHub + Few-shot + Tool Memory≥ 90% (Phase 2)예정
+ Graphiti + Agent Memory≥ 95% (Phase 3)예정

현실적으로 DataHub 연동 + hard 쿼리 예시 보강으로 83-87% 구간은 도달 가능합니다. 90%는 Tool Memory + LLM 분류기 전환까지 필요하고, 95%는 Graphiti까지 가야 합니다.