dev notes

Text-to-SQL 피드백 루프 설계 [3]

2026-02-2014 min read
공유

벤치마크보다 피드백#

Text-to-SQL 쪽을 보면서 점점 확실해진 게 하나 있었다. 처음 SQL을 얼마나 잘 만들었는가보다, 틀렸을 때 그걸 어떻게 다시 시스템으로 먹이느냐가 더 중요하다는 점이다.

실제로 "활성 사용자 수" 같은 질문 하나만 봐도 그렇다. 시스템은 status = 'active'를 보고 활성이라고 판단할 수 있지만, 현업에서는 최근 30일 로그인 기준을 말하는 걸 수도 있다. 이건 모델이 똑똑하냐의 문제가 아니라, 조직 안에서 그 용어가 어떻게 쓰이는가의 문제다.

그래서 결국 벤치마크 숫자만 보는 쪽보다는, 운영 중에 들어오는 피드백을 어떻게 쌓고 교정할지에 더 관심이 가게 됐다. 이번 글은 그 피드백 루프를 중심으로 적어보려고 한다.

전체 구조#

사용자가 SQL 결과를 받음
    ↓
[👍 / 👎 피드백]
    ↓
┌─ 👍 → 검증된 쿼리로 샘플 저장소에 자동 등록
│        → 다음에 비슷한 질문이 오면 RAG에서 참고
│
└─ 👎 → 에스컬레이션 (팀 알림)
         → Feedback Learner가 오류 분석
         → 용어 사전 자동 교정
         → 수정된 SQL이 있으면 샘플로 저장

1단계: 피드백 수집#

사용자가 생성된 SQL에 대해 👍 또는 👎를 남깁니다.

👍 피드백 처리:

긍정 피드백이 들어오면 해당 질문-SQL 쌍을 샘플 쿼리 저장소에 자동 등록합니다. 이 샘플은 RAG의 few-shot 예시로 활용됩니다. 비슷한 질문이 나중에 들어오면 벡터 검색에서 이 샘플이 참고 자료로 뜨게 됩니다.

이게 쌓이면 시스템이 점점 해당 도메인에 특화됩니다. 처음에는 범용 LLM의 SQL 생성 능력에 의존하지만, 피드백이 쌓일수록 조직에 맞는 패턴을 학습해나갑니다.

👎 피드백 처리:

부정 피드백은 두 가지 경로로 처리됩니다:

  1. 에스컬레이션: 담당 팀에 알림을 보내서 수동 확인을 요청합니다. LinkedIn과 당근페이에서 검증된 패턴입니다. 실패 케이스를 팀이 직접 확인하고, 올바른 SQL을 작성해서 해결하면 그 결과도 샘플로 등록됩니다.

  2. 자동 분석: Feedback Learner가 어디서 틀렸는지 분석합니다 (2단계에서 설명).

사용자가 👎과 함께 수정된 SQL을 직접 제출하는 경우가 가장 가치 있는 피드백입니다. 질문 → 잘못된 SQL → 올바른 SQL이라는 삼중 쌍이 만들어지니까요.

2단계: 피드백 학습 — 용어 매핑 교정#

부정 피드백이 쌓이면, 왜 틀렸는지 패턴이 보이기 시작합니다. 대부분의 오류는 용어 매핑 문제입니다.

예를 들어 사용자가 "활성 사용자"라고 질문했는데, 시스템이 users 테이블의 status = 'active'로 해석한 경우. 실제로는 user_activity 테이블에서 최근 30일 내 로그인 기록이 있는 사용자를 뜻하는 거였다면, 이건 용어 정의의 문제입니다.

Feedback Learner가 하는 일:

  1. 잘못된 SQL과 올바른 SQL(또는 사용자 코멘트)을 비교
  2. LLM에게 "어떤 용어가 잘못 번역됐는지" 분석을 요청
  3. 분석 결과에서 교정할 용어 매핑을 추출
  4. 용어 사전에 자동 반영
입력:
  질문: "지난달 활성 사용자 수"
  잘못된 SQL: SELECT COUNT(*) FROM users WHERE status = 'active'
  올바른 SQL: SELECT COUNT(DISTINCT user_id) FROM user_activity
              WHERE login_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)

분석 결과:
  "활성 사용자" → users.status = 'active' (X)
  "활성 사용자" → user_activity 테이블, 최근 30일 로그인 기록 (O)

교정:
  용어 사전에 "활성 사용자" 항목 추가/수정

이 교정이 반영되면 다음부터 "활성 사용자"라는 단어가 나올 때 올바른 테이블과 조건을 찾게 됩니다.

3단계: 능동적 학습 — 모르면 물어보기#

시스템이 확신이 낮을 때, 그냥 틀린 SQL을 내놓는 것보다 사용자에게 물어보는 게 낫습니다.

SQL Agent가 confidence 점수를 함께 반환하는데, 이 점수가 임계값 이하이면 Active Learner가 명확화 질문을 생성합니다.

사용자: "매출 상위 10개 보여줘"

시스템 (confidence 낮음):
  "매출"이 어떤 기준인지 확인이 필요합니다.
  - 총 매출액 기준인가요?
  - 순이익 기준인가요?
  - 최근 1개월 기준인가요, 전체 기간인가요?

사용자가 선택지를 고르면 그 정보가 SQL 생성에 반영되고, 동시에 용어 사전에도 학습됩니다. 다음에 같은 사용자가 "매출"이라고 하면 이전 선택을 기억합니다.

이 패턴의 장점은 틀린 결과를 주고 사후에 피드백을 받는 것보다, 사전에 확인해서 첫 번째 결과의 정확도를 높인다는 것입니다.

피드백 루프의 선순환#

세 단계가 합쳐지면 이런 선순환이 만들어집니다:

시스템 배포
    ↓
사용자가 질문 → SQL 생성
    ↓
👍 → 샘플 쿼리 자동 등록 → RAG 검색 품질 향상
👎 → 오류 분석 → 용어 사전 교정 → 다음 생성 시 정확도 향상
확신 낮음 → 사전 질문 → 정확한 의도 파악 → 첫 결과 품질 향상
    ↓
시간이 지날수록 도메인 특화

처음에는 범용 LLM의 능력에 의존하지만, 피드백이 쌓이면서 점점 해당 조직의 데이터와 용어에 맞춰집니다.

에스컬레이션 관리#

모든 실패를 자동으로 해결할 수는 없습니다. 에스컬레이션 시스템이 필요한 이유입니다.

에스컬레이션 대상:

  • 부정 피드백이 들어온 경우
  • SQL 생성 자체가 실패한 경우
  • 실행은 됐지만 결과가 비어있는 경우

에스컬레이션이 해결되면(담당자가 올바른 SQL을 작성하면) 그 결과가 다시 샘플 쿼리와 용어 사전에 반영됩니다. 수동 해결이지만 그 결과는 자동으로 학습됩니다.

측정해야 할 것들#

피드백 루프가 잘 동작하는지 확인하려면:

  • 긍정 피드백 비율 추이: 시간이 지나면서 올라가야 합니다
  • 에스컬레이션 빈도 추이: 시간이 지나면서 줄어야 합니다
  • 용어 사전 크기 추이: 꾸준히 늘어나야 합니다
  • 같은 유형의 오류 반복 여부: 한 번 교정된 오류가 다시 발생하면 학습이 안 된 것입니다
  • 명확화 질문 빈도: 처음에는 많지만 점차 줄어야 합니다

피드백 시스템의 테스트 가능한 설계#

피드백 루프는 "사용자가 실제로 피드백을 남기면 시스템이 개선된다"는 전체 흐름을 보장해야 합니다. 하지만 LLM과 벡터 DB를 매번 띄워서 테스트할 수는 없습니다. FeedbackStore를 외부 의존성 없이 동작하도록 설계해서, 핵심 로직을 빠르게 검증합니다.

긍정/부정 피드백 수집#

