NL2SQL

대화 맥락 기반 NL2SQL 후속 질의 이해 및 정확도 개선

2026.08.27 · 6분 읽기 · 1,441 단어
📊 슬라이드

개요

요약

"출금만 보여줘"라는 두 번째 질문만 보면 계좌도 기간도 알 수 없다. 이전 대화를 참고해서 이런 불완전한 질문을 혼자서도 실행할 수 있는 정확한 질문으로 완성하는 과정을 정리한다.

목차

오페리아 에이전트 output 가드레일 및 다양한 노드에서 정확도 개선을 위해 구성함.

사용자는 매번 완전한 질문을 하지 않는다.

첫 질문: 급여계좌의 최근 한 달 거래내역 보여줘
다음 질문: 출금만 보여줘

두 번째 문장만 보면 어떤 계좌인지, 어느 기간인지 알 수 없다. 하지만 사람은 앞선 대화를 보고 다음과 같이 이해한다.

급여계좌의 최근 한 달 출금 거래내역을 보여줘

이 설계의 목적은 이런 불완전한 질문을 혼자서도 실행할 수 있는 정확한 NL2SQL용 질문으로 완성하는 것이다.

1. 처리 흐름

1.1 한눈에 보는 흐름

사용자 원문 보존
→ 오타·띄어쓰기 교정
→ 계좌·기간·조건 등 질문의 의미 추출
→ 독립 질문인지 후속 질문인지 판단
→ 이전 대화와 조회 결과에서 필요한 값만 재사용
→ 부족하거나 애매한 값은 사용자에게 확인
→ 완성된 질문을 NL2SQL에 전달

이 과정은 보안 검사를 통과한 뒤, Planner가 실행 계획을 만들기 전에 Coordinator에서 끝낸다.

1.2 Coordinator 상세 프로세스

Gate1 보안 검사 통과

1. 원문에서 숫자·날짜·계좌번호·식별자 보호

2. 독립적으로 준비할 작업 수행
   - 오타·띄어쓰기 교정
   - 이전 대화·NL2SQL 결과 요약 조회
   - 해당 업무의 슬롯 정의 조회

3. 교정 과정에서 보호한 값이 바뀌지 않았는지 확인

4. 교정문에서 날짜·금액·비교 조건을 코드로 파싱

5. 문장 의미와 후속 질문 여부 분석

6. 현재 질문의 조회 종류 분류

7. 후속 질문이면 이전 값 중 현재 조회와 맞는 것만 병합

8. 계좌·상품처럼 조회가 필요한 값은 후보를 찾아 해소

9. 기본값 적용, 값 검증, 부족한 필수값 계산

10. 부족하거나 애매한 값이 있으면 사용자에게 질문
    모두 채워졌으면 완성 질문 확정

Planner → NL2SQL

중요한 점은 오타 교정, 문장 분석, 이전 대화 참고를 한 문장 생성 작업으로 뭉개지 않는 것이다. 각 단계의 입력과 결과를 남겨야 어디서 의미가 달라졌는지 확인할 수 있다.

2. 원문과 완성 질문은 따로 보관한다

사용자가 입력한 원문을 직접 덮어쓰지 않는다.

용도
원문 감사, 재현, 오교정 복구
교정문 오타와 띄어쓰기를 정리한 분석용 문장
완성 질문 이전 문맥까지 반영해 NL2SQL에 전달할 문장

예를 들면 다음과 같다.

원문: 그중 지냔달 천마넌 이상 출금만
교정문: 그중 지난달 천만원 이상 출금만
완성 질문: 직전 조회 계좌에서 지난달 출금 거래 중 천만원 이상인 내역을 조회해줘

숫자, 날짜, 계좌번호, 회사명처럼 바뀌면 안 되는 값은 교정 전에 보호하고 교정 후에도 같은지 확인한다.

내부에서는 다음처럼 값을 분리한다.

{
  "originalQuery": "그중 지냔달 천마넌 이상 출금만",
  "correctedQuery": "그중 지난달 천만원 이상 출금만",
  "isFollowUp": true,
  "protectedSpans": [
    {"type": "NUMBER", "raw": "천마넌"}
  ],
  "normalizedIntent": "직전 조회 계좌에서 지난달 출금 거래 중 천만원 이상인 내역을 조회해줘"
}

normalizedIntent는 SQL문이 아니다. 이전 대화 없이 읽어도 뜻을 알 수 있도록 완성한 자연어 질문이다.

3. 형태소보다 중요한 것은 의미다

명사, 동사, 형용사를 나누는 것만으로는 SQL을 만들 수 없다. 단어가 질문에서 어떤 역할인지 알아야 한다.

질문: 올해 고객별 매출 상위 5개 보여줘

기간: 올해
분류 기준: 고객
조회 지표: 매출
정렬: 매출 내림차순
개수: 5개

