자연어 데이터베이스 인터페이스 구축
사용자가 일상적인 영어로 질문하면 모델이 함수 호출을 통해 SQL을 생성하고, 애플리케이션이 쿼리를 안전하게 실행한 뒤 모델이 결과를 설명하는 시스템을 만듭니다.
자연어 데이터베이스 인터페이스 구축은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Engineering Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
자연어에서 SQL로: 비전
SQL 질의를 하나도 작성하지 않고 데이터베이스에 '지난달에 1,000달러 넘게 지출한 고객은 누구인가요?'라고 물어 답을 받는 상황을 상상해 보십시오. 자연어 데이터베이스 인터페이스는 함수 호출을 사용해 LLM이 SQL을 생성하도록 하고, 애플리케이션이 이를 안전하게 실행한 다음, 모델이 결과를 쉬운 영어로 설명하도록 합니다. 이 패턴을 사용하면 기술 지식이 없는 사용자도 데이터에 쉽게 접근할 수 있습니다.
시스템 아키텍처 개요
NL-to-SQL 파이프라인은 다음 네 가지 구성 요소가 함께 작동합니다.
- 스키마 컨텍스트: LLM이 데이터베이스 스키마를 받아 어떤 테이블과 열이 존재하는지 알 수 있습니다.
- SQL 생성: 모델이 함수 호출 인자로 SQL 질의를 생성합니다.
- 안전한 실행: 앱이 질의를 검증하고 실행한 뒤 결과를 반환합니다.
- 결과 설명: 모델이 질의 결과를 받아 자연어로 설명합니다.
데이터베이스 질의 도구 정의
SQL SELECT 문을 받는 query_database 함수를 정의합니다. 함수 정의에 포함된 스키마 설명은 어떤 테이블과 열을 사용할 수 있는지 모델에 알려 주므로, 모델이 추측하지 않고 정확한 질의를 생성할 수 있습니다.
query_db_tool = {
'type': 'function',
'function': {
'name': 'query_database',
'description': '''Execute a read-only SQL query on the company database.
Use this to answer questions about customers, orders, and products.
Only SELECT statements are allowed. Never use DROP, DELETE, UPDATE, or INSERT.
Available tables:
- customers (id, name, email, created_at, country)
- orders (id, customer_id, total_amount, status, created_at)
- order_items (id, order_id, product_id, quantity, unit_price)
- products (id, name, category, price, stock_quantity)
''',
'parameters': {
'type': 'object',
'properties': {
'sql': {
'type': 'string',
'description': 'A valid PostgreSQL SELECT statement.'
},
'explanation': {
'type': 'string',
'description': 'One-sentence explanation of what this query does.'
}
},
'required': ['sql', 'explanation']
}
}
}안전한 SQL 실행
검증 없이 모델이 생성한 원시 SQL을 절대 실행하지 마십시오. SELECT 문만 허용하고, 위험한 키워드는 거부하며, 메모리 문제를 방지하도록 결과 행 수를 제한하고, 읽기 전용 데이터베이스 트랜잭션에서 실행하는 안전 계층을 구현하십시오. LLM이 생성한 코드를 실행할 때는 다층 방어가 매우 중요합니다.
import re
import psycopg2
DANGEROUS_KEYWORDS = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'TRUNCATE', 'ALTER', 'CREATE', 'EXEC']
def execute_safe_query(sql: str, max_rows: int = 100) -> list:
'''Execute a read-only SQL query with safety guards.'''
sql_upper = sql.upper().strip()
# Only allow SELECT
if not sql_upper.startswith('SELECT'):
raise ValueError('Only SELECT statements are allowed.')
# Block dangerous keywords
for keyword in DANGEROUS_KEYWORDS:
if re.search(r'\b' + keyword + r'\b', sql_upper):
raise ValueError(f'Forbidden keyword: {keyword}')
conn = psycopg2.connect('postgresql://readonly_user:pass@localhost/appdb')
with conn:
with conn.cursor() as cur:
# Enforce read-only transaction
cur.execute('SET TRANSACTION READ ONLY')
cur.execute(sql)
columns = [desc[0] for desc in cur.description]
rows = cur.fetchmany(max_rows)
return [dict(zip(columns, row)) for row in rows]시스템 프롬프트에 스키마 컨텍스트 삽입
모델이 전체 데이터베이스 스키마를 볼 수 있으면 더 나은 SQL을 생성합니다. 테이블 정의, 열 이름과 형식, 범주형 열의 예시 값을 포함하는 시스템 프롬프트를 작성하십시오. 그러면 모델이 추측하지 않고 country = 'US'를 사용할지 country_code = 'US'를 사용할지 판단할 수 있습니다.
SYSTEM_PROMPT = '''You are a data analyst assistant with access to the company database.
When users ask data questions, use the query_database tool to look up the answer.
Always explain your query in plain English before executing it.
Database schema:
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
email VARCHAR UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
country VARCHAR(2) -- ISO 2-letter code: 'US', 'UK', 'DE', etc.
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
total_amount NUMERIC(10,2),
status VARCHAR -- 'pending', 'shipped', 'delivered', 'cancelled'
created_at TIMESTAMPTZ DEFAULT NOW()
);
Only use columns that exist in the schema above.
'''모델을 위한 질의 결과 형식 지정
원시 데이터베이스 결과(딕셔너리 목록)는 모델에 돌려보내기 전에 읽기 쉬운 텍스트로 형식을 지정해야 합니다. 모델이 답변을 설명할 때 참고할 수 있도록 결과 집합을 간결한 표현(테이블 또는 JSON 요약)으로 변환하십시오. 수천 개의 행을 보내지 말고, 큰 결과 집합은 요약하십시오.
import json
def format_results(rows: list, max_display: int = 20) -> str:
if not rows:
return 'The query returned no results.'
total = len(rows)
display = rows[:max_display]
# Format as a simple table
if display:
columns = list(display[0].keys())
lines = [' | '.join(columns)]
lines.append('-' * len(lines[0]))
for row in display:
lines.append(' | '.join(str(row[col]) for col in columns))
result = '\n'.join(lines)
if total > max_display:
result += f'\n... ({total - max_display} more rows not shown)'
return result전체 파이프라인 구현
이제 모든 요소를 결합해 보겠습니다. 사용자의 질문을 처리하고, 모델을 호출해 SQL을 생성하고, 질의를 안전하게 실행한 다음, 설명을 위해 결과를 다시 전달하는 함수를 구현합니다. 모델은 원래 질문과 질의 결과를 모두 받아 쉬운 영어로 답변을 생성합니다.
from openai import OpenAI
import json
client = OpenAI()
def answer_data_question(user_question: str) -> str:
messages = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': user_question}
]
# First call: get SQL from model
resp = client.chat.completions.create(
model='gpt-4o', messages=messages, tools=[query_db_tool]
)
assistant_msg = resp.choices[0].message
messages.append(assistant_msg)
if resp.choices[0].finish_reason == 'tool_calls':
tc = assistant_msg.tool_calls[0]
args = json.loads(tc.function.arguments)
print(f'Executing: {args["explanation"]}')
print(f'SQL: {args["sql"]}')
try:
rows = execute_safe_query(args['sql'])
result_text = format_results(rows)
except ValueError as e:
result_text = f'Query blocked: {str(e)}'
messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result_text})
# Second call: narrate results
final = client.chat.completions.create(model='gpt-4o', messages=messages)
return final.choices[0].message.content
return assistant_msg.content여러 단계의 데이터 질문 처리
복잡한 질문에는 여러 질의가 필요할 수 있습니다. '수익 기준 상위 5명의 고객은 누구이며, 이들의 가장 최근 주문은 무엇인가요?'라는 질문에는 상위 고객을 찾는 질의와 해당 고객의 주문을 가져오는 질의, 두 개가 필요합니다. finish_reason='stop'이 될 때까지 디스패치 루프를 여러 번 실행하여 모델이 순차적으로 여러 도구 호출을 수행하도록 허용하십시오.
def answer_complex_question(user_question: str) -> str:
messages = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': user_question}
]
for _ in range(5): # Max 5 query rounds
resp = client.chat.completions.create(
model='gpt-4o', messages=messages, tools=[query_db_tool]
)
msg = resp.choices[0].message
messages.append(msg)
if resp.choices[0].finish_reason == 'stop':
return msg.content # Model is done
# Process tool call and loop
tc = msg.tool_calls[0]
args = json.loads(tc.function.arguments)
try:
rows = execute_safe_query(args['sql'])
result = format_results(rows)
except Exception as e:
result = f'Error: {str(e)}'
messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result})
return 'Could not complete the analysis within the step limit.'SQL 삽입 위험 방지
SELECT만 허용하는 보호 장치가 있어도 악의적인 모델이나 공격적인 사용자가 하위 질의 또는 주석을 이용한 속임수로 데이터를 유출하려 할 수 있습니다. 추가 보호 조치로는 SELECT 권한만 가진 읽기 전용 데이터베이스 사용자 사용, 별도의 연결 풀에서 실행, 그리고 질의의 테이블 이름이 스키마 허용 목록과 일치하는지 검증하는 방법이 있습니다.
ALLOWED_TABLES = {'customers', 'orders', 'order_items', 'products'}
def validate_tables_in_sql(sql: str) -> bool:
'''Check that only whitelisted tables are referenced in the query.'''
import sqlparse
parsed = sqlparse.parse(sql)[0]
table_names = set()
from_seen = False
for token in parsed.flatten():
if token.ttype is sqlparse.tokens.Keyword and token.value.upper() in ('FROM', 'JOIN'):
from_seen = True
elif from_seen and token.ttype is sqlparse.tokens.Name:
table_names.add(token.value.lower())
from_seen = False
unknown = table_names - ALLOWED_TABLES
if unknown:
raise ValueError(f'References unknown tables: {unknown}')
return True자주 사용하는 질의 캐싱
많은 비즈니스 질문은 같은 답으로 반복해서 요청됩니다. '고객이 몇 명이나 있나요?', '지난달 수익은 얼마였나요?'와 같은 질문이 그렇습니다. 이러한 결과를 짧은 TTL로 Redis에 캐시하십시오. 질의를 실행하기 전에 캐시를 확인하면 데이터베이스 부하를 줄이고 자주 묻는 분석 질문에 더 빠르게 응답할 수 있습니다.
import redis
import hashlib
import json
r = redis.Redis.from_url('redis://localhost:6379')
def cached_query(sql: str, ttl_seconds: int = 300) -> list:
cache_key = 'nl_query:' + hashlib.sha256(sql.encode()).hexdigest()
cached = r.get(cache_key)
if cached:
return json.loads(cached)
rows = execute_safe_query(sql)
r.setex(cache_key, ttl_seconds, json.dumps(rows, default=str))
return rows사용자에게 질의 설명하기
생성된 SQL 질의를 자연어 답변과 함께 사용자에게 보여 주어 신뢰를 쌓으십시오. 사용자가 '이 질의를 실행했습니다: SELECT COUNT(*) FROM customers WHERE country = ?UK?'와 같이 실행된 내용을 볼 수 있으면 답이 올바른지 확인하고 SQL 패턴도 배울 수 있습니다. 도구 스키마의 explanation 필드가 이 용도에 적합합니다.
빠른 확인
자연어 데이터베이스 인터페이스 구축에 대한 이해도를 확인해 보십시오.
레슨 요약
이번 레슨에서는 query_database 도구 스키마가 스키마 컨텍스트를 주입하여 모델이 정확한 SQL을 생성하도록 한다는 점, 실행 전에 안전성 검증으로 SELECT가 아닌 문과 위험한 키워드를 차단해야 한다는 점, 그리고 모델 호출을 반복하는 루프를 통해 순차적인 질의가 필요한 여러 단계의 데이터 분석을 수행할 수 있다는 점을 배웠습니다. 다음으로는 AI를 외부 도구에 연결하기 위한 개방형 표준인 Model Context Protocol(MCP)을 살펴보겠습니다.
자주 묻는 질문
“자연어 데이터베이스 인터페이스 구축” 강의는 무료인가요?
네 — “자연어 데이터베이스 인터페이스 구축” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“자연어 데이터베이스 인터페이스 구축”에서 뭘 배우나요?
사용자가 일상적인 영어로 질문하면 모델이 함수 호출을 통해 SQL을 생성하고, 애플리케이션이 쿼리를 안전하게 실행한 뒤 모델이 결과를 설명하는 시스템을 만듭니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“자연어 데이터베이스 인터페이스 구축” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- API용 함수 스키마 정의
- 애플리케이션에서 도구 호출 처리
- 병렬 함수 호출
- 자연어 데이터베이스 인터페이스 구축