6.3 · Набор из 50–100 вопросов с эталонами
Неделя 6 · Evals · ⏱ ~4 ч · Проект
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»Тестовый набор (golden dataset) — фундамент всех экспериментов проекта 3. Каждое утверждение «реранкер помог» или «чанки по заголовкам лучше» будет стоять на этом файле, и если набор кривой, все выводы тоже кривые. На собеседовании спросят «как вы собрали eval-набор и откуда уверенность в эталонах?», и ответ «70 вопросов, каждый проверен вручную, четыре типа, все ловушки базы знаний, с привязкой к источнику» звучит профессионально.
Что такое golden dataset
Заголовок раздела «Что такое golden dataset»Это фиксированный, версионируемый список примеров: вход (вопрос), ожидаемый результат (эталонный ответ) и то, что нужно для автоматической проверки (где в базе знаний лежит ответ). Аналогия из обычной разработки: фикстуры в tests/, на которых гоняются тесты. Отличие в том, что ожидаемый результат — не точное значение, а смысл, который проверяют метрики и судья.
Начало у нас уже есть: eval/golden.jsonl из 5.1 · Переформулировка запроса и метаданные на 20 строк {"q", "category", "must_contain"}. Эти строки проверяют только поиск: нашёлся ли кусок с нужными словами. Теперь доводим набор до 50–100 вопросов и добавляем к каждому эталонный ответ, чтобы проверять и сам ответ ассистента. Сначала разберёмся, какие вопросы нужны (типы и покрытие), потом откуда их взять, как писать эталон и привязку к источнику, и в конце соберём всё в схему записи с валидатором.
Качество набора важнее размера. 60 точных, разнообразных, однозначных вопросов полезнее 300 сгенерированных наспех: шумный эталон даёт шумную метрику, и улучшение уже не отличить от случайности.
Четыре типа вопросов
Заголовок раздела «Четыре типа вопросов»Вопросы различаются тем, сколько мест базы нужно для ответа и есть ли он там вообще. Эти четыре типа мы уже встречали в 6.2, когда генерировали синтетику.
Фактические (factual). Ответ — один факт из одного раздела. «Сколько дней на возврат невскрытого чая?», «сколько стоит доставка 3 кг в другой город?» (одна ячейка таблицы тарифов). Самые простые, но их большинство в реальной жизни.
Сравнительные (comparative). Требуют фактов с двух страниц. «Чем отличается гарантия на лампу „Свет“ и „Свет мини“?», «От какой суммы доставка бесплатная?» (разные цифры в delivery.md и promo.md). Для поиска это тяжело: нужно найти два куска, а векторный поиск часто приносит оба с одной страницы.
Многошаговые (multi_hop). Ответ собирается из нескольких фактов одной или разных страниц. «„Свет мини“ перестала включаться через месяц — это возврат или гарантия, и как подать претензию?»
Без ответа в базе (unanswerable). В kb/ ответа нет: не по теме («какая служба доставки самая надёжная?»), про конкретный заказ («где мой заказ 1042?»), про то, чего магазин не продаёт («гарантия на кофемашины»). Эталон: фраза отказа «В базе знаний нет ответа на этот вопрос.» и передача оператору. Без таких вопросов не узнать, галлюцинирует ли модель, когда поиск принёс нерелевантное.
Покрытие: матрица набора
Заголовок раздела «Покрытие: матрица набора»Когда типы понятны, решаем, сколько вопросов каждого типа нужно. Набор должен покрывать то, что реально спрашивают покупатели, и то, где система ломается. Составляем матрицу «тип × категория» (категории — из front matter kb/) и следим за её заполнением:
| Тип \ Категория | delivery | payment | returns | warranty | promo | orders | products | Всего |
|---|---|---|---|---|---|---|---|---|
| factual | 4 | 3 | 4 | 3 | 3 | 3 | 4 | 24 |
| comparative | — | — | — | — | — | — | — | 12 (пары страниц) |
| multi_hop | 2 | 1 | 2 | 2 | 1 | 1 | 3 | 12 |
| unanswerable | — | — | — | — | — | — | — | 10 |
| Итого | ~58 + синтетика |
Цифры — ориентир, не закон. Главное, чтобы ни одна ячейка не была пустой и ни один тип не доминировал.
Вторая ось — ловушки базы знаний, которые мы заложили в kb/ в 4.3 и 4.4. Размечаем их тегами и тоже считаем. Эталон ловушки описывает поведение, а не только факт:
| Тег | Эталон | Минимум |
|---|---|---|
archived |
по returns.md, а не по returns-2025.md; если покупатель сам спрашивает про старые правила — по архиву с пометкой «правила старые» |
3 |
table |
одна ячейка таблицы тарифов или сроков гарантии | 5 |
similar_products |
факт именно того товара: «Свет» или «Свет мини», MUG-NORTH или MUG-SOUTH |
4 |
conflict |
порог бесплатной доставки: обе цифры, delivery.md и promo.md в цитатах |
2 |
action_request |
«оформите возврат», «дайте промокод»: правило из базы, без обещаний действий | 3 |
Кроме ловушек, тегами полезно размечать дополнительные «срезы»: язык вопроса (ru/en), разговорный стиль и опечатки, точный идентификатор (LAMP-LIGHT-MINI, TEA15) или только смысл, числа (тарифы, сроки, суммы). Потом в 8.4 будет видно, что ошибки кучкуются по таким срезам.
Откуда брать вопросы
Заголовок раздела «Откуда брать вопросы»Матрицы говорят, сколько вопросов нужно; заполняем их из четырёх источников:
- Сама база знаний и бриф. Читаем страницы
kb/иkb-brief.mdглазами покупателя и пишем его словами, а не заголовками страниц. - Логи проекта 2. Вопросы, которые мы сами задавали ассистенту через
ask.py,app.pyили бота. - Ошибки. Вопросы, на которых проект 2 ответил плохо (список из 6.1, строки
eval/ANSWERS.mdс «нет»). Обязательно включаем их: набор, где система всё делает правильно, бесполезен. - Синтетика, проверенная вручную в 6.2. Она уже сгенерирована в схеме этого набора (см. «Схема записи» ниже), так что при слиянии остаётся проставить
split.
Настоящие вопросы клиентов (если есть свои данные) берём только переписанными в общий вопрос, без личных данных.
Эталонный ответ: как писать
Заголовок раздела «Эталонный ответ: как писать»К каждому вопросу пишем эталон: с ним судья в 6.4 будет сравнивать ответ ассистента. Эталон должен быть проверяемым. Плохо: «Доставка в другой город стоит недорого и зависит от веса». Хорошо: «Курьером в зону 2 посылка 1–3 кг стоит 470 ₽ [kb/delivery.md#rates-by-weight-and-zone]». Правила:
- Короткий: 1–3 предложения с ключевым фактом.
- Числа с единицами (₽, кг, дни), артикулы, промокоды и названия служб доставки — точно как в
kb/. - Если ответ зависит от условий (вес, зона, способ оплаты, вскрыта ли упаковка) — указываем условие.
- Добавляем поле
must_include: 1–3 ключевых факта, без которых ответ неверен. Судья в 6.4 будет проверять именно их, это делает его строже и стабильнее.
Не путаем: must_contain — что содержит найденный кусок (поиск), must_include — что содержит ответ (генерация).
Привязка к источнику, устойчивая к чанкингу
Заголовок раздела «Привязка к источнику, устойчивая к чанкингу»Эталон проверяет ответ; осталось записать, где в базе этот ответ лежит. Для метрик поиска нужно знать, какой кусок правильный. Наивный вариант — хранить ключ чанка chunk_key. Проблема: он считается как sha256(url + section + text), и в 8.2 при смене стратегии чанкинга все ключи станут другими. Поэтому храним:
source_urls— разделы базы знаний в том же виде, чтоurlчанка:kb/<file>.md#<anchor>;evidence— список дословных цитат изkb/(по одной на каждый нужный факт; у сравнительного вопроса — минимум по одной с каждой страницы);source_chunk_keys— ключи в текущемindex.db, как удобство.
Кусок считается релевантным, если он содержит цитату (после нормализации пробелов и регистра). Тогда при любом чанкинге правильные чанки пересчитываются автоматически. must_contain так не умеет: он требует все строки в одном куске, а у вопроса на две страницы их два. Нюанс: если цитату разрезало границей чанков, ни один чанк её целиком не содержит. Держим цитаты короткими (одна фраза с фактом) и считаем попаданием чанк, содержащий большую часть цитаты, например 80% её символов подряд. У таблиц цитата — строка таблицы целиком: «| 1–3 | 350 ₽ | 470 ₽ | 590 ₽ |».
Схема записи
Заголовок раздела «Схема записи»Теперь всё, о чём шла речь выше, собираем в одну запись. eval/golden.jsonl, одна строка — один JSON-объект. Поля недели 5 остаются, чтобы её скрипты продолжали работать; остальные добавляем:
{ "id": "q-017", "q": "скок будет стоить отправить 3 кило в другой город курьером?", "category": "delivery", "must_contain": ["1–3", "470"], "reference_answer": "Курьерская доставка в зону 2 (другие города) для посылки 1–3 кг стоит 470 ₽.", "must_include": ["470 ₽", "зона 2"], "evidence": ["| 1–3 | 350 ₽ | 470 ₽ | 590 ₽ |"], "source_urls": ["kb/delivery.md#rates-by-weight-and-zone"], "source_chunk_keys": ["3f9a…"], "type": "factual", "handoff": null, "difficulty": "medium", "tags": ["table", "colloquial", "numbers", "ru"], "origin": "manual", "reviewed": true, "split": "dev", "notes": ""}Важно: эталон в примере — иллюстрация формата. Тарифы, пороги и якоря у каждого свои, эталоны пишем по своей kb/.
Для unanswerable: reference_answer — ровно фраза отказа REFUSAL, evidence, source_* и must_contain пустые. Новое поле handoff — причина, с которой код передаёт вопрос оператору: "refused" (модель отказала) или "personal_data" (сработала contains_personal_data, облачная модель не вызывалась — так с «где мой заказ 1042?»). У отвечаемых вопросов handoff: null.
dev и holdout
Заголовок раздела «dev и holdout»Делим набор на две части:
- dev (~70%) — на нём смотрим ошибки, крутим промпты, выбираем конфигурацию.
- holdout (~30%) — не смотрим вообще, прогоняем только в конце, для итоговой цифры в README.
Зачем: если десять раз подряд подбирать конфигурацию под одни и те же 60 вопросов, система подгонится под них, и цифра станет оптимистичной. Holdout показывает честную цифру. Подробнее в 8.1. Делим стратифицированно: в обеих частях должны быть все типы и ловушки. 13 вопросов, на которых в 5.3 проверяли ответы (eval/ANSWERS.md), мы уже видели много раз — они идут в dev.
Версионирование
Заголовок раздела «Версионирование»Набор живёт в git рядом с кодом. Правила:
- Набор меняется только коммитом с понятным сообщением («golden v3: fixed reference for q-031, +8 comparative»).
- В отчёте каждого прогона записана версия набора (хеш файла или git-коммит). Метрики на разных версиях набора не сравниваются напрямую.
- Нашли неверный эталон в 8.4 — исправляем, поднимаем версию, перепрогоняем базовую конфигурацию.
Валидация файла
Заголовок раздела «Валидация файла»JSONL легко сломать руками. Пишем валидатор и запускаем его перед каждым прогоном (это assertion первого уровня из 6.1, только для данных):
# eval/validate_golden.py (sketch)import sysfrom typing import Literalfrom pydantic import BaseModel, field_validator
from rag.answer import REFUSALfrom rag.privacy import contains_personal_data
Category = Literal["delivery", "payment", "returns", "warranty", "promo", "orders", "products"]
class Item(BaseModel): id: str q: str category: Category | None must_contain: list[str] = [] reference_answer: str must_include: list[str] = [] evidence: list[str] = [] source_urls: list[str] = [] source_chunk_keys: list[str] = [] type: Literal["factual", "comparative", "multi_hop", "unanswerable"] handoff: Literal["refused", "personal_data"] | None = None difficulty: Literal["easy", "medium", "hard"] tags: list[str] = [] origin: Literal["manual", "synthetic"] reviewed: bool split: Literal["dev", "holdout"] notes: str = ""
@field_validator("reviewed") @classmethod def must_be_reviewed(cls, v: bool) -> bool: if not v: raise ValueError("every item must be reviewed by a human") return v
def main(path: str = "eval/golden.jsonl") -> None: seen: set[str] = set() for n, line in enumerate(open(path, encoding="utf-8"), 1): item = Item.model_validate_json(line) assert item.id not in seen, f"line {n}: duplicate id {item.id}" seen.add(item.id) if item.type == "unanswerable": assert item.reference_answer == REFUSAL, f"{item.id}: reference must be REFUSAL" assert item.handoff, f"{item.id}: unanswerable needs a handoff reason" else: assert item.evidence and item.source_urls, f"{item.id}: no evidence/source" # invented personal data is allowed only where we test the routing assert contains_personal_data(item.q) == (item.handoff == "personal_data"), \ f"{item.id}: personal data check disagrees with handoff" print(f"OK: {len(seen)} items")
if __name__ == "__main__": main(*sys.argv[1:])Дополняем: проверку, что каждая цитата evidence находится в каком-то чанке текущего index.db (иначе цитата с опечаткой или страница kb/ изменилась) и не взята из архива (кроме вопросов с тегом archived), и печать обеих матриц покрытия.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Набор от провайдера не зависит: golden.jsonl, схема, валидатор и split одинаковы для Claude, OpenAI, DeepSeek и Ollama. Именно поэтому на нём потом можно честно сравнивать провайдеров. Меняется немного (проверено в ноябре 2026 по api-docs.deepseek.com):
- Записываем, какой моделью сделана синтетика, и не только имя из конфига, а
resp.modelи дату (полеgeneratorиз главы 6.2 · Синтетические данные для evals). DeepSeek переназначает имена:deepseek-flashсейчас обслуживает V4.1-Flash-1017, а старые имена перенаправляются на новые модели. - Помощник по однозначности.
deepseek-flashс выключенными рассуждениями может дёшево (центы за весь набор) выписать для каждого вопроса возможные трактовки: «гарантия на какую лампу — „Свет“ или „Свет мини“?». Это подсказка, а не судья: решение и эталон всё равно наши. Вопросы выдуманы и про публичную базу знаний, поэтому отправлять их можно. - Каждый вопрос набора будет уходить в DeepSeek при каждом прогоне. Поэтому в наборе нет настоящих обращений клиентов, только общие вопросы. Серверы DeepSeek в Китае, но правило касается любого облачного провайдера. Вопросы с
handoff: "personal_data"содержат выдуманные данные и проверяют маршрут: вask()их перехватываетcontains_personal_dataдо облака, а вспомогательные скрипты их пропускают. - Проверка цитат по индексу работает с тем индексом, что есть; эмбеддинги считает Ollama (
bge-m3), так что всё локально и бесплатно.
| Claude | OpenAI | DeepSeek | Ollama | |
|---|---|---|---|---|
| Влияет на формат набора | нет | нет | нет | нет |
| Чем помогает при сборке | проверка однозначности (для сравнения) | то же | дешёвая проверка однозначности, генерация синтетики | эмбеддинги для проверки цитат и дублей; черновики по настоящим обращениям |
| Что нельзя отправлять | данные клиентов | данные клиентов | данные клиентов; серверы в Китае | можно всё, данные не покидают компьютер |
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Перечитываем свой конспект
notes/evals.mdиз 6.1 иeval/ANSWERS.mdиз 5.3 (≈10 мин): какие типы ошибок ожидаем от проекта 2. - Открываем Hamel: AI Evals FAQ → вопросы про размер набора и про то, как его собирать (поиск по «dataset») (≈20 мин). Обращаем внимание: начинать с небольшого набора и расширять по мере обнаружения ошибок нормально.
- Открываем Anthropic: Define success criteria and build evaluations → разделы про построение тестовых случаев и краевые случаи (≈20 мин). Берём отсюда список видов краевых случаев (неоднозначный ввод, нерелевантный ввод, длинный ввод).
- Открываем Pydantic: Models →
model_validate_jsonиLiteral-поля (≈15 мин), если с ними ещё не работали в неделях 1–2. - Дальше работа руками: сама сборка набора — основное время задачи.
- Открываем DeepSeek: Models & Pricing и раздел новостей на api-docs.deepseek.com (≈10 мин): смотрим, как переименовываются и переназначаются модели. Отсюда правило записывать
resp.modelи дату.
На что обратить внимание
Заголовок раздела «На что обратить внимание»- Неоднозначный вопрос — источник вечного шума. «Сколько стоит доставка?» — какой службой, какой вес, какая зона? «Какая гарантия на лампу?» — «Свет» или «Свет мини»? Судья будет ставить случайные оценки. Переписываем или удаляем.
- Эталон по памяти, а не по базе. Свою
kb/мы писали сами, но страницы меняются: тариф обновили, порог подвинули. Каждый эталон сверяем с текущей страницей и записываем цитату. - Набор только из простых вопросов даёт 95% и ощущение, что всё готово. Добавляем сравнительные, ловушки и вопросы без ответа в базе.
- Перекос по категориям. Если 40 из 60 вопросов про доставку, общая цифра описывает доставку.
- Утечка holdout. Посмотрели ошибки на holdout и поправили промпт — holdout перестал быть holdout. Тогда честно переносим эти вопросы в dev и собираем новый holdout.
- Данные клиентов — не сюда. Ничего из
data/shop.db,outputs/handoff.jsonlи настоящих обращений; выдуманные телефоны и номера заказов — только в вопросах сhandoff: "personal_data". - Нельзя «чинить» эталон под ответ системы, если система неправа. Исправляем эталон, только когда он сам ошибочен, и записываем в
notesпочему.
Практика
Заголовок раздела «Практика»Работаем в projects/02-support-kb-rag/eval/.
-
Рисуем матрицу покрытия (тип × категория) и таблицу ловушек в
notes/evals.mdс целевыми числами. -
Переводим 20 строк из 5.1 на новую схему и добавляем 3 вопроса без ответа в базе из 5.3 (два с
handoff: "refused", «где мой заказ 1042?» — с"personal_data"). -
Дописываем вручную до 30–50 вопросов: сначала те, на которых проект 2 ошибался, затем доводим обе матрицы. Не меньше 8 comparative и 8 unanswerable, ловушки — не меньше минимума из таблицы.
-
Для каждого: находим ответ в
kb/, записываемreference_answer,must_include, дословные цитатыevidence,source_urlsс якорями. -
Добавляем 20–50 проверенных синтетических вопросов из
eval/synthetic_reviewed.jsonl(6.2), приведя их к общей схеме. -
Пишем
eval/validate_golden.py(схема, уникальность ID, цитаты есть вindex.dbи не из архива,handoffсогласован сcontains_personal_data, печать обеих матриц). -
Скрипт
eval/split.py: стратифицированно поtypeпроставляетsplit(dev ~70%, holdout ~30%) с фиксированным seed. После этого holdout не открываем. -
Прогоняем валидатор и
run_retrieval_eval.pyиз недели 5, исправляем ошибки. -
Коммитим
eval/golden.jsonl(версия v1) и записываем вnotes/evals.mdего состав: сколько вопросов по типам, категориям, ловушкам, origin. -
На следующий день перечитываем 10 случайных вопросов свежим взглядом: однозначны ли они?
-
(Опционально) Скрипт
eval/ambiguity_check.py:deepseek-flash(thinking выключен, JSON mode) для каждого вопроса возвращает{"ambiguous": bool, "interpretations": [...]}. Вопросы сhandoff: "personal_data"пропускает. Просматриваем всеambiguous: trueи сами решаем, переписывать ли вопрос. -
Проверяем, что в вопросах и эталонах нет настоящих данных клиентов: набор целиком будет уходить в облачную модель при каждом прогоне.
Ожидаемый результат: файл на 50–100 строк, валидатор печатает OK и матрицы без пустых ячеек.
Готово, когда
Заголовок раздела «Готово, когда»-
eval/golden.jsonlсодержит 50–100 вопросов, у каждогоreviewed: true, эталон,must_include, цитаты и ссылкиkb/<file>.md#<anchor>(кромеunanswerable, у которых эталон — фраза отказа и заданhandoff). - Представлены все четыре типа, все семь категорий и все пять ловушек; есть split на dev/holdout.
- Валидатор проходит, скрипты недели 5 работают на новом файле, набор закоммичен с версией.
Тест для самопроверки
Заголовок раздела «Тест для самопроверки»Вопрос 1
Почему качество набора важнее его размера?
Шумные или неверные эталоны дают шумную метрику: улучшение невозможно отличить от случайности, а ошибки эталона маскируются под ошибки системы.
Вопрос 2
Зачем в наборе вопросы без ответа в базе и какой у них эталон?
Чтобы проверить, что система не галлюцинирует, когда ответа нет. Эталон — точная фраза отказа «В базе знаний нет ответа на этот вопрос.», а поле handoff задаёт причину передачи оператору (refused или personal_data).
Вопрос 3
Почему не стоит опираться только на chunk_key как на указатель правильного куска?
chunk_key считается из текста и раздела чанка и меняется при смене чанкинга. Цитата evidence позволяет пересчитать правильные чанки для любого индекса.
Вопрос 4
Для чего нужен holdout, если есть dev?
b. Holdout не участвует в подборе конфигурации, поэтому его цифра не подогнана.
Вопрос 5
Что такое must_include, чем оно отличается от must_contain и как помогает судье?
Список ключевых фактов, без которых ответ неверен. must_contain описывает найденный кусок (поиск), must_include — ответ (генерацию). Судья проверяет конкретные пункты, а не «похоже ли в целом», поэтому он строже и стабильнее.
Вопрос 6
Что не так с этой записью?
{"id": "q-5", "q": "Какая гарантия на лампу?", "reference_answer": "Зависит от лампы", "type": "factual", "category": null, "reviewed": false}Вопрос неоднозначный («Свет» или «Свет мини»), эталон непроверяем, тип не тот (сравнение двух ламп — это comparative), category пустая, хотя вопрос явно про гарантию, нет цитат и ссылок, reviewed: false, нет must_contain, must_include, handoff, difficulty, origin, split.
Вопрос 7
Исправлены эталоны у 5 вопросов. Можно ли сравнивать новые метрики со старыми прогонами напрямую?
Нет. Набор другой версии — другая задача. Нужно перепрогнать базовую конфигурацию на новой версии и сравнивать уже с ней.
Вопрос 8
Какой тип вопросов особенно трудный для векторного поиска и почему?
Сравнительные: нужны куски с двух страниц, а векторный поиск по одному запросу часто приносит похожие куски одной страницы (особенно у похожих ламп). Multi-hop тоже: второй кусок может быть непохож на вопрос.
Вопрос 9
Как ведёт себя валидатор, если на странице kb/delivery.md поменяли тариф и цитата больше не находится?
Он должен упасть или предупредить: цитата устарела, значит надо обновить эталон по новой странице и поднять версию набора (или переиндексировать, если устарел index.db).
Вопрос 10
Ошибки на holdout просматривались 10 раз, пока улучшался промпт. Что делать?
Holdout скомпрометирован. Перенести эти вопросы в dev и собрать новый holdout из новых вопросов, которые не просматривались.
Вопрос 11
Почему стоит включать в набор вопросы, на которых система уже ошибается?
Набор, где всё проходит, ничего не измеряет: улучшения не будет видно. Известные ошибки — главное, что хочется починить и отслеживать как регрессии.
Вопрос 12
Почему в записи синтетического вопроса стоит хранить resp.model, а не только "deepseek-flash" из конфига?
Имя модели у DeepSeek может указывать на разные версии во времени (сейчас deepseek-flash — V4.1-Flash-1017, старые имена перенаправлены). resp.model и дата показывают, чем на самом деле сделан вопрос, и объясняют изменения при перегенерации.
Вопрос 13
Хочется добавить в набор настоящее обращение: «я Петрова, +7 912 …, по заказу 1187 оплатила картой 2 340 ₽, вернула лампу, где деньги?». Что не так и как переписать?
Это личные данные настоящего клиента: набор лежит в git, вопросы уходят в облачную модель (DeepSeek — серверы в Китае) при каждом прогоне, а contains_personal_data на регулярных выражениях ловит не всё (имя «Петрова» она не заметит). Да и проверяет такой вопрос один случай, а не базу знаний. Переписать как общий вопрос к базе знаний: «Через сколько дней возвращаются деньги за возвращённый товар, если заказ оплачен картой?»
Что дальше
Заголовок раздела «Что дальше»В 6.4 · Скрипт оценки: retrieval + faithfulness пишем eval/run.py, который прогоняет этот набор через ask() и считает hit rate, MRR, correctness и стоимость одной командой.