Сапёр в эпоху LLM: Создание Text-to-SQL агента для базы данных SAP ERP

Титульный рисунок
Титульный рисунок

Привет, Хабр! Если вы читали мою прошлую статью Сапёр в эпоху LLM: Повайбкодим на ABAP , то уже знаете, что попытка «повайбкодить» на ABAP с помощью LLM — затея, мягко говоря, неоднозначная. Модели «галлюцинируют», выдумывают несуществующие BAPI и таблицы, и в целом чувствуют себя в закрытой экосистеме SAP не очень уверенно. Как говорится, вайбкодинг не задался. В комментариях к статье прозвучала здравая мысль: будь у модели больше контекста, она бы справилась лучше.Раз появились такие идеи — значит, пора воплощать их в жизнь. На этот раз — новая серия экспериментов: в этот раз займемся переводом вопросов по SAP из обычного языка в SQL-запросы, плюс построим агента с необходимыми для этого инструментами.

Советуют - дай больше контекста в запрос, и все должно сработать. Часто это действительно работает. Но есть нюанс. «Больше» в мире SAP — это очень много. Десятки тысяч стандартных объектов. Попытка «скормить» всё это богатство в контекст любого современного LLM обречена на провал — мы просто упремся в лимит токенов. Ниже на скриншоте яркий пример - просто название всех существующих таблиц в системе превышает 6 млн токенов

Превышение токенов
Превышение токенов

Кто-то скажет: «Можно попробовать сохранить всю документацию и использовать RAG и векторные базы данных?». Идея для других областей вполне жизнеспособная, но с SAP нас ждёт засада. Названия объектов в SAP часто очень похожи, отличаются буквально одним символом. Для семантического поиска это почти одно и то же, а для системы — две совершенно разные сущности. Такая «мешанина» при поиске по смыслу приведёт к ещё большим ошибкам.
Так что же, всё безнадёжно? Не совсем.
Но здесь скрывается интересный парадокс, который и стал отправной точкой для этого исследования.

В чём секретное преимущество SAP для LLM? В его колоссальном цифровом следе. За десятилетия существования SAP вокруг него выросла гигантская экосистема: официальная документация на Help Portal, тысячи тредов на SAP Community и Stack Overflow, бесчисленные книги, блоги и опенсорс-проекты на GitHub. Многое из этого попало в обучающие данные больших языковых моделей.

В отличие от уникальной, закрытой корпоративной базы данных, стандартные таблицы SAP для LLM — старые знакомые. Модель «видела» VBAK, соединенную с VBAP, тысячи раз в разных контекстах. Она подсознательно «знает», что VBELN — это номер сбытового документа, а BUKRS — код компании.

С другой стороны, эта «осведомленность» не спасает от ранее озвученных проблем - многочисленных "галюцинаций"

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

Так родилась идея создать агента, способного транслировать запросы на естественном языке в SQL-запросы к базе данных SAP, и проверить, сможет ли такой подход преодолеть проблемы системы.

Готовим полигон для испытаний

1. Тестовый датасет

Чтобы проверить, как справится LLM, я собрал небольшой датасет из 13 вопросов. Подход был простой:

  • Вопросы должны быть конкретными и понятными. Такие, на которые может ответить любой SAP-специалист, не переспросив десять раз «а что именно имелось в виду». Например вопрос "Покажи мне название задач потока операций, которые завершились с ошибкой", надо заменить на "Покажи мне название задач потока операций в статусе ошибка", тк интерпритации , что такое ошибка может быть несколько.

  • Есть правильный ответ. Для каждого вопроса я заранее приготовил эталон, чтобы потом можно было честно сравнить результат модели с истиной.

  • Определенные области. Я специально взял задачи из интеграции, поддержки системы, стандартой демо базы, тк это обезличенные, технически нейтральные. Это позволяет проверять, как модель работает в разных зонах SAP, не переживая за конфидинциальность.

  • Только стандарт. Все запросы построены на стандартных таблицах и полях. Кастомные объекты у каждой компании свои и уникальные, тестировать на них смысла нет.

