До сих пор агент отвечал текстом, который удобно читать человеку. Но часто ответ агента читает программа: заявку нужно записать в базу, оценку тональности — показать на графике, категорию обращения — отправить нужному отделу. Разбирать свободный текст регулярными выражениями ненадёжно: модель то добавит вежливую фразу, то поменяет порядок полей.
Вторая проблема — контроль. Агент может зациклиться на вызовах инструментов, а инструмент — упасть с исключением и оборвать весь запуск. В первом модуле вы решали это вручную через max_steps и try/except. В этом уроке вы научитесь получать от агента ответ строго по схеме Pydantic и подключать middleware, которые ограничивают число шагов и перехватывают ошибки инструментов.
Что уже умеем
В уроке 1.5 вы получали ответ модели по схеме Pydantic, в уроке 1.6 ограничивали цикл агента параметром max_steps. В модуле 2 вы собрали агента на create_agent с инструментами, системным промптом и памятью, а в прошлом уроке написали первый middleware с @before_model. Сегодня объединим эти навыки.
Что вы узнаете в рамках статьи
- как получить ответ агента по схеме Pydantic через
response_format; - где искать структурированный ответ в результате агента;
- как ограничить число вызовов модели с помощью
ModelCallLimitMiddleware; - как ограничить вызовы инструментов через
ToolCallLimitMiddleware; - как перехватывать ошибки инструментов через
@wrap_tool_call.
Ключевые термины
response_format— параметрcreate_agent, в который передаётся схема финального ответа агента.structured_response— ключ результата агента, где лежит проверенный объект схемы.- Встроенный middleware — готовый класс LangChain, решающий типовую задачу: лимиты, повторы, суммаризация.
- Хук — момент цикла агента, в который выполняется middleware: перед моделью, после модели, вокруг вызова инструмента.
@wrap_tool_call— декоратор middleware, который оборачивает каждый вызов инструмента и может обработать его результат или ошибку.
Ответ по схеме: response_format
Опишите нужный ответ моделью Pydantic, как в уроке 1.5, и передайте её в параметр response_format. Агент, как и раньше, может вызывать инструменты сколько нужно, но в конце обязан вернуть данные по схеме. LangChain проверит их и положит готовый объект в result["structured_response"].
from typing import Literal
from pydantic import BaseModel, Field
from langchain.agents import create_agent
class Ticket(BaseModel):
"""Разобранное обращение клиента."""
category: Literal["доставка", "оплата", "возврат", "другое"]
order_id: int | None = Field(description="Номер заказа, если он указан")
urgent: bool = Field(description="Нужна ли срочная реакция")
summary: str = Field(description="Суть обращения в одном предложении")
agent = create_agent(
model="openai:gpt-6-luna",
tools=[],
system_prompt="Ты разбираешь обращения клиентов интернет-магазина.",
response_format=Ticket,
)
result = agent.invoke({"messages": [{"role": "user", "content":
"Заказ 1077 должен был прийти вчера, а его нет! Завтра улетаю, срочно!"}]})
ticket = result["structured_response"]
print(ticket)
print(ticket.category, ticket.urgent)
Что увидим в консоли:
category='доставка' order_id=1077 urgent=True summary='Клиент не получил заказ 1077 вовремя и просит срочно решить вопрос.'
доставка True
ticket — настоящий объект Ticket: поля имеют правильные типы, category принимает только одно из четырёх значений, а order_id — число. Такой результат можно сразу сохранить в базу без ручного разбора.
Для OpenAI LangChain использует встроенный механизм structured outputs провайдера, который вы видели в уроке 1.5. Для моделей без такой поддержки схема передаётся как специальный инструмент: этот способ можно включить явно, обернув схему в ToolStrategy из langchain.agents.structured_output. В обоих случаях код чтения ответа одинаковый.
Структурированный ответ вместе с инструментами
response_format не мешает агенту работать с инструментами. Модель сначала собирает данные, а затем оформляет итог по схеме.
from langchain.tools import tool
ORDERS = {1042: "доставлен", 1077: "в пути, задержка 2 дня"}
@tool
def order_status(order_id: int) -> str:
"""Возвращает статус заказа по его номеру."""
return ORDERS.get(order_id, f"Ошибка: заказ {order_id} не найден")
class Answer(BaseModel):
order_id: int
status: str = Field(description="Статус заказа из инструмента")
reply: str = Field(description="Ответ клиенту, не длиннее двух предложений")
agent = create_agent(
model="openai:gpt-6-luna",
tools=[order_status],
response_format=Answer,
)
result = agent.invoke({"messages": [{"role": "user", "content": "Где мой заказ 1077?"}]})
answer = result["structured_response"]
print(answer.status, "|", answer.reply)
Что увидим: в пути, задержка 2 дня | Ваш заказ 1077 в пути, доставка задерживается на 2 дня. Приносим извинения. Программа получила и данные для системы (status), и готовый текст для клиента (reply). Ищите ответ именно в structured_response: последнее сообщение в result["messages"] при этом может быть служебным.
Ограничение шагов: ModelCallLimitMiddleware
Помните max_steps из урока 1.6? В LangChain его роль играет встроенный middleware. ModelCallLimitMiddleware считает обращения к модели и останавливает агента при достижении лимита. Параметр run_limit ограничивает обращения за один вызов invoke, а thread_limit — за весь диалог, но он работает только с checkpointer. ToolCallLimitMiddleware аналогично ограничивает вызовы инструментов — всех сразу или одного, указанного в tool_name.
from langchain.agents.middleware import ModelCallLimitMiddleware, ToolCallLimitMiddleware
agent = create_agent(
model="openai:gpt-6-luna",
tools=[order_status],
middleware=[
ModelCallLimitMiddleware(run_limit=5, exit_behavior="end"),
ToolCallLimitMiddleware(tool_name="order_status", run_limit=3),
],
)
Что увидим: обычные вопросы агент решает за 2–3 шага, и лимиты незаметны. Если модель зациклится, на пятом обращении агент остановится. При exit_behavior="end" он завершит работу спокойно, без исключения, а при "error" выбросит исключение, которое вы обработаете в своём коде. Лимит на order_status не даёт агенту перебирать номера заказов: четвёртый вызов будет заблокирован, и модель получит сообщение об этом вместо результата.
Ошибки инструментов: @wrap_tool_call
В уроке 2.2 вы возвращали текст ошибки из самого инструмента. Но неожиданные исключения всё равно случаются: сеть упала, сторонняя библиотека выбросила ошибку. Чтобы не оборачивать в try/except каждый инструмент, напишите один middleware с @wrap_tool_call. Он получает запрос на вызов и функцию handler, которая выполняет инструмент.
from collections.abc import Callable
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
from langchain.tools.tool_node import ToolCallRequest
@wrap_tool_call
def handle_tool_errors(
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage],
) -> ToolMessage:
name = request.tool_call["name"]
print(f"Вызов {name} с аргументами {request.tool_call['args']}")
try:
return handler(request)
except Exception as e:
return ToolMessage(
content=f"Инструмент {name} временно недоступен ({e}). Сообщи клиенту.",
tool_call_id=request.tool_call["id"],
)
agent = create_agent(
model="openai:gpt-6-luna",
tools=[order_status],
middleware=[handle_tool_errors, ModelCallLimitMiddleware(run_limit=5)],
)
Что увидим: при каждом вызове в консоли появится строка с именем инструмента и аргументами — это простое логирование. Если инструмент выбросит исключение, агент не упадёт: модель получит ToolMessage с понятным текстом и скажет клиенту, что сервис временно недоступен. Обратите внимание на tool_call_id: он связывает ответ с конкретным запросом модели, как вы делали вручную в уроке 1.6.
Middleware передаются списком и могут сочетаться: обработка ошибок, лимиты, обрезка истории из прошлого урока. Каждый решает одну задачу, а вместе они делают агента надёжным.
Частые ошибки
- Искать структурированный ответ в последнем сообщении. Проверенный объект лежит в
result["structured_response"], а не вresult["messages"][-1].content. - Делать все поля схемы обязательными. Если номера заказа в обращении нет, модель будет вынуждена его выдумать. Необязательные поля объявляйте как
int | None. - Запускать агента без лимита шагов. Без
ModelCallLimitMiddlewareзациклившийся агент тратит деньги, пока не сработает внутренний предел фреймворка. - Отдавать модели сырой текст исключения. В нём бывают пути к файлам и детали инфраструктуры. Пишите в
ToolMessageкороткое понятное сообщение.
Наиболее частые вопросы
- Можно ли использовать
response_formatбез инструментов? — Да, тогда агент работает как надёжный извлекатель данных: один вызов модели и проверенный объект на выходе. - Что будет, если модель вернёт данные не по схеме? — LangChain проверит ответ и при ошибке попросит модель исправиться. Поведение настраивается параметром
handle_errorsуToolStrategy. - В каком порядке выполняются middleware? — В порядке списка. Если результат зависит от порядка, держите middleware как можно более независимыми друг от друга.
- Чем
run_limitотличается отthread_limit? —run_limitсчитает обращения за один вызовinvoke,thread_limit— за весь диалог в потоке и требует checkpointer.
Итоги
Поздравляем с завершением второго модуля! Теперь ваш агент отдаёт программе проверенные данные по схеме, не уходит в бесконечный цикл и не падает от ошибки инструмента. Вы освоили LangChain 1.x целиком: модели и сообщения, инструменты, системный промпт, память и middleware — а значит, можете собрать надёжного агента для реальной задачи.
Дальше агенту понадобятся знания, которых нет в модели: ваши документы, регламенты, база знаний. В третьем модуле вы начнёте с эмбеддингов и семантического поиска — основы, на которой строятся RAG-агенты, отвечающие по вашим данным со ссылками на источники.