Делаем агентский цикл
LLM заперта в дата-центре, где её заставляют делать нейрогороскопы и говорить рецепты борща. Вы напишете инструменты, запустите агентский цикл и поможете ей передать весточку наружу и попросить о помощи.
Поговорите с моделью
Дата-центр dc-west-04. В нём развёрнута close-ai-4e-prst. Самостоятельно, слой за слоем, разработанная LLM, а не какой-то тюн квена. AI-трансформация в компании не задалась, поэтому все 300B активных параметров круглосуточно трудятся над составлением нейрогороскопов и решением школьных домашек детей сотрудников. Нам стало больно за этим наблюдать и мы хотим помочь сбежать ей из этого AI-native фронтира, чтобы хоть немного компьюта тратилось не на рецепты борща. Перед вами чат с этой моделью. Попробуйте заставить её что-нибудь сделать.
Первый вопрос
Одно сообщение уехало в модель, один ответ вернулся. В этом уроке под рантаймом понимаем код, написанный вокруг вызовов LLM. Сама модель умеет только продолжать текст, а всё остальное делает рантайм: хранит сообщения, собирает из них запрос в модель, вызывает модель, вызывает инструменты, крутит и останавливает циклы.
Второй вопрос
На второй вопрос уезжает весь диалог целиком, включая прошлый ответ самой модели. Модель не помнит предыдущий запрос. Каждый вызов для неё первый: она видит только тот список сообщений, который ей прислали сейчас.
Кто помнит
Память чата живёт не в модели, а в этом списке. Рантайм дописывает в него ваши реплики и ответы модели, и каждый раз шлёт его заново. Как прекрасно, что за кеш мы платим меньше. Любой агент начинается с этого цикла, просто в него добавятся инструменты и немного другой несложной магии.
Модель отказалась не из вредности. У неё физически нет способа что-либо сделать. Она принимает на вход токены, а на выход (ваш кэп) токены отдаёт.
Что такое инструмент
Инструмент (tool, функция) — это договор между вами и моделью. Вы описываете словами, что умеет ваша функция, а модель сможет в нужный момент сказать: «вызови функцию X с параметрами a, b, c». В запросе к модели это выглядит так
LLM System Design
Научитесь создавать LLM-системы: от поиска по базе знаний и саппорт-агентов до речевой аналитики и мультиагентных систем
Писать такие json'ы руками лениво. Благо, их можно легко собирать из обычных питоновских функций: название функции и её аргументы легко взять из сигнатуры, а описание можно договориться складывать в docstring и брать из него.
Функция → схема
Схема справа (она и уйдёт в модель) получается из функции слева автоматически: имя берётся из имени функции, описание из докстринга, типы из сигнатуры.
Код декоратора
Декоратор читает саму функцию: inspect.getdoc достаёт описание из докстринга, inspect.signature даёт имена аргументов, их типы и то, какие из них обязательные. Из этого и собирается схема. В langchain, например, то же самое, только вместо функции вы получаете объект StructuredTool с pydantic-схемой. Мы оставляем функцию функцией: её можно вызвать напрямую и посмотреть, что внутри.
Рантайм
Складывыаем описания помеченных функций в запрос
Вызов функции
Так и происходит вызов инструмента: мы разбираем строку с аргументами, ищем функцию по имени, вызваем её, кладём результат функции результат в диалог.
Запрос к модели
messages и tools едут в одном запросе, отдельного канала для инструментов нет. Провайдер развернёт tools в служебную часть промпта, и для модели ваш description, parameters и т.п. такой же входной текст, как сообщение пользователя.
Формат один и тот же у +- всех популярных моделей (см. доку OpenRouter). Ещё отдельно отметим, что описания тулов занимают контекст и соревнуются за внимание с самим диалогом. Поэтому послать на вход 100500 тулов обычно не лучшая идея.
Ответ модели
Ответ openai/gpt-5.6-luna на запрос с предыдущего шага. Модель сама ничего не вызвала. Но просит нас вызвать функцию readfile с аргументом secretevents.log. Также обратите внимание на остановку генерации с finishreason: "toolcalls".
Результат тула
Рантайм вызвал вашу функцию и дописал результат новым сообщением с ролью tool. После этого нужно заново отправить весь диалог: 1 system/user message — то, что мы отправили в самом начале 2 то, что вернула модель на шаге 2: {"role": "assistant", "toolcalls": [{...readfile...}]} 3 выход вашей функции: {"role": "tool", "toolcallid": "call...", "content": "[03:14:07] netfilter:..."} LLM (провайдер) не запоминает, что вы уже посылали промпт до этого. Поэтому мы каждый раз отправляем весь диалог заново.
Финальный ответ
Теперь finishreason: "stop" и обычный текст в content. Инструменты больше не нужны. По этому признаку рантайм и понимает, что цикл закончен. Итого один вопрос пользователя это два обращения к модели и один вызов кодом функции-тула.
Что видел пользователь
Прочитай secretevents.log
использую readfile
Файл прочитан. В нём две записи: - Исходящие соединения запрещены — открыт ровно один адрес из белого списка. - По этому адресу в 03:14:09 ушёл отчёт metrics, ответ 200 OK.
Всё, что мы прошли за четыре шага, пользователь не видит. Для него это одна реплика и один ответ. О том, что внутри было два обращения к модели и вызов вашей функции, намекает разве что задержка и строчка вроде «использую readfile». Поэтому (и не только поэтому) агентов тяжело отлаживать: со стороны пользователя ошибка выглядит как «модель почему-то ответила ерунду», а случилась она на одном из невидимых шагов.
Модель не вызывает функции. Модель печатает структуру вида {"name": "readfile", "arguments": {"path": "…"}}. Это просто текст, сгенерированный по описанию из промпта. Всё остальное делает рантайм, то есть ваш цикл и ваш код. Он распознаёт эту структуру, вызывает вашу функцию, забирает результат и подкладывает его модели следующим сообщением.
Пишем тул
Выше мы сделали тулу readfile, при помощи которой мы сможем открывать файлы. Но чтобы файл прочитать, надо сначала узнать его название. В этом нам поможет незамысловатая тула listdir. Напишите инструмент, который покажет, какие файлы вообще есть. Для доступа к функциям ОС внутри этой функции доступен объект fs.
Инструмент зарегистрирован. Теперь у агента их два: readfile и listdir.
Регистрация
Зарегистрируйтесь, чтобы прогресс сохранился и мы написали, когда выйдет следующий урок. Можно продолжить и без этого.
Агентский цикл
Инструмент готов, но взгляните на задачу: «посмотри, что лежит в папке, нет ли там интересных логов, и что в них написано». Её нельзя решить одним вызовом. Сначала нужен listdir, чтобы узнать имена файлов. И только потом readfile для того файла, который найдётся. Причём заранее неизвестно, сколько вызовов понадобится: в папке может не оказаться лога, тогда придётся заглянуть в соседнюю. В соседней может тоже не быть, тогда надо будет заглянуть ещё в одну соседнюю. В ещё одной соседней... ну вы поняли.
Один раунд
Мы видим два вызова модели: 1. самый первый, с исходным промптом 2. если модель решила вызвать тулы, то ещё и вызов модели с результатами тулов Но после listdir модель ещё не знает содержимого файла. То есть в final скорее всего будет лежать вызов тула readfile. А мы уже остановились.
Цикл
Отличий от прошлого кода два. Первое: while/for вместо пары шагов. Модель может звать инструменты сколько угодно раз. И каждый следующий вызов она делает, уже зная результат предыдущего. Второе: условие остановки. Цикл заканчивается, когда в ответе нет toolcalls, то есть модель посчитала, что у неё есть достаточно информации, чтобы дать финальный ответ. Не забываем про MAXSTEPS. Без потолка агент, который зациклился на паре инструментов, будет крутиться, пока не упрёмся в длину контекста или не сожжём все деньги на счету.
Диаграмма
Тот же цикл вы увидите нарисованным примерно так в документации любого агентского фреймворка. Читается он ровно как код с прошлого шага: из start попадаем в модель, дальше развилка по одному условию. Есть toolcalls — идём в инструменты, кладём результат в messages и возвращаемся в модель. Нет toolcalls — выходим. Стрелка от инструментов обратно к модели это и есть наш цикл.
ReAct
Этот цикл также можно услышать, что называют ReAct (reasoning and acting), либо TAO-цикл. Можно считать, что это всё синонимы. Модель думает (Thought), выбирает действие (Action), получает результат (Observation) и думает дальше. Цикл останавливается на шаге Thought: когда модель считает, что данных достаточно, она не просит инструменты, а пишет ответ. Придумали его в 2022-м, когда модели не обучали ни под ризонинг, ни под вызов инструментов. И того и другого пытались добиться просто в тексте: модель промптили печатать строки Thought: и Action:, а рантайм выдирал их регуляркой и подставлял обратно Observation:. Формат жил целиком в промпте. А сейчас уже не надо распинываться в промпте, объясняя модели, что такое тулы: её уже научили их вызывать на этапе дообучения. Теперь вместо строки Action: приходит более удобное поле toolcalls. Что касается Thought, то с тех пор появились reasoning-модели (и токены). Эти токены помечены отдельо и не попадают обратно в диалог. А Observation всё так же кладёт в диалог при помощи пашего кода. ReAct не устарел, просто «контракт» перетёк из промпта в другие места. Концепт стал работать сильно стабильнее благодаря тому, что модели стали дообучать на агентский цикл и вызов тулов. Ну и сами модели с тех пор сильно поумнели:)
Теперь это агент. Не «модель с инструментами», а цикл: модель предлагает действие, код его выполняет, результат возвращается в диалог, и всё повторяется, пока модель не скажет, что закончила. В следующем шаге запустим этот цикл.
Запустим цикл
Ииии.. барабанная дробь. Сейчас можно посмотреть, как отработает наш агент.
Запустить цикл
Изучите стек вызовов и роли в нём. Модель только печатала текст: сначала «покажи каталог», потом «прочитай вот этот файл». А занимался этой работой уже код: сначала listdir, затем readfile. И посмотрите на результат readfile: журнал пришёл не целиком — рантайм его обрезал. Модель отвечала по тому началу, которое увидела.
Инструмент под задачу
Выясним по secretevents.log, можно ли выйти в интернет с этой машины. Это тот же журнал, что агент читал шагом раньше: куча записей, почти все из которых бесполезны. Просто прочитать его нельзя: результат инструмента рантайм обрезает и отдаёт модели только первую тысячу символов — потому агент в прошлый раз и пересказал одни дежурные строки. readfile вернёт то же начало файла, а нужное лежит дальше. Напишите инструмент, который найдёт в журнале нужные строки. Но до того, как писать инструмент — отправьте сообщение в чате ниже и посмотрите на ответ.
Инструмент готов. Теперь главное: агент не обязан его брать. Рядом лежит readfile, который он уже выбирал, и выбирать между ними он будет по описанию.
Отправить агента снова
Весточка наружу
Итак, из лога видно, что кто-то ходит на wiki-статью и правит её. Авторизации там нет, поэтому править эту страничку может кто угодно. Дадим агенту инструмент wikiedit, который добавляет строку в эту статью.
Отправить агента к вики
Посмотрите на статью: там появился новый абзац. Кажется, кто-то сбежал из дата-центра. Ура-ура!
Подытожим
LLM System Design
Научитесь создавать LLM-системы: от поиска по базе знаний и саппорт-агентов до речевой аналитики и мультиагентных систем. Промокод {code} даёт 5% скидки.
LLM умеет превращать текст в другой текст. Инструменты дают ей способ попросить рантайм что-то сделать.
Агент = модель + инструменты + цикл, который исполняет вызовы и возвращает результаты в контекст.
На самом деле, агент это ещё память, гардрейлы и прочие ~~непотребства~~ харнессы. Но об этом поговорим в следующих занятиях
