惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
博客园 - 三生石上(FineUI控件)
S
Securelist
U
Unit 42
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Simon Willison's Weblog
Simon Willison's Weblog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
T
Tenable Blog
The Hacker News
The Hacker News
The Register - Security
The Register - Security
IT之家
IT之家
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Privacy & Cybersecurity Law Blog
博客园_首页
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
A
Arctic Wolf
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
V2EX
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
V
Visual Studio Blog
月光博客
月光博客
爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как мы считаем недельное меню в Pikni Food: пачки, остатки и solver вместо списка рецептов
Dimentr_AI · 2026-06-17 · via Все публикации подряд на Хабре

Средний

10 мин

0

Когда мы только начали собирать Pikni Food, идея выглядела довольно простой: пользователь отвечает на несколько вопросов, мы подбираем блюда, считаем калории и показываем список покупок.

На первый взгляд это похоже на обычное приложение с рецептами. Но довольно быстро стало понятно, что самая интересная часть начинается не в рецептах, а в довольно базовой продуктовой корзине.

Например, меню требует 620 г гречки, а в магазине она продаётся пачкой 900 г. Рецепту нужно 180 г творога, а упаковка может быть 400 г. Огурец в салате можно посчитать как 70 г, но купить его всё равно придётся штукой или лотком. Если считать только съеденные граммы, неделя выглядит аккуратно и недорого, ну а если считать чек, то… получается совсем другая история.

Так мы пришли к выводу, что мы делаем не просто «генератор рецептов», а планировщик продуктовой корзины. А в нём важно не только КБЖУ, но и многое другое, например: упаковки, остатки, сроки годности, цены магазинов, техника на кухне, время готовки и тд.

Ниже расскажу как это всё устроено, какие алгоритмы пробовали, с какими проблемами столкнулись и как мы их решали.

Готовое меню по дням

Готовое меню по дням

Что есть в продукте сейчас

Pikni Food сейчас работает как PWA на piknifood.ru и как Telegram Mini App. Нативное приложение пока не делали: для MVP важнее было дать человеку ссылку и быстро проверить, есть ли польза, чем сначала пройти историю со сторами.

В onboarding пользователь задает:

  • сколько людей ест из одной корзины;

  • цель: похудеть, поддерживать вес или набрать;

  • рост, вес, возраст и активность;

  • бюджет на день;

  • количество дней и приемов пищи;

  • аллергены и нелюбимые продукты;

  • технику на кухне;

  • ограничение по времени готовки.

На выходе получается меню по дням, КБЖУ, стоимость, список покупок, остатки и план готовки.

Онбординг: бюджет, срок и параметры меню

Онбординг: бюджет, срок и параметры меню

В текущем каталоге 110 ингредиентов, 741 рецепт и 120 готовых рационов. Для ингредиента мало знать «гречка, 343 ккал». Нужны поля, которые кажутся бухгалтерией, но без них план быстро начинает врать:

{
  "key": "buckwheat_raw",
  "pack_size_g": 900,
  "price_per_pack": 80,
  "shelf_life_days": 365,
  "food_group": "grain"
}

Такие поля отвечают на вопросы, которые обычно появляются только после красивой демки, а именно:

  • Сколько человек реально купит? Что останется? Можно ли этот остаток использовать завтра? Насколько больно открыть пачку ради одного блюда?

Почему простая схема сломалась

Самая очевидная архитектура выглядит так:

рецепты -> ингредиенты -> список покупок

Мы с нее и начали. Берем блюда, складываем ингредиенты, получаем корзину.

Проблема в том, что рецепт живет в граммах, а магазин живет в упаковках. Если меню съедает 620 г гречки, пользователь покупает 900 г. Если меню съедает 180 г творога, остаток надо либо встроить в следующие блюда, либо честно показать, что он останется.

После этого схема стала менее красивой, зато ближе к реальной жизни:

