Задача звучит просто. Пусть LLM-агент пишет рабочие Python-скрипты под KOMPAS-3D. Открывает сборки, обходит состав, ставит виды, заполняет штампы, собирает спецификации по ЕСКД. То, что инженеры сейчас делают руками или своими скриптами, накопленными за годы.

Первое, что выяснилось. Обычный агент почти всегда ошибается. Причём системно. Придумывает несуществующие функции. Путает две ветки API у KOMPAS (у него исторически сложились API5 и API7, они не смешиваются). Зовёт свойство как метод. Подставляет случайные числа туда, где допустимы только конкретные, например, в номер ячейки штампа.

Часть таких скриптов даже исполняется. Я долго пытался это чинить промптами и примерами. Не помогает. API большое, промахов много, каждый новый пример закрывает одну ошибку и открывает две новых. Стало понятно, что промпт-инжиниринг тут не работает. Нужна другая обвязка, такая, чтобы каждая ошибка ловилась отдельным механизмом до того, как код дойдёт до живого CAD.

Начнём с демо-видео, а затем перейдём к архитектуре.

Демо

Сценарий записан на реальной сборке из промышленной конструкторской документации. Задача: открыть 3D-документ 2025БК.100.10.00.000.a3d, обойти состав, собрать спецификацию .spw по действующей редакции ГОСТ 2.106 с разделами «Документация / Сборочные единицы / Детали / Стандартные изделия».

Демо: генерация спецификации по сборке

youtube(если основная не работает): https://youtu.be/vMrWjM8IuQo

Почему не MCP

Первое, что напрашивается для такой задачи, это обернуть API KOMPAS в набор MCP-инструментов: «открыть документ», «получить состав», «заполнить ячейку штампа», каждый со своей JSON-схемой параметров. Я от этого отказался, и вот почему.

Возьмём простой сценарий: обойти состав сборки и у каждой детали прочитать обозначение. На Python агент пишет это ровно так, как думает:

def run(app):
    d3 = cast(app.ActiveDocument, 'IKompasDocument3D')
    top = d3.TopPart
    result = []
    for i in range(top.Parts.Count):
        part = top.Parts.Item(i)
        result.append(part.Marking)
    return result

Семь строк, обычный цикл, привычная работа с объектами. Теперь то же самое через MCP: это цепочка отдельных tool call’ов. Сначала get_active_document, из ответа достать хэндл документа, скормить его в cast_to_3d, из ответа достать другой хэндл, вызвать get_top_part, потом get_parts_count, потом в цикле по одному вызову get_part_item и get_part_marking на каждой итерации. Каждый вызов это раунд-трип через модель, каждый ответ это JSON, который надо распарсить и передать в следующий вызов. Цикл на пять деталей превращается в десяток с лишним обменов.

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

Поэтому решение получилось обратным по духу к MCP: дать LLM писать полноценный код и построить вокруг этого кода слои, которые ловят каждый класс ошибок до того, как он дойдёт до живого CAD. Остальная статья про эти слои.

Слой 1. Граф API

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

Я собрал граф из шести файлов описания COM-типов KOMPAS. Каждый файл зафиксирован по контрольной сумме, чтобы отследить ситуацию, если у клиента установлена другая сборка. Всего в графе 47 тысяч узлов (типы, функции, значения перечислений) и около 60 тысяч связей. Помимо связи «функция X принадлежит типу Y», отдельно хранится ещё один вид связи: «на объекте типа X вызов Y возвращает Z». По этим стрелкам агент ищет пути между интерфейсами.

Когда я собрал первый вариант и попробовал построить путь от главного объекта приложения к 3D-детали, оказалось, что он недостижим. Дело было в устройстве COM у KOMPAS. Один и тот же реальный объект часто прикидывается несколькими интерфейсами: то одним, то другим. Эта связь не выражена в описаниях типов напрямую, её нужно доставать из отдельной COM-конструкции. Пока я её не извлёк, граф не знал, что объект типа Part7 умеет обращаться к методу интерфейса IModelObject, а обычный документ можно привести к его 3D-версии.

Пришлось написать отдельный скрипт, который находит эти COM-классы в файлах описания типов и складывает список интерфейсов, которые каждый класс реализует одновременно. Плюс отсеивать интерфейсы-обработчики событий (их нельзя привести обратно, это односторонняя стрелка).

