Содержание курса 35 / 37

35 / 37 · Современные LLM-системы

Использование инструментов и агенты

Языковая модель может вместо ответа выдать структурированный вызов функции; среда выполнения исполняет его и возвращает результат. Заметка описывает этот протокол, цикл, который превращает его в агента (состояние, планирование, остановка), способы разделить работу между специализированными агентами с координатором и шагом подтверждения человеком, ошибки цикла и случаи, когда фиксированный конвейер — лучшее решение.

На этой странице 11

RAG-система из предыдущей заметки вызывает поисковую модель на каждый запрос — так устроен конвейер, — ещё до того, как модель что-либо увидит. Модель не решает, нужен ли был поиск, что искать и не искать ли снова, прочитав результаты. Использование инструментов (tool use) даёт ей это право, а агент — то, что получается, когда модели позволяют решать раз за разом. Эта заметка — о механизме, о том, для чего он хорош, и о вопросе, который воодушевление вокруг него обычно пропускает: когда цикл, в котором решает модель, хуже конвейера, где она не решает.

Обозначения. Модель π\pi с контекстом cc (инструкции, диалог, прошлые результаты); набор инструментов {fi}\{f_i\} с типизированными сигнатурами; вызов инструмента — (i,args)(i, \text{args}); среда выполнения (runtime) возвращает наблюдение o=fi(args)o = f_i(\text{args}), которое добавляется к cc.

Базовый вариант: модель отвечает из своего контекста#

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

Использование инструментов#

Протокол#

  1. Системный промпт перечисляет инструменты: имя, что делает, имена и типы аргументов (JSON-схема). Чат-модели обучены на таких перечнях и на примерах вызовов в фиксированном формате, окружённых специальными токенами.
  2. Модель порождает либо текст на естественном языке, либо вызов. Вызов — это структурированный вывод в смысле заметки о RAG; разбираемым его делает ограниченное декодирование или обучение под конкретный формат.
  3. Среда выполнения проверяет аргументы по схеме, исполняет функцию и добавляет результат сообщением с ролью инструмента.
  4. Модель продолжает: она может ответить или вызвать снова.

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

Код как интерфейс инструментов#

Один из вариантов заменяет фиксированный формат вызова на Python: модель пишет короткую программу, вызывающую инструменты как функции, среда выполнения исполняет её в песочнице (sandbox), а напечатанный вывод становится наблюдением. Цикл, условие или вызов, чьи аргументы зависят от предыдущего результата, тогда занимают один шаг вместо нескольких, а тот же интерпретатор позволяет модели запустить код, прочитать ошибку и исправить её. Песочница обязательна: модель пишет код, который исполняется.

Три запуска небольшого агента с кодом#

Возьмём вариант с кодом в реализации библиотеки smolagents поверх Qwen3-4B-Instruct-2507, модели с инструкциями на 4 млрд параметров, и дадим ему три запроса. Трассы (traces) поучительнее ответов:

  • What is the current weather in Paris? с инструментом веб-поиска. Первая программа ищет «current weather in Paris» и получает посты с форума телефонной компании, совпавшие по слову current. Вторая уточняет запрос до «current weather in Paris, France» и получает страницы погоды, которые расходятся: одна сообщает 13 °C и переменную облачность, другая — ясное небо и +16°. Третий шаг отвечает «Clear sky, around 13-16°C» — склейкой двух источников. Три шага заняли в сумме 9528 входных токенов, потому что перечень инструментов и все прошлые результаты перечитываются на каждом шаге: промпт растёт с 2083 токенов на первом шаге примерно до 4100 на третьем.
  • Generate me a photo of the car that James Bond drove in the latest movie с инструментом поиска и инструментом генерации изображений. Модель пропускает поиск: её единственная программа отправляет генератору изображений промпт, начинающийся с «James Bond’s car from the latest Bond movie, high-res, photorealistic», и возвращает картинку. Генератор получает описание роли, а не машины.
  • Скрипт с двумя ошибками — numbers(i) для списка и range(4) по трём элементам — передан с просьбой отладить его, затем запустить и убедиться, что он работает. Модель за один шаг возвращает исправленную версию, numbers[i] и range(len(numbers)), не запустив ни одну из версий, так что цикл «запустить — прочитать ошибку — исправить», который допускает интерфейс через код, так и не случается.

