Phase 5 · Text-to-SQL

LLM Text-to-SQL
아키텍처 설계

사용자가 자연어(한국어)로 질문하면 LLM이 SQL SELECT 쿼리를 자동 생성하여 구조화된 데이터베이스를 직접 조회하고, 결과를 자연어로 요약하는 하이브리드 RAG+SQL 시스템입니다.

RAG와 Text-to-SQL의 역할 비교

기존 RAG(비정형 문서 검색)와 Text-to-SQL(정형 데이터 조회)을 결합한 하이브리드 방식으로, 질문 유형에 따라 최적의 경로를 자동 선택합니다.

구분 RAG (기존) Text-to-SQL (신규)
대상 비정형 문서 (PDF, DOCX 등) 정형 데이터 (DB 테이블)
검색 방식 벡터 유사도 + 키워드 SQL 쿼리 생성 → 실행
응답 문서 기반 설명 데이터 조회 결과 + 요약
예시 "청년내일채움공제 가입 조건은?" "이번 달 신규 가입자 수는?"

Query Router — 3가지 분기 경로

📄

RAG 경로

개념/절차/설명 질문
→ 기존 문서 검색 파이프라인
키워드: 설명, 방법, 이유, 절차, 가이드

🔢

SQL 경로

데이터 조회/집계 질문
→ Text-to-SQL 파이프라인
키워드: 몇 건, 합계, 평균, 목록, 조회

🔀

Hybrid 경로

데이터 + 설명 모두 필요
→ 양쪽 결과 통합 (SQL 우선)
키워드: 분석, 추이, 비교 + 조회

통합 아키텍처

Query Router가 질문 의도를 분류하고, 각 파이프라인이 독립적으로 동작한 후 Context Merger에서 결과를 통합합니다.

🏗️ 전체 흐름도
💬 사용자 질문 자연어 (한국어)
📡 Query Router 의도 분류 (키워드 기반)
RAG
📄 RAG Pipeline 문서 벡터 검색 (기존)
HYBRID
🔀 양쪽 병렬 실행 RAG + SQL 동시 호출
SQL
🔢 Text-to-SQL SQL 생성 → 실행 (신규)
🔗 Context Merger SQL 결과 + 문서 검색 통합 · 충돌 시 SQL 우선
🧠 Ollama LLM 자연어 응답 생성
Response 답변 + 인용 + SQL 결과 + caveats

Text-to-SQL 파이프라인 상세

1
📋

SchemaMetadataManager

질문 관련 테이블만 선별 · DDL + 샘플 데이터 + 동의어 로드

2
🤖

SQLGenerator

LLM 호출 (질문 + 스키마 + Few-shot) → SQL SELECT 생성

3
🛡️

SQLValidator

sqlparse AST · 단일 SELECT · 테이블 허용목록 · PII 보호

실패 → 재시도 (최대 2회)
4
💰

CostEstimator

EXPLAIN 비용 상한 검사 · 풀스캔 감지 (운영: 필수 / 개발: 선택)

5

SQLExecutor

READ ONLY · timeout 5초 · 최대 1000행 · 동시성 세마포어

PG 에러 → 재시도
6
📊

ResultFormatter

마크다운 테이블 + 자연어 요약 + PII 마스킹 + 불확실성 고지

신규 컴포넌트 명세

3개의 신규 파일로 구성되며, 기존 코드 패턴(pgvector_store.py, rag_orchestrator.py)을 따릅니다.

📡 app/query_router.py — 질문 의도 분류

classify_intent(question: str) -> Literal["rag", "sql", "hybrid"]
분류 키워드 예시
SQL 몇 건, 총합, 합계, 평균, 개수, 목록, 리스트, 조회, 정렬, 최대, 최소, 데이터
RAG 설명, 방법, 이유, 차이점, 절차, 가이드, 주의사항
Hybrid 분석, 추이, "비교하고 조회", "설명하면서 조회"

⚙️ app/text_to_sql.py — 핵심 엔진

📋

SchemaMetadataManager