형태소 정보는 내부 분석 자료로만 관리한다. NL2SQL에는 형태소 태그를 붙인 문자열이 아니라 사람이 읽을 수 있는 완성된 자연어 질문을 전달한다.

실제 분석 결과는 다음처럼 구조화할 수 있다.

{
  "originalQuery": "올해 고객별 매출 상위 5개 보여줘",
  "correctedQuery": "올해 고객별 매출 상위 5개 보여줘",
  "isFollowUp": false,
  "tokens": [
    {"surface": "올해", "lemma": "올해", "pos": "NOUN", "role": "TIME_RANGE"},
    {"surface": "고객별", "lemma": "고객", "pos": "NOUN", "role": "DIMENSION"},
    {"surface": "매출", "lemma": "매출", "pos": "NOUN", "role": "METRIC"},
    {"surface": "상위", "lemma": "상위", "pos": "NOUN", "role": "SORT"},
    {"surface": "5개", "lemma": "5", "pos": "NUM", "role": "LIMIT"},
    {"surface": "보여줘", "lemma": "보여주다", "pos": "VERB", "role": "REQUEST_ACTION"}
  ],
  "semanticSlots": {
    "metrics": ["매출"],
    "dimensions": ["고객"],
    "filters": [],
    "timeRange": {"kind": "THIS_YEAR"},
    "aggregation": "SUM",
    "sort": {"key": "매출", "direction": "DESC"},
    "limit": 5
  },
  "ambiguities": [],
  "normalizedIntent": "올해 고객별 매출 합계를 내림차순으로 정렬해 상위 5개를 조회해줘"
}

pos는 단어의 문법적 종류이고 role은 그 단어가 조회에서 맡는 실제 역할이다.

예시
NOUN 명사 고객, 매출, 올해
VERB 동사 보여주다, 비교하다
ADJECTIVE 형용사 높은, 많은
NUM 숫자 5, 천만원
METRIC 조회할 수치 매출, 잔액, 거래금액
DIMENSION 나눠서 볼 기준 고객별, 계좌별, 지역별
TIME_RANGE 조회 기간 올해, 지난달
FILTER_VALUE 조회 조건값 서울, 출금
SORT 정렬 조건 상위, 내림차순
LIMIT 가져올 개수 5개

예를 들어 "매출"이 명사라는 사실만으로는 부족하다. 이 질문에서는 조회할 수치인 METRIC이고 "고객별"은 결과를 나누는 기준인 DIMENSION이라는 의미 역할까지 알아야 SQL 조건을 올바르게 만들 수 있다.

반대로 "매출이 가장 많이 증가한 상품"처럼 증가액인지 증가율인지 알 수 없다면 임의로 하나를 고르지 않는다.

{
  "ambiguities": [
    { "field": "comparison.measure", "candidates": ["INCREASE_AMOUNT", "INCREASE_RATE"] }
  ]
}

4. 이전 대화는 답변 문장이 아니라 구조로 기억한다

최종 답변을 다시 AI로 요약하면 이전 오류가 다음 질문에서 사실처럼 굳어질 수 있다. 그래서 다음 대화에 필요한 정보는 코드가 구조화된 조회 결과에서 직접 만든다.

이전 질문: 올해 고객별 매출 상위 5개
지표: 매출 합계
분류 기준: 고객
기간: 올해
정렬: 매출 내림차순
결과 건수: 5
순위 참조: 1~5위 가능

이 정보가 있으면 다음과 같은 질문을 해석할 수 있다.

후속 질문 처리
"그중 서울만" 이전 조건에 지역=서울을 추가
"출금만 보여줘" 이전 계좌·기간을 유지하고 출금 조건 추가
"두 번째 회사는?" 이전 결과의 2위 항목을 참조
"작년과 비교해줘" 이전 지표를 유지하고 비교 기간 추가

이전 결과에 순위나 지표가 없으면 임의로 만들지 않고 사용자에게 묻는다.

다음 대화용 요약은 예를 들어 이런 구조다.

{
  "previousQuery": "올해 고객별 매출 합계 상위 5개 조회",
  "metrics": ["매출 합계"],
  "dimensions": ["고객"],
  "filters": [{"field": "기간", "value": "2026년"}],
  "sort": {"field": "매출 합계", "direction": "DESC"},
  "limit": 5,
  "resultColumns": ["고객명", "매출 합계"],
  "resultCount": 5,
  "rankReference": {"available": true, "from": 1, "to": 5},
  "completeResult": true
}

프롬프트에는 이 구조를 그대로 길게 넣기보다 필요한 항목만 짧게 직렬화한다.

<nl2sql_turn complete="true">
질의: 올해 고객별 매출 합계 상위 5개
지표: 매출 합계 | 분류: 고객 | 기간: 2026년
정렬: 매출 합계 내림차순 | 결과: 5건 | 순위참조: 1~5
</nl2sql_turn>

