Введение (проблематика)
В типовом BI-проекте данные проходят некоторый путь от источника данных до аналитического отчёта:
Источник данных → ETL-процессы → DWH (витрины) → OLAP-cube → Меры
На выходе получаем множество мер — ключевых показателей бизнеса
вроде: "Выручка", "Средний чек", "Конверсия"; каждая из которых это результат
цепочки трансформаций данных через SQL-процедуры, представления
и DAX-формулы.
Когда в проекте более 200 мер, удержать все детали в голове сложно, и при вопросе пользователя: - "Откуда берётся значение в мере [Долг поставщика]?", разработчик вынужден:
Открыть .bim файл, найти DAX-формулу меры;
Определить, какие OLAP-таблицы она использует;
Найти соответствующие DWH views;
Проследить до stored procedures и понять логику расчета;
Сформулировать ответ.
В зависимости от характера вопроса и сложности самого показателя на одну меру может потребоваться 15-20 минут.
Архитектура решения
Первоначальный подход и его особенности

Первоначальное решение было реализовано в виде MCP-сервера, основная задача которого — создавать однотипные промпты для списка переданных мер, с указанием расположения составных частей проекта (.bim-файла, процедур и представлений). Ответ MCP-сервера возвращается Cursor-агенту и тот действует в соответствии с полученной инструкцией - создает новое описание, вставляет в документацию.
В этой статье не рассматриваются вопросы подключения MCP-server к Cursor,
поскольку на данную тему уже есть публикации, в том числе на Хабре.
Плюсы:
- автоматизируем значительную часть "ручного труда", связанного с составлением описания мер, учитывающего особенности всего процесса трансформации данных внутри проекта.
Недостатки: выявленные при эксплуатации
- при единовременной передаче обширного списка мер может не хватить контекстного окна модели, выбранной при запуске Cursor-агента, для составления документации по всему перечню мер (агент падает);
- при обработке списка мер агент может либо зацикливаться, либо частично переписывать ранее созданные блоки для уже обработанных мер (приводит к перерасходу токенов);
- вышеперечисленные пункты требуют ручного управления процессом - разбивать список мер на небольшие части (2-3 меры), следить за ходом выполнения;
- при обработке даже 2-3 мер за один запуск Cursor-агента желателен выбор "дорогих" моделей для успешного завершения задачи.
Пример вызова MCP-server tech-spec (в ветке main находится версия MCP-сервера, вызов которого показан на скрине).

