Граница между циклом, взаимодействием и подтверждением
—Разделение ответственности за принятие решений и выполнение перед повышением автономности LLM—
Когда мы объединили несколько функций LLM, работавших последовательно, в единый поток, сначала мы думали, что для создания Agentic Workflow будет достаточно автоматически вызывать следующую функцию. Однако на практике требуемая структура включала не только порядок вызовов. Она должна была определять, какие действия можно выполнить в текущем состоянии, запрашивать необходимые решения пользователя и проверять, что результат, одобренный пользователем, совпадает с фактической целью выполнения. Чтобы последовательно выполнять эти решения и проверки, мы разделили границы между Workflow Loop, Domain Adapter, структурированным Interaction и Approval на основе снимков состояния.
1. Предпосылки и проблема
Изначально каждая функция работала как независимый шаг. Когда пользователь вводил данные, один шаг формировал результат, передавал его как входные данные следующему шагу, а затем выбирался следующий шаг выполнения. Хотя у нас были функции для создания, проверки, изменения и сохранения результатов, пользователь по-прежнему должен был самостоятельно связывать весь поток. Функции были завершёнными, но опыт выполнения на уровне целевого результата оставался фрагментированным.
Простого вызова нескольких API в заранее определённом порядке было недостаточно. Запросы должны были изменяться в зависимости от решений пользователя, а предыдущие шаги — дополняться или выполняться повторно на основе результатов проверки. Связывание ветвлений, повторных попыток и ожидания ответов пользователя с помощью простых циклов и условных операторов всё больше усложняет управление состоянием и возобновление работы по мере роста потока. Поэтому мы внедрили Agentic Workflow для управления текущим состоянием и следующим действием. Он запрашивает необходимые сведения и ожидает подтверждения перед операциями, которые сложно отменить, а затем возобновляет выполнение с места остановки.
При проектировании Agentic Workflow первой разделённой нами ответственностью стала ответственность LLM, выполняющей Workflow, и LLM, выполняющей функции предметной области. Workflow LLM выбирает следующий возможный шаг на основе текущего состояния выполнения. Domain LLM создаёт, проверяет или изменяет результаты внутри каждого Tool. Если Workflow LLM также создаёт результаты предметной области или окончательные входные данные Tool, она может обратиться к значениям, отличающимся от проверенного состояния. И наоборот, если Domain LLM решает даже, какой шаг выполнять следующим, правила выполнения начинают пересекаться между функциями. Поэтому Workflow Loop управлял порядком выполнения, ожиданием и возобновлением. Domain Adapter проверял разрешённые в текущий момент действия и входные данные Tool перед вызовом доменного Tool. Благодаря такому разделению изменения правил предметной области можно обрабатывать внутри Adapter и Tool без изменения общего Workflow.
2. Разделение ответственности за выполнение
Общая структура отделяет общую процедуру выполнения от конкретных бизнес-целей. Workflow Loop управляет порядком выполнения, ожиданием и возобновлением, а также определяет следующее действие. Domain Adapter выбирает функции, соответствующие бизнес-цели, и подготавливает вызов Domain Tool. Interaction·Approval связывает пользователя и Loop, а Loop и Adapter соединены через интерфейс вызова Decision·Tool. Две Session связаны только идентификаторами, что отделяет информацию об управлении выполнением от бизнес-результатов.
사용자 -- Interaction / Approval --> Workflow Loop
Workflow Loop <-> Domain Adapter -> Domain Tool
Workflow Session: 실행 / 대기 / 재개 | Domain Session: 업무 결과 / 검증 / 버전
Workflow Session хранит текущее Decision, ожидающие Interaction·Approval, а также сведения о том, завершён ли процесс, завершился ли он ошибкой или был заблокирован политикой. Loop считывает эту информацию, чтобы вызвать Tool или дождаться ответа пользователя, а после вызова сохраняет сведения об успехе или ошибке и следующую позицию выполнения. Здесь состояние Workflow не означает содержимое бизнес-результата; оно показывает, насколько продвинулось выполнение и чего оно ожидает. Loop не интерпретирует смысл бизнес-результатов.
Domain Session хранит результаты, созданные в процессе достижения конкретной бизнес-цели, версии результатов, статус проверки и цели для изменения. Adapter считывает эту информацию, чтобы определить, какой Tool выполнить и требуется ли решение пользователя, а затем формирует окончательные входные данные из проверенного результата. Adapter также проверяет, что одобренный результат совпадает с результатом, предназначенным для выполнения. Здесь состояние Domain показывает, какой результат сформировал бизнес-процесс и можно ли ему доверять.
Иными словами, Loop определяет, «как выполнять цикл», а Adapter — «что сейчас разрешено сделать». Loop выбирает общую обработку в зависимости от типа Decision, а Adapter на основе результата проверки в Domain Session определяет, разрешить ли Tool изменения или сначала запросить подтверждение пользователя.
2.1 Decision и Policy
Workflow Loop и Domain Adapter обмениваются следующим действием через Decision. Когда Adapter вычисляет разрешённые действия с помощью Policy и возвращает Decision, Loop выполняет его. Decision представляет вызов Tool, вопрос пользователю, запрос подтверждения, завершение, ошибку или состояние ожидания. Policy ограничивает действия, которые может вернуть Adapter. Если проверка завершается неуспешно, разрешён только Tool изменения, а не подтверждённые пользователем Tools изменения блокируются.
Здесь LLM — это Workflow LLM. Сначала мы тестировали поток разрешения и блокировки, используя только правила Policy, без LLM. Позже Workflow LLM выбирала одно из разрешённых действий или предлагала только краткое намерение. Adapter формировал окончательные входные данные Tool, считывая проверенный результат из Domain Session и сведения о подтверждении из Workflow Session. LLM не записывает эти значения напрямую.
Мы также не переписывали существующие функции, основанные на шагах. Мы обернули их так, чтобы Tool Registry вызывал уже проверенные функции, а Workflow выполнялся только через Registry. Это позволило сравнивать поведение существующих функций при внедрении нового потока выполнения. Вместо того чтобы с самого начала создавать чрезмерно абстрактные общие модули, мы решили проверить их контракты в реальных потоках, прежде чем принимать дальнейшие решения.
책임 기준 Workflow Loop는 실행 생명주기를, Domain Adapter는 업무 결과 해석과 허용 행동을, Domain Tool은 실제 기능 수행을 담당합니다.
3. Структурированное Interaction
После разделения ответственности за выполнение мы выявили пробелы в контракте пользовательского ввода. Изначально Workflow возвращал только строковые сообщения с вопросами, которые нужно было отправить пользователю. Для простых вопросов этого было достаточно, но было сложно выразить способы взаимодействия, повышающие удобство использования, например выбор вариантов, указание цели или просмотр изменений. Backend сообщал только о необходимости задать вопрос, а UI должен был самостоятельно определять, что именно нужно сделать пользователю.
Чтобы решить эту проблему, мы определили Interaction не как текст на экране, а как смысл действия пользователя. Мы использовали интерфейс, состоящий из полей type, target, options, required и других. type указывает, является ли взаимодействие обычным вводом, выбором, просмотром или чем-либо ещё, а target определяет, к какому рабочему результату относится ответ. options содержит выбираемые значения для взаимодействия типа selection, а required указывает, может ли процесс продолжиться без ответа. UI выражает этот контракт с помощью компонентов, соответствующих контексту, например поля ввода в чате, карточек выбора или экрана сравнения изменений.
{
"type": "REVIEW_DIFF",
"target": "result",
"required": true
}
4. Подтверждение на основе снимка состояния и состояние
Среди решений пользователя Tools, изменяющие данные или выполняющие внешние операции, требовали более строгого контракта подтверждения. Если хранить подтверждение как одно логическое значение, сохраняется лишь факт нажатия пользователем кнопки подтверждения. Невозможно определить, какой результат был одобрен и в каком состоянии. Поэтому одно и то же значение подтверждения можно было повторно использовать даже после изменения результата или его статуса проверки.
Поэтому мы привязали подтверждение к снимку состояния цели выполнения. Информация о подтверждении включала целевой Tool, версию и хеш результата, хеш результата проверки и idempotency key. Непосредственно перед выполнением мы сравнивали текущий результат с результатом на момент подтверждения. Если отличался хотя бы один из целевого Tool, версии или хешей, выполнение не производилось. Пользователь подтверждал не просто то, что «это можно выполнить», а конкретный идентифицируемый результат.
Эта проверка также предотвращает изменения между моментом подтверждения и моментом выполнения. Другой запрос может изменить тот же результат сразу после того, как пользователь просмотрел его на экране, либо результат проверки может измениться, пока запрос ожидает в очереди выполнения. Проверка условий только в момент подтверждения не обнаружила бы такие изменения. Повторная проверка идентичности непосредственно перед выполнением гарантирует, что содержимое, увиденное пользователем, совпадает с фактически сохранённым содержимым.
실행 가능 = 승인됨
&& 대상 Tool 일치
&& 결과 버전 / 해시 일치
&& 검증 결과 해시 일치
&& 멱등성 키 유효
Мы также различали несоответствия и ошибки выполнения Tool. Если подтверждение отсутствовало, устарело или отличалось от текущего результата, Policy блокировала операцию до вызова Tool. Напротив, внутренняя или внешняя ошибка, возникшая после вызова разрешённого Tool, классифицировалась как ошибка выполнения. Разделение этих состояний упрощает определение того, можно ли повторить операцию или пользователю необходимо подтвердить её заново либо обновить состояние.
4.1 Аудит и идемпотентность
Запросы подтверждения, результаты подтверждения, вызовы Tool и блокировки политикой записывались как Audit Events. Аудитные записи — это не только журналы, создаваемые постфактум. Они служат основой для следующего Decision, позволяя интерпретировать результаты предыдущего выполнения, а операторам — определять, почему конкретное выполнение продолжилось или остановилось. Группировка связанных событий в одном потоке выполнения позволяет отслеживать ответы пользователя и результаты Tool вместе.
Мы использовали idempotency key, чтобы предотвратить повторное выполнение одного и того же запроса подтверждения. Даже если произойдёт повторная попытка из-за сбоя сети или пользователь несколько раз нажмёт кнопку, уже обработанный ключ не выполняется повторно. Важно, что мы включили проверку идемпотентности не только внутрь Tool, но и в контракты подтверждения и выполнения. Это позволяет с точки зрения Workflow объяснить, что одно и то же намерение было обработано только один раз.
4.2 Персистентное состояние
Изначально мы передавали часть состояния Workflow через запросы и ответы. Это было удобно для коротких функциональных тестов, но затрудняло гарантированное возобновление после обновления страницы, перезапуска сервера или длительного ожидания подтверждения. Если информация о подтверждении и аудите зависит от состояния, хранящегося на клиенте, серверу также сложно самостоятельно определить достоверное состояние текущего выполнения.
В последующей работе мы сохраняли Workflow Session, состояние подтверждения и состояние аудита с помощью JPA.
5. Критерии применения и заключение
В этой работе при проектировании Agentic Workflow мы применяли следующие критерии. Во-первых, мы разделили цикл выполнения и решение в предметной области. Определяя границу, мы проверяли, требуется ли логике знать содержимое бизнес-результата. Если ей нужно было знать смысл результата или разрешённые правила, мы помещали её в Adapter; если это была процедура, повторяющаяся независимо от предметной области, например ожидание, возобновление или завершение, мы помещали её в Loop. Без этого критерия добавление новых потоков постоянно увеличивало бы количество условных операторов и интерпретаций состояния в Loop.
Во-вторых, мы позволили LLM предлагать действия в пределах разрешённой области, но не дали ей владеть окончательными входными данными Tool. Policy вычисляет возможные действия, а Adapter формирует входные данные, используя проверенный результат и информацию о подтверждении. Workflow LLM лишь выбирает действие в этих пределах; она не создаёт новое содержимое результата и не формирует значения подтверждения. Такое разделение позволяет отличать значения, предложенные LLM, от фактов, проверенных приложением. Повышение автономности — это не то же самое, что отказ от контроля над выполнением.
В-третьих, вместо передачи пользователю содержимого только в виде строки мы выразили его в формате действия. Для обычного ввода, выбора и просмотра требуются разные инструкции и способы ответа. type указывает тип действия, а target задаёт рабочий результат, к которому относится ответ. Когда требуется выбор, options передаёт возможные значения, а required указывает, может ли процесс продолжиться без ответа. Цель структурированного Interaction — не управлять UI, а прояснить способ взаимодействия между пользователем и Workflow.
В-четвёртых, мы привязали подтверждение к снимку состояния цели выполнения на момент подтверждения. Сохранение только факта нажатия кнопки не может предотвратить изменения после подтверждения или повторное выполнение. Целевой Tool, версию и хеш результата, статус проверки и idempotency key необходимо повторно проверить непосредственно перед выполнением. Если условия не совпадают, Tool не вызывается, а событие записывается как блокировка политикой. И наоборот, если после прохождения условий внутри Tool возникает ошибка, она записывается как ошибка выполнения. Различение этих исходов позволяет определить, требуется ли повторное подтверждение или возможна повторная попытка.
Workflow Session, состояния подтверждения и аудита, необходимые для прерывания и возобновления, в последующей работе сохранялись с помощью JPA.
В этой работе мы организовали Agentic Workflow не как структуру, автоматически связывающую вызовы LLM, а как структуру, разделяющую ответственность за принятие решений и выполнение. Мы разделили роли Loop и Adapter, прояснили с помощью Interaction, к какой цели относятся ответы пользователя, и сопоставили одобренный результат с фактической целью выполнения через Approval. В конечном счёте важнее было не заставить LLM делать больше, а чётко определить, кто, что и на каком основании решает, а также на каком основании выполняется операция.
DEVKC