Агент поддержки правильно нашёл двойное списание и подготовил возврат на 48 700 рублей. Оператор увидел знакомую карточку, нажал зелёную кнопку и только потом заметил, что сумма относится к двум разным заказам. Формально человек участвовал. Практически интерфейс предложил ему подписать чужое решение, не показав основания.
Зелёная кнопка в этой истории работала как подпись на документе, который дали читать через щель в двери. Формальность соблюдена, ответственность передана, содержание никто не видел.
Human-in-the-loop полезен не присутствием человека, а местом контроля. Подтверждение ставят перед необратимым или дорогим side effect, дают проверяющему факты и не позволяют изменить действие после одобрения.
Решение и исполнение надо разнести
Модель создаёт proposal. Policy engine вычисляет риск. Для безопасной операции оркестратор может продолжить сам; опасная операция переходит в pending_approval.
model proposal → schema validation → policy decision
├─ allow → execute
├─ deny
└─ require approval → bind → approve → executeclass ActionProposal(BaseModel):
tool: Literal["refund", "send_email", "close_ticket"]
args: dict
evidence: list[str]
reason: str
async def submit(proposal: ActionProposal, actor: User) -> str:
decision = policy.evaluate(proposal, actor)
if decision == "deny":
raise PermissionError("Действие запрещено policy")
if decision == "allow":
return await execute(proposal)
payload = proposal.model_dump_json()
approval = await approvals.create(
payload=payload,
payload_hash=sha256(payload.encode()).hexdigest(),
expires_in=timedelta(minutes=30),
requested_by=actor.id,
)
return approval.idПри исполнении сервис заново проверяет hash, срок действия, полномочия одобрившего и актуальные preconditions. Одобрение возврата на 48 700 рублей нельзя применить к предложению на 84 700, даже если модель «уточнила» сумму в следующем сообщении.
Риск задаётся политикой
Политика должна опираться на действие и контекст, а не на уверенность модели. Для refund имеют значение сумма, валюта, владелец бюджета, число возвратов по заказу и свежесть evidence. Для письма важны адресат, внешний домен, вложения и наличие персональных данных. confidence=0.97 права не выдаёт.
Пример правил:
- action: refund
when: amount_minor <= 5000 and currency == "RUB"
effect: allow
- action: refund
when: amount_minor <= 100000 and currency == "RUB"
effect: require_approval
approver_role: support_lead
- action: refund
effect: denyЧеловек должен видеть diff и evidence: что будет изменено, в какой системе, на какую сумму, из каких записей это следует и можно ли отменить. Кнопка «Подтвердить» рядом с пересказом модели является не контролем, а способом быстро распределить ответственность.
Минимальный набор для карточки согласования: что изменится (было → станет), в какой системе, на какую сумму, из каких первичных записей это следует, обратимо ли это и что произойдёт, если не подтверждать. Шесть пунктов, каждый в одну строку. Всё, что длиннее, оператор перестанет читать к концу первой недели, и вы получите ту же зелёную кнопку, только с абзацем текста над ней.
Где ставить человека
Контроль перед действием нужен для денег, удаления, публикации, внешней коммуникации, изменения прав и операций с юридическими последствиями. После действия человек полезен для выборочного аудита дешёвых обратимых операций. При неоднозначных данных нужен не approve, а запрос решения: оператор выбирает один из вариантов или исправляет proposal.
Очередь согласований тоже часть production-системы. У неё должны быть SLA, замещение отсутствующего сотрудника, срок годности proposal и безопасное состояние по истечении срока. Если очередь стоит два дня, агент не автоматизировал процесс, а аккуратно переложил папку на другой стол.
Failure modes
Approval fatigue. Сотрудник подтверждает сотни одинаковых карточек. Снижайте объём через risk tiers и выборочный аудит, а не прячьте риск.
Человек не видит источников. Покажите первичные записи и конфликтующие данные, не только объяснение модели.
TOCTOU. Между одобрением и исполнением изменился заказ. Проверяйте version или preconditions непосредственно перед side effect.
Самоодобрение. Инициатор или сервисный аккаунт одновременно выступает approver. Для существенных операций требуется разделение ролей.
Повтор после таймаута. UI сообщает ошибку, хотя возврат прошёл, и оператор нажимает снова. Нужны idempotency key и проверка статуса у платёжного провайдера.
Редактирование после approval. Любое изменение аргументов аннулирует одобрение и создаёт новую заявку.
Сколько согласований выдержит человек
Очередь согласований — это работа живого сотрудника, и она измеряется как работа: сколько карточек в час, сколько времени на карточку, какая доля отклоняется.
Три числа, по которым видно, что контроль превратился в ритуал:
- Доля отклонений близка к нулю. Значит, policy пропускает в очередь то, что и так безопасно, а человек штампует.
- Среднее время на карточку меньше десяти секунд. Прочитать основания за это время нельзя.
- Очередь растёт быстрее, чем разбирается. Автоматизация не случилась, появился новый узкий проход.
Лечение всегда одно и то же: не «уговорить операторов быть внимательнее», а сузить поток. Поднять порог автоматического разрешения там, где ущерб мал; ужесточить запрет там, где он велик; оставить в очереди только середину, где решение действительно нужно человеку. Выборочный аудит постфактум для дешёвых операций даёт больше пользы, чем сплошное подтверждение, которое никто не читает.
Правило главы
Человек подтверждает конкретный неизменяемый side effect на основании первичных фактов. Policy решает, когда это требуется; hash, preconditions и idempotency защищают решение между кнопкой и исполнением.
Чеклист главы
Практикум
Задача 35.1 — approval flow с порогами. ⭑⭑
Реализуйте approval flow для refund(order_id, amount_minor, currency). Суммы до 50 рублей разрешайте автоматически, до 1 000 рублей отдавайте руководителю поддержки, остальные запрещайте.
Критерии приёмки:
Подсказки:
- Хеш считайте от канонизированного JSON аргументов: порядок ключей и форматирование не должны влиять на результат.
- Отдельно проверьте сценарий «платёж прошёл, ответ не дошёл»: без idempotency key он превращается в двойной возврат.
Задача 35.2 — карточка, по которой можно принять решение. ⭑⭑
Спроектируйте и соберите интерфейс подтверждения, где оператор видит основания, а не пересказ.
Критерии приёмки:
Подсказки:
- Заложите в тестовый набор две-три карточки, где правильное действие — отклонить. Без них вы измеряете скорость, а не контроль.
- Причины отклонений — лучший источник новых правил policy: они показывают, что человек знает, а система ещё нет.
Задача 35.3 — учения по опасному действию. ⭑⭑⭑
Проведите учения, где всё, что может пойти не так между кнопкой и исполнением, идёт не так.
Критерии приёмки:
Подсказки:
- Учения удобно проводить на песочнице платёжного провайдера: там можно заказать нужные отказы.
- Отзыв прав approver — редкий, но показательный сценарий: он проверяет, что полномочия сверяются в момент исполнения, а не только в момент нажатия.