python
def test_positive_feedback(self, store):
    fb = FeedbackEntry(
        tenant_id="t1",
        question="매출 보여줘",
        generated_sql="SELECT SUM(amount) FROM orders",
        positive=True,
    )
    result = store.submit(fb)
    assert result.id is not None
    assert result.positive is True
 
def test_negative_feedback(self, store):
    fb = FeedbackEntry(
        tenant_id="t1",
        question="매출 보여줘",
        generated_sql="SELECT * FROM orders",
        positive=False,
        comment="잘못된 SQL",
        correct_sql="SELECT SUM(amount) FROM orders",
    )
    result = store.submit(fb)
    assert result.positive is False
    assert result.comment == "잘못된 SQL"

부정 피드백에는 commentcorrect_sql 필드가 있어서, 사용자가 올바른 SQL을 직접 제출할 수 있습니다. 이 삼중 쌍(질문 -> 잘못된 SQL -> 올바른 SQL)이 가장 가치 있는 피드백 데이터입니다.

만족도 통계 — 테넌트별 격리#

피드백 루프가 잘 동작하는지 확인하려면 만족도 추이를 봐야 합니다. 테넌트별로 격리된 통계가 필요합니다.

python
def test_satisfaction_rate(self, store):
    for positive in [True, True, True, False]:
        store.submit(FeedbackEntry(
            tenant_id="t1", question="q", generated_sql="sql",
            positive=positive,
        ))
    stats = store.get_stats()
    assert stats["satisfaction_rate"] == 0.75
 
def test_stats_by_tenant(self, store):
    store.submit(FeedbackEntry(tenant_id="t1", question="q1", ...positive=True))
    store.submit(FeedbackEntry(tenant_id="t2", question="q2", ...positive=False))
    store.submit(FeedbackEntry(tenant_id="t1", question="q3", ...positive=True))
 
    stats_t1 = store.get_stats("t1")
    assert stats_t1["total"] == 2
    assert stats_t1["positive"] == 2
 
    stats_t2 = store.get_stats("t2")
    assert stats_t2["total"] == 1
    assert stats_t2["negative"] == 1

멀티테넌트 환경에서 A 테넌트의 피드백이 B 테넌트의 통계에 영향을 주면 안 됩니다. get_stats(tenant_id) 호출 시 해당 테넌트의 피드백만 집계되는지를 테스트로 보장합니다.

부정 피드백 조회 — 에스컬레이션 대상 추출#

에스컬레이션 시스템이 제대로 동작하려면, 부정 피드백만 필터링해서 가져올 수 있어야 합니다.

python
def test_negative_only(self, store):
    store.submit(FeedbackEntry(...positive=True))
    store.submit(FeedbackEntry(...positive=False))
    store.submit(FeedbackEntry(...positive=False))
 
    negatives = store.get_negative_feedbacks()
    assert len(negatives) == 2
    assert all(not fb.positive for fb in negatives)
 
def test_negative_limit(self, store):
    for i in range(10):
        store.submit(FeedbackEntry(...positive=False))
    negatives = store.get_negative_feedbacks(limit=3)
    assert len(negatives) == 3

limit을 두는 이유는, 부정 피드백이 수천 건 쌓여있을 때 전부 가져오면 LLM 분석에 시간이 너무 오래 걸리기 때문입니다. 최근 N건만 가져와서 배치로 처리합니다.

Text-to-SQL은 배포한다고 끝나는 기능이 아니었다. 운영하면서 틀린 질문이 들어오고, 사람이 고쳐주고, 그걸 다시 시스템이 배우는 과정을 계속 타게 된다.

그래서 중요한 건 첫 점수보다도 같은 실수를 두 번 반복하지 않게 만드는 구조였다. 사용자가 피드백을 남길 수 있어야 하고, 그 피드백이 다시 샘플 쿼리나 용어 사전 교정으로 연결되어야 하고, 자동으로 못 풀면 사람에게 넘어가는 안전망도 있어야 한다.

결국 피드백 루프는 부가 기능이 아니라, 이 시스템이 실제 서비스에서 버티게 해주는 핵심 축에 더 가까웠다.

Connected Notes