8.5 · Проект 3: таблица экспериментов в README
Неделя 8 · Evals · ⏱ ~4 ч · Проект
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»README Проекта 3 — главный артефакт для собеседований из всего плана: он показывает умение не просто собрать RAG, а измерять и улучшать его. Фраза «подняли correctness с X% до Y%, и вот таблица, как» с честными цифрами, разбросом и выводами отличает кандидата от сотен с «чат-ботом по PDF». Ревьюер тратит на README пару минут, поэтому история должна читаться с первого экрана.
⚠️ Публичны в проекте только выдуманные вещи: база знаний kb/, набор eval/golden.jsonl и прогоны на нём. Прогон можно публиковать, только если в нём нет ни одного настоящего сообщения клиента; outputs/handoff.jsonl, логи бота и снимки базы магазина в репозиторий не попадают никогда.
Что ищет читатель
Заголовок раздела «Что ищет читатель»Неделя 8 оставила после себя журналы experiments/EXP-*.md, прогоны в eval/runs/ и таблицу всех прогонов eval/RESULTS.md. Всё это — сырьё; README превращает его в историю, которую чужой человек прочтёт за пару минут. Начинаем с того, что этот человек ищет. Нанимающий инженер, открыв репозиторий, хочет за 1–2 минуты понять:
- Задача: что за система и какая проблема.
- Как измеряется качество: набор, метрики, судья и его проверка.
- Результат: откуда стартовали, куда пришли, на чём.
- Как думает автор: гипотезы, что не сработало, что удивило, ограничения.
- Можно ли это запустить: одна команда для evals.
Пункт 4 недооценивают. Таблица, где всё помогло, выглядит подозрительно; честное «реранкер большой модели ничего не дал, а маленькой дал +N п.п.» и «улучшение на 3 п.п. в пределах шума, не засчитываем» говорят о зрелости больше, чем любая цифра.
Структура README
Заголовок раздела «Структура README»Пять вопросов читателя задают порядок разделов. Проект 3 живёт в той же папке projects/02-support-kb-rag/, что и проект 2, поэтому README один: история evals становится его первой частью, а архитектура и Quickstart из 5.5 остаются ниже — на них ссылаемся, а не дублируем.
# Shop Support Assistant: evals & experiments
One-paragraph summary: what it is + headline result("correctness 0.6x → 0.8x on a 7x-question human-reviewed set, at N× lower cost")
## Results at a glance ← table + one chart## The system ← short diagram, link to the Architecture section (project 2)## How quality is measured ← dataset, metrics, judge validation, noise## Experiments ← table with all experiments + per-experiment conclusions## What worked, what didn't, what surprised us## Error analysis ← distribution of failure causes, the fix## Limitations## How to run evals ← commands, env vars, cost of a run## Repo layoutЗаголовочный абзац и «Results at a glance» — самое важное: многие дальше не читают.
Таблица экспериментов
Заголовок раздела «Таблица экспериментов»Сердце README — таблица, где каждая строка — один эксперимент недели 8. Колонки — те же метрики, что в журналах EXP-*.md: качество поиска и ответа, цена, латентность и вывод по правилу значимости:
| # | Эксперимент | hit@5 | correctness | faithfulness | $ / 100 q | p50 / p90, с | Вывод |
|---|---|---|---|---|---|---|---|
| 0 | Baseline: fixed-512, гибрид + переформулировка, модель L | 0,71 | 0,64 ± 0,02 | 0,83 | … | … | — |
| 1 | Chunking по заголовкам (EXP-01) | 0,84 | 0,72 ± 0,02 | 0,86 | … | … | Принято: +13 п.п. hit (11/2, p≈0,02) |
| 2 | Модель S без реранкера (EXP-02) | … | … | … | … | … | Отклонено: ниже порога качества |
| 3 | Модель S + реранкер (EXP-02) | … | … | … | … | … | Принято: качество в пределах шума от L, в N раз дешевле |
| 4 | Фильтр по товару (EXP-03, из разбора ошибок) | … | … | … | … | … | … |
| Final (holdout) | … | … | … | … | … | Честная цифра |
(Цифры иллюстративные — у каждого будут свои.) Правила:
- Кумулятивная или независимая? Указываем явно. Обычно строки кумулятивны (каждая добавляет изменение к предыдущей принятой), отклонённые эксперименты помечены и не переносятся дальше.
- Разброс в формате «± половина размаха трёх прогонов» или «(min–max)»; формат объясняем под таблицей.
- Колонка «Вывод» с правилом значимости из 8.1: принято / отклонено / тенденция и парные расхождения.
- Holdout отдельной строкой. Если цифра на holdout ниже dev — так и пишем; это ожидаемо и честно.
- Версия набора. Если
golden.jsonlменялся (исправления после 8.4), либо перепрогоняем все строки на финальной версии (лучше, если недорого), либо указываем версию в каждой строке.
Таблица генерируется скриптом из summary.json прогонов, а не вручную: ручное копирование чисел — источник ошибок. Скрипт eval/make_table.py читает список прогонов из experiments/runs.yaml (пути к папкам eval/runs/<timestamp>_<index>_<retriever>_k<k>/) и печатает markdown. eval/RESULTS.md остаётся журналом всех прогонов подряд, а таблица в README — отобранной историей.
Один график
Заголовок раздела «Один график»Таблица точна, но динамику и компромиссы глаз схватывает быстрее на картинке. Один хороший график стоит половины текста. Варианты:
- «качество против стоимости» с точками конфигураций и фронтом Парето (из 8.3);
- «correctness по шагам» — столбцы baseline → … → final с отрезками разброса.
Подписи осей, единицы, легенда; PNG в docs/ или experiments/, вставлен в README.
Раздел «Как измеряется качество»
Заголовок раздела «Раздел «Как измеряется качество»»Цифрам в таблице поверят, только если понятно, как они получены. Этот раздел — коротко, но конкретно:
- Набор: сколько вопросов, типы и категории (матрица «тип × категория» из 6.3), ловушки (архивная страница, таблицы тарифов, похожие товары, противоречие порога бесплатной доставки), сколько ручных и сколько синтетических, все проверены человеком, dev/holdout. Вопросы выдуманы, база знаний тоже.
- Метрики: hit@k, MRR, correctness, faithfulness, правильные отказы,
archived_in_context— по одной строке определения. - Судья: бинарный, с критикой; согласие с ручной разметкой на N примерах: X% (TPR/TNR). Отказы проверяет код, а не судья.
- Шум: разброс повторных прогонов, правило значимости.
Об этом спросят на собеседовании, поэтому формулировки здесь — заготовки для ответов.
«Что помогло, что нет, что удивило»
Заголовок раздела ««Что помогло, что нет, что удивило»»Таблица отвечает «что изменилось», этот раздел — «чему научились»: пункт 4 из списка вопросов читателя. 3–6 пунктов, каждый с цифрой и объяснением механизма. Примеры формата (не результаты):
- «Чанкинг по заголовкам дал самый большой прирост: таблица тарифов по весу и зоне перестала отрываться от своего заголовка».
- «Реранкер не помог большой модели (в пределах шума), но маленькой дал +N п.п.: она хуже игнорирует лишний контекст».
- «Удивило: 13% провалов были ошибками набора (устаревшие эталоны после правки
kb/), а не системы».
Раздел «Как запустить evals»
Заголовок раздела «Раздел «Как запустить evals»»Последний вопрос читателя — можно ли это запустить. Ревьюер должен суметь повторить. Минимум:
## How to run evals
uv sync # versions pinned in uv.lock cp .env.example .env # fill in your API keys uv run python ingest.py # builds index.db from kb/ (committed) uv run python eval/run.py --config eval/configs/final.json # dev split uv run python eval/compare.py eval/runs/<A> eval/runs/<B> uv run python eval/make_table.py experiments/runs.yaml
A full run on the dev split costs about $X and takes ~Y min (prices as of <date>)..env.example содержит имена переменных без значений. Указываем, сколько стоит прогон: это забота о читателе и показатель того, что автор считает деньги.
Гигиена публичного репозитория
Заголовок раздела «Гигиена публичного репозитория»README готов, но публичный репозиторий показывает не только его, а всю историю коммитов. Перед push проверяем:
- Секреты.
.envв.gitignore; поиск по истории:git log -p | grep -iE "sk-|api_key|secret"(и аналогично по рабочей копии) или gitleaks из 5.5. Если ключ хоть раз попал в коммит — отзываем ключ у провайдера; удаление из истории не помогает, если репозиторий уже был публичным. После push смотрим, не пришло ли предупреждение от GitHub secret scanning. - Никаких данных магазина и клиентов. Проект 3 про выдуманную базу знаний и выдуманный набор. Никаких
shop.db,analytics.dbи путей к../shop-live/из проекта 1,outputs/handoff.jsonl, логов бота с вопросами клиентов, токенов Telegram. Прогоны вeval/runs/публикуем, только если они сделаны наgolden.jsonlбез единого настоящего сообщения; прогон, куда хоть раз попали реальные вопросы, остаётся локально. Данные магазина в курсе выдуманы, но обращаемся с ними как с настоящими. - База знаний и чужой контент.
kb/иgolden.jsonlкоммитим: это наши выдуманные тексты, без них не собрать индекс и не повторить evals; короткие цитатыevidenceв наборе — из своей же базы. Кэшraw/web/с чужой публичной страницей (необязательный пункт 4.4) не коммитим. - Большие файлы.
index*.db(index.db,index_ctx.db,index_fixed512.db,index_headings.db), эмбеддинги, кеш судьи — в.gitignore. Коммитимsummary.jsonиconfig.jsonпрогонов (маленькие),results.jsonl— по желанию. - Версии зависимостей закреплены (
uv.lockилиrequirements.txtс точными версиями).
Питч на 60 секунд
Заголовок раздела «Питч на 60 секунд»Та же история нужна и без экрана: на собеседовании её просят рассказать. README заодно служит сценарием устного рассказа. Репетируем вслух:
Я сделал ассистента поддержки интернет-магазина, который отвечает по базе знаний со ссылкой на раздел, и построил для него систему оценки: 7x вопросов четырёх типов, проверенных вручную, метрики поиска и ответа, LLM-судья, сверенный с моей разметкой на N примерах. Провёл четыре эксперимента. Самый сильный эффект дал чанкинг по заголовкам, +X п.п. hit rate. Разбор ошибок показал, что 30% провалов — поиск находит страницу похожего товара, например гарантию «Свет мини» вместо «Свет»; фильтр по товару убрал большую часть. В итоге correctness выросла с A до B на отложенной выборке, а замена модели на маленькую с реранкером снизила стоимость в N раз без потери качества в пределах шума.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Структура README от провайдера не зависит. Меняется то, что читатель должен узнать о моделях, чтобы цифрам можно было верить и чтобы их можно было повторить. Факты о провайдерах сверены в ноябре 2026 по api-docs.deepseek.com и docs.ollama.com.
Что обязательно написать про модели
Заголовок раздела «Что обязательно написать про модели»- Точные id и даты.
deepseek-flash/deepseek-v4-proплюсresp.modelиз прогонов и дата: DeepSeek переназначает имена (сейчасdeepseek-flash— это V4.1-Flash-1017), и через полгода «deepseek-flash» будет другой моделью. - Режим рассуждений ответчика и судьи (thinking включён/выключен) — он меняет и качество, и цену, и шум.
- Для Ollama — точный тег и железо. «
qwen3:4b, ноутбук с 8 ГБ RAM, без отдельной видеокарты, контекст 8k» — без этого латентность локальной модели ничего не значит. Указываем, что теги Ollama по умолчанию квантизованы. - Судья и его проверка. «Judge:
deepseek-v4-pro, thinking off, temperature 0; agreement with our labels X% on N answers; second opinion from <Claude/OpenAI model> on disputed cases». Если судья из того же семейства, что ответчик, пишем, как проверяли самопредпочтение. - Цена с оговорками. Для DeepSeek: по ценам на дату, с учётом кеша, и в каком окне (пик или вне пика) делались прогоны. Для Ollama: «$0 API, runs on the user’s device», рядом p50/p90 латентности.
- Сравнение с Claude/OpenAI — только если оно действительно прогонялось. Одна-две строки «для сравнения» на тех же вопросах сильно усиливают README; выдуманные или «по рейтингам» цифры его убивают.
.env.example и «How to run» на нашем стеке
Заголовок раздела «.env.example и «How to run» на нашем стеке»# .env.example — names only, no valuesDEEPSEEK_API_KEY=ANSWER_PROVIDER=ANSWER_MODEL=ANSWER_MODEL_LOCAL=EMBED_BACKEND=EMBED_MODEL=RERANK_MODEL=GEN_MODEL=# optional, for the comparison rowsANTHROPIC_API_KEY=OPENAI_API_KEY=# optional: Langfuse tracing (only the fictional golden set goes there)LANGFUSE_PUBLIC_KEY=LANGFUSE_SECRET_KEY=LANGFUSE_BASE_URL=# local models, OpenAI-compatible form: http://localhost:11434/v1OLLAMA_BASE_URL=В «How to run evals» добавляем шаги для локальной части: установить Ollama по официальной инструкции для своей ОС с ollama.com (на macOS — проверить приложение codesign -dv и spctl -a -vv перед первым запуском), ollama pull <точный тег> для ответчика и ollama pull bge-m3 для эмбеддингов, и как задать контекст (OLLAMA_CONTEXT_LENGTH). Хороший жест для ревьюера: конфигурация, которая работает вообще без облачных ключей (локальный ответчик + локальные эмбеддинги, только метрики поиска и проверки отказов кодом), чтобы проект можно было запустить за 5 минут бесплатно. Указываем и стоимость полного прогона на DeepSeek (например, «≈$0.6 at peak prices, about half off-peak, as of
Приватность — одной строкой, но точно
Заголовок раздела «Приватность — одной строкой, но точно»Ревьюер спросит: «DeepSeek — китайский провайдер, а данные?» Готовый ответ в README: «Only the public, fictional knowledge base (kb/) and the fictional eval questions are sent to DeepSeek. Real customer messages are answered by a local model only; no shop database, customer data or keys are in this repository». Строку пишем, только если это правда, и проверяем перед push (раздел «Гигиена публичного репозитория» выше).
Сравнение провайдеров для README
Заголовок раздела «Сравнение провайдеров для README»| Что показать | Claude | OpenAI | DeepSeek | Ollama |
|---|---|---|---|---|
| Идентификатор модели | id из доки (с датой снапшота) | id из доки | id + resp.model + дата |
точный тег + квантизация |
| Цена | по странице цен на дату | по странице цен на дату | по странице цен, кеш, пик/вне пика | $0 API + железо и латентность |
Ключ в .env.example |
ANTHROPIC_API_KEY (опционально) |
OPENAI_API_KEY (опционально) |
DEEPSEEK_API_KEY |
ключ не нужен |
| Вопрос ревьюера | «почему не только Claude?» | то же | «а данные?» | «насколько хуже и на каком железе?» |
Маршрут изучения
Заголовок раздела «Маршрут изучения»- GitHub Docs: About READMEs (≈10 мин): где GitHub показывает README, относительные ссылки, изображения.
- GitHub Docs: Basic writing and formatting syntax → таблицы, изображения, сворачиваемые блоки (≈15 мин).
- Anthropic: Contextual Retrieval ещё раз, уже как пример подачи (≈10 мин): как коротко представлены эксперименты и цифры. Берём оттуда манеру «изменение → эффект в процентах → цена».
- Перечитываем свой README Проекта 2 (5.5) (≈10 мин): Проект 3 на него ссылается, а не дублирует.
- Перечитываем журналы
experiments/EXP-*.md(≈20 мин) и выписываем для каждого одну фразу вывода. - DeepSeek: Models & Pricing в день публикации README (≈5 мин): сверяем id и цены в таблице с текущими, ставим дату.
- Страница своей локальной модели на ollama.com/library (≈5 мин): точный тег и размер — в раздел «How to run evals».
На что обратить внимание
Заголовок раздела «На что обратить внимание»- Цифры без разброса и размера набора выглядят как маркетинг. Всегда «на N вопросах», «± разброс».
- Только dev-цифры. Финальный результат — holdout; если его нет, читатель-инженер это заметит.
- Ручное копирование чисел в таблицу — опечатки и расхождения с прогонами. Таблицу генерирует скрипт.
- Несравнимые строки: разные версии набора без пометки.
- Скрытые неудачи. Отклонённые эксперименты — тоже результат, их показывают.
- README, который нельзя запустить: нет
.env.example,kb/не закоммичена, не закреплены версии. - Секрет в истории git — отзывать ключ, а не только чистить историю.
- Прогон с настоящими вопросами клиентов в
eval/runs/. Публиковать можно только прогоны на выдуманном наборе. - Стена текста. Первый экран: абзац итога + таблица + график.
- «deepseek-flash» без даты и
resp.model— через полгода цифры будет невозможно ни повторить, ни объяснить. - Латентность Ollama без железа («p90 9 s») ничего не говорит: на ноутбуке с 8 ГБ и на сервере с GPU это разные числа.
- Строки «Claude/OpenAI для сравнения» без реальных прогонов. Только то, что прогнали сами на том же наборе.
Практика
Заголовок раздела «Практика»- Создаём
experiments/runs.yaml: список строк таблицы (имя эксперимента, путь к папке прогона вeval/runs/, кумулятивно ли, вывод). - Пишем
eval/make_table.py: читаетsummary.jsonуказанных прогонов, печатает markdown-таблицу с hit@5, correctness (с разбросом), faithfulness, $/100q, p50/p90, выводом. - Если версия набора менялась, перепрогоняем ключевые конфигурации (baseline и принятые шаги) на финальной версии, если позволяет бюджет; иначе помечаем версии в таблице.
- Строим один график (качество против цены или correctness по шагам) →
experiments/*.png. - Пишем README по структуре выше: итоговый абзац, таблица, график, как измеряется качество, эксперименты, «что помогло / нет / удивило», разбор ошибок, ограничения, «Как запустить evals», структура репо; разделы проекта 2 (архитектура, Quickstart) остаются ниже.
- В разделе «How quality is measured» указываем модели ответчика и судьи с точными id,
resp.model, режимом thinking и датой; для локальной модели — тег, железо, контекст. - Обновляем
.env.example:DEEPSEEK_API_KEY, переменные моделей, опциональные ключи для сравнения и Langfuse,OLLAMA_BASE_URL— только имена. Проверяем.gitignore(.env,index*.db*, кеши судьи,raw/,outputs/,data/). - Делаем конфигурацию
eval/configs/local_only.json, которая запускается без облачных ключей (локальный ответчик, только метрики поиска и проверки отказов кодом). - Проверяем историю и рабочую копию на секреты; убеждаемся, что нет
*.db,outputs/handoff.jsonlи прогонов с настоящими сообщениями клиентов (git ls-files,git log --all --stat -- '*.db' 'outputs/*'). Только после этой проверки добавляем в README строку о приватности. - Проходим «Как запустить evals» с нуля в чистой папке (новый clone, новое окружение), включая
local_only.json. Исправляем всё, что не сработало. - Push на GitHub. Открываем README в браузере: таблица и график отображаются, ссылки работают.
- Репетируем вслух питч на 60 секунд, записываем его в
notes/pitch.md.
Готово, когда
Заголовок раздела «Готово, когда»- README рассказывает историю улучшений в цифрах: старт → финиш на holdout, каждый эксперимент с выводом, разброс и размер набора указаны.
- Есть разделы «что помогло / не помогло / удивило», «Ограничения», «Как запустить evals».
- Evals запускаются по README в чистом клоне.
- Репозиторий на GitHub без секретов, без данных магазина и без настоящих сообщений клиентов;
kb/иeval/golden.jsonlна месте.
Тест для самопроверки
Заголовок раздела «Тест для самопроверки»Вопрос 1
Что должно быть на первом экране README Проекта 3?
Абзац с задачей и главным результатом (старт → финиш, на каком наборе, цена), таблица результатов и график.
Вопрос 2
Зачем показывать отклонённые эксперименты?
Они показывают, как автор думает и проверяет гипотезы; таблица, где всё помогло, выглядит неправдоподобно, а честные отрицательные результаты полезны и читателю.
Вопрос 3
Почему итоговую цифру берут с holdout, а не с dev?
Dev использовался для выбора конфигураций и разбора ошибок, поэтому его цифры оптимистичны; holdout не участвовал в выборе.
Вопрос 4
Что не так с такой строкой в README?
Добавили реранкер — качество выросло с 74% до 77%!!!Нет размера набора и разброса; +3 п.п. скорее всего в пределах шума (нет парного сравнения); не сказано, что зафиксировано, сколько стоило и как изменилась латентность; восклицательные знаки вместо вывода.
Вопрос 5
Зачем генерировать таблицу скриптом из summary.json?
Чтобы цифры в README точно совпадали с прогонами и таблицу можно было пересобрать после новых экспериментов без ручных ошибок.
Вопрос 6
В истории git нашёлся коммит с API-ключом, репозиторий уже публичный. Что делать в первую очередь?
b. Ключ уже мог быть скопирован; сначала отзыв, затем чистка истории по желанию.
Вопрос 7
Почему kb/ и eval/golden.jsonl коммитят, а outputs/handoff.jsonl и прогон, в котором были настоящие вопросы клиентов, — нет?
kb/ и набор выдуманы и принадлежат автору, без них не собрать индекс и не повторить evals. handoff.jsonl и такой прогон содержат сообщения клиентов — персональные данные, которые не публикуют (и не отправляют в облако), даже если данные магазина в курсе выдуманы: обращаемся с ними как с настоящими.
Вопрос 8
Что должно быть в .env.example?
Имена нужных переменных окружения (без значений), чтобы читатель знал, что заполнить.
Вопрос 9
Строки таблицы посчитаны на разных версиях набора. Как поступить?
Перепрогнать ключевые конфигурации на финальной версии набора; если это слишком дорого — явно пометить версию набора в каждой строке и не сравнивать несравнимое.
Вопрос 10
Почему стоит указать в README стоимость одного прогона evals?
Читатель понимает, во что обойдётся повторить, и видит, что автор считает деньги — это часть инженерной работы.
Вопрос 11
Почему в README мало написать «answer model: deepseek-flash»? Что добавить?
Имя у DeepSeek может указывать на разные версии во времени. Нужны resp.model из прогонов, дата, режим thinking и цены на дату (с учётом кеша и окна пик/вне пика).
Вопрос 12
Ревьюер спрашивает: «Вы отправляли данные в DeepSeek, это же Китай?» Что должно быть правдой в проекте, чтобы ответить коротко, и где это написано?
В DeepSeek уходили только публичная выдуманная база знаний и выдуманные вопросы набора; настоящие сообщения клиентов обрабатывает только локальная модель, а данных магазина, клиентов и ключей в репозитории нет нигде. Это одна строка в README (раздел о приватности) и проверка репозитория перед push.
Что дальше
Заголовок раздела «Что дальше»Проект 3 завершён. Следующий этап — агенты: начинаем с 9.1 · Building effective agents + OWASP LLM Top 10; навык evals вернётся в 10.3 · Проект 4: evals агента на 20 случаях.