
简介这是一套面向计算机相关专业学生与开发者的毕业设计级项目源码主题为基于Python、知识图谱Neo4j与生成式AI的智能食谱推荐系统适合用作毕设、课程设计、作业或项目立项演示也可供基础较好的学习者二次修改扩展。压缩包共43个文件约682KB以tsx与less为主配合ts、json、yaml等配置与样式文件另有Python脚本、shell部署脚本及图片素材前端页面、组件、布局与数据配置分层清晰便于按模块阅读与调试。项目已通过mac与Windows 10/11运行验证并附详细文档与全部数据资料答辩评审分达95分可帮助读者快速理解知识图谱构建、食谱推荐逻辑与生成式AI融合的实现思路掌握从数据组织到界面呈现的完整链路。目前已有294人学习适合需要完整高分毕设方案与可运行代码参考的读者下载研究。1. 从一份毕业设计标题拆开PythonNeo4j生成式AI的智能食谱推荐系统到底在做什么很多同学看到「基于Python知识图谱(Neo4j)和生成式AI的智能食谱推荐系统」这个标题第一反应是「又是一个堆名词的毕设」。但把它拆开看这其实是一条非常清晰的技术链路用 Python 做数据采集与处理把食谱、食材、口味、人群、营养这些实体和关系抽出来构建成一张知识图谱存进 Neo4j再用生成式 AI 把图谱检索出来的结构化结果翻译成人能读的推荐理由和菜谱文案。它解决的核心问题是传统推荐系统只会算「买了 A 的人还买了 B」但说不清「为什么推荐这道菜」而知识图谱恰好能补上「可解释性」这一环。这套方案适合三类人一是正在做毕设、需要一套完整可跑通项目的同学二是想入门知识图谱构建、但不知道拿什么数据练手的开发者三是想看看生成式 AI 怎么和结构化数据结合、而不是只会调 API 聊天的人。下面我按「数据怎么进图谱 → 图谱怎么查 → AI 怎么接 → 坑在哪」的顺序把这条链路讲透。2. 食谱知识图谱的数据建模实体、关系与 Neo4j 图结构怎么定2.1 先想清楚图谱里到底放什么节点和边知识图谱构建最容易翻车的地方不是技术而是建模。很多人一上来就写代码结果节点类型混乱、关系方向随意后面查询越写越别扭。食谱领域的常见建模方式是围绕「一道菜能不能被推荐给某个人」来设计。核心节点类型一般有这几类Recipe菜谱、Ingredient食材、Category菜系/品类、Flavor口味、Nutrient营养元素、User用户、HealthGoal健康目标比如减脂、增肌、控糖。关系则包括Recipe-[:CONTAINS]-Ingredient菜谱包含食材、Recipe-[:BELONGS_TO]-Category菜谱属于菜系、Recipe-[:HAS_FLAVOR]-Flavor菜谱口味、Ingredient-[:RICH_IN]-Nutrient食材富含营养、User-[:PREFERS]-Flavor用户偏好口味、User-[:HAS_GOAL]-HealthGoal用户健康目标。这样建模的好处是推荐时可以从 User 出发沿着 PREFERS、HAS_GOAL 走到 Flavor 和 HealthGoal再反向找到匹配的 Recipe整条路径天然可解释。常见做法是先用纸笔或 draw.io 把节点和关系画出来确认查询路径能走通再动手写导入脚本。2.2 用 Python 把 CSV 数据导入 Neo4j 的最小脚本假设你已经把食谱数据整理成了三个 CSVrecipes.csv菜谱基本信息、ingredients.csv食材及营养、relations.csv菜谱-食材对应关系。下面是用官方 neo4j Python 驱动批量导入的最小可跑脚本。from neo4j import GraphDatabase import csv # 连接 Neo4j默认 bolt 协议端口 7687 driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, 你的密码)) def load_recipes(tx, path): with open(path, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: # MERGE 保证重复执行不会产生重复节点 tx.run( MERGE (r:Recipe {id: $id}) SET r.name $name, r.category $category, r.calorie toFloat($calorie), idrow[id], namerow[name], categoryrow[category], calorierow[calorie] ) def load_ingredients(tx, path): with open(path, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: tx.run( MERGE (i:Ingredient {name: $name}) SET i.protein toFloat($protein), i.fat toFloat($fat), namerow[name], proteinrow[protein], fatrow[fat] ) def load_relations(tx, path): with open(path, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: # 先匹配两端节点再建关系 tx.run( MATCH (r:Recipe {id: $rid}), (i:Ingredient {name: $iname}) MERGE (r)-[:CONTAINS]-(i), ridrow[recipe_id], inamerow[ingredient_name] ) with driver.session() as session: session.execute_write(load_recipes, data/recipes.csv) session.execute_write(load_ingredients, data/ingredients.csv) session.execute_write(load_relations, data/relations.csv) driver.close()逻辑说明整个脚本分三步先建菜谱节点再建食材节点最后建关系。用 MERGE 而不是 CREATE 是关键因为毕设调试阶段你会反复跑导入脚本CREATE 会让节点翻倍MERGE 则按唯一键去重。参数说明id和name分别作为 Recipe 和 Ingredient 的业务主键建议在 Neo4j 里给这两个属性建唯一约束否则 MERGE 性能会随数据量下降。CREATE CONSTRAINT recipe_id IF NOT EXISTS FOR (r:Recipe) REQUIRE r.id IS UNIQUE; CREATE CONSTRAINT ingredient_name IF NOT EXISTS FOR (i:Ingredient) REQUIRE i.name IS UNIQUE;这两条约束建议在导入前执行导入几万条数据时能明显感觉到差别。2.3 数据从哪来爬虫与公开数据集的取舍食谱数据来源无非两种公开数据集和自己爬。公开数据集胜在干净、字段规整适合快速跑通链路自己爬胜在数据新鲜、可定制但清洗成本高。常见做法是先用公开数据集把图谱跑通验证查询和推荐逻辑没问题后再针对性地爬一批补充数据。爬虫部分用 requests BeautifulSoup 就够了重点是遵守目标站点的 robots 协议、控制请求频率。清洗阶段要处理的问题包括食材名称同义「西红柿」和「番茄」、单位不统一克/毫升/适量、口味标签缺失。这些脏数据如果不在入库前处理后面图谱查询会返回一堆重复或空结果这是血泪经验。3. Neo4j 查询与推荐逻辑从用户偏好走到候选菜谱3.1 用 Cypher 写一条可解释的推荐查询图谱建好之后推荐的核心就是一条 Cypher 查询。假设用户偏好「清淡」口味、目标是「减脂」我们要找出同时满足这两个条件、且热量低于阈值的菜谱。MATCH (u:User {id: u001})-[:PREFERS]-(f:Flavor {name: 清淡}) MATCH (u)-[:HAS_GOAL]-(g:HealthGoal {name: 减脂}) MATCH (r:Recipe)-[:HAS_FLAVOR]-(f) WHERE r.calorie 300 OPTIONAL MATCH (r)-[:CONTAINS]-(i:Ingredient) RETURN r.name AS 菜谱, r.calorie AS 热量, collect(i.name) AS 主要食材 ORDER BY r.calorie ASC LIMIT 10逻辑说明前两个 MATCH 从用户节点出发分别锁定口味和健康目标第三个 MATCH 用口味作为桥梁找到候选菜谱再用 WHERE 过滤热量OPTIONAL MATCH 是为了把食材也带出来方便后面生成推荐理由。参数说明r.calorie 300这个阈值不是拍脑袋定的减脂场景下一般按每餐 300-500 千卡控制具体数值可以根据用户基础代谢调整。LIMIT 10 是给生成式 AI 的输入做长度控制太多候选会让后面的文案生成变慢。这条查询的价值在于返回结果里每一条都能追溯到「因为用户偏好清淡 目标减脂 热量达标」这就是可解释推荐的骨架。3.2 多跳查询从一个节点出发怎么查多条路径热搜里常有人问「neo4j 查询从一个节点出发如何查询多条」这在食谱推荐里非常典型。比如从一道菜出发既要查它的食材又要查它的口味还要查有没有相似菜谱。MATCH (r:Recipe {name: 番茄炒蛋}) OPTIONAL MATCH (r)-[:CONTAINS]-(i:Ingredient) OPTIONAL MATCH (r)-[:HAS_FLAVOR]-(f:Flavor) OPTIONAL MATCH (r)-[:BELONGS_TO]-(c:Category) RETURN r.name, collect(DISTINCT i.name) AS 食材, collect(DISTINCT f.name) AS 口味, c.name AS 菜系这里用多个 OPTIONAL MATCH 而不是一条长路径是因为不同关系的基数不一样硬拼成一条路径会产生笛卡尔积结果数量爆炸。这是新手最容易踩的坑之一以为一条 MATCH 写完更优雅实际上查询计划和结果集都不可控。3.3 把图谱查询结果转成推荐候选集实际系统里推荐逻辑不会只跑一条查询。常见做法是分三路召回再合并一路按口味匹配一路按健康目标匹配一路按食材相似度匹配。每路各取 Top N合并去重后按加权分数排序。权重可以简单设为口味 0.4、健康目标 0.4、食材相似 0.2具体数值根据你的评测结果调。def recommend(user_id, top_k10): flavor_recipes run_query(FLAVOR_QUERY, user_id) goal_recipes run_query(GOAL_QUERY, user_id) similar_recipes run_query(SIMILAR_QUERY, user_id) scores {} for r in flavor_recipes: scores[r[name]] scores.get(r[name], 0) 0.4 for r in goal_recipes: scores[r[name]] scores.get(r[name], 0) 0.4 for r in similar_recipes: scores[r[name]] scores.get(r[name], 0) 0.2 ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return ranked[:top_k]逻辑说明三路召回各自独立避免单条查询过于复杂合并时用字典累加分数天然完成去重。参数说明top_k 控制最终返回数量一般 5-10 条比较合适太多用户看不过来太少显得推荐能力弱。4. 生成式 AI 接入把图谱结果变成人话推荐理由4.1 为什么不能直接让大模型推荐菜谱很多人第一反应是「直接问大模型推荐菜谱不就行了」。问题在于大模型不知道你的用户偏好数据也不知道你图谱里到底有哪些菜它生成的推荐是「通用知识」而不是「个性化结果」而且容易编造不存在的菜。正确姿势是图谱负责「选什么」生成式 AI 负责「怎么说」。图谱给出结构化候选和推荐依据AI 把这些依据组织成自然语言。4.2 构造 Prompt把图谱结果喂给模型def build_prompt(user_profile, candidates): lines [] for c in candidates: lines.append( f菜谱{c[name]}热量{c[calorie]}千卡 f主要食材{、.join(c[ingredients])}口味{c[flavor]} ) candidate_text \n.join(lines) prompt f你是一个食谱推荐助手。用户偏好{user_profile[flavor]}口味 健康目标是{user_profile[goal]}。以下是系统筛选出的候选菜谱 {candidate_text} 请从中挑选3道最合适的并为每道菜写一段50字以内的推荐理由 说明为什么适合这位用户。不要编造候选列表之外的菜谱。 return prompt逻辑说明Prompt 里明确给出用户画像和候选列表并加一句「不要编造候选列表之外的菜谱」这是防止模型幻觉的关键约束。参数说明推荐理由限制 50 字以内是为了控制输出长度和响应时间如果做的是对话式交互可以把历史对话也拼进去但要注意总 token 数。4.3 输出校验别让模型自由发挥生成式 AI 的输出必须做校验否则它可能把「番茄炒蛋」写成「西红柿炒鸡蛋」或者凭空加一道不存在的菜。常见做法是把模型输出里的菜名和候选列表做一次匹配匹配不上的直接丢弃或重新生成。这一步看起来多余但在实际系统里能挡掉大部分翻车情况。def validate_output(text, candidates): valid_names {c[name] for c in candidates} for name in valid_names: if name in text: return True return False如果校验不通过可以重试一次或者降级为直接展示图谱查询结果。这个降级策略很重要因为生成式 AI 接口偶尔会超时或返回异常不能让整个推荐流程挂掉。5. 避坑与排查这套系统最容易翻车的五个地方5.1 现象导入脚本跑第二遍节点数量翻倍原因用了 CREATE 而不是 MERGE或者 MERGE 的键不是唯一约束字段。解决所有节点导入统一用 MERGE并提前建好唯一约束。已经翻倍的数据可以用MATCH (n:Recipe) WITH n.name AS name, collect(n) AS nodes WHERE size(nodes) 1找出重复节点再清理。5.2 现象Cypher 查询返回结果数量远超预期原因多个 MATCH 之间没有关联条件产生了笛卡尔积。解决检查每个 MATCH 是否通过变量正确连接必要时拆成多个 OPTIONAL MATCH或者用 WITH 分步聚合。5.3 现象Neo4j 启动后内存占用飙升、查询变慢原因默认配置没有针对数据量调整页面缓存和堆内存分配不合理。解决在 neo4j.conf 里调整dbms.memory.heap.initial_size、dbms.memory.heap.max_size和dbms.memory.pagecache.size一般堆内存给 2-4G页面缓存给 1-2G具体看机器配置。热搜里「neo4j 没有使用配置文件内存」说的就是这个问题改完配置要重启服务。5.4 现象生成式 AI 返回的菜名和图谱里的对不上原因模型幻觉或者 Prompt 里候选列表格式不清晰导致模型理解偏差。解决Prompt 里用明确的分隔符列出候选加约束语句输出后做名称校验校验不过就降级展示原始结果。5.5 现象爬虫跑一半被封 IP 或返回空数据原因请求频率过高或者没有设置 User-Agent。解决加随机延时比如 1-3 秒、轮换 User-Agent、遵守 robots 协议。数据量不大时优先考虑公开数据集别在爬虫上耗太多时间。6. 让推荐结果可评测三个能写进毕设的验证技巧系统跑通只是第一步毕设答辩时老师一定会问「你怎么证明推荐是有效的」。这里给三个我常用的验证方法都不复杂但能让你的项目从「能跑」变成「有说服力」。第一个是离线评测。把用户历史偏好数据按 8:2 切成训练集和测试集用训练集生成推荐看测试集里的菜谱有多少出现在推荐列表里算命中率Hit Rate和 MRR。这两个指标不需要标注数据自己就能算。具体做法是对每个用户取他测试集里的一道菜看推荐列表里排第几排第一得 1 分排第二得 0.5 分以此类推最后取平均。第二个是消融对比。分别跑「只用图谱召回」「只用生成式 AI 直接推荐」「图谱AI」三种配置让几个同学盲评推荐理由的合理性。通常图谱AI 的组合在「理由可信度」上明显高于纯 AI这个对比结果写进毕设很有说服力。第三个是路径可解释性检查。随机抽 20 条推荐结果手动核对每条推荐背后的图谱路径是否成立。比如推荐了「清蒸鲈鱼」路径应该是「用户偏好清淡 → 清蒸鲈鱼口味清淡 → 用户目标减脂 → 鲈鱼热量低」。如果发现路径断裂说明图谱关系有缺失需要补数据。// 检查某条推荐背后的完整路径 MATCH path (u:User {id: u001})-[:PREFERS|HAS_GOAL*1..2]-(n)-[*1..2]-(r:Recipe {name: 清蒸鲈鱼}) RETURN path LIMIT 5这条查询能把推荐依据的路径可视化出来答辩时直接展示比空口说「可解释」强得多。最后说个我自己的习惯每次改完推荐逻辑先跑一遍固定的 10 个测试用户把结果存成 JSON 对比。这样能快速发现「改 A 影响了 B」的回归问题。知识图谱生成式 AI 这套组合链路长、变量多没有回归测试很容易改着改着就崩了。希望帮到你。本文还有配套的精品资源点击获取