Не ERP, а надзиратель: система для моего производства, которая всех тормошит

Больше всего на моём производстве терялись задачи. Утром я ставлю задачу, человек берёт её в работу. Через час к нему подходит другой человек со своим «срочно», и он переключается. Про первую задачу вспоминают через неделю, когда она уже нужна, а её никто не начинал. Виноватых нет: каждый был занят делом.
С чертежами было похоже. Чертёж обновили, а в цеху лежит старый. Или чертёж есть, но никто не помнит, у кого последняя версия. Вопросы по чертежу я записывал на бумажку, на ходу, прямо в цеху. Потом возвращался в офис, садился за компьютер и разбирал свои записи. На это уходило отдельное время, а часть записей до компьютера просто не доезжала.
Я посмотрел, что с этим делают большие системы, и решил собрать свою. Ушло около трёх недель чистой работы: по выходным и вечерам, в свободное от основной работы время. Получилась система APP Motors: задачи, наряды, склад, инструмент, чертежи. А главное в ней — агент, который смотрит на всё сразу, видит цепочки задач и сам напоминает людям, пока я занят другим. Ниже о том, как она устроена, для чего она мне и почему, по-моему, так можно закрыть задачи почти любого небольшого производства.
Почему не ERP
ERP и MES я не ругаю, на большом заводе без них никак. Но для небольшого производства это слишком дорого и долго: внедрение на месяцы, интегратор, лицензии на каждого пользователя, обучение. И слишком много всего: в системе сотни экранов, а нам нужны десять.
Главное для меня другое. ERP хранит, но не тормошит. Задача, которую никто не двигает, лежит в ней так же спокойно, как в блокноте. Чтобы что-то сдвинулось, кто-то должен зайти, найти, позвонить. Обычно этот кто-то я. А чтобы задача вообще попала в ERP, надо дойти до компьютера и заполнить форму, и в этот момент половина задач и теряется.
Мне нужно было другое:
задачу ставят за десять секунд, голосом с телефона, прямо там, где она возникла;
все видят, кто что делает и что горит;
сроки и простои отслеживает сама система, а не я;
чертежи, склад и история не теряются, даже если кто-то ошибся;
всё это работает на обычном сервере, без лицензий и интеграторов.
Кто этим пользуется
Сейчас в системе работают технолог-наладчик, два конструктора, мастер цеха, который распределяет работу, рабочие цеха, системный администратор и технический директор. В цеху стоит планшет с приложением, у остальных приложение на телефоне, а у кого компьютер под рукой, тот работает в веб-кабинете. Уведомления приходят в шторку телефона, как сообщения из мессенджера.

Внутри то, что есть у любого производства:
задачи «кто что делает»: купить, заказать, сделать, спроектировать, с фото и сроками;
наряды и операции на изделие;
склад: приход, выдача, резервы материалов под наряд, места хранения;
инструмент поштучно, с историей, у кого он сейчас;
чертежи и документы с ревизиями;
замечания производства, дефициты и заявки на закупку.
Каждый видит свою часть. В системе 11 ролей: владелец, администратор, конструктор, технолог, диспетчер, исполнитель в цеху, склад, закупщик, сборщик, директор и отдельная роль для ИИ-агента. Права выдаются по действиям: «создать задачу», «зарезервировать материал», «утвердить ревизию». Новая роль собирается из готовых прав без правки кода.
Отдельно есть личные задачи. Их видят только автор и исполнитель, даже владелец системы их не видит. В сводки и цепочки они не попадают.

Задача рождается в цеху
Ради этого я всё и делал. Захожу в цех быстро что-то обсудить, по ходу разговора всплывает вопрос к конструктору. Раньше я бы записал его на бумажку, донёс до офиса, сел за компьютер и написал конструктору, если бы не забыл. Сейчас я достаю телефон и надиктовываю задачу прямо там.
Телефон распознаёт речь, а кнопка «Разобрать с ИИ» раскладывает сказанное по полям: название, описание, тип, срочность, срок, исполнитель. На проверке фраза вида «конструктору к пятнице, срочно, …» стала задачей с типом «Спроектировать», срочностью «Срочно», сроком в пятницу 23:59 и нужным конструктором в поле исполнителя. Я смотрю, что всё правильно, и жму «Создать». Это несколько секунд, и я даже не вышел из цеха.

