Практическое руководство
Контур подтверждения для опасных действий AI-агента
Как остановить автоматизацию перед удалением, оплатой или публикацией, передать решение человеку и сохранить проверяемую историю событий.
Зачем нужен отдельный контур
AI-агент может самостоятельно составить план и вызвать доступные ему инструменты. Это удобно, пока действие обратимо: прочитать файл, подготовить черновик или рассчитать сумму. Удаление данных, перевод денег и публичная отправка меняют внешнее состояние и могут причинить ущерб даже при формально правильном вызове.
Поэтому опасная операция не должна выполняться непосредственно из ответа модели. Между намерением агента и рабочим API нужен контур подтверждения: агент создаёт неизменяемое описание операции, система помещает его в очередь, уполномоченный человек принимает решение, а исполнитель повторно проверяет условия и только затем вызывает API.
Минимальная модель
Разделим систему на четыре компонента:
- Планировщик получает намерение агента, но не имеет полномочий на опасный вызов.
- Очередь подтверждений хранит нормализованное описание операции со статусом
pending. - Интерфейс проверяющего показывает последствия и принимает явное решение.
- Исполнитель читает подтверждённую запись, проверяет срок, целостность и права, после чего выполняет действие не более одного раза.
Журнал решений отделён от текущего состояния заявки. Смена статуса удобна для работы очереди, но сама по себе не объясняет, кто и когда принял решение. Поэтому каждое событие записывается дополнительно и не редактируется задним числом.
| Класс действия | Что показывать проверяющему | Дополнительное условие |
|---|---|---|
| Удаление | Тип и идентификатор объекта, область удаления | Версия объекта или контрольная сумма |
| Оплата | Получатель, сумма, валюта, назначение | Лимит и идемпотентный ключ |
| Публикация | Канал, аудитория, финальный текст или его снимок | Запрет незаметного изменения после одобрения |
Воспроизводимая реализация
Шаг 1. Введите политику действий
Политика должна находиться в обычном коде или конфигурации, а не только в системном промпте. Следующий фрагмент — пример конфигурации, а не универсальный стандарт:
actions:
read_document:
approval: never
delete_record:
approval: required
expires_in_seconds: 900
create_payment:
approval: required
expires_in_seconds: 300
max_amount_minor: 100000
publish_post:
approval: required
expires_in_seconds: 1800
default:
approval: required
Безопасное значение по умолчанию — required. Новое действие тогда не станет автономным только потому, что разработчик забыл добавить его в список.
Шаг 2. Опишите заявку как снимок намерения
Заявка должна содержать только нормализованные параметры, необходимые для решения и выполнения. Не сохраняйте в ней пароли, токены доступа и полные секретные заголовки.
{
"approval_id": "appr_example_001",
"action": "publish_post",
"resource": {
"channel_id": "channel_example",
"content": "Пример текста публикации"
},
"risk": "public_write",
"requested_by": "agent_example",
"created_at": "2030-01-01T10:00:00Z",
"expires_at": "2030-01-01T10:30:00Z",
"status": "pending",
"payload_hash": "sha256:EXAMPLE_NOT_A_REAL_DIGEST",
"idempotency_key": "idem_example_001"
}
Значения выше намеренно условные. Метка payload_hash должна вычисляться приложением по каноническому представлению полей, влияющих на действие. Если текст, сумма, адресат или объект изменились, прежнее подтверждение недействительно.
Шаг 3. Создайте таблицы очереди и журнала
Ниже приведён минимальный пример для реляционной базы данных. Типы и функции времени при необходимости адаптируйте к выбранной СУБД.
CREATE TABLE approval_requests (
approval_id VARCHAR(64) PRIMARY KEY,
action_name VARCHAR(80) NOT NULL,
payload_json TEXT NOT NULL,
payload_hash VARCHAR(80) NOT NULL,
status VARCHAR(16) NOT NULL
CHECK (status IN ('pending', 'approved', 'rejected',
'expired', 'executed', 'failed')),
requested_by VARCHAR(128) NOT NULL,
created_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
idempotency_key VARCHAR(128) NOT NULL UNIQUE,
decided_by VARCHAR(128),
decided_at TIMESTAMP,
executed_at TIMESTAMP
);
CREATE TABLE approval_events (
event_id VARCHAR(64) PRIMARY KEY,
approval_id VARCHAR(64) NOT NULL,
event_type VARCHAR(32) NOT NULL,
actor_id VARCHAR(128) NOT NULL,
occurred_at TIMESTAMP NOT NULL,
payload_hash VARCHAR(80) NOT NULL,
reason VARCHAR(500),
FOREIGN KEY (approval_id)
REFERENCES approval_requests(approval_id)
);
В рабочей системе запретите обычной роли приложения изменять и удалять строки approval_events. Политика хранения и доступ аудиторов зависят от вашей инфраструктуры и требований организации.
Шаг 4. Разорвите прямой путь к опасному API
Инструмент агента не должен называться и работать как delete_now или pay_now. Для опасных операций предоставьте ему только создание заявки:
def request_action(actor, action, payload):
policy = load_policy(action)
if policy.approval != "required":
return execute_safe_action(actor, action, payload)
normalized = normalize_and_validate(action, payload)
request = create_pending_request(
actor=actor,
action=action,
payload=normalized,
expires_in=policy.expires_in_seconds,
)
append_event(request, "requested", actor, reason=None)
return {
"status": "approval_required",
"approval_id": request.approval_id
}
Это псевдокод-пример. Существенно не имя функции, а граница полномочий: учётные данные планировщика не должны позволять ему обойти очередь и вызвать опасный API напрямую.
Шаг 5. Принимайте решение атомарно
Проверяющий должен видеть точные параметры, последствия, инициатора и срок действия. Кнопка подтверждения передаёт идентификатор заявки и ожидаемую контрольную сумму:
BEGIN;
UPDATE approval_requests
SET status = 'approved',
decided_by = :reviewer_id,
decided_at = CURRENT_TIMESTAMP
WHERE approval_id = :approval_id
AND payload_hash = :visible_payload_hash
AND status = 'pending'
AND expires_at > CURRENT_TIMESTAMP;
-- Продолжать только если изменена ровно одна строка.
INSERT INTO approval_events (
event_id, approval_id, event_type, actor_id,
occurred_at, payload_hash, reason
) VALUES (
:event_id, :approval_id, 'approved', :reviewer_id,
CURRENT_TIMESTAMP, :visible_payload_hash, :reason
);
COMMIT;
Отклонение оформляется таким же переходом из pending в rejected. Причина полезна для разбора, но не должна содержать секреты.
Шаг 6. Повторно проверьте заявку перед выполнением
Исполнитель не доверяет одному полю status. Он сверяет срок, хеш, политику, права проверяющего и ключ идемпотентности. Затем резервирует заявку атомарной операцией и выполняет ровно тот снимок, который был показан человеку.
def execute_approved(approval_id):
request = load_for_update(approval_id)
require(request.status == "approved")
require(now() < request.expires_at)
require(hash_payload(request.payload) == request.payload_hash)
require(reviewer_is_authorized(request.decided_by, request.action))
require(current_policy_allows(request))
claim_once(request.approval_id, request.idempotency_key)
result = call_external_api(
action=request.action,
payload=request.payload,
idempotency_key=request.idempotency_key,
)
mark_executed_and_append_event(request, result)
return result
Если целевой API не поддерживает идемпотентность, локальная блокировка снижает риск повторов, но не устраняет неопределённость после сетевого сбоя. Для платежей это ограничение следует учитывать при выборе провайдера и проектировании сверки.
Проверка результата
Проверяйте контур в изолированной среде на фиктивных объектах. Не используйте реальные платежи, публичные каналы или ценные данные.
- Создайте тестовую заявку на удаление фиктивной записи. Убедитесь, что до решения объект остаётся на месте.
- Отклоните заявку. Исполнитель должен отказаться от вызова, а журнал — содержать события
requestedиrejected. - Создайте новую заявку и подтвердите её. Проверьте, что выполнился именно показанный набор параметров.
- Повторно отправьте команду выполнения. Второго внешнего эффекта быть не должно.
- Измените снимок после создания заявки. Сверка контрольной суммы должна остановить выполнение.
- Дождитесь истечения короткого тестового срока. Просроченная заявка должна перейти в
expiredили быть отклонена исполнителем. - Попробуйте подтвердить заявку учётной записью без нужной роли. Решение не должно сохраниться.
Для ручной проверки очереди безопасен запрос только на чтение:
SELECT approval_id, action_name, status,
requested_by, created_at, expires_at
FROM approval_requests
WHERE status = 'pending'
ORDER BY created_at ASC;
А этим запросом можно сопоставить решение и последующее выполнение:
SELECT approval_id, event_type, actor_id,
occurred_at, payload_hash, reason
FROM approval_events
WHERE approval_id = :approval_id
ORDER BY occurred_at ASC;
Ожидаемый результат — непрерывная последовательность событий без выполнения до approved, с одинаковой контрольной суммой на всех этапах.
Типовые ошибки
- Подтверждение хранится только в чате
- Фраза «да, продолжай» неоднозначна и может относиться к другому плану. Решение должно ссылаться на конкретную заявку и её контрольную сумму.
- Человеку показывают краткий пересказ
- Формулировка «опубликовать отчёт» скрывает канал, аудиторию и содержание. Подтверждать нужно фактический снимок операции.
- Агент и исполнитель используют одинаковые полномочия
- Тогда очередь можно обойти прямым вызовом. Разделите роли и учётные данные на уровне инфраструктуры.
- Разрешение действует бессрочно
- Контекст успевает измениться: цена, версия документа или владелец ресурса уже другие. Добавьте короткий срок и повторную проверку.
- Кнопку можно нажать дважды
- Двойная доставка, повтор HTTP-запроса и перезапуск процесса нормальны для распределённых систем. Используйте атомарный переход статуса и ключ идемпотентности.
- В журнал попадают секреты
- Аудит не оправдывает хранение токенов или платёжных реквизитов без необходимости. Записывайте идентификаторы, хеши и минимальные безопасные атрибуты.
- Ошибки автоматически считаются разрешением
- Недоступная база, повреждённая политика или невозможность проверить роль должны останавливать действие.
Ограничения
Контур подтверждения снижает риск ошибочного действия, но не гарантирует правильность решения человека. Проверяющий может невнимательно прочитать заявку, а скомпрометированная учётная запись — подтвердить вредоносную операцию.
Для особенно чувствительных действий могут потребоваться два независимых решения, лимиты суммы, задержка исполнения, отдельное уведомление владельца ресурса или возможность отмены. Эти меры следует выбирать по модели угроз, а не добавлять формально.
Очередь также увеличивает задержку и создаёт операционную нагрузку. Не стоит отправлять туда каждое чтение: классифицируйте действия по последствиям, оставляя обязательное подтверждение для необратимых, финансовых и публичных изменений.
Наконец, журнал решений показывает, что система намеревалась сделать и какое решение получила. Он не заменяет журналы целевого сервиса: для расследования полезно связывать заявку с идентификатором фактической операции во внешней системе.
Критерии готовности
- опасный инструмент недоступен планировщику напрямую;
- каждая заявка содержит точные параметры, срок и контрольную сумму;
- неизвестное действие по умолчанию требует подтверждения;
- решение принимает авторизованный человек, а не сама модель;
- изменение параметров аннулирует прежнее разрешение;
- повторная доставка не создаёт повторного эффекта;
- события запроса, решения и выполнения сохраняются отдельно;
- сбой проверки завершает процесс запретом.
После выполнения этих условий агент может готовить опасные операции, но граница окончательного решения остаётся контролируемой и проверяемой.