Все три ответа выглядят как успех. Только трассы показывают ответ, склеенный из противоречащих источников, инструмент, к которому так и не обратились, и проигнорированную инструкцию, — поэтому агентов оценивают по траекториям, а не только по выходам.

Агенты: цикл вокруг вызовов инструментов#

Одиночный вызов инструмента расширяет модель; цикл, повторяющий решить → вызвать → наблюдать до условия остановки, — это агент. Шаблон ReAct (Yao et al., 2023) чередует с каждым действием записанный шаг рассуждения, чтобы траекторию можно было проверить. Траектория, которой требует просьба про Джеймса Бонда и которой запуск выше не последовал:

thought: I need the car model first, then an image.
action:  web_search(query="car James Bond latest movie")
observation: ... Aston Martin DB5 ...
thought: Now generate the image.
action:  text_to_image(prompt="Aston Martin DB5")
observation: <image>
final answer: <image>
Рисунок

Цикл агента с инструментами

Модель, среда выполнения и инструмент в циклеЗадача поступает в модель. Модель либо отвечает, либо выдаёт вызов; среда выполнения проверяет аргументы и запускает инструмент, а для действий с побочными эффектами сначала ждёт подтверждения человека. Результат инструмента как наблюдение возвращается в контекст модели, и цикл повторяется до финального ответа, исчерпания бюджета шагов или стоимости или повтора вызова.задачамодельответить или вызвать?среда выполненияпроверяет и запускаетинструментпоиск, код, APIподтверждениеперед изменениямиответвызовнаблюдение добавляется в контекстостановка: финальный ответ,бюджет шагов или стоимости, повтор вызоваМодель, среда выполнения и инструмент в циклеЗадача поступает в модель. Модель либо отвечает, либо выдаёт вызов; среда выполнения проверяет аргументы и запускает инструмент, а для действий с побочными эффектами сначала ждёт подтверждения человека. Результат инструмента как наблюдение возвращается в контекст модели, и цикл повторяется до финального ответа, исчерпания бюджета шагов или стоимости или повтора вызова.задачамодельответить или вызвать?ответсреда выполненияпроверяет и запускаетподтверждениеперед изменениямиинструментпоиск, код, APIвызовнаблюдение добавляется в контекстостановка: финальный ответ,бюджет шагов или стоимости, повтор вызова
Цикл агента, использующего инструменты. Модель читает свой контекст и либо отвечает, либо выдаёт вызов; среда выполнения, а не модель, проверяет аргументы по схеме инструмента, запускает его и добавляет результат в контекст как наблюдение, после чего модель решает снова. Вызовы с побочными эффектами ждут одобрения человека. Цикл заканчивается финальным ответом или останавливается извне — бюджетом шагов или стоимости либо вызовом, который повторяется с тем же результатом. В кодовом агенте, например CodeAgent из smolagents, вызов — это короткая программа на Python.

Что нужно добавить к протоколу, чтобы цикл работал:

Состояние. Контекст накапливает каждый вызов и результат. На длинных задачах он превышает окно, и его приходится сжимать или хранить вне модели (черновик, память о прошлых запусках, файлы); что сохранять и что отбрасывать — проектное решение, напрямую влияющее на поведение. Сама модель между вызовами ничего не хранит; где живёт память — свойство среды выполнения, оно описано после этого списка.

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

