Урок курса

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

AI Agents PRO: LangGraph, AutoGen и LLMOps в продакшне

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

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

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

Пример. Допустим, нам необходимо получать информацию из базы данных через простые словесные запросы: есть таблица клиентов, и нужно узнать, сколько среди них человек с балансом выше 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, то есть защиты по факту нет: агент успеет сжечь бюджет задолго до срабатывания. Поэтому лимит выставляют вручную и под конкретную задачу — как здесь.

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

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

ReAct: как выглядит один прогон агента

У цикла, который мы собрали, есть имя — ReAct, от reasoning and acting: модель чередует рассуждение и действие. Она смотрит на то, что уже произошло, решает, что делать дальше, делает один шаг и снова смотрит на результат. Никакого плана целиком наперёд у неё нет: решение принимается заново на каждой итерации, с учётом всего, что накопилось.

Разберём это на одном прогоне целиком. Дадим агенту два инструмента и задачу, которую в один шаг не решить.

import sqlite3
import smtplib
from contextlib import closing
from email.message import EmailMessage

from langchain.tools import tool
from langchain.agents import create_agent

@tool
def run_sql(query: str) -> str:
    """Выполнить SQL-запрос к базе клиентов и вернуть результат или текст ошибки."""
    try:
        with closing(sqlite3.connect("clients.db")) as conn:
            return str(conn.execute(query).fetchall())
    except sqlite3.Error as exc:
        return f"SQLError: {exc}"

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Отправить письмо на указанный адрес."""
    message = EmailMessage()
    message["To"], message["Subject"] = to, subject
    message.set_content(body)
    with smtplib.SMTP("localhost") as server:
        server.send_message(message)
    return "sent"

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

task = "Посчитай клиентов с балансом выше 1000 и отправь число на ivan@example.com"

result = agent.invoke(
    {"messages": [{"role": "user", "content": task}]},
    {"recursion_limit": 12},
)

for message in result["messages"]:
    message.pretty_print()

Обратите внимание на run_sql: ошибка базы не поднимается наверх, а возвращается строкой. Это сделано намеренно — сейчас станет видно, зачем.

Трасса

Вот что напечатал последний цикл — полный список сообщений, накопленный за прогон:

Трасса прогона в консоли: запрос пользователя, ошибка SQL на угаданном имени колонки, запрос структуры таблицы, правильный подсчёт, отправка письма и финальный ответ

Разберём по шагам, что здесь произошло.

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

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

Третья итерация. Имя колонки известно, запрос переписан, получено число.

Четвёртая итерация. Данные есть, остаётся вторая половина задачи — письмо. Модель сама решает, что теперь очередь send_email, и подставляет в аргументы результат прошлого шага.

Пятая итерация. Модель возвращает обычное сообщение без заявок на вызов — цикл завершается сам. Лимит в 12 шагов не понадобился.

Что уходит в модель на каждой итерации

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

Рост списка сообщений от итерации к итерации: на каждом шаге в модель уходит весь накопленный диалог

Из этого следуют два практических вывода.

Первый: агент помнит свои ошибки. Модель не повторила неудачный запрос, потому что видела его результат. Если инструмент проглатывает ошибку и возвращает пустоту вместо описания проблемы, эта способность пропадает — модель будет тыкаться вслепую. Поэтому текст ошибки для агента полезнее аккуратного None.

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

Где смотреть трассу

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

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

Агент, чат-бот и промпт-цепочка

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

Чат-бот

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

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

Промпт-цепочка

Промпт-цепочка — фиксированная последовательность шагов, которую вы написали сами. Модель встроена в неё как один из шагов.

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template(
    "Преобразуй вопрос в SQL-запрос к таблице clients(name, account_balance). "
    "Верни только SQL, без пояснений и без markdown-разметки.\nВопрос: {question}"
)

chain = prompt | llm

sql = chain.invoke({"question": "Сколько клиентов с балансом выше 1000?"}).text
print(sql)
rows = conn.execute(sql).fetchall()
print(rows)
Вывод цепочки: сгенерированный моделью SQL-запрос и результат его выполнения — [(2,)]

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

Такая конструкция часто оказывается правильным выбором. Она дешевле — один вызов модели вместо пяти, — предсказуема и отлаживается как обычный код.

Про запись через |: это LCEL, способ сборки цепочек из ранних версий LangChain. Он рабочий и никуда не делся, но набор готовых цепочек вынесен в отдельный пакет langchain-classic, а основным строительным блоком в документации стал агент. Так что не удивляйтесь, если в свежих руководствах цепочек почти не встретите — сам приём от этого хуже не стал.

Агент

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

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

Три конструкции: чат-бот, промпт-цепочка и агент — кто определяет последовательность шагов

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

Сводная таблица

Чат-бот Промпт-цепочка Агент
Кто определяет следующий шаг Пользователь в диалоге Автор кода, до запуска Модель, во время выполнения
Действия во внешних системах Нет Есть, в заданном порядке Есть, порядок и набор выбирает модель
Вызовов модели на задачу Один на сообщение Фиксированное число Заранее неизвестно, нужен лимит
Реакция на ошибку шага Отвечает текстом Падает или идёт по заранее описанной ветке Видит ошибку и может выбрать другой шаг
Повторяемость Разный текст ответа Одинаковый путь, разный текст на шагах с моделью Путь может отличаться от запуска к запуску

Про память и «обучение на опыте»

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

Отсюда практическое следствие: «агент запомнил» означает «кто-то сохранил это и подставил обратно». Если такого механизма в коде нет, следующий запуск начнётся с чистого листа.

Как выбирать

Рабочий вопрос ровно один: можете ли вы заранее нарисовать последовательность шагов?

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

Когда агент не нужен

Свобода выбирать шаги — не бесплатная опция, а обмен. Разберём, чем именно вы платите, и в каких задачах цена оказывается выше пользы.

Цена

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

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

Непредсказуемость пути

Модель выбирает шаги заново на каждом запуске. Тот же запрос завтра может пойти другим маршрутом: другой запрос к базе, другое число обращений, другой итоговый текст.

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

Каждый инструмент — это выданное разрешение

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

Поэтому необратимые действия либо не отдают агенту вовсе, либо ставят перед ними подтверждение человеком: агент готовит вызов, человек утверждает. Такая схема называется human-in-the-loop и решает большинство страхов вокруг автономности, не отменяя пользы от агента.

Автономность не означает «работает сам»

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

Автономность здесь означает только одно: между шагами не нужен человек. Сопровождения агент требует не меньше обычного сервиса, а обычно больше.

Где агент не нужен

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

Второе: короткий диалог без действий. Ответить на «как сбросить пароль» — работа чат-бота с поиском по базе знаний. Цикл с инструментами тут просто добавит задержку и стоимость.

Третье: заранее известная последовательность шагов. Если процесс описан как «сделать А, потом Б, потом В», агент лишний: он будет каждый раз заново решать то, что уже решено, и иногда ошибаться в этом.

Четвёртое: среда, где действия запрещены или требуют строгого разграничения прав. Если агенту нельзя дать ни одного инструмента, остаётся модель, отвечающая текстом, — то есть не агент.

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

Дерево выбора: нужен ли агент — по наличию действий, известности порядка шагов и требованию воспроизводимости

Правило на каждый день

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

Задача 1

Агенту дали инструмент run_sql, и в трассе видно, что он «вызвал» его. Что при этом вернула сама модель?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Агент зациклился: раз за разом дёргает один и тот же инструмент, получает одну и ту же ошибку и пробует снова. Что прервёт такой прогон?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

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

Сопоставьте элементы

Без регистрации · результат не сохраняется

Задача 4

Инструмент run_sql переписали: вместо строки SQLError: no such column он теперь молча возвращает пустую строку. Что изменится в прогоне из разобранной трассы? Выберите все подходящие варианты.

Выберите все подходящие варианты
Без регистрации · результат не сохраняется

Задача 5

В разобранной трассе прогон занял пять итераций: четыре с заявками на вызов инструментов и одна с финальным ответом. Сколько раз за этот прогон текст исходной задачи пользователя был отправлен в модель?

Без регистрации · результат не сохраняется

Задача 6

Сервис раз в сутки берёт выгрузку заказов, считает скидку по утверждённому регламенту и пишет результат в базу. Регламент меняется раз в квартал, результат должны уметь предъявить и повторить. Что здесь уместно?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 7

Выбор инструмента по ключевым словам

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

Задание

Реализуйте функцию choose_tool(query, tools). Первый аргумент — текст запроса пользователя, второй — словарь: имя инструмента → список его ключевых слов.

Вес инструмента — это суммарное число вхождений всех его ключевых слов в запрос. Считайте вхождения методом str.count без учёта регистра; он находит непересекающиеся вхождения, и этого здесь достаточно. Верните имя инструмента с наибольшим весом. Если наибольший вес набрали несколько инструментов, верните лексикографически меньшее имя. Если ни одно ключевое слово не встретилось, верните None.

Пример

tools = {
    "calc": ["sum", "add", "plus"],
    "db": ["sql", "select", "table"],
}

choose_tool("How to select from a table in SQL?", tools)  # "db"
choose_tool("Please add 2 and 2", tools)                   # "calc"
choose_tool("Hello world", tools)                          # None

Примечание

Запрос уже разобран и приходит аргументом — читать ввод не нужно. Пустых ключевых слов в списках нет, но сам словарь tools может оказаться пустым, а запрос — пустой строкой; в обоих случаях ответ None.

Разрешена только стандартная библиотека Python 3.12, и та здесь не понадобится.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 8

Сколько токенов съедает прогон агента

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

Навык прикладной: именно так прикидывают, во что обойдётся агент, когда в диалоге накапливаются результаты инструментов, а прогон идёт не пять шагов, а пятьдесят.

Задание

Реализуйте функцию total_prompt_tokens(trace). Аргумент — список пар (роль, размер) в том порядке, в каком сообщения появлялись в диалоге. Роль — одна из трёх строк: "user", "ai", "tool". Размер — длина сообщения в токенах.

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

Пример

trace = [("user", 10), ("ai", 5), ("tool", 3), ("ai", 7)]
total_prompt_tokens(trace)  # 28

Первый вызов модели видит только запрос пользователя — это 10 токенов. Второй видит запрос, ответ модели и результат инструмента: 10 + 5 + 3 = 18. Итого 28.

Примечание

Ответ модели и результаты инструментов в свой же вызов не попадают — они появляются в диалоге после него. Пустая трасса и трасса без единой роли "ai" дают 0.

Разрешена только стандартная библиотека Python 3.12. Решение должно обходить список один раз — не считайте сумму заново на каждом вызове модели.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 9

Порядок вызовов с зависимостями

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

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

Задание

Реализуйте функцию plan_calls(count, deps). Вызовы пронумерованы от 1 до count. Список deps состоит из пар (u, v), каждая означает: вызов u должен быть исполнен раньше вызова v.

Верните список номеров в допустимом порядке исполнения. Порядок должен быть единственным, поэтому зафиксируем правило выбора: если готовых к исполнению вызовов несколько, всегда берите тот, у которого меньше номер. Если зависимости образуют цикл и порядка не существует, верните None.

Пример

plan_calls(4, [(1, 2), (1, 3), (2, 4), (3, 4)])  # [1, 2, 3, 4]
plan_calls(3, [(3, 1)])                          # [2, 3, 1]
plan_calls(2, [(1, 2), (2, 1)])                  # None

Во втором примере вызов 2 идёт первым: он готов сразу, а его номер меньше, чем у тройки, которая тоже готова.

Примечание

Зависимостей может не быть вовсе — тогда ответ это просто номера по возрастанию. Повторяющиеся пары в deps возможны, и каждую нужно учитывать отдельно: две одинаковые пары дают счётчику значение два. Петля вида (u, u) — это цикл, ответ None.

Разрешена только стандартная библиотека Python 3.12; для выбора наименьшего готового номера удобен модуль heapq. Вызовов может быть до 200 000, поэтому решение должно быть линейным по размеру входа.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 10

Соберите цикл агента

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

Задание

Реализуйте функцию run_agent(model, tools, task, max_steps).

Начните диалог с одного сообщения — запроса пользователя task. Дальше повторяйте: вызовите model.invoke(messages), добавьте полученный ответ в диалог, а затем разберите его заявки на вызовы. Если заявок нет, прогон закончен — верните текст ответа. Если есть, исполните каждую по очереди и добавьте её результат в диалог отдельным сообщением, после чего вызовите модель снова.

Число обращений к модели ограничено значением max_steps. Если лимит израсходован, а модель так и не ответила без заявок, верните строку "LIMIT_REACHED".

Что приходит в аргументах

model — объект с методом invoke(messages), принимающим текущий список сообщений. Возвращает ответ с двумя полями: text — обычный текст, и tool_calls — список заявок. Каждая заявка это словарь с ключами name (имя инструмента), args (словарь аргументов) и id.

tools — словарь: имя инструмента → функция. Функцию вызывают именованными аргументами из args, возвращает она строку.

Пример

def count_clients(threshold: int) -> str:
    return "2"

# Модель просит инструмент, получает результат и отвечает текстом.
run_agent(model, {"count_clients": count_clients}, "Сколько клиентов?", 12)
# "Клиентов с балансом выше 1000 — двое."

# Модель на каждом шаге снова просит инструмент, лимит равен двум.
run_agent(model, {"count_clients": count_clients}, "Сколько клиентов?", 2)
# "LIMIT_REACHED"

Примечание

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

Инструменты не бросают исключений и всегда возвращают строку, так что оборачивать их в try здесь не нужно. Ответ модели может содержать несколько заявок сразу — исполняйте их в том порядке, в каком они лежат в tool_calls.

model и tools приходят готовыми — создавать их не нужно. Разрешена только стандартная библиотека Python 3.12.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку