Как подключить локальный ИИ к реляционной базе при помощи агента и при этом ничего не сломать

29.10.2026
Поделиться
Ссылка скопирована

Однажды один из слушателей моего курса задал вопрос, можно ли подключить локальную нейросеть к базе данных? Я, конечно, ответил «да»: ведь в оболочках, с которыми работаю, не раз видел инструменты для такой интеграции. Но видел — не значит настраивал сам. Вопрос меня сильно зацепил, и я решил сам во всём разобраться, пройти весь путь руками, а не просто показать кнопку подключения.

Можно было бы, конечно, быстро поставить PostgreSQL, создать пару пустых таблиц и получить эффектную демонстрацию. Но это неинтересно. Хотелось какого-то реального кейса, поработать, так сказать, с живой базой. При этом подключаться к действующей базе и рисковать данными — это не мой подход, поэтому я взял тестовую копию внутренней системы учёта обучения1 и развернул её на отдельном стенде.

Моя задача была довольно конкретной: задавать обычным языком вопрос вроде «покажи расписание на следующую неделю» и получать таблицу с курсами, датами, количеством слушателей, форматом обучения, преподавателем и аудиторией. В эксперименте я использовал локальную модель Qwen 3.5 9B Q4_K_M. Желание было простое — получить рабочий результат и по дороге понять, какие части этой конструкции за что отвечают.

С базами данных до этого я работал, скорее, на уровне «установить, запустить и подключиться». Когда дело дошло до схемы, связей и запросов, мои знания быстро закончились. Я никогда не был достаточно усидчивым, чтобы спокойно и последовательно изучить SQL во всей его красе. Зато здесь появился практический повод, локальный Qwen стал напарником: предлагал запросы и помогал разбирать структуру, а я проверял результат на тестовой копии и уточнял задачу.

Содержание статьи

Почему модели нельзя давать логин и пароль

Языковая модель сама по себе не подключается к базе и не «видит» таблицы: она генерирует текст. Чтобы получить внешние данные, ей нужен инструмент, который выполнит запрос и вернёт результат в контекст диалога.

Казалось бы, можно дать агенту полный доступ к SQL и поручить ему писать запросы самостоятельно. Но небольшие локальные модели могут ошибаться в названиях полей, условиях и соединениях таблиц. Большая модель — это тоже не гарантия. Если запрос выполнится не так, как ожидалось, можно получить неверный отчёт, раскрыть лишние данные или даже затронуть исходную базу.

Я предпочёл не полагаться только на качество генерации, а ограничить возможности технически. Oracle хранит данные и выполняет SQL. Подготовленные представления определяют, какие поля вообще доступны. Python-сервис публикует только заранее написанные операции. А агент в AI-приложении решает, какой инструмент вызвать по формулировке пользователя.

Архитектура схемы

Компонент Его задача
Oracle Database Исходные данные. К ней подключается выделенный пользователь с правами только на чтение.
Представления — VIEW Собирают нужные сведения из нескольких сущностей и показывают только выбранные поля.
Python + MCP Сервис подключается к базе и предоставляет агенту ограниченный набор инструментов.
HTTP endpoint /mcp Канал, по которому AI-приложение вызывает инструменты; доступ закрыт bearer-токеном.
AI-приложение и агент Получают описание инструментов, выбирают подходящий по запросу пользователя и формируют ответ по возвращённым данным.

Для меня MCP оказался удобной прослойкой между языковой моделью и обычным кодом. Агенту не нужно знать устройство базы и сочинять SQL с нуля: он видит список доступных инструментов, их параметры и описание. А уже сервер знает, как именно получить расписание или проверить соединение.

Первый шаг: ограниченный пользователь и представления

Для чтения используется отдельный пользователь. Ему разрешается подключение к базе и чтение только специально подготовленных представлений. Это главный предохранитель: доступ к остальным объектам базы ему не предоставляется.

Создание пользователя и назначение прав SQL
CREATE USER AI_READONLY
  IDENTIFIED BY "<СЕКРЕТ>"
  ACCOUNT UNLOCK;

GRANT CREATE SESSION TO AI_READONLY;

GRANT READ ON APP_SCHEMA.AI_SCHEDULE_VIEW
  TO AI_READONLY;
GRANT READ ON APP_SCHEMA.AI_ENROLLMENTS_VIEW
  TO AI_READONLY;
GRANT READ ON APP_SCHEMA.AI_CLASS_DATES_VIEW
  TO AI_READONLY;

Дальше пришлось разобраться, как устроены данные и какие связи нужны для расписания. Именно здесь пригодились представления VIEW. Если объяснять на привычном системному администратору языке, то можно привести аналогию с символьной ссылкой, через которую обращаешься к результату, хотя в данном случае сам набор данных не обязательно хранится в виде отдельной копии.

