SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-26

Как настроить self-hosted evals для AI-агентов

Создайте собственную систему оценки AI-агентов без сторонних сервисов. Руководство по внедрению цикла тестирования: от сбора трассировок до отслеживания pass rate в каждом коммите.

Что такое self-hosted evals для AI-агентов

Self-hosted evals для AI-агентов — это четыре компонента, которые вы храните в собственном репозитории: файл с сохраненными кейсами, скрипт для запуска агента на этих кейсах, набор проверок для оценки каждого ответа и таблица результатов, по которой можно делать выборки. Ни один из этих элементов не требует стороннего вендора. Весь цикл состоит из нескольких сотен строк кода на Python и одного файла SQLite.

Агент работал в демо, потому что вы сами подобрали пять входных параметров. Он сломался на второй неделе, потому что изменилась строка промпта, модель или описание инструмента, а никакие метрики это не отслеживали. Цикл оценки превращает субъективное «стало хуже» в объективное «процент успешных ответов упал с 58 из 60 до 51 из 60 на коммите 4f1c9ab».

Цикл состоит из четырех этапов, и этому руководству соответствует по одному разделу на каждый этап: сбор реальных трассировок, перевод интересных случаев в разряд кейсов, оценка каждого кейса при каждом изменении и сохранение процента успешных ответов рядом с коммитом, который их сгенерировал. Этот цикл работает независимо от того, на чем вы запускаете агента, а фреймворки для self-hosted агентов, заслуживающие внимания различаются в основном тем, какой объем трассировки они предоставляют вам автоматически.

Почему агент перестает работать на второй неделе

Агент состоит из промпта, модели, набора определений инструментов и контекста, который извлекается во время выполнения. Все четыре компонента могут измениться без внесения правок в код приложения, поэтому обычный code review не выявит проблем.

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

Вторая причина — модель. Фиксируйте точную строку модели, которую вы отправляли при каждом запуске, claude-haiku-4-5-20251001, а не сокращенное название, которое вы держите в уме. Падение процента успешных ответов в день смены модели можно диагностировать только тогда, когда модель указана в строке лога.

Третья причина — инструменты. Переформулировка описания инструмента меняет условия, при которых модель решает его вызвать. Если ваши инструменты поступают через MCP-серверы, запущенные на VPS, схема находится в другом процессе, поэтому она может измениться без каких-либо изменений в вашем репозитории. Четвертая причина — поиск: тот же вопрос попадает в индекс, который был перестроен за ночь, и ответ формируется на основе нового документа.

Создание эталонного набора из уже собранных трассировок

Не придумывайте тестовые сценарии. Берите их из реального трафика. Если вы уже используете self-hosted Langfuse tracing для вашего агента, каждый запрос сохраняется вместе с входными данными, вызовами инструментов и ответами. Это именно тот исходный материал, который необходим для создания сценария.

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

export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
  "$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
  | jq '.data[0]'

Прочитайте одну запись, прежде чем писать код для парсинга. Строки возвращаются в поле data, но имена полей, содержащих вопрос и ответ, зависят от того, как ваш агент инструментирует спаны. Поэтому ориентируйтесь на то, что видите фактически, а не на то, что ожидали увидеть. Затем создайте сценарии вручную, по одному JSON-объекту на строку, в файле evals/cases.jsonl:

{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}

Пять правил, которые помогут поддерживать набор в рабочем состоянии:

  • Для начала достаточно от 40 до 80 сценариев. Если их меньше 20, один нестабильный тест будет изменять процент успешных прохождений на 5 пунктов, и на такие скачки показателей перестанут обращать внимание.
  • Любая исправленная ошибка из production должна превращаться в сценарий в день исправления. Эта привычка позволяет набору развиваться в нужном направлении.
  • Один сценарий — одно проверяемое поведение. Сценарий, который одновременно проверяет сумму возврата и тон общения, не даст полезной информации при сбое.
  • Поле id никогда не меняется, так как идентификатор используется для сравнения результатов текущего запуска с результатами прошлого месяца.
  • Выполняйте деидентификацию перед коммитом. Этот файл попадет в git, поэтому удалите имена клиентов и любые номера заказов, которые вам не принадлежат.