профиль пользователя
+ бюджет
+ КБЖУ
+ аллергены
+ техника
+ цены
+ фасовки
+ реальные остатки
-> допустимое меню
-> покупка целых упаковок
-> прогноз остатков
-> план готовки

Главный сдвиг: продукт перестал быть строкой внутри рецепта. Он стал полноценным ресурсом! Если дома есть 280 г риса, это должно влиять на выбор блюд. Если открыт творог, его лучше доесть быстро, а если осталось масло, можно не паниковать, ведь оно подождет.

Бюджет тоже пришлось считать аккуратнее. Можно собрать меню, где «съеденная часть» укладывается в 480 рублей в день, а на кассе получается 760, потому что закупка идет пачками. Такое меню может выглядеть корректно в расчётах, но на практике пользователь платит за реальные упаковки в корзине, а не за условную себестоимость использованных граммов.

Ограничения: не все одинаково жесткие

Внутри у нас есть сетка слотов:

день 1 / завтрак
день 1 / обед
день 1 / ужин
день 2 / завтрак
...

В каждый слот надо поставить блюдо. При этом часть ограничений жесткая:

— аллергены; — стоп-лист ингредиентов; — доступная техника; — базовая совместимость блюда и приема пищи.

Если у человека аллергия на молочку, блюдо с творогом не должно попасть в план. Тут не нужен компромисс.

А часть ограничений мягкая:

— белок; — жиры и углеводы; — бюджет; — разнообразие; — использование остатков; — штраф за скоропорт.

С ними бывает неприятнее. Если человек ставит очень жесткий бюджет и высокий белок, можно честно сказать «решений нет». Но для бытового планировщика иногда полезнее показать меню с небольшим недобором белка и явно отметить компромисс, чем оставить пустой экран.

Первый подход: жадно доедать пачки

Первый работающий вариант был pack-aware greedy. Идея простая: выбирать следующее блюдо так, чтобы оно использовало уже купленные продукты и меньше плодило остатки.

Примерная логика:

ингредиент уже есть дома → плюс
нужно открыть новую пачку → считаем будущий остаток
остаток скоропортящийся → штрафуем сильнее
блюдо уже было недавно → штрафуем

Плюс такого подхода в скорости. На мобильном интерфейсе это важно: нажал кнопку, меню появилось почти сразу.

Минус тоже ожидаемый: greedy хорошо выбирает «следующее» блюдо, но может испортить неделю целиком. Сегодня он радостно доедает курицу, а через два дня из-за этого не остается нормального ужина под бюджет и белок.

После пары таких сценариев стало понятно, что меню надо уметь трясти целиком, а не собирать только слева направо.

Simulated annealing как рабочая страховка

Следующим слоем стал simulated annealing. Он умеет брать текущее меню, менять одно блюдо, пересчитывать энергию и иногда принимать ухудшение, чтобы выбраться из локальной ямы.

Упрощенно энергия плана выглядит так:

отклонение от КБЖУ
+ выход за бюджет
+ повторы блюд
+ скоропортящиеся остатки
- использование продуктов из холодильника
- любимые ингредиенты

SA не пытается идеально выровнять каждый отдельный день. Внутри дня он допускает небольшие отклонения по калориям, белкам, жирам и углеводам, но при этом следит, чтобы средние значения за весь период оставались в нужном диапазоне. Поэтому один день может быть чуть калорийнее или беднее по белку, если это компенсируется другими днями недели.

Отдельная бытовая деталь — совместимость «слотов». Если для ужина мало вариантов, иногда можно взять обеденное блюдо. А вот тяжелый обед на завтрак чаще выглядит странно. Формально это мелочь, но без таких правил меню становится математически допустимым, но глупым.

MIP оставили для офлайна

Еще один слой — MIP на HiGHS. Его мы используем не на каждый пользовательский клик, а для офлайн-генерации и проверки готовых рационов.

Там выбор блюда в конкретный слот превращается в бинарную переменную:

x[recipe, day, meal] = 0 или 1

