콘텐츠로 이동

[참고] 자연어 질의 시스템 설계 — 역질문으로 구체화하기

Text-to-SQL 심화에서 VectorDB + dbt 온톨로지로 SQL을 생성하는 기술을 다뤘습니다. 이 페이지에서는 한 단계 더 나아가, 사용자의 모호한 질문을 역질문으로 구체화하는 시스템을 설계합니다.


1. 문제 — "그래서 뭘 원하는 거야?"

사용자: "이번달 활성 사용자 알려줘"

이 한 문장에 모호한 부분이 최소 4가지 있습니다:

모호한 부분 가능한 해석
이번달 KST 기준? UTC 기준? 4/1~4/30? 4/1~오늘?
활성 사용자 DAU 합산? MAU? 로그인 기준? 이벤트 발생 기준?
국가 전체? 한국만? 일본 포함?
결과 형태 숫자 하나? 일별 추이? 국가별 비교?

LLM이 이 모호함을 무시하고 아무 해석이나 골라서 SQL을 생성하면, 결과는 나오지만 사용자가 원한 답이 아닐 확률이 높습니다.

모호한 채로 실행하면 생기는 일

사용자: "이번달 활성 사용자 알려줘"
시스템: (DAU 합산으로 해석) → "4,230,000명입니다"
사용자: "아니 그게 아니라 MAU... 그리고 한국만..."
시스템: (재실행) → "312,000명입니다"
사용자: "UTC 기준이야? KST로 다시..."
3번 왕복. 차라리 처음에 물어보는 게 빠릅니다.


2. 해결 — 역질문(Clarification)을 통한 구체화

핵심 아이디어

LLM이 SQL을 생성하기 전에, 모호한 부분을 감지하고 역질문으로 구체화한다.

사용자: "이번달 활성 사용자 알려줘"
시스템: "다음을 확인할게요:
        1. 기간: KST 기준 2026-04-01 ~ 2026-04-30 맞나요?
        2. '활성 사용자'는 해당 월에 1회 이상 로그인한
           유저 수(MAU)인가요?
        3. 전체 국가 기준인가요, 특정 국가만 볼까요?"
사용자: "네 맞아요, 한국만요"
시스템: → SQL 생성 → 실행 → "312,000명입니다"

한 번의 역질문으로 3번의 재실행을 줄입니다.

흐름도

┌────────────────────────────────────────────────────┐
│  사용자 입력: "이번달 활성 사용자 알려줘"                │
└───────────────────────┬────────────────────────────┘
┌────────────────────────────────────────────────────┐
│  Step 1. 의도 분석 (Intent Analysis)                  │
│                                                      │
│  LLM이 질문에서 추출:                                  │
│  - 기간: "이번달" (모호)                               │
│  - 메트릭: "활성 사용자" (모호 — DAU? MAU?)             │
│  - 필터: 없음 (모호 — 국가 미지정)                      │
│  - 결과 형태: 미지정                                   │
└───────────────────────┬────────────────────────────┘
┌────────────────────────────────────────────────────┐
│  Step 2. 모호성 감지 (Ambiguity Detection)             │
│                                                      │
│  dbt 메타데이터와 대조:                                 │
│                                                      │
│  "활성 사용자" → metrics.yml 검색                       │
│    → dau: "해당 일자에 1회 이상 로그인한 유저 수"         │
│    → mau: "해당 월에 1회 이상 로그인한 유저 수"          │
│    → 2개 매칭 → 모호함 감지!                            │
│                                                      │
│  "이번달" → 기간 파라미터                               │
│    → 서버 타임존 확인 필요 → 모호함 감지!                 │
│                                                      │
│  국가 필터 → schema.yml의 country_code                  │
│    → 허용값: KR, JP, US, TW → 미지정 → 모호함 감지!     │
└───────────────────────┬────────────────────────────┘
                  모호한 부분이 있는가?
                   ╱              ╲
                 Yes               No
                  ↓                 ↓
┌──────────────────────┐  ┌──────────────────────┐
│  Step 3a. 역질문       │  │  Step 3b. 바로 SQL    │
│                        │  │  생성 & 실행          │
│  구체화 질문 생성       │  │                      │
│  → 사용자 응답 대기     │  │  (모호함 없으면       │
│  → 응답 반영           │  │   즉시 실행)          │
│  → Step 2로 재검증     │  │                      │
└──────────┬─────────────┘  └──────────┬───────────┘
           ↓                           ↓
