Секрет воспроизводимой и переносимой оптимизации: промежуточное представление (IR) ORPilot.
Почему агенту оптимизации ИИ на производственном уровне необходимы воспроизводимость и переносимость, и как информационное распространение помогает их достичь.
Делиться
В своем предыдущем посте я подробно рассмотрел четыре ключевых нововведения ORPilot, которые делают его ориентированным на производство инструментом с открытым исходным кодом для LLM-анализа операций, а именно: агент интервью, агент сбора данных, агент вычисления параметров и промежуточное представление (IR). Среди этих четырех нововведений IR является наиболее важным, отличающим ORPilot от академического прототипа и наделяющим его потенциалом стать инструментом производственного уровня, поскольку он решает две наиболее важные для производственной среды задачи: воспроизводимость и переносимость. В этом посте я подробно расскажу о структуре IR в ORPilot.
Что такое ИК-излучение?
Существует проблема, о которой почти никто не говорит при обсуждении моделей оптимизации, созданных с помощью ИИ: что происходит после первого решения?
Вы запускаете свою модель. Получаете оптимальное решение. А затем, три недели спустя, вам нужно повторно запустить её с обновлёнными данными о спросе. Или вашему коллеге на другом компьютере нужно воспроизвести результат. Или ваша компания решает перейти с Gurobi на решатель с открытым исходным кодом из-за стоимости лицензирования. Или вы хотите спросить: «Что, если мы увеличим мощность предприятия на 20%?» С большинством существующих инструментов LLM-for-OR ответ на все эти вопросы один и тот же: вам нужно начать всё сначала, снова вызвать LLM, снова оплатить стоимость API, снова сгенерировать код решателя и надеяться получить ту же структуру модели. Однако агент оптимизации на основе искусственного интеллекта с открытым исходным кодом ORPilot предлагает альтернативное решение этой проблемы: промежуточное представление (IR).
IR — это типизированная JSON-схема, не зависящая от конкретного решателя, которая описывает полную математическую структуру оптимизационной модели. Не код оптимизации, а саму модель, выраженную в форме, независимой от какого-либо конкретного решателя.
Структура ИК-панели ORPilot состоит из пяти основных разделов.
(1) Множества: именованные коллекции сущностей, такие как Работники, Задачи, Заводы, Периоды. Каждое множество знает, откуда берутся его члены: из CSV-файла, скалярного количества или жестко заданного списка.
(2) Параметры: индексированные числовые данные из CSV-файлов, каждый из которых связан со своим доменом (который задает ему индекс) и с точными именами столбцов, необходимыми для его загрузки.
(3) Переменные: переменные решения с указанием типа (непрерывный, бинарный, целочисленный), области определения, границ и структурных флагов.
(4) Цель: дерево символических выражений над переменными и параметрами — суммами, разностями, произведениями, индексированными суммами в нейтральной для решателя форме.
(5) Ограничения: именованные символические ограничения с областями определения, деревьями выражений и смыслом (<= или = или >=). Каждое ограничение представляет собой полный, самоописывающийся объект.
Давайте рассмотрим это на конкретном примере задачи распределения задач между работниками, приведенном ниже.
Пример задачи распределения задач между работниками
В этой задаче четырем работникам необходимо назначить четыре задачи, по одной задаче на каждого работника, по одному работнику на задачу. Каждая пара (работник, задача) имеет стоимость, указанную в CSV-файле. Мы стремимся минимизировать общую стоимость назначения. Это классическая задача о назначении, представляющая собой задачу целочисленного программирования.
Данные хранятся в двух файлах:
(1) sets.csv (все элементы множества в одном месте):
элемент set_name
рабочие w1
работники w2
рабочие w3
рабочие w4
задачи t1
задачи t2
задачи t3
задачи t4
(2) assignment_costs.csv (матрица затрат):
worker_id task_id cost
w1 t1 2.0
w1 t2 4.0
… … …
Вот полный информационный запрос по данной проблеме:
{ "problem_class": "AssignmentProblem", "model_type": "Mixed Integer Program", "sense": "minimize", "sets": { "Workers": { "size": null, "index_symbol": "w", "source": "sets.csv", "column": "element", "filter_column": "set_name", "filter_value": "workers", "ordered": false }, "Tasks": { "size": null, "index_symbol": "t", "source": "sets.csv", "column": "element", "filter_column": "set_name", "filter_value": "tasks", "ordered": false } }, "parameters": { "assignment_cost": { "domain": ["Workers", "Tasks"], "type": "float", "source": "assignment_costs.csv", "column": "cost", "index_columns": ["worker_id", "task_id"], "missing_default": "inf" } }, "variables": { "assign": { "description": "1 if worker w is assigned to task t, 0 otherwise", "label": "assignments", "domain": ["Workers", "Tasks"], "type": "binary", "lower_bound": 0, "upper_bound": 1, "upper_bound_set": null, "exclude_diagonal": false, "domain_filter": null } }, "constraints": { "one_task_per_worker": { "domain": ["Workers"], "expression": { "operation": "indexed_sum", "over": ["Tasks:t"], "body": {"type": "variable", "name": "assign", "indices": ["w", "t"]} }, "sense": "=", "rhs": {"type": "constant", "value": 1} }, "one_worker_per_task": { "domain": ["Tasks"], "expression": { "operation": "indexed_sum", "over": ["Workers:w"], "body": {"type": "variable", "name": "assign", "indices": ["w", "t"]} }, "sense": "=", "rhs": {"type": "constant", "value": 1} } }, "objective": { "sense": "minimize", "expression": { "operation": "indexed_sum", "over": ["Workers:w", "Tasks:t"], "body": { "operation": "multiply", "left": {"type": "parameter", "name": "assignment_cost", "indices": ["w", "t"]}, "right": {"type": "variable", "name": "assign", "indices": ["w", "t"]} } } } }
Давайте разберем, для чего предназначен каждый раздел и почему были приняты те или иные проектные решения.
Наборы
Поле «sets» указывает, откуда берутся элементы множества. Наиболее важным проектным решением в поле «sets» является соглашение об источнике данных. ORPilot требует, чтобы все элементы множества находились в одном файле sets.csv, используя двухколоночный формат: «set_name» и «element». Каждое множество — сущности (работники, задачи, заводы) и временные множества (периоды, месяцы) — представляет собой отфильтрованный фрагмент этого файла. В данной задаче поле «Workers» означает: загрузить элементы из sets.csv, прочитать столбец «element», оставить только строки, где столбец «set_name» равен «workers». Результатом компиляции будет Workers = [“w1”, “w2”, “w3”, “w4”].
Эта конвенция имеет два преимущества. Во-первых, все основные данные находятся в одном месте. Добавление работника означает добавление строки в sets.csv, а не изменение нескольких файлов. Во-вторых, поле «filter_value» проверяется на соответствие фактическим уникальным значениям в sets.csv во время генерации IR, что позволяет выявлять опечатки до того, как код решателя создаст пустые наборы. Поле «index_symbol» («w» для Workers, «t» для Tasks) — это имя переменной цикла, которое будет отображаться в скомпилированном коде решателя, например, «for w in Workers, for t in Tasks». Его необходимо выбрать, чтобы избежать конфликтов символов между вложенными циклами (см. правило теневого отображения ниже). Поле «ordered» здесь имеет значение false для обоих наборов, но оно становится критически важным для моделей с временной индексацией. Упорядоченный набор поддерживает ссылки на временные задержки, например, ссылку на inventory[t-1] внутри ограничения периода t.
Параметры
Поле «параметры» связывает данные с моделью. Параметр «assignment_cost» имеет шесть структурных полей.
(1) “domain”: [“Workers”, “Tasks”] — этот параметр индексируется обоими наборами, создавая двумерную таблицу.
(2) “type”: “float” — тип данных этого параметра — float.
(3) “источник”: “assignment_costs.csv” — точное имя файла (с расширением), содержащего данные.
(4) «столбец»: «стоимость» — столбец CSV, содержащий числовые значения для загрузки.
(5) “index_columns”: [“worker_id”, “task_id”] — столбцы CSV, которые служат ключами, в том же порядке, что и “domain”. Поле “index_columns” является одним из наиболее важных элементов IR. Без него компилятор не может определить, какие столбцы в CSV соответствуют каким наборам доменов. Исторически сложилось так, что распространенной причиной сбоя было угадывание компилятором неправильного имени ключевого столбца и молчаливая загрузка неправильных данных. IR требует, чтобы правильные имена столбцов всегда указывались явно.
(6) “missing_default”: “inf” — указывает компилятору, что любая пара (рабочий процесс, задача), отсутствующая в CSV, должна рассматриваться как имеющая бесконечную стоимость, то есть маршрут недоступен. Это правильная семантика для параметров стоимости и штрафа.
Переменные
Поле «переменные» определяет решения, которые необходимо принять в модели оптимизации. Переменная «присваивать» является бинарной и индексируется по «области определения»: [«Рабочие», «Задачи»]. Таким образом, во время компиляции компилятор выполняет сборку (при условии использования решателя PuLP):
assign = {(w, t): pulp.LpVariable(f"assign_{w}_{t}", cat="Binary") for w in Workers for t in Tasks}
Некоторые ключевые структурные флаги, которые здесь не используются, но заслуживают внимания, — это «exclude_diagonal», «domain_filter» и «upper_bound_set».
Для переменных, индексируемых в одном и том же множестве дважды, например, «arc[Location, Location]» в модели маршрутизации, установка параметра «exclude_diagonal=true» указывает компилятору пропустить диагональ (i, i). Ни одно местоположение не перемещается само в себя. Компилятор генерирует исключение.
if l1 == l2: continue
используется метод `.get(key, 0)` для всех обращений, поэтому отсутствие ключей никогда не приводит к ошибке `KeyError`.
Когда таблица затрат содержит меньше строк, чем полное декартово произведение множеств ее доменов (например, в CSV-файле существуют только допустимые маршруты), установка параметра «domain_filter» в значение, равное его имени, ограничивает переменную только этими комбинациями. Компилятор генерирует генератор с оператором «if (i, j) in transport_cost», поэтому несуществующие маршруты никогда не создаются в качестве переменных.
Для целочисленных переменных, естественной верхней границей которых является мощность множества (например, переменные положения MTZ в алгоритме исключения подтуров), установка параметра “upper_bound_set”=”Customers” приводит к тому, что компилятор выдает “len(Customers)” в качестве верхней границы, сохраняя независимость модели от данных даже при изменении размера множества между запусками.
Ограничения
Раздел «Ограничения» содержит деревья выражений, описывающие ограничения, определенные для этой модели. Именно здесь IR наиболее резко отличается от файла кода. Ограничения хранятся не в виде строк или кода, а в виде деревьев выражений. Каждое ограничение имеет: (1) «область определения»: множества, по которым компилятор будет проходить циклом для генерации одного экземпляра ограничения для каждой комбинации. Например, «область определения»: [«Рабочие»] означает одно ограничение на каждого рабочего. (2) «выражение»: левая часть, представляющая собой рекурсивное дерево узлов. (3) значение: знак для этого ограничения, «=», «<=» или «>=». (4) «правая часть»: правая часть, также дерево выражений (но содержащее только константы и параметры, никогда не переменные, которые должны быть перемещены в левую часть). Давайте внимательно рассмотрим ограничение «one_task_per_worker».
"one_task_per_worker": { "domain": ["Workers"], "expression": { "operation": "indexed_sum", "over": ["Tasks:t"], "body": {"type": "variable", "name": "assign", "indices": ["w", "t"]} }, "sense": "=", "rhs": {"type": "constant", "value": 1} },
В узле «выражение» выше поле «over» использует псевдоним «Tasks:t», чтобы явно указать имя переменной цикла «t» для этой внутренней суммы. Это необходимо, потому что «t» уже является index_symbol множества Tasks, и когда область определения внешнего ограничения не включает Tasks, компилятор не будет иметь «t» в области видимости, но псевдоним заставляет его существовать внутри суммы. Всякий раз, когда множество в «over» уже встречается в области определения ограничения (с тем же index_symbol), используйте псевдоним, чтобы избежать перекрытия внешней переменной цикла. В противном случае внутренняя «t» будет перекрывать внешнюю «t», и сумма всегда будет вычислять assign[t, t] (диагональ самоцикла), а не предполагаемую сумму.
Цель
В рамках информационного исследования цель формулируется следующим образом.
"objective": { "sense": "minimize", "expression": { "operation": "indexed_sum", "over": ["Workers:w", "Tasks:t"], "body": { "operation": "multiply", "left": {"type": "parameter", "name": "assignment_cost", "indices": ["w", "t"]}, "right": {"type": "variable", "name": "assign", "indices": ["w", "t"]} } } }
Внешний цикл «indexed_sum» одновременно перебирает как рабочих, так и задачи, используя псевдонимы «Workers:w» и «Tasks:t» для явного указания имен переменных цикла. Тело цикла представляет собой узел умножения, параметр × переменная, что является единственной формой умножения, допускаемой IR в линейной модели. В результате получается один член для каждой пары (рабочий, задача), суммируемый в общую стоимость.
Это простейшая форма целевой функции: одна индексированная сумма. Более сложные целевые функции объединяют несколько индексированных сумм с помощью вычитания. Допустим, модель имеет как стоимость назначения, так и бонус за определенные назначения: максимизировать сумму (бонус[w,t] × назначение[w,t]) – сумму (стоимость[w,t] × назначение[w,t]). Это будет закодировано следующим образом:
вычесть(
indexed_sum(over Workers,Tasks: bonus[w,t] × assign[w,t]),
indexed_sum(over Workers,Tasks: cost[w,t] × assign[w,t])
)
Одно важное правило вычитания: никогда не вставляйте операцию вычитания справа от другой операции вычитания. Поскольку вычитание — это бинарная операция (левое минус правое), размещение другой операции вычитания справа меняет знак внутреннего члена на противоположный.
вычесть(A, вычесть(B, C))
= A – (B – C)
= A – B + C ← C должно было быть вычтено, но в итоге получается ДОБАВЛЕНО
Допустим, целевая сумма — это выручка — стоимость доставки — стоимость хранения. Распространённая ошибка в системах управления жизненным циклом продукции заключается в том, что они иногда группируют эти две статьи затрат вместе справа:
вычесть(доход, вычесть(стоимость_доставки, стоимость_удержания))
= выручка – (стоимость_доставки – стоимость_хранения)
= выручка – стоимость доставки + стоимость хранения
Это неправильно, поскольку затраты на хранение превращаются в доход. Модель по-прежнему работает, и решатель по-прежнему возвращает «оптимальное» значение, но значение целевой функции остается неизменным.
Неверно, завышено в 2 раза по показателю holding_cost. Правильная форма — плоская цепочка слева направо:
вычесть(вычесть(доход, стоимость доставки), стоимость хранения)
= (доход – стоимость доставки) – стоимость хранения
= выручка – стоимость доставки – стоимость хранения
ORPilot имеет валидатор семантики IR, который выявляет шаблон вложенности справа до компиляции и указывает конкретный термин, знак которого был изменен, чтобы LLM мог исправить порядок цепочки.
От ИК-спектроскопии к коду решателя
Компилятор IR — это детерминированное программное обеспечение, не использующее LLM. При наличии одного и того же файла ir.json и одних и тех же CSV-файлов данных он всегда генерирует идентичный код решателя. Всегда. В настоящее время компилятор поддерживает пять бэкендов: PuLP, Pyomo, OR-Tools, Gurobi и CPLEX. Переключение бэкендов не требует изменения модели. IR остаётся тем же; меняется только целевая платформа компиляции. Это означает, что вы можете архивировать ir.json вместе с вашими данными и точно воспроизвести любой прошлый результат, не выполняя ни одного вызова API. Вы можете переключиться с Gurobi на PuLP, выполнив команду: orpilot compile-ir output/ir.json --solver pulp --run . Одна команда, ни одного вызова LLM, та же структура модели. Вы можете запустить проверку CI/CD на выходных данных решателя, зафиксировав ir.json и запустив компилятор в вашем конвейере. Вы можете поделиться файлом ir.json с коллегой на другом компьютере, и он сможет решить ту же модель, не нуждаясь в вашем API-ключе LLM и даже не понимая задачу с нуля.
Конвейер компиляции ИК-спектров
После проверки файла ir.json, ORPilot предлагает облегченный конвейер компиляции: ir.json + CSV-данные → Компилятор IR → Код решателя → Выполнение кода. Этот конвейер не включает в себя ни одного вызова LLM от начала до конца. Он быстрый, недорогой и полностью детерминированный. Единственный вызов LLM во всем рабочем процессе — это тот, который изначально создал файл ir.json. Команда CLI: orpilot compile-ir output/ir.json –run. Она компилирует IR, выполняет модель и генерирует отчет о решении. Для переключения решателей: orpilot compile-ir output/ir.json –solver pyomo –run.
Семантический валидатор информационного поиска
Перед сохранением и компиляцией IR-файла ORPilot запускает семантический валидатор, который выявляет ошибки моделирования, представляющие собой структурно корректный JSON, но математически некорректные данные. В настоящее время валидатор выявляет три основные категории ошибок, которые являются распространенными сбоями LLMS во время экспериментов.
1. Ошибки знака в балансе запасов. Она обнаруживает случаи, когда все переменные потока в ограничении баланса оказываются на одной стороне (например, inv = приток + отток вместо inv = приток – отток). Правильное тождество: ending_inv = beginning_inv + inflow – outflow. Нарушения этого тождества приводят к моделям, которые либо нежизнеспособны (случай избыточного ограничения), либо неограниченны (случай недостаточного ограничения), и ошибку знака практически невозможно обнаружить в скомпилированном коде.
2. Отсутствие начального ограничения. Если существует ограничение баланса с временной задержкой, валидатор требует соответствующий вариант «_init», представляющий ограничение в начальный период времени. Отсутствие начального ограничения может оставить первый период без ограничений, что приведет к созданию неограниченной модели, даже если ограничение для последующего периода корректно.
3. Вложенное вычитание в целевой функции. Иногда разработчик IR LLM пишет subtract(A, subtract(B, C)), имея в виду последовательное вычитание затрат B и C из дохода A. Однако математически это выражение вычисляется как A – (B – C) = A – B + C, меняя знак C с затрат на доход. Модель по-прежнему решает задачу в «оптимальном» режиме, но значение целевой функции завышено в 2 раза по сравнению с C. Валидатор обнаруживает вложенность справа и указывает на затронутый термин, чтобы LLM мог переписать целевую функцию в виде простой цепочки слева направо.
При сбое проверки конкретное сообщение об ошибке передается обратно в LLM в виде целевого запроса на повторную попытку. LLM не видит сообщение «недействительный IR», но видит сообщение типа «ошибка знака баланса запасов: переменная разгрузка, по-видимому, отрицательная (коэффициент -1), но должна быть вычтена из притока, а не добавлена к нему».
Почему информационные запросы важны для анализа «что если»
Воспроизводимость и переносимость IR-модели естественным образом расширяются: находит применение систематический анализ сценариев «что если». После решения модели и сохранения её IR-модели, бизнес-пользователь обычно хочет изучить, как оптимальное решение изменяется при различных предположениях. Что если спрос увеличится на 20% в третьем квартале? Что если стоимость сырья вырастет до 15 долларов за единицу? Что если мы добавим ограничение, согласно которому ни один поставщик не должен составлять более 40% от общего объема закупок? Структура IR-модели позволяет тривиально и недорого обрабатывать две категории запросов «что если». Первая категория — это изменения данных. Если вопрос изменяет только значения параметров (оставляя структуру модели неизменной), вам нужно только обновить CSV-файлы. JSON-файл IR-модели остаётся неизменным. Запустите компилятор.
сопоставьте с новыми данными и выполните повторное решение. Это операция без вызовов LLM. Таким образом можно запустить сотни сценариев без затрат на API.
Вторая категория — структурные изменения. Если вопрос изменяет ограничение, добавляет новое или меняет цель, вы редактируете JSON-файл IR напрямую. Поскольку IR представляет собой типизированный документ, прошедший проверку схемы, с четко определенным деревом выражений, такие изменения локализованы. Добавление ограничения сводится к добавлению нового объекта ограничения, а не к поиску по сотням строк.
код, специфичный для решателя, пытается найти, где внести изменения.
Это качественно иные отношения с вашей оптимизационной моделью, чем те, которые предлагает любой другой существующий инструмент. Вместо одноразового артефакта вы получаете живую, редактируемую структуру модели, которую вы можете анализировать и изменять независимо от LLM.
Более широкая картина
IR затрагивает фундаментальный аспект взаимосвязи между ИИ и производственным программным обеспечением: результаты работы ИИ должны быть проверяемыми, переносимыми и надежными. Файл кода решателя, сгенерированный LLM, представляет собой непрозрачный объект. Если что-то не так, вам потребуется LLM для исправления. Если вы хотите что-то изменить, вы либо достаточно хорошо понимаете синтаксис API решателя, чтобы отредактировать его самостоятельно, либо снова вызываете LLM. Модель существует только в виде кода. IR отделяет интеллектуальное моделирование (для которого требуется LLM) от вычислительного этапа (для которого LLM не требуется). Задача LLM — создать чистый, структурированный JSON-артефакт. После того, как этот артефакт создан и проверен, он принадлежит вам, а не LLM. Именно это проектное решение, больше чем что-либо еще в ORPilot, делает его подходящим для производственного развертывания, а не для академической демонстрации.
- GitHub
- Бумага
Гуангруй Се: посмотреть все Гуангруй Се
Источник: towardsdatascience.com
Оцените материал:
Похожие записи
У ChatGPT — СДВГ, у Grok — тревожность, а у Gemini — аутизм: учёные поставили диагнозы самым популярным нейронкам.
16.12.2025
Курсы по промптингу? Почитайте детям сказку на ночь
13.06.2026
Xebia: О создании информационной базы для агентов искусственного интеллекта и последующем ускорении этого процесса.
11.06.2026Присоединяйтесь и подпишитесь на рассылку самых свежих новостей по Email
Получайте свежие новости и идеи на почту. Без спама — только самое интересное.
Нажимая «Подписаться», вы соглашаетесь с политикой конфиденциальности.
