Создание цикла улучшения агента с использованием LangGraph и инструментов оценки LangSmith.
Пошаговый цикл LangGraph и LangSmith: сбор данных, три уровня оценки, кластеризация неудач, наборы данных для регрессионного анализа в офлайн-режиме и предварительная оценка перед выпуском.
Источники: Architecting the Agent Improvement Loop: Production-Grade Eval Pipelines
Внедрение агента в производство требует наименьших усилий. Но именно улучшение его функционала позже отличает хобби-решения от надежных систем. Многие команды выпускают первую версию, а затем месяцами отвечают на жалобы по одной. Каждый патч — это реакция на конкретный случай, каждое исправление ограничивается узкой областью, и агент в целом так и не совершенствуется. Постепенно накапливаются временные решения.
Обычная причина заключается в том, что с агентом обращаются как с обычным программным обеспечением. В обычных программах код объясняет, что делает система, поэтому для найдения ошибки достаточно просмотреть исходный код. Однако с агентами такое предположение не работает. Код описывает лишь то, что системе разрешено делать, в то время как записи отслеживают ее реальные действия. Именно этот разрыв лежит в основе любой серьезной системы оценки, включая те, которые описывает Anthropic в своих технических материалах, и LangChain в документации LangSmith.
Ниже приведен полный цикл улучшений для агента LangGraph. Он начинается с простого агента ReAct, записывает его следы в LangSmith, добавляет три уровня оценки, выявляет повторяющиеся паттерны сбоев, преобразует эти сбои в постоянный офлайн-набор тестов и проводит оценку регрессии перед выпуском любых исправлений. Каждый этап сопровождается кодом, объяснением и ожидаемым результатом.
Цикл работает следующим образом:
- Агент обрабатывает реальный запрос пользователя и генерирует запись, охватывающую каждый вызов LLM, вызов инструмента и промежуточный результат.
- Проверки на основе кода оценивают каждую запись с точки зрения формата и корректности использования инструментов, судья на основе LLM оценивает полезность и релевантность, а люди проверяют более тонкие сбои.
- Записи с низкими оценками группируются по паттернам сбоев, таким как неправильный инструмент, отклонение в логике рассуждений или неправильное понимание намерения пользователя.
Таким образом агент со временем становится лучше, а не просто накапливает патчи. Существует шесть этапов:
- Этап 1, основа: агент LangGraph ReAct с отслеживанием действий с помощью LangSmith.
- Этап 2, сбор данных: извлечение информации о работе в производственных условиях в виде структурированных записей.
- Этап 3, оценка: проверка кода, оценка с помощью большой языковой модели и проверка человеком.
- Этап 4, выявление закономерностей: преобразование оценённых данных в категории ошибок.
- Этап 5, тестовые наборы в автономном режиме: преобразование ошибок в набор данных, защищённый от регрессий.
Этап 1: агент-основа
На первом этапе создается небольшой агент поддержки клиентов, который отвечает на вопросы о счете. У него есть два инструмента: один используется для поиска информации о счете, а другой — для проверки недавних заказов; при этом применяется заранее созданный агент ReAct от LangGraph, являющийся стандартным шаблоном для агентов с вызовами инструментов. Включен режим отслеживания LangSmith, благодаря чему каждая операция, вызов LLM и вызов инструмента автоматически записываются.
Для работы этой системы необходимы три компонента:
- LangGraph StateGraph представляет собой граф, обеспечивающий связь между узлом, принимающим решения о действиях, и узлом, выполняющим инструменты. Вспомогательная функция
create_react_agentоборачивает этот шаблон.
LANGSMITH_TRACING=true. После этого каждый вызов LangChain или LangGraph генерирует запись отслеживания без изменений в коде.@tool из LangChain; они предоставляются агенту с именем и описанием, которые помогают выбирать подходящий инструмент.Настройка окружения
Сначала необходимо установить соответствующие библиотеки:
# Install the libraries we need
pip install langgraph langchain langchain-openai langsmith
Устанавливаются четыре пакета. langgraph обеспечивает среду выполнения графов и заранее скомпилированный агент ReAct. langchain предоставляет декоратор @tool и типы сообщений. langchain-openai оборачивает чат-модель OpenAI в интерфейс LangChain. langsmith — это библиотека-клиент, используемая для взаимодействия с бэкендом LangSmith с целью отслеживания и оценки.
Далее идут переменные окружения, которые включают функцию отслеживания без изменений в коде:
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=your_langsmith_key
export LANGSMITH_PROJECT=agent-improvement-loop
export OPENAI_API_KEY=your_openai_key
LANGSMITH_TRACING=true — это переключатель. LANGSMITH_PROJECT указывает название проекта, в котором группируются трейсы, что позволяет позже фильтровать их. Два остальных параметра представляют собой учетные данные для LangSmith и поставщика моделей.
После настройки этих четырех переменных любой вызов LangChain или LangGraph в процессе отправляет трейс в LangSmith. Нет необходимости в дополнительной обертке или модификации, поскольку отслеживание выполняется в фоновом режиме.
Создание агента
Сначала импорты:
# Step 1: Import the pieces we need
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
Импорты включают три компонента. ChatOpenAI оборачивает API для завершения чатов OpenAI для LangChain. Декоратор tool преобразует обычную функцию на Python в объект, который может использоваться агентом. create_react_agent — это готовый вспомогательный инструмент LangGraph, который формирует граф в стиле ReAct для вызова инструментов.
Затем идут два инструмента:
# Step 2: Define two tools the agent can call
@tool
def get_account_info(account_id: str) -> str:
"""Look up basic information for an account by account_id."""
fake_accounts = {
"A100": "Account A100: plan=pro, status=active, email=ada@example.com",
"A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
}
return fake_accounts.get(account_id, f"No account found with id {account_id}")
@tool
def get_recent_orders(account_id: str) -> str:
"""Return the three most recent orders for a given account_id."""
fake_orders = {
"A100": "Orders for A100: #9001 shipped, #9002 processing, #9003 returned",
"A200": "Orders for A200: no orders in the last 90 days",
}
return fake_orders.get(account_id, f"No orders for {account_id}")
Каждый инструмент принимает параметр account_id и возвращает строку. Документация к каждой функции имеет большое значение, поскольку нейросеть читает этот текст при принятии решения о том, какой инструмент вызвать. Декоратор извлекает имя функции, параметры и документацию для формирования схемы, которую видит модель. В реальной системе эти функции бы запрашивали данные из базы данных или API, но в демонстрационной версии используются жестко заданные строки, что обеспечивает автономность и воспроизводимость.
Наконец, собирается агент:
# Step 3: Wire the tools into a ReAct agent
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tools = [get_account_info, get_recent_orders]
agent = create_react_agent(model, tools)
Модель чата создается с температурой нуля для обеспечения воспроизводимости. Инструменты помещаются в список и передаются вместе с моделью в функцию create_react_agent, которая возвращает скомпилированный LangGraph StateGraph, уже содержащий шаг рассуждения, шаг выполнения инструмента и условные связи, соединяющие их.
Тестирование агента
Простой запрос позволяет его протестировать:
# Step 4: Invoke the agent with a simple query
result = agent.invoke({
"messages": [{"role": "user", "content": "What plan is account A100 on?"}]
})
for message in result["messages"]:
print(message.type, ":", message.content)
В вызове передаётся словарь с ключом messages. Это связано с тем, что функция create_react_agent опирается на заранее созданную схему MessagesState, состояние которой представляет собой просто список сообщений в чате, который агент расширяет по мере обработки информации и вызова инструментов. Затем в цикле выводится каждый тип сообщения вместе с его содержимым, что позволяет увидеть всю последовательность действий.
Ожидаемый вывод:
human : What plan is account A100 on?
ai :
tool : Account A100: plan=pro, status=active, email=ada@example.com
ai : Account A100 is on the pro plan.
Агент получил сообщение от пользователя, затем отправил сообщение от ИИ, которое запустило вызов инструмента (его содержимое пустое, поскольку в нем только вызов инструмента без текста). Инструмент вернул строку с данными аккаунта, и агент завершил работу ответом. При открытии LangSmith теперь виден один новый трейс в проекте agent-improvement-loop, при этом каждый шаг записывается.
В этом коде не было механизма для захвата трейсов. Как только в среде установлено LANGSMITH_TRACING=true, каждый вызов отправляет входные данные, результаты и промежуточные шаги в LangSmith. Именно эта возможность наблюдения является основой для дальнейшей работы цикла.
На первом этапе агент, выполняющий вызовы инструментов, работает в рамках LangGraph, причем при каждой запуске генерируется трейс. Однако трейс, находящийся в панели управления, пока не является сигналом о необходимости улучшений. Его необходимо извлечь и использовать, что и является задачей следующего этапа.
Этап 2: сбор трейсов
На этом этапе трейсы извлекаются из LangSmith и передаются в код для анализа. Трейс — это один полный запуск агента с входными данными, выходными данными, вызовами инструментов и информацией о задержках. Python-клиент LangSmith перечисляет запуски проекта, фильтрует их по временному интервалу и статусу, преобразуя их в объекты, которые могут быть использованы на этапе оценки.
Несколько терминов помогают лучше понимать код:
- Запуск — это отдельный записанный результат работы, будь то полный вызов агента или один вызов LLM внутри него. Один вызов агента формирует дерево запусков.
- Корневой запуск — это запуск высшего уровня одного вызова агента; именно он отображается, когда требуется одна запись на каждый запрос пользователя.
- Проект — это пространство имён, к которому относится запуск; оно задаётся через переменную
LANGSMITH_PROJECT. Фильтрация по проекту позволяет разделять запуски в продакшене и локальные запуски для разработки.
Загрузка недавних запусков
Сначала создается клиент:
# Step 1: Create a LangSmith client
from langsmith import Client
from datetime import datetime, timedelta
client = Client()
Client импортируется из пакета langsmith и создается без аргументов. Он самостоятельно считывает значение LANGSMITH_API_KEY из окружения, поэтому никакие учетные данные не передаются.
Затем выводятся последние запуски с правами root:
# Step 2: List recent root runs from our project
recent_runs = list(client.list_runs(
project_name="agent-improvement-loop",
is_root=True,
start_time=datetime.utcnow() - timedelta(days=1),
))
print(f"Found {len(recent_runs)} root runs in the last 24 hours")
client.list_runs вызывается с тремя фильтрами. project_name ограничивает поиск проектом, is_root=True сохраняет только вызовы агента верхнего уровня, а не каждый вложенный вызов LLM или инструмента, а start_time ограничивает результаты последними 24 часами. Итератор является ленивым, поэтому list() принуждает его к формированию реального списка.
Ожидаемый результат:
Found 42 root runs in the last 24 hours
Сохранение только важных полей
В результате прямой обработки с помощью LangSmith формируется множество полей, но для оценки требуются всего пять элементов: входные данные, выходные данные, время задержки, наличие ошибок и информация о дочерних процессах, указывающая на используемые инструменты.
# Step 3: Build a compact trace record from each run
def compact_trace(run):
return {
"run_id": str(run.id),
"inputs": run.inputs,
"outputs": run.outputs,
"error": run.error,
"latency_ms": (run.end_time - run.start_time).total_seconds() * 1000
if run.end_time else None,
"tool_calls": [
child.name for child in client.list_runs(
trace_id=run.trace_id, run_type="tool"
)
],
}
traces = [compact_trace(run) for run in recent_runs]
print(f"Collected {len(traces)} compact traces")
print("Example tool calls:", traces[0]["tool_calls"])
Этот вспомогательный инструмент принимает сырой результат выполнения и преобразует его в небольшой словарь. Параметры входа и выхода (run.inputs, run.outputs) содержат запрос пользователя и окончательный ответ, в то время как run.error равен None при успешном выполнении и строке при ошибке. Задержка определяется путем вычитания end_time из start_time. Для вызова инструментов требуется второй запрос в LangSmith с параметром run_type="tool" и тем же trace_id, что позволяет получить только результаты выполнения инструментов в данной иерархии заданий.
Ожидаемый результат:
Collected 42 compact traces
Example tool calls: ['get_account_info']
Идея не в том, чтобы скопировать трейсы из LangSmith и бросить его. Компактная версия просто удобна для оценки, в то время как полные трейсы остаются в LangSmith для углубленной диагностики. Сохранение подробного представления в панели управления и более лаконичного — для анализа — является разумным способом разделения возможностей наблюдения и оценки.
Проверка сборщика
# Step 4: Inspect the first trace end to end
import json
print(json.dumps(traces[0], indent=2, default=str))
Первый след сохраняется в виде отступленного JSON с использованием json.dumps. Аргумент default=str заставляет модуль json преобразовывать в строку всё то, что он не может сериализовать самостоятельно, включая типы datetime и UUID, которые возвращает LangSmith.
Ожидаемый результат:
{
"run_id": "b1d2e3f4-...",
"inputs": {"messages": [{"role": "user", "content": "What plan is account A100 on?"}]},
"outputs": {"messages": [{"role": "ai", "content": "Account A100 is on the pro plan."}]},
"error": null,
"latency_ms": 1423.51,
"tool_calls": ["get_account_info"]
}
Теперь каждая партия продукции имеет чистую, структурированную запись, которая будет дополнена на этапе оценки. Сбор трейсов стоит недорого, а главная сложность заключается в том, чтобы сделать их полезными. Многие команды останавливаются на панели управления и задаются вопросом, почему возможность наблюдения так и не приводит к улучшениям. Трейсы без оценок являются шумом, а трейс без оценки — просто строкой из журнала событий.
Этап 3: оценка трейсов
Три уровня оценки формируют единый дополненный трейс. Статья инженеров Anthropic об оценках описывает именно такую структуру: оценщики на основе кода, которые быстры, объективны и воспроизводимы; оценщики на основе моделей, которые гибки и масштабируемы; а также человеческие оценщики, которые являются золотым стандартом, но дороги. Здесь создаётся по одному примеру каждого типа.
- Проверки на основе кода — это быстрые детерминистические функции, которые подтверждают структурные факты, такие как использование агентом правильного инструмента или соответствие вывода ожидаемому формату. Они позволяют выявлять очевидные сбои без дополнительных затрат.
- Судья в виде большой языковой модели — это вторая модель, которая оценивает субъективные качества, такие как полезность, релевантность и тон. Она может обрабатывать большие объемы данных, поскольку работает с каждой записью, но её необходимо калибровать по человеческому мнению, прежде чем кто-либо сможет ей доверять.
- Человеческий аудит позволяет эксперту в данной области рассмотреть записи, которые автоматизация пропустила или оценила неконсистентно. Именно это служит эталоном, по которому калибруются остальные два уровня.
Уровень 1: корректность инструмента
# Step 1: Define a rule that says which tool SHOULD be called for a given query
import re
def expected_tool_for_query(query: str) -> str:
q = query.lower()
if re.search(r"\b(order|shipment|shipped|return)\b", q):
return "get_recent_orders"
if re.search(r"\b(plan|account|status|email)\b", q):
return "get_account_info"
return "any"
def score_tool_correctness(trace):
query = trace["inputs"]["messages"][0]["content"]
expected = expected_tool_for_query(query)
actual = trace["tool_calls"][0] if trace["tool_calls"] else None
if expected == "any":
return 1.0
return 1.0 if actual == expected else 0.0
Определены две функции. expected_tool_for_query применяет регулярные выражения к тексту запроса, чтобы определить, какой инструмент должен был им заняться. Это грубая эвристика, подходящая для выявления очевидных ошибок маршрутизации, но не подходящая для промышленного использования. score_tool_correctness берет запрос пользователя из трейса, определяет ожидаемый инструмент и сравнивает его с первым инструментом, который на самом деле использовал агент. Несоответствие получает оценку 0.0, а соответствие — 1.0.
В промышленной версии регулярные выражения могут быть заменены на более совершенный классификатор или движок правил, основанный на намерениях пользователя. Принцип остается прежним: сначала происходит детерминистическая проверка, а затем — дорогостоящая оценка с использованием LLM.
Уровень 2: судья в виде LLM для оценки полезности
# Step 2: Score qualitative properties with an LLM judge
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
judge_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
judge_prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert reviewer of AI agent responses. "
"Rate the response on a scale of 1 to 5 for helpfulness. "
"Respond with only a single integer, nothing else."),
("user", "User question: {question}\n\nAgent response: {response}\n\nScore:"),
])
def score_helpfulness(trace):
question = trace["inputs"]["messages"][0]["content"]
response = trace["outputs"]["messages"][-1]["content"]
chain = judge_prompt | judge_model
result = chain.invoke({"question": question, "response": response})
try:
return int(result.content.strip()) / 5.0
except ValueError:
return 0.0
Небольшая цепочка LangChain создается путем подачи шаблона запроса в модель-судью. Ограничение на длину сообщения системы заставляет судью возвращать только одно целое число от 1 до 5, что позволяет детерминированно интерпретировать результат. Внутри функции score_helpfulness взимаются данные вопроса пользователя и окончательного ответа агента из записи выполнения цепочки, запускается цепочка, а целое число преобразуется в число с плавающей точкой в диапазоне от 0 до 1, чтобы его можно было легко суммировать с оценкой инструмента. В качестве судьи используется gpt-4o-mini, поскольку он дешев и быстр.
Судьи на основе больших языковых моделей являются стохастическими, поэтому два запуска на одной и той же записи могут дать незначительно разные оценки. Рекомендации компании Anthropic предусматривают калибровку судьи с использованием набора данных с метками, добавленными людьми, прежде чем полагаться на нее, и третий слой именно этот набор и создает.
Слой 3: ревизия человеком в качестве очереди аннотаций
У LangSmith есть встроенная очередь аннотаций, где пользователи добавляют метки к трейсам в панели управления. Чтобы пример был самодостаточным, аналогичная структура моделируется в коде.
# Step 3: Model a human review signal
def score_human(trace, human_labels: dict) -> float | None:
run_id = trace["run_id"]
return human_labels.get(run_id)
human_labels = {
"b1d2e3f4-...": 1.0,
"c2e3f4g5-...": 0.0,
}
Функция ищет идентификатор выполнения в словаре с метками, указанными человеком. Если такая метка существует, она возвращается; в противном случае функция возвращает None, что означает, что элемент ещё не был проверен. В производственной среде этот словарь будет находиться за API очереди аннотаций LangSmith. Важно то, как ведут себя оценки: они редкие, поскольку их получают только некоторые записи; они имеют приоритет, так как переопределяют другие в случае разногласий; кроме того, они используются для калибровки системы оценки LLM.
Сочетание трёх слоев
# Step 4: Run all three scorers and attach scores to each trace
def enrich(trace):
trace["scores"] = {
"tool_correctness": score_tool_correctness(trace),
"helpfulness": score_helpfulness(trace),
"human": score_human(trace, human_labels),
}
return trace
enriched = [enrich(t) for t in traces]
print(json.dumps(enriched[0]["scores"], indent=2))
Функция enrich добавляет все три показателя к трейсу и возвращает его. Она применяется к каждому трейсу, собранному во время сбора данных, в результате чего формируется список обогащенных трейсов, каждый из которых содержит структурированный словарь scores. Регулярные выражения, основанные на правилах, определяют показатель tool_correctness, судья на основе LLM — показатель helpfulness, а значение human представляет собой число с плавающей точкой или None.
Ожидаемый результат:
{
"tool_correctness": 1.0,
"helpfulness": 0.8,
"human": null
}
Три оценки противоречат друг другу намеренно. Показатель корректности инструмента указывает на то, что был вызван правильный инструмент, а судья на основе ИИ говорит, что ответ заслуживает 4 из 5 баллов. Человек-рецензент, вступивший позже, может сказать, что инструмент был правильным, но формулировка не соответствовала тону бренда. Каждый уровень выявляет то, что упускают остальные, поэтому их комбинирование обеспечивает более полный анализ и достоверные результаты за различную цену.
Теперь каждый отчет содержит три оценки: одну, основанную на правилах, другую — оценку судьи ИИ и, при наличии, оценку человека. Оценки без структуры почти так же бесполезны, как их отсутствие, поэтому далее рассматриваются шаблоны.
Этап 4: поиск шаблонов сбоев
Как только в логах появляются данные о результатах, тот же тип сбоя начинает возникать снова и снова. Агент выбирает неподходящий инструмент для определенной формулировки запроса. Его логика дает сбои при обработке многоэтапных запросов. Ответы технически верны, но не отвечают на истинные потребности пользователя. Каждый тип сбоя требует своего способа устранения — будь то переписывание описания инструмента, обновление инструкций или изменение структуры рабочего процесса. Самая сложная часть — определение типа сбоя, а его устранение обычно происходит просто.
LangChain предлагает агента Insights, разработанного именно для масштабной обработки подобных проблем. Он обрабатывает логи, группирует их по причинам сбоев и помогает командам определить, что требует первоочередного внимания. Упрощенная версия этой же концепции реализована в приведенном ниже коде.
Фильтрация по причинам сбоев
# Step 1: Keep only traces where at least one score is low
def is_low_scoring(trace):
scores = trace["scores"]
if scores["human"] is not None and scores["human"] < 0.5:
return True
if scores["tool_correctness"] < 0.5:
return True
if scores["helpfulness"] < 0.6:
return True
return False
failures = [t for t in enriched if is_low_scoring(t)]
print(f"{len(failures)} of {len(enriched)} traces flagged as low scoring")
is_low_scoring выполняет три проверки. Оценка человека ниже 0,5 приводит к отнесению трейса к категории сбоев. В остальных случаях решение принимается на основе автоматических оценок: если точность инструмента ниже 0,5, это означает, что агент сделал неверный маршрутизацию, а если полезность ниже 0,6, это означает, что судья-LLM не одобрил ответ. Пороговые значения можно настроить, и это разумные стандартные значения для первого прохода. Фильтр обрабатывает дополненные трейсы из предыдущего этапа для формирования набора сбоев.
Ожидаемый результат:
9 of 42 traces flagged as low scoring
Группировка сбоев по категориям
Здесь сбои маркируются с помощью простого эвристического метода. В производственных условиях для этой цели можно использовать алгоритмы кластеризации или агента Insights Agent от LangChain, который именно это и делает на основе данных LangSmith.
# Step 2: Tag each failure with a category based on its scores
def categorize(trace):
scores = trace["scores"]
if scores["tool_correctness"] < 0.5:
return "wrong_tool"
if scores["helpfulness"] < 0.6 and scores["tool_correctness"] >= 0.5:
return "unhelpful_answer"
if scores["human"] is not None and scores["human"] < 0.5:
return "human_flagged"
return "other"
from collections import Counter
categories = Counter(categorize(t) for t in failures)
print(categories.most_common())
Функция categorize проверяет элементы в порядке приоритета. Неправильный инструмент представляет собой наиболее серьезную структурную проблему, поэтому он маркируется в первую очередь. Бесполезный ответ, сформированный с использованием правильного инструмента, указывает на проблемы с формулировкой запроса или генерацией ответа. Ошибки, замеченные человеком и прошедшие мимо автоматизации, попадают в отдельную категорию. Стандартный класс Counter в Python подсчитывает количество элементов в каждой категории; он импортируется прямо здесь, поскольку это его первое использование.
Ожидаемый результат:
[('wrong_tool', 5), ('unhelpful_answer', 3), ('human_flagged', 1)]
Категория указывает на решение проблемы. Пять случаев сбоев из-за неподходящего инструмента свидетельствуют о несоответствии описания инструмента или формулировки запроса. Три бесполезных ответа при использовании правильного инструмента указывают на проблему в самом запросе, генерирующем ответы. Сбой, замеченный только человеком, означает, что автоматизированные системы оценки что-то упустили, поэтому, вероятно, требуется новый эксперт.
Анализ отдельного шаблона
# Step 3: Print the inputs and outputs for every wrong_tool failure
wrong_tool = [t for t in failures if categorize(t) == "wrong_tool"]
for trace in wrong_tool[:3]:
print("QUERY:", trace["inputs"]["messages"][0]["content"])
print("TOOLS CALLED:", trace["tool_calls"])
print("RESPONSE:", trace["outputs"]["messages"][-1]["content"])
print("---")
Набор сбоев сузился до случаев wrong_tool, и цикл выводит запрос пользователя, фактически вызванные инструменты и окончательный ответ для первых трех из них. Именно здесь человек или более умный LLM изучает сбои и формулирует гипотезу, например, что каждый запрос, содержащий слово «subscription», запускает функцию get_recent_orders вместо get_account_info. Эта гипотеза становится целью для исправления.
Ожидаемый вывод:
QUERY: Is my subscription active?
TOOLS CALLED: ['get_recent_orders']
RESPONSE: I could not find any recent orders to confirm your subscription status.
---
QUERY: Tell me about my subscription to your product
TOOLS CALLED: ['get_recent_orders']
RESPONSE: There are no recent orders for this account.
---
Три примера на экране наглядно показывают эту закономерность. Понятие «подписка» семантически ближе к «статусу учетной записи», чем к «заказам», однако агент направляет запросы на основе поверхностного совпадения ключевых слов. Вероятное решение — внести однострочную правку в документацию функции get_account_info, добавив упоминание подписки, или создать краткое системное сообщение, объясняющее разницу.
Этот этап завершается описанием сценариев сбоев с указанием их количества, категорий и примеров. Исправление, сделанное сегодня, мало чего стоит, если на следующей неделе изменения другого инженера снова его нарушат, поэтому исправление необходимо подтвердить с помощью теста.
Этап 5: ноутбуки для тестирования в офлайн-режиме
Здесь сбои превращаются в статические тесты. Каждый реальный сбой в производственной среде добавляется в набор данных LangSmith в качестве примера, содержащего входные данные (запрос пользователя), ожидаемое поведение (правильный инструмент или приемлемый ответ) и необязательные метаданные, такие как категория сбоя. После сохранения данные остаются там навсегда, поэтому один и тот же сбой не может возникнуть снова.
Офлайн-оценка в LangSmith состоит из трех частей:
- Набор данных — это названный сбор примеров, каждый из которых содержит входные данные и, при необходимости, ожидаемые выходные.
- Целевая функция — это код, который проверяется; для агента это обычно его метод
.invoke. - Оценщики — это функции, которые оценивают выходные данные целевой функции по каждому примеру, аналогично оценщикам на этапе оценивания.
Создание набора данных
# Step 1: Create a LangSmith dataset for our agent
dataset_name = "agent-production-failures"
dataset = client.create_dataset(
dataset_name=dataset_name,
description="Production traces where the agent failed. Each example captures the user query and the expected tool call.",
)
print(f"Created dataset: {dataset.id}")
Тот же client из этапа сбора данных вызывает функцию create_dataset с именем и описанием, после чего возвращается объект набора данных, .id которого можно будет использовать позже. В реальных сценариях развертывания это делается с проверкой на отсутствие объекта перед его созданием, но в демонстрации предполагается первый запуск.
Ожидаемый результат:
Created dataset: 3f2a1b0c-...
Превращение неудач в примеры
# Step 2: Convert each failure trace into a dataset example
examples = []
for trace in failures:
query = trace["inputs"]["messages"][0]["content"]
expected_tool = expected_tool_for_query(query)
examples.append({
"inputs": {"question": query},
"outputs": {"expected_tool": expected_tool},
"metadata": {"category": categorize(trace), "source_run_id": trace["run_id"]},
})
client.create_examples(dataset_id=dataset.id, examples=examples)
print(f"Added {len(examples)} examples to {dataset_name}")
Код проходит по каждой трассе сбоя и создаёт пример словаря с тремя полями. Поле inputs содержит вопрос пользователя, поле outputs — поведение, которое необходимо протестировать (в данном случае это инструмент, который должен был быть вызван), а поле metadata содержит категорию и исходный идентификатор запуска, чтобы каждый пример можно было отследить до его источника. Один вызов client.create_examples загружает всё сразу.
Ожидаемый результат:
Added 9 examples to agent-production-failures
В реальных сценариях развертывания обычно позволяют рецензенту определять ожидаемый результат, поскольку гипотеза на основе регулярных выражений является лишь отправной точкой. Человек, утверждающий каждый пример, обеспечивает более надежные эталонные данные, и именно для этого предназначена очередь аннотаций LangSmith.
Оценщик регрессии
# Step 3: Write an evaluator that checks the agent's tool call against the expected tool
def tool_match_evaluator(inputs: dict, outputs: dict, reference_outputs: dict):
actual_messages = outputs.get("messages", [])
tool_called = None
for m in actual_messages:
if getattr(m, "tool_calls", None):
tool_called = m.tool_calls[0]["name"]
break
expected = reference_outputs["expected_tool"]
score = 1.0 if (expected == "any" or tool_called == expected) else 0.0
return {"key": "tool_match", "score": score}
tool_match_evaluator соответствует формату, ожидаемому LangSmith: он получает inputs примера, фактические outputs агента и reference_outputs примера. Он проходит по сообщениям вывода агента, находит первое, содержащее вызов инструмента, и извлекает название этого инструмента. Затем он сравнивает его с ожидаемым инструментом и возвращает словарь с key и score. Значение key становится названием метрики, отображаемой в LangSmith. Структура соответствует официальному руководству по быстрой настройке оценки в LangSmith.
Целевая функция
# Step 4: Wrap our agent in a function that takes a LangSmith example and returns its output
def run_agent(inputs: dict) -> dict:
result = agent.invoke({
"messages": [{"role": "user", "content": inputs["question"]}]
})
return result
run_agent преднамеренно оставлен минимальным. Исходя из inputs примера набора данных, он вызывает агента и возвращает любой полученный в виде словаря результат, что соответствует требованиям функции evaluate от LangSmith. Агент, который вызывается, был создан на этапе формирования основы.
Преднамеренно сделан так, чтобы обертка была простой. Любой код, помещённый внутрь неё, выполняется при каждой оценке и увеличивает затраты, тогда как простая передача данных позволяет оценщику тестировать сам агент, а не обертку.
Таким образом, набор данных существует, оценщик подключён, и каждая неудача в производстве теперь становится постоянным примером для тестирования. На заключительном этапе этот оценщик запускается перед любыми изменениями.
Этап 6: закрытие цикла
Это ворота регрессии. Агент получает изменения, например обновленное описание инструмента, предназначенное для устранения найденной ранее ошибки маршрутизации «подписок». Перед выпуском вся база данных прошлых сбоев пропускается через новую версию. LangSmith сохраняет оба результата в качестве экспериментов и показывает, пример за примером, где новая версия улучшила ситуацию, а где дала сбои. Выпуск происходит только тогда, когда новая база сравнения превосходит старую и ничто из ранее исправленного не дало сбоев.
Проведение оценки базовой версии
# Step 1: Run the evaluator across the dataset with the current agent
from langsmith.evaluation import evaluate
experiment_results = evaluate(
run_agent,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="agent-v1-baseline",
max_concurrency=4,
)
evaluate импортируется из langsmith.evaluation. Ему передаются цель run_agent, имя набора данных (которое LangSmith связывает с набором данных предыдущей стадии), список оценщиков, префикс эксперимента для идентификации запуска, а также параметр параллельности равный четырем, чтобы оценка выполнялась параллельно. Функция возвращает объект с результатами эксперимента, указывающий на запуск в LangSmith.
Ожидаемый вывод:
View the evaluation results for experiment: 'agent-v1-baseline-...' at:
https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
9/9 runs | avg tool_match: 0.33
Из девяти примеров с ошибками текущий агент по-прежнему неправильно обрабатывает большинство из них, и это является базовым уровнем.
Отправка исправления и повторная оценка
Исправление, предложенное на этапе анализа шаблонов, заключается в переписывании документации функции get_account_info на языке, соответствующем формату подписок.
# Step 2: Update the tool docstring to teach the agent about subscriptions
@tool
def get_account_info_v2(account_id: str) -> str:
"""Look up basic information for an account, including plan, status,
subscription tier, and contact email. Use this tool for any question
about the account itself, including subscription status."""
fake_accounts = {
"A100": "Account A100: plan=pro, status=active, email=ada@example.com",
"A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
}
return fake_accounts.get(account_id, f"No account found with id {account_id}")
tools_v2 = [get_account_info_v2, get_recent_orders]
agent_v2 = create_react_agent(model, tools_v2)
def run_agent_v2(inputs: dict) -> dict:
result = agent_v2.invoke({
"messages": [{"role": "user", "content": inputs["question"]}]
})
return result
Инструмент для получения информации о аккаунте был перепроектирован с более подробной документацией, в которой прямо указывается статус подписки. Поскольку именно документация читается большими языковыми моделями при выборе инструмента, её расширение напрямую решает проблему с маршрутизацией. Агент был пересоздан с использованием обновленного инструмента и помещён в новую функцию run_agent_v2. Все остальные элементы остались без изменений.
# Step 3: Run the evaluator on the v2 agent
experiment_results_v2 = evaluate(
run_agent_v2,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="agent-v2-docstring-fix",
max_concurrency=4,
)
Идентичная оценка выполняется снова на том же наборе данных с использованием того же оценщика, причем меняется только целевое значение. Новый experiment_prefix позволяет отличать такие запуски в интерфейсе LangSmith, чтобы их можно было сравнить с базовым вариантом. LangSmith пропускает каждый пример через агента версии v2, сохраняет полную информацию о его работе и оценивает каждый результат с помощью оценщика.
Ожидаемый результат:
View the evaluation results for experiment: 'agent-v2-docstring-fix-...' at:
https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
9/9 runs | avg tool_match: 0.89
Рейтинг вырос с 0,33 до 0,89. Восемь из девяти ошибок устранены, а девятая — это по-настоящему неоднозначный запрос, который, возможно, следует поместить в очередь для аннотации человеком.
Before (v1): After (v2):
wrong_tool: 5 wrong_tool: 1
unhelpful: 3 unhelpful: 0
human: 1 human: 0
avg score: 0.33 avg score: 0.89
Несколько факторов сделали это сравнение надежным:
- Исходная оценка показала конкретный результат еще до внесения каких-либо изменений в код.
- Исправление заключалось в одной точечной правке — добавлении одного документирования.
- Повторная оценка позволила измерить точное влияние этой правки на выборку реальных сбоев.
- Набор тестовых данных оставался неизменным, поэтому сравнение проводилось на равных условиях.
- Каждый исходный пример сбоя теперь закреплен в качестве теста, так что последующие изменения не могут незаметно нарушить его работу.
Именно поэтому процесс улучшения становится компузитным. Каждое исправление добавляет новые примеры в набор данных, и каждая будущая версия тестируется на всех прошлых сбоях. Со временем процесс развертывания становится быстрее, а не медленнее, поскольку уверенность растет вместе с объемом данных.
Теперь весь цикл выполняется полностью от начала до конца. Собираются данные о ходе работы, к которым добавляются оценки; проявляются определённые закономерности; сбои превращаются в тесты в автономном режиме; оценка проводится до выпуска продукта, а изменения внедряются только тогда, когда показатели улучшаются. Каждый цикл начинается с более высокого уровня.
Один полный ежедневный запуск
Один оператор может выполнить весь цикл один раз при наличии свежих данных производства.
# Full loop driver
def run_improvement_loop():
# 1. Pull traces
runs = list(client.list_runs(
project_name="agent-improvement-loop",
is_root=True,
start_time=datetime.utcnow() - timedelta(days=1),
))
traces = [compact_trace(r) for r in runs]
# 2. Enrich with scores
enriched = [enrich(t) for t in traces]
# 3. Find failures and categorize them
failures = [t for t in enriched if is_low_scoring(t)]
categories = Counter(categorize(t) for t in failures)
# 4. Add failures to the dataset
new_examples = [{
"inputs": {"question": t["inputs"]["messages"][0]["content"]},
"outputs": {"expected_tool": expected_tool_for_query(
t["inputs"]["messages"][0]["content"])},
"metadata": {"category": categorize(t), "source_run_id": t["run_id"]},
} for t in failures]
if new_examples:
client.create_examples(dataset_id=dataset.id, examples=new_examples)
# 5. Re-evaluate the current agent against the full dataset
result = evaluate(
run_agent_v2,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="daily-regression",
)
return {
"traces_collected": len(traces),
"failures_found": len(failures),
"by_category": dict(categories),
"examples_added": len(new_examples),
}
print(run_improvement_loop())
Эта функция объединяет шесть этапов воедино. Она загружает данные за прошлый день, оценивает их, классифицирует сбои, добавляет новые сбои в качестве примеров для набора данных и выполняет оценку регрессии. В результате возвращается небольшой словарь с показателями, который можно записывать в журнал или отправлять в Slack ежедневно.
Ожидаемый результат:
{
"traces_collected": 186,
"failures_found": 12,
"by_category": {"wrong_tool": 4, "unhelpful_answer": 6, "human_flagged": 2},
"examples_added": 12,
"regression_score": 0.87
}
Результаты показывают: за день было зафиксировано 186 примеров генерации контента без изменений в коде агента, причем у двенадцати из них был получен хотя бы один низкий балл качества. Теперь количество бесполезных ответов превысило количество ошибочных вызовов инструментов, что указывает на то, что исправление проблем с маршрутизацией принесло результат, и следующее улучшение должно касаться генерации ответов. Балл регрессии остался на уровне 0,87, значит, ни одно из изменений за неделю не нарушило ранее существовавших проблем. Двенадцать новых примеров были добавлены в постоянный набор данных, где они помогут избежать повторений.
Пять улучшений, которые стоит внедрить в ближайшее время
Каждое из этих улучшений связано с функцией LangChain, LangGraph или LangSmith, которую можно включить без необходимости перестраивать всю архитектуру.
- Замените классификатор на основе регулярных выражений на методы эмбеддингов.
OpenAIEmbeddingsпозволяет генерировать эмбеддинги для каждого запроса, благодаря чему трассы сбоев группируются по семантическому сходству, а не по правилам ключевых слов, что помогает выявлять более тонкие проблемы, такие как несоответствие тона. - Замените встроенный судью на основе LLM библиотеку OpenEvals. В руководстве по быстрому запуску LangSmith используется
openevalsс готовыми промптами, такими какCORRECTNESS_PROMPT; он работает эффективнее, чем ручно созданный судья, и предоставляет варианты без эталонных данных для онлайн-мониторинга. - Добавьте оценку многократных диалогов. Механизм оценки многократных диалогов в LangSmith анализирует всю переписку, а не отдельные ответы, что позволяет выявлять смещения в логике рассуждений, которые ускользают от однократных оценщиков в чат-агентах.
LANGSMITH_PROJECT=agent-canary в течение дня и затем сравнив её показатели с показателями контрольного проекта, вы убедитесь, что она заменит продакшн-версию только в том случае, если будет значительно лучше.Затем цикл повторяется с каждым разом на более высокой отправной точке, что обеспечивает лучшие трейсы, более точные оценки и улучшенный агент.