질문 관련 테이블만 선별하여 DDL + 샘플 데이터 반환 (스키마 최소 노출 원칙). 한국어 동의어 매핑, 스키마 동기화 검사, 샘플 데이터 비식별화 지원.

get_schema_context()
check_schema_sync()
refresh_cache()
🤖

SQLGenerator

Ollama LLM을 호출하여 자연어 → SQL 변환. 에러 보정 시 retry_context에 이전 에러 정보를 포함하여 재생성.

generate_sql(question, schema_context, few_shot, retry_context)
🛡️

SQLValidator

sqlparse AST 분석 기반 검증. 단일 SELECT만 허용, 차단 키워드, 테이블 허용목록(주 통제), 서브쿼리 깊이 제한(2단계), 함수 허용목록, PII 컬럼 보호.

validate(sql) -> ValidationResult
💰

CostEstimator

EXPLAIN (FORMAT JSON)으로 실행 전 비용 상한/풀스캔 위험 검사. 운영: 필수 적용, 개발: 선택적.

check_cost(sql) -> CostCheckResult

SQLExecutor

읽기전용 커넥션 (sql_reader 역할), READ ONLY, timeout 5초, 최대 1000행. 동시성 제어 세마포어 적용.

execute(sql, offset, limit) -> SQLExecutionResult
📊

ResultFormatter

SQL 결과를 마크다운 테이블로 변환, 자연어 요약 생성. PII 마스킹 후처리 + 불확실성 고지 자동 첨부.

format_standalone_response()
format_for_llm_context()

🗄️ app/sql_store.py — DB 연결 관리

🔗

pgvector_store.py 패턴 준수

SQLStoreConfig + get_readonly_connection() + ensure_sql_schema() 구조. 기존 PostgresConfig 패턴과 동일한 환경변수 기반 설정.

데이터베이스 설계

시스템 메타데이터 테이블 3개와 샘플 비즈니스 테이블 4개로 구성됩니다.

🗃️ 신규 시스템 테이블

📖

sql_schema_catalog

LLM 프롬프트에 주입할 테이블/컬럼의 한국어 설명, PII 여부, 마스킹 유형, 동의어 정보를 관리합니다.

주요 컬럼: is_pii, pii_mask_type, synonyms, sample_values
📝

sql_few_shot_examples

SQL 생성 정확도 향상을 위한 Few-shot 예제 (한국어 질문 → 정답 SQL 쌍). 난이도별 분류 및 사용 횟수 추적.

주요 컬럼: question, sql_query, difficulty
📜

sql_query_log

모든 SQL 생성/실행 이력의 감사 로그. 요청자, 생성된 SQL, 검증 결과, 비용, 실행 시간, 에러 코드 기록.

주요 컬럼: request_id, user_id, cost_estimate, error_code

📦 샘플 비즈니스 테이블

테이블명 설명 주요 컬럼 PII 컬럼
subscribers 가입자 정보 subscriber_id, company_name, subscription_date, program_type, status, monthly_contribution name
payment_history 납부 이력 payment_id, subscriber_id, payment_date, payment_month, amount, payment_status -
consultation_log 상담 이력 consultation_id, subscriber_id, consultation_date, channel, category, resolved counselor_name
program_products 프로그램 상품 product_id, program_name, monthly_amount_min/max, duration_months, government_match_ratio -

🔐 읽기전용 DB 역할

🔑

접근 통제 원칙

테이블 접근 차단의 주 통제 수단은 애플리케이션 레벨 SQLValidator의 허용목록이며, DB 역할 권한(sql_reader)은 보조 방어로 작용합니다. PostgreSQL 버전별로 시스템 카탈로그 REVOKE 동작이 다를 수 있으므로, DB 권한에만 의존하지 않습니다.

-- 읽기전용 역할 생성 CREATE ROLE sql_reader WITH LOGIN PASSWORD 'reader_secret'; GRANT CONNECT ON DATABASE rag TO sql_reader; GRANT USAGE ON SCHEMA public TO sql_reader; -- 비즈니스 테이블에만 SELECT 권한 부여 GRANT SELECT ON subscribers, payment_history, consultation_log, program_products TO sql_reader; -- RAG 내부 테이블 접근 차단 (보조 방어) REVOKE ALL ON document_chunks, ingest_history, reocr_queue FROM sql_reader;

