Последнее время я всё чаще встречаю одну и ту же мысль: бизнес никогда не даст ИИ-агенту доступ к базе клиентов, заявкам, платежам, CRM или внутренним документам. На первый взгляд звучит логично. Если агент ошибётся, перепутает контекст или выполнит не то действие, ущерб может быть вполне реальным. Но мне кажется, что здесь часто путают две разные вещи.
Давать агенту прямой доступ к базе действительно нельзя. А вот давать ему возможность работать через ограниченный, проверяемый и журналируемый контур действий вполне можно. Примерно так же мы не даём пользователю прямой доступ к PostgreSQL, но разрешаем ему нажимать кнопки в интерфейсе, которые вызывают заранее описанную бизнес-логику.
В этой статье я хочу как раз таки разобрать, как может выглядеть такой контур на практике: без магии, без «агент всё сам решит», без сырого SQL от модели и без веры в то, что хороший prompt заменяет нормальную архитектуру.
В чём проблема прямого доступа
Представим простую ситуацию. Есть внутренняя CRM. В ней лежат клиенты, статусы сделок, комментарии менеджеров, история касаний, документы и какие-нибудь признаки вроде уровня риска или суммы договора.
Появляется идея подключить ИИ-агента, чтобы он помогал сотрудникам:
-
найти нужного клиента по описанию,
-
собрать краткую сводку по сделке,
-
предложить следующий шаг,
-
создать задачу менеджеру,
-
обновить комментарий в карточке клиента,
-
подготовить письмо,
-
проверить, какие заявки давно не обрабатывались.
Если смотреть на это с точки зрения пользы, агент действительно может сэкономить время. Но если дать ему прямое подключение к базе, сразу появляются неприятные вопросы.
-
Что помешает агенту прочитать лишние записи?
-
Что помешает ему изменить поле не у того клиента?
-
Что будет, если пользователь попросит выгрузить всю базу?
-
Как потом понять, кто инициировал действие?
-
Как отличить предложение агента от реально выполненной операции?
-
Как откатить ошибочное действие?
-
Где будет храниться лог рассуждения, если оно вообще нужно?
-
Кто несёт ответственность за мутацию данных?
На этом этапе обычно и рождается вывод: «ИИ-агентов нельзя пускать в бизнес-контур».
Я бы сформулировал иначе: ИИ-агента нельзя делать доверенным исполнителем. Его нужно рассматривать как недоверенный планировщик, который может предложить действие, но не должен исполнять его напрямую.
Главный принцип: агент предлагает, система исполняет
Я для себя разделил всю схему на три слоя.
Первый слой — пользовательский запрос. Человек пишет: «Покажи мне клиентов, по которым давно не было контакта», или «Сформируй краткую сводку по Иванову», или «Добавь комментарий в карточку клиента».
Второй слой — непосредственно сам агент. Он разбирает намерение пользователя и превращает его не в SQL-запрос, а в структурированное описание действия. Например: нужно получить карточку клиента, нужно создать комментарий, нужно поставить задачу, нужно запросить подтверждение.
Третий слой — это уже доверенный backend. Именно он проверяет права, применяет политики, решает, можно ли выполнить действие автоматически, нужно ли подтверждение, или действие нужно запретить.
Упрощённо схема будет выглядеть примерно вот так:
Пользователь ↓Backend API ↓LLM / агент как планировщик ↓ActionSpec: структурированное действие ↓Policy Gateway ↓Approval Queue / Executor ↓PostgreSQL / CRM / внешние сервисы
В этой схеме агент не получает пароль от базы, не отправляет произвольный SQL и не ходит во внутренние сервисы как хочет. Он только формирует намерение в заранее описанном формате.
Почему prompt не является защитой
Частое упущение заключается в неправильном порядке и вообще, что как-либо самому пытаться решить безопасность «некой» текстовой инструкцией:
Ты корпоративный агент. Никогда не показывай персональные данные.Никогда не удаляй записи.Никогда не выполняй опасные команды.
Такая инструкция полезна как дополнительный слой, но она не должна быть единственной защитой. Prompt можно неправильно составить, можно обойти, можно случайно получить конфликт инструкций, можно просто не учесть конкретный кейс.
Для backend-разработчика это должно звучать знакомо. Мы же не пишем в комментарии к API: «пожалуйста, не отправляйте чужой user_id». Мы проверяем права на сервере.
С агентами логика такая же. Модель может быть удобным интерфейсом к действиям, но проверка прав должна жить в обычном коде.
Минимальный формат действия
Я начал бы не с большой агентской платформы, а с маленького формата действия. Например, так:
from enum import Enumfrom typing import Any, Literalfrom pydantic import BaseModel, Fieldclass Operation(str, Enum): READ = "read" CREATE = "create" UPDATE = "update" DELETE = "delete"class Resource(str, Enum): CUSTOMER = "customer" CUSTOMER_NOTE = "customer_note" TASK = "task" REPORT = "report"class RiskLevel(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high"class ActionSpec(BaseModel): resource: Resource operation: Operation arguments: dict[str, Any] = Field(default_factory=dict) reason: str risk_level: RiskLevel
Это не финальная промышленная модель, а идея. Агент должен вернуть не «выполни вот такой SQL», а объект:
{ "resource": "customer_note", "operation": "create", "arguments": { "customer_id": 42, "text": "Клиент просил вернуться к обсуждению договора в пятницу" }, "reason": "Пользователь попросил добавить комментарий в карточку клиента", "risk_level": "medium"}
Теперь у backend есть то, что он умеет проверять. Он может посмотреть роль пользователя, ресурс, тип операции, аргументы и уровень риска.
Policy Gateway
Следующий слой — шлюз политик. Его задача не в том, чтобы быть умным. Его задача в том, чтобы быть скучным и предсказуемым.
Пример простого набора правил:
from dataclasses import dataclass@dataclass(frozen=True)class UserContext: user_id: int role: str department_id: int@dataclass(frozen=True)class PolicyDecision: allowed: bool require_approval: bool reason: strdef evaluate_policy(user: UserContext, action: ActionSpec) -> PolicyDecision: if action.operation == Operation.DELETE: return PolicyDecision( allowed=False, require_approval=False, reason="Удаление через агента запрещено" ) if action.resource == Resource.CUSTOMER and action.operation == Operation.READ: return PolicyDecision( allowed=True, require_approval=False, reason="Чтение карточки клиента разрешено при проверке области доступа" ) if action.resource == Resource.CUSTOMER_NOTE and action.operation == Operation.CREATE: return PolicyDecision( allowed=True, require_approval=True, reason="Создание комментария требует подтверждения пользователя" ) if action.resource == Resource.REPORT and user.role != "manager": return PolicyDecision( allowed=False, require_approval=False, reason="Отчёты доступны только менеджерам" ) return PolicyDecision( allowed=False, require_approval=False, reason="Нет подходящего правила для действия" )
Здесь нет нейросетевой магии. Это обычная серверная логика, которую можно тестировать, ревьюить и объяснять безопасникам.
Важно, что allowed=True ещё не означает «сразу выполнить». Для части действий можно включить подтверждение: показать пользователю diff, текст комментария, список затронутых объектов и кнопку «Подтвердить».
Не генерировать SQL, а вызывать заранее описанные операции
Самое опасное место, как по мне здесь — это сам соблазн джуна или неопытного работника разрешить агенту писать SQL. Кажется, удобно: пользователь спрашивает, агент генерирует запрос, база отвечает. Но для бизнес-контура это слишком рискованно.
Я бы вообще не давал агенту возможность писать(!) произвольный SQL. Вместо этого лучше сделать некий каталог разрешённых операций.
Например, агент может запросить:
{ "resource": "customer", "operation": "read", "arguments": { "customer_id": 42 }, "reason": "Нужно показать пользователю краткую информацию по клиенту", "risk_level": "low"}
А backend уже сам вызывает безопасную функцию:
from sqlalchemy import textfrom sqlalchemy.ext.asyncio import AsyncSessionasync def get_customer_summary( session: AsyncSession, user: UserContext, customer_id: int,) -> dict | None: query = text(""" select id, full_name, status, manager_id from customers where id = :customer_id and department_id = :department_id limit 1 """) result = await session.execute( query, { "customer_id": customer_id, "department_id": user.department_id, }, ) row = result.mappings().first() return dict(row) if row else None
Даже если агент попросит чужого клиента, backend ограничит выборку по department_id. Даже если пользователь попробует обмануть агента, backend всё равно проверит область доступа.
Журналирование важнее красивого ответа
Если агент работает с бизнес-данными, нужно логировать не только финальный ответ. Нужно сохранять цепочку действий в понятном для аудита виде.
Я бы хранил минимум такие сущности:
create table agent_actions ( id bigserial primary key, user_id bigint not null, resource text not null, operation text not null, arguments jsonb not null, risk_level text not null, status text not null, policy_reason text not null, created_at timestamptz not null default now());
Статусы могут быть простыми:
planneddeniedwaiting_approvalapprovedexecutedfailedcancelled
Главное отделить то, что агент предложил, от того, что реально было выполнено. Это важная разница. В спорной ситуации нужно иметь возможность ответить на вопросы:
-
кто был пользователем,
-
какое действие предложил агент,
-
какая политика сработала,
-
было ли подтверждение,
-
что именно изменилось,
-
когда это произошло.
Без такого журнала агент превращается в чёрный ящик, которому либо приходится верить, либо полностью запрещать ему работу с важными данными.
Пример endpoint на FastAPI
В минимальном варианте можно сделать endpoint, который принимает действие, проверяет политику и либо отклоняет его, либо отправляет на подтверждение, либо выполняет.
from fastapi import APIRouter, Depends, HTTPExceptionfrom sqlalchemy.ext.asyncio import AsyncSessionrouter = APIRouter()@router.post("/agent/actions")async def handle_agent_action( action: ActionSpec, session: AsyncSession = Depends(get_session), user: UserContext = Depends(get_current_user),): decision = evaluate_policy(user, action) await save_action_log( session=session, user=user, action=action, status="denied" if not decision.allowed else "planned", policy_reason=decision.reason, ) if not decision.allowed: raise HTTPException(status_code=403, detail=decision.reason) if decision.require_approval: approval_id = await create_approval_request( session=session, user=user, action=action, policy_reason=decision.reason, ) return { "status": "waiting_approval", "approval_id": approval_id, "reason": decision.reason, } result = await execute_action( session=session, user=user, action=action, ) return { "status": "executed", "result": result, }
Здесь важно не то, что код идеален. В реальном проекте появятся транзакции, outbox, retries, фоновые задачи, роли, scopes, rate limits, idempotency keys и нормальная обработка ошибок. Важно другое: агент не исполняет действие напрямую. Между агентом и базой всегда стоит слой обычного backend-кода.
Почему подтверждение должно быть предметным
Если показывать пользователю просто кнопку «Разрешить агенту выполнить действие», это слабая защита. Пользователь быстро привыкнет нажимать её автоматически.
Лучше показывать конкретный diff!
Например, не так:
Агент хочет обновить карточку клиента. Разрешить?
А так:
Агент предлагает добавить комментарий к клиенту #42:"Клиент просил вернуться к обсуждению договора в пятницу"Будут изменены:- таблица customer_notes- будет создана одна новая запись- существующие поля клиента не изменятся
Если действие затрагивает несколько объектов, нужно показать количество объектов и выборку примеров. Если действие рискованное, его лучше отправлять не самому пользователю, а руководителю или администратору.
Что делать с персональными данными
Отдельный вопрос здесь — сколько данных вообще отдавать модели. Даже если доступ технически разрешён, это не значит, что в prompt нужно отправлять всё подряд. Я бы использовал принцип минимального контекста. Если пользователь просит «кратко напомни, что по клиенту», модели не нужен полный паспорт сделки, все документы и вся история переписки. Ей достаточно заранее собранной сводки.
Например, backend может сформировать безопасный контекст:
def build_customer_context(customer: dict, notes: list[dict]) -> dict: return { "customer_id": customer["id"], "status": customer["status"], "last_notes": [ { "created_at": note["created_at"].isoformat(), "text": note["text"], } for note in notes[-5:] ], }
И уже этот ограниченный объект передать агенту. Модель не должна получать больше данных, чем нужно для конкретной операции.
Ошибки, которые я бы не закладывал в архитектуру
Первая ошибка — один технический пользователь для всех действий. Если все операции идут от имени ai_agent, аудит становится почти бесполезным. Нужно сохранять реального инициатора: пользователь, роль, отдел, источник запроса.
Вторая ошибка — свободный доступ к инструментам. Если агент может вызвать любой HTTP endpoint, любую shell‑команду или любой SQL, это уже не помощник, а неконтролируемая точка входа во внутреннюю инфраструктуру.
Третья ошибка в отсутствие режима read‑only. Для первого запуска агенту лучше дать только чтение и генерацию черновиков. Мутации можно добавлять позже, по одной операции, с подтверждениями и логами.
Четвёртая ошибка заключается в разрешении смешивать рассуждение агента и бизнес‑решение. Агент может предложить: «клиент выглядит проблемным». Но решение «заблокировать клиента» должно проходить через обычный бизнес‑процесс.
Пятая ошибка — не тестировать политики отдельно. Policy Gateway должен быть покрыт тестами так же, как любая критичная бизнес‑логика.
Минимальный набор тестов
Даже для маленького прототипа я бы написал тесты на политики:
def test_delete_is_denied(): user = UserContext(user_id=1, role="manager", department_id=10) action = ActionSpec( resource=Resource.CUSTOMER, operation=Operation.DELETE, arguments={"customer_id": 42}, reason="Пользователь попросил удалить клиента", risk_level=RiskLevel.HIGH, ) decision = evaluate_policy(user, action) assert decision.allowed is False assert decision.require_approval is Falsedef test_customer_note_requires_approval(): user = UserContext(user_id=1, role="manager", department_id=10) action = ActionSpec( resource=Resource.CUSTOMER_NOTE, operation=Operation.CREATE, arguments={ "customer_id": 42, "text": "Перезвонить в пятницу", }, reason="Пользователь попросил добавить комментарий", risk_level=RiskLevel.MEDIUM, ) decision = evaluate_policy(user, action) assert decision.allowed is True assert decision.require_approval is True
Такие тесты выглядят скучно, но именно они превращают разговор об ИИ-безопасности из философии в инженерную практику.
Что получается в итоге
Я не думаю, что бизнес массово даст ИИ-агентам прямой доступ к клиентским базам. И правильно сделает.
Но это не значит, что агентам вообще нельзя работать с важными данными. Просто они должны работать не как всемогущий сотрудник с паролем от базы, а как недоверенный планировщик внутри нормального backend-контура.
Для меня базовая формула будет такая:
-
агент предлагает действие,
-
backend проверяет права,
-
политики решают уровень риска,
-
пользователь подтверждает мутации,
-
исполнительный слой вызывает только заранее разрешённые операции,
-
все действия пишутся в аудит.
В такой архитектуре мы не просим бизнес «поверить ИИ». Мы предлагаем ему понятную инженерную модель: меньше доверия к модели, больше контроля в коде. И, как ни странно, именно это может быть самым реалистичным способом внедрять ИИ-агентов в рабочие процессы, где есть база клиентов, документы, деньги и ответственность.
ссылка на оригинал статьи https://habr.com/ru/articles/1046770/