К курсу

Цикл агента

Один шаг цикла агентаПрактика: один шаг циклаЦикл в кодеПрактика: цикл в кодеОстановка и повторыПрактика: остановка и повторыЧто решает только кодПрактика: что решает код

Надёжность в проде

Таймаут и ключ идемпотентностиПрактика: таймаут и ключ идемпотентности

Устройство агентов

Workflow или агентПрактика: workflow или агентReAct и plan-and-executeПрактика: ReAct и plan-and-executeОдин агент или ройПрактика: один агент или рой

Контекст и стоимость

Почему растёт счёт за агентаПрактика: токены и кэшКонтекст-инженерияПрактика: управление окномПамять агентаПрактика: что агент хранит

Инструменты и протоколы

MCPПрактика: MCP
←→ENВойти
Агенты › Цикл агента

Цикл в коде

Опирается наОдин шаг цикла агента

Этот блок переводит телефонный разговор из прошлого блока в код. Пройдём цикл по строкам, потом посмотрим, что бывает, когда модель просит две вещи сразу и когда инструмент падает.

Мы всё ещё делаем агента поддержки Northwind, прямо на Python SDK от Anthropic.

Телефонный разговор как функция

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

def run_agent(client, session, text):
    messages = [{"role": "user", "content": text}]
    for _ in range(MAX_STEPS):
        reply = client.messages.create(model=MODEL, max_tokens=1024,
                                       system=SYSTEM, tools=TOOLS,
                                       messages=messages)
        messages.append({"role": "assistant", "content": reply.content})
        if reply.stop_reason == "end_turn":
            return text_of(reply)
        if reply.stop_reason != "tool_use":
            return HANDOFF_MESSAGE
        results = [execute(block, session) for block in reply.content
                   if block.type == "tool_use"]
        messages.append({"role": "user", "content": results})
    return HANDOFF_MESSAGE

Вот что делает каждая часть.

messages — это ваш блокнот. В начале там сообщение покупателя, и дальше он только растёт.

client.messages.create — это звонок. Вы отправляете инструкции, список инструментов и весь блокнот.

Первый append записывает слова коллеги. Ответ попадает в блокнот целиком и раньше, чем вы что-то по нему сделаете.

end_turn значит, что коллега закончил, и вы возвращаете текст. У этого агента нет серверных инструментов и stop sequences, поэтому любая другая причина, например max_tokens или refusal, значит, что полного ответа нет, и покупатель уходит к человеку.

results — это выполнение просьб. Запускается каждая, а второй append кладёт все ответы в одно сообщение.

range(MAX_STEPS) — это предел разговора. Про него следующий блок.

Две просьбы сразу

Коллега может попросить о двух вещах за один раз. Поэтому он нумерует просьбы, а вы, отвечая, называете номер. Иначе ответы перепутаются.

Покупатель спрашивает про заказы 4417 и 5120. В одном ответе модели два поиска, поэтому код всегда идёт по списку, даже если обычно в нём один элемент.

Один ответ, две просьбы. Каждый результат называет id своей просьбы, и все они уходят одним сообщением.
Один ответ, две просьбы. Каждый результат называет id своей просьбы, и все они уходят одним сообщением.

В API Anthropic действуют три правила:

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

В других API та же идея под другими именами:

Anthropic Messages

OpenAI Responses

OpenAI Chat Completions

Модель просит

блок tool_use

элемент function_call

tool_calls в ответе

Номер просьбы

tool_use_id

call_id

tool_call_id

Вы отвечаете

tool_result в сообщении user

элемент function_call_output

сообщение с ролью tool

Когда инструмент падает

Коллега просит найти заказ 4471, а такого заказа нет. Вы так и говорите, и он спрашивает у покупателя верный номер. Бросать трубку вы бы не стали.

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

Неизвестный заказ — это информация, поэтому она возвращается модели как результат инструмента:

def execute(block, session):
    result = {"type": "tool_result", "tool_use_id": block.id}
    try:
        result["content"] = dispatch(block.name, block.input, session)
    except Exception as error:
        result["content"] = f"Failed: {error}"
        result["is_error"] = True
    return result

Пишите текст достаточно конкретно, чтобы модель могла выбрать, что делать дальше.

Что не решает модель

Коллега не сообщает вам, кто покупатель. Вы и так знаете это из сессии вошедшего пользователя.

Личность покупателя не аргумент инструмента. customer_id нет в схеме инструмента, dispatch берёт его из session.

if name == "lookup_order":
    return orders.lookup(order_id=args["order_id"],
                         customer_id=session.customer_id)

Модели умеют выдумывать аргументы: справочник OpenAI предупреждает о параметрах, которых нет в схеме. Strict mode приводит аргументы в соответствие со схемой. Но верны ли значения, он не говорит, поэтому ваш код их проверяет, а dispatch передаёт дальше только ожидаемые поля.

Вывод

Глубже

Parallel tool use (Anthropic)Как запускать несколько вызовов из одного ответа и отвечать на них.Handle tool calls (Anthropic)Результаты с `is_error` и правила порядка.Function calling (OpenAI)`call_id`, параллельные вызовы и strict mode у OpenAI.
‹ Назад
Предложить улучшениеНашёл ошибку