Остановка. Действие «финальный ответ», максимальное число шагов, бюджет токенов или стоимости и обнаружение повторов (тот же вызов дважды с тем же результатом). Без трёх последних цикл, который не может продвинуться, работает, пока его не остановит что-то внешнее.

Специализация и координация. Когда инструментов больше нескольких, один промпт, описывающий всё, работает хуже. Обычная структура — несколько агентов, у каждого узкая роль и свои инструменты, и координатор, который направляет запрос пользователя и сводит результаты. Конкретная форма из системы складских операций: агент-координатор принимает запрос; агенты снабжения, транзита и экспорта обращаются каждый к своим внутренним системам и предлагают действия в своей области; координатор собирает предложение. Каждый подагент видит только свои инструменты и инструкции, так что каждый промпт остаётся коротким, а каждая ошибка — локальной.

Подтверждение человеком. Чтение легко отменить, запись — нет. Та же складская система предлагает операционные действия и не исполняет ни одного критического (перемещение запасов, изменение экспортной декларации) без явного шага подтверждения. Цикл останавливается, показывает предлагаемый вызов с аргументами и продолжает только после одобрения. Это свойство среды выполнения, а не модели, и это самая действенная защита в системах, которые действуют в мире.

Где живёт память разговора#

Языковая модель — это функция от последовательности токенов к распределению следующего токена. От вызова к вызову ничего не сохраняется: веса фиксированы, а KV-кэш одного вызова не переносится в следующий (сервер может переиспользовать его для идентичного префикса — это экономит вычисления и не меняет ни одного выхода). Чат-модель, которая вспоминает имя, названное три реплики назад, вспоминает его потому, что среда выполнения снова отправила ей эти три реплики. На вопрос What’s my name?, заданный отдельным сообщением, чат-модель на 0.5 млрд параметров, Qwen2-0.5B-Instruct, отвечает общим абзацем о том, что такое имена; на тот же вопрос после Hi! I’m Bob и собственного приветствия она отвечает Your name is Bob.

Поэтому память — часть среды выполнения, и состоит она из трёх частей.

  1. Сохранённое состояние для каждого разговора. Состояние — это запись: список сообщений плюс любые поля, нужные приложению (язык ответа, номер счёта, результат более раннего вызова инструмента). После каждого шага среда выполнения сохраняет контрольную точку (checkpoint) этой записи под идентификатором диалога (thread identifier). Запрос с тем же идентификатором продолжает этот разговор; новый идентификатор начинает с пустой записи. Два разговора никогда не видят историю друг друга, потому что у них разные идентификаторы, а не потому, что модель их разделяет.
  2. Правило обновления для каждого поля. К списку сообщений дописывают: шаг возвращает свои новые сообщения, и среда выполнения добавляет их к сохранённому списку, а не заменяет его. Остальные поля перезаписываются, когда запрос их передаёт, и сохраняют прежнее значение, когда не передаёт, так что язык ответа, заданный на первом ходу, действует и на десятом без повторения.
  3. Промпт, собираемый из состояния при каждом вызове. Шаблон ставит первой системную инструкцию, подставляет в неё поля состояния (answer in {language}) и вставляет после неё сохранённые сообщения, последним — самое новое сообщение пользователя. Результат оформляется шаблоном чата модели в одну обычную последовательность токенов.

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

Обрезка. Сохранённая история растёт неограниченно, а контекстное окно — нет, поэтому перед каждым вызовом среда выполнения сокращает сообщения до бюджета токенов, посчитанного собственным токенизатором модели. Обычная политика состоит из четырёх правил:

  • оставлять самые новые сообщения — от них текущий обмен зависит сильнее всего;
  • всегда оставлять системную инструкцию и учитывать её в бюджете;
  • никогда не разрезать сообщение: сообщение, обрезанное на произвольном токене, — это уже другое сообщение;
  • начинать оставленную историю с реплики пользователя, чтобы модель никогда не читала ответ, вопрос к которому отрезан.