┌────────────────────────────────────────────────────┐
│  Step 4. SQL 생성 (Text-to-SQL)                      │
│                                                      │
│  VectorDB에서 유사 쿼리 검색 (RA-SQL)                  │
│  + 구체화된 파라미터로 SQL 생성                         │
│  + 자기 검증 후 실행                                   │
└───────────────────────┬────────────────────────────┘
┌────────────────────────────────────────────────────┐
│  Step 5. 결과 반환                                    │
│                                                      │
│  "2026년 4월 한국 MAU는 312,000명입니다."               │
│  (+ 사용한 조건 명시)                                  │
└────────────────────────────────────────────────────┘

3. 모호성 감지 — dbt 메타데이터가 근거

역질문이 "LLM의 감"이 아니라 dbt에 정의된 메타데이터를 근거로 생성되는 것이 핵심입니다.

모호성 유형과 감지 방법

모호성 유형 감지 근거 역질문 예시
메트릭이 2개 이상 매칭 metrics.yml에서 유사 메트릭 검색 "DAU인가요, MAU인가요?"
기간이 모호 질문에 상대적 시간 표현 감지 "KST 기준 4/1~4/30 맞나요?"
필수 디멘션 누락 schema.yml의 주요 필터 컬럼 확인 "전체 국가인가요, 특정 국가만?"
결과 형태 미지정 집계 수준이 불명확 "숫자 하나? 일별 추이? 국가별 비교?"
비즈니스 용어 충돌 용어집에서 동일 용어 복수 정의 "매출은 gross 기준? net 기준?"

메트릭 매칭 예시

# dbt metrics.yml — "매출" 관련 메트릭이 3개
metrics:
  - name: gross_revenue
    label: "총 매출 (Gross)"
    description: "환불/수수료 차감   결제액"

  - name: net_revenue
    label: "순 매출 (Net)"
    description: "결제액 - 환불 - 수수료. 재무 보고 기준."

  - name: arpu
    label: "ARPU"
    description: "유저당 평균 매출"
사용자: "이번달 매출 알려줘"

시스템 내부:
  "매출" → VectorDB 검색 → 3개 메트릭 매칭
  → 모호함 감지!

역질문:
  "'매출'에 대해 다음 중 어떤 기준인가요?
   1. 총 매출 (gross_revenue) — 환불/수수료 차감 전
   2. 순 매출 (net_revenue) — 재무 보고 기준
   3. ARPU — 유저당 평균 매출"

모호하지 않으면 바로 실행

# metrics.yml — "신규 가입자"는 1개만 정의
metrics:
  - name: new_signups
    label: "신규 가입자"
    description: "해당 일자에 가입 완료한 유저 수"
사용자: "어제 신규 가입자 몇 명이야?"

시스템 내부:
  "신규 가입자" → 1개 매칭 (new_signups)
  "어제" → 명확한 날짜 (2026-04-11)
  국가 필터 → 이 메트릭은 국가별 분리 없음
  → 모호함 없음!

→ 역질문 없이 바로 SQL 생성 & 실행
→ "어제(4/11) 신규 가입자는 1,847명입니다."

핵심 원칙

모호할 때만 물어보고, 명확하면 바로 실행한다. 매번 역질문하면 사용자가 짜증납니다.


4. 역질문 전략 — 똑똑하게 물어보기

기본값 제안 (Default Suggestion)

열린 질문보다 기본값을 제안하고 확인받는 방식이 사용자 경험에 좋습니다.

# 나쁜 역질문 (열린 질문)
"기간을 지정해주세요."
"어떤 메트릭을 원하시나요?"

# 좋은 역질문 (기본값 제안)
"기간: KST 기준 2026-04-01 ~ 2026-04-30 맞나요?"
"'활성 사용자'는 MAU(월간 로그인 유저 수) 기준이 맞나요?"

한 번에 모아서 질문

모호한 점이 여러 개면 한 번에 모아서 질문합니다.

# 나쁜 패턴 (핑퐁)
시스템: "기간이 언제인가요?"
사용자: "이번달"
시스템: "국가는요?"
사용자: "한국"
시스템: "메트릭은요?"
사용자: "..."  ← 이쯤 되면 귀찮음

# 좋은 패턴 (한 번에)
시스템: "다음을 확인할게요:
        1. 기간: KST 기준 2026-04-01 ~ 2026-04-30 (맞나요?)
        2. 메트릭: MAU — 월간 1회 이상 로그인 유저 수 (맞나요?)
        3. 국가: 전체 (특정 국가만 볼까요?)

        맞으면 '확인', 수정할 부분이 있으면 알려주세요."