보안 아키텍처

API 접근부터 감사 로그까지 7단계 방어 체계로 안전한 SQL 실행 환경을 보장합니다.

🛡️ 7단계 방어 체계
1️⃣ API 접근 제어
RBAC 기반 엔드포인트별 권한 검사 sql:read / sql:execute 역할 분리
2️⃣ 입력 검증
Pydantic 모델 + 요청 크기 제한
3️⃣ 쿼리 라우팅
SQL 분류된 질문만 SQL 파이프라인 진입
4️⃣ SQL 검증 (주 통제)
sqlparse AST 분석 테이블 허용목록 함수 허용목록 PII 컬럼 보호
5️⃣ 비용 검증
EXPLAIN 비용 상한 풀스캔 감지
6️⃣ 실행 샌드박스
sql_reader 역할 (보조 방어) READ ONLY timeout 5초 행 제한 1000
7️⃣ 감사 + 알림
sql_query_log 기록 이상행위 탐지 반복 위반 차단

👤 API 접근 제어 (RBAC)

엔드포인트 메서드 최소 권한 설명
/sql/schema GET sql:read 스키마 조회 (읽기 전용)
/sql/schema/{table} GET sql:read 테이블 상세 조회
/sql/query POST sql:execute SQL 생성 + 실행
/sql/validate POST sql:read SQL 검증만 (실행 없음)
/rag/query (queryMode=sql) POST sql:execute RAG 통합 SQL 조회

🔒 PII 보호 정책

조회 유형 처리 방식
SELECT name FROM ... 차단 또는 마스킹 자동 래핑
SELECT COUNT(*) GROUP BY name 집계만 허용
WHERE name = '김철수' 조건 허용, 결과에서 마스킹
마스킹 유형 원본 결과
name 김철수 김OO
phone 010-1234-5678 010-****-5678
email user@example.com u***@example.com

⚔️ 공격 벡터별 대응

공격 대응
LLM이 DELETE/DROP 생성 sqlparse 구문 타입 검사 + DB 역할에서 이중 차단
세미콜론 다중 구문 sqlparse로 statement 분리, 2개 이상이면 거부
주석 기반 우회 sqlparse 토큰 분석으로 주석 제거 후 재파싱
UNION으로 시스템 테이블 조회 테이블 허용목록(주 통제)에서 차단 + UNION 패턴 검사
고비용 쿼리 (DoS) EXPLAIN 비용 상한 + timeout 5초 + 행 제한
PII 컬럼 alias 우회 결과 반환 전 PII 마스킹 후처리 (원본 컬럼 추적)
Hybrid 모드 컨텍스트 오염 RAG 컨텍스트와 SQL 생성 프롬프트를 분리된 LLM 호출로 격리
반복 위반 질의 동일 user_id에서 연속 검증 실패 시 로그 경고 + 임시 차단

실패 처리 및 응답 정책

에러 코드 체계, 대체 응답 전략, 재시도 시 의미 보존 검증으로 안정적인 사용자 경험을 보장합니다.

🚨 에러 코드 체계

에러 코드 HTTP 설명 사용자 메시지
SQL_VALIDATION_ERROR 422 SQL 검증 실패 질문을 SQL로 변환했으나 안전 검증에 실패했습니다.
SQL_EXECUTION_ERROR 500 SQL 실행 오류 SQL 실행 중 오류가 발생했습니다.
SQL_TIMEOUT_ERROR 504 타임아웃 (5초 초과) 조회 시간이 초과되었습니다.
SQL_COST_EXCEEDED 422 비용 상한 초과 쿼리 비용이 높아 실행이 제한되었습니다.
SQL_PERMISSION_ERROR 403 권한 부족 해당 데이터에 대한 조회 권한이 없습니다.
SQL_GENERATION_FAILED 500 SQL 생성 실패 질문을 SQL로 변환하지 못했습니다.
SQL_NO_RESULTS 200 결과 0건 조건에 해당하는 데이터가 없습니다.