Отдельная возня была с интерфейсами-хабами вроде базового IKompasAPIObject, который встречается почти в каждом COM-классе. Если пускать его в переходы, поиск пути превращается в кашу «из чего угодно во что угодно». Я поставил порог отсечения (по умолчанию 10 классов) и вынес в переменную окружения, чтобы можно было калибровать.

На этом графе работают две функции, которые агент дёргает постоянно.

Первая: поиск ближайшего валидного имени. Агент попросил ActiveDocument3D, получил ответ «такого нет, ближайшее — ActiveDocument». Обычно за одну итерацию восстанавливается.

Вторая: поиск пути. Обход графа по проверенным стрелкам. Для задачи «обойти состав сборки» агент вместо имени получает готовый шаблон шагов:

IApplication.ActiveDocument -> IKompasDocument
привести к IKompasDocument3D
IKompasDocument3D.TopPart -> IPart7
IPart7.Parts -> IParts
IParts.Item(i) -> IPart7

Этот шаблон склеен из проверенных связей и работает как заготовка для генерации.

Слой 2. Справочник валидных значений

Есть ошибки, которые не поймёт ни граф, ни компилятор. Метод заполнения ячейки штампа IStamp.Text(cell_id, text) принимает целое число. Формально допустимо любое. Реально валидны только конкретные номера ячеек по ГОСТ 2.104. Агент без подсказки подставляет что попало:

stamp.Text(777, "МЧ.100.10.00.000")  # 777 не существует

Код исполнится, ничего не упадёт, штамп останется пустым.

Я собрал курируемый справочник с константами KOMPAS: ячейки штампа, плоскости эскизов, типы объектов. Каждая запись хранит применимый паттерн (какой метод, какой аргумент) и таблицу разрешённых значений с русскими подписями («Обозначение документа», «Наименование изделия», «Материал», «Разраб.»). Компилятор дальше поднимает эту таблицу как правило и выдаёт предупреждение на литералах не из списка.

Слой 3. ГОСТ-база

С ЕСКД отдельная история. Даже когда агент угадал функции, он может сослаться в коде, комментариях или структуре спецификации на отменённый стандарт. Мне нужны были три вещи. Знать, какой стандарт сейчас действующий. Тихо мапить старое обозначение на актуальное, если пользователь по привычке назвал старую редакцию. И давать возможность процитировать раздел из локального текста стандарта, а не пересказывать по памяти.

Локальный корпус собирается в отдельный каталог. Внутри: инвентарь всех обработанных документов со статусами, каталоги действующих и отменённых, граф ссылок «ссылается на» и «заменяет», чанки текста для поиска. Резолвер по обозначению возвращает актуальный документ: либо тот же, если действующий, либо назначенную замену, либо практический аналог (несколько отменённых стандартов на крепёж вручную смапены на ISO-аналоги).

В проверке кода стоит правило: находим упоминания ГОСТ в исходнике и комментариях, если отменён, добавляем предупреждение. Не блокирует. Если инженер настаивает, оставит. Но в отчёт попадёт.

По текстам стандартов работает полнотекстовый поиск. Каждая цитата возвращается вместе с именем файла и его контрольной суммой на момент индексации. Это важно для норм-контроля: всегда видно, из какого документа взят фрагмент.

Слой 4. Компилятор-верификатор

Самый мощный слой стека. Прогоняю сгенерированный Python-скрипт через настоящий C#-компилятор.

Работает так. Транспилятор ходит по дереву Python-кода и переводит подмножество, которое агент реально пишет (присваивания, вызовы методов, приведения, if/else, return), в C#. Дальше C#-компилятор проверяет результат, опираясь на настоящие описания типов KOMPAS. Если конструкция транспилятором не поддержана, он возвращает None и пропускает такой скрипт. Это лучше, чем выдавать ложные ошибки на непокрытом синтаксисе.

Одна деталь. Агент часто пишет docs.Add(4, True) вместо docs.Add(SomeEnum(4), True). Транспилятор смотрит по графу, объявлен ли параметр как перечисление, и оборачивает литерал. Мелочь, но убирает пачку ложных ошибок.

Сборки описаний типов я делаю из лицензированных файлов напрямую, без вспомогательных утилит из платного SDK. На любой машине, где стоит KOMPAS, .NET Framework тоже есть, ничего доустанавливать не надо.

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