В итоге получился такой набор из 13 вопросов (под катом):

Вопрос

Тема

Сложность

1

Сколько исходящих IDoc типа ORDRSP было создано за последние 4 дня?

Интеграция

Низкая

2

Выведи список входящих IDoc из системы HYB_PROD с распределением по типам сообщений (MESTYP) на даты с 20.07.2025 по 24.07.2025.

Интеграция

Средняя

3

Собери 5 самых частых типов ошибок входящих IDoc со статусом 51 (EDIDS) за последнюю неделю, учитывая только последний статус для каждого IDoc.

Мониторинг

Высокая

4

Сколько существует в расписании авиарейсов с вылетом из Франкфурта?

Демо

Низкая

5

Из аэропорта Франкфурта в пределах Германии на самолёте с вместимостью >400 мест: в какие города можно улететь?

Демо

Средняя

6

Найди фирму - изготовителя самолетов, продукцию которого чаще всего используют в авиарейсах.

Демо

Средняя

7

Выведи название 10 стандартных таблиц данных с наибольшим количеством полей

Метаданные

Средняя

8

Покажи мне название задач потока операций в статусе ошибка, которые были созданы 30.09.2025.

Workflow

Низкая

9

Найди тип выходного документа (со средством отправки - на печать) ,который чаще создавался и был успешно обработан за 24.09.2025

Outputs

Низкая

10

В Application Log выведи 10 самых частых результатов об ошибках за май, с указанием программы-источника.

Базис

Средняя

11

Найди фоновые задания, запускавшиеся 20.09.2025, которые длились более 2 часов и завершились успешно. Выведи длительность.

Базис

Средняя

12

Найди фоновые задания, завершившиеся с ошибкой сегодня, и выведи программу, на которой произошла ошибка.

Базис

Средняя

13

Сколько произошло дампов в системе за июнь 2025 года?

Базис

Низкая

2. Инструменты

В испытуемого была выбрана система ChatCPT 4.1. Какой то специальной идее за выбором этой LLM нет, просто она была доступна для .Но техническая реалезация эксперемента гибкая, и вы можете повторить его на любой модели. В качестве инструментов, как и в прошлый раз, был выбран универсальный Python для написании логики и вызова API + SQLite для хранения и анализа результатов.

Этап 1: Проверка «в лоб» — минимум инструментов, максимум галлюцинаций

Для начала я решил проверить, сможет ли модель справиться с задачей без внешних инструментов, полагаясь только на свои знания. Я просто попросил ее вернуть готовый SQL-запрос в ответ на мой запрос.
Результат оказался предсказуемым. Из 13 уникальных вопросов успешно решены были только 2, что составляет всего 15% успеха.
В остальных случаях — полный набор «галлюцинаций», до боли знакомый по прошлой статье:

  • Выдуманные таблицы: Модель уверенно пыталась делать SELECT из несуществующей таблицы SCITY (5 вопрос).

  • Выдуманные поля: В запросах то и дело появлялись несуществующие поля вроде CREATDAT, CRETTS, STARTDATE (2, 3, 11 вопрос).

  • Синтаксические ошибки: Попытки использовать некорректные операторы, например, UFLAG & 64 в SQL (10 вопрос).

Вывод: подход «в лоб» не работает. Модели не хватает знаний о реальной структуре БД, и без обратной связи она абсолютно слепа.

Этап 2. Агент с инструментами

После провала «в лоб» стало понятно: модели нужна помощь.Идея в том, чтобы дать LLM возможность использовать внешние инструменты для проверки своих гипотез.Я предоставил агенту три таких инструмента, которые он мог вызывать по своему усмотрению:

  • are_tables_present(table_names: list): Проверяет существование таблиц в системе.

  • get_table_fields(table_name: str): Возвращает список полей для таблицы с их описаниями и свойствами.

  • run_sap_sql_query(sql_query: str): Выполняет SQL-запрос и возвращает результат или ошибку.