🔄 대체 응답 전략 (Fallback)

에러 보정 재시도 (최대 2회)

에러 컨텍스트를 포함하여 SQL 재생성. 재시도 시 의미 보존 검증 (기간조건, 상태값, 집계대상 불변 확인).

테이블 범위 축소

관련 테이블 1개로 제한하여 재시도. 복잡한 JOIN을 단순화합니다.

RAG Fallback

SQL 포기, 기존 RAG 파이프라인으로 자동 전환. "데이터 조회에 실패하여 문서 검색 결과로 응답합니다" 안내.

질의 재작성 유도

RAG도 결과 없으면 "다음과 같이 질문을 바꿔보세요: [예시]" 제안 메시지를 반환합니다.

🔍 재시도 시 의미 보존 검증

보존 항목 검증 방법
기간 조건 원본 SQL의 DATE/TIMESTAMP 조건과 재생성 SQL의 조건 비교. 기간이 변경되었으면 거부.
상태값 필터 WHERE 절의 상태값(active, overdue 등) 유지 여부 확인
집계 대상 SELECT 절의 집계 함수(COUNT, SUM 등)와 대상 컬럼 유지 여부 확인
대상 테이블 핵심 테이블이 제거되지 않았는지 확인. 변질 감지 시 Fallback 전환.

API 설계

신규 SQL 전용 엔드포인트 4개 + 기존 RAG 엔드포인트에 queryMode 확장.

POST /sql/query 독립 Text-to-SQL 조회

요청

{ "question": "이번 달 신규 가입자 수는?", "tables": ["subscribers"], "maxRows": 100, "offset": 0, "model": null }

응답 (성공)

{ "sql": "SELECT COUNT(*) AS ...", "columns": ["신규가입자수"], "rows": [[42]], "row_count": 1, "total_count": 42, "natural_language_summary": "이번 달 신규 가입자는 총 42명입니다.", "caveats": ["이번 달 = 2026년 2월 기준"], "execution_time_ms": 23.5, "validation_passed": true, "retries_used": 0 }
메서드 엔드포인트 설명
GET /sql/schema 사용 가능한 테이블 목록 + 설명 + 컬럼 수 + 행 수 추정값
GET /sql/schema/{table} 테이블 상세 스키마 (컬럼 목록, PII 여부, 타입, 샘플 데이터, Few-shot 예제)
POST /sql/validate SQL 검증만 수행 (실행 없음). 유효성, 위반 항목, 정제된 SQL, 예상 비용 반환

🔀 기존 POST /rag/query 확장

🔄

auto

기본값. Query Router가 자동 판별

📄

rag

기존 RAG만 사용

🔢

sql

Text-to-SQL만 사용

🔀

hybrid

양쪽 결과 통합

LLM 프롬프트 전략

SQL 생성 프롬프트 템플릿, Few-shot 선택 기준, 에러 보정 루프로 SQL 생성 정확도를 극대화합니다.

당신은 PostgreSQL SQL 전문가입니다. 사용자의 한국어 질문을 읽고 정확한 SQL SELECT 쿼리를 생성하세요. ### 규칙 1. SELECT 문만 생성하세요. INSERT/UPDATE/DELETE/DROP 등 금지. 2. PostgreSQL 문법을 사용하세요. 3. 반드시 아래 스키마에 존재하는 테이블과 컬럼만 사용하세요. 4. 한국어 문자열 값은 작은따옴표로 감싸세요. 5. 결과 행 수가 불확실하면 LIMIT 100을 추가하세요. 6. 컬럼 별칭은 한국어로 지정하세요 (예: COUNT(*) AS 건수). 7. SQL만 출력하세요. 설명이나 코드블록은 포함하지 마세요. 8. [마스킹 대상] 컬럼은 직접 SELECT하지 마세요. {schema_context} {few_shot_examples} {retry_error_context} ### 질문 {question}

🔄 에러 보정 루프