사용자: "한국만요"  ← 한 번에 끝

학습하는 역질문

같은 사용자가 반복적으로 같은 조건을 지정하면, 다음부터 기본값으로 적용합니다.

# 첫 번째 질의
사용자: "이번달 매출"
시스템: "국가는 전체인가요?"
사용자: "한국만"

# 두 번째 질의 (학습된 기본값 적용)
사용자: "지난달 매출"
시스템: "지난달(3월) 한국 순매출 기준으로 조회할까요?"
        ← 이전 선택을 기본값으로 제안

5. MCP 서버 설계

추가할 Tool

기존 MCP 서버에 역질문 기능을 추가합니다.

@mcp.tool()
def analyze_question(question: str) -> dict:
    """사용자의 자연어 질문을 분석하고, 모호한 부분을 식별합니다.

    반환값에 clarifications가 있으면 사용자에게 역질문이 필요하고,
    없으면 바로 SQL을 생성해도 됩니다.

    Args:
        question: 사용자의 자연어 질문
    """
    # 1. 질문에서 의도 추출
    intent = extract_intent(question)  # 메트릭, 기간, 필터 등

    # 2. dbt 메타데이터와 대조하여 모호성 감지
    ambiguities = detect_ambiguities(intent)

    # 3. 결과 반환
    return {
        "intent": {
            "metrics": intent.metrics,       # ["활성 사용자"]
            "time_range": intent.time_range,  # "이번달"
            "filters": intent.filters,        # []
        },
        "clarifications": [
            {
                "field": a.field,
                "reason": a.reason,
                "options": a.options,
                "default": a.default,
            }
            for a in ambiguities
        ],
        # clarifications가 비어있으면 → 모호함 없음 → 바로 실행 가능
    }
@mcp.tool()
def generate_sql(
    question: str,
    resolved_params: dict,
) -> dict:
    """구체화된 파라미터로 SQL을 생성합니다.

    analyze_question의 역질문이 해소된 후 호출합니다.

    Args:
        question: 원래 자연어 질문
        resolved_params: 구체화된 파라미터
            예: {"metric": "mau", "time_range": "2026-04-01~2026-04-30",
                 "country": "KR", "granularity": "total"}
    """
    # 1. VectorDB에서 유사 쿼리 검색 (few-shot)
    similar = find_similar_queries(question, top_k=3)

    # 2. 관련 스키마 검색
    schema = find_relevant_schema(question)

    # 3. 메트릭 정의 조회
    metric_def = get_metric_definition(resolved_params["metric"])

    # 4. LLM에게 SQL 생성 요청 (few-shot + 스키마 + 메트릭 + 파라미터)
    sql = llm_generate_sql(
        question=question,
        params=resolved_params,
        similar_queries=similar,
        schema_context=schema,
        metric_definition=metric_def,
    )

    # 5. 검증
    validation = validate_sql(sql)

    return {
        "sql": sql,
        "validation": validation,
        "used_metric": metric_def,
        "used_tables": extract_tables(sql),
    }

Tool 호출 흐름

LLM Agent의 동작:

1. 사용자: "이번달 활성 사용자 알려줘"

2. LLM → analyze_question("이번달 활성 사용자 알려줘")
   응답: {
     "intent": {"metrics": ["활성 사용자"], "time_range": "이번달"},
     "clarifications": [
       {"field": "metric", "options": ["dau", "mau"], "default": "mau"},
       {"field": "time_range", "default": "2026-04-01~2026-04-30"},
       {"field": "country", "options": ["전체","KR","JP","US","TW"]}
     ]
   }

3. LLM → 사용자에게 역질문 (clarifications 기반)

4. 사용자: "MAU 맞고, 한국만"

5. LLM → generate_sql(
     question="이번달 활성 사용자 알려줘",
     resolved_params={
       "metric": "mau",
       "time_range": "2026-04-01~2026-04-30",
       "country": "KR",
     }
   )

6. LLM → run_query(sql) → 결과 반환

6. UI 설계 — 질의 페이지

BI 도구(Superset 등)에 연동하거나, 독립 웹 페이지로 구현할 수 있습니다.

채팅형 인터페이스

