Deep Agents, кэширование узлов, обработка ошибок, наблюдаемость с LangSmith.
Deep Agents, кэширование, устойчивость к ошибкам и наблюдаемость. Всё, что отличает работающий прототип от системы, которой можно доверить пользователей.
Deep Agents — это готовая обвязка поверх LangChain для агентов, которые работают долго и решают объёмные задачи. Из коробки в ней собрано то, что иначе пришлось бы подключать по частям: планирование через список задач, файловая система, субагенты, управление контекстом.
from deepagents import create_deep_agent
agent = create_deep_agent(
model="openai:gpt-4o-mini",
tools=[search],
system_prompt="Ты помощник для исследований.",
)Помимо переданных инструментов агент получает встроенные. Файловые операции (ls, read_file, write_file, edit_file, glob, grep) дают ему рабочее пространство: длинные материалы он складывает в файлы, а не держит в контексте. Инструмент делегирования создаёт временных субагентов под подзадачи, а write_todos позволяет вести план работы.
Планирование стоит объяснить отдельно, потому что оно выглядит странно: агент пишет список задач сам себе. Смысл в том, что план попадает в контекст и удерживает цель на длинной дистанции. Без него агент к двадцатому шагу нередко забывает, зачем начинал.
Файловые инструменты работают через бэкенд, и выбор бэкенда определяет, где живут данные.
StateBackend держит файлы в состоянии графа: они исчезают вместе с диалогом, зато ничего не пишется на диск. FilesystemBackend работает с реальными файлами и правилами доступа. StoreBackend кладёт содержимое в долговременное хранилище LangGraph, поэтому файлы переживают диалог. CompositeBackend распределяет операции между несколькими бэкендами.
from deepagents.middleware.filesystem import FilesystemMiddleware
agent = create_deep_agent(
model="openai:gpt-4o-mini",
middleware=[FilesystemMiddleware(backend=backend, tools=["read_file", "ls"])],
)Параметр tools ограничивает набор файловых операций. Агенту, которому достаточно читать, запись не выдают: чем меньше прав, тем меньше способов сломать что-нибудь.
Выбирать Deep Agents стоит, когда задача длинная и объёмная (исследование, разбор кодовой базы, подготовка отчёта). Для короткого агента с тремя инструментами это лишний вес: create_agent даст тот же результат прозрачнее.
Если узел дорогой, а входные данные повторяются, его результат можно кэшировать. Политика задаётся на узле, само хранилище кэша при компиляции:
from langgraph.cache.memory import InMemoryCache
from langgraph.types import CachePolicy
builder.add_node("search", search_function, cache_policy=CachePolicy(ttl=60))
graph = builder.compile(cache=InMemoryCache())Ключ кэша считается по входному состоянию узла, поэтому попадания случаются только при точном совпадении входа. Отсюда практическое следствие: кэшировать узел, который принимает всю историю сообщений, бессмысленно, ведь она меняется каждый шаг. Кэш работает на узлах с узким входом вроде «текст запроса» или «идентификатор документа».
Значение ttl подбирают по природе данных: справочник курсов валют живёт минуты, описание товара часы.
Отказ на любом шаге — это норма, а не исключение: провайдер отвечает 429, внешний API отваливается по таймауту, модель возвращает аргументы не по схеме. Разница между прототипом и системой в том, что происходит дальше.
Повторы вызовов закрываются готовыми мидлварями:
from langchain.agents.middleware import ModelRetryMiddleware, ToolRetryMiddleware
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[fetch_data],
middleware=[
ModelRetryMiddleware(max_retries=3),
ToolRetryMiddleware(max_retries=2),
],
)Повтор помогает при временных сбоях. При устойчивом отказе провайдера нужен запасной путь, и его даёт ModelFallbackMiddleware: при исчерпании попыток запрос уходит другой модели.
Ошибки инструментов лучше не превращать в падение запуска. ToolErrorMiddleware отдаёт модели текст ошибки как результат вызова, и та может исправить аргументы или выбрать другой инструмент. Для агента это разница между «упал» и «попробовал иначе».
У графа есть лимит рекурсии, считающий супершаги. По умолчанию он достаточно велик, и упереться в него означает, что агент уже долго крутится вхолостую и успел потратить заметные деньги.
graph.invoke(inputs, config={"recursion_limit": 25})Более осмысленный контроль дают лимиты вызовов: ModelCallLimitMiddleware ограничивает обращения к модели в пределах запуска и всего диалога, ToolCallLimitMiddleware делает то же для инструментов, в том числе для конкретного инструмента по имени. Ограничение на дорогой инструмент часто полезнее общего лимита.
Отдельно существует управляемое значение оставшихся шагов: узел может посмотреть, сколько бюджета осталось, и завершить работу аккуратно, вместо того чтобы упереться в лимит с исключением.
Агент — это недетерминированная система, и логами уровня «вызвана функция» его не отладить. Нужна трассировка, где видно дерево вызовов с промптами, ответами, аргументами инструментов, токенами и задержками.
Подключение делается переменными окружения, код менять не нужно:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="ваш-ключ"
export LANGSMITH_PROJECT="my-agent-prod"После этого каждый запуск попадает в проект целиком: вызовы модели с входом и выходом, вызовы инструментов с аргументами и результатами, переходы состояния, ошибки. Отдельный проект для продакшена стоит завести сразу, иначе эксперименты разработчиков смешаются с реальным трафиком.
Три вопроса, на которые трассировка отвечает быстрее любых логов: почему агент выбрал этот инструмент (видно промпт целиком), где ушло время (видны задержки по шагам), откуда взялся ответ (видна вся цепочка).
Стоимость запуска. Сумма токенов по шагам умножается на тариф, и здесь регулярно обнаруживается, что половину расхода даёт один инструмент, возвращающий слишком много.
Число шагов до ответа. Рост среднего числа шагов — это ранний признак деградации: агент начал ходить кругами.
Доля неуспешных вызовов инструментов. Если модель постоянно ошибается в аргументах конкретного инструмента, проблема в его описании или схеме, а не в модели.
Задержка по перцентилям. Среднее скрывает хвост, а пользователи замечают именно его.
Трассировка показывает, что произошло, но не отвечает на вопрос, стало ли лучше после правки промпта. Для этого нужны наборы примеров и оценка на них.
Практика простая. Соберите два-три десятка реальных запросов с ожидаемым поведением, прогоняйте их после каждого значимого изменения, сравнивайте результаты. Проверять можно и программно (вызван ли нужный инструмент, есть ли в ответе обязательные поля), и моделью-судьёй по критериям.
Ценность даже маленького набора велика: он превращает разговор «мне кажется, стало лучше» в измеримый факт и ловит регрессии, которые ручное тестирование пропускает.
Кэширование узла с историей сообщений на входе. Попаданий не будет, а сложность появится.
Повторы без ограничения. Три повтора по три у вложенных уровней дают девять запросов и минуты ожидания вместо быстрого отказа.
Трассировка только в разработке. Именно в продакшене нужны данные о том, что агент делает с реальными запросами.
Отсутствие набора примеров. Без него любое изменение промпта — это ставка вслепую.
Далее: Тестирование и оценка агентов