Практика · Уровень: средний
Режим dry run для агента с правом изменять данные
Агент с доступом к рабочим интеграциям может создать документ, отправить сообщение или сменить статус раньше, чем разработчик заметит ошибку в его плане. Разберём схему, при которой сначала формируется проверяемый список действий, а исполнение включается только отдельным явным решением.
Что именно защищает dry run
Dry run — режим, в котором система рассчитывает и показывает предполагаемые действия, но не выполняет операции с побочными эффектами. Он полезен не только при отладке: предварительный просмотр можно оставить постоянной частью интерфейса для чувствительных операций.
Ключевое правило: флаг предварительного просмотра должен проверяться до вызова изменяющей интеграции. Просьба в системном промпте «ничего не отправляй» недостаточна. Модель может неверно понять инструкцию, а обёртка инструмента — вызвать внешний API сразу после разбора аргументов.
Надёжная схема состоит из двух фаз:
- Планирование: агент формирует структурированный план без доступа к изменяющим инструментам.
- Исполнение: отдельный исполнитель валидирует утверждённый план и только затем вызывает разрешённые операции.
Шаг 1. Опишите действия как данные
Не позволяйте планировщику произвольно вызывать функции интеграций. Сначала договоритесь о небольшом формате плана. Ниже — пример, а не описание конкретного API:
{
"plan_id": "preview-local-001",
"mode": "dry-run",
"actions": [
{
"type": "document.create",
"target": "team-folder",
"input": {
"title": "Черновик отчёта",
"body": "Пример содержимого"
}
},
{
"type": "message.send",
"target": "review-channel",
"input": {
"text": "Черновик готов к проверке"
}
}
]
}
Поля должны описывать намерение, но не содержать секретов. Не добавляйте токены, пароли и заголовки авторизации в план, журнал или экран предварительного просмотра. Учётные данные остаются внутри слоя интеграции.
Для каждого типа действия заранее задайте допустимые поля. Не принимайте от модели URL, имя метода API или произвольное тело запроса, если исполнитель может собрать их сам.
Шаг 2. Разделите планировщик и исполнитель
Планировщик получает задачу и возвращает только план. Исполнитель не интерпретирует исходную просьбу пользователя и не дописывает новые действия: его входом служит уже проверенный объект.
async function preparePlan(task, planner) {
const plan = await planner.createPlan(task);
validatePlan(plan);
return {
...plan,
mode: "dry-run",
executable: false
};
}
async function executePlan(plan, integrations, approval) {
validatePlan(plan);
if (plan.mode !== "execute") {
return {
status: "preview",
executed: [],
actions: redactForPreview(plan.actions)
};
}
if (!approval || approval.planId !== plan.plan_id) {
throw new Error("Исполнение не подтверждено");
}
const results = [];
for (const action of plan.actions) {
assertAllowed(action);
results.push(await runAllowedAction(action, integrations));
}
return { status: "executed", results };
}
Обратите внимание: ветка dry-run возвращает описание действий до обращения к runAllowedAction. Это важнее названия флага. Если код сначала вызывает интеграцию, а затем помечает ответ как предварительный, никакого безопасного режима нет.
Шаг 3. Добавьте белый список и ограничения
Даже подтверждённый план не должен означать безусловное исполнение. Проверяйте тип операции, цель, размер данных и количество действий.
const policy = {
allowedActions: [
"document.create",
"message.send",
"status.update"
],
allowedTargets: [
"team-folder",
"review-channel",
"demo-board"
],
limits: {
maxActions: 5,
maxMessageLength: 1000
}
};
function assertAllowed(action) {
if (!policy.allowedActions.includes(action.type)) {
throw new Error("Тип действия запрещён");
}
if (!policy.allowedTargets.includes(action.target)) {
throw new Error("Цель действия запрещена");
}
if (
action.type === "message.send" &&
action.input.text.length > policy.limits.maxMessageLength
) {
throw new Error("Сообщение превышает лимит");
}
}
Белый список лучше запретительного списка: новая неизвестная операция будет отклонена по умолчанию. Для удаления данных, массовой рассылки, платежей и изменения прав доступа полезно предусмотреть отдельный класс разрешений или полностью исключить такие действия из автоматического исполнения.
Шаг 4. Сделайте подтверждение привязанным к плану
Кнопка «Выполнить» не должна просто менять глобальный флаг. Между просмотром и запуском план мог измениться. Рассчитайте отпечаток нормализованного плана и включите его в подтверждение.
approval = {
"plan_id": "preview-local-001",
"plan_hash": "sha256:результат-локального-расчёта",
"approved_action_count": 2
}
При исполнении заново вычислите отпечаток и сравните его с подтверждённым. Значение выше намеренно условное: реальный хеш должен рассчитываться приложением из конкретного плана. Если план изменился, покажите новый предварительный просмотр и запросите повторное подтверждение.
Шаг 5. Запускайте безопасно
Для локального примера режим удобно задавать аргументом командной строки. Команда предварительного просмотра не должна требовать рабочих учётных данных:
node agent-runner.js plan --task-file ./examples/task.json --dry-run
Фактическое исполнение оформите отдельной командой, принимающей сохранённый план и подтверждение:
node agent-runner.js execute \
--plan-file ./tmp/approved-plan.json \
--approval-file ./tmp/approval.json
Эти команды — шаблон интерфейса для собственного проекта. Они не предполагают существования пакета или внешнего сервиса. В рабочей реализации режим по умолчанию должен быть dry-run; переход в execute происходит только через явную команду или подтверждение.
Проверка результата
Проверьте не только текст ответа агента, но и границу интеграций. Для воспроизводимой проверки замените реальные адаптеры записывающими заглушками:
function createRecordingIntegrations() {
const calls = [];
return {
calls,
document: {
create: async (input) => {
calls.push({ operation: "document.create", input });
return { simulated: true };
}
}
};
}
const integrations = createRecordingIntegrations();
const result = await executePlan(dryRunPlan, integrations, null);
if (result.status !== "preview") {
throw new Error("Ожидался предварительный просмотр");
}
if (integrations.calls.length !== 0) {
throw new Error("В dry run была вызвана изменяющая интеграция");
}
Это пример проверочного сценария, а не утверждение о готовом тесте в вашем проекте. Для своей реализации подтвердите четыре свойства:
- план содержит все предполагаемые операции и понятные цели;
- в режиме
dry-runсчётчик вызовов изменяющих адаптеров равен нулю; - план без совпадающего подтверждения не исполняется;
- неизвестное действие или цель отклоняются до обращения к интеграции.
Если интеграция поддерживает собственный тестовый режим, используйте его как дополнительный слой, но не вместо локальной блокировки. Внешняя «песочница» всё равно может создавать тестовые записи или отправлять сообщения тестовым получателям.
Типовые ошибки
- Dry run существует только в промпте
- Исполнитель обязан технически блокировать изменяющие вызовы. Поведение нельзя доверять только текстовой инструкции модели.
- Один и тот же метод и планирует, и исполняет
- Так трудно доказать отсутствие побочных эффектов. Возвращайте план из одной функции и передавайте его другой.
- Предпросмотр скрывает важные параметры
- Пользователь должен видеть цель, тип действия и итоговое содержимое. Скрывайте секреты, но не последствия операции.
- Подтверждение не связано с версией плана
- Проверяйте идентификатор и хеш. Старое подтверждение не должно подходить изменённому плану.
- Чтение ошибочно считается всегда безопасным
- Читающие запросы могут раскрывать персональные или закрытые данные. Для них также нужны разрешения, минимальный объём ответа и безопасное журналирование.
- Повторный запуск дублирует действие
- Передавайте идемпотентный ключ там, где интеграция это поддерживает, и храните локальный статус исполнения каждого пункта плана.
Ограничения подхода
Предварительный просмотр показывает намерение системы, но не гарантирует, что внешнее состояние останется прежним до исполнения. Документ могут удалить, права — изменить, а канал — переименовать. Перед фактическим вызовом повторно проверяйте доступность цели и ограничения политики.
Dry run также не моделирует все ошибки внешнего API: лимиты, сетевые сбои и частичное исполнение проявятся только при реальном запросе. Поэтому исполнитель должен сохранять результат каждого действия, прекращать зависимые шаги после ошибки и не обещать автоматический откат там, где интеграция его не поддерживает.
Наконец, предварительный просмотр не заменяет минимальные права. Агенту следует выдавать только те разрешения, которые нужны для утверждённых операций, даже если режим dry-run включён по умолчанию.
Итоговая схема
- Планировщик создаёт структурированный план без изменяющих инструментов.
- Валидатор проверяет схему, белый список целей и лимиты.
- Интерфейс показывает очищенный от секретов предварительный просмотр.
- Пользователь подтверждает конкретную версию плана.
- Исполнитель повторно проверяет план и вызывает только разрешённые адаптеры.
- Журнал фиксирует результат каждого действия без секретов и лишних данных.
Главный критерий готовности прост: при dry-run изменяющие адаптеры не вызываются вообще. Если это свойство проверяется на границе интеграций, тестировать логику агента на реалистичных данных становится заметно безопаснее.