데이터 분석이나 서비스 운영 과정에서 데이터베이스에 직접 쿼리를 날려 정보를 확인해야 하는 경우가 빈번하다. 하지만 SQL 문법에 익숙하지 않은 사용자나, 복잡한 스키마를 가진 데이터베이스의 경우 매번 쿼리를 작성하는 것이 번거로울 수 있다. 이를 해결하기 위해 거대 언어 모델(LLM)이 자연어를 SQL로 변환하고 실행 결과까지 해석해 주는 SQL RAG(Retrieval-Augmented Generation) 시스템을 구축해 보았다.
본 포스팅에서는 SQLite를 활용해 환경을 구성하고, LangChain과 LangGraph를 이용하여 스스로 판단하고 오류를 수정하며 답변하는 에이전트 구축 과정을 상세히 기록한다.
1. SQLite의 특징과 활용
본 프로젝트의 데이터베이스로 채택한 SQLite는 서버가 필요 없는 독립적인 관계형 데이터베이스 관리 시스템(RDBMS)이다.
- 경량성 및 독립성: 별도의 서버 프로세스를 가동할 필요 없이, 하나의 파일(.db) 형태로 데이터베이스가 관리된다. 따라서 설치 과정이 극도로 단순하며 복사만으로도 전체 데이터를 이전할 수 있다.
- 표준 SQL 지원: ANSI-SQL 표준을 폭넓게 지원하므로 트랜잭션 처리, 인덱싱, 뷰, 트리거 등 현대적인 DBMS가 갖추어야 할 기능을 대부분 제공한다.
- 적합한 유즈케이스: 동시 접속자가 매우 많은 대규모 서비스보다는 모바일 애플리케이션의 로컬 저장소, 프로토타입 개발, 그리고 이번 프로젝트와 같은 데이터 분석 실습용으로 최적의 선택이다.
2. 환경 구성 및 데이터 준비
실습을 위해 샘플 데이터베이스인 Chinook.db를 다운로드한다. 이 데이터베이스는 앨범, 아티스트, 송장(Invoice) 등 음악 상점 운영에 필요한 다양한 테이블을 포함하고 있어 SQL RAG 테스트에 적합하다.
import requests
from langchain_community.utilities import SQLDatabase
# 1. 샘플 DB 다운로드
url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db"
response = requests.get(url)
if response.status_code == 200:
with open("Chinook.db", "wb") as file:
file.write(response.content)
print("Chinook.db 다운로드 완료")
# 2. LangChain SQLDatabase 연동
db = SQLDatabase.from_uri("sqlite:///Chinook.db")
print(f"사용 가능 테이블: {db.get_usable_table_names()}")
LangChain의 SQLDatabase 클래스는 LLM이 데이터베이스와 상호작용할 수 있도록 스키마 정보를 추출하고 쿼리를 실행하는 인터페이스를 제공한다.
3. Agentic Workflow의 필요성
단순히 질문을 던져 SQL만 생성하는 방식은 한계가 명확하다. LLM이 존재하지 않는 컬럼명을 생성하거나 문법 오류를 범할 수 있기 때문이다. 이를 보완하기 위해 Agentic Workflow가 필요하다.
에이전트는 다음과 같은 '추론과 행동(Reasoning and Acting)' 과정을 반복한다.
- 테이블 목록 확인: 질문과 관련된 테이블이 무엇인지 파악한다.
- 스키마 조회: 선택한 테이블의 상세 구조(컬럼, 타입, 관계)를 확인한다.
- 쿼리 생성 및 실행: 정보를 바탕으로 SQL을 작성하고 실행한다.
- 오류 수정: 실행 결과 에러가 발생하면 에러 메시지를 바탕으로 쿼리를 수정한다.
- 최종 답변: 결과 데이터를 바탕으로 사용자의 언어로 답변한다.
4. LangGraph를 이용한 정교한 에이전트 설계
LangGraph를 사용하면 각 단계를 노드(Node)로 정의하고, 조건에 따라 흐름을 제어하는 엣지(Edge)를 구성하여 복잡한 로직을 구조화할 수 있다.
4-1 주요 노드 구성
- chatbot (list_tables): 사용자의 첫 질문을 받아 데이터베이스 내에 어떤 테이블들이 있는지 탐색하는 도구를 호출한다.
- call_get_schema: 질문과 연관성이 높은 테이블들의 스키마(CREATE TABLE 문 등)를 LLM이 학습하도록 전달한다.
- generate_query: 스키마 정보를 바탕으로 구문 오류가 없는 SQL 쿼리를 생성한다. 이때 LLM에게 결과 제한(Limit)과 불필요한 컬럼 배제를 지시하는 시스템 프롬프트를 주입한다.
- check_query: 생성된 쿼리를 정규표현식으로 정제(Sanitize)한 뒤 실제 DB에서 실행한다.
- answer: DB로부터 반환된 원시 데이터(Raw data)를 자연어 답변으로 변환한다.
4-2 조건부 엣지와 자가 수정(Self-Correction) 로직
에이전트의 핵심은 실패를 인지하고 다시 시도하는 능력에 있다. should_correct 함수를 통해 실행 결과에 'Error' 문구가 포함되어 있다면 다시 generate_query 노드로 돌아가 쿼리를 재작성하도록 설계했다.
def should_correct(state):
txt = state["messages"][-1].content
if "Error:" in txt or "error" in txt.lower():
# 오류 발생 시 쿼리 재생성 노드로 이동
return "generate_query"
else:
# 정상 실행 시 답변 생성 노드로 이동
return "answer"
5. 에이전트 실행 및 결과 확인
"2009년에 가장 많은 매출을 올린 영업 사원은 누구인가요?"라는 복잡한 질문을 던져 보았다. 이 질문은 Employee, Customer, Invoice 테이블 간의 조인(Join)과 연도 필터링, 그리고 집계(Aggregation)가 필요하다.
에이전트는 다음과 같은 흐름으로 작업을 수행했다.
- Employee, Invoice 등의 테이블 존재 여부 확인.
- 각 테이블의 컬럼명과 관계(FK) 파악을 위한 스키마 조회.
- strftime('%Y', InvoiceDate) = '2009' 조건을 포함한 SQL 생성.
- 쿼리 실행 후 매출액이 가장 높은 사원 이름 획득.
- 한국어로 최종 답변 제공.
6. 결론 및 고찰
LangGraph를 활용한 SQL RAG 구축은 단순히 모델의 성능에만 의존하는 것이 아니라, 시스템의 구조를 통해 정확도를 높이는 작업이다.
특히 쿼리 실행 중 발생하는 예외 상황을 에이전트 스스로 인지하고 수정하는 루프를 구성함으로써, 할루시네이션(Hallucination)을 대폭 줄일 수 있었다. 향후에는 쿼리 실행 전 보안 검사(DML 차단 등)를 더욱 강화하거나, 다국어 쿼리 최적화 기능을 추가하여 확장할 수 있을 것으로 판단된다.
데이터베이스라는 정형 데이터의 세계와 LLM이라는 비정형 데이터 처리 도구가 만났을 때, 진정한 데이터 민주화가 가능해짐을 이번 프로젝트를 통해 다시 한번 확인했다.
'개념 정리 step2 > AI Agent' 카테고리의 다른 글
| [RAG 시스템 구축] 벡터 데이터베이스, 환경 여부 평가, 코드 설정 (2) (0) | 2026.04.01 |
|---|---|
| [RAG 시스템 구축] 벡터 데이터베이스부터 앙상블 리트리버까지 (1) (0) | 2026.03.31 |
| [LangGraph] LLM 도구 사용법 (Tool Calling & Agents) (5) (0) | 2026.03.23 |
| [AI Agent] "Tool Calling Agent의 개념"과 "Tavily"를 활용한 웹 검색 챗봇 구현 (4) (0) | 2026.03.20 |
| [LangGraph] 상태 업데이트 및 워크플로우 제어 (3) (0) | 2026.03.19 |
