Оценка¶
Eval объявляет, что значит «хорошо» и как это измерить. Запуск даёт
отслеживаемый ран плюс сохранённый результат по каждому кейсу, поэтому регрессия
— это дифф между двумя ранами, а не цифра, которую кто-то помнит.
from mlango.evals import Eval, contains_all, token_f1, used_tool
from support.agents import Support
from support.datasets import SupportCases
class AnswerQuality(Eval):
"""Отвечает ли агент именно по документации?"""
class Meta:
dataset = SupportCases
target = Support
input_field = "question"
expected_field = "answer"
case_id_field = "id"
scorers = {
"overlap": token_f1,
"cited": contains_all("docs/"),
"searched": used_tool("search_docs"),
}
threshold = 0.6
python manage.py evaluate support.AnswerQuality
python manage.py evaluate support.AnswerQuality --show-failures
python manage.py evaluate support.AnswerQuality --min-pass-rate 0.9 # для CI
Опции Meta¶
| Опция | Назначение |
|---|---|
dataset |
Откуда берутся кейсы |
target |
Agent, Model или любой вызываемый объект |
input_field |
Поле со входом, по умолчанию "input" |
expected_field |
Поле с эталонным ответом, если он есть |
case_id_field |
Поле, идентифицирующее кейс, чтобы результаты сходились между ранами |
scorers |
{имя: скорер} — каждый записывается отдельно |
threshold |
Средний балл, начиная с которого кейс считается пройденным |
max_cases |
Ограничить число кейсов |
fail_fast |
Остановиться на первой ошибке вместо записи и продолжения |
Скореры¶
Скорер принимает (output, expected) и возвращает float в [0, 1] или bool.
| Скорер | Проверяет |
|---|---|
exact_match, iexact_match |
Равенство, опционально без учёта регистра |
contains |
Ожидаемый текст встречается в выводе |
contains_all(*needles) |
Долю присутствующих подстрок |
not_contains(*needles) |
Что ни одна не встречается |
regex_match(pattern) |
Совпадение с шаблоном |
json_equals |
Структурное совпадение двух JSON |
json_subset |
Долю ожидаемых ключей с совпадающими значениями |
numeric_close(tolerance) |
Числа в пределах допуска |
length_between(min, max) |
Длину вывода в диапазоне |
token_f1 |
F1 по пересечению слов — дешёвая замена сходству |
used_tool(name) |
Что агент обратился к конкретному инструменту |
llm_judge(agent, rubric=...) |
Оценку другим агентом по рубрике |
Объявлять несколько — это и есть смысл: когда ран регрессирует, видно, какой именно критерий сломался.
scorers = {
"correct": exact_match,
"concise": length_between(maximum=400),
"no_hedging": not_contains("как ИИ", "я не могу"),
}
Оценивать поведение, а не только текст¶
used_tool получает весь AgentRun, а не его текст, поэтому оценка может
проверять, что агент сделал:
Любой скорер может подписаться на это, выставив fn.wants_run = True.
Судья на LLM¶
from mlango.evals import llm_judge
from support.agents import Judge
scorers = {
"helpfulness": llm_judge(
Judge(),
rubric="Поставь 1, если ответ решает вопрос, и 0, если нет.",
)
}
Судью просят вернуть только число, а всё непарсящееся получает 0, а не бросает исключение — одно запутавшееся суждение не должно ронять весь прогон.
Свой скорер¶
def mentions_price(output, expected) -> bool:
"""True, когда ответ приводит сумму из эталона."""
return any(t in str(output) for t in str(expected).split() if t.startswith("₽"))
Своя логика¶
Переопределяйте хуки, когда декларации недостаточно:
class AnswerQuality(Eval):
class Meta:
dataset = SupportCases
target = Support
input_field = "question"
def cases(self):
"""Только сложные."""
return SupportCases.objects.filter(difficulty="hard")
def predict(self, case):
"""Запустить цель как вам угодно."""
return Support().run(case.question, session_id=f"eval-{case.id}")
def score(self, case, output) -> dict:
return {"cited": case.source in output.output}
def decide(self, scores) -> bool | None:
"""Превратить оценки скореров в pass или fail."""
return scores["cited"]
Чтение отчёта¶
report = AnswerQuality.evaluate()
report.total
report.passed
report.pass_rate
report.mean_scores() # {'overlap': 0.71, 'cited': 0.5}
report.failures() # все непройденные кейсы
report.run.uuid
Каждый кейс сохраняется со входами, выводом, ожидаемым значением, оценками по каждому скореру и — для агентов — трейсом, который он породил. Поэтому падение в админке ведёт прямо к пошаговому трейсу, вызвавшему его.
Ошибки записываются, а не роняют¶
Кейс, бросивший исключение, записывается со стектрейсом, и прогон продолжается.
Один сломанный кейс не должен выбрасывать результаты остальных девяноста девяти.
Поставьте fail_fast = True, если предпочитаете остановиться.
В непрерывной интеграции¶
Возвращает ненулевой код ниже порога, чего достаточно, чтобы уронить пайплайн. Для обучения моделей аналогичный предохранитель — колбэк: