자체 30문항 기준 NL2SQL 정확도를 80%까지 올렸습니다(이전 글 ).

같은 모델을 공개 벤치마크(BIRD Mini-Dev)로 돌려보니 결과가 완전히 달랐습니다. BIRD는 11개 도메인 DB에 걸쳐 500개 질문이 담긴 NL2SQL 표준 벤치마크입니다.

80% → 56%

학습한 유통 도메인에서는 잘 맞췄는데 처음 보는 도메인으로 넘어가니 절반 수준이었습니다.

이 56%를 넘겨보려고 9번 실험했습니다. (처음엔 솔직히 금방 될 줄 알았습니다.)

한 줄 결론

SQL 생성 시도 횟수를 늘린다고 될 문제가 아니라, 처음부터 한 번에 맞춰야 되는 문제였습니다.

왜 SQL을 여러 개 만들어 고르는 방식(multi-candidate)을 시도했나

SQL을 하나만 만들지 말고 여러 개를 만든 뒤에, 그중에서 가장 좋은 걸 고릅니다.

NL2SQL에서 이미 널리 쓰이는 방식입니다.

  • self-consistency (같은 질문을 여러 번 생성해 다수결로 선택)
  • execution-based selection (실행 결과로 후보를 거르는 방식)
  • multi-agent pipeline (여러 에이전트가 분업해서 SQL 생성)

BIRD 리더보드 상위 모델들도 대부분 이 방향입니다.

두 가지 가설을 세웠습니다.

  1. 한 번 생성에는 정확도 한계가 있습니다
  2. 여러 개 만들고 잘 고르면 그 한계를 넘을 수 있습니다

1. 힌트를 더 주면 좋아질까 (4번 실패)

BIRD에서는 효과가 없었습니다. 오히려 잘 맞추던 정답을 오답으로 바꿔버리기도 했습니다.

LLM이 더 잘 고르게 힌트를 줬습니다.

예를 들면 다음과 같습니다.

  • 어떤 집계 함수 써야 하는지
  • 어떤 컬럼이 “매출"인지
  • 관련 컬럼 후보 목록

결과는 간단했습니다. 자체 30문항은 유지됐지만, BIRD에서는 변화 없음 이었습니다.

일부 케이스에서는 결과가 더 나빠졌습니다. 원래 products.price를 잘 고르던 질문에서 힌트를 주자 order_items.unit_price로 바뀌었습니다. 힌트가 정답을 오답으로 바꿔버린 셈입니다.

오답의 60%는 SQL “형태"에서 나왔습니다. 컬럼 선택 문제가 아니었습니다.

예:

  • SUM(CASE WHEN ...)
  • COUNT(CASE WHEN ...)

둘 다 맞는 SQL이지만 NULL 처리 차이 때문에 결과가 달라집니다.

힌트로 컬럼을 바꿀 수는 있지만 SQL 작성 방식을 바꾸지는 못합니다.

2. 여러 개 만들고 고르면 되지 않을까 (3번 실패)

선택지를 여러 개 만들어도 답이 바뀌지 않았습니다. 결국 첫 번째로 만든 SQL을 그대로 썼습니다.

하이퍼파라미터는 k=3, temperature=0.3으로 두고 결과 기반 선택을 적용했습니다. 그래도 정확도는 56%로 그대로였습니다.

지표값
후보 3개가 같은 결과92%
첫 번째 후보 채택100%

selector가 단 한 번도 첫 번째 후보를 뒤집지 못했습니다.

“+8% 오른 것처럼 보였던” 착시

처음엔 48% → 56%로 보였습니다.

다시 채점해보니 원래도 56%였습니다. 채점 로직 변경으로 생긴 착시였습니다. (이걸 계기로 채점 드리프트 가드도 만들었습니다.)

다양성을 늘리면 될까?

  • temperature ↑
  • 프롬프트 변형 5개 추가

SQL 텍스트는 달라졌지만 실행 결과는 그대로였습니다.

62%의 질문에서 5개 후보가 전부 같은 결과

temperature를 올리고 프롬프트를 바꿔도 실행 결과는 달라지지 않았습니다.

3. 시스템이 강제로 다른 후보를 만들면? (3번 실패)

강제로 선택지를 여러 개 만들어도 selector는 정답을 고르지 못했습니다. 실행 결과만으로는 판별 자체가 불가능했습니다.

LLM이 다양성을 못 만들면 시스템이 강제로 만들어봤습니다.