Пример полученного описания меры [Кол-во]
1. Общая информация
Название меры: Кол-во
Тип: Мера (Measure)
Источник: OLAP.bim
Папка отображения: Базовые меры
Формат отображения: #,0
2. Назначение (Цель/задача)
Мера "Кол-во" предназначена для подсчета общего количества товаров/услуг в заказах клиентов с учетом определенных фильтров. Данная мера является базовой и используется для расчета других производных показателей в аналитических отчетах.ionos
3. Исходные данные
Основная таблица: М001 Заказы Клиентов
Основное поле: [Количество]
Дополнительные поля для фильтрации:
[ВГО булево] - булево поле для исключения ВГО (внутригрупповые операции)
[Отмененный] - булево поле для исключения отмененных заказов
4. Логика обработки
4.1. Выражение DAX
CALCULATE (
SUM ( 'М001 Заказы Клиентов'[Количество] ),
NOT 'М001 Заказы Клиентов'[ВГО булево],
NOT ( 'М001 Заказы Клиентов'[Отмененный] )
)
4.2. Описание логики
Основная агрегация: Суммирование значений поля [Количество] из таблицы 'М001 Заказы Клиентов'.
Фильтрация по ВГО: Исключение записей, где [ВГО булево] = TRUE.
Фильтрация по статусу: Исключение отмененных заказов, где [Отмененный] = TRUE.
Контекстная фильтрация: Мера автоматически применяет фильтры из срезов и других измерений.
5. Результат
5.1. Выходные данные
Тип данных: Числовой (Decimal)
Формат отображения: Целое число с разделителями тысяч (#,0)
Единица измерения: Штуки/единицы товара
5.2. Примеры использования
Общее количество товаров в заказах - без дополнительных фильтров.
Количество по клиентам - с фильтром по конкретному клиенту.
Количество по периодам - с фильтром по дате заказа.
Количество по товарным группам - с фильтром по категории товара.
6. Зависимости
6.1. Таблицы
М001 Заказы Клиентов - основная таблица фактов.
Связанные справочные таблицы через внешние ключи.
6.2. Поля
[Количество] - основное поле для агрегации.
[ВГО булево] - поле для фильтрации.
[Отмененный] - поле для фильтрации.
6.3. Связанные меры
Мера может использоваться в качестве основы для:
Производных мер (проценты, коэффициенты).
Расчетных полей.
Других агрегированных показателей.
6.4. Поток данных
OLAP мера: [Кол-во] в кубе OLAP.
Представление DWH: Данные для меры поступают из представления [uni].[vt_order].
Таблицы фактов DWH: Представление [uni].[vt_order] агрегирует данные из следующих таблиц:
[dbo].[fact_order]
[dbo].[fact_cancelled_order]
[dbo].[fact_order_characteristics_received]Хранимые процедуры: Эти таблицы фактов загружаются и обрабатываются следующими хранимыми процедурами:
[dbo].[sp_load_fact_order]
[dbo].[sp_load_fact_cancelled_order]
[dbo].[sp_load_fact_order_characteristics_received]
[dbo].[sp_load_fact_order]
Назначение: Процедура формирует основной набор данных для анализа продаж.
Источники и логика:
Основные данные: Берутся из документа "Заказ клиента" из системы 1С. Для каждой товарной позиции в заказе извлекается информация о количестве, цене, сумме, предоставленных скидках и ставке НДС.
Обогащение данных: Информация о заказе дополняется сведениями из связанных справочников 1С:
Клиент и партнер: Кто является покупателем.
Менеджер: Кто ответственный за заказ.
Номенклатура: Какой товар заказан.
Организация: От лица какой нашей организации оформлен заказ.
Склад, подразделение, сезон: Дополнительные аналитические разрезы.
Финансовые показатели: Процедура также рассчитывает состояние оплаты по заказу, обращаясь к регистру "Расчеты с клиентами", и определяет наличие просроченной задолженности.
Бизнес-ценность: Позволяет получить полную и обогащенную картину по каждому заказу для построения отчетов по продажам, маржинальности и эффективности работы менеджеров.
[dbo].[sp_load_fact_cancelled_order]
Назначение: Процедура собирает данные по отмененным позициям в заказах клиентов.
Источники и логика:
Основные данные: Как и в предыдущей процедуре, источником является документ "Заказ клиента" из 1С. Ключевое отличие — отбираются только те строки товаров, у которых установлен признак "Отменено".
Дата отмены: Для понимания, когда именно произошла отмена, процедура обращается к регистру "История изменений заказа клиента".
Контекст заказа: Вся остальная информация (клиент, менеджер, суммы, скидки) подтягивается аналогично процедуре загрузки основных заказов, чтобы сохранить полный контекст на момент отмены.
Бизнес-ценность: Дает возможность анализировать объем и причины отмен, оценивать упущенную выручку и выявлять тенденции, связанные с отказами от товаров.
[dbo].[sp_load_fact_order_characteristics_received]
Назначение: Процедура отслеживает поступление товаров по заказам, у которых в процессе обработки менялись характеристики (например, цвет, размер).
Источники и логика:
Основные данные: Процедура отталкивается от данных, где зафиксированы факты поступления товаров по заказам, в которых были изменения (fact_order_characteristics_changes).
Связь с заказом: Используя номер заказа, процедура обращается к исходному документу "Заказ клиента" в 1С, чтобы получить полную информацию о клиенте, менеджере, дате и других атрибутах заказа.
Фильтр: Отбираются только те записи, которые отражают фактическое поступление товара (quantity_received > 0), а не просто резервирование.
Бизнес-ценность: Позволяет контролировать и анализировать исполнение "сложных" заказов, где характеристики товара были изменены. Это помогает понять, как такие изменения влияют на сроки и полноту выполнения заказа.
Эволюция проекта

Для устранения недостатков, выявленных во время эксплуатации, было решено использовать вызов Cursor CLI из скрипта, и управлять процессом при помощи LangGraph.
Получаем агента на базе LangGraph, который:
- INPUT NODE — инициализирует состояние графа, принимая запрос пользователя и пути к рабочим директориям (процедуры, OLAP, документация);
- PROCESS MEASURES LIST — обработка полученного сообщения от пользователя (структурирование данных). Задача - извлечь из текста структуру JSON с топиками (бизнес-разделами) и мерами;
- CURSOR CLI — итерируется по извлеченному списку мер. Для каждой меры вызываем cursor-agent с инструкцией использовать MCP Server tech-spec.
Документация Cursor CLI.
Репозиторий LangGraph c учебными материалами и инструкциями по установке.
Плюсы:
Итеративный вызов Cursor CLI, с передачей в качестве аргумента лишь одной меры, обеспечивает:
- возможность использовать более дешевые модели, так как снижаются требования к размеру окна контекста;
- логирование результатов выполнения узла;
- сохранение описания в отдельный документ (решается проблема с зацикливанием Cursor-агента, и перерасходом токенов).
Недостатки:
- так как циклический вызов Cursor CLI выполняется внутри одного узла, то логируется общий итог выполнения "ноды", без декомпозиции в графе обработки каждой, отдельно взятой, меры.
Для наглядности обратимся к скринам, описывающим пример обработки такого промпта:
# Продажи
## [Кол-во], [Рейтинг Потенциал]


На первом скрине видно, что в measure_list_node входной промпт был переложен в структуру, описанную для атрибута input_mrs класса OverallState.
class OverallState(TypedDict):
"""This is the general graph state"""
question: str
input_mrs: List[Dict[str, List[str]]]
processed_mrs: List[Dict[str, List[str]]]
missed_mrs: List[Dict[str, List[str]]]
created_docs: List[str]
directories: DirectoryList
Каждая из перечисленных мер ('Кол-во', 'Рейтинг Потенциал') была отработана циклическими вызовами Cursor CLI внутри cursor_cli_node. Однако, в Waterfall трассировки мы не видим деталей по каждому отдельному вызову Cursor CLI. Это стало поводом переработки графа в его итоговую структуру.
Итоговое решение

Главное отличие новой версии — переход от линейной обработки к циклическому графу состояний (Cyclic State Graph).
Ключевые изменения:
- введен узел process_router_node, который управляет итерациями. Он выбирает одну текущую меру для обработки и передаёт её исполнителю (Cursor CLI);
- узел Cursor CLI теперь обрабатывает ровно одну задачу за раз и возвращает управление роутеру;
- добавлен новый узел combine_doc_node - теперь, после завершения всех итераций, агент автоматически собирает все созданные фрагменты в единый документ combined_manual.md.
Финальная версия агента находится в репозитории lc-manual-creator-agent .
Рассмотрим пример работы агента, передав ему следующий промпт:
# Продажи
## [Кол-во], [Рейтинг Потенциал]
# Транспорт
## [Кол-во рейсов]


Отлично! Теперь мы видим последовательные вызовы cursor_cli_node, с фиксацией в state результатов работы каждого шага.

Напомним, что перед агентом стояла задача составить описание трёх мер из двух бизнес-разделов (Продажи: "Кол-во" и "Рейтинг Потенциал"; Транспорт: "Кол-во рейсов").
Общая продолжительность работы агента составила ~ 10 мин. (625 сек.).
Вызов Cursor CLI осуществлялся с хинтом --model "auto".
Как это работает: полный цикл выполнения
Рассмотрим детально, что происходит внутри агента от момента получения запроса до создания финальной документации.
Шаг 1: Input Node — инициализация состояния
Что происходит:
Агент получает пользовательский запрос в произвольном формате (желательно подавать в markdown);
Инициализирует граф состояний (
OverallState) с путями к рабочим директориям;Проверяет наличие необходимых компонентов: .bim-файла, SQL-процедур, представлений.
Пример входных данных:
# Продажи
## [Кол-во], [Рейтинг Потенциал]
# Транспорт
## [Кол-во рейсов]
Состояние графа после выполнения:
{
"question": "# Продажи\n## [Кол-во]...",
"input_mrs": [], # пока пусто, заполнится на следующем шаге
"directories": {
"stored_procedures_dir": "/path/to/procedures",
"views_dir": "/path/to/views",
"olap_dir": "/path/to/olap",
"store_doc_dir": "/path/to/output"
}
}
Шаг 2: Measure List Node — интеллектуальный парсинг
Что происходит:
Узел отправляет запрос к локальной LLM (через LM Studio) для извлечения структурированного списка мер в формате JSON.
Промпт для LLM:
Извлеки из текста структуру: каждая тема (заголовок #)
и связанные с ней меры (в квадратных скобках).
Верни JSON в формате:
[{"topic": "Продажи", "measures": ["Кол-во", "Цена"]}, ...]
Механизм фалбэка:
Если LLM недоступна или возвращает ошибку (таймаут, неправильный формат), агент автоматически переключается на regex-парсинг:
# Упрощённая логика фалбэка
try:
response = llm_call(user_input)
measures = parse_json(response)
except Exception:
# Откат на regex
measures = regex_parse(user_input)
Результат парсинга:
"input_mrs": [
{"topic": "Продажи", "measures": ["Кол-во", "Рейтинг Потенциал"]},
{"topic": "Транспорт", "measures": ["Кол-во рейсов"]}
]
Этот шаг критически важен: от качества структурирования зависит корректность всех последующих операций.
Шаг 3: Process Router Node — управление итерациями
Что происходит:
Роутер определяет следующий шаг выполнения, отвечая на вопрос: "Остались ли необработанные меры?"
Логика маршрутизации:
# Псевдокод логики
def router(state: OverallState):
# Собираем все обработанные меры (успешные + пропущенные)
processed_measures = set()
for item in state["processed_mrs"] + state["missed_mrs"]:
for measure in item["measures"]:
processed_measures.add((item["topic"], measure))
# Ищем необработанные меры в input_mrs
for topic_dict in state["input_mrs"]:
for measure in topic_dict["measures"]:
if (topic_dict["topic"], measure) not in processed_measures:
# Нашли необработанную → передаём в CLI
return "cursor_cli_node"
# Все меры обработаны → объединение документов
return "combine_doc_node"
Важная деталь: Роутер не обрабатывает все меры сразу, а выбирает одну текущую меру и передаёт её в исполнитель. Это обеспечивает гранулярность логирования.
Шаг 4: Cursor CLI Node — генерация документации
Что происходит:
Для выбранной меры формируется промпт и вызывается Cursor CLI с инструкцией использовать MCP-сервер tech-spec.
Формирование промпта:
prompt = f"""
Используя MCP-сервер tech-spec, создай техническую спецификацию для меры:
- Топик: {current_topic}
- Мера: {current_measure}
Пути к компонентам проекта:
- OLAP: {state.directories.olap_dir}
- Процедуры: {state.directories.stored_procedures_dir}
- Представления: {state.directories.views_dir}
Сохрани результат в файл: {store_doc_dir}/{current_topic}_{current_measure}.md
"""
Вызов CLI:
cursor-agent --print --force --output-format text \
--model auto "{prompt}"
Обработка результата:
Парсинг вывода CLI для извлечения пути к созданному файлу;
-
Обновление состояния:
Если успешно → добавить в
processed_mrs, сохранить путь вcreated_docs;Если ошибка/таймаут (10 мин) → добавить в
missed_mrs;
input_mrsостаётся неизменным — сохраняется исходный список для отчётности.
Таймаут и обработка ошибок:
try:
result = subprocess.run(
["cursor-agent", ...],
timeout=600, # 10 минут на меру
capture_output=True
)
# Извлечение пути к файлу из stdout
file_path = extract_file_path(result.stdout)
# Добавляем в processed, НО НЕ удаляем из input_mrs
state["processed_mrs"].append({
"topic": topic,
"measures": [measure]
})
state["created_docs"].append(file_path)
except subprocess.TimeoutExpired:
state["missed_mrs"].append({
"topic": topic,
"measures": [measure],
"reason": "timeout"
})
Шаг 5: Возврат к Router — циклическая проверка
Что происходит:
После обработки одной меры управление возвращается в Process Router Node (см. Шаг 3).
Граф состояний:
cursor_cli_node → process_router_node → [есть меры?]
↙ ↘
Да Нет
↓ ↓
cursor_cli_node combine_doc_node
Цикл продолжается до тех пор, пока все меры не будут обработаны или добавлены в missed_mrs.
Шаг 6: Combine Doc Node — объединение документации
Что происходит:
Когда все меры обработаны, агент собирает отдельные файлы в единый документ.
Логика объединения:
def combine_documents(state: OverallState):
combined_content = "# Техническая документация по мерам\n\n"
# Группировка по топикам
for topic_dict in state["processed_mrs"]:
combined_content += f"## {topic_dict['topic']}\n\n"
# Чтение каждого созданного документа
for doc_path in state["created_docs"]:
if topic_dict["topic"] in doc_path:
with open(doc_path, 'r') as f:
combined_content += f.read() + "\n\n"
# Сохранение объединённого файла
output_path = f"{state.directories.store_doc_dir}/combined_manual.md"
with open(output_path, 'w') as f:
f.write(combined_content)
Результат:
Создаётся файл combined_manual.md со структурой:
# Техническая документация по мерам
## Продажи
### Мера: [Кол-во]
[полное описание]
### Мера: [Рейтинг Потенциал]
[полное описание]
## Транспорт
### Мера: [Кол-во рейсов]
[полное описание]
Шаг 7: END — завершение и отчёт
Отчёт о выполнении:
{
"input_mrs": [ # ИСХОДНЫЙ список, неизменный
{"topic": "Продажи", "measures": ["Кол-во", "Рейтинг Потенциал"]},
{"topic": "Транспорт", "measures": ["Кол-во рейсов"]}
],
"processed_mrs": [ # Успешно обработанные
{"topic": "Продажи", "measures": ["Кол-во", "Рейтинг Потенциал"]},
{"topic": "Транспорт", "measures": ["Кол-во рейсов"]}
],
"missed_mrs": [], # Пропущенные (пусто, если всё успешно)
"created_docs": [
"/path/to/output/Продажи_Кол-во.md",
"/path/to/output/Продажи_Рейтинг_Потенциал.md",
"/path/to/output/Транспорт_Кол-во_рейсов.md"
],
"execution_time": "625 секунд (~10 минут)"
}
Преимущества архитектуры
Аспект |
Реализация |
Комментарий |
|---|---|---|
Гранулярность |
Одна мера за итерацию |
Детальное логирование каждого шага |
Отказоустойчивость |
Таймауты + missed_mrs |
Продолжение работы при ошибках |
Масштабируемость |
Циклический граф |
Обработка 10, 100, 1000 мер без изменения кода |
Прозрачность |
LangGraph Studio |
Визуализация в реальном времени |
Гибкость |
LLM + regex фалбэк |
Работа даже при недоступности AI |
Спасибо за внимание!