1️⃣ 시도 1
(질문 + 스키마 + Few-shot) → SQL 생성 → 검증 → 비용검사 → 실행
2️⃣ 검증 실패 시 재시도
이전 SQL 검증 실패 사유 포함 → 재생성 의미 보존 검증: 기간/상태/집계 대상 변경 감지
3️⃣ 실행 실패 시 재시도
PG 에러 메시지 포함 → 재생성 의미 보존 검증 동일 적용
모두 실패
Fallback 전략 적용 (테이블 축소 → RAG → 재작성 유도)

🧠 모델 권장 사항

환경 모델 VRAM
개발 PC (GTX 1660 Ti 6GB) qwen2.5:3b-instruct-q4_K_M (기존) ~2GB
운영 서버 qwen2.5:7b-instruct-q4_K_M 또는 qwen2.5-coder:7b ~4.5GB

배포 및 운영

기존 파일 수정 범위, 환경별 설정 기준값, 롤백 시나리오를 포함한 운영 가이드입니다.

📝 기존 파일 수정 범위

⚙️

app/settings.py

TEXT_TO_SQL_* 환경변수 18개 추가

🌐

app/api.py

Pydantic 모델 + 4개 신규 엔드포인트 + queryMode 추가

🔗

app/rag_orchestrator.py

SQL 파이프라인 호출 + 컨텍스트 병합 + SSE 이벤트 추가

🐳

docker-compose.yml

환경변수 추가 + sql_reader 역할 초기화 스크립트

📦

requirements.txt

sqlparse>=0.5.0 1개 패키지 추가

🔧 환경별 설정 기준값

설정 개발 (PC) 스테이징 운영
TIMEOUT_MS 10000 5000 5000
MAX_ROWS 1000 500 1000
MAX_COST 50000 10000 10000
COST_CHECK_ENABLED false true true
CONCURRENCY 4 2 2
MODEL (기본모델) (기본모델) qwen2.5:7b
로깅 레벨 DEBUG INFO WARN

롤백 시나리오

Step 1: 기능 비활성화

# .env 수정 TEXT_TO_SQL_ENABLED=false # API 컨테이너 재시작 docker compose restart api

Step 3: 데이터 정합성

SQL 관련 DB 테이블은 삭제하지 않음 (데이터 보존).
기존 RAG 파이프라인은 영향 없음.
재활성화: TEXT_TO_SQL_ENABLED=true 후 재시작.

Step 2: 검증 항목 확인 방법 기대 결과
SQL 엔드포인트 비활성화 curl /sql/query 404 또는 "기능 비활성화" 응답
RAG queryMode=sql 무시 POST /rag/query {"queryMode":"sql"} RAG로 처리, SQL 무시
SSE 스트리밍 정상 POST /rag/query/stream SQL 단계 이벤트 미발생
기존 RAG 정상 동작 POST /rag/query {"question":"테스트"} 기존과 동일한 응답

구현 로드맵

5개 Phase로 단계적 구현. 각 Phase는 독립적으로 검증 가능합니다.

Phase 1: 기반 구축

settings.py 환경변수 추가, sql_store.py 생성, 시스템 테이블 3개 생성, sql_reader 역할 생성 + 권한 검증, 샘플 비즈니스 테이블 + 테스트 데이터, sql_schema_catalog 한국어 설명 등록.

Phase 2: 핵심 엔진

text_to_sql.py 생성 — SchemaMetadataManager, SQLGenerator, SQLValidator (허용목록 주 통제), CostEstimator (환경별 정책), SQLExecutor (동시성 제어 + total_count), ResultFormatter (PII 마스킹), 의미 보존 검증. requirements.txt에 sqlparse 추가. 단위 테스트 작성.

Phase 3: API 연동

api.py에 4개 엔드포인트 추가, Pydantic 모델, 에러 코드 체계 적용, sql_query_log 감사 로그 기록, Fallback 전략 구현, 통합 테스트.

Phase 4: RAG 통합

query_router.py 생성, Query 모델에 queryMode 추가, rag_orchestrator.py 수정 (SQL 파이프라인 + 컨텍스트 병합 + 격리된 LLM 호출), Hybrid 충돌 해결, SSE 이벤트 추가.

