Создание интерфейса базы данных на естественном языке
Создайте систему, в которой пользователи задают вопросы на обычном английском языке, модель генерирует SQL с помощью вызова функций, приложение безопасно выполняет запрос, а модель описывает результаты.
«Создание интерфейса базы данных на естественном языке» — бесплатный урок AI Engineering Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Engineering Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Engineering Academy содержит 4 уроков всего.
Естественный язык в SQL: видение
Представьте, что Вы спрашиваете базу данных: «Какие клиенты потратили больше 1 000 долларов в прошлом месяце?» — и получаете ответ, не написав ни одного SQL-запроса. Интерфейс к базе данных на естественном языке использует вызов функций: LLM генерирует SQL, Ваше приложение безопасно выполняет его, а модель описывает результаты обычным языком. Этот подход делает доступ к данным удобным для пользователей без технической подготовки.
Обзор архитектуры системы
Конвейер NL-to-SQL состоит из четырёх взаимодействующих компонентов:
- Контекст схемы: LLM получает схему Вашей базы данных и узнаёт, какие таблицы и столбцы существуют.
- Генерация SQL: модель генерирует SQL-запрос как аргумент вызова функции.
- Безопасное выполнение: Ваше приложение проверяет и выполняет запрос, а затем возвращает результаты.
- Описание результатов: модель получает результаты запроса и объясняет их на естественном языке.
Определение инструмента запроса к базе данных
Определите функцию query_database, которая принимает инструкцию SQL SELECT. Описание схемы в определении функции сообщает модели, какие таблицы и столбцы доступны, поэтому она создаёт точные запросы, не делая предположений.
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Обработка многоэтапных вопросов о данных
Для сложных вопросов может потребоваться несколько запросов. Запрос «Кто входит в нашу пятёрку лучших клиентов по выручке и каковы их самые последние заказы?» требует двух запросов: сначала нужно найти лучших клиентов, затем получить их заказы. Разрешите модели выполнять несколько последовательных вызовов инструментов, запуская цикл диспетчеризации несколько раз, пока 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Кэширование распространённых запросов
Многие деловые вопросы задают неоднократно и получают один и тот же ответ: «Сколько у нас клиентов?» или «Какой была выручка в прошлом месяце?» Кэшируйте эти результаты в Redis с небольшим TTL. Проверяйте кэш перед выполнением запроса — это снижает нагрузку на базу данных и ускоряет ответы на распространённые аналитические вопросы.
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, и опасные ключевые слова до выполнения, а цикл вызовов модели позволяет проводить многоэтапный анализ данных, требующий последовательных запросов. Далее мы рассмотрим протокол контекста модели (MCP) — открытый стандарт для подключения ИИ к внешним инструментам.
Часто задаваемые вопросы
Урок «Создание интерфейса базы данных на естественном языке» бесплатный?
Да — полный текст урока «Создание интерфейса базы данных на естественном языке» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Engineering Academy, подпишись на CoddyKit PRO. Курс AI Engineering Academy содержит 4 уроков всего.
Чему я научусь в уроке «Создание интерфейса базы данных на естественном языке»?
Создайте систему, в которой пользователи задают вопросы на обычном английском языке, модель генерирует SQL с помощью вызова функций, приложение безопасно выполняет запрос, а модель описывает результа… Ты практикуешь AI Engineering Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать AI Engineering Academy?
Предыдущий опыт не требуется. AI Engineering Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Создание интерфейса базы данных на естественном языке»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке AI Engineering Academy?
Да. Каждый урок AI Engineering Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Определение схем функций для API
- Обработка вызовов инструментов в приложении
- Параллельный вызов функций
- Создание интерфейса базы данных на естественном языке