Как это реализовано «под капотом», как соединить Python и SAP ERP? Вариантов может быть несколько: можно написать OData-сервис для трансляции вызовов в SQL, использовать RFC-вызовы (например, RFC_READ_TABLE) c помощью библиотеки PyRFC . Или мой вариант - использовать SAP GUI Scripting для взаимодействия с транзакцией DBACOCKPIT. Выбор пал на этот вариант , как на самый быстрый в реализации и не требующий особых полномочий. И разумеется, соблюдаем строгий принцип безопасности: LLM применяется только для чтения данных, а все операции изменения заблокированы на программном уровне даже на тестовой системе.

Проектируем «мозг» агента

В качестве архитектуры я выбрал подход, похожий на SGR (Schema-Guided Reasoning ). Агент на каждом шаге определяет, что ему делать дальше, и генерирует JSON-объект следующего шага: проверить таблицы, изучить поля, сделать пробный запрос или выдать финальный ответ. Мой Python-скрипт выступал в роли диспетчера: получал JSON, вызывал нужный инструмент и возвращал результат агенту для следующего шага.Цикл размышлений останавливался, когда LLM решала, что задача выполнена, либо заканчивались шаги для размышления, которые мы явно прописываем в цикле. Схема агента и промт следующий:

# ===== СХЕМЫ =====
class Tool_GetTableFields(BaseModel):
    tool: Literal["gettablefields"]
    table_name: Annotated[str, MinLen(1), MaxLen(40)]
class Tool_RunSapSqlQuery(BaseModel):
    tool: Literal["runsapsql_query"]
    query: Annotated[str, MinLen(10)]
    name: Optional[Annotated[str, MinLen(2), MaxLen(40)]] = None
class FinalAnswer(BaseModel):
    intent_summary: Annotated[str, MinLen(8), MaxLen(400)]
    sql_used: Annotated[str, MinLen(10)]
    result_summary: Annotated[str, MinLen(8), MaxLen(2000)]
    confidence: Annotated[float, Ge(0.0), Le(1.0)]
class Step_SelectTables(BaseModel):
    kind: Literal["select_tables"]
    thought: Annotated[str, MinLen(10)]
    tables_to_verify: Annotated[List[str], MinLen(1)]
class Step_ExploreAndProbe(BaseModel):
    kind: Literal["explore_and_probe"]
    thought: Annotated[str, MinLen(10)]
    actions: Annotated[List[Union[Tool_GetTableFields, Tool_RunSapSqlQuery]], MinLen(1)]
class Step_ExecuteFinalQuery(BaseModel):
    kind: Literal["execute_final_query"]
    thought: Annotated[str, MinLen(10)]
    final_sql: Annotated[str, MinLen(10)]
class Step_ProvideFinalAnswer(BaseModel):
    kind: Literal["provide_final_answer"]
    answer: FinalAnswer
class NextStep(BaseModel):
    next_step: Union[Step_SelectTables, Step_ExploreAndProbe, Step_ExecuteFinalQuery, Step_ProvideFinalAnswer]