Модель заодно чинит то, что телефон расслышал плохо. Наговорено «уплотнитильные кольца для корпуса ам12, штук двадцать». На выходе название «Закупить уплотнительные кольца к корпусу АМ-12», в описании «Количество: 20 штук» и пометка «Уточнить: спецификация (размер, материал, ГОСТ/ТУ)». Ещё система нашла в базе задачу по этому корпусу и предложила связать новую с ней как «нужно для».
Дальше всё идёт само. Конструктору сразу прилетает уведомление в шторку. Если ему что-то непонятно, он пишет вопрос в комментарии, и уже мне прилетает уведомление с вопросом. Я отвечаю, он берёт задачу в работу. Цепочка запустилась, вопрос начал решаться в ту же минуту, а не на следующий день, когда я доберусь до компьютера. Новую общую задачу видят все, кто работает в системе, поэтому, если она касается ещё кого-то, он узнает о ней сразу.
Секунды на заполнении формы тут мелочь. Пропадает целый круг: записать, дойти, сесть, вспомнить, что имел в виду, набрать, отправить. А с ним пропадают и задачи, которые на этом кругу терялись.
Исполнителя система ищет у себя на сервере по списку сотрудников, до модели. В текст, который уходит в Claude (здесь Haiku), вместо имени подставляется «[исполнитель]». Три разбора при первой проверке стоили вместе 0,004 $.
На этом же разборе меня поймал первый баг, найденный уже на живой системе. Функция принимала контекст базы (проекты, похожие задачи), но в сам запрос к модели его не добавляла. Тесты были зелёные: они подменяли функцию уровнем выше и не смотрели, что реально уходит в модель. Теперь есть тест, который проверяет собранный запрос целиком.
Надзиратель
Поставить задачу быстро — половина дела. Вторая половина — чтобы её не забыли, когда придёт следующее «срочно». Этим занимается надзиратель: фоновая задача, которая каждые пять минут обходит все активные задачи и решает, кому что пора напомнить.
Решают простые правила, которые можно прочитать в коде. Языковая модель подключается в одном месте, когда надо по-человечески сформулировать совет мне. Если модель недоступна, всё остальное продолжает работать. Тормошить людей надзиратель может только днём, с 8:00 до 21:00 по Москве. Ночью он молчит, даже если всё горит.
Сроки: лесенка напоминаний
Исполнитель получает напоминание за сутки до срока, потом за два часа. Каждое уходит один раз на значение срока: если автор перенёс срок, лесенка начинается заново, а исполнителю приходит уведомление «срок: было → стало». После просрочки напоминание повторяется не чаще раза в три часа, с текстом вроде этого (задача для примера):
Просрочено: Заказать подшипники
Срок был 13.11 23:59 МСК, просрочка 1 д 4 ч.
Сделайте или напишите в комментарии, что мешает.
Просьба написать, что мешает, стоит в каждом напоминании. Мне проще прочитать «жду поставщика до среды», чем гадать, почему задача стоит. А в цепочках, о которых ниже, комментарий ещё и считается движением, и простой отсчитывается заново.
«Готово» нажимает автор
Исполнитель не может сам закрыть задачу. Он переводит её «на проверку», и для этого нужно приложить хотя бы одно фото или файл: собранный узел, накладную, скриншот. Принимает или возвращает задачу автор. Вернуть можно только с комментарием, что не так. Если автор и исполнитель один человек, он принимает сам.
Так «сделал» перестаёт означать «я считаю, что сделал»: у каждой закрытой задачи есть фото и человек, который его посмотрел.
Цепочки: кто держит работу
Любую задачу можно поставить как «нужно для» другой: чертёж нужен для детали, деталь нужна для сборки. В карточке задачи есть кнопка «Нужно для этой задачи…», которая сразу открывает создание подзадачи. Из таких связей получается цепочка, и надзиратель смотрит на неё целиком.

Узкое место цепочки — активная задача, у которой нет своих незакрытых подзадач. Её никто не ждёт снизу, её просто кто-то должен сделать. Простой считается от последнего движения: смены статуса, комментария или нового фото. Порог зависит от срочности самой срочной задачи в цепочке:
STALL_AFTER: dict[str, timedelta] = {
TaskPriority.CRITICAL.value: timedelta(hours=4), # «Нужно вчера»
TaskPriority.HIGH.value: timedelta(days=1), # «Срочно»
TaskPriority.NORMAL.value: timedelta(days=2),
TaskPriority.LOW.value: timedelta(days=5), # «Не срочно»
}
Когда порог пройден, надзиратель пишет тому, кто держит узкое место: из-за него стоит цепочка, и вот кто ждёт. Тем, кто ждёт, он пишет, что можно пока переключиться на другую работу и им сообщат, когда будет готово. Если цепочка с пометкой «Нужно вчера», он сразу пишет и мне, не дожидаясь сводки. Так выглядят первые два сообщения (задачи для примера, в живых уведомлениях вместо ролей стоят имена):
Из-за задачи стоит цепочка: Чертёж крышки
Нет движения 1 д 3 ч; срок не назван — назовите его, чтобы остальные могли планировать.
Ждут: Сборщик (по «Собрать узел»). Если есть сложности — напишите в комментарии, что мешает.
Ждём: Чертёж крышки
Ваша задача «Собрать узел» ждёт «Чертёж крышки» у Конструктора (срок пока не назван).
Пока можно переключиться на другую работу — сообщим, когда будет готово.
Когда подзадачу принимают, ждущие сразу получают «готово, можно продолжать». Цепочку видно и в карточке задачи: у родителя «ждём 1 из 2», у подзадачи «нужна для задачи такой-то», по звеньям можно переходить.
Чтобы не превратиться в спам, каждое напоминание запоминается вместе с моментом последнего движения. Пока узкое место стоит, держателю повторяют не чаще порога, а ждущим пишут один раз за эпизод простоя. Сдвинулось и снова встало — новый эпизод.
Сводка в 9:00 и 18:00
Дважды в день мне приходит сводка: сколько задач активно, что просрочено, что горит в ближайшие сутки, что с пометкой «Нужно вчера», что без исполнителя, что не двигалось три дня и больше, сколько задач у каждого. В конце раздел про цепочки с простоем. Если ничего не горит, сводка так и пишет: «Всё спокойно».

