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

В этой статье — первой в серии про AI-агентов и локальные LLM-инструменты — мы разберём, из чего агент состоит технически: паттерн ReAct, function calling (вызов функций) и планировщики задач. Это фундамент, без которого сложно понять, как устроены Open Interpreter, AutoGen, CrewAI, LangGraph и другие инструменты, о которых пойдёт речь в следующих статьях серии.

Пять признаков, по которым легко отличить агента от чат-бота

Прежде чем нырять в архитектуры, полезно закрепить практический чек-лист. Если система обладает хотя бы тремя из пяти пунктов ниже — это уже агент, а не чат-бот с красивым интерфейсом:

  • система может вызывать внешние инструменты (API, shell, браузер, база данных), а не только генерировать текст;
  • у системы есть цикл из нескольких шагов между запросом пользователя и финальным ответом;
  • решение о том, какой инструмент вызвать и в каком порядке, принимает модель, а не жёстко прописанный код;
  • система способна заметить ошибку выполнения и скорректировать план, а не просто вывалить исключение;
  • у системы есть память о промежуточных результатах текущей задачи, выходящая за рамки одной реплики.

Дальше — детальный разбор трёх архитектурных паттернов, которые обеспечивают эти признаки на практике.

Чат-бот: один запрос — один ответ

Классический чат-бот работает по схеме «запрос-ответ» (request-response). Пользователь пишет сообщение, модель генерирует текст, диалог сохраняется в истории и отправляется целиком при следующем запросе. Никакого выполнения кода, никакого доступа к файловой системе или API — модель не может ничего «сделать», только сказать.

Упрощённый пример запроса к API в таком режиме:

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Как перезапустить nginx на Ubuntu?"}
    ]
)

print(response.content[0].text)

Модель ответит текстом с командой systemctl restart nginx, но сама её не выполнит. Дальше — дело человека. Это и есть главный признак чат-бота: результат работы — текст, а не действие.

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

Агент: цикл «думать → действовать → наблюдать»

Агент отличается тем, что модель встроена в программный цикл (agent loop), который умеет:

  • дать модели список доступных инструментов (функций, команд, API);
  • получить от модели структурированный запрос на вызов конкретного инструмента с параметрами;
  • выполнить этот вызов в реальной среде — на сервере, в браузере, в базе данных;
  • вернуть результат модели как наблюдение (observation);
  • повторять цикл, пока задача не будет решена.

Такой цикл называют ReAct — сокращение от «Reasoning + Acting», термин из статьи Yao et al. (2022). Идея простая: модель рассуждает вслух («нужно проверить статус сервиса»), затем действует (вызывает инструмент), затем видит результат и снова рассуждает. Псевдокод цикла:

state = {"task": "проверить, почему сайт недоступен", "history": []}

while not done:
    thought = llm.reason(state)          # что делать дальше
    action = llm.decide_action(thought)  # какой инструмент вызвать
    observation = execute_tool(action)   # реальное выполнение
    state["history"].append((thought, action, observation))
    done = llm.is_task_complete(state)

На практике этот цикл почти всегда реализован не вручную, а через встроенный в модель function calling — модель сама решает, когда пора вызвать инструмент, и возвращает не свободный текст, а структурированный JSON с именем функции и аргументами.

Function calling: как модель «нажимает кнопки»

Function calling (в терминологии Anthropic — tool use) — это механизм, при котором ты описываешь модели доступные функции в виде JSON Schema, а модель вместо текста возвращает структурированный вызов. Пример описания одного инструмента для Anthropic API:

tools = [
    {
        "name": "check_service_status",
        "description": "Проверяет статус systemd-сервиса на удалённом сервере",
        "input_schema": {
            "type": "object",
            "properties": {
                "host": {"type": "string", "description": "Имя или IP сервера"},
                "service": {"type": "string", "description": "Имя сервиса, например nginx"}
            },
            "required": ["host", "service"]
        }
    }
]

response = client.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "Проверь, работает ли nginx на srv-ai01"}]
)

Если модель решит, что нужен вызов функции, в response.content появится блок с type == "tool_use", содержащий имя функции и распарсенные аргументы — например {"host": "srv-ai01", "service": "nginx"}. Дальше твой код должен реально выполнить проверку (например, по SSH выполнить systemctl is-active nginx) и отправить результат обратно модели как tool_result. Модель прочитает результат и решит, что делать дальше — доложить пользователю или вызвать ещё один инструмент.

