1. 项目概述从“今天吃什么”到一周菜单的自动化革命“今天吃什么”——这大概是每个家庭主厨、独居青年甚至食堂管理者每天都要面对的“灵魂拷问”。手动规划一周菜单不仅要考虑营养均衡、口味变化还得兼顾采购预算和烹饪时间费时费力不说还容易陷入“老三样”的循环。kwokronny/week-menu-generate这个项目正是为了解决这个普遍痛点而生。它本质上是一个智能化的周度菜单生成器通过预设的规则、偏好和约束条件自动为你编排出一份合理、多样且可执行的一周饮食计划。我最初接触这类工具是因为自己在家带娃既要保证孩子营养又不想在“吃什么”上耗费太多脑细胞。手动列菜单经常顾此失彼要么食材买多了浪费要么临时起意却发现缺东少西。这个项目吸引我的地方在于它试图将菜单规划这件充满主观性和随机性的事情用一套可配置的、逻辑清晰的程序来自动化。它不仅仅是随机组合几个菜名而是融入了对饮食结构、食材复用、烹饪复杂度等多维度的考量。对于追求生活效率的家庭、需要控制成本的小型餐饮业主、或是希望饮食管理更加科学健康的健身人士来说这样一个工具能节省大量决策时间让“吃”这件事变得更有条理也更富趣味性。2. 核心设计思路规则引擎驱动下的个性化菜单构建一个优秀的菜单生成器绝不能是简单的随机抽奖。kwokronny/week-menu-generate的核心思路是构建一个基于规则的“决策引擎”。这个引擎的输入是你的饮食偏好与约束输出则是一份可行的周计划。其设计巧妙之处在于它将复杂的饮食规划问题分解为几个可量化、可配置的模块。2.1 核心数据模型菜品库与标签体系一切的基础是一个结构化的菜品库。库中的每道菜都不是一个简单的字符串名称而是一个携带了丰富元数据的对象。这些元数据或者说“标签”是系统进行智能匹配的关键。典型的标签包括菜系川菜、粤菜、日料、西餐等用于保证口味多样性。主要食材鸡肉、牛肉、鱼、豆腐、绿叶蔬菜等用于均衡营养和规划采购。烹饪方式炒、炖、烤、凉拌、蒸等关联到烹饪时间和厨具使用。烹饪复杂度/时间快手菜15分钟、家常菜30分钟、大菜60分钟以上用于合理分配每日的烹饪精力。餐别早餐、午餐、晚餐、加餐有些菜品可能适合多个餐别。特殊属性素食、辣味、儿童喜爱、适合带便当等。通过这套标签体系系统就能“理解”每一道菜。例如“番茄炒蛋”可能被标记为【菜系家常】【食材番茄、鸡蛋】【烹饪方式炒】【复杂度快手】【餐别午餐、晚餐】。2.2 规则引擎从偏好到计划的逻辑桥梁有了数据就需要规则来驱动决策。项目的核心逻辑是一系列可配置的规则这些规则共同作用筛选和排列菜品。主要规则类型包括多样性规则确保一段时间内不重复。例如“同一主要食材如鸡肉在连续三天内最多出现一次”“同一菜系在两天内不重复”。这直接解决了吃腻的问题。均衡性规则从营养和食材类型角度进行约束。例如“每天确保有至少一份绿色蔬菜”“每周至少安排两次鱼虾类菜品”“豆制品每周出现2-3次”。这为营养均衡提供了基础保障。操作性规则考虑实际的烹饪场景。例如“工作日晚上只安排‘快手菜’或‘可提前备好的菜’”“周末可以安排一道‘大菜’”“周日晚餐尽量简单为下周储备精力”。这让生成的菜单更接地气易于执行。连续性/复用规则聪明的采购计划。例如“如果周一用了半颗包菜周二可以生成另一道也用包菜的菜避免浪费”“周三炖了鸡汤周四可以用鸡汤做一道烩菜或汤面”。这个规则能显著减少食材浪费是菜单规划的高级技巧。硬性排除规则根据用户忌口或偏好进行过滤。例如“所有含有‘香菜’的菜品排除”“孩子过敏的‘海鲜’类菜品排除”。这些规则通常被赋予不同的权重和优先级系统在生成菜单时会尝试寻找满足所有或大部分高优先级规则的最优解。这本质上是一个约束满足问题。2.3 生成算法权衡与优化的艺术如何从几百道菜中选出一周21餐假设三餐的搭配项目可能采用以下一种或多种算法思路基于规则的筛选随机选择最简单的方式。先根据当日餐别、复杂度等硬性规则过滤出一批候选菜品再根据多样性规则剔除最近用过的最后从剩余菜品中随机挑选。这种方式实现简单但优化程度有限。回溯算法这是一种更系统的方法。从第一餐开始尝试选择菜品如果选择导致后续无法满足规则比如把爱吃的菜都提前用完了就回退到上一步重新选择。这种方法能更好地找到全局较优解但计算量可能随菜品库增大而增加。启发式搜索/遗传算法对于追求更优解的场景可以将一周菜单视为一个“个体”通过随机生成一批“个体”即多份周菜单然后定义“适应度函数”例如规则满足度越高、食材复用率越高则分数越高通过多轮“进化”选择、交叉、变异筛选出适应度最高的菜单方案。这种方式能处理非常复杂的规则组合是项目进阶的方向。在实际项目中为了兼顾性能和效果往往会采用混合策略先用快速规则进行初筛和每日框架搭建再在局部如相邻几天使用回溯或优化算法进行微调。注意规则并非越多越好。过于复杂的规则集可能导致无解或生成时间过长。一个好的实践是从核心规则忌口、餐别开始逐步添加多样性、均衡性规则并根据生成结果的满意度进行微调。3. 关键技术点与实现细节拆解理解了设计思路我们来看看具体实现时有哪些技术关键点和值得深究的细节。一个健壮的菜单生成器其技术栈的选择和细节处理决定了它的可用性和用户体验。3.1 数据结构设计与存储方案菜品数据如何组织是首要问题。使用关系型数据库如SQLite、PostgreSQL是直观的选择可以方便地通过标签进行多表关联查询。例如-- 简化的表结构示例 CREATE TABLE dishes ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, cuisine TEXT, main_ingredient TEXT, cook_time TEXT, complexity TEXT ); CREATE TABLE tags ( id INTEGER PRIMARY KEY, dish_id INTEGER, tag_name TEXT, -- 如 vegetarian, spicy, kids_favorite FOREIGN KEY (dish_id) REFERENCES dishes(id) );但对于需要频繁进行复杂规则匹配和集合运算的场景将菜品数据加载到内存中使用对象如Python的字典、类实例或文档型数据库如MongoDB来存储可能性能更佳。每个菜品对象包含其所有标签的列表在内存中进行过滤和计算速度更快。我的实操心得在项目初期或菜品数量不大1000时我强烈推荐使用纯文本文件如JSON或YAML来维护菜品库。它无需数据库服务版本控制友好读写直观。例如一个dishes.json文件[ { id: 1, name: 番茄炒蛋, cuisine: 家常, primary_ingredients: [番茄, 鸡蛋], cooking_method: 炒, time_required: 15, complexity: easy, meal_types: [lunch, dinner], tags: [vegetable, quick, stir-fry] } ]程序启动时将其读入内存进行后续操作。这种方式对于个人使用或小型项目来说简单且完全够用。3.2 规则的定义与执行引擎规则如何被定义和解析一种灵活的方式是使用配置式规则。例如用一个rules.yaml文件来定义diversity_rules: - type: ingredient_cooldown ingredient_field: primary_ingredients cooldown_days: 2 strength: high - type: cuisine_cooldown cuisine_field: cuisine cooldown_days: 1 strength: medium balance_rules: - type: weekly_quota tag: fish min_occurrences: 2 max_occurrences: 3 operational_rules: - type: complexity_by_day day_of_week: [Monday, Tuesday, Wednesday, Thursday, Friday] allowed_complexity: [easy, medium] meal_type: dinner程序需要有一个规则引擎来解析这些配置并在生成过程中应用它们。引擎的工作流程通常是针对待安排的一餐先用所有规则对菜品库进行过滤得到一个“合格候选池”如果池子为空则根据规则强度strength逐个放宽“中等”或“低”强度的规则直到有候选菜品为止。实现技巧将规则实现为独立的函数或类方法每个方法接收“当前已生成的菜单历史”和“候选菜品”作为输入返回一个布尔值或一个过滤后的菜品列表。这样便于单元测试和规则组合。3.3 生成流程的编排与容错处理菜单生成不是一个单步操作而是一个多步骤的流程。一个稳健的流程如下初始化与加载加载菜品库、用户配置忌口、偏好强度、规则集。确定生成框架明确要生成几天的菜单每天几餐。例如生成下周一到周五的午餐和晚餐。顺序生成按天、按餐别顺序生成。这是最自然的逻辑也便于应用“依赖之前结果”的规则如复用食材。单餐决策 a.硬性过滤应用所有“高”强度规则和硬性排除规则忌口得到基础候选集。 b.评分与排序对基础候选集中的每个菜品根据其他规则计算一个“偏好分数”。例如符合“本周需要多吃蔬菜”规则则加分距离上次食用同食材的天数越短则扣分为了多样性。 c.随机选择从评分最高的前N个菜品中随机选择一个。引入随机性可以避免每次生成完全一样的菜单增加趣味性。冲突解决与回溯如果某餐经过规则过滤后无菜可选则触发冲突解决机制。可以尝试① 放宽最近添加的一条“中等”强度规则② 回溯到上一餐重新选择。需要设置最大回溯步数防止无限循环。后处理与优化全部生成后可以进行一次全局检查例如计算本周总营养概览、生成采购清单或者对个别明显不合理的搭配进行手动微调系统应支持手动替换功能。常见陷阱死循环。当规则之间存在矛盾或菜品库本身无法满足所有规则时程序可能陷入无限的回溯或尝试中。必须设置最大迭代次数或超时时间并在失败时给出清晰的提示例如“无法在‘无海鲜’和‘每周吃两次鱼’的规则下生成菜单请调整规则或扩充菜品库”。4. 从代码到餐桌完整实操与功能扩展假设我们使用Python来实现一个简化版的周菜单生成器。下面我将勾勒出核心模块和关键代码片段并探讨如何将其功能变得真正实用。4.1 基础版本实现核心代码框架首先定义我们的数据模型和规则。# models.py from typing import List, Dict, Any, Optional from dataclasses import dataclass from enum import Enum class Complexity(Enum): EASY easy MEDIUM medium HARD hard class MealType(Enum): BREAKFAST breakfast LUNCH lunch DINNER dinner dataclass class Dish: id: int name: str cuisine: str primary_ingredients: List[str] cooking_method: str complexity: Complexity meal_types: List[MealType] tags: List[str] dataclass class ScheduledMeal: day: str # e.g., Monday meal_type: MealType dish: Dish # rules.py from abc import ABC, abstractmethod class Rule(ABC): 规则基类 abstractmethod def filter(self, candidate_dishes: List[Dish], context: Dict[str, Any]) - List[Dish]: 过滤候选菜品列表返回符合规则的菜品 pass class IngredientCooldownRule(Rule): 食材冷却规则过去N天内出现过的食材其相关菜品优先级降低或排除 def __init__(self, cooldown_days: int): self.cooldown_days cooldown_days def filter(self, candidate_dishes: List[Dish], context: Dict[str, Any]) - List[Dish]: history context.get(meal_history, []) # 已安排的餐食历史 recent_ingredients set() # 从历史中提取最近N天内用过的食材 # ... 简化处理这里假设context中直接提供了最近用过的食材集合 banned_ingredients context.get(recent_ingredients, set()) filtered_dishes [] for dish in candidate_dishes: # 如果菜品的主要食材与近期食材无交集则保留 if not (set(dish.primary_ingredients) banned_ingredients): filtered_dishes.append(dish) # 也可以设计为扣分而非直接过滤 return filtered_dishes class ComplexityRule(Rule): 根据餐别和时间限制复杂度 def __init__(self, allowed_complexities: List[Complexity]): self.allowed allowed_complexities def filter(self, candidate_dishes: List[Dish], context: Dict[str, Any]) - List[Dish]: return [d for d in candidate_dishes if d.complexity in self.allowed]接下来是生成器的核心。# generator.py import random from typing import List from models import Dish, MealType, ScheduledMeal from rules import Rule class MenuGenerator: def __init__(self, dish_library: List[Dish], rules: List[Rule]): self.dish_library dish_library self.rules rules def generate_weekly_menu(self, days: List[str], meal_types_per_day: List[MealType]) - List[ScheduledMeal]: 生成一周菜单 scheduled_meals [] meal_history_context [] # 用于记录已安排的历史供规则使用 for day in days: for meal_type in meal_types_per_day: # 准备当前决策的上下文 context { day: day, meal_type: meal_type, meal_history: scheduled_meals.copy(), # 可以计算并放入 recent_ingredients 等 } # 初始候选集为整个菜品库 candidates [d for d in self.dish_library if meal_type in d.meal_types] # 应用所有规则进行过滤 for rule in self.rules: candidates rule.filter(candidates, context) if not candidates: # 处理无候选菜品的情况记录日志尝试放宽规则或使用备用菜品 print(f警告{day} {meal_type.value} 无符合条件的菜品规则 {rule.__class__.__name__} 过滤后为空。) # 简单处理重置候选集为符合餐别的基本集跳过此规则 candidates [d for d in self.dish_library if meal_type in d.meal_types] break # 从最终候选集中随机选择一道菜 if candidates: selected_dish random.choice(candidates) scheduled_meal ScheduledMeal(dayday, meal_typemeal_type, dishselected_dish) scheduled_meals.append(scheduled_meal) meal_history_context.append(scheduled_meal) else: # 即使放宽规则后仍无菜可选 print(f错误无法为 {day} {meal_type.value} 安排菜品。) # 可以插入一个默认的“外出就餐”或“自由发挥”虚拟菜品 scheduled_meals.append(ScheduledMeal(dayday, meal_typemeal_type, dishNone)) return scheduled_meals4.2 让项目变得实用关键功能扩展基础生成只是第一步。要让这个工具真正融入生活还需要以下几个实用功能采购清单生成这是核心价值之一。程序在生成菜单后应能遍历所有菜品聚合所需食材并根据常见包装单位如“一颗”、“一盒”、“500克”进行合并和量化。实现思路为每个菜品对象增加一个ingredients字段这是一个字典列表例如[{name: 番茄, quantity: 2, unit: 个}, {name: 鸡蛋, quantity: 3, unit: 个}]。生成菜单后遍历所有ScheduledMeal累加相同食材的数量。进阶功能考虑库存。允许用户输入现有库存生成的采购清单自动减去库存量。手动调整与锁定功能自动生成的结果不可能100%满意。必须提供界面命令行或图形界面让用户能轻松地替换某一天的某一道菜、锁定某道菜必须出现在本周菜单中或者禁止某道菜出现。实现思路在生成算法中“锁定”的菜品优先放入指定位置“禁止”的菜品在初始过滤时就被移除“替换”功能可以在生成后直接修改scheduled_meals列表。历史记录与重复偏好系统可以记录用户每次生成的菜单以及用户后续的手动调整。通过分析历史数据可以学习到用户的隐性偏好比如用户经常把生成的某道鱼换成鸡说明可能更偏爱鸡肉并在下次生成时赋予相关菜品更高权重。导出与集成将最终菜单和采购清单导出为PDF、图片或直接同步到笔记软件如Notion、Obsidian、待办应用如Todoist或购物APP形成工作流闭环。4.3 部署与使用从脚本到服务对于个人使用一个命令行脚本就足够了。你可以创建一个config.yaml来配置你的忌口、偏好规则然后运行python generate_menu.py --week-start 2023-10-30来生成下一周的菜单。对于家庭共享或小型团队可以将其部署为一个简单的Web服务使用Flask、FastAPI等框架。前端提供一个清爽的界面让家人或成员可以提交自己的忌口查看生成的菜单并进行投票或调整。后端则运行上述生成逻辑。我的部署经验我使用Docker将整个应用容器化数据库用SQLite文件挂载到卷上Web服务用Gunicorn运行。这样部署到家里的NAS或云服务器上都非常方便。定时任务如每周日早上自动生成新菜单并通过Telegram Bot推送到家庭群组大家可以直接在聊天中点击投票或提出替换建议。5. 常见问题、优化思路与避坑指南在实际开发和使用的过程中你肯定会遇到各种各样的问题。下面我总结了一些典型场景和解决思路。5.1 菜品库的构建与维护冷启动难题项目最大的挑战往往是起步阶段——空的菜品库。手动录入几十上百道菜非常枯燥。解决方案1从食谱网站或APP导入。寻找提供API或结构化数据的食谱平台编写爬虫或使用现有工具进行导入。注意遵守版权和 robots.txt 协议。可以只导入菜名和关键标签具体做法和用料后续慢慢补充。解决方案2社区共享。如果项目是开源的可以鼓励用户贡献自己的菜品库文件JSON格式。建立一个中央仓库大家共享和丰富菜品库。解决方案3渐进式丰富。不要追求一步到位。最初只录入你最常做的20-30道菜。每周使用系统生成菜单时如果遇到想吃的菜不在库里就顺手添加进去。几个月后你的个人菜品库就会非常丰富了。5.2 规则冲突与“无解”困境当规则设置得过于严格或菜品库太小时系统常常会报错无法生成菜单。排查流程检查日志查看是哪个规则导致候选集为空。是“不吃辣”和“每周两道川菜”冲突了还是“只要快手菜”但周末也想安排导致无解简化规则暂时关闭所有非核心规则只保留忌口和餐别看是否能生成。然后逐一启用规则定位到冲突点。扩充候选池检查对应餐别和规则的候选菜品数量是否过少。例如“周六晚餐”且“复杂度为简单”的菜品可能只有3道而“食材不重复”规则要求7天不重样这显然不可能。需要增加此类菜品或调整规则如允许周六晚餐复杂度为中等。设计建议为规则引入“弹性”或“权重”。不要总是“必须”而是“最好”。用评分系统代替二元过滤。例如“食材多样性”可以作为一个加分项而不是硬性排除项。这样即使没有完美解也能给出一个“较好”的方案。5.3 生成结果单调或不符合直觉有时生成的菜单看起来“可行”但总觉得别扭比如连续三天吃不同做法的鸡肉。优化方向改进多样性算法不要只对“主要食材”做冷却可以对“食材大类”禽肉、红肉、水产、豆制品、绿叶蔬菜也做冷却。确保营养来源的轮换。引入“套餐”或“固定搭配”概念有些菜天生就是搭配着吃的比如“红烧肉”配“清炒西兰花”“咖喱牛肉”配“米饭”。可以在菜品库中定义“组合菜”或设置“推荐搭配”标签生成时优先将搭配菜安排在同一天。人工微调优先承认算法的局限性把系统定位为“灵感生成器”和“后勤助手”而不是“最终决策者”。提供便捷的调整界面让用户拥有最后决定权。5.4 性能考量当菜品库增长到数千规则有几十条时简单的遍历和过滤可能会变慢。优化策略索引与缓存对菜品库建立内存索引例如按“餐别”、“主要食材”、“菜系”建立反向索引字典。过滤时先通过索引快速缩小范围再进行精细筛选。预计算一些基于全局历史的规则如本周已用食材可以在生成过程中动态维护一个集合而不是每次都从头计算。算法优化如果采用搜索算法合理设置剪枝条件和最大深度避免组合爆炸。5.5 数据持久化与版本管理你的菜品库、规则配置都是精心调整的成果需要妥善管理。强烈建议使用版本控制系统如Git来管理你的菜品库dishes.json和配置文件config.yaml或rules.yaml。每次尝试新菜谱或调整规则后都提交一次这样可以轻松回溯到之前的版本。分离个人数据与程序代码将数据文件放在独立的目录如data/通过配置文件指定路径。这样更新程序时不会覆盖个人数据。开发这样一个工具最大的收获不是代码本身而是它迫使你更系统地思考“吃饭”这件事。你开始有意识地去分析自己的饮食结构尝试新的菜谱并更合理地规划采购。它从一个技术项目慢慢变成了一个改善生活质量的伴侣。