Ограничения держат калории, белок, бюджет, повторы, совместимость слотов и фильтры по аллергенам/технике/времени. В документации к оптимизатору у нас есть оценка: меню на 3 дня и 3 приема пищи дает около 4000 binary vars и обычно решается за 1-3 секунды на MacBook M-серии. Для рантайма это уже многовато, а для офлайн-батча нормально.

Так получился гибрид без красивой легенды про «один правильный алгоритм»:

  • готовые рационы для частых сценариев;

  • выбор и починка этих рационов в pikni_rations.js;

  • SA как запасной путь, когда шаблонов не хватает;

  • MIP для офлайн-генерации;

  • отдельный расчет упаковок, закупки и остатков.

На практике такая приземленная схема удобнее. Если сломался retail-слой, мы чиним retail-слой. Если плохо подбираются замены, смотрим scoring замен. Если нормальный рацион отваливается только из-за закупки целых пачек, появляется отдельный путь вроде pack_budget_relaxed, а не переписывается весь solver.

Список покупок оказался отдельным продуктом

Сначала список покупок казался финальной табличкой после меню. Оказалось, это почти отдельная подсистема. Надо сложить ингредиенты по всем людям, вычесть то, что уже есть дома, округлить до целых пачек, посчитать стоимость, показать остатки и не перепутать реальные остатки с прогнозными.

Список покупок: целые упаковки, цена и остатки

Список покупок: целые упаковки, цена и остатки

Простой пример:

нужно по меню: 620 г гречки
фасовка: 900 г
покупаем: 1 пачку
остаток: 280 г

Для гречки это спокойный остаток. Для творога такой же остаток уже аргумент в пользу блюд с творогом в ближайшие дни.

Самый неприятный класс багов был вокруг «pending» остатков. Есть реальные продукты в холодильнике. А есть прогноз: что останется после текущей закупки, которую пользователь еще не сделал. Если эти два типа смешать, следующий план начинает оптимизироваться вокруг фантомных продуктов.

У нас был именно такой случай: пользователь утверждает меню, приложение считает будущие остатки, а потом при повторной генерации эти остатки уже могли выглядеть как настоящие. Пришлось явно вести pending- лоты и снимать этот флаг только тогда, когда покупка действительно стала прошлым событием.

Похожая история случалась и с обновлением цен. После изменения priceVersion нельзя было пересчитывать уже утвержденную покупку из «мутировавшего» холодильника: легко было получить двойное вычитание нашего так сказать, инвентаря. Поэтому появился snapshot утвержденной покупки: что именно надо было купить в момент approval, сколько граммов и по каким пачкам.

Цены: больнее, чем хотелось

В тестовом каталоге можно написать:

{
  "chicken_breast": 520,
  "buckwheat_raw": 80
}

В реальности «курица стоит 520» — почти бессмысленная фраза. Цена зависит от города, сети, магазина, акции, наличия, канала доставки и конкретного SKU (Stock Keeping Unit — уникальный идентификатор конкретной товарной позиции: бренд, вес, упаковка, фасовка). «Куриное филе» в нашем каталоге и конкретная упаковка на сайте магазина — разные сущности, которые еще надо сматчить.

Поэтому появился слой price snapshot. Локальный dev-сервер отдает snapshot, внутри которого есть catalog_patch - он обновляет цены и фасовки в нашем каталоге, после чего меню и корзина пересчитываются.

Форма патча примерно такая:

{
  "ingredient_id": "buckwheat_raw",
  "retailer_sku_id": "...",
  "sku_name": "Крупа гречневая, 900 г",
  "price_per_pack": 80,
  "pack_size_g": 900,
  "price_captured_at": "..."
}

В dev-сервере snapshot кэшируется на 30 минут. Есть stale-if-error до 12 часов: если live-источник упал, лучше показать устаревшую, но явно помеченную цену, чем превратить меню в пустой экран.