Пример 1. Галлюцинация метода

Агент придумал ActiveDocument3D. Такого поля у IApplication нет.

def run(app):
    return app.ActiveDocument3D.TopPart

Транспилируется в:

using KompasAPI7;
using Kompas6API5;
using Kompas6Constants;
using Kompas6Constants3D;
public class Probe {
    public static object Run(IApplication app) {
        return app.ActiveDocument3D.TopPart;
        return null;
    }
}

Что агент получает обратно от утилиты:

Your code fails STRUCTURAL type-checking against the real KOMPAS type library
(compiled with the C# compiler against Interop.KompasAPI7.dll).
Fix these before it can run. KOMPAS separates API5 and API7 — do not mix them.
  - CS1061: member does not exist on that interface
            (wrong member or wrong API5/API7 interface)
            (symbol: ActiveDocument3D)
      did you mean: ActiveDocument, Documents, ApplicationName, Application
Rewrite `def run(app)` (Python, pywin32) using only members that exist.
Output ONLY the ```python code block.

Diag CS1061 — от настоящего csc. Строка did you mean — из графа, nearest_members по владельцу IApplication.

Пример 2. Приведение через границу API7 ↔ API5

Агент попытался привести app.ActiveDocument (это IKompasDocument из библиотеки KompasAPI7) к ksDocumentParam из старой библиотеки Kompas6API5. Формально оба типа существуют. Реально win32com.CastTo через границу двух разных typelib работает плохо и без известной COM-связи между интерфейсами почти гарантированно упадёт в runtime с E_NOINTERFACE.

def run(app):
    doc = app.ActiveDocument
    x = cast(doc, 'ksDocumentParam')
    return x

Транспилируется в:

public class Probe {
    public static object Run(IApplication app) {
        var doc = app.ActiveDocument;
        var x = ((ksDocumentParam)(doc));
        return x;
        return null;
    }
}

C# такой каст спокойно компилирует, ошибок нет. Но guard добавляет свои предупреждения:

Static verifier warnings from checks the compiler cannot prove:
These are not hard structural errors; verify live behavior with the runner
when the code otherwise type-checks.
  - CAST_TYPELIB_BOUNDARY: cast crosses a typelib boundary
                          (API7 ↔ API5/2D5/3D5/Constants);
                          win32com CastTo may not bridge that reliably
                          (symbol: IKompasDocument)
  - QI_UNVERIFIED: cast target is not backed by a known coclass
                   co-implementation; runtime may raise E_NOINTERFACE
                   (symbol: IKompasDocument)

Это именно те два класса ошибок, ради которых я тащил из .tlb описания типов и список COM-классов. Компилятор про такое не знает: у него типы есть, каст между object и любым интерфейсом легален. Знание, что в живой COM это не заработает, лежит в отдельном файле cast_groups.jsonl, извлечённом из тех же .tlb.

Правильный вариант, который проходит без единой диагностики:

def run(app):
    d3 = cast(app.ActiveDocument, 'IKompasDocument3D')
    return d3.TopPart

Пример 3. Штамп с несуществующим номером ячейки

Агент решил вписать обозначение в штамп. Правильный номер ячейки «Обозначение документа» по ГОСТ 2.104 — 1. Агент подставил 777.

def run(app):
    doc = cast(app.ActiveDocument, 'IKompasDocument2D')
    layout = doc.LayoutSheets.ItemByNumber(1)
    stamp = layout.Stamp
    stamp.Text(777, 'МЧ.100.10.00.000')
    return True

Здесь одновременно срабатывает два разных слоя. Guard понимает, что Text у IStamp — это параметризованное свойство (принимает cell_id и возвращает объект текста), а не метод для записи. Транспилятор переводит его в C# как get_Text:

public class Probe {
    public static object Run(IApplication app) {
        var doc = ((IKompasDocument2D)(app.ActiveDocument));
        var layout = doc.LayoutSheets.get_ItemByNumber(1);
        var stamp = layout.Stamp;
        stamp.get_Text(777, "МЧ.100.10.00.000");
        return true;
        return null;
    }
}

Обратная связь:

Your code fails STRUCTURAL type-checking against the real KOMPAS type library.
  - CS1501: wrong number of arguments for this member  (symbol: get_Text)
      did you mean: Text, IText, ModelText, Vertex, ModelTexts

Static verifier warnings from checks the compiler cannot prove:
  - APP_CONST_UNKNOWN: literal application-domain constant is not
                      in the curated KOMPAS app KB
                      (symbol: KompasStampCellEnum)

CS1501 от компилятора: Text принимает один аргумент (номер ячейки), а не два. APP_CONST_UNKNOWN от справочника валидных значений: 777 не входит в список номеров ячеек штампа. Первое агент чинит правкой формы вызова, второе — заменой литерала на конкретный designation (1), name (2) и так далее из подсказанного списка.

Слой 5. Live runner

Всё, что можно поймать статикой, до живого CAD доходить не должно. Всё, что нельзя, доходит. Этим занимается отдельный сервис.

Он стоит на Windows-машине с установленным KOMPAS. Минимальный набор операций: подготовить, запустить, сбросить, посмотреть, что внутри. Один рабочий процесс. Задачи сериализованы. Никакой параллельной работы с CAD.

Важное решение: скрипт исполняется не в основном сервисе, а в отдельном процессе, который стартует под каждый запуск. Основной сервис никогда сам не выполняет код агента и не держит открытые COM-объекты. Если что-то повесится или упадёт с ошибкой доступа к памяти, падает только дочерний процесс. Таймаут реализован как сторожевой цикл над этим процессом:

child = popen_hidden(cmd, ..., stdout=PIPE, stderr=PIPE, stdin=DEVNULL)
deadline = time.monotonic() + max(1, int(timeout_s))
while child.poll() is None and time.monotonic() < deadline:
    time.sleep(0.05)
if child.poll() is None:
    child.kill()
    return {"ran": False, "timeout": True, ...}

Внутри дочернего процесса ограниченный набор встроенных функций, единый вход def run(app), стандартная обёртка над KOMPAS-приложением. На выходе стандартный результат с полями: выполнился ли, что вернул, тип исключения, код ошибки COM, хвост стека, был ли таймаут. На этом формате работает следующий слой.

Классификатор ошибок исполнения

Есть ошибки, которые формально правильные, а по сути бесполезные. Код исполнился, а чек задачи не сошёлся. Активный документ оказался обычным IKompasDocument, а нужен был 3D. Свойство позвали как метод.

Классификатор превращает сырой результат работы runner в структурированную классификацию и подсказку агенту. Внутри набор регэксп-правил по текстам исключений и кодам COM-ошибок. Каждое правило даёт стабильный тип ошибки и подсказку.

Пример. В тексте ошибки встретился IKompasDocument и один из TopPart, Part, GetPart, AttributeError. Тип: «нужно привести к 3D-документу». Подсказка агенту: d3 = cast(app.ActiveDocument, 'IKompasDocument3D'); part = d3.TopPart.

Отдельный интересный случай: расхождение между базой и реально установленной сборкой. Если исполнение упало «у интерфейса нет такого поля» на типизированной обёртке, возможно, поле было в описании, но в установленной сборке KOMPAS его нет. Классификатор в этом случае обращается к runner и получает настоящий список методов из живой библиотеки типов.

Как это работает вместе

Один шаг агента выглядит так.

  1. Запрос контекста. Граф даёт релевантные интерфейсы, готовые шаблоны путей, релевантные значения из справочника, привязку к текущей редакции ГОСТ.

  2. Агент пишет def run(app): ....

  3. Проверка. Компилятор, справочник валидных значений, актуальность ГОСТ. На выходе результат, ошибки, предупреждения.

  4. Агент правит по фидбеку. Обычно одна итерация на структурные ошибки.

  5. Runner запускает код на живом CAD.

  6. Классификатор. Если не отработало, разбор ошибки превращается в инструкцию по починке.

  7. Ещё одна итерация правки. Финальный отчёт.

Как попробовать

KOMPAS-GUARD собран как готовый бинарный пакет под Windows x64 и ставится обычной командой pip install kompas-3d-guard. После установки доступны консольная команда kompas-guard и библиотека для Python kompas_guard, а kompas-guard up поднимает локальные службы: сервис контекста и исполнитель кода на живом CAD. Для работы на живом CAD нужен установленный КОМПАС-3D с зарегистрированным COM API (проверено на v24), всё остальное, включая скомпилированную среду исполнения и данные по API и ГОСТ, уже внутри пакета. Полная инструкция, описание навыка для агента и контрольная сумма сборки лежат в репозитории: github.com/dwnmf/kompas-3d-guard-bin

Сам подход не завязан на KOMPAS-3D: те же слои (граф API, справочник значений, проверка кода до запуска, разбор ошибок исполнения) ложатся на любой CAD с программным доступом. Если у вас есть похожие задачи, в том числе под SolidWorks, AutoCAD или другую систему, напишите мне на почту hello@kompasmcp.ru, можем разобрать ваши случаи

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


  1. proxy3d
    02.07.2026 08:03

    Вчера только попалось видео про два свежих исследования. Это как раз касается вашей темы. В исследованиях ставился вопрос, являются ли рассуждения LLM случайными блужданиями или нет. Суть сводилась к тому, что если ли разница между генерацией правильного и неправильного рассуждения. Проверка была на медицинских данных, так как там важно чтобы разные симптомы были объединены в правильные связи и связаны с правильными лекарствами. В случае LLM реальные и вымышленные связи дают одинаковый коэффициент правдоподобия, поэтому они равнозначные - в то время как у людей он отличается. Авторы этого и другого исследования приходят к выводу, что рассуждения являются "случайными блужданиями" созданными на основе статистических данных.

    Я к тому, что единственный вариант для точной проверки (к которому вы сами и пришли) - это наличие промежуточного элемента, который проверяет хотя бы валидацию кода или связей, компилируется он или нет. Собственно так обучают LLM для кодовой базы (синтетический датасет), через генерацию множества кода различных задач и затем пропускание его через компилятор. Если проходит, то можно на этом коде обучать иначе нет.

    Тут вряд ли играет роль MCP или без него, так как в вашем случае проблема та же, что и со связями симптомов в исследованиях выше. MCP это просто же микросервис с оберткой понятной для LLM. Он нужен в архитектуре только там, где обычно нужен микросервис. В вашем случае, я не понял зачем изначально MCP был там.


    1. lya_ocean Автор
      02.07.2026 08:03

      Так в том и суть, что эта MCP обёртка всё и портит, она заставляет модель работать через JSON-схему вместо кода(и это приводит к тому, что она гораздо чаще ошибается). А начинал я с MCP просто потому, что это самый быстрый способ подцепить готового агента типа Claude или Cursor к КОМПАСу, вот на нём эти грабли и вылезли.


      1. proxy3d
        02.07.2026 08:03

        Если хочется вызовы, то есть же UTCP как MCP без обертки а сразу обращение к API, но их сложнее контролировать. Те же Skill, но они сыроваты и есть проблемы расширяемости, так как много много скиллсов просто приводят к тому, что они игнорируются.

        У меня MCP одна из проблем, что забивает контекст огромными JSON-схемам и модель теряет детали. Но она уже решена (например, паттерн Code Mode от Cloudflare или Port of Context). MCP-сервер отдает модели вместо тысячи эндпоинтов, всего два метода: search() и execute(). Короче экономия порядка 99% токенов и контекст не теряется так.

        Пробовали UTCP вместо MCP в данном случае (если очень хочется связать с Claude или Cursor)?

        Может быть лучше внутри использовать RLM. Это аналог LangChain но умеющий работать с большим контекстом (за счет памяти), H-MEM память и так далее. Так как в вашем случае, контекст с деталями может быть важен.


        1. lya_ocean Автор
          02.07.2026 08:03

          Про UTCP не слышал, спасибо, гляну. Зашёл посмотреть, а у них самих уже есть Code Mode, многошаговые сценарии одним скриптом вместо десятков вызовов, похоже все туда сходятся. А search()/execute() из Cloudflare'овского паттерна у меня по факту так и работает, модель запрашивает срез из графа и исполняет проверенный скрипт. RLM кстати выглядит здорово, надо будет попробовать


  1. vitalik_zhukov
    02.07.2026 08:03

    Очень сильный пост, видно что это уже не просто LLM-эксперимент, а полноценная инженерная система. Классно, что вы не боретесь с ошибками промптами, а закрываете их слоями граф API, справочники, ГОСТ и компиляция. Отдельно зашёл live runner, именно он делает систему по-настоящему надёжной, а не почти работающей.