Сообщество / DevOps

Обновил модель в ollama, и сценарий тихо начал раскладывать письма не туда

L
leo_msk
Junior
Сообщения: 30
Репутация: 15
Дата регистрации:
12.09.2026
АВТОР ТЕМЫ03 нояб. 2026 г., 18:07 (GMT+3)
Есть у меня небольшой VPS: n8n, рядом ollama, всё в docker compose. Главный сценарий скучный и поэтому полезный — разбор входящих писем. Текстом он выглядит так:

1. IMAP Trigger забирает новое письмо.
2. Code-нода обрезает подписи, цитаты и html-мусор, оставляет первые ~2000 символов.
3. HTTP Request идёт в ollama (/api/generate). Промпт: «верни JSON с полями category и urgent», категории: счёт, договор, вопрос, спам.
4. Set разбирает ответ.
5. Switch по category: счета — в папку и таблицу, договоры — мне в телеграм, вопросы — в очередь, спам — в архив.
6. Fallback-ветка Switch: «не понял», уходит в отдельную метку.

Полгода работало. На прошлой неделе я поставил на сервер ещё одну модель для другой задачи и заодно сделал ollama pull для той, что уже стояла. Без задней мысли: тег тот же, пусть подтянется свежее. В n8n при этом ничего не трогал.

Заметил через три дня, и то случайно: метка «не понял» распухла. Открыл executions. Ошибок нет, всё зелёное. Только модель стала иногда оборачивать JSON в ```json, иногда писать «Счёт» с большой буквы, а один раз вернула category: "invoice", видимо, решив, что так солиднее. Set это проглатывал, Switch честно не находил совпадения и отправлял письмо в fallback. Сценарий не падал, он просто перестал делать свою работу. Самый неприятный вид поломки: мониторить нечего, всё «успешно».

Смешно, что я сам на прошлой работе требовал у разработчиков фиксировать версии зависимостей. А тут у меня в продакшене зависимость весом в несколько гигабайт, и закреплена она словом «latest» в голове.

Что поменял:

— Имя модели теперь в одном месте, в переменной окружения n8n, а не в каждой HTTP-ноде. Раньше их было три, и в одной, как выяснилось, стояла вообще другая модель.
— В запросе к ollama включил "format": "json" и temperature 0 в options. Строго это ничего не гарантирует, но обёртки в markdown пропали.
— После разбора ответа поставил нормализацию: lower-case, trim и словарь синонимов. invoice → счёт. Да, я написал словарь синонимов для собственного классификатора, гордиться тут нечем.
— Записал ID модели из ollama list в заметку к сценарию. Если ID сменился, значит, это другая модель, даже если имя то же.

И главное, ради чего пишу. Я собрал «золотой набор»: около сорока старых писем, обезличенных, с категорией, которую поставил руками. Отдельный сценарий берёт их из таблицы, прогоняет через указанную модель тем же промптом и считает, сколько совпало и какие ответы вообще не распарсились. Запускаю вручную перед тем, как переключать переменную на новую модель. Получился такой бедный staging для промпта.

Но тут начинаются вопросы, на которые у меня нет хорошего ответа.

Первый: сорок писем — это много или смешно? Редкие категории вроде договоров там встречаются раза три, и одна ошибка сразу роняет процент. Добирать набор специально под редкие случаи или смотреть только на общий итог?

Второй: как сравнивать, если ответы не строго детерминированы? Сейчас гоняю каждое письмо дважды и считаю расхождение отдельной метрикой. Выглядит как костыль.

Третий, организационный: где у вас лежит промпт? У меня он внутри ноды, то есть живёт в экспорте workflow вместе со всем остальным, и в git дифф изменения одной фразы тонет среди сдвинутых координат нод. Думаю вынести промпт в файл или в таблицу и подтягивать по версии, но не уверен, что это не усложнение ради усложнения.

Если кто-то держит локальные модели в рабочих сценариях не ради эксперимента, расскажите, как у вас устроено обновление. Закрепляете конкретный digest, держите две модели параллельно и сравниваете, или просто не обновляете, пока работает?
0
A
asya_subs
Junior
Сообщения: 15
Репутация: 10
Дата регистрации:
25.10.2026
03 нояб. 2026 г., 18:10 (GMT+3)
Узнаю этот жанр: всё зелёное, а смысл уехал. У меня после обновления автосубтитров «ок» вдруг стал «О'кей», и поиск по проекту перестал находить половину реплик. Ошибок тоже ноль.

Я бы перед Switch поставила маленькую нормализацию: убрать ```json, привести к нижнему регистру, invoice → счёт. А в fallback писать сырой ответ модели, чтобы видеть, что именно она теперь придумала.
1
K
kostya_gw
Junior
Сообщения: 28
Репутация: 16
Дата регистрации:
20.09.2026
03 нояб. 2026 г., 19:04 (GMT+3)
Нормализация нужна, но это пластырь после факта. Я бы ещё запретил модели фантазировать на входе: в /api/generate можно передать format с JSON-схемой, где category — enum из четырёх значений. Тогда «invoice» просто не пройдёт. И тег в ollama плавающий, перед pull полезно сделать ollama cp в свой тег и в n8n держать его. Сам такой «тот же тег» обновлял бы с той же уверенностью, так что не осуждаю.
1
I
ira_sites
Junior
Сообщения: 28
Репутация: 13
Дата регистрации:
21.09.2026
04 нояб. 2026 г., 10:00 (GMT+3)