┌─────────────────────────────────────────────────┐
│  📊 데이터 질의                                    │
├─────────────────────────────────────────────────┤
│                                                   │
│  [사용자]  이번달 활성 사용자 알려줘                  │
│                                                   │
│  [시스템]  다음을 확인할게요:                         │
│                                                   │
│           ┌─────────────────────────────────┐     │
│           │ 기간: 2026-04-01 ~ 04-30 (KST)  │     │
│           │ 메트릭: ○ DAU  ● MAU            │     │
│           │ 국가: [전체 ▾]                   │     │
│           │                                  │     │
│           │        [확인]  [수정]             │     │
│           └─────────────────────────────────┘     │
│                                                   │
│  [사용자]  한국만 볼게요 → [확인]                    │
│                                                   │
│  [시스템]  2026년 4월 한국 MAU는 312,000명입니다.    │
│                                                   │
│           ┌─────────────────────────────────┐     │
│           │ 사용한 조건                       │     │
│           │ - 메트릭: MAU (월간 로그인 유저)   │     │
│           │ - 기간: 2026-04-01 ~ 04-30 KST  │     │
│           │ - 국가: KR                       │     │
│           │                                  │     │
│           │ [SQL 보기]  [대시보드에서 보기]     │     │
│           └─────────────────────────────────┘     │
│                                                   │
│  ┌──────────────────────────────────┐  [전송]     │
│  │ 질문을 입력하세요...               │            │
│  └──────────────────────────────────┘             │
└─────────────────────────────────────────────────┘

역질문 UI 패턴

모호성 유형 UI 컴포넌트 예시
메트릭 선택 라디오 버튼 ○ DAU ● MAU
기간 확인 날짜 피커 + 확인 버튼 [2026-04-01] ~ [2026-04-30]
국가 필터 드롭다운 [전체 / KR / JP / US / TW]
결과 형태 선택지 ○ 숫자 하나 ○ 일별 추이 ○ 국가별 비교

Superset 연동 방식

[독립 질의 페이지]
  ↓ 사용자 질문 + 역질문 해소
[MCP 서버]
  ↓ SQL 생성
두 가지 경로:

  경로 A: 직접 실행
    → MCP 서버의 run_query Tool로 DB 직접 조회
    → 결과를 질의 페이지에 테이블/차트로 표시

  경로 B: Superset 연동
    → Superset SQL Lab API 호출
      POST /api/v1/sqllab/execute/
      { "database_id": 1, "sql": "SELECT ..." }
    → 결과를 Superset 차트로 렌더링
    → 또는 Superset 대시보드 링크 생성

7. 구현 로드맵

Phase 1: analyze_question 구현

- dbt manifest.json 파서에 모호성 감지 로직 추가
- 메트릭 매칭 (유사 이름 검색)
- 기간 파싱 (상대적 표현 → 절대 날짜 변환)
- 필수 디멘션 누락 감지

Phase 2: 채팅 UI 프로토타입

- 간단한 웹 페이지 (Flask/FastAPI + HTML)
- 채팅 인터페이스
- 역질문 → 응답 → SQL 생성 → 결과 표시

Phase 3: 학습 기능

- 사용자별 선호 저장 (기본 국가, 기본 메트릭 등)
- 자주 쓰는 질문 패턴 → 역질문 스킵
- 새 질문-SQL 쌍을 VectorDB에 자동 축적

Phase 4: Superset 통합

- Superset API 연동
- 결과를 차트로 시각화
- 기존 대시보드와 연결

8. 정리

사용자의 모호한 질문
dbt 메타데이터 기반 모호성 감지
역질문으로 구체화 (모호할 때만)
VectorDB + 유사 쿼리로 SQL 생성
검증 후 실행 → 결과 반환

이 시스템의 강점은 세 가지가 맞물린다는 점입니다:

구성 요소 역할
dbt 온톨로지 모호성 감지의 근거 — "활성 사용자가 2개 정의되어 있다"
VectorDB SQL 생성의 지식 — "비슷한 질문에 이런 SQL을 썼다"
역질문 루프 정확도의 보장 — "사용자가 원한 게 맞는지 확인"

역질문은 "LLM이 모르니까 물어보는 것"이 아니라, "조직의 메트릭 체계에 모호함이 있으니 명확히 하는 것"입니다. 그 근거가 dbt 코드에 있기 때문에, 역질문의 품질도 코드와 함께 진화합니다.


참고 자료


← Text-to-SQL 심화 · MCP로 자체 Semantic Layer API →

댓글