Spring Data JDBC Skill для AI-агентов: качество Opus дешево | AiManual
AiManual Logo Ai / Manual.
24 Июн 2026 Гайд

Spring Data JDBC Skill для AI-агентов: как получить качество Opus за копейки

Гайд: как с помощью Spring Data JDBC и дешевых LLM добиться точности Opus при работе с базами данных. Экономьте токены, не жертвуя надежностью.

Реклама
partv1

Почему кормить LLM сырыми SQL-запросами — это выжигание денег

Типичная сцена: AI-агент получает задачу «выбрать все заказы клиента за последний месяц». LLM генерирует SQL, отправляет его в базу, парсит результат. Звучит логично? На практике это — финансовое безумие. Opus 4.5 сожрет тысячи токенов только на том, чтобы сформулировать правильный запрос. А если SQL ошибочный — цикл повторяется.

Ещё хуже: генерация SQL через LLM — главный источник недетерминированных багов. Один раз она выдаст SELECT * FROM orders WHERE user_id = ?, в другой — SELECT * FROM orders WHERE customer_id = ?. Разница в одной букве, а результат — потеря данных. В агентном workflow Suzano именно от таких ошибок отказывались на первых итерациях.

Ключевое заблуждение: умная LLM == умный SQL. Нет. LLM не понимает схему базы, она просто угадывает. Доверять генерацию запросов нейросети — всё равно что нанять джуна, который не смотрел на ER-диаграмму.

Решение лежит на поверхности: нужно дать агенту готовые функции, которые принимают параметры и выполняют безопасные, заранее написанные запросы. Паттерн называется tool calling или функция-инструмент. Spring Data JDBC здесь — идеальный кандидат: он лёгкий, не тащит JPA-магию и легко оборачивается в REST-подобные методы.

Spring Data JDBC Skill: архитектура, которая режет затраты в 10 раз

Вместо того чтобы заставлять LLM писать SQL, мы создаём набор Skill-функций. Каждая функция — это метод репозитория, который описан в JSON Schema и зарегистрирован в AI-агенте. Агент выбирает нужную функцию, передаёт параметры, получает результат. Весь «умный» SQL остаётся в Java-коде.

💡
Почему JDBC, а не JPA? Spring Data JDBC — прямой наследник JDBC без слоя кешей и lazy loading. Агент не должен ждать, пока Hibernate соберёт граф объектов. JDBC режим «запрос-ответ» даёт минимальную латентность. Для сравнения: в этой статье мы сжимали латентность поиска с 3500 до 700 мс — похожий принцип.

Архитектура выглядит так:

  • Класс сущности (например, Order).
  • Репозиторий, наследующий CrudRepository.
  • Сервис, который вызывает методы репозитория и возвращает DTO.
  • Описание функции (JSON Schema) для LLM-провайдера.
  • Регистрация функции в ToolCallback или аналогичном механизме.

Зачем здесь Spring AI? Этот фреймворк (подробный обзор тут) предоставляет готовые адаптеры для OpenAI, Anthropic, Ollama и других. Он автоматически сериализует вызов функции и встраивает её в промпт. Без Spring AI пришлось бы руками парсить ответы — а это частая причина багов.

Пошаговая сборка: от интерфейса репозитория до AI-инструмента

Покажу на примере: агент должен уметь получать заказы по ID клиента и статусу. Сделаем две функции. Весь код — Java 21, Spring Boot 3.4, Spring Data JDBC 3.4, Spring AI 1.4.

1 Сущность и репозиторий

@Table("orders")
public record Order(@Id Long id, Long customerId, String status, BigDecimal total) {}
public interface OrderRepository extends CrudRepository<Order, Long> {
    List<Order> findByCustomerIdAndStatus(Long customerId, String status);
    List<Order> findByCustomerId(Long customerId);
}

Spring Data JDBC сам сгенерирует SQL на основе имени метода. Без магии, без runtime-прокси — чистый SQL, который вы написали. Это безопасно и предсказуемо.

2 Сервис-обёртка с DTO

@Service
public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    // Функция 1: получить заказы клиента по статусу
    public List<OrderDTO> getOrdersByCustomerAndStatus(Long customerId, String status) {
        return repository.findByCustomerIdAndStatus(customerId, status)
                .stream()
                .map(o -> new OrderDTO(o.id(), o.customerId(), o.status(), o.total()))
                .toList();
    }

    // Функция 2: получить все заказы клиента
    public List<OrderDTO> getOrdersByCustomer(Long customerId) {
        return repository.findByCustomerId(customerId)
                .stream()
                .map(o -> new OrderDTO(o.id(), o.customerId(), o.status(), o.total()))
                .toList();
    }
}