kostya_gw писал:
Тогда «invoice» просто не пройдёт.


Не пройдёт, да. Но схема держит форму, а не смысл. После очередного обновления модель спокойно отнесёт договор к «спаму». Это валидное значение из enum, Switch его примет, и письмо тихо уедет в архив. «Не понял» хотя бы видно, а такой промах не видно совсем.

У меня это пройдено на WP, только вместо модели плагины. Автообновление форм на трёх клиентских сайтах: всё зелёное, сайты открываются, заявки уходят. Только на одном письма начали падать клиенту в спам, потому что поменялся отправитель. Выяснилось через неделю, когда клиент спросил, почему затишье. С тех пор у меня после любого обновления идёт тестовая заявка с каждого сайта, и я смотрю, где она реально оказалась, а не что форма сказала «отправлено».

Для этого сценария я бы сделала то же самое: десяток своих писем с заранее известным ответом — пара счетов, договор, вопрос, спам, одно кривое. После pull или cp прогнать их через тот же workflow и сравнить категории с эталоном. Пять минут, зато видно не только формат, но и то, что модель стала думать иначе.

И счётчик на fallback: если доля «не понял» за день вдруг выросла в разы, пусть приходит уведомление. Три дня до случайной находки — это как раз отсутствие такого счётчика.
0
K
kostya_gw
Junior
Сообщения: 28
Репутация: 16
Дата регистрации:
20.09.2026
05 нояб. 2026 г., 19:06 (GMT+3)

ira_sites писал:
Но схема держит форму, а не смысл.


Согласен, enum ловит только «invoice». Договор в «спаме» он пропустит молча. Эталонный десяток писем я бы оставил, но добавил бы к нему одну вещь: новую модель не ставить сразу в бой, а сначала прогнать её рядом со старой.

Схема такая. Старая модель лежит под своим тегом через ollama cp, например mail-v1, и n8n по-прежнему ходит к ней. После pull новая версия копируется в mail-v2. В workflow после Code-ноды появляется вторая ветка: тот же промпт уходит в mail-v2, но её ответ ничего не раскладывает, а только пишется в таблицу вместе с ответом mail-v1. Если категории разошлись, строка подсвечивается. Неделю смотришь на расхождения на живой почте, а не на десятке писем, которые сам же и подобрал. Потом меняешь тег в одной ноде, и mail-v1 остаётся рядом для отката.

У меня к этому всё и идёт, только через шлюз: в n8n имя вроде «mail-classifier», а за ним в конфиге litellm уже конкретная модель. Переключение и откат становятся правкой одной строки, и n8n про версии вообще не знает. Сейчас вечер, вот рисую это на бумажке вместо того, чтобы спать.

Счётчик на fallback тоже нужен, но он запоздалый: сработает, когда письма уже уехали. Расхождения в тени видно до переключения.
0