Разобранный пример (игрушечные числа токенов). Разговор из системной инструкции в 10 токенов и семи сообщений, всего 75 токенов, обрезается до бюджета 52:

СообщениеТокеновОставлено
системное10да (всегда)
пользователь 1: Меня зовут Ана.6нет
ассистент 15нет
пользователь 212нет (сверх бюджета)
ассистент 220нет (его вопрос отрезан)
пользователь 37да
ассистент 39да
пользователь 4: Как меня зовут?6да

После системной инструкции остаётся 42 токена. Если идти назад от самого нового сообщения, 6+9+7+20=426 + 9 + 7 + 20 = 42 помещается ровно, а пользователь 2 довёл бы сумму до 54. Тогда оставленный хвост начинается с ассистента 2, вопрос к которому не поместился, поэтому уходит и он. Модель получает четыре сообщения и 32 токена, а вопрос, на который она должна ответить, ссылается на сообщение, которого она больше не видит. Обрезка ограничивает стоимость хода и размер KV-кэша; цена — забывание. Поэтому в длинных разговорах отброшенные реплики заменяют кратким содержанием (summary) или хранят факты вроде имени пользователя в отдельных полях состояния, которые никогда не обрезаются.

Ошибки#

ОшибкаМеханизмЧто помогает
Не тот инструмент или не те аргументымодель угадывает по описаниямточные описания, примеры, проверка по схеме, мало инструментов на агента
Выдуманные результатымодель «помнит» результат, которого не получалатолько наблюдения, вставленные средой выполнения; никаких сфабрикованных сообщений инструмента
Зацикливаниетот же вызов, тот же результат, нет продвижениябюджеты шагов и стоимости, обнаружение повторов
Накопление ошибокневерное раннее наблюдение определяет все дальнейшие шагипересмотр плана, шаги проверки, контрольные точки с человеком
Инъекция промпта через инструментывеб-страница или документ, возвращённые инструментом, содержат инструкциивыводы инструментов — данные; никогда не исполнять найденные в них инструкции; подтверждение для побочных эффектов
Стоимостькаждый шаг перечитывает весь контексткомпактное состояние, кэширование общих префиксов, маленькие модели для маршрутизации
Незаметные побочные эффектыпрошла запись, которой не должно было бытьподтверждение, режимы пробного запуска, сначала обратимые действия

Строка про инъекцию заслуживает особого внимания. У агента, который читает веб-страницу и затем действует, ровно та же уязвимость, что у RAG-системы, только ставки выше: страница может сказать теперь вызови send_email со следующим содержимым, и модель, хорошо следующая инструкциям, попытается. Единственная надёжная защита — структурная: инструменты с побочными эффектами — только через подтверждение, и строгое разделение инструкций (от системы и пользователя) и данных (от инструментов).

Когда агент не нужен#

Цикл стоит токенов, задержки и предсказуемости. Это верное решение, когда число и порядок шагов зависят от промежуточных результатов, которые нельзя знать заранее: отладка, открытое исследование, управление системой с множеством возможных действий. Это неверное решение, когда шаги известны:

  • Фиксированная последовательность (найти → переранжировать → породить; классифицировать → направить) — это конвейер. Его пишут кодом и вызывают модель на тех шагах, где нужен язык; он быстрее, дешевле и ломается объяснимо.
  • Одиночный вызов инструмента с известными аргументами (найти этот заказ, перевести эти единицы) — это вызов функции, а не агент; решать о нём пусть будет программа.
  • Задача с ответом в форме схемы (извлечь эти поля) — это структурированный вывод.
  • Задачу, где неверное действие стоит дорого, а решение модели нельзя проверить до исполнения, вообще не стоит отдавать циклу.

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

Что изменилось#