В представления я включал только то, что требовалось для сценария: название курса, даты занятий, имя преподавателя, аудиторию и агрегированные сведения о слушателях. Внутренние имена таблиц и лишние поля не нужны. Сложные связи остаются на стороне базы, а наружу выходит компактная и понятная модель данных.

Основную массу SQL-запросов предлагал Qwen, который пыхтел на моей бедной 5060, разогревая её почти до 70 градусов и выдавая около 50–70 токенов в секунду. Весь процесс описывать не буду: там были обычные пробы и ошибки. В итоге мы получили рабочие представления и проверили, что они возвращают нужные данные.

Самым полезным оказался отдельный слой с фактическими датами занятий. Дата начала курса не всегда помогает найти занятия на следующей неделе: многодневный курс мог начаться месяц назад и при этом продолжаться сейчас. Поэтому календарь занятий я вынес отдельно и использовал при построении недельного расписания.

Поднимаем MCP-сервер на Python

Сам сервер получился обычным Python-приложением. В эксперименте используются FastMCP, драйвер oracledb, python-dotenv для загрузки конфигурации и Uvicorn для HTTP-запуска. Зависимости удобно держать в виртуальном окружении, чтобы они не смешивались с остальными пакетами системы.

Подготовка окружения и установка зависимостей Bash
mkdir -p ~/mcp-oracle-serv
cd ~/mcp-oracle-serv
python3 -m venv mcp
source mcp/bin/activate

cat > requirements.txt <<'EOF'
mcp>=1.28,<2
oracledb>=3.3,<4
python-dotenv>=1.0,<2
uvicorn>=0.34,<1
EOF

pip install --upgrade pip
pip install -r requirements.txt

Параметры подключения вынесены в .env и хранятся отдельно от кода сервера: имя пользователя, пароль, адрес базы и токен доступа. Для HTTP-сервера в моём варианте использовался порт 8083, а MCP endpoint доступен по пути /mcp.

Параметры подключения .env
ORACLE_USER=AI_READONLY
ORACLE_PASSWORD="<СЕКРЕТ>"
ORACLE_DSN=<DB_HOST>:1521/<PDB_SERVICE>

MCP_HOST=0.0.0.0
MCP_PORT=8083
MCP_BEARER_TOKEN="<MCP_TOKEN>"
MCP_ALLOWED_HOSTS=localhost:8083,127.0.0.1:8083,host.docker.internal:8083

У сервера есть ещё одна важная деталь: проверка Host-заголовка и защита от DNS rebinding. Это не замена сетевым ограничениям или аутентификации, а дополнительный рубеж. Если приложение работает в контейнере, адрес, по которому идёт обращение к хостовой машине, может отличаться от localhost — это нужно учитывать при подключении.

Что именно может делать агент

Сам по себе факт, что сервер подключился к базе, ещё не означает, что агент умеет ею пользоваться. Инструменты нужно объявить в коде и описать так, чтобы модель понимала, когда и с какими аргументами их вызывать. В Python это делается декоратором @mcp.tool(). Например, проверка соединения может выглядеть так:

Инструмент проверки соединения Python
@mcp.tool()
def database_status() -> str:
    """Проверить доступность Oracle и контекст подключения."""
    sql = """
        SELECT
            SYS_CONTEXT('USERENV', 'SESSION_USER') AS SESSION_USER,
            SYS_CONTEXT('USERENV', 'CON_NAME') AS CONTAINER_NAME,
            SYSDATE AS DATABASE_TIME
        FROM dual
    """
    return query_json(sql)

Функция query_json в этом примере — вспомогательный код сервера: она открывает соединение, выполняет заранее заданный запрос и возвращает строки в структурированном виде. Модель получает доступ не к SQL-консоли, а к результату работы конкретного инструмента.

В моей конфигурации набор операций выглядел так:

  • database_status — проверить подключение, пользователя сеанса, контейнер Oracle и время базы.
  • training summary и data_quality_summary — получить сводные количества по типам обучения и проверить полноту связей между записями.
  • search_schedule — найти занятия по названию курса, городу, преподавателю, датам и другим допустимым фильтрам.
  • get_week_schedule — собрать расписание за период и вернуть его готовой Markdown-таблицей.

Фильтры дат и текстовые параметры передаются в SQL через bind-переменные, а количество возвращаемых строк ограничивается. Это важнее, чем может показаться на первый взгляд: мы не вставляем произвольный текст из запроса пользователя в SQL и не позволяем модели самой сочинить запрос любой сложности.

Дьявол в деталях

На этапе тестирования выяснилось, что запрос «покажи расписание на следующую неделю» нельзя надёжно реализовать простым фильтром по дате начала курса. Если курс стартовал раньше, но занятия продолжаются в выбранный период, он всё равно должен попасть в отчёт.