Сначала выполняйте детерминированные проверки, так как они бесплатны

Любая задача с однозначным ответом должна проверяться простым утверждением (assertion). Это не требует вызова модели, не создает затрат и исключает двусмысленность. Детерминированные проверки позволяют выявить структурные регрессии, которые нарушают работу систем вокруг вашего агента: JSON не парсится, инструмент не был вызван, в ответе содержится запрещенная фраза или ответ не содержит ссылок на источники.

Только одна функция должна «знать» о вашем агенте. Все остальные компоненты тестового окружения должны быть универсальными.

import json, os, urllib.request

def run_agent(case):
    req = urllib.request.Request(
        os.environ["AGENT_URL"],
        data=json.dumps({"input": case["input"]}).encode(),
        headers={"content-type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=120) as resp:
        return json.load(resp)


def deterministic(case, result):
    text = result.get("output", "")
    called = [c["name"] for c in result.get("tool_calls", [])]
    failures = []
    for tool in case.get("must_call", []):
        if tool not in called:
            failures.append(f"tool not called: {tool}")
    for phrase in case.get("must_not_include", []):
        if phrase.lower() in text.lower():
            failures.append(f"forbidden phrase: {phrase}")
    if len(called) > case.get("max_tool_calls", 12):
        failures.append(f"too many tool calls: {len(called)}")
    return failures

Учитывайте бюджет вызовов инструментов в этом списке. Если агент сегодня решает задачу за 3 вызова, а завтра за 11, это регрессия, даже если итоговый ответ верен, так как вы платите за каждый совершенный вызов.

LLM в роли судьи и четыре способа, которыми это приводит к ошибкам

Все, что проходит проверку утверждениями, требует оценки со стороны. LLM-судья — это второй вызов модели: она получает вопрос, ответ агента и один критерий, после чего выносит вердикт. Это единственный практический способ оценить, отвечает ли ответ на то, что спросил пользователь.

Четыре правила делают судью пригодным к работе:

  • Бинарный вердикт, никогда не используйте шкалу от 1 до 10. Шкала почти всегда дает 7 или 8, поэтому число не меняется, и вы ничего из этого не извлекаете.
  • Один критерий на вызов. Спрашивайте либо о сумме возврата, либо о тоне, но не о том и другом сразу.
  • Предоставляйте судье ожидаемый ответ, если он существует. Оценивать по эталону гораздо проще, чем оценивать абстрактно.
  • Принудительно задавайте формат вывода и строго его парсите.
from anthropic import Anthropic

client = Anthropic()  # reads ANTHROPIC_API_KEY from the environment


def judge_prompt(case, output):
    return (
        "You grade one answer against one criterion.\n"
        "Reply with JSON only, in this exact shape:\n"
        '{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
        f"Criterion: {case['rubric']}\n"
        f"Question: {case['input']}\n"
        f"Answer: {output}\n"
        "Length is not a criterion. Judge only the criterion above."
    )


def judge(case, output, model):
    msg = client.messages.create(
        model=model,
        max_tokens=200,
        messages=[{"role": "user", "content": judge_prompt(case, output)}],
    )
    return json.loads(msg.content[0].text)

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

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

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

Предвзятость к позиции. Если вы используете судью для сравнения двух ответов, A и B, поменяйте их местами и запустите снова. Вердикт, который меняется при смене мест, означает, что парное сравнение пока небезопасно для данного критерия.

Дрейф критериев. Расплывчатые критерии делают судей слишком уступчивыми. «Является ли ответ полезным» пропускает почти всё. «Указана ли в ответе сумма возврата в долларах» пропускает только то, что вы имели в виду. Переписывайте каждый критерий до тех пор, пока он не будет указывать на конкретный проверяемый факт.

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

Оценка бюджетными моделями с эскалацией до передовых

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

ChartCost to judge 1,000 eval cases, list prices, August 2026
The data behind this chart
[
  {
    "label": "Haiku 4.5, Batch API",
    "usd_per_1000_judge_calls": "0.90"
  },
  {
    "label": "Haiku 4.5",
    "usd_per_1000_judge_calls": "1.80"
  },
  {
    "label": "Sonnet 5",
    "usd_per_1000_judge_calls": "3.60"
  },
  {
    "label": "Opus 5",
    "usd_per_1000_judge_calls": "9.00"
  }
]

Эти цифры предполагают около 1200 входных токенов и 120 выходных токенов на один вызов оценщика, что является реалистичным объемом для одного вопроса, одного ответа и одного критерия. Оценка 1000 случаев стоит 1.80 долларов США при использовании Claude Haiku 4.5 и 9.00 при использовании Claude Opus 5. Разница кажется незначительной, пока вы не произведете расчеты. Набор из 60 случаев, оцениваемый при каждом коммите, при 40 коммитах в неделю, дает 2400 вызовов оценщика в неделю еще до запуска ночных заданий.

К задачам оценки применимы две скидки, и они суммируются. Запуски оценки не являются интерактивными, поэтому Batch API сокращает цены на ввод и вывод вдвое в обмен на асинхронную доставку, что отражено в первой строке таблицы. Рубрикатор и инструкции идентичны байт в байт в каждом вызове, поэтому эффективно работает prompt caching: чтение из кэша стоит одну десятую от базовой цены ввода, а запись в кэш на пять минут стоит 1,25 от базового ввода, поэтому кэш окупается после первого же попадания. Это прейскурантные цены Anthropic по состоянию на август 2026 года, а Sonnet 5 доступна по вводным ценам до 31 августа 2026 года, поэтому после этой даты третья полоса вырастет.

Иерархия проверок:

  • Детерминированные проверки для каждого случая. Отсутствие затрат на API.
  • Оценка небольшой моделью для случаев, прошедших предыдущие проверки.
  • Оценка передовой моделью только в тех случаях, когда небольшая модель сообщает о неудаче или о прохождении с низкой уверенностью.
  • Проверка человеком на небольшой выборке раз в неделю.
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"


def grade(case, result):
    hard = deterministic(case, result)
    if hard:
        return False, "deterministic", "; ".join(hard)
    first = judge(case, result["output"], CHEAP)
    if first["verdict"] == "pass" and first["confidence"] == "high":
        return True, CHEAP, first["reason"]
    second = judge(case, result["output"], STRICT)
    return second["verdict"] == "pass", STRICT, second["reason"]

Этот подход жертвует точностью оценки ради экономии, поэтому измеряйте этот компромисс, а не предполагайте его. Раз в месяц оценивайте весь набор строгим оценщиком и сравнивайте два столбца. Если они расходятся более чем в нескольких случаях, ваш рубрикатор слишком нестрог для небольшой модели, и исправлять нужно именно рубрикатор. Контроль расходов самого агента — это отдельная задача, описанная в контроле расходов для AI-агента на VPS.

Отслеживание процента успешных прохождений тестов с течением времени в вашей системе

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

CREATE TABLE IF NOT EXISTS results (
  run_id      TEXT NOT NULL,
  ran_at      TEXT NOT NULL,
  git_sha     TEXT NOT NULL,
  agent_model TEXT NOT NULL,
  case_id     TEXT NOT NULL,
  passed      INTEGER NOT NULL,
  graded_by   TEXT NOT NULL,
  reason      TEXT
);
SELECT run_id, git_sha, agent_model,
       count(*) AS cases,
       round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;

Загрузите схему с помощью sqlite3 evals/results.db < evals/schema.sql, а затем проанализируйте тренды командой sqlite3 -box evals/results.db < evals/passrate.sql. Год ежедневных запусков для 60 тестовых случаев дает около 22,000 строк, поэтому хранилище данных не превратится в отдельный сложный проект. В статье Использование SQLite в продакшене на VPS описаны настройки, которые становятся важными, если этот файл используется совместно на нескольких машинах.

Программа запуска тестов выводит ту же информацию для человека:

run 2026-08-05T09:14:22Z  sha 4f1c9ab  model claude-sonnet-5  58/60 pass (96.7%)
FAIL refund-double-charge  deterministic: tool not called: create_refund
FAIL pto-policy-question   judge(opus): reply gives no dollar amount

Запускайте набор тестов для изменений, которые могут нарушить работу агента, — то есть при редактировании промптов, смене моделей и изменении инструментов, а не при каждом коммите в репозитории. Хук pre-push охватывает быстрый поднабор тестов:

cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-push

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

Выборочная проверка человеком вместо сплошной

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

Пятнадцать случаев по три минуты каждый занимают 45 минут в неделю. Это позволяет вносить корректировки в критерии оценки там, где ваше мнение расходится с мнением судьи, а также выявлять новые типы ошибок, которые никто не мог предвидеть. Записывайте вердикт человека в ту же таблицу, где graded_by установлено в human, чтобы проверка совпадения оценок судьи и человека превратилась в простой запрос к базе данных, а не в попытку вспомнить детали.

Что может выйти из строя в самой системе тестирования

anthropic.RateLimitError при первом полном запуске. Одновременный запуск 60 тестов превышает лимит запросов или токенов для вашего тарифного плана. Ограничьте параллелизм 4 воркерами и перенесите ночной запуск на использование Batch API.

json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) от проверяющего модуля. Модель ответила текстом или обернула JSON в блок кода. Повторите попытку один раз, затем зафиксируйте случай как ошибку. Никогда не засчитывайте ошибку парсинга как успешный результат, так как набор тестов, превращающий ошибки в успехи, будет стремиться к 100%, в то время как качество работы агента будет падать.

Нестабильные тесты (flaky cases). Один и тот же входной набор проходит в одном запуске и проваливается в другом, так как агент использует вероятностную выборку при генерации ответа. Запустите нестабильный тест 3 раза и запишите долю успешных попыток, вместо того чтобы удалять тест. Если тест проходит 2 раза из 3, это реальная проблема надежности, с которой столкнется пользователь.

Устаревание эталонных данных (golden set rot). Кто-то редактирует ожидаемый ответ, чтобы «зеленый» статус теста вернулся. Проверяйте diff-файлы для evals/cases.jsonl так же тщательно, как и изменения в коде агента, поскольку этот файл является вашим формальным определением корректности.

Набор тестов, который никогда не выдает ошибок. Если уровень успешных прохождений держится на отметке 100% в течение месяца, значит, набор перестал соответствовать продукту. Выгрузите 10 последних трассировок, найдите те, с которыми агент справился плохо, и добавьте их в набор. Затем намеренно сломайте что-нибудь и убедитесь, что результат запуска стал «красным» — это проверка, которую мутационное тестирование применяет к набору тестов, и единственный способ убедиться, что ваш набор тестов все еще эффективен.

FAQ

Сколько кейсов нужно для набора оценки AI-агента?

Начните с 40–80 кейсов и расширяйте набор на основе реальных сбоев. При количестве менее 20 один нестабильный результат меняет процент успешных прохождений на 5 пунктов, поэтому показатель теряет информативность. После нескольких сотен кейсов каждый запуск требует реальных денег и времени, а предельная полезность нового кейса невелика. Важен не объем, а доля известных вам типов производственных сбоев, которые хотя бы раз представлены в наборе.

Можно ли доверять LLM-судье при оценке агента?

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

Какая модель должна проводить оценку?

Оценивайте дешево и повышайте уровень при необходимости. Детерминированные проверки бесплатны, поэтому они выполняются первыми для каждого кейса. Небольшая модель справляется с очевидными успешными результатами. Только сбои и вердикты с низкой уверенностью передаются передовой модели. По прейскуранту на август 2026 года оценка 1000 кейсов стоит около 1.80 долларов США при использовании Claude Haiku 4.5 и около 9.00 при использовании Claude Opus 5. Поскольку запуски оценки асинхронны, использование Batch API сокращает обе суммы вдвое.

Заменяют ли оценки мониторинг в production?

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