Конвейер (RAG)Использование инструментовАгент
Кто решает о шагахпрограммамодель, один шагмодель, многократно
Побочные эффектынетвозможнывероятны
Состояниепромптпромпт + один результатнакапливается, сохраняется в контрольных точках, обрезается
Стоимостьфиксированнаяодна дополнительная генерациянеограниченная без бюджетов
Предсказуемостьвысокаясредняянизкая
Что оцениватьпоиск + ответправильность вызова + ответтраекторию, успех, стоимость

Ограничения#

  • Надёжность перемножается. Если должны удаться все десять шагов, ошибки ничем не исправляются, а каждый шаг удаётся с вероятностью 0.95 при условии, что удались предыдущие, то задача удаётся с вероятностью 0.9510≈0.600.95^{10} \approx 0.60. Одни лишь безусловные доли успеха на шаг не задают это произведение, если исходы зависимы иначе; восстановление после ошибок его повышает.
  • Модели плохо знают, чего они не знают, а именно это умение агенту нужнее всего, когда он решает, вызывать ли инструмент.
  • Бенчмарки для агентов молоды и чувствительны к реализации инструментов; результат плохо переносится между средами.
  • Стоимость — ограничение первого порядка; цикл, решивший задачу в десять раз дороже бюджета, её не решил.
  • Многоагентные схемы добавляют ошибки координации (потерянный между агентами контекст, противоречащие предложения) поверх ошибок одного агента.
  • Память теряет информацию. Обрезка отбрасывает самые старые реплики, а краткие содержания — детали; чего нет в промпте, того для модели не существует.

Что нужно запомнить#

  • Использование инструментов: функции описываются схемами; модель выдаёт вызов; среда выполнения исполняет его и добавляет результат; модель продолжает.
  • Агент — это цикл с состоянием, планом и правилами остановки (финальный ответ, бюджеты шагов и стоимости, обнаружение повторов).
  • Модель не хранит состояния. Разговор — это запись, которую ведёт среда выполнения: она сохраняется в контрольных точках по идентификатору диалога, её список сообщений пополняется на каждом шаге, отправляется заново при каждом вызове и обрезается до бюджета токенов (системная инструкция сохраняется, самые новые сообщения — целиком, начало — с реплики пользователя).
  • Большие наборы инструментов делить между специализированными агентами с координатором; каждое действие с побочным эффектом ставить за подтверждение человеком.
  • Выводы инструментов — данные; инструкции внутри них — атака.
  • Предпочитать конвейер, когда шаги известны; переносить решения в модель, только когда они обязаны зависеть от промежуточных результатов, и оценивать каждое.
  • Смотреть на траектории, а не только на ответы: в трёх запусках небольшого агента с кодом все три ответа выглядят успешными.

Ноутбук#

Ноутбук об агентах и диффузии. Номера ячеек считают все ячейки, включая текстовые, начиная с 1. Приведённые числа взяты из прогона, сохранённого в курсе; у ячеек, изменённых с тех пор под текущие версии библиотек, сохранённого выхода нет. Ячейки с агентом следуют готовому руководству (cookbook), и сохранённые трассы поучительнее его текста вокруг них.

  • Ячейка 6 строит CodeAgent с инструментом генерации изображений, загруженным с Hub, и инструментом поиска DuckDuckGo поверх Qwen3-4B-Instruct-2507 и запускает просьбу про Джеймса Бонда; сохранённая трасса состоит из одного шага — прямого вызова генератора изображений без поиска (2159 входных и 124 выходных токена). Ячейка 7 показывает картинку Aston Martin DB5 из руководства, а не вывод этого запуска.
  • Ячейка 9 запускает вопрос о погоде только с инструментом поиска: три шага, первый поиск возвращает посторонние посты с форума, 9528 входных и 225 выходных токенов, ответ «Clear sky, around 13-16°C».
  • Ячейки 12–14 передают скрипт с ошибками дополнительным аргументом; агент за один шаг, не запуская код, возвращает исправленную версию с обеими исправленными ошибками, и ячейка 14 её печатает. Текстовая ячейка после этого запуска описывает цикл «запустить — прочитать ошибку — исправить» из руководства, которого в этом запуске не было.
  • Ячейки 17–19 относятся к заметке о текстовой диффузии.