3 Описание функций (JSON Schema) и регистрация в агенте

Используем Spring AI ToolCallback. Описываем каждую функцию через аннотации @Tool (доступен в Spring AI 1.4+).

@Service
public class OrderTools {
    private final OrderService orderService;

    public OrderTools(OrderService orderService) {
        this.orderService = orderService;
    }

    @Tool(name = "get_orders_by_customer_and_status", description = "Возвращает заказы клиента по ID и статусу")
    public List<OrderDTO> getOrdersByCustomerAndStatus(
            @ToolParam(description = "ID клиента") Long customerId,
            @ToolParam(description = "Статус заказа (NEW, PROCESSING, SHIPPED)") String status) {
        return orderService.getOrdersByCustomerAndStatus(customerId, status);
    }

    @Tool(name = "get_all_orders_by_customer", description = "Возвращает все заказы клиента по ID")
    public List<OrderDTO> getAllOrdersByCustomer(
            @ToolParam(description = "ID клиента") Long customerId) {
        return orderService.getOrdersByCustomer(customerId);
    }
}

Теперь подключаем инструменты к агенту. В конфигурации Spring AI:

@Bean
public ChatClient chatClient(ChatClient.Builder builder, List<ToolCallback> toolCallbacks) {
    return builder
            .defaultSystem("Ты — помощник по работе с заказами. Используй инструменты для запросов в БД.")
            .defaultTools(toolCallbacks)
            .build();
}

Готово! Теперь агент (даже на дешёвой модели вроде Claude 3.5 Haiku через Ollama) может выполнять точные запросы к базе, тратя на вызов функции 200-300 токенов вместо 2000+ на генерацию SQL.

Три грабли, на которые наступают все (и как их обойти)

Грабли Последствия Решение
Слишком общие названия функцийАгент вызывает не ту функцию или игнорирует параметрыИменуйте как в REST: get_orders_by_customer_and_status, а не fetch_data
Отсутствие описаний у параметровLLM передаёт мусорные значения (например, статус "active" вместо "ACTIVE")Добавляйте enum-ограничения в "enum": ["NEW","PROCESSING","SHIPPED"]
Не проверять результаты на стороне агентаПустой список трактуется как ошибка, начинается бесконечный ретрайВозвращайте явный флаг: "orders": [], "found": false

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

Ещё одна типичная ошибка — транзакции. Если функция выполняет запись, а агент вызывает её параллельно, возможны состояния гонки. Решение: сделать все методы write-функций синхронизированными или использовать @Transactional на уровне сервиса.

Считаем деньги: Opus против дешевой модели + JDBC Skill

Возьмём типовой сценарий: агент обрабатывает 10000 запросов в месяц. Каждый запрос требует двух обращений к БД.

Сценарий Среднее токенов на вызов Стоимость 1K токенов (вход) Итого в месяц
Opus 4.5, генерация SQL~1500 токенов (вход 1000 + выход 500)$0.015 (выход ~$0.075)~$75
Haiku 3.5 + JDBC Skill~200 токенов (вход 100 + выход 100)$0.00025 (выход ~$0.00125)~$1.25

Разница в 60 раз. И это без учёта повторов на ошибках. Если Opus генерирует невалидный SQL в 20% случаев, цена взлетает до $90. Haiku с инструментом ошибается только в выборе функции (редко, если хорошо описали).

Кстати, на Open Agent Leaderboard видно: агенты с tool calling стабильно обходят чистые LLM по точности при одинаковой цене. А инструменты на Spring Data JDBC ещё и быстрее — в бенчмарке IBM JDBC-функции показали latency в 2 раза ниже, чем JPA.

Совет, который вы не ожидали

Самый частый вопрос: «А если я захочу дать агенту доступ к любой таблице?» Не делайте так. Полный произвольный SQL через функцию — это дыра в безопасности и гарантированный хаос. Вместо этого создайте мета-функцию, которая возвращает описание схемы, и позвольте агенту вызывать только заранее зарегистрированные запросы. Это как DESCRIBE TABLE, только для AI.

Попробуйте внедрить такой подход в своём проекте. Начните с одной критической таблицы — результаты удивят. Если понадобится оптимизировать промпты для выбора функций, рекомендую глянуть на GEPA optimize_anything — он как раз умеет подбирать описания инструментов, чтобы LLM реже ошибалась.

Подписаться на канал