В предыдущей главе мы увидели, что ИИ-агент состоит из четырёх элементов — голова (LLM), инструменты, память и цикл (управление). В этой главе мы наконец возьмёмся за дело. Цель проста: собрать «рабочего агента» из четырёх элементов в минимальной конфигурации, реально его запустить и понаблюдать за поведением. Никаких пышных функций пока не добавляем. Сначала научимся своими руками крутить сердце агента — «дать LLM подумать, вызвать инструмент, вернуть результат, снова дать подумать и остановиться по завершении».

Цель этой главы

«Собрать и запустить одного минимального агента своими руками»

Понять цикл
Сможете объяснить один оборот агента: «подумать → вызвать → вернуть → снова подумать → остановиться».
Уметь дать инструмент
Разберётесь в определении инструмента и аргументах и сможете дать LLM один инструмент.
Уметь наблюдать
Увидите по логам, где он принимает решения и где ошибается, и сделаете мостик к следующей главе.

Что такое минимальный агент

«Минимальный агент» — это четыре элемента предыдущей главы, собранные каждый в самой маленькой форме. Ни роскошной памяти, ни множества инструментов, ни нескольких агентов не нужно. Как в таблице ниже, начинаем с конфигурации, урезанной до предела. Если понять эту точку, дальше останется лишь наращивать каждый элемент.

🧠 Голова

Просто вызываем одну модель Claude. В промпте одной строкой задаём роль: «Ты — ассистент, умеющий пользоваться инструментами».

🛠 Инструменты

Инструмент всего один. Хоть поиск, хоть вычисление. Сначала — простая функция с надёжной работой.

📝 Память

Достаточно одного массива истории диалога. Просто складываем реплики по порядку. О постоянном хранении пока не думаем.

🔄 Цикл

Один while. Крутим по правилу «есть запрос инструмента — выполнить, нет — готово». Задаём только предел числа оборотов.

💡 Отличие от «одного вызова API» — только цикл. Если просто один раз спросить LLM и получить ответ — это обычное использование API. В тот момент, когда добавляется повторение — «выполнить инструмент, вернуть результат и снова дать подумать», — это становится агентом. По сути прибавляется всего один этот цикл. Именно поэтому короткий путь — сначала полностью понять цикл.

Что внутри агентного цикла

Сердце агента — один цикл. Программа крутит то же самое, что человек: «изучить, подумать, поработать руками, посмотреть на результат и, если ещё нет, повторить». Сначала уловим один оборот по схеме.

ШАГ 1
Дать подумать

Передаём LLM историю диалога и список инструментов. LLM решает: «ответить» или «использовать инструмент».

ШАГ 2
Вызвать инструмент

Если LLM ответила «хочу использовать этот инструмент с такими аргументами», мы реально выполняем эту функцию.

ШАГ 3
Вернуть результат

Дописываем результат (возвращаемое значение инструмента) в историю и снова передаём LLM. Здесь возвращаемся к шагу 1.

ШАГ 4
Остановиться по завершении

Если 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. Внутри у обоих один и тот же цикл. Сначала разберём различие.

A. Рукописный цикл
крутите while сами

Вызовы API и выполнение инструментов крутятся в вашем собственном while. Видно всё, что происходит внутри, поэтому идеально для обучения. И контроль полный.

Когда подходит: хотите понять устройство / контролировать в деталях.

B. Tool runner из SDK
отдать цикл библиотеке

В цикл выполнения инструментов, который предоставляют 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.