1. 문제 상황: 자연어의 변동성과 LLM의 확률적 '재현성' 부족
지난 포스팅에서는 PostgreSQL과 pgvector를 활용해 질문에 필요한 DB 스키마만 동적으로 제공하는 RAG 아키텍처를 소개했습니다. 하지만 스키마만 제공하고 쿼리 생성을 위임했을 때, 실제 서비스 레벨에서는 두 가지 치명적인 한계가 발생했습니다.
- 재현성 부족: LLM은 본질적으로 다음 토큰의 확률을 계산하는 모델입니다. 사용자의 자연어 표현이 미세하게 달라질 때마다, 조건부 확률의 분포가 흔들리며 생성되는 SQL의 구조가 매번 달라졌습니다.
- 할루시네이션: 확률적 생성 과정에서 훈련 데이터의 지식과 주입된 스키마가 충돌하며, 존재하지 않는 테이블이나 컬럼을 제멋대로 참조하는 현상이 발생했습니다.
결국 LLM의 확률적 생성 결과를 엔지니어링 단계에서 통제하여, 일관되고 예측 가능한 SQL 생성을 보장하는 프롬프트 아키텍처가 필요했습니다. 저는 이를 해결하기 위해 '어휘의 정규화'와 '구조의 제약'이라는 투트랙 전략을 설계했습니다.
2. 어휘의 정규화
게임 도메인의 특성상 유저들은 정식 명칭보다 줄임말과 은어를 압도적으로 많이 사용합니다. 유저가 *"뭉가 2렙"*이라고 질문했을 때, 단순한 키워드 매칭이나 Zero-shot 프롬프트로는 이를 DB에 존재하는 name = '뭉툭한 가시'로 정확히 연결하기 어렵습니다.
이를 해결하기 위해 단순 하드코딩 딕셔너리가 아닌, 자연어 처리의 벡터 공간 모델을 활용한 의미 기반 검색 파이프라인을 구축했습니다.
텍스트 임베딩은 단어나 문장을 고차원의 밀집 벡터로 변환하는 기술입니다. 이 공간 안에서는 의미가 비슷한 단어일수록 벡터 간의 거리가 가깝게 위치합니다.
저는 데이터베이스에 embedding_lookup이라는 별도의 메타데이터 테이블을 생성하고, 게임 내 수많은 약어(뭉가, 뭉가시)와 정식 표준어(뭉툭한 가시)를 임베딩하여 벡터 형태로 저장했습니다.
유저의 입력이 들어오면 파이프라인은 다음과 같이 동작합니다.
- 유저 질문의 핵심 키워드를 임베딩 모델을 통해 N차원 벡터로 변환합니다.
- pgvector를 활용하여 embedding_lookup 테이블 내의 표준어 벡터들과 코사인 유사도 연산을 수행합니다.
- 유사도가 가장 높은(가장 의미가 가까운) K개의 표준어를 추출하여 유저의 질문을 치환합니다.
"뭉가 2렙" -> (Vector 유사도 검색 및 치환) -> "뭉툭한 가시 레벨 2"
이러한 Vector 유사도 기반의 치환 작업을 통해 다채롭고 파편화된 자연어 입력이 들어와도, LLM은 정제된 텍스트를 바탕으로 정확한 조건절(WHERE)을 생성할 수 있는 안전성을 확보하게 되었습니다.
3. 구조의 제약: In-Context Learning을 이용한 Few-Shot Prompting
어휘의 모호성을 벡터 검색으로 통제했다면, 다음은 SQL 문법과 조인(Join) 구조의 통제입니다. 아무리 질문이 표준화되어도 LLM이 쿼리 구조를 제멋대로 구성하면 런타임 에러가 발생합니다.
현대의 대형 언어 모델들은 가중치를 업데이트하는 파인튜닝 없이도, 프롬프트 문맥 내에 주어진 몇 가지 예시만으로 새로운 작업을 수행하는 인컨텍스트 러닝 능력을 갖추고 있습니다. 이를 활용한 기법이 바로 Few-shot Prompting입니다.
LLM에게 스키마만 주고 "쿼리를 알아서 짜줘"라고 하는 것은, 너무 넓은 확률 공간(SQL 문법 전체)을 모델 스스로 탐색하게 방치하는 것과 같습니다. 반면, 도메인에 특화된 '질문-완벽한 SQL' 쌍을 프롬프트에 동적으로 주입(Few-shot)하면, 모델이 다음에 출력할 토큰의 확률 분포가 우리가 제공한 예시의 패턴쪽으로 강하게 제약됩니다.
저는 사용자의 질문 의도를 분류한 뒤, 해당 의도와 가장 완벽하게 매칭되는 3~5개의 고품질 쿼리 예시를 프롬프트에 주입했습니다. 이를 통해 모델은 우리 시스템이 요구하는 특정 테이블의 JOIN 방식과 서브쿼리 구조를 정확하게 모방하기 시작했습니다.
4. 측정 결과
이론적 한계를 극복하기 위해 두 가지 제어 장치를 결합한 결과, 파이프라인은 놀라울 정도로 안정화되었습니다.
[측정 결과 1: 재현성 테스트]
- 테스트 질의: "뭉가 2레벨 효과가 어떻게 돼?" (동일 질문 5회 반복)
| 측정 항목 | Few-shot & 임베딩 적용 전 (Zero-shot) | 적용 후 (투트랙 전략) |
| SQL 구조 일치율 | 2/5 | 4/5 (1회 컬럼 누락 제외 일치) |
| 할루시네이션 발생 | 3/5 (없는 테이블 참조) | 0/5 (발생 없음) |
| 운영 영향도 | 간헐적 답변 생성 실패 | 안정적인 정상 응답 반환 |
[측정 결과 2: 생성 안정성 (표현 변형) 테스트]
- 테스트 질의 변형: "뭉가 2단계", "레벨2가 뭐야?", "2렙 효과"
| 측정 항목 | Few-shot & 임베딩 적용 전 (Zero-shot) | 적용 후 (투트랙 전략) |
| 정상 SQL 생성 성공률 | 2/3 (표현마다 쿼리 형태 변동됨) | 3/3 (모두 동일한 패턴으로 생성) |
5. 마치며
결과적으로, 단순한 스키마 주입만으로는 확률 모델인 LLM을 실제 서비스 레벨에서 통제하기 어렵다는 것을 확인했습니다.
embedding_lookup 테이블 기반의 벡터 시맨틱 검색으로 자연어 어휘의 변동성에 대비하고, In-Context Learning(Few-shot)으로 쿼리의 구조적 뼈대를 잡아주는 투트랙 전략. 이를 통해 자연어 입력의 변수에도 흔들리지 않는 재현성을 확보하고, 할루시네이션을 완벽히 억제하여 봇 응답의 신뢰도를 실서비스 투입 가능 수준으로 끌어올릴 수 있었습니다.
'AI' 카테고리의 다른 글
| [Claude] 서브에이전트에서 에이전트 팀으로 (0) | 2026.05.21 |
|---|---|
| [Claude] 멀티에이전트 Spark 학습 구축기 (0) | 2026.05.19 |
| [Claude 가이드] 나만의 Claude 사용 팁 (0) | 2026.05.13 |
| [RAG 실전 적용기] 챗봇이 DB를 직접 뒤지게 만들다: Text-to-SQL과 pgvector 도입기 (1) | 2026.05.04 |
| RAG 기초 — LLM의 한계를 극복하는 가장 현실적인 방법 (0) | 2026.05.01 |