Budowanie interfejsu bazy danych w języku naturalnym
Utwórz system, w którym użytkownicy zadają pytania w prostym języku angielskim, model generuje SQL za pomocą wywoływania funkcji, aplikacja bezpiecznie wykonuje zapytanie, a model opisuje wyniki.
Budowanie interfejsu bazy danych w języku naturalnym to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.
Język naturalny na SQL: wizja
Proszę wyobrazić sobie zadanie pytania bazie danych: „Którzy klienci wydali w zeszłym miesiącu więcej niż 1000 USD?” — i otrzymanie odpowiedzi bez napisania ani jednego zapytania SQL. Interfejs bazy danych w języku naturalnym wykorzystuje wywoływanie funkcji, aby umożliwić LLM wygenerowanie kodu SQL; aplikacja bezpiecznie go wykonuje, a model opisuje wyniki zwykłym językiem. Ten wzorzec demokratyzuje dostęp do danych dla użytkowników nietechnicznych.
Przegląd architektury systemu
Potok NL-to-SQL składa się z czterech współpracujących ze sobą komponentów:
- Kontekst schematu: LLM otrzymuje schemat bazy danych, dzięki czemu wie, jakie tabele i kolumny istnieją.
- Generowanie SQL: model generuje zapytanie SQL jako argument wywołania funkcji.
- Bezpieczne wykonanie: aplikacja sprawdza i wykonuje zapytanie, a następnie zwraca wyniki.
- Opis wyników: model otrzymuje wyniki zapytania i wyjaśnia je w języku naturalnym.
Definiowanie narzędzia Query Database
Należy zdefiniować funkcję query_database, która przyjmuje instrukcję SQL SELECT. Opis schematu w definicji funkcji informuje model, które tabele i kolumny są dostępne, dzięki czemu generuje on poprawne zapytania bez zgadywania.
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']
}
}
}Bezpieczne wykonywanie SQL
Nigdy nie należy wykonywać surowego kodu SQL wygenerowanego przez model bez uprzedniej walidacji. Należy wdrożyć warstwę bezpieczeństwa, która: zezwala wyłącznie na instrukcje SELECT, odrzuca niebezpieczne słowa kluczowe, ogranicza liczbę wierszy wynikowych, aby zapobiec problemom z pamięcią, oraz wykonuje zapytania w transakcji bazy danych tylko do odczytu. Obrona warstwowa ma kluczowe znaczenie podczas wykonywania kodu wygenerowanego przez 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]Wstrzykiwanie kontekstu schematu do komunikatu systemowego
Model generuje lepszy kod SQL, gdy może zobaczyć pełny schemat bazy danych. Należy utworzyć komunikat systemowy zawierający definicje tabel, nazwy i typy kolumn oraz przykładowe wartości dla kolumn kategorialnych. Dzięki temu model wie, czy użyć country = 'US', czy country_code = 'US', zamiast zgadywać.
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.
'''Formatowanie wyników zapytania dla modelu
Surowe wyniki bazy danych (listy słowników) należy przed odesłaniem do modelu sformatować jako czytelny tekst. Zbiór wyników należy przekształcić w zwięzłą reprezentację — tabelę lub podsumowanie w formacie JSON — do której model może się odwołać podczas opisywania odpowiedzi. Należy unikać wysyłania tysięcy wierszy; duże zbiory wyników trzeba podsumować.
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 resultImplementacja pełnego potoku
Połączmy wszystkie elementy: funkcję, która przetwarza pytanie użytkownika, wywołuje model w celu wygenerowania SQL, bezpiecznie wykonuje zapytanie i przekazuje wyniki z powrotem do opisania. Model otrzymuje zarówno pierwotne pytanie, jak i wyniki zapytania, a następnie tworzy odpowiedź w zwykłym języku angielskim.
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.contentObsługa wieloetapowych pytań dotyczących danych
Złożone pytania mogą wymagać wielu zapytań. Pytanie „Kim jest 5 naszych klientów o najwyższych przychodach i jakie były ich ostatnie zamówienia?” wymaga dwóch zapytań: pierwszego do znalezienia klientów o najwyższych przychodach, a następnie drugiego do pobrania ich zamówień. Należy zezwolić modelowi na wykonywanie wielu sekwencyjnych wywołań narzędzi, uruchamiając pętlę obsługi wielokrotnie, aż do momentu, gdy 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.'Zapobieganie ryzyku wstrzyknięcia SQL
Nawet przy zabezpieczeniu zezwalającym wyłącznie na SELECT sprytny model (lub atakujący użytkownik) może próbować wyprowadzić dane za pomocą podzapytań albo sztuczek opartych na komentarzach. Dodatkowe zabezpieczenia obejmują: użycie użytkownika bazy danych tylko do odczytu, który ma wyłącznie uprawnienie SELECT, działanie w osobnej puli połączeń oraz sprawdzanie, czy nazwy tabel w zapytaniu są zgodne z białą listą schematu.
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 TrueBuforowanie często używanych zapytań
Wiele pytań biznesowych jest zadawanych wielokrotnie i ma tę samą odpowiedź: „Ilu mamy klientów?” albo „Jakie były przychody w zeszłym miesiącu?”. Należy buforować te wyniki w Redisie z krótkim TTL. Przed wykonaniem zapytania trzeba sprawdzić pamięć podręczną — zmniejsza to obciążenie bazy danych i przyspiesza odpowiedzi na często zadawane pytania analityczne.
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 rowsWyjaśnianie zapytań użytkownikom
Buduj zaufanie, pokazując użytkownikom wygenerowane zapytanie SQL obok odpowiedzi w języku naturalnym. Gdy użytkownicy widzą „Wykonano to zapytanie: SELECT COUNT(*) FROM customers WHERE country = ?UK?”, mogą zweryfikować poprawność odpowiedzi i poznać wzorce SQL. Pole explanation w naszym schemacie narzędzia doskonale się do tego nadaje.
Szybkie sprawdzenie
Sprawdź swoją wiedzę na temat tworzenia interfejsu bazy danych w języku naturalnym.
Podsumowanie lekcji
W tej lekcji nauczyli się Państwo, że: schemat narzędzia query_database wstrzykuje kontekst schematu, dzięki czemu model generuje poprawny kod SQL, walidacja bezpieczeństwa musi blokować instrukcje inne niż SELECT oraz niebezpieczne słowa kluczowe przed wykonaniem, a pętla wywołań modelu umożliwia wieloetapową analizę danych wymagającą sekwencyjnych zapytań. Następnie poznamy Model Context Protocol (MCP), otwarty standard łączenia AI z zewnętrznymi narzędziami.
Często zadawane pytania
Czy lekcja „Budowanie interfejsu bazy danych w języku naturalnym” jest bezpłatna?
Tak — pełny tekst „Budowanie interfejsu bazy danych w języku naturalnym” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Budowanie interfejsu bazy danych w języku naturalnym”?
Utwórz system, w którym użytkownicy zadają pytania w prostym języku angielskim, model generuje SQL za pomocą wywoływania funkcji, aplikacja bezpiecznie wykonuje zapytanie, a model opisuje wyniki. Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?
Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.
Ile czasu zajmuje lekcja „Budowanie interfejsu bazy danych w języku naturalnym”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?
Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Definiowanie schematów funkcji dla API
- Przetwarzanie wywołań narzędzi w aplikacji
- Równoległe wywoływanie funkcji
- Budowanie interfejsu bazy danych w języku naturalnym