# ===== ПРОМПТ =====
SCHEMA_JSON = json.dumps(NextStep.model_json_schema(), indent=2, ensure_ascii=False)
SYSTEM_PROMPT = f"""
Ты ассистент по SAP (ECC/S/4). Преобразуй запрос пользователя в SQL пошагово.
На каждом ходе верни РОВНО ОДИН JSON по схеме <JSON SCHEMA>.
ИНСТРУМЕНТЫ:
- are_tables_present
- get_table_fields
- run_sap_sql_query
ПРАВИЛА РАБОТЫ:
- CDS/HANA views не использовать. Z* таблицы не предлагать.
- При сомнениях существования таблиц — select_tables; для анализа полей таблиц — gettablefields.
- Разрешены пробные запуски в процессе размышления run_sap_sql_query. Ключевая особенность пробных запусков — всегда использовать ORDER BY для детерминированности и LIMIT для безопасности.
- Финальный SQL  - выполняется отдельно , без ограничений. 
ВСПОМОГАТЕЛЬНАЯ ИНФОРМАЦИЯ ДЛЯ ПОИСКА ОТВЕТА:
- У многих объектов в системе есть основная запись(header), и позиции, подпозиции, статусы итп. При поиске и связях не забываем группировать по основному номеру, если это требуется.


ВОЗВРАЩАЙ ТОЛЬКО JSON ПО СХЕМЕ.
<JSON SCHEMA>
{SCHEMA_JSON}
</JSON SCHEMA>
""".strip()

Этап 3: Получение сводных результатов

Прогнав те же 13 вопросов через нового агента, я получил кардинально иные результаты. Вместо галлюцинаций и синтаксических ошибок я увидел осмысленную цепочку действий.
Вот сухая выжимка из логов, которая говорит сама за себя:
Агент успешно справился с 11 из 13 задач (85% успеха)! Разница колоссальная.

Давайте посмотрим на один из провальных кейсов первого этапа, который агент выполнил легко.

Задача. Найди фоновые задания, запускавшиеся 20.09.2025, которые длились более 2 часов и завершились успешно. Выведи длительность.

Подход «в лоб».

SELECT BJ.JOBCOUNT, BJ.JOBNAME, BJ.STATUS, BT.STARTDATE, BT.STARTTIME, BT.ENDDATE, BT.ENDTIME, 
  ( 
    (TO_NUMBER(SUBSTRING(BT.ENDTIME,1,2)) * 3600 + TO_NUMBER(SUBSTRING(BT.ENDTIME,3,2)) * 60 + TO_NUMBER(SUBSTRING(BT.ENDTIME,5,2)))
    - (TO_NUMBER(SUBSTRING(BT.STARTTIME,1,2)) * 3600 + TO_NUMBER(SUBSTRING(BT.STARTTIME,3,2)) * 60 + TO_NUMBER(SUBSTRING(BT.STARTTIME,5,2)))
  ) / 3600 AS DURATION_HOURS
FROM BTCJOB BJ
JOIN BTCTIME BT ON BJ.JOBCOUNT = BT.JOBCOUNT
WHERE BT.STARTDATE = '20250920'
  AND BJ.STATUS = 'F'
  AND (
        (
          (TO_NUMBER(SUBSTRING(BT.ENDTIME,1,2)) * 3600 + TO_NUMBER(SUBSTRING(BT.ENDTIME,3,2)) * 60 + TO_NUMBER(SUBSTRING(BT.ENDTIME,5,2)))
          - (TO_NUMBER(SUBSTRING(BT.STARTTIME,1,2)) * 3600 + TO_NUMBER(SUBSTRING(BT.STARTTIME,3,2)) * 60 + TO_NUMBER(SUBSTRING(BT.STARTTIME,5,2)))
        ) / 3600
      ) > 2
ORDER BY DURATION_HOURS DESC

Ошибка: invalid table name: Could not find table/view BTCJOB

Как действовал Агент.

Агент_Размышление1
Агент_Размышление1
Агент_Размышление2
Агент_Размышление2

Как видите, возможность пошагово проверять свои гипотезы кардинально меняет дело.

Этап 4: Анализ ошибок и повторная обработка

Проанализируем, ошибочнчные результаты. Оставшиеся 2 неудачи были связаны не с синтаксисом, а с более тонкими логическими ошибками - был перепутан тип входящего и исходящего IDoc. Делаем вывод, что агенту не хватает знаний о текстовых значениях к доменам. Он должен чеко понимать, что исходящий тип - это 1 , а входящий -2 . Поможем ему в этом, а именно создадим еще один инструмент

  • get_domain_texts(domain_name: str): Возвращает список с постоянными идентификаторами и названиями домена.

