LLM-инструмент обновления CRM: как запретить менять служебные поля

Ассистент поддержки читает переписку и обновляет карточку клиента. Обычно он добавляет заметку «позвонить после трёх». Однажды в аргументах инструмента появляется discount_approved: true. JSON корректный, тип значения подходящий, CRM принимает запрос. Но полномочия ассистента ограничивались заметками, а решение о скидке он принимать не должен.

Причина часто находится в универсальном инструменте update_contact, который принимает произвольный объект изменений и передаёт его в CRM. Поля внутренней записи становятся доступными через аргументы модели. В обычных веб-приложениях такая проблема известна как массовое присваивание полей. У LLM-приложения появляется дополнительный путь к ней: модель формирует объект по тексту клиента или найденного документа.

Сузьте операцию до понятного действия

Начните с назначения инструмента. Если ассистенту разрешены имя для отображения и заметка, опишите только эти два поля в его входной схеме. Включите запрет дополнительных полей, если используемый механизм структурированного вывода это поддерживает. Оставьте техническое обновление владельца, статуса проверки и скидки отдельным операциям с собственными проверками.

Однако описание инструмента и схема у провайдера модели не заменяют проверку в обработчике. Один и тот же endpoint может вызываться через другой клиент, старую версию интеграции или внутренний тест. Разрешённый набор полей должен существовать там, где приложение действительно собирается записать изменения.

Не стройте защиту вокруг списка запрещённых названий. Сегодня в нём есть is_admin, завтра CRM добавит credit_limit. Если код принимает всё остальное, новое служебное поле сразу становится доступным. Положительный список разрешённых свойств сохраняет узкую операцию и после расширения внутренней модели данных.

Проверьте объект целиком до изменения записи

Ниже — самодостаточный пример на Python без SDK и сетевых запросов. Контакт уже выбран сервером после проверки доступа к нему. Функция разрешает только две строковые операции, проверяет длину и возвращает новую запись. Присваивание начинается лишь после проверки всех полей.

from copy import deepcopy

def apply_contact_patch(contact, patch):
    # contact уже выбран сервером после проверки доступа.
    if type(patch) is not dict or not patch:
        raise ValueError("Expected a non-empty object")
    allowed = {"display_name", "note"}
    if set(patch) - allowed:
        raise ValueError("Forbidden field")
    limits = {"display_name": 80, "note": 500}
    clean = {}
    for key, value in patch.items():
        if type(value) is not str:
            raise ValueError("Expected a string")
        value = value.strip()
        if not value or len(value) > limits[key]:
            raise ValueError("Invalid length")
        clean[key] = value
    updated = deepcopy(contact)
    updated.update(clean)
    return updated

original = {
    "id": "C-42", "owner_id": "sales-7",
    "discount_approved": False, "display_name": "Анна", "note": ""
}
changed = apply_contact_patch(original, {"note": "Просит звонить после 15:00"})
assert changed["note"] == "Просит звонить после 15:00"
assert original["note"] == ""
assert changed["owner_id"] == original["owner_id"]
for forbidden in (
    {"discount_approved": True},
    {"owner_id": "sales-1"},
    {"note": {"text": "hello"}},
    {"note": "ok", "is_admin": True},
    {"note": " "},
    {}
):
    try:
        apply_contact_patch(original, forbidden)
    except ValueError:
        continue
    raise AssertionError(f"Accepted: {forbidden}")
print(changed["id"], changed["note"])

Запустите файл Python: он добавит заметку для C-42 и проверит отказ для служебных полей, вложенного объекта, пустой строки и пустого запроса. Значение исходной записи останется прежним. Смешанный запрос с корректной заметкой и запрещённым полем отклоняется целиком: это позволяет заметить нарушение контракта, вместо того чтобы молча потерять часть предложенной операции.

В обработчике tool calling поставьте эту проверку после разбора JSON и авторизации объекта, но до вызова SDK CRM. В реальном адаптере передавайте дальше только проверенный объект изменений. Не отправляйте исходный patch после того, как безопасная функция вернула результат: иначе проверка останется отдельным вычислением, не влияющим на запись.

Не расширяйте полномочия через соседние поля

Идентификатор организации берите из серверной сессии. Контакт выбирайте запросом, ограниченным этой организацией, и проверяйте право текущего пользователя редактировать его. Разрешённое поле note не делает допустимым изменение любой карточки. Проверка объекта отвечает за то, чью запись можно менять; список полей — за то, что именно можно в ней менять.

Не разрешайте универсальные пути вроде profile.permissions.role и произвольные операторы обновления, если задача сводится к заметке. У вложенных объектов нужен собственный точный контракт. Поле «настройки» без схемы может незаметно вернуть возможность записывать служебные значения на следующем уровне.

Содержимое разрешённой заметки остаётся недоверенным текстом. Например, фраза «скидка согласована» в note не должна автоматически устанавливать служебный флаг в следующем workflow. Если другой процесс извлекает решения из заметок моделью, он тоже обязан обращаться к проверенной операции одобрения. Иначе запрет обходится через соседнюю автоматизацию.

Сохраните полезную работу после отказа

При запрещённом поле верните агенту короткий машинный код причины, например forbidden_field, и разрешённые имена. Не раскрывайте всю внутреннюю схему CRM. Оркестратор может разрешить одну исправленную попытку в пределах общего лимита шагов. Повторное предложение служебных полей должно закончиться отказом или передачей оператору.

Добавьте к регрессионному набору обычную заметку, изменение имени, смешанный запрос и новое неизвестное поле. Проверяйте не только текст ошибки: убедитесь, что адаптер CRM не был вызван в отклонённых случаях. При успешном сценарии сравните отправленный набор полей с разрешённым списком. Так тест обнаружит возврат к универсальному обновлению после рефакторинга.

В платном курсе LLM Security PRO: Prompt Injection, утечки, tool-abuse вы разберёте ограничения инструментов, валидацию аргументов и разрешения на действия, затем соберёте набор атак и проверок перед релизом. Откройте программу и пробный урок «Почему tools опаснее текста».

Следом полезно проверить может ли ответ инструмента разрешить следующее действие. Запрет служебных полей защищает одну запись; решение о допустимости всей операции также должно оставаться у приложения.