실험 1: 컬럼 강제

  • 올바른 컬럼 강제 → 60% 성공
  • 잘못된 컬럼 강제 → 0% 성공

schema binding이 정확도를 결정합니다.

실험 2: selector 검증

정답 / 오답 / 그럴듯한 오답 / 변형 → 4개 후보 중 고르게 했습니다.

28.6% (거의 랜덤)

가장 치명적인 사례

DB에 저장된 실제 값은 체코어 VYBER(현금 인출)였는데 LLM은 이걸 모른 채로 SQL을 만들었습니다. 후보 4개 모두 WHERE 조건에 영어(cash withdrawal)를 썼습니다. 당연히 매칭되는 행이 없었습니다.

결과:

  • 4개 모두 빈 결과 (0 row)
  • selector는 4개가 같은 결과를 내자 “합의"로 판단
  • 오답 선택

정답이 후보에 없으면 합의를 해도 의미가 없습니다.

실험 3: 점수 기반 selector

전략을 바꿨습니다. 실행 결과만 보는 게 아니라 반환된 값의 분포, 컬럼 수, 행 수 등 여러 신호를 종합해 각 후보에 점수를 매기는 V2 selector를 만들었습니다.

결과:

  • V1: 40%
  • V2: 33%

더 정교하게 만들었는데 오히려 더 나빠졌습니다.

qid 819가 이유를 잘 보여줍니다. V2가 정답(gold) SQL에 55점, 명백히 틀린(obvious_wrong) SQL에 75점을 매겼습니다. 정답보다 오답에 더 높은 점수를 준 셈입니다.

실행 결과만으로는 어떤 SQL이 더 “맞는지” 판단할 수 없습니다.

실패해서 폐기한 시도들

세 방향 모두 폐기했습니다.

  1. 힌트 기반 보정 → 실패
  2. LLM multi-candidate → 실패
  3. 실행 결과 기반 선택 → 실패

공통 원인

문제는 “선택” 단계가 아니라 “생성하기 이전” 단계입니다.

SQL을 실행한 뒤 고르는 방식으로는 한계가 있습니다. 실행 전에 이미 올바른 테이블/컬럼이 잡혀 있어야 합니다.

그래서 방향을 바꿨다

스키마 이해를 강화해서 한 번에 맞추기

스키마 바인딩 계획(Schema Binding Plan)

SQL을 바로 만들지 않습니다.

  1. 먼저 사용할 테이블 / 컬럼 / join 조건을 JSON으로 생성
  2. 시스템이 검증
  3. 그 다음 SQL 생성

3차 실험에서 확인했습니다.

LLM은 binding만 맞으면 100% 따릅니다.

스키마 해석 단계 가 실제 병목이었습니다. SQL 생성 자체의 문제가 아니었습니다.

실험은 실패했지만 남은 자산들

실험은 실패했지만 교훈은 남았습니다.

  • 채점 드리프트 가드. 채점 로직이 조금씩 바뀌어도 이전 결과와 비교할 수 있도록 맞춰주는 코드입니다. 이게 없었다면 이번 실험이 “+8%p 성공"으로 잘못 기록됐을 것입니다.

  • signal classifier. “정답을 고를 수 있는 실마리가 있는가"를 STRONG/MISLEADING 등 4단계로 분류하는 도구입니다. “selector가 약한 건지, 판별 근거 자체가 없는 건지"를 숫자로 구분할 수 있게 만들었습니다.

  • 강제 binding 검증 코드. LLM에게 “이 컬럼을 써라"고 지시했을 때 실제로 생성된 SQL에 그 컬럼이 들어갔는지 자동으로 확인하는 코드입니다(SQL 파서 sqlglot 사용). 다음 단계 schema grounding에도 그대로 쓸 수 있습니다.

  • 중단 조건 / 실험 설계 체계. 실험 전에 “여기서 안 되면 접는다"는 기준을 미리 정해두고, 작은 스팟 체크로 빠르게 방향을 결정하는 방식입니다.

그리고 하나 더.

SOTA가 맞다고 해서, 내 문제에도 맞는 건 아닙니다.

AI 3개에게 물었더니 10개 중 8개가 multi-candidate를 추천했습니다. 실험 데이터가 없었다면 그대로 따라갔을 것입니다.

마무리

8번 글에서는 “80%는 시작"이라고 썼었는데 그건 학습된 도메인 기준이었습니다. 처음 보는 도메인(Unseen Domain)에서는 56%입니다.

이게 진짜 시작점입니다.