Агент поддержки правильно нашёл двойное списание и подготовил возврат на 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. Любое изменение аргументов аннулирует одобрение и создаёт новую заявку.
Правило главы
Человек подтверждает конкретный неизменяемый side effect на основании первичных фактов. Policy решает, когда это требуется; hash, preconditions и idempotency защищают решение между кнопкой и исполнением.
Практикум
Реализуйте approval flow для refund(order_id, amount_minor, currency). Суммы до 50 рублей разрешайте автоматически, до 1 000 рублей отдавайте руководителю поддержки, остальные запрещайте. Сохраните proposal, hash, инициатора, approver, время и результат исполнения.
Проверьте пять случаев: изменение суммы после approval, истёкшее согласование, повторный клик, смена версии заказа и попытка самоодобрения. В журнале должно быть видно не рассуждение модели целиком, а proposal, evidence, решение policy и фактический ответ платёжного API.