Первая часть цикла показала, как собрать read-only агента внутри Джеймикс-приложения: пользователь задаёт вопрос на естественном языке, ChatClient из Spring ИИ запускает agent loop и дёргает @Tool-методы до тех пор, пока не наберёт достаточно данных для ответа. Вторая часть делает шаг, который превращает демо в рабочий инструмент: агент получает право менять данные.
Для этого к доменной модели добавлена сущность ReplenishmentRequest — заявка на пополнение склада с полями товара, склада-получателя, количества, статуса и автора. К набору tools добавлены два write-метода: reserveStock увеличивает поле StockItem.reserved, createReplenishmentRequest создаёт новую заявку. Тестовый сценарий намеренно выбран с граничным случаем: пользователь просит зарезервировать двенадцать мешков кофе Ethiopia Yirgacheffe на складе в Гамбурге, хотя в наличии только восемь. Агент должен заметить несоответствие, зарезервировать доступный остаток и вернуть человекочитаемое объяснение — без участия разработчика в логике принятия решения.
Ключевой вопрос, который возникает при переходе к записи, — под каким пользователем выполняется tool. В Джеймикс аудит-аннотации @CreatedBy и @CreatedDate заполняются автоматически из SecurityContext. Проблема в том, что agent loop по умолчанию работает в фоновом потоке, где этого контекста нет. Если не решить задачу явно, поле createdBy останется пустым, и аудиторский след оборвётся. Авторы статьи разбирают, как пробросить аутентификацию в поток агента.
Помимо аутентификации, в сущность добавлен отдельный булев флаг initiatedByAgent. Это не замена createdBy, а дополнение: аудитор должен видеть одновременно и пользователя, который инициировал диалог, и факт того, что запись пришла не из формы, а из tool-вызова языковой модели. Такое разделение важно при разборе инцидентов — когда нужно понять, был ли это человеческий ввод или автономное решение агента.
Валидация входных данных в write-tools устроена намеренно жёстко, потому что данные приходят не от пользователя напрямую, а от модели. Для reserveStock отрицательное или нулевое quantity возвращает ошибку без исключения — агент получает структурированный ReserveResult и может принять решение дальше. Для createReplenishmentRequest введён жёсткий лимит в 1000 единиц на заявку и обрезка строки reason до 512 символов. Статус заявки хранится в базе как String, но наружу выставлен через enum ReplenishmentStatus с методами getId() и fromId() — чтобы остальной код не работал с сырыми строками и не зависел от того, что именно модель решит передать в поле.
Оба write-метода аннотированы @Transactional. Это означает, что частичное резервирование — когда доступно меньше запрошенного — либо фиксируется целиком, либо откатывается, без промежуточных состояний. Агент получает в ответе фактически зарезервированное количество и примечание о разнице, после чего самостоятельно решает, создавать ли заявку на недостающий объём или вернуть пользователю объяснение.
Авторы подчёркивают: описанные решения — контекст безопасности в потоке агента, аудит-флаг, валидация на стороне tool, явные транзакционные границы — это именно то, что отличает прототип от приложения, которое можно показывать заказчику. Spring ИИ и большинство туториалов по агентам эти вопросы не поднимают, потому что в read-only сценарии они просто не возникают. Полный исходник доступен для клонирования и запуска.


