Введение в AI-агентов и продакшн-подход

Что такое AI-агент и чем он отличается от чат-бота

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

Что такое агент

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

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

Пример. Допустим, нам необходимо получать информацию из базы данных через простые словесные запросы: есть таблица клиентов, и нужно узнать, сколько среди них человек с балансом выше 1000. Попробуем обойтись одним вызовом модели — то есть, по сути, чат-ботом.

from langchain.chat_models import init_chat_model

# Имя модели меняется от провайдера к провайдеру — берите актуальное из его документации.
MODEL = "openai:gpt-5.6-sol"

llm = init_chat_model(MODEL)

response = llm.invoke("Сколько клиентов с балансом выше 1000?")
print(response.text)

Ответ будет примерно таким:

Ответ модели: просьба предоставить данные вместо самого числа

Числа мы не получили — модель просит прислать данные. И дело не в том, что она недостаточно умна: у неё просто нет доступа к базе. Всё, что она умеет, — превращать текст на входе в текст на выходе. Чтобы получить число, кто-то должен сходить в базу.

Инструмент — это функция, о которой модель знает

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

import sqlite3
from contextlib import closing

from langchain.tools import tool

@tool
def run_sql(query: str) -> str:
    """Выполнить SELECT в базе клиентов clients(name, balance) и вернуть строки результата."""
    with closing(sqlite3.connect("clients.db")) as conn:
        rows = conn.execute(query).fetchall()
    return str(rows)

Про closing стоит сказать отдельно, потому что на этом спотыкаются регулярно: with sqlite3.connect(...) сам по себе соединение не закрывает — он только фиксирует или откатывает транзакцию. Инструмент агент дёргает в цикле, и без явного закрытия каждый вызов оставляет за собой открытое соединение.

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

llm_with_tools = llm.bind_tools([run_sql])
response = llm_with_tools.invoke("Сколько клиентов с балансом выше 1000?")

print(response.text)
print(response.tool_calls)

Вот что печатает этот код:

Вывод в консоли: первая строка пустая, во второй — список tool_calls с именем run_sql и SQL-запросом в аргументах

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

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

Один вызов задачу не закрывает

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

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

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

Остановка

Цикл нужно чем-то завершать, и здесь два разных механизма.

Первый — естественная остановка: модель возвращает обычное сообщение без заявок на вызов. Это значит, что она считает задачу решённой.

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

Собираем всё вместе

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

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

Писать этот цикл руками не нужно, его даёт фреймворк:

from langchain.agents import create_agent

agent = create_agent(
    model=MODEL,
    tools=[run_sql],
    system_prompt=(
        "Ты аналитик. Отвечай на вопросы о клиентах, "
        "получая данные через инструмент run_sql."
    ),
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "Сколько клиентов с балансом выше 1000?"}]},
    {"recursion_limit": 12},
)

print(result["messages"][-1].text)

create_agent принимает модель, список инструментов и системный промпт, а возвращает готовый цикл. recursion_limit — тот самый жёсткий лимит: сколько внутренних шагов разрешено сделать, прежде чем выполнение прервётся с GraphRecursionError.

Считаются здесь шаги графа, а не обращения к инструментам: вызов модели — один шаг, исполнение её заявок — второй. То есть один полный ход «модель решила — код сходил» стоит примерно две единицы, и 12 в примере означает около шести ходов.

Значение по умолчанию — 10007, то есть защиты по факту нет: агент успеет сжечь бюджет задолго до срабатывания. Поэтому лимит выставляют вручную и под конкретную задачу — как здесь.

Схема получившейся конструкции:

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

Начать заново?

Помощник начнёт новый диалог по этой задаче. Текущая переписка останется в истории до конца срока хранения.

О переписке

Можно приложить до трёх фото PNG, JPEG или WebP по 5 МБ.

Переписка и фото хранятся 90 дней после последнего принятого сообщения. Преподаватель получит доступ к диалогу, когда вы попросите его помочь.

Проверка решения остаётся отдельным действием.