Поэтому инструмент сначала выбирает занятия, у которых есть фактические даты внутри заданного интервала, и только потом добавляет информацию о курсе и считает слушателей. Для этого используются три сценария:

  • Без дат — следующая рабочая неделя с понедельника по пятницу, рассчитанная относительно даты Oracle.
  • Одна дата — семидневный период, включающий указанную дату.
  • Две даты — интервал с включением обеих границ; максимальная длина ограничена 366 днями.

При подсчёте учитываются уникальные слушатели, отдельно очное и дистанционное обучение. А вот количество дней и даты начала и окончания описывают весь курс, а не только дни внутри выбранной недели. Это пришлось явно проговорить в описании инструмента, иначе модель могла бы трактовать цифры не так, как ожидает пользователь.

Закрываем endpoint и подключаем клиента

MCP-сервер запускается через Uvicorn и публикует Streamable HTTP endpoint /mcp. Для проверки доступности процесса предусмотрен отдельный маршрут /health, который не обращается к содержимому базы. Для вызовов MCP в этой конфигурации проверяется заголовок Authorization: Bearer <token>; без корректного токена сервер возвращает 401 Unauthorized.

Запуск MCP-сервера Bash
# Из каталога проекта, при активном виртуальном окружении
python server.py

Я отдельно проверял, что с неверным токеном вызвать инструменты нельзя, а проверка /health не раскрывает данные. Отключать межсетевой экран ради работы MCP не нужно: доступ следует разрешать только с тех узлов и портов, которым он действительно требуется.

Open WebUI

  1. Откройте настройки интеграций и добавьте MCP-сервер с транспортом Streamable HTTP.
  2. Укажите адрес сервера. Если Open WebUI работает в Docker, для обращения к сервису на хосте может использоваться адрес http://host.docker.internal:8083/mcp.
  3. Передайте заголовок авторизации Authorization: Bearer <MCP_TOKEN> в настройке подключения.
  4. Сохраните конфигурацию и проверьте, что инструменты обнаружены. После этого выберите модель и разрешите агенту вызывать подключённые инструменты.

AnythingLLM

В AnythingLLM сервер можно зарегистрировать в конфигурационном файле списка MCP-серверов. Для Streamable HTTP задаются тип подключения, URL и заголовок авторизации:

Конфигурация подключения AnythingLLM JSON
{
  "mcpServers": {
    "training-data": {
      "type": "streamable",
      "url": "http://host.docker.internal:8083/mcp",
      "headers": {
        "Authorization": "Bearer <MCP_TOKEN>"
      }
    }
  }
}

После изменения конфигурации нужно обновить список серверов в настройках навыков агента и убедиться, что функции MCP отображаются верно.

Как я проверял результат

Потребовались следующие шаги:

  1. Вызвать database_status и убедиться, что соединение открывается именно под пользователем с ограниченными правами.
  2. Запросить расписание за известный период и сравнить даты, число занятий и количество слушателей с контрольной выборкой из базы.
  3. Проверить период, в котором нет занятий, неверный формат даты, обратный порядок дат и превышение ограничения по объёму выдачи.
  4. Убедиться, что неверный токен блокирует обращение к MCP, а в ответах нет полей, не предусмотренных опубликованными представлениями.
Ответ локальной модели: расписание курсов с 1 по 5 августа 2022 года
Расписание курсов в ответ на запрос обычным языком.

В результате агент возвращает таблицу с названием курса, числом учебных дней, датами, количеством слушателей, форматом обучения, преподавателем и аудиторией. Когда я впервые получил такой ответ на обычный текстовый вопрос, то был крайне рад и ещё несколько часов терзал модель промптами, проверяя, что вся связка работает как надо.

Ответ локальной модели: создание Excel-файла со сведениями о слушателях
Создание Excel-файла по запросу пользователя.

Что в итоге получилось

Вместо прямого доступа модели к базе я получил промежуточный слой с ограниченными правами и понятными операциями. Oracle выполняет SQL-запросы, представления скрывают сложность исходной схемы, MCP-сервер публикует инструменты, а агент выбирает их по смыслу запроса. Каждый компонент делает свою часть работы, и при необходимости его можно проверять отдельно.

Мне хотелось пройти путь самостоятельно, хотя, конечно, в оболочках есть и готовые коннекторы. В моём случае важна была не скорость клика по кнопке, а понимание всей цепочки — от прав пользователя до финальной таблицы в ответе модели. Заодно стало ясно, что подключить ИИ к базе — не значит дать ему пароль от всего хозяйства. Гораздо полезнее заранее решить, какие вопросы он должен уметь задавать данным, и предоставить для этого именно необходимые инструменты.

И ещё: для всех задач по написанию Python- и SQL-кода я активно использовал помощь локальной модели, поэтому получился симбиоз, где сама нейросеть помогала строить её инфраструктуру. Полученным результатом остался крайне доволен, а слушатели моих курсов по направлению LLM Alchemy теперь смогут ознакомиться не только с теорией, но и с практикой механизма интеграции реляционных баз данных.

Поделиться
Ссылка скопирована

Возврат к списку