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 перед сборкой/подъёмом
стека.
295 lines
25 KiB
Markdown
295 lines
25 KiB
Markdown
# Оценка качества суммаризации по уровням 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` — пока
|
||
только методика, реальных данных нет, см. разделы 4–5):
|
||
|
||
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-хост, разделы 4–5): более крупная модель должна снять
|
||
именно эти 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-сервера по уровням.
|