Это ключевое отличие от чат-бота: агент не просто предполагает, что нужно сделать, а фактически делает и видит реальный результат, прежде чем сформулировать финальный ответ.

Планировщики: когда одного цикла мало

ReAct отлично работает для коротких задач в несколько шагов. Но если задача сложная — например, «разверни на srv-ai01 новый inference-сервис, настрой мониторинг и пришли отчёт» — модели выгоднее сначала составить план, а потом выполнять его по шагам, а не думать «на лету» после каждого действия.

Архитектура планировщик-исполнитель (planner-executor) разделяет роли:

  • Планировщик — модель (или отдельный вызов модели) декомпозирует задачу на подзадачи заранее, до начала выполнения.
  • Исполнитель — отдельный цикл ReAct, который берёт одну подзадачу и доводит её до конца, используя инструменты.
  • Контроллер — следит за прогрессом, при необходимости просит планировщик пересобрать план (replanning), если что-то пошло не так.

Упрощённая схема на псевдокоде:

plan = planner_llm.decompose(task)
# plan = ["Проверить доступность srv-ai01", "Установить Docker",
#         "Запустить контейнер с моделью", "Настроить проверку здоровья"]

results = []
for step in plan:
    result = executor_agent.run(step)   # тут работает ReAct-цикл со своими tools
    results.append(result)
    if executor_agent.failed:
        plan = planner_llm.replan(task, results)  # пересборка плана при сбое

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

Важно понимать компромисс между двумя подходами. Чистый ReAct без предварительного плана гибче — модель реагирует на реальность на каждом шаге и не тратит токены на составление плана, который может оказаться неверным. Но на длинных задачах он склонен «блуждать»: без общей картины модель может зациклиться на второстепенной детали и потерять исходную цель. Planner-executor решает эту проблему ценой дополнительного вызова модели и риска, что план, составленный без учёта реальных препятствий, придётся не раз пересобирать. На практике многие production-агенты используют гибрид: лёгкий план из 3-5 крупных шагов плюс ReAct-цикл внутри каждого шага.

Роль памяти в архитектуре агента

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

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

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

Практический пример: мини-агент на локальной модели

Если у тебя в лаборатории (домен itpages.lab.local, сеть 192.168.10.0/24) поднят сервер srv-ai01 с локальной LLM через Ollama или LM Studio, ты можешь собрать простейший агент буквально на requests, без сторонних фреймворков — это полезное упражнение для понимания механики, прежде чем переходить к готовым инструментам:

import requests, json

OLLAMA_URL = "http://srv-ai01.itpages.lab.local:11434/api/chat"

def call_model(messages, tools=None):
    payload = {"model": "llama3.1", "messages": messages, "stream": False}
    if tools:
        payload["tools"] = tools
    r = requests.post(OLLAMA_URL, json=payload, timeout=60)
    return r.json()

Дальше вокруг этой функции строится тот самый цикл наблюдения и действия. Подробный рабочий пример агента «с нуля» на голом API, с полноценной обработкой tool_use и tool_result, разобран в отдельной статье этой серии.

Частые ошибки и как их избежать

  • Путают «модель с большим промптом» и агента. Если ты просто просишь модель «представь, что ты выполнил команду X и скажи результат» — это фантазия модели, а не реальное действие. Настоящий агент обязан реально выполнять вызовы и передавать модели фактический результат, иначе она будет «галлюцинировать» успех там, где на деле ничего не произошло.
  • Нет ограничения на количество итераций цикла. Без max_iterations агент может зациклиться, бесконечно вызывая один и тот же инструмент, особенно если результат неоднозначный. Всегда ставь жёсткий лимит шагов и таймаут.
  • Инструменты описаны слишком расплывчато. Если description и input_schema инструмента неточные, модель будет путать параметры или вызывать не тот инструмент. Пиши описания так, будто объясняешь функцию младшему разработчику, который видит её впервые.
  • Инструментам дают слишком много полномочий сразу. Агент, у которого есть доступ и к чтению файлов, и к их удалению, и к сетевым запросам одним и тем же вызовом shell-команды, — это риск, а не удобство. Дроби инструменты на узкие функции с понятной зоной ответственности и там, где возможно, разделяй «безопасные» операции чтения и «опасные» операции изменения состояния.

Что дальше

Серия про AI-агентов только начинается — вот с чем продолжить разбираться дальше: