Глава 8: Процессы и компилируемые инструкции
Новые идеи в этой главе
- Режимы композиции Инструкций: Мы определяем три способа объединения Инструкций: неявный (влияние через контекст), слияние (объединение в один вызов LLM на этапе компиляции) и последовательный (конвейеры для зависимых шагов).
- Компилируемые и разделенные конвейеры: Сложные рабочие процессы не выполняются «на лету». Они проектируются как конвейеры, которые компилируются и разделяются на части, учитывающие контекст (LLM или Сервер). В результате создаются надежные, эффективные и самоописываемые
Vibe
Процессов. - Адаптер
LaunchProcess
: Мы вводим универсальнуюИнструкцию
, которая работает как макрос. Она предоставляет единый интерфейс дляVessel
, чтобы запускать любойПроцесс
с состоянием, отделяя легковесного агента от сложного и длительного рабочего процесса. - Контекст через
references
: Чтобы управлять размером контекста LLM, шагиПроцесса
явно объявляют свои зависимости от данных через мета-свойствоreferences
. Движок рабочего процесса использует это, чтобы передавать в LLM только нужные данные из состояния процесса. Это обеспечивает эффективность и масштабируемость.
Эта глава описывает мощную архитектуру для определения и выполнения сложных, многошаговых рабочих процессов, которые органично сочетают когнитивные способности LLM с надежными вычислениями на стороне сервера. Главная задача — создать гибкую, модульную и очень эффективную систему. В основном это достигается за счет минимизации переключений контекста и умной обработки задач с состоянием, длительных и требующих больших данных, таких как пакетная обработка и мультиплексирование.
Идея в том, чтобы уйти от простых, блокирующих «вызовов инструментов». Мы стремимся к модели, где задачи можно определять как динамические, компилируемые конвейеры. Они разумно разделяются для выполнения в наиболее подходящем контексте — либо как единый, объединенный запрос к LLM, либо как процесс с состоянием, управляемый сервером для более сложных задач.
Основа: Инструкции как составные инструменты
В основе системы лежит Инструкция: JSON Schema, которая служит инструментом или «мыслительным примитивом» для LLM. Однако, в отличие от традиционных инструментов LLM, это не просто триггеры для внешних действий. Это часть богатой экосистемы композиции с тремя различными режимами.
Три режима композиции инструкций
-
Неявная композиция: Инструкции могут существовать рядом и влиять на поведение друг друга через контекст. Например, инструкция «быть вежливым» естественным образом влияет на результат отдельной инструкции «ответить пользователю», хотя они и не связаны напрямую. Это позволяет достичь более тонкого и естественного поведения.
-
Композиция через слияние (макросы): Инструкция может служить «макросом», который оборачивает другую инструкцию или схему. Это операция на этапе компиляции, где схемы сливаются в единый, плоский шаблон для LLM. Обертка может добавлять свои собственные шаги (например, для планирования, анализа или оценки) между шагами вложенной инструкции. По соглашению, такие «мыслительные» шаги или метаданные начинаются с подчеркивания (
_
), чтобы отличать их от конечных свойств вывода. Это позволяет создавать сложные, управляемые мыслительные процессы, которые все равно выполняются как один эффективный вызов LLM. -
Последовательная композиция (конвейеры): Для задач, требующих четкой последовательности зависимых шагов, инструкции объединяются в конвейер. Концептуальную задачу, такую как назначение встречи, которую можно представить как
Назначить(НайтиСлот(ПолучитьДоступность(НайтиУчастников(p))))
, можно выразить яснее в виде конвейера:"Назначить встречу" | НайтиУчастников | ПолучитьДоступность | НайтиОбщийСлот | СоставитьПриглашение
Этот конвейер затем компилируется в единую схему с упорядоченными свойствами, такими как
step1_identify_participants
,step2_fetch_availability
и т.д. Важно, что последующие шаги могут ссылаться на результаты предыдущих, заставляя LLM следовать логической цепочке зависимостей.
Выполнение логики на стороне сервера
Ключевая задача — организовать работу, которая должна происходить вне непосредственного контекста LLM. Наша архитектура определяет для этого два различных подхода, которые компилятор выбирает в зависимости от сложности и требований задачи.
1. Прямые вызовы сервера (по принципу "выстрелил и забыл")
Для простых, неблокирующих побочных эффектов полный Vibe Процесса избыточен. Если единственное требование к инструкции на стороне сервера — это запустить одно автономное действие, результат которого не нужен немедленно, это можно обработать как Прямой вызов сервера.
- Пример использования: Идеально подходит для таких действий, как логирование, отправка уведомления или увеличение счетчика.
- Выполнение: LLM
Vessel
'а выполняет инструкцию, последним шагом которой является неблокирующий запрос к определенному действию на сервере. - Без Vibe Процесса: Сервер выполняет действие, и взаимодействие на этом заканчивается. Базовый движок рабочих процессов (например, Temporal) гарантирует надежное выполнение действия с повторными попытками в случае сбоя, но Vibe Процесса с состоянием не создается, поскольку конвейер не продолжается. Это легковесный и эффективный способ обработки простых побочных эффектов.
Алиса: «То есть для простого действия, вроде отправки лайка, система использует «Прямой вызов сервера», который работает по принципу "выстрелил и забыл"? Мой Vessel просто отправляет запрос и идет дальше?» Боб: «Именно. Это эффективно для простых побочных действий. Но для чего-то более сложного, например, бронирования авиабилета, где есть несколько шагов — найти рейсы, выбрать место, оплатить, подтвердить — нужен «Процесс с состоянием». Это Vibe Процесса, который действует как менеджер проекта: отслеживает каждый шаг и следит, чтобы ничего не потерялось, даже если приходится ждать ответа от внешнего сервиса».
2. Процессы с состоянием (управляемые рабочие процессы)
Для любой задачи, которая является длительной, требует сохранения состояния или включает блокирующие серверные действия, результаты которых нужны для следующих шагов, система использует Процесс с состоянием. Эту роль выполняет Vibe Процесса
.
Эти процессы выполняются в рамках отказоустойчивого движка рабочих процессов, такого как Temporal, что дает несколько ключевых преимуществ «из коробки»:
- Сохранность: Состояние процесса сохраняется даже после перезапуска или сбоя сервера.
- Надежность: Автоматически обрабатываются повторные попытки для неудачных серверных действий (активностей).
- Управление состоянием: Прозрачно сохраняются результаты каждого шага, поэтому промежуточные вычисления никогда не теряются.
Жизненный цикл процесса: проектирование, компиляция и выполнение
Путь от сложной идеи до исполняемого Процесса
— это не спонтанное событие, а структурированный жизненный цикл проектирования.
-
Проектирование и анализ: Новый
Процесс
проектируется как конвейер. На этом этапе конвейер компилируется. Компилятор анализирует весь граф зависимостей, чтобы определить схему выполнения.- Если конвейер сводится к одному, конечному, неблокирующему действию на сервере, он генерирует «Инструкцию для прямого вызова сервера».
- Во всех остальных случаях он разделяет конвейер на части, учитывающие контекст (LLM или Сервер) и создает точку входа для
Процесса
.
-
Генерация точки входа: (Для процессов с состоянием) Компилятор извлекает первую часть конвейера, полностью основанную на LLM. Она становится Инструкцией точки входа для
Процесса
— компактной, самодостаточной схемой, которую можно интегрировать прямо в набор инструментовVessel
. -
Запуск
Vessel
-ом и созданиеПроцесса
: LLMVessel
-а выполняетИнструкцию точки входа
. Самый последний шаг этой инструкции — это запрос на сервер для запуска полного рабочего процесса. Серверный движок рабочих процессов получает этот запрос и немедленно создает VibeПроцесса
. -
Выполнение под управлением
Процесса
: С этого момента существует VibeПроцесса
, который контролирует весь поток выполнения. Его первое действие — выполнить первый блокирующий вызов сервера (например, запрос к базе данных). Поскольку процесс является долговечным, он будет ждать результата столько, сколько потребуется. Когда серверное действие завершается, результат прозрачно передается обратно в работающий процесс. Процесс также может выполнять неблокирующие шаги по принципу "выстрелил и забыл" (как описанные выше Прямые вызовы сервера) в рамках своего потока, не дожидаясь их завершения.Vessel
больше не участвует в этом.
Эта модель позволяет Vessel
оставаться легковесными и отзывчивыми, запуская сложные процессы, не перегружаясь их полной сложностью. Состояние, промежуточные результаты и длительная логика надежно управляются выделенным Vibe Процесса
и его базовым движком рабочих процессов.
Управление контекстом с помощью references
Чтобы поддерживать эффективность и не выходить за пределы контекста/схемы LLM, движок рабочих процессов не отправляет всю историю процесса в LLM на каждом шаге. Вместо этого он использует управление контекстом, основанное на явном, машиночитаемом объявлении зависимостей.
-
Мета-свойство
references
: Каждый шаг в скомпилированной схеме конвейера может содержать специальное мета-свойство с именемreferences
. Это свойство содержит массив строк, где каждая строка — это путь к конкретному выводу из предыдущего шага, который необходим текущему шагу (например,["step1_define_rules", "step4_find_user.output"]
). Эти метаданные предназначены только для движка рабочих процессов и удаляются из схемы перед отправкой в LLM. -
Состояние, хранящееся в движке рабочих процессов: Полное состояние и все промежуточные результаты всего процесса надежно хранятся в базовом движке рабочих процессов (например, Temporal).
-
Подача контекста в нужный момент: Когда процессу необходимо выполнить блок шагов, основанных на LLM, движок считывает массив
references
для этого шага. Он использует эти пути для извлечения только необходимых данных из своего состояния, создавая минимальный объект контекста. -
Минимальный промпт для LLM: Финальный промпт, отправляемый в LLM, содержит только схему для текущего блока работы (без свойства
references
) и минимальный объект контекста, содержащий только явно запрошенные данные.
Это гарантирует, что LLM получает только ту информацию, которая ему нужна, предотвращая перегрузку контекста. Давайте посмотрим, как это работает на практике.
Пример: Поток данных с references
После завершения блокирующего шага step4_find_user
, следующий шаг step5_draft_email
определяется с его зависимостями данных в массиве references
:
"step5_draft_email": {
"description": "Составить персонализированное письмо пользователю, найденному на предыдущем шаге.",
"references": [ "step4_find_user.output" ],
"type": "object",
"properties": {
"recipientName": {
"description": "Используйте поле 'userName' из объекта 'step4_find_user.output', предоставленного в контексте промпта.",
"type": "string"
},
"emailBody": { "type": "string" }
}
}
Движок рабочих процессов обрабатывает это так:
- Он видит
references: [ "step4_find_user.output" ]
. - Он извлекает весь объект вывода
{ "userId": "...", "userName": "..." }
из своего состояния. - Он создает минимальный контекст и вставляет его в промпт для LLM.
--- Контекст из предыдущих шагов ---
{
"step4_find_user": {
"output": {
"userId": "u-12345",
"userName": "Jane Doe"
}
}
}
--- Конец контекста ---
Пожалуйста, сгенерируйте JSON для шага step5_draft_email, используя предоставленную выше информацию.
- Он отправляет схему для
step5_draft_email
в LLM, но без свойстваreferences
. Затем LLM использует предоставленный контекст, чтобы следовать инструкциям в поляхdescription
. Это обеспечивает чистый и мощный механизм для передачи данных от серверных действий обратно в процесс рассуждения LLM.
Алиса: «Эта штука с
references
звучит умно. То есть вместо того, чтобы движок рабочего процесса отправлял LLM всю историю процесса для каждого маленького шага, он просто отправляет те конкретные результаты, которые нужны?» Боб: «Именно. Это как попросить коллегу о помощи. Ты не пересказываешь ему всю историю проекта, а просто говоришь: «Вот вчерашнее письмо от клиента, можешь набросать ответ?» Это делает общение целенаправленным и эффективным».
Интеграция Процессов и Vessel's через макрос-запускатель
Мы установили, что Vessel
работают с набором Инструкций
, а Процессы
— это сложные рабочие процессы с состоянием. Последний шаг — элегантно их соединить. Мы делаем это, придерживаясь правила: Vessel
взаимодействуют только с Инструкциями
, и вводим общий, многоразовый макрос для запуска рабочих процессов.
Инструкция LaunchProcess
Вместо того чтобы Vessel
использовали Процессы
напрямую, они используют специальную, общую Инструкцию
под названием LaunchProcess
. Эта Инструкция
функционирует как макрос, который принимает Vibe Процесса
в качестве основного аргумента.
Как это работает: Динамическое слияние во время выполнения
-
Действие
Vessel
: LLMVessel
-а решает, что ему нужно выполнить сложную задачу. Он выбирает инструментLaunchProcess
из своего набора и указывает на конкретный VibeПроцесса
(например,BillingReportProcess
). -
Раскрытие макроса: Система перехватывает это. Вместо того чтобы показывать LLM общую схему
LaunchProcess
, она выполняет динамическое слияние:- Она проверяет целевой Vibe
BillingReportProcess
. - Она извлекает предварительно скомпилированную Инструкцию точки входа (первый блок процесса, основанный на LLM).
- Она оборачивает эту конкретную Инструкцию точки входа в структуру макроса
LaunchProcess
.
- Она проверяет целевой Vibe
-
Единое представление инструмента: LLM получает единую, динамически сгенерированную схему инструмента, которая выглядит специфичной для задачи (например, «Сгенерировать отчет о биллинге»). Она содержит все поля из Инструкции точки входа процесса.
-
Создание процесса: LLM заполняет поля. Самый последний шаг, обрабатываемый логикой макроса
LaunchProcess
, — это вызов на сервер, который создает рабочий процессBillingReportProcess
, передавая данные, которые только что сгенерировал LLM.
Этот подход невероятно мощный:
- Простота:
Vessel
не нужно знать внутренние деталиПроцесса
. Им нужен всего один инструмент:LaunchProcess
. - Единообразие: Все сложные действия с состоянием инициируются через один и тот же механизм.
- Независимость:
Процессы
можно проектировать и обновлять независимо, и пока их Инструкция точки входа действительна, любойVessel
может их запустить.
Используя макро-Инструкцию
в качестве универсального адаптера, мы создаем чистый, надежный и масштабируемый мост между нашими Vessel
для немедленного выполнения и нашими Процессами
с состоянием.
Алиса: «Хорошо, значит, мой Vessel никогда не обращается к
Процессу
напрямую. Он знает только одну Инструкцию:LaunchProcess
. Это как универсальная кнопка "старт" для любой большой работы?» Боб: «Ты все правильно поняла. Vessel говорит: «Я хочу запуститьLaunchProcess
дляBillingReport
». Система находитBillingReportProcess
, его конкретные стартовые инструкции и передает эту «настроенную» кнопку старта LLM Vessel-а для заполнения. Vessel остается простым и чистым, в то время как сложная механика обрабатывается Процессом».
Анатомия скомпилированного Vibe Процесса
Чтобы сделать Процесс
самодостаточным и исполняемым, результаты компиляции и разделения хранятся непосредственно в schema
самого Vibe Процесса
.
Схема (schema
) Vibe Процесса
содержит стандартный блок $defs
из JSON Schema. Этот блок содержит весь разделенный конвейер, где каждый блок хранится как отдельное определение. Мы используем четкое соглашение об именах, чтобы различать контекст для каждого блока:
LLM_<chunk_name>
: Схема для блока шагов, выполняемых одним вызовом LLM.SERVER_<chunk_name>
: Определение для блока из одной или нескольких активностей, выполняемых на сервере движком рабочих процессов.
Инструкция точки входа Процесса
— это просто ссылка на первый LLM_
блок в его $defs
(например, "$ref": "#/$defs/LLM_InitialPrompt"
).
Когда используется макрос LaunchProcess
, он динамически считывает схему целевого Vibe Процесса
, находит первое определение LLM_
и представляет его как инструмент для выполнения Vessel
-ом. Это делает каждый Процесс
самоописываемой, запускаемой сущностью.
Расширенные возможности Процессов
Архитектура Процесса
создана для работы в промышленных масштабах, где пакетная обработка, мультиплексирование и параллелизм являются основными элементами.
Пакетная обработка, мультиплексирование и итерация на стороне сервера
Процесс
предназначен для работы с асинхронными потоками данных, а не просто с единичными экземплярами. Когда процессу нужно обработать несколько элементов одновременно (мультиплексирование), он делает это в рамках одного переключения контекста.
-
Мультиплексирование на уровне схемы: Скомпилированный конвейер — это единый, плоский JSON-объект, где каждый шаг является свойством, умноженным на размер пакета. Чтобы управлять сложностью схемы и делать структуру предсказуемой для LLM, мы избегаем вложенных массивов для пакетов. Вместо этого каждый элемент в пакете для каждого шага становится отдельным свойством. Например, конвейер из 4 шагов, обрабатывающий пакет из 3 элементов, будет скомпилирован в единый плоский объект с 12 свойствами верхнего уровня (например,
step1_item1
,step1_item2
,step1_item3
,step2_item1
и т.д.). Это позволяет LLM обрабатывать весь пакет работы как один большой объект, что очень эффективно. -
Нативная асинхронная итерация: На сервере скомпилированный конвейер процесса напрямую сопоставляется с асинхронным итератором, используя нашу библиотеку
@augceo/iterators
. Это обеспечивает высокоэффективную, параллельную обработку как шагов, основанных на LLM, так и программных шагов, с встроенной поддержкой управления нагрузкой (back-pressure) и ресурсами. Это также минимизирует переключения контекста на сервере, позволяя выполнить несколько программных шагов перед тем, как снова вызывать LLM.
Сочетая мощную, композитную систему Инструкций
с надежной, ориентированной на состояние и пакетную обработку архитектурой Процессов
, мы можем создать высокомасштабируемую и эффективную систему. Vessel
используют Инструкции
для немедленных, одноразовых задач и выступают в роли инициаторов для сложных Процессов
, которые берут на себя тяжелую работу по выполнению длительных, требующих состояния и больших данных рабочих процессов.
Соглашения о схемах для компилируемых конвейеров
Чтобы сделать скомпилированные конвейеры однозначными как для LLM, так и для серверного движка рабочих процессов, мы используем набор четких соглашений непосредственно в JSON schema.
1. Префиксы для специальных свойств
_
(Префикс подчеркивания): Обозначает внутренний «мыслительный» шаг для LLM. Это поля, которые LLM должен заполнить в процессе рассуждения, но они считаются промежуточной работой и не являются частью конечного, основного вывода.$
(Префикс доллара): Обозначает метрику для логирования. Когда LLM заполняет поле типа"$qualityScore": 8
, система автоматически фиксирует это как метрику, связанную с задачей, и это не загромождает основной вывод.
2. Определение шагов конвейера: LLM против контекста сервера
Любое свойство в схеме конвейера представляет собой шаг. Схема определяет, как различать простой, неблокирующий шаг, выполняемый в контексте LLM, и блокирующий шаг, который требует обращения к серверу.
Шаги в контексте LLM (неблокирующие)
По умолчанию каждый шаг в блоке конвейера, выполняемом LLM, является неблокирующим. LLM просто заполняет свойства схемы шага и переходит к следующему.
С точки зрения LLM, разница между «мыслительным» шагом и шагом, который запускает серверное действие по принципу "выстрелил и забыл", невелика. Серверный интерпретатор отвечает за то, чтобы заметить, если имя шага (например, step3_log_event
) совпадает с зарегистрированным серверным действием, и выполнить его асинхронно. Сам конвейер не ждет.
"step3_log_event": {
"description": "Залогировать событие на сервере. Не блокирует выполнение конвейера.",
"type": "object",
"properties": {
"eventName": { "type": "string", "const": "UserAction" },
"details": { "$ref": "#/properties/_reasoning_for_action" }
}
}
Шаги в контексте сервера (блокирующие)
Шаг явно определяется как блокирующий вызов сервера, если его схема содержит свойство output
на верхнем уровне. Это свойство сигнализирует движку рабочих процессов, что он должен приостановить выполнение, запустить серверное действие и дождаться результата, который соответствует схеме output
. Остальные свойства шага неявно определяют его входные данные.
Чтобы сделать схему последовательной, свойство output
делается nullable
. Когда LLM генерирует конвейер, который включает блокирующий шаг, его задача — установить поле output
в null
, так как он еще не может знать результат.
Когда следует использовать Процесс с состоянием вместо Прямого вызова сервера?
* [x] Для любой задачи, которая является длительной или должна переживать перезапуски сервера.
* [x] Когда серверное действие является блокирующим, и его результат требуется для последующего шага в конвейере.
* [x] Для любого рабочего процесса, который требует надежного управления состоянием для промежуточных результатов.
* [ ] Для любого простого, неблокирующего побочного эффекта, такого как логирование или отправка уведомления.
* [ ] Когда весь рабочий процесс может быть завершен за один вызов LLM.