А так же добавим новые инструменты в код и укажем в промте возможность его использования

# ===== СХЕМЫ =====
class Tool_GetDomainTexts(BaseModel):
    tool: Literal["get_domain_texts"]
    domain_name: Annotated[str, MinLen(1), MaxLen(40)]

Пробуем прогнать новую реализацию на ошибочных вопросах. Агент разобрался в типах сообщений и выдал верный SQL. Бинго!

Инсайты и практические выводы

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

  • Фиксируйте всё.

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

  • Ограничивайте запросы.

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

  • Пошаговый подход работает.

    Да, это медленнее, но зато значительно надёжнее. Проверка таблицы → проверка полей → пробный SQL — эта последовательность реально снижает количество «галлюцинаций».

  • Не верьте в 100% повторяемость.

    Один и тот же промпт, запущенный несколько раз, может дать разные цепочки рассуждений. Для оценки стабильности я прогонял одни и те же вопросы несколько раз.

    Эти простые правила позволили заметно повысить процент правильных ответов. И именно на этом этапе стало ясно: да, у такого подхода есть перспектива.

Бонус-трек: Специализированный агент vs. универсальные платформы

Напоследок мне стало интересно: а как справятся с этой задачей другие, более общие агентские платформы? Я взял свои три инструмента и подключил их как MCP к IDE Trae, которую упоминал в прошлой статье, и прогнал те же вопросы на разных LLM.

MCP для SAP SQL в Trae
MCP для SAP SQL в Trae

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

Заключение

Эксперимент показал простую вещь: напрямую заставить LLM писать корректные SQL-запросы для SAP не получается. Даже сильная модель вроде GPT быстро спотыкается на чуть более сложных условиях, придумывает таблицы и поля, путает связи.
Как только у модели появляются инструменты — ситуация меняется. Проверка таблиц, проверка полей, пробные запросы — эти простые шаги резко повышают вероятность получить рабочий результат. Да, агент работает медленнее, и тратит намного больше токенов, но зато выдаёт то, что реально можно выполнить в системе.
Сравнение с другими агентами тоже подтверждает: персонализированное решение под SAP работает стабильнее, чем универсальные конструкции. Контроль шагов и жёсткая схема выигрывают у «магии» без правил.
Поэтому мой вывод такой:

  • «Наивный» подход без инструментов в SAP не работает.

  • Агент с проверками и пошаговой логикой — это уже рабочий вариант.

  • Дальше можно думать про расширение: подтягивать связи и описание типов из DDIC, подключать внешний поиск итд.

Код для построения агентов, доступен в репозитории на GitHub Text to SQL SAP Agent.
Пробуйте, экспериментируйте! Буду рад вашим комментариям, идеям и критике.

Комментарии (2)


  1. GordonFreemann
    08.10.2025 23:56

    Не понял, где тут участвует SAP Gui Scripting? Большинство задач решаются стандартными транзакциями (или это были просто тестовые примеры?)

    На самом деле успешно вайбкодить можно и без "обратной связи", но важно указывать релиз, SP level и тд. Владение ABAP/Java в достаточной степени обязательно, чтоб критически осмыслять предложенный код, смочь исправить косяки самому, если ллм не справляется. Имо не стоит ожидать от ллм комплексного решения, но как каркас для будущего функционала, идея реализации(to-be) оно всё таки годное вполне.


    1. gennadybanin Автор
      08.10.2025 23:56

      Через Sap GUI Scripting, мы в программе обращаемся к открытой у нас в другом окне транзакции DBACOCKPIT(SQL редактор). Получается следущий маршрут доставки запросов в SAP : LLM ->Python ->GUI Scripting ->SQL редактор