생성된 SQL문과 전체 결과 데이터는 일반 대화 기록에 넣지 않는다. 필요한 조건, 컬럼, 건수, 순위 가능 여부만 남기고 민감한 값은 마스킹한다.

5. 이전 값은 전부 가져오지 않는다

후속 질문이라고 해서 이전 조건을 모두 승계하면 안 된다. 현재 질문과 맞는 값만 재사용한다.

이전: 급여계좌의 지난달 출금 내역
현재: 그 계좌 잔액은?

재사용: 급여계좌
재사용하지 않음: 지난달, 출금

현재 질문에서 새로 말한 값이 있다면 이전 값보다 우선한다. 완전히 새로운 질문이면 이전 값을 가져오지 않는다.

처리 결과에는 값의 출처도 함께 남긴다.

{
  "slotCandidates": {
    "account": { "value": "급여계좌", "source": "PREVIOUS_TURN" },
    "period": { "value": "LAST_MONTH", "source": "PREVIOUS_TURN" },
    "transactionDirection": { "value": "WITHDRAWAL", "source": "CURRENT_UTTERANCE" }
  }
}

현재 발화에서 나온 값이 이전 값보다 우선하며 새 조회 종류에서 사용할 수 없는 슬롯은 병합 단계에서 제거한다.

6. 부족한 값은 슬롯필링으로 채운다

슬롯필링은 조회에 필요한 빈칸을 채우는 기능이다.

사용자: 거래내역 보여줘
필요한 값: 계좌, 기간
현재 값: 없음

시스템은 먼저 이전 대화나 계좌 목록으로 값을 찾는다. 그래도 알 수 없는 필수값만 사용자에게 묻는다.

시스템: 어느 계좌인가요? [급여계좌] [운영계좌]
사용자: 급여계좌
시스템: 기간을 선택해 주세요. [1주일] [1개월] [3개월]

문장 분석은 슬롯 후보를 제안하고 슬롯필링은 후보를 검증하고 부족한 값을 확정한다. 필요한 값이 모두 채워졌을 때만 Planner와 NL2SQL로 넘긴다.

정규화 단계가 "기간=지난달", "금액=천만원 이상", "거래방향=출금" 같은 후보를 넘기면 슬롯필링은 이전 계좌 정보와 합치고 값을 검증한다. 계좌가 여전히 없으면 "어느 계좌인가요?"라고 묻고 모든 필수값이 채워지면 다음과 같이 질문을 완성한다.

급여계좌에서 지난달 출금 거래 중 천만원 이상인 내역을 조회해줘

사용자에게 보여주는 선택지는 AI가 임의로 만들지 않고 관리자가 등록한 값이나 실제 조회 후보에서 만든다.

7. 반드시 지켜야 할 원칙

  • 원문을 보존한다
    교정문과 완성 질문은 별도 값으로 관리한다.
  • 없는 내용을 만들지 않는다
    이전 결과로 확인할 수 없으면 사용자에게 묻는다.
  • 현재 질문을 우선한다
    이전 값은 새 질문과 맞는 것만 가져온다.
  • 결과를 구조로 기억한다
    AI가 만든 답변 문장을 다시 사실 근거로 사용하지 않는다.
  • 민감정보를 보호한다
    계좌번호와 개인정보는 대화 요약에 넣기 전에 마스킹한다.
  • 완성 질문을 실제로 사용한다
    NL2SQL과 게이트웨이는 원문보다 완성 질문을 우선해야 한다.
  • 실패하면 추측하지 않는다
    원문 또는 교정문으로 안전하게 돌아가거나 사용자에게 확인한다.

8. 기대 효과

  • 오타와 띄어쓰기 때문에 조회 조건이 빠지는 문제 감소
  • "그중", "그 계좌", "지난번처럼" 같은 꼬리물기 질문 처리
  • 이전 조건을 잘못 이어붙이는 오류 감소
  • 계좌·기간이 빠진 질문을 필요한 만큼만 되묻기
  • 근거 없는 조건이나 결과를 만들어내는 할루시네이션 감소
  • NL2SQL에 전달되는 조건·기간·정렬·개수의 일관성 향상

9. 한 줄 정리

질의 정규화 및 분석은 단순한 오타 교정이 아니다. 원문·교정문·완성 질문을 따로 관리하고 단어의 문법보다 조회에서 맡는 역할을 먼저 본다. 이전 대화는 답변 문장이 아니라 구조화된 요약으로 남겨두고 후속 질문이 오면 그중 맞는 값만 가져다 쓴다. 그래도 부족한 값은 슬롯필링이 사용자에게 되묻는다. 확인할 수 없는 내용은 만들지 않고 꼭 필요한 것만 되묻는다는 원칙 하나가 꼬리물기 질문까지 안정적으로 처리하는 핵심이다.