Перейти к содержимому
Документация

Руководство: человек в контуре (HITL)

Некоторые шаги не должны выполняться, пока человек не скажет «да»: вернуть деньги, отправить рассылку на сто тысяч адресов, опубликовать то, что написала модель, удалить записи, которые собирались годами. Human-in-the-loop, коротко HITL, — это подход, при котором автоматический процесс останавливается в такие моменты, показывает человеку, что сейчас произойдёт, и продолжается только после его решения. Это руководство объясняет идею, встраивает согласование в пайплайн и показывает, как одобрять из входящих и из собственных инструментов и как сделать так, чтобы согласующий об этом узнал.

Что такое человек в контуре#

Полностью автоматические потоки быстрые, полностью ручные — безопасные; HITL берёт безопасность человеческого решения и применяет её только там, где это важно, оставляя остальное автоматическим. Человек — не узкое место для каждого запуска, а только для тех немногих, что дошли до гейта, и поток сохраняет своё состояние во время ожидания, так что решение может прийти через минуты или дни. Типичные места для гейта:

  • Деньги и договоры. Возврат выше порога, выплата, скидка, о которой договорился агент.
  • Контент от ИИ. Модель набрасывает ответ, резюме или изменение кода; человек читает, прежде чем это отправят, опубликуют или смержат. Модель делает работу, ответственность остаётся у человека.
  • Необратимые действия в масштабе. Массовая рассылка, смена цен по всему каталогу, выкатка на всех клиентов.
  • Неоднозначность, которую машина не разрешит. Две записи клиента, которые могут быть одним человеком; документ, подходящий под две категории. Вместо одобрения человек отвечает на вопрос, и поток использует ответ.

Как это устроено на платформе#

В графовом пайплайне гейт — это узел Human gate (кнопка + Human в палитре узлов). Когда запуск доходит до него, он получает статус Ожидание, его состояние сохраняется, и ни один контейнер не остаётся занятым. У узла два режима:

  • Одобрить / отклонить: человек видит промпт и нажимает Одобрить или Отклонить. У узла два исходящих ребра, approve и reject (на холсте подписаны ok и no), и запуск идёт по выбранному.
  • Вопрос (свободный ответ): человек отвечает значением, любым JSON, и запуск продолжается по единственному ребру с ответом, приложенным к payload.

Ожидающие запуски собираются в разделе Алерты → Согласования боковой панели, а ещё их можно возобновить из редактора пайплайна или через API. Каким бы путём ни пришло решение, запуск продолжается ровно с того места, где остановился, а решение или ответ вливаются в payload для следующих шагов.

1. Собираем пайплайн с согласованием#

Пример — поток возврата: функция готовит возврат, человек его одобряет, и в зависимости от решения возврат применяется или клиенту сообщают об отказе. Откройте Пайплайны, создайте новый графовый пайплайн и добавьте узлы:

  1. Ручной триггер (кнопка + Manual в палитре) в качестве старта. Вебхук или расписание работают так же, когда поток проверен.
  2. Узел Функция с функцией, которая считает возврат и возвращает что-то вроде {"customer": "…", "amount": 120, "reason": "…"}. Соедините триггер с ним, а его выход ok (success в JSON) — с гейтом.
  3. Узел Human gate с режимом Одобрить / отклонить. В поле Промпт (шаблон, необязательно) напишите, что должен увидеть согласующий; это шаблон поверх входящего payload, так что Вернуть {{input.amount}} клиенту {{input.customer}}? отрисуется с реальными значениями каждого запуска. Шаблон может быть и JSON-объектом, когда согласующему нужно показать несколько полей.
  4. Ещё два узла Функция: один к выходу гейта ok (approve), применяющий возврат, другой к no (reject), уведомляющий клиента. Любая ветка может быть длиннее или отсутствовать; гейт без ребра reject просто завершает запуск при отклонении.

Тот же граф в JSON. Нажмите JSON на панели инструментов редактора, вставьте его, заменив id функций на свои, и нажмите Применить в редакторе:

graph.jsonjson
{
  "schemaVersion": 1,
  "nodes": [
    { "id": "start", "kind": "manualTrigger", "name": "Start",
      "position": { "x": 0, "y": 0 }, "config": {} },
    { "id": "draft", "kind": "lambda", "name": "Draft the refund",
      "position": { "x": 0, "y": 140 },
      "config": { "functionId": "<draft-refund function id>", "onError": "failPipeline" } },
    { "id": "gate", "kind": "humanGate", "name": "Approve the refund",
      "position": { "x": 0, "y": 280 },
      "config": {
        "mode": "approve",
        "promptTemplate": {
          "text": "Refund {{input.amount}} to {{input.customer}}?",
          "amount": "{{input.amount}}"
        }
      } },
    { "id": "apply", "kind": "lambda", "name": "Apply the refund",
      "position": { "x": -180, "y": 420 },
      "config": { "functionId": "<apply-refund function id>" } },
    { "id": "decline", "kind": "lambda", "name": "Tell the customer",
      "position": { "x": 180, "y": 420 },
      "config": { "functionId": "<notify-declined function id>" } }
  ],
  "edges": [
    { "id": "e1", "sourceNodeId": "start", "targetNodeId": "draft" },
    { "id": "e2", "sourceNodeId": "draft", "targetNodeId": "gate", "sourceHandle": "success" },
    { "id": "e3", "sourceNodeId": "gate", "targetNodeId": "apply", "sourceHandle": "approve" },
    { "id": "e4", "sourceNodeId": "gate", "targetNodeId": "decline", "sourceHandle": "reject" }
  ],
  "metadata": { "pipeline": { "executionMode": "async" } }
}

