В предыдущей главе мы увидели, что ИИ-агент состоит из четырёх элементов — голова (LLM), инструменты, память и цикл (управление). В этой главе мы наконец возьмёмся за дело. Цель проста: собрать «рабочего агента» из четырёх элементов в минимальной конфигурации, реально его запустить и понаблюдать за поведением. Никаких пышных функций пока не добавляем. Сначала научимся своими руками крутить сердце агента — «дать LLM подумать, вызвать инструмент, вернуть результат, снова дать подумать и остановиться по завершении».
«Собрать и запустить одного минимального агента своими руками»
Что такое минимальный агент
«Минимальный агент» — это четыре элемента предыдущей главы, собранные каждый в самой маленькой форме. Ни роскошной памяти, ни множества инструментов, ни нескольких агентов не нужно. Как в таблице ниже, начинаем с конфигурации, урезанной до предела. Если понять эту точку, дальше останется лишь наращивать каждый элемент.
Просто вызываем одну модель Claude. В промпте одной строкой задаём роль: «Ты — ассистент, умеющий пользоваться инструментами».
Инструмент всего один. Хоть поиск, хоть вычисление. Сначала — простая функция с надёжной работой.
Достаточно одного массива истории диалога. Просто складываем реплики по порядку. О постоянном хранении пока не думаем.
Один while. Крутим по правилу «есть запрос инструмента — выполнить, нет — готово». Задаём только предел числа оборотов.
💡 Отличие от «одного вызова API» — только цикл. Если просто один раз спросить LLM и получить ответ — это обычное использование API. В тот момент, когда добавляется повторение — «выполнить инструмент, вернуть результат и снова дать подумать», — это становится агентом. По сути прибавляется всего один этот цикл. Именно поэтому короткий путь — сначала полностью понять цикл.
Что внутри агентного цикла
Сердце агента — один цикл. Программа крутит то же самое, что человек: «изучить, подумать, поработать руками, посмотреть на результат и, если ещё нет, повторить». Сначала уловим один оборот по схеме.
Передаём LLM историю диалога и список инструментов. LLM решает: «ответить» или «использовать инструмент».
Если LLM ответила «хочу использовать этот инструмент с такими аргументами», мы реально выполняем эту функцию.
Дописываем результат (возвращаемое значение инструмента) в историю и снова передаём LLM. Здесь возвращаемся к шагу 1.
Если LLM возвращает финальный ответ, не используя инструмент, считаем цель достигнутой и выходим из цикла.
Суть в том, что шаги 1–3 крутятся по кругу, а выход происходит только на шаге 4. За один оборот LLM может и не закончить. «Сначала поискать, посмотреть результат и снова поискать» — так, за много оборотов, она приближается к цели. В псевдокоде это на удивление коротко.
# минимальный агентный цикл (псевдокод) messages = [ user("Узнай среднюю температуру за прошлый месяц и обобщи") ] while True: reply = llm.think(messages, tools=[search_tool]) # ШАГ1 думаем if reply.wants_tool: # есть запрос инструмента? result = run_tool(reply.tool_name, reply.args) # ШАГ2 вызываем messages.append(assistant(reply)) messages.append(tool_result(result)) # ШАГ3 возвращаем else: return reply.text # ШАГ4 останавливаемся по завершении
⚠️ Обязательно ставьте предел числа оборотов. Псевдокод выше ради наглядности использует while True, но в реальном коде всегда ставьте ограничитель «не более N оборотов». Бесконечный цикл, где LLM без конца вызывает инструменты, — это реальный и частый инцидент. Подробнее — в разделе Подводные камни.
Даём один инструмент
Теперь дадим LLM один инструмент. Здесь важно понять: LLM не может сама выполнять код. Всё, что она умеет, — словами запросить «хочу использовать этот инструмент с такими аргументами». Реально запустить функцию и вернуть результат — всегда наша работа (работа вашей программы).
Для этого мы сначала передаём LLM определение инструмента. Определение инструмента — это инструкция о том, «что этот инструмент умеет и какие аргументы принимает». LLM читает её и решает, когда и как им пользоваться.
Например: search_weather «по названию места и месяцу возвращает среднюю температуру». Описание — материал для решений LLM, поэтому пишите его тщательно.
Какие аргументы и какого типа принимаются, задаётся схемой (определение в формате JSON). Например: city (строка) и month (число).
Реально работающее тело кода. Принимает аргументы, которые вернула LLM, и возвращает результат. Здесь достаточно обычной функции.
Хитрость в том, чтобы из этого набора из трёх пунктов ①② передать LLM, а ③ держать у себя. Когда LLM запросит «хочу использовать search_weather(city=\"Париж\", month=6)», мы принимаем это имя и аргументы, вызываем функцию ③ и возвращаем её результат в диалог как результат инструмента. Посмотрим на псевдокод.
# ① и ② … определение инструмента, передаваемое LLM search_tool = { "name": "search_weather", "description": "по городу и месяцу возвращает среднюю температуру (°C)", "input_schema": { "city": "string", "month": "number", }, } # ③ … сама функция, которую держим у себя. Просто функция def run_tool(name, args): if name == "search_weather": return weather_db.lookup(args["city"], args["month"]) return "unknown tool"
📊 Описание — часть промпта. description инструмента — единственная зацепка, по которой LLM решает, «когда им пользоваться». Если оно расплывчато, инструмент либо не используется, либо применяется невпопад. Хитрость — писать так, будто объясняешь человеку: «что возвращает и когда применять». Проектирование инструментов мы копнём глубже в следующей главе про MCP.
Как это реализуется — рукописный цикл и SDK
Собрав всё вышеперечисленное, вы получите работающего первого агента. Реализовать это можно в основном двумя способами. Хотите изучить устройство — пишите вручную; хотите облегчить рабочую задачу — берите SDK. Внутри у обоих один и тот же цикл. Сначала разберём различие.
Вызовы API и выполнение инструментов крутятся в вашем собственном while. Видно всё, что происходит внутри, поэтому идеально для обучения. И контроль полный.
Когда подходит: хотите понять устройство / контролировать в деталях.
В цикл выполнения инструментов, который предоставляют SDK вроде Claude, вы регистрируете определения инструментов и функции и доверяете ему. Шаблонную часть писать не нужно, и это надёжно.
Когда подходит: хотите быстро собрать / доверить шаблонную обработку продакшну.
A. Рукописный цикл в целом — это просто прямое соединение всех предыдущих деталей. Хранить историю диалога, вызвать LLM, при запросе инструмента выполнить и вернуть результат, иначе выйти — вот и всё.
# A. рукописный цикл (псевдокод / в стиле сообщений Claude) messages = [ user(goal) ] for step in range(MAX_STEPS): # ← ограничитель по пределу reply = client.messages.create( model="claude-…", messages=messages, tools=[search_tool], ) log(step, reply) # ← ради наблюдения обязательно логируем if reply.stop_reason == "tool_use": out = run_tool(reply.tool_name, reply.tool_args) messages += [ assistant(reply), tool_result(out) ] else: break # получен финальный ответ → выход
Если использовать B. tool runner из SDK, этот самый цикл for берёт на себя библиотека. Вы пишете лишь «определение инструмента», «саму функцию» и «начальную цель». Цикл, управление историей и возврат результатов инструментов обрабатываются внутри.
# B. отдаём tool runner из SDK (псевдокод) runner = ToolRunner( model="claude-…", tools=[search_tool], # определение handlers={"search_weather": run_tool}, # сама функция ) answer = runner.run(goal) # цикл крутится внутри
💡 Точные имена методов — в официальной документации. client.messages.create и ToolRunner здесь — псевдокод, передающий общую идею. Реальные имена аргументов, форма возвращаемых значений и ID моделей различаются от SDK к SDK и часто обновляются, поэтому всегда сверяйтесь с актуальной версией в официальной документации. Сначала ухватить концепцию, а детали заполнить по документации — так вы не сделаете лишний крюк. Пошаговое введение см. также в материале «Как создать ИИ-агента» (для начинающих).
Рекомендация — «сначала один раз написать вручную по способу A, а дальше облегчать себе жизнь способом B». Один раз прокрутив цикл самостоятельно, вы перестаёте видеть в SDK чёрный ящик и, застряв, можете представить, что там внутри происходит.
Запускаем и наблюдаем
Когда собрали — запускаем. Здесь важно не просто запустить, а понаблюдать: «где и о чём он думал, где ошибся». В разработке агентов именно качество наблюдения определяет результат. Поэтому логи закладываем с самого начала.
Как минимум на каждом обороте фиксируйте следующее. В рукописном цикле хватит одной строки log(step, reply).
Решил ли на этом обороте «использовать инструмент» или дал финальный ответ. Точка ветвления решения.
Имя инструмента и аргументы. Если здесь что-то не так, и результат собьётся. Место, где расхождение в понимании LLM проявляется чаще всего.
Возвращаемое значение инструмента. Слишком большое, пустое, с ошибкой — здесь прячется то, что собьёт следующее решение.
Когда реально запустите, проступит интересное (и хлопотное) поведение. Например: LLM вызывает инструмент, когда он не нужен. И наоборот — не вызывает там, где должна бы, и отвечает по одной памяти. Слегка ошибается в названии города в аргументах. Повторяет один и тот же поиск много раз, хотя хватило бы одного. Такие «повадки в решениях» замечаешь, только глядя в логи.
✅ Логи — не «продвинутая функция», а необходимость с самого начала. Внутренности агента плохо видны, поэтому без логов совершенно непонятно, «почему он так поступил». Пока достаточно и одной строки print. Систематизированное наблюдение — оценку и наблюдаемость — мы всерьёз разберём в главе 5. А пока выработайте здесь «привычку смотреть».
Подводные камни
Даже в минимальной конфигурации, стоит запустить, — обязательно упрётесь в несколько стен. Все они — классические подводные камни разработки агентов, которые мы будем решать глава за главой. Здесь ухватим «суть» и «доступную сейчас первую помощь», а полноценное решение оставим последующим главам.
Без конца вызывает один и тот же инструмент и никак не завершается. Крутится вхолостую, не сумев решить, что цель достигнута.
Первая помощь: обязательно задать предел числа оборотов. Обнаруживать и останавливать последовательные вызовы с одинаковыми аргументами.
Инструмент возвращает огромный JSON или всю страницу целиком, вы возвращаете это в диалог как есть, и растут и затраты, и задержки.
Первая помощь: на стороне инструмента оставлять только нужные поля / сначала обобщать. Не передавать весь текст без обработки.
Чем больше оборотов, тем сильнее накапливается история диалога; приближаясь к пределу, всё становится медленнее, дороже и нестабильнее.
Первая помощь: сжимать старые реплики обобщением / выбрасывать ненужные результаты инструментов. Полноценно — в следующих главах.
📊 Все три — проблема «проектирования контекста». И бесконечный цикл, и разбухание возвращаемых значений, и раздувание контекста в корне сводятся к проектированию контекста — «что и сколько передавать LLM». Этот подход систематизирован как контекстная инженерия и становится хребтом разработки агентов. Для начала возьмите девизом «не передавать лишнего и не накапливать лишнего».
- Минимальный агент = четыре элемента, собранные в самом маленьком виде. Отличие от одного вызова API — только цикл.
- Цикл повторяет один оборот: «подумать → вызвать инструмент → вернуть результат → снова подумать → остановиться по завершении». Ограничитель по числу оборотов обязателен.
- У инструмента определение (имя, описание, аргументы) передаётся LLM, а сама функция выполняется у вас. LLM только запрашивает.
- Реализация — двумя способами: рукописный цикл (для обучения) и tool runner из SDK (для рабочих задач). Внутри одно и то же.
- С самого начала наблюдайте по логам и находите классические подводные камни: бесконечный цикл, разбухание возвращаемых значений, раздувание контекста.
Как только запустите одного агента, следующий шаг — всерьёз наращивать инструменты. В следующей главе 3 «MCP и подключение инструментов» мы стандартным способом (через MCP) подключим инструменты к внешним сервисам и данным и разом расширим руки агента. Если захотите свериться с общей картиной из предыдущей главы, вернитесь к главе 1.