Phase 5: 프론트엔드 + 마무리

프론트엔드 SQL 조회 탭/토글, 스키마 브라우저 UI, SQL 결과 테이블 렌더링, docker-compose.yml 환경변수, 환경별 설정 적용, 롤백 체크리스트 검증, 문서 정리.

데이터 흐름 예시

1️⃣ 독립 SQL 조회

💬

"이번 달 미납 건수는?"

POST /sql/query

1. RBAC 권한 확인 → sql:execute 통과 2. SchemaMetadataManager → payment_history 스키마 로드 3. SQLGenerator → SELECT COUNT(*) AS 미납건수 FROM payment_history WHERE payment_status = 'overdue' AND ... 4. SQLValidator → 단일 SELECT, 테이블 허용, PII 미접근 → 통과 5. CostEstimator → 비용 120 (상한 10000 이내) → 통과 6. SQLExecutor → 실행 → rows=[[7]], total_count=7 7. ResultFormatter → "이번 달 미납 건수는 총 7건입니다." + caveats

2️⃣ 하이브리드 조회

💬

"청년내일채움공제 미납 현황과 처리 절차를 알려주세요"

POST /rag/query {"queryMode": "auto"} → Router: "hybrid"

📄

RAG 파이프라인

임베딩 → 하이브리드 검색 → 리랭킹 → "처리 절차" 관련 문서 컨텍스트 추출

🔢

SQL 파이프라인 (격리)

스키마(관련 테이블만) → SQL 생성(독립 LLM 호출) → 검증 → 비용검사 → 실행 → "미납 현황" 데이터

🔗

Context Merger (충돌 시 SQL 우선)

SQL 실시간 데이터 + RAG 문서를 결합하여 LLM이 통합 답변 생성. answer + citations + sql_result 반환.

3️⃣ 실패 → Fallback 흐름

💬

"최근 3개월 부서별 가입 추이를 보여줘"

POST /sql/query → 복잡한 쿼리 → 검증 실패 → 재시도 → 비용 초과 → 재시도 → 성공

시도 1: SQL 생성 → 검증 실패 (미등록 함수 사용)
재시도 1: 에러 컨텍스트 → 재생성 → 의미 보존 통과 → 검증 통과 비용 검사: 비용 15000 → 상한 10000 초과
재시도 2: "인덱스 활용" → 재생성 → 의미 보존 통과 → 비용 8000 → 실행 성공

정책 결정 사항

구현 시 해석 차이를 방지하기 위해 확정된 정책입니다.

정책 결정 환경변수
SQL 실패 시 재시도 우선순위 프롬프트 수정 → 테이블 범위 축소 → RAG fallback MAX_RETRIES=2
queryMode=auto 분류 기준 규칙기반 단독으로 시작. 정확도 부족 시 LLM 보조 도입 ROUTE_MODE=auto
결과 요약 방식 단순 집계는 템플릿, 복합 결과(2개 이상 컬럼)는 LLM -
운영 모델 선택 단일 공용 모델로 시작. SQL 정확도 80% 미만 시 전용 모델 분리 MODEL=
EXPLAIN 비용 검사 개발: off / 운영: on COST_CHECK_ENABLED
비용 초과 시 재시도 총 재시도 횟수에 포함 (별도 증가 없음) MAX_RETRIES=2
total_count 산출 결과 < LIMIT이면 행수, 아니면 별도 COUNT. 비용 높으면 null -
접근 통제 주 수단 앱 레벨 SQLValidator 허용목록 (DB 역할은 보조) ALLOWED_TABLES
Hybrid 충돌 우선순위 SQL(실시간 데이터) 우선, caveats에 안내 -
의미 보존 검증 재시도 시 기간/상태/집계 대상 변경 감지 → 변경 시 fallback -

Text-to-SQL 기능 구현을 검토 중이신가요?

기존 RAG 시스템에 정형 데이터 조회 기능을 추가하여 더욱 강력한 하이브리드 AI 시스템을 구축하세요.

이메일 문의하기 고도화 로드맵 보기