Сейчас это нельзя честно продавать как «точные цены ближайшего магазина». Если источник дает city-level web-каталог, значит это ориентировочные цены по городу. Для точности до конкретного darkstore нужен официальный API, партнерский фид или стабильный delivery endpoint, а этого у нас, к сожалению, нет…

Так вот, это то самое место где продукт может очень легко обмануть пользователя. Поэтому мы стараемся оставлять в интерфейсе корректные формулировки, например — не «точная цена», а «ориентир по городу».

Готовка тоже влезла в оптимизацию

Меню может быть нормальным по цифрам и плохим по жизни.

Если каждый вечер пользователю нужно готовить с нуля отдельное блюдо почти час, такой план быстро перестаёт быть удобным — и человек скорее закажет доставку, чем будет следовать меню. Поэтому появился экран готовки: блюда группируются в сессии, показывается примерное время и порядок действий.

План готовки: сессии и блюда

План готовки: сессии и блюда

Идея не в том, чтобы всех превратить в фанатов meal prep. Скорее так: если рис уже варится, курицу можно готовить рядом, а часть блюда просто разогреть завтра. Это не такая эффектная часть продукта, но без неё недельное меню быстро становится просто красивой таблицей, а не рабочим сценарием на неделю.

PWA и Telegram Mini App: мелочи, которые все равно надо делать

Технически фронт довольно простой: React, PWA, сборка через esbuild, сервис-воркер, локальное состояние, отдельные скрипты для retail-слоя и SEO-страниц.

Telegram Mini App добавил свою россыпь деталей: BackButton, fullscreen, safe area, initData, внешние ссылки и CloudStorage. У CloudStorage лимит 4096 байт на ключ, поэтому состояние приходится чанковать. Не самая героическая инженерия, но ровно из таких вещей и складывается ощущение, что приложение нормально живет в WebView)

PWA тоже не бесплатная галочка. Сервис-воркер легко начинает мешать API, если случайно кэшировать не то. Поэтому статику, фотографии и динамические ручки пришлось явно развести. Например, API не должен внезапно получать HTML приложения вместо JSON (казалось бы очевидно, но всё же).

Что реально ломалось

Самые неприятные баги были не про кнопки, а про корректность расчётов. Несколько примеров из тех, что запомнились:

  • после изменения рецептов устаревали метрики готовых рационов, и бюджет начинал расходиться с пересчётом;

  • в семейном сценарии цель могла подтянуться от первого человека, а не от текущего профиля;

  • dislike по chicken_breast сначала не всегда блокировал близкие куриные варианты вроде бедра;

  • нормальный рацион мог проиграть только потому, что закупка целыми пачками выглядела дороже съеденной части;

  • прогнозные остатки текущей покупки пытались стать реальным инвентарем слишком рано;

  • после обновления цен утвержденная корзина могла пересчитаться из уже измененного состояния.

Часть таких вещей сейчас закрыта smoke-тестами. Например, есть проверки на allergy leak, tech leak, disliked affinity, сезонность, stale recipe cache, pending leftovers и другие сценарии, которые звучат конечно скучно, но только до тех пор, пока один из них не начинает портить реальные меню.

Что дальше

Ближайшие задачи это:

— надёжнее подключить retail-цены; — улучшить качество рецептов и шагов готовки; — собирать реальные сигналы: что заменяют, что готовят, что выбрасывают; — проверять retention, а не бежать сразу в монетизацию

Pikni Food пока MVP. У него хватает ещё незакрытых проблем, и не хочется делать вид, что мы уже решили вопрос о питании на неделю раз и навсегда.

Но сама задача оказалась сильно интереснее, чем выглядела в начале. Снаружи это «сделайте мне меню». Внутри — пачки, цены, остатки, soft constraints, WebView, retail, сервис-воркер и много мелких решений, которые либо помогают человеку реально купить продукты, либо превращают планировщик в красивую игрушку.

В общем, было довольно здорово поделиться своим опытом здесь. Надеюсь, когда-нибудь, мы закроем вопрос о быстром и удобном питанием на неделю раз и навсегда!