Нажмите Опубликовать. Редактор проверит граф, сообщит о недостающем — например, о шаге согласования без исходящих связей — и вернёт номер версии. В продакшене выполняются только опубликованные версии, и только их запуски видны в оповещениях и истории.

2. Запускаем и одобряем#

Нажмите Запустить вручную и передайте payload для функции-черновика, например {"orderId": "A-1042"}. Первый шаг выполняется, запуск доходит до гейта и его статус меняется на Ожидание. Больше ничего не происходит, пока кто-то не решит; запуск может ждать столько, сколько нужно.

Откройте раздел Алерты → Согласования в боковой панели. Каждый ожидающий запуск показывает пайплайн, время старта, отрисованный Промпт / инструкции и Выход предыдущего шага, чтобы согласующий видел реальные данные, а не их пересказ. Приостановленный запуск также виден в редакторе пайплайна с панелью Ввод человека сбоку.

Нажмите Одобрить. Запуск возобновляется, выполняется ветка apply, и запуск завершается зелёным; таймлайн выполнения показывает шаг гейта с решением и тем, кто его принял. Запустите снова и нажмите Отклонить, чтобы увидеть другую ветку.

3. Спросить вместо одобрения#

Переключите Режим гейта на Вопрос (свободный ответ), когда потоку нужна информация, а не разрешение. У узла тогда одно исходящее ребро, а в разделе согласований появляется поле Ответ (JSON) с кнопкой Отправить ответ. Что бы человек ни отправил — число, строку, объект, — это приходит в payload как humanGate.answer.

Частая форма — агент, который спрашивает. Функция предлагает три варианта ответа, гейт спрашивает, какой отправить или не отредактировать ли его, а следующий шаг отправляет выбранный текст. Варианты можно вставить в шаблон промпта, чтобы весь обмен помещался на одном экране.

4. Одобряем из собственных инструментов#

Раздел согласований — клиент небольшого API, и таким же клиентом может быть ваш Slack-бот, бэк-офис или мобильное приложение. Токен с правом записи на jobs перечисляет ожидающие запуски и возобновляет или отменяет их:

terminalbash
# Runs waiting for a person — the same list the inbox shows
curl "https://api.inquir.org/pipeline-graph-executions/waiting" \
  -H "Authorization: Bearer $INQUIR_TOKEN"

# Approve (or send {"decision":"reject"})
curl -X POST "https://api.inquir.org/pipeline-graph-executions/<executionId>/resume" \
  -H "Authorization: Bearer $INQUIR_TOKEN" -H "Content-Type: application/json" \
  -d '{"decision":"approve"}'

# Question mode: any JSON value is the answer
curl -X POST "https://api.inquir.org/pipeline-graph-executions/<executionId>/resume" \
  -H "Authorization: Bearer $INQUIR_TOKEN" -H "Content-Type: application/json" \
  -d '{"answer":{"discount":10}}'

# Give up on a paused run
curl -X POST "https://api.inquir.org/pipeline-graph-executions/<executionId>/cancel" \
  -H "Authorization: Bearer $INQUIR_TOKEN"

Поле decision принимает approve или reject для гейтов в режиме approve; гейты в режиме question принимают answer. Возобновление запуска, который не ждёт, возвращает конфликт, так что два согласующих, нажавших кнопку одновременно, не применят возврат дважды.

5. Сообщаем согласующему#

Шаг согласования, о котором никто не знает, — это запуск, который ждёт вечно. Создайте правило оповещения вида Проверка человеком (пауза до решения) с каналом Slack: каждый раз, когда запуск встаёт на паузу, согласующие видят сообщение с пайплайном, промптом и ссылкой на раздел согласований. Руководство по оповещениям показывает, как его создать.

Заметки по дизайну#

  • Не ждите итогового результата на стороне вызывающего. Вызывающий вебхук не может держать HTTP-соединение, пока человек думает: вызов возвращается, как только запуск встаёт на паузу у гейта, с id запуска и статусом ожидания. Опрашивайте запуск или пусть итог отправит шаг после гейта. Вариант Асинхронный в поле Вызов настроек пайплайна пока только сохраняется как предпочтение.
  • Параллельные ветки продолжают работать. Если граф ветвится до гейта, остальные ветки выполняются, пока гейт ждёт, а запуск завершается только когда готово всё, включая ветку с гейтом.
  • Делайте промпт самодостаточным. Согласующий должен принять решение по одному промпту: сумма, клиент, причина, ссылка на запись. Кладите поля в шаблон, а не заставляйте открывать вывод предыдущего шага.
  • Решите, что значит отсутствие ответа. Запуски ждут бесконечно — так задумано. Отменяйте устаревшие через API из функции по расписанию или алертите на их возраст; ребро reject, которое прибирает за собой, лучше брошенного запуска.
  • Сохраняйте след. История выполнения хранит шаг гейта с решением, ответом и временем. Для регулируемых потоков эта запись и есть аудит-лог; не обходите гейт для «доверенных» входов — сузьте его условием.

Куда дальше#

Справочник по пайплайнам описывает каждый тип узла, шаблоны и режимы выполнения; руководство по оповещениям поможет сделать так, чтобы согласования не оставались незамеченными. Хороший следующий шаг — поставить согласование перед тем единственным автоматическим действием в вашей системе, которое вам меньше всего хотелось бы увидеть запущенным по ошибке.