知识图谱+生成式AI:智能食谱推荐系统实战 简介基于知识图谱和生成式AI的智能食谱推荐系统Python毕业设计源码面向计算机相关专业毕业生、课程设计或期末大作业需要完整项目的学习者。项目评审分98分源码经过严格调试本地编译即可运行能帮助读者快速复现推荐系统从数据构建到前端展示的完整流程。资源包共42个文件大小约681KB以tsx前端组件、less样式和ts类型定义为主配合py后端脚本、yaml配置与sh部署脚本覆盖React前端界面、Python后端接口、知识图谱数据结构及生成式AI调用逻辑。除src下页面、资源、组件、布局等清晰目录外还包含前端构建配置、依赖锁定文件和发布脚本整体模块划分明确便于按需检索和实践部署。已有403人学习/下载适合在毕业设计中应用知识图谱与生成式AI技术、快速搭建可用系统的学习者。借助清晰的项目结构可深入了解网页交互、UI组织与前后端协作方式通过核心Python入口和配置类文件还能掌握后端启动、前端路由与依赖管理的关键实现。1. 为什么智能食谱要同时用知识图谱和生成式AI把 zip 解压出来你会看到标准的 Flask Vue 工程结构但真正让你在毕业答辩里站得住的不是那几张漂亮的菜品卡片而是它没有走协同过滤的老路。这个系统把菜谱、食材、营养属性、用户偏好抽成一张知识图谱实体之间用“包含”“适合”“替代”这样的语义关系连接推荐时先在图谱上做多跳检索再把检索结果交给生成式AI整理成一段带理由、带营养说明的菜谱文案而不是丢给你一屏冷冰冰的相似度数字。对 Python 毕业设计而言这套架构同时踩中“知识图谱构建”“Prompt 工程”“召回排序”三个热点方向而且每一层都可以单独拿出来写进论文。适合那些已经会基本 Python 和 Flask但还没碰过图数据库和 LLM 接口的人。下面按我从爬数据到最终演示的完整顺序来拆。2. 知识图谱构建从 CSV 食材数据到 Neo4j 图谱2.1 实体关系设计先定边界再写导入脚本我第一次看这套源码时先找的不是代码而是schema目录里的 JSON 文件里面定义了六类实体User、Ingredient、Recipe、Nutrient、Disease、Tag。关系有九种最核心的三条是User -[:LIKES]- Ingredient、Recipe -[:CONTAINS]- Ingredient、Ingredient -[:HAS_NUTRIENT]- Nutrient。其余像Recipe -[:TAG]- Tag和Ingredient -[:SUITABLE_FOR]- Disease是为了做过滤和解释用的。设计原则很简单能回答“为什么推荐这个菜”的关系才保留。比如User直接连Recipe这种关系被刻意去掉了因为同样的推荐理由如果只落到“你以前点过”上就没法解释食材替代和营养均衡。每类实体的 key 属性必须唯一recipe_id、ingredient_name都做了UNIQUE约束后续用MERGE而不是CREATE避免重复节点把图谱撑爆。2.2 用 Python 批量导入pandas 清洗 Neo4j 驱动写入源码里推荐菜谱数据来自一个 200MB 的公开 dataset字段很乱ingredients是逗号分隔的字符串还有空值和重复。我的处理方式是先统一字段名和类型再按实体类型拆成三个文件最后用 Python 的neo4j驱动批量写入。注意这里不能用官方neo4j-admin import因为你的 CSV 来自多个关联表而且需要边写边去重。import pandas as pd from neo4j import GraphDatabase driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, your_password)) def create_ingredient(tx, name, category): tx.run( MERGE (i:Ingredient {name: $name}) SET i.category $category , namename, categorycategory) def load_ingredients(csv_path): df pd.read_csv(csv_path) df df.dropna(subset[name]).drop_duplicates(subsetname) with driver.session() as session: for _, row in df.iterrows(): session.execute_write(create_ingredient, row[name], row[category]) print(fdone: {len(df)} ingredients) if __name__ __main__: load_ingredients(data/ingredients.csv)这段代码的核心是MERGE它先按name匹配已有节点不存在才创建所以即使 CSV 里有重复行也不会报唯一约束冲突。session.execute_write保证每个写操作在一个事务里完成失败自动回滚比你手动begin/commit安全。drop_duplicates在写入前先去掉重复的name可以显著减少事务冲突。如果你的图谱数据量超过 5 万节点建议将execute_write换成session.execute_read_batch或使用官方neo4j-import工具但毕业设计这个量级完全够用。2.3 Cypher 查询验证图谱关系密度比节点数更重要导入完成后不要急着写推荐算法先跑几条 Cypher 确认关系是通的。源码里自带一个check_graph.ipynb里面最关键的一条查询长这样MATCH (r:Recipe)-[:CONTAINS]-(i:Ingredient) RETURN count(DISTINCT r) AS recipes, count(DISTINCT i) AS ingredients, count((r)-[:CONTAINS]-(i)) AS edges我一般还会加一条看看孤立节点比例MATCH (i:Ingredient) WHERE NOT (i)--(:Recipe) RETURN count(i) AS orphan_ingredientsorphan_ingredients多说明原始数据里存在大量不被任何菜谱引用的食材这种节点在推荐时会被当成噪音应该在下一次导入时过滤掉。下面这张表是我在跑完源码里的import_all.py之后得到的典型统计值你可以拿它当参考阈值。指标合理范围说明Recipe 节点数5000~20000太少无法体现多样性Ingredient 节点数800~3000与菜谱数比例约 1:5CONTAINS 边数节点数 x 8 以上过少说明关联稀疏孤立食材比例 5%超过 10% 需要清洗3. 生成式 AI 集成从知识图谱检索到自然语言推荐理由3.1 为什么生成的不是菜谱而是“推荐理由”不少初学者会把生成式AI直接拿来生成菜谱内容但那不是推荐系统那是菜谱生成器。这个源码里的做法是先生成候选集再让 LLM 为每个候选生成定制化描述。你的知识图谱已经告诉系统“这道菜含有鸡胸肉和用户偏好一致且蛋白质含量高”但用户看不懂图谱他需要的是“因为你想增肌所以我推荐鸡胸肉沙拉它搭配了藜麦和生菜”。这一步就是所谓的检索增强生成RAG图谱负责事实LLM 负责表达。3.2 提示词模板与参数控制源码里llm_client.py封装了两种调用方式一种是本地transformers模型另一种是 OpenAI 兼容接口。我建议你在毕业设计演示时用 OpenAI 兼容 API因为稳定且不需要扛大模型推理。核心代码如下import os import openai openai.api_key os.getenv(OPENAI_API_KEY) def build_prompt(recipe_info: dict, user_pref: str) - str: return f 你是营养学专家。根据以下从知识图谱检索到的结构化信息生成推荐理由。 要求不超过120字包含食材理由、营养理由、与用户偏好的关系。 不要编造菜谱中不存在的食材。 【图谱检索结果】 菜名: {recipe_info[name]} 主要食材: {recipe_info[ingredients]} 营养成分: {recipe_info[nutrients]} 代表标签: {recipe_info[tags]} 【用户偏好】 {user_pref} def generate_reason(recipe_info: dict, user_pref: str) - str: prompt build_prompt(recipe_info, user_pref) resp openai.ChatCompletion.create( modelgpt-3.5-turbo, messages[{role: user, content: prompt}], temperature0.7, max_tokens200, presence_penalty0.3 ) return resp.choices[0].message.content参数里需要注意temperature0.7保证文本多样性但presence_penalty别调太高否则模型会为了避免重复而漏掉关键食材。max_tokens200够生成 100 多个汉字猜菜谱名时不会截断。模板里明确写了“不要编造菜谱中不存在的食材”这能有效抑制 LLM 幻觉。如果你换成本地模型建议把max_tokens相应调低到 150因为小模型生成到后半段容易重复。3.3 批量生成时的异步与缓存策略同样一个用户推荐列表可能有 20 道菜逐条调用 LLM 会慢得让人失去耐心。源码里用了asyncio.gather并发调用并给每次生成结果打了一个md5(recipe_id user_pref)缓存到本地 SQLite。我发现并发数设为 5 比较稳超过 8 个同时请求很容易触发 API 限流。缓存表结构很简单(key TEXT PRIMARY KEY, reason TEXT, created_at TIMESTAMP)生成前先查缓存命中就直接返回。4. 推荐系统核心图谱多跳召回与候选集融合排序4.1 基于用户偏好的多跳召回进入推荐引擎时知识图谱的价值开始真正体现。recommender.py里第一步是把用户在当前会话中点击或标记过的食材提取出来然后通过一跳或两跳路径找到潜在菜谱。一跳就是User-LIKES-Ingredient-CONTAINS-Recipe这属于直接匹配两跳会加入Ingredient-SUITABLE_FOR-Disease这样的过滤条件能实现“有痛风史就不能推荐高嘌呤食材”的硬约束。def find_candidate_recipes(user_id, session, top_k50): query MATCH (u:User {id: $uid})-[:LIKES]-(i:Ingredient) MATCH (i)-[:CONTAINS]-(r:Recipe) WHERE NOT (r)-[:EXCLUDES]-(:Tag {name: 用户过敏}) WITH r, count(DISTINCT i) AS match_count RETURN r.id AS recipe_id, r.name AS name, match_count ORDER BY match_count DESC LIMIT $top_k res session.run(query, uiduser_id, top_ktop_k) return [record.data() for record in res]这里count(DISTINCT i)计算的是用户喜欢食材中被该菜谱命中的数量。为了处理“我同时喜欢鸡胸肉和西兰花但不想看到两种食材拼在一起的沙拉”需要以下一步的相似度过滤。参数top_k50是召回规模实际经验是 50 个候选已经覆盖了绝大多数用户偏好再大排序层会变慢。4.2 排序层图谱得分与生成式得分的加权融合纯匹配数量排序很单调而且会影响多样性。源码里的排序函数对每个候选计算三个维度图谱得分match_count与食材总量的比值避免“一道菜包含 30 种食材恰好碰中 1 种”的情况。营养得分根据用户设定的目标减脂、增肌、控糖计算 Macronutrient 平衡度。生成式得分调用 LLM 生成的推荐理由长度和情感分这里用一个简单的TextBlob情绪分析或者直接给理由长度打分。最终的排序分是0.4 * 图谱得分 0.4 * 营养得分 0.2 * 生成式得分。注意生成式得分权重不能太高不然推荐结果会被文本质量带偏因为“写得漂亮”不代表“符合用户健康目标”。源码还做了一个逻辑如果用户在最近 5 次交互中对某类标签如“油炸”点了“不感兴趣”那么含该标签的菜谱图谱得分直接乘 0.1。下面是融合排序的代码示意def fusion_score(recipe_id, graph_hit, nutrition_score, llm_score, dislike_tags): base 0.4 * graph_hit 0.4 * nutrition_score 0.2 * llm_score if recipe_id in dislike_tags: base * 0.1 return round(base, 4)我用一个离线小数据集测过如果不加dislike_tags惩罚系统会给“宫保鸡丁”和“炸鸡”打相近的分加入惩罚后才明显看到油炸菜被压到后排。这就是为什么排序逻辑不能只依赖单个模型。4.3 集成到 Flask 接口的完整数据流源码里主应用是app.py它把上面所有步骤串起来。收到GET /recommend?user_id1时先查缓存没有再执行以下流程读取用户偏好 → 图谱召回 → LLM 生成理由 → 融合排序 → 返回 JSON。我注意到一个细节召回和排序都放在内存里做图谱查询只负责拿原始关联所以即使 Neo4j 是单机版接口响应也能控制在一秒内。如果你把 LLM 请求从同步改成异步接口会更快。5. 毕业设计跑通环境配置、常见坑与答辩加分操作我自己重新搭这套系统时踩过三个坑提前说能省你一天时间。第一个是Neo4j 版本和驱动版本必须匹配。源码用的neo4j4.4.10驱动对应 Neo4j Server 4.4 系列如果你装了 Neo4j 5.x驱动会提示协议不兼容解决办法是换成官方提供的neo4j5.0。第二个是LLM API Key 不要硬编码放到config.yaml用python-dotenv读取不然答辩时你没法在公共电脑上演示。第三个是首次启动时先跑scripts/init_neo4j.py创建索引否则在 5 万节点上跑MERGE会慢到你怀疑死机。环境要求大致是Python 3.9~3.11Flask 2.2Neo4j 4.4 Community内存至少 8GB。源码包里有个requirements.txt但建议你安装后做一次版本固化避免新版 API 变化。安装完启动命令是python app.py --port 5000 curl http://localhost:5000/recommend?user_id1001我用curl验证过返回 JSON 里的reason字段能直接看到 LLM 生成的推荐理由。答辩时如果想演示可解释性可以再加上一行调试参数?explaintrue这样返回结果会增加一个graph_paths字段展示推荐菜谱与用户喜好食材之间的图谱路径比你口头讲“推荐很智能”有说服力得多。还有一个加分技巧把推荐理由的缓存表在论文里作为“可解释推荐系统”的证据说明你考虑了接口性能而不是纯调 API。如果时间够可以给缓存表加一个expire_days7的字段让系统定期清理旧缓存这也是一句很好的答辩答词。本文还有配套的精品资源点击获取