Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Тестирование и оценка агентов
testing_and_evaluation

Тестирование и оценка агентов

Тесты инструментов, узлов и маршрутизаторов, подмена модели заглушкой, имитация инструментов, наборы примеров и эксперименты в LangSmith, программные оценщики и модель-судья.

Тестирование и оценка агентов

Агент недетерминирован: на один и тот же запрос он может ответить по-разному. Это не отменяет тестирование, но меняет то, что именно проверяют.

#Что здесь ломается по сравнению с обычным кодом

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

Выход в разделении. Детерминированную часть системы (инструменты, узлы, маршрутизаторы) тестируют обычными тестами со строгими проверками. Недетерминированную часть (решения модели) оценивают статистически на наборе примеров.

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

#Инструменты тестируются как обычные функции

Инструмент — это функция с аннотациями. Модели в тесте нет, поэтому проверяют её как любой другой код: краевые случаи, ошибки, форматы.

def test_search_returns_summary(): result = search_database.invoke({"query": "оплата", "limit": 5}) assert "оплата" in result assert len(result) < 2000 # результат не должен раздувать контекст

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

Если инструмент декорирован @tool, вызывать его в тесте нужно через invoke со словарём аргументов: декоратор превращает функцию в объект инструмента.

Отдельно стоит проверять схему аргументов. Она попадает в промпт, и её поломка ломает агента, хотя код формально работает:

def test_tool_schema_is_stable(): schema = search_database.args_schema.model_json_schema() assert set(schema["properties"]) == {"query", "limit"} assert schema["properties"]["query"]["description"]

#Узлы и маршрутизаторы тоже обычные функции

Узел графа принимает состояние и возвращает обновление. Никакого рантайма для этого не нужно, если внутри нет обращения к модели:

def test_classify_detects_billing(): result = classify({"text": "Не пришёл счёт за апрель", "category": "", "reply": ""}) assert result == {"category": "billing"}

Маршрутизатор проверяется так же и особенно этого заслуживает: ошибка в нём отправляет весь поток не туда, а на глаз такое незаметно.

def test_route_sends_to_tools_when_tool_calls_present(): state = {"messages": [AIMessage(content="", tool_calls=[{"name": "search", "args": {}, "id": "1"}])]} assert should_continue(state) == "tools"

#Граф с подменённой моделью

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

Один из способов подмены — это мидлварь, перехватывающая вызов модели:

from langchain.agents.middleware import wrap_model_call from langchain.messages import AIMessage def stub_model(responses): calls = iter(responses) @wrap_model_call def _stub(request, handler): return next(calls) return _stub def test_agent_calls_tool_then_answers(): agent = create_agent( model="openai:gpt-4o-mini", tools=[get_weather], middleware=[stub_model([ AIMessage(content="", tool_calls=[{"name": "get_weather", "args": {"city": "Казань"}, "id": "1"}]), AIMessage(content="В Казани солнечно."), ])], ) result = agent.invoke({"messages": [{"role": "user", "content": "Погода в Казани?"}]}) assert "солнечно" in result["messages"][-1].text

Первый ответ заглушки просит вызвать инструмент, второй завершает цикл. Тест выполняется мгновенно, не тратит токены и падает ровно тогда, когда ломается логика.

#Имитация инструментов

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

from langchain.agents.middleware import LLMToolEmulator agent = create_agent( model="openai:gpt-4o-mini", tools=[charge_card, send_email], middleware=[LLMToolEmulator(tools=["charge_card", "send_email"])], )

Это удобно на ранней стадии, когда интеграции ещё не готовы, и для проверки поведения агента в сценариях, которые трудно воспроизвести по-настоящему (отказ платежа, пустой результат поиска).

#Наборы примеров и оценка

Тесты отвечают на вопрос «работает ли код». Вопрос «стало ли лучше после правки промпта» они не закрывают. Для этого нужен набор примеров и прогон по нему.

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

from langsmith import Client client = Client() dataset = client.create_dataset( dataset_name="support-agent-v1", description="Реальные обращения в поддержку с ожидаемым исходом", ) client.create_examples( dataset_id=dataset.id, examples=[ { "inputs": {"question": "Где мой счёт за апрель?"}, "outputs": {"category": "billing", "must_call": "find_invoice"}, }, ], )

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

def called_expected_tool(inputs: dict, outputs: dict, reference_outputs: dict) -> bool: tools_used = {tc["name"] for msg in outputs["messages"] for tc in getattr(msg, "tool_calls", [])} return reference_outputs["must_call"] in tools_used def target(inputs: dict) -> dict: return agent.invoke({"messages": [{"role": "user", "content": inputs["question"]}]}) client.evaluate( target, data="support-agent-v1", evaluators=[called_expected_tool], experiment_prefix="prompt-v3", )

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

#Какими бывают оценщики

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

Оценщики на основе модели нужны там, где проверяется качество текста: соответствует ли ответ вопросу, вежлив ли тон, нет ли выдуманных фактов. Модель-судья получает вопрос, ответ и критерии и выставляет оценку.

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

#Онлайн-оценка

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

Обратная связь от пользователей (палец вверх или вниз) тоже фиксируется как оценка и потом становится источником новых примеров для набора: те запросы, где агент ошибся, самые ценные.

#Как встроить это в разработку

Юнит-тесты инструментов и узлов гоняются на каждый коммит: они быстрые и детерминированные.

Тесты графа с подменённой моделью тоже подходят для каждого коммита по той же причине.

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

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

#Частые ошибки

Сравнение ответа со строкой-эталоном. Тест начнёт мигать при первом же обновлении модели.

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

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

Доверие единичной оценке судьи. Смотреть нужно на распределение по набору, а не на вердикт по одному примеру.

Прогон набора без фиксации версии промпта. Сравнивать эксперименты можно, только если понятно, что именно менялось между ними.

Устранение неисправностей

Далее: Продакшен: развёртывание и лучшие практики