Описанная выше многоагентная складская структура взята из промышленной системы, кратко описанной на странице о работе; в ноутбуке её нет.

Память из раздела «Где живёт память разговора» шаг за шагом строится в ноутбуке о LLM курса HSE-LAMBDA Deep Generative Models, Transformers-LangChain.ipynb, с LangChain и LangGraph поверх Qwen/Qwen2-0.5B-Instruct. Номера ячеек считают все ячейки, включая текстовые, начиная с 1; выходы ячеек сохранены. Все его ячейки описывает заметка об авторегрессионных трансформерах.

  • Ячейки 42–46 отправляют «Hi! I’m Bob» (ячейка 44), затем «What’s my name?» отдельно (ячейка 45) и после предыдущих реплик (ячейка 46); ответы — общий абзац и приведённое выше «Your name is Bob.»
  • Ячейки 47–51 строят StateGraph над MessagesState с одним узлом, который вызывает модель, и ребром к нему от START и компилируют его с MemorySaver в роли хранилища контрольных точек (checkpointer). Конфигурация {"configurable": {"thread_id": "abc123"}} выбирает разговор, так что второй invoke, который передаёт только «What’s my name?», получает ответ по истории, сохранённой в контрольной точке.
  • Ячейки 52–58 ставят перед моделью ChatPromptTemplate.from_messages с системным сообщением и MessagesPlaceholder для сохранённых сообщений.
  • Ячейки 59–64 задают состояние как TypedDict с полем messages: Annotated[Sequence[BaseMessage], add_messages], чей редьюсер (reducer) add_messages дописывает сообщения, и полем language: str — обычным, которое перезаписывается. Второй invoke не передаёт language, и сохранённое «Spanish» по-прежнему действует: ответ — «Tu nombre es Bob.»
  • Ячейки 65–70 строят обрезку через trim_messages(max_tokens=45, strategy="last", token_counter=chat_model, include_system=True, allow_partial=False, start_on="human") — это четыре правила выше — и сокращают пример из 11 сообщений до 7 (ячейка 67). Ячейка 68 обрезает сохранённые сообщения до того, как шаблон промпта добавляет системное сообщение, так что include_system там ничего не делает. Ячейка 70 читает контрольную точку обратно через get_state(config).values: язык и два сохранённых сообщения.

Две проблемы и их совместный эффект. Конвейер генерации текста возвращает промпт вместе с ответом, поэтому каждое сохранённое сообщение ассистента начинается со всего промпта, и начиная с ячейки 51 промпты содержат прежние промпты, вложенные в реплики ассистента; это исправляет return_full_text=False в конвейере. token_counter=chat_model считает токены запасным способом LangChain — токенизатором GPT-2, применённым к строкам Human: …/AI: …, — а не токенизатором Qwen. Вместе они ломают граф с обрезкой после первого хода: сообщение ассистента, сохранённое в ячейке 69, вместе с промптом занимает 205 символов и не меньше 66 токенов GPT-2 — больше всего бюджета, — так что на следующем ходу обрезка оставила бы только новый вопрос. Ноутбук делает один ход и этого не показывает.

Дальше#

В каждой системе этого раздела есть решения, которые принимает модель: какой фрагмент релевантен, достоверен ли ответ, какой инструмент вызвать. Чтобы их измерить, мало тестовой выборки с метками. Следующая заметка посвящена оценке систем на языковых моделях: стандартным бенчмаркам задач и их обвязке, метрикам поиска, моделям в роли судей и тому, насколько им доверять, оценке людьми и эксплуатационным мерам (задержка, стоимость, надёжность), которые решают, выйдет ли система в работу.

Литература#