Здесь и работает модель (Claude Haiku). По каждой стоящей цепочке она пишет одну фразу-совет и готовое сообщение, которое я могу переслать держателю. Имена людей в модель не уходят: перед запросом сервер заменяет их на «Сотрудник 1», «Сотрудник 2», а в ответе меняет обратно у себя.
labels: dict[str, str] = {}
def lab(name: str) -> str:
return labels.setdefault(name, f"Сотрудник {len(labels) + 1}")
# ... запрос к модели только с метками ...
back = {v: k for k, v in labels.items()}
def unlab(text: str) -> str:
for label in sorted(back, key=len, reverse=True): # «Сотрудник 12» раньше «Сотрудник 1»
text = text.replace(label, back[label])
return text
В промпте модели прямо написано: без угроз, без денег и наказаний, технические параметры не придумывать. Сообщение от моего имени система сама не отправляет никогда, только предлагает текст. Отправляю я.
Ночной обход производства
Задачи не единственное, что зависает. Раз в сутки другой агент проверяет производственную часть: наряд выпущен, а ни одна операция не началась больше двух дней; операция в работе больше недели; замечание производства висит открытым две недели; изменение в чертеже согласовано, но не внесено неделю; дефицит материала открыт пять дней, а заявки на закупку нет.
Находит это обычный SQL. Модель (здесь Sonnet) получает уже готовый список, расставляет приоритеты и пишет дайджест. Сама ничего не закрывает и в справочники не пишет: у роли ИИ-агента прав на запись нет. Рядом работает агент дефицитов: сравнивает, сколько материала нужно под открытые наряды, с тем, что есть на складе за вычетом резервов, и заранее говорит, чего не хватит.
Почему ничего не теряется
«Ничего не теряется» держится на нескольких жёстких правилах:
складские записи нельзя удалить или исправить; ошибку исправляют обратной проводкой, остаток всегда считается по журналу движений;
ревизия чертежа не редактируется: новая версия — новая запись, старая уходит в архив с автором, датой и причиной замены; у документа одна действующая ревизия, и в цех попадает только она;
каждое важное действие пишется в журнал аудита: кто, что, было, стало;
все действия ИИ-агентов тоже в журнале, с ценой и временем каждого вызова.
Это ровно те две боли, с которых всё началось. Старый чертёж в цеху больше не окажется: действующая ревизия одна. А задача, которую отодвинуло чужое «срочно», не пропадёт: она висит на доске со сроком, и надзиратель про неё напомнит.
Плюс резервная копия каждую ночь, и её забирают на мой компьютер. После того как в ноябре хостинг потерял мой сервер целиком (я писал, как переезжал), базу этой системы подняли как раз из такой копии.
Как это сделано и сколько заняло
Сервер на Python: FastAPI, PostgreSQL, Redis, RabbitMQ и Celery для фоновых задач, MinIO для файлов, Nginx. Приложение на Kotlin и Jetpack Compose. Уведомления в шторку через Firebase. Всё в Docker Compose на обычном VPS на 8 ГБ, где рядом живут ещё два моих проекта.
Около трёх недель чистой работы, не подряд: в основном выходные и вечера. Git это хорошо видит: из 245 коммитов 120 пришлись на субботы и воскресенья.
Писал я систему вместе с командой ИИ-агентов, и это отдельная тема. На Хабре про такие команды уже много хорошего, например «Один разработчик, 14 агентов, восемь месяцев». У меня то же самое, только под производство: тех-лид, бэкенд, Android, тестировщик, проверяющий безопасность и контролёр релиза, который не верит отчётам остальных и проверяет каждый пункт сам. Получилось около 73 тысяч строк на Python в сервере, около 43 тысяч на Kotlin в приложении и примерно полторы тысячи тестов. Как устроена эта команда и где она ошибалась, расскажу в одной из следующих статей.
Похожий путь уже описывали коллеги: в «Нейроскладе компонентов для микроэлектроники» тоже небольшое производство и своя система вместо таблиц.
Почему это подходит не только мне
В коде почти нет ничего про моторы. Роли собираются из прав, справочники материалов, инструмента и мест хранения заполняются под своё производство, маршруты операций задаёт технолог. Цепочки задач, напоминания, приёмка с фото и сводки вообще не знают, что именно производят. Слово «электродвигатели» есть в паре промптов, чтобы модели было понятнее, о чём речь.
Поэтому я думаю, что такая схема подходит почти любому небольшому производству, где работа проходит через несколько человек, будь то мебель или сборка электроники. Задача там теряется так же, как у меня: между людьми, в момент передачи.
Что дальше
Сейчас готовится модуль аналитики. Основа для него уже есть: каждый переход задачи и наряда пишется в журнал с временем и автором, и из журнала видно, сколько задача была в работе, сколько ждала и на ком стояла. Модуль соберёт это в графики: по каждой конкретной задаче — сколько она заняла и где были простои, по производству в целом — где работа застревает чаще всего. Поверх графиков будет агент, который смотрит на эти цифры и показывает слабые места процесса. Тогда можно будет разбирать ошибки по фактам, а не по памяти: например, почему сборка заняла две недели вместо трёх дней и на каком звене она простояла.
Остальное по плану:
бухгалтерия: связь с учётом, чтобы закупки и списания не вбивать второй раз;
сквозное ведение заказа от заявки клиента до отгрузки, с теми же цепочками и напоминаниями;
свои экраны в приложении для конструктора, технолога, закупщика и директора (сейчас в приложении есть экраны исполнителя, сборщика, склада и диспетчера, а доска задач у всех);
кнопка «Отправить» прямо в совете из сводки, чтобы не копировать текст руками;
несколько площадок в одной системе.
В следующих статьях цикла разберу по частям: как семь агентов превращают голосовое сообщение в черновик задачи, как работает команда ИИ-агентов, которая писала систему, и почему приложение на планшете говорило «нет соединения», хотя сервер ответил 200.
Вместо вывода
Хранить данные умеет любая таблица. Мне нужна была система, которая не даёт забыть: задачу ставишь там, где она возникла, она сразу у исполнителя, а дальше за ней смотрят агенты и тормошат, пока она не закрыта с фото. Теперь я могу зайти в цех, поговорить, надиктовать три задачи и уйти, не думая, что что-то из этого потеряется по дороге в офис.
Идеальной её не назову: экраны для половины ролей ещё в работе, аналитика готовится, бухгалтерия пока в планах. Про каждую из этих частей напишу отдельно, когда доделаю.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.