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 перед сборкой/подъёмом
стека.
25 KiB
Оценка качества суммаризации по уровням 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 /healthLLM-сервера и сверяет реально загруженную модель через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/пуст. - Оценка качества — ручная экспертная по каждому файлу результата, по трём
критериям:
- Полнота тем — все ключевые темы/решения/цифры транскрипта попали в соответствующие разделы формата, ничего существенного не потеряно на границах чанков (особенно у длинного многотемного транскрипта — 3 чанка и reduce);
- Галлюцинации — модель не добавляет фактов, имён, цифр, сроков, которых нет в транскрипте;
- Следование формату — ровно 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) — единственный уровень, доступный локально.
Как запускался:
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-хосте):
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, если сервер поднят с другой моделью —
скрипт сам подскажет, что не так.
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-хосте):
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).
6. Наблюдения и рекомендации
По результатам реального прогона min (раздел 3; medium/max — пока
только методика, реальных данных нет, см. разделы 4–5):
- Полнота по типам транскриптов. Короткие однотемные записи
(
01,03) и однотемные записи среднего размера с одним чанком (04,05) обрабатываются полнее и надёжнее, чем длинный многотемный транскрипт с map-reduce (02) — там, где нужен reduce, у 4B-модели заметно чаще не срабатывают инструкции промпта про снятие закрытых вопросов и объединение дублей (раздел 3). Диалог с цифрами (05) и гостем (04) сами по себе не оказались сложнее дляmin— проблема именно в reduce-стадии, а не в теме/числе спикеров. - Систематических галлюцинаций фактов/цифр не найдено — числа
воспроизводятся дословно даже в «числовом» стресс-тесте (
05, все 19 пунктов сверены вручную). Единственная найденная смысловая ошибка — конфляция двух разных фактов в одном разделе одного файла (04), не выдумывание нового факта из ничего. - Систематическая, а не случайная проблема — это несогласованность между разделами и через reduce: незакрытые вопросы, на которые уже прозвучал ответ (2 из 5 файлов, включая случай БЕЗ reduce — структурный пробел в самом map-промпте, не только слабость модели), задвоенные решения с разной атрибуцией ответственного при reduce (1 файл), и путаница «уже сделано» / «предстоит сделать» (2 случая в одном файле).
- Длительность на CPU совпадает по порядку величины с ориентиром
ADR-004 («часовой транскрипт ≈ 6,5 мин»): реально получено ~8 мин на
02-long-multitopic.txt(3 map + 1 reduce). Расхождение объясняется разделяемым с dev-стеком хостом, не архитектурной проблемой. - Инфраструктурный риск, не связанный с моделью/промптами: на 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 те же ошибки останутся систематическими и на большей модели —
это станет основанием для отдельного осознанного решения команды о правке
промптов, с повторным прогоном этого же корпуса до и после правки.
Как повторить и добавить уровень
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-сервера по уровням.