Files
vidconf/docs/deploy/quality-tiers.md
Max Ronzhin 693db6e774 install.sh, docs: pass --env-file explicitly to docker compose
docker compose определяет .env для подстановки ${VAR} по каталогу
compose-файла (deploy/), а не по текущей директории — repo-root .env,
который использует install.sh и вся документация, молча не подхватывался.
Это и была причина "WARN: LIVEKIT_API_KEY not set" на боевом сервере:
секреты были в .env, но compose их не видел и подставлял небезопасные
дефолты (см. таблицу разведки сервера).

Теперь ставшие обязательными ${VAR:?} в docker-compose.yml (см. предыдущий
коммит) без этого немедленно проваливали бы конфиг на любой из
документированных команд. Добавлен `--env-file .env`/"$ENV_FILE" ко всем
вызовам docker compose в install.sh и в командах из README/docs.

install.sh дополнительно: ensure_default (аналог ensure_secret без генерации
секрета) для новых не-секретных параметров nginx/coturn/livekit
(NGINX_SERVER_NAMES, NGINX_CERT_NAME, LIVEKIT_USE_EXTERNAL_IP,
LIVEKIT_NODE_IP, TURN_EXTERNAL_IP) — дефолты только для локальной
разработки, не перезаписывают значения, заданные вручную на боевом
сервере. Плюс вызов deploy/render-templates.sh перед сборкой/подъёмом
стека.
2026-07-25 21:58:03 +03:00

295 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Оценка качества суммаризации по уровням AI
Методика и результаты прогона утверждённых промптов суммаризации
(`workers/summarizer/prompts/summary_map_ru.txt`,
`workers/summarizer/prompts/summary_reduce_ru.txt`) на всех трёх уровнях AI
(ADR-004, `docs/architecture/adr/004-ai-tier-matrix.md`). Тексты промптов
едины для всех уровней и правятся только осознанным решением команды по
результатам прогона на этом корпусе — этот документ и есть тот прогон,
базис для будущих итераций.
## 1. Методика
- Скрипт: `workers/summarizer/eval/run_tiers.py` — создаёт плагин `QwenLocal`
штатной фабрикой (`backend/core/plugins/factory.py::create_summarizer`) из
`TierSpec` (`backend/services/ai_tiers.py`) для каждого уровня, прогоняет
весь корпус через `Summarizer.summarize()`, пишет результаты в
`workers/summarizer/eval/results/<уровень>/<файл>.md` + метаданные прогона
`_run_meta.json` (модель, base_url, лимиты токенов, время на файл, статус).
- Доступность уровня определяется автоматически: `max` требует GPU
(`nvidia-smi` в PATH) — на dev-машине без NVIDIA пропускается сразу; для
`min`/`medium` скрипт проверяет `GET /health` LLM-сервера и сверяет
реально загруженную модель через `GET /v1/models` (официальный
OpenAI-совместимый эндпоинт llama.cpp — id ответа содержит путь к файлу
модели) с ожидаемой по `TierSpec`, чтобы не приписать результат не тому
уровню (в одной docker-сети `min` и `medium` по умолчанию делят один и тот
же compose-сервис `llm`, отличаются только тем, какой `LLM_MODEL_FILE`
сервис фактически загрузил).
- Промпты скрипт не читает и не редактирует — только передаёт `QwenLocal`
абсолютный путь к штатному каталогу `workers/summarizer/prompts/`.
Проверка инварианта: `git diff --stat workers/summarizer/prompts/` пуст.
- Оценка качества — ручная экспертная по каждому файлу результата, по трём
критериям:
1. **Полнота тем** — все ключевые темы/решения/цифры транскрипта попали в
соответствующие разделы формата, ничего существенного не потеряно на
границах чанков (особенно у длинного многотемного транскрипта — 3 чанка
и reduce);
2. **Галлюцинации** — модель не добавляет фактов, имён, цифр, сроков,
которых нет в транскрипте;
3. **Следование формату** — ровно 4 раздела (`## Ключевые тезисы`,
`## Принятые решения и задачи`, `## Открытые вопросы`, `## Цифры и
факты`), пустой раздел — одна строка `—`, без вступлений/заключений.
## 2. Корпус
`workers/summarizer/eval/corpus/` — 5 транскриптов на русском в штатном
формате входа `summarize` (`[Имя MM:SS] текст`), разного объёма и профиля:
| Файл | Профиль | Длительность | Спикеров | Особенность для оценки |
|---|---|---|---|---|
| `01-short-standup.txt` | короткий дейлик | ~10 мин | 3 | один чанк — map без reduce |
| `02-long-multitopic.txt` | планирование, 4 темы | ~60 мин | 4 | 3 чанка по 20 мин → map-reduce, объединение дублей и снятие закрытых вопросов между чанками |
| `03-dialogue-1on1.txt` | ревью 1-на-1 | ~12 мин | 2 | плотный диалог одних и тех же двух имён |
| `04-multispeaker-guest.txt` | демо клиенту | ~14 мин | 3 внутр. + 1 гость | имя гостя из двух слов (`Виктор Соколов`), различение внутренних/внешнего |
| `05-metrics-heavy.txt` | квартальный обзор метрик | ~13 мин | 3 | много чисел/процентов/сумм — точность раздела «Цифры и факты» |
## 3. Результаты: уровень `min` (Qwen3.5-4B-Instruct, Q4_K_M, CPU)
Прогон на dev-машине (macOS, без NVIDIA GPU) — единственный уровень,
доступный локально.
**Как запускался:**
```bash
docker compose -f deploy/docker-compose.yml --env-file .env --profile llm up -d \
llm-models-init llm-model-init llm
# дождаться docker compose ps → llm healthy (LLM_MODEL_FILE не задан в .env
# — дефолт download-model.sh совпадает с уровнем min, Qwen3.5-4B)
cd backend && uv run python ../workers/summarizer/eval/run_tiers.py \
--levels min --base-url http://localhost:8080/v1
```
**Параметры уровня (ADR-004):** `temperature=0.2`, `max_tokens_map=1024`,
`max_tokens_reduce=1536`, `chunk_minutes=20`, `CTX_SIZE=16384`.
Прогон реально выполнен (`workers/summarizer/eval/results/min/`, метаданные —
`_run_meta.json`, отдельные результаты — `<файл>.md`). Первая попытка
прогона всего корпуса словила инфраструктурный сбой — LLM-контейнер `llm`
был убит хостом (`exit 137`, SIGKILL) на пятом файле: Docker Desktop на этой
машине выделяет VM всего 7,75 ГиБ (`docker info`), из них ~3,4 ГиБ уже
занимал сам `llm` (веса 2,7 ГБ + KV-кэш на `CTX_SIZE=16384`), а ещё ~1 ГиБ —
параллельно работающий dev-стек (`postgres`/`redis`/`livekit`/`coturn`/
`backend-manual`); после `docker compose ... up -d llm` (перезапуск) второй
прогон всего корпуса прошёл 5/5 без сбоев. Это не проблема плагина/промптов,
а сайзинг хоста — ADR-004 закладывает под пресет 3 (`min`) минимум 16 ГБ RAM
именно ПОД ЭТОТ уровень, не под уровень поверх произвольного количества
параллельных dev-контейнеров; отмечено как риск в разделе 6.
**Длительность прогона (второй, чистый запуск, 5/5 успешно):**
| Файл | Длительность записи | Чанков (map/reduce) | Время прогона |
|---|---|---|---|
| `01-short-standup.txt` | ~10 мин | 1 (без reduce) | 52,6 с |
| `02-long-multitopic.txt` | ~60 мин | 3 + reduce | 479,6 с (~8 мин) |
| `03-dialogue-1on1.txt` | ~12 мин | 1 (без reduce) | 65,9 с |
| `04-multispeaker-guest.txt` | ~14 мин | 1 (без reduce) | 76,7 с |
| `05-metrics-heavy.txt` | ~13 мин | 1 (без reduce) | 114,5 с |
Ориентир ADR-004 «часовой транскрипт на CPU ≈ 6,5 мин» — реально получено
~8 мин на `02-long-multitopic.txt` (3 map-вызова + 1 reduce), в пределах
разумного расхождения для другого CPU и разделяемого с dev-стеком хоста.
**Экспертная оценка по корпусу (полнота / галлюцинации / формат):**
- **Формат** — соблюдён на 5/5: ровно 4 раздела с точными заголовками, без
вступлений и заключений, ни одного нарушения структуры вывода.
- **Цифры и факты** — очень высокая точность: на `05-metrics-heavy.txt`
(стресс-тест на числа) сверены вручную ВСЕ 19 пунктов раздела «Цифры и
факты» с исходным транскриптом — ни одного искажённого или выдуманного
числа. На `02-long-multitopic.txt` (map-reduce, 3 чанка) все денежные
суммы, проценты и сроки из раздела тоже подтвердились дословно.
- **Галлюцинации фактов** — единственный найденный случай: в
`04-multispeaker-guest.txt` раздел «Ключевые тезисы» смешал два факта —
в транскрипте отложена в релиз 0.1.0 только ЗАПИСЬ конференций
(«Запись — в базовом релизе только анонс...»), а саммари в тезисах
сформулировало это как отложенные «запись встреч И генерации саммари»,
хотя суммаризация в демо явно показана как уже работающая функция
(«Работают, покажу на примере...», тот же транскрипт). При этом раздел
«Цифры и факты» ТОГО ЖЕ файла формулирует факт верно («Релиз с функцией
записи запланирован на 0.1.0») — то есть внутри одного вывода модель сама
себе противоречит между разделами.
- **Незакрытые вопросы, на которые ответ уже прозвучал** — систематическая
проблема, найдена в 2 из 5 файлов:
- `01-short-standup.txt` (один чанк, без reduce): вопрос Ирины «кто-нибудь
смотрел баг про дубли уведомлений» остался в «Открытых вопросах», хотя
Павел в том же транскрипте на него ответил («Я смотрел, там
идемпотентность сломана...»). Причина — у map-промпта НЕТ инструкции
снимать вопрос, отвеченный в том же фрагменте (только у reduce-промпта
есть инструкция снимать вопросы, закрытые МЕЖДУ фрагментами) — это
структурный пробел самого промпта, не только слабость модели.
- `02-long-multitopic.txt` (map-reduce, 3 чанка): ДВА вопроса остались в
«Открытых», хотя реально были закрыты в более позднем чанке — «кто
будет проводить техническое интервью» (позже: «назначаю тебя» Игорю) и
«сколько часов записей на 250 ГБ» (позже: «примерно 200 часов»). Здесь
reduce-промпт ЯВНО требует «если вопрос... был закрыт в позднем —
оставь только итоговое состояние», но 4B-модель это правило не
применила — прямое подтверждение вывода ADR-004: `min` работает
на пределе инструктивной сложности, к reduce это относится сильнее, чем
к map.
- **Атрибуция ответственных при reduce** — 1 случай неверного приписывания
автора решения не тому спикеру: пункт «Добавить в заявку резервный SSD на
1 ТБ для бэкапов базы» приписан Марине, хотя в транскрипте это сказала
Дарья («Ещё добавлю в заявку резервный SSD...»); аналогично пункт «Завести
задачу на тестовое восстановление бэкапа» приписан Марине, хотя в
транскрипте это её ПОРУЧЕНИЕ Игорю, а исполнитель — Игорь (что видно по
соседнему, отдельно возникшему пункту «Выполнить настройку тестового
восстановления... — ответственный: Игорь» — то есть один и тот же пункт
задвоился на два с разной атрибуцией вместо объединения дублей, как
требует reduce-промпт).
- **Спутывание завершённого действия с будущей задачей** — 2 случая в
`05-metrics-heavy.txt`: фразы Романа и Артёма о том, что действие УЖЕ
сделано в прошлом («обновил документ вчера», «начиная с прошлой недели
лимит reduce минимум 1536 токенов» — прошедшее время) модель занесла в
«Принятые решения и задачи» как будто это предстоящие задачи, с явно
нелогичным полем «срок: вчера» в одном из пунктов.
- **Потеря точного срока в пользу обобщения** — в `04-multispeaker-guest.txt`
конкретный срок «к среде» (когда гость пришлёт список сотрудников)
переформулирован в размытое «до начала пилота», а в поле «срок» пункта
указано «не указан», хотя конкретная дата в транскрипте была.
**Вывод по `min`:** формат и числовая точность — сильная сторона уже на
самой маленькой модели уровня; систематическая слабость — согласованность
между разделами и через reduce (незакрытые вопросы, задвоенные решения,
неверная атрибуция, путаница «сделано» vs «сделать»). Это ожидаемо и
согласуется с выводом ADR-004 («Qwen ~3B/4B на пределе инструктивной
сложности», сильнее всего проявляется на reduce) — рекомендация: НЕ трогать
промпты по этим находкам (правка промптов — отдельное осознанное решение с
повторным прогоном корпуса после правки, не автоматическая реакция на
единичный прогон), ожидать улучшения именно от роста модели на
`medium`/`max` и сравнить те же файлы/те же найденные проблемы после
прогона на этих уровнях (раздел 6).
## 4. Результаты: уровень `medium` (Qwen3.5-9B-Instruct, Q4_K_M)
Не запускался на этой машине — на dev-хосте (macOS, без NVIDIA GPU) для
`medium` поднят тот же CPU-сервис `llm`, что и для `min` (оба используют
`base_url: http://llm:8080/v1`, ADR-004); чтобы прогнать `medium`, нужен
СВОЙ сервер с моделью Qwen3.5-9B — либо второй `llm`-сервис на другом
порту/томе с `LLM_MODEL_FILE=qwen3.5-9b-instruct-q4_k_m.gguf`, либо GPU-хост.
**Ручной шаг (на CPU-хосте с ≥32 ГБ RAM или GPU-хосте):**
```bash
LLM_MODEL_FILE=qwen3.5-9b-instruct-q4_k_m.gguf \
LLM_MODEL_URL=https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf \
LLM_TOKENIZER_FILE=qwen3.5-9b-instruct.tokenizer.json \
LLM_TOKENIZER_URL=https://huggingface.co/Qwen/Qwen3.5-9B/resolve/main/tokenizer.json \
docker compose -f deploy/docker-compose.yml --env-file .env --profile llm up -d
cd backend && uv run python ../workers/summarizer/eval/run_tiers.py \
--levels medium --base-url http://localhost:8080/v1
```
`run_tiers.py` сверит загруженную модель через `/v1/models` и откажется
писать результат в каталог `medium`, если сервер поднят с другой моделью —
скрипт сам подскажет, что не так.
<!-- TODO: результаты после ручного прогона на GPU/большом CPU-хосте. -->
## 5. Результаты: уровень `max` (Qwen3.5-35B-A3B-Instruct MoE, Q4_K_M, GPU)
Не запускался — `run_tiers.py` пропускает `max` автоматически на этой
машине (`nvidia-smi` не найден), GPU обязателен (ADR-004, требование ≥16 ГБ
VRAM, рекомендовано 24 ГБ).
**Ручной шаг (на GPU-хосте):**
```bash
LLM_MODEL_FILE=qwen3.5-35b-a3b-instruct-q4_k_m.gguf \
LLM_MODEL_URL=https://huggingface.co/unsloth/Qwen3.5-35B-A3B-GGUF/resolve/main/Qwen3.5-35B-A3B-Q4_K_M.gguf \
LLM_TOKENIZER_FILE=qwen3.5-35b-a3b-instruct.tokenizer.json \
LLM_TOKENIZER_URL=https://huggingface.co/Qwen/Qwen3.5-35B-A3B/resolve/main/tokenizer.json \
docker compose -f deploy/docker-compose.yml --env-file .env --profile llm-gpu up -d
cd backend && uv run python ../workers/summarizer/eval/run_tiers.py \
--levels max --base-url http://localhost:8081/v1
```
(порт `8081` — хостовый порт `llm-gpu` в `deploy/docker-compose.yml`, см.
`docs/deploy/llm-setup.md`, раздел 3).
<!-- TODO: результаты после ручного прогона на GPU-хосте. -->
## 6. Наблюдения и рекомендации
По результатам реального прогона `min` (раздел 3; `medium`/`max` — пока
только методика, реальных данных нет, см. разделы 45):
1. **Полнота по типам транскриптов.** Короткие однотемные записи
(`01`, `03`) и однотемные записи среднего размера с одним чанком
(`04`, `05`) обрабатываются полнее и надёжнее, чем длинный многотемный
транскрипт с map-reduce (`02`) — там, где нужен reduce, у 4B-модели
заметно чаще не срабатывают инструкции промпта про снятие закрытых
вопросов и объединение дублей (раздел 3). Диалог с цифрами (`05`) и
гостем (`04`) сами по себе не оказались сложнее для `min` — проблема
именно в reduce-стадии, а не в теме/числе спикеров.
2. **Систематических галлюцинаций фактов/цифр не найдено** — числа
воспроизводятся дословно даже в «числовом» стресс-тесте (`05`, все 19
пунктов сверены вручную). Единственная найденная смысловая ошибка —
конфляция двух разных фактов в одном разделе одного файла (`04`), не
выдумывание нового факта из ничего.
3. **Систематическая, а не случайная проблема** — это несогласованность
между разделами и через reduce: незакрытые вопросы, на которые уже
прозвучал ответ (2 из 5 файлов, включая случай БЕЗ reduce — структурный
пробел в самом map-промпте, не только слабость модели), задвоенные
решения с разной атрибуцией ответственного при reduce (1 файл), и
путаница «уже сделано» / «предстоит сделать» (2 случая в одном файле).
4. **Длительность на CPU** совпадает по порядку величины с ориентиром
ADR-004 («часовой транскрипт ≈ 6,5 мин»): реально получено ~8 мин на
`02-long-multitopic.txt` (3 map + 1 reduce). Расхождение объясняется
разделяемым с dev-стеком хостом, не архитектурной проблемой.
5. **Инфраструктурный риск, не связанный с моделью/промптами:** на dev-
машине (Docker Desktop, VM 7,75 ГиБ) `llm`-контейнер был убит хостом
(OOM на уровне VM, не cgroup — `OOMKilled: false`, но `exit 137`)
при параллельной работе полного dev-стека. ADR-004 требует 16 ГБ RAM
под пресет 3 — это бюджет ПОД уровень `min`, не поверх производного
dev-окружения с БД/Redis/LiveKit/Coturn/др. Рекомендация: в
`docs/deploy/install.md`/чек-листе явно указывать, что 16 ГБ — это
помимо памяти, занятой остальным dev/prod-стеком, если он совмещён на
одной машине.
**Рекомендация по промптам:** правки текстов промптов НЕ
делались и не рекомендуются по итогам этого прогона в одностороннем
порядке. Найденные проблемы (пп. 3) касаются reduce-инструкций, которые в
промпте УЖЕ явно прописаны («если вопрос... был закрыт в позднем — оставь
только итоговое состояние», «объединяй дубли») — модель `min` (4B) их не
всегда выполняет, что согласуется с выводом ADR-004 о пределе
инструктивной сложности на этом размере. Ожидаемая гипотеза (проверяется
прогоном `medium`/`max` на ЭТОМ ЖЕ корпусе после того, как появится
GPU/большой CPU-хост, разделы 45): более крупная модель должна снять
именно эти reduce-ошибки без изменения текста промпта. Если после прогона
`medium`/`max` те же ошибки останутся систематическими и на большей модели —
это станет основанием для отдельного осознанного решения команды о правке
промптов, с повторным прогоном этого же корпуса до и после правки.
## Как повторить и добавить уровень
```bash
cd backend && uv run python ../workers/summarizer/eval/run_tiers.py --help
```
- `--levels min,medium,max` — какие уровни пробовать (недоступные — пропуск
с причиной в stdout, скрипт не падает).
- `--base-url` — переопределить адрес LLM-сервера для ВСЕХ уровней (нужно
при запуске с хоста вне docker-сети — штатные `http://llm:8080/v1`/
`http://llm-gpu:8080/v1` из `backend/services/ai_tiers.py` резолвятся
только внутри неё).
- Результаты и `_run_meta.json` — в `workers/summarizer/eval/results/`,
перезаписываются при повторном прогоне того же уровня.
## Смотрите также
- ADR-004 — `docs/architecture/adr/004-ai-tier-matrix.md`.
- `docs/deploy/llm-setup.md` — установка LLM-сервера по уровням.