基于Neo4j知识图谱与生成式AI的智能食谱推荐系统

📅 发布时间:2026/10/10 12:39:04
基于Neo4j知识图谱与生成式AI的智能食谱推荐系统
简介一份融合Python后端、Neo4j知识图谱与生成式AI的智能食谱推荐系统毕业设计项目面向软件工程、人工智能、电子信息等计算机相关专业的在校学生、教师及企业开发者可用于毕业设计、课程设计、项目初期立项演示也适合作为入门到进阶的学习范本。包内除完整源码外还提供详细文档、全部数据资料与环境配置脚本整体以React前端模块tsx/less/ts、Python服务端逻辑、JSON/YAML配置及Shell发布脚本为核心共43个文件约682KB目录结构清晰便于按模块检索与定位。该项目已获导师认可并以95分通过毕业答辩代码经Windows与macOS环境运行验证。目前已有294人学习/下载适合需要参考完整智能推荐系统实现、Neo4j图谱构建和生成式AI接入的高分项目范本可直接在此框架上进行功能扩展与实践改进。1. 智能食谱推荐为什么基于知识图谱而不是单纯大模型同样拿“冰箱里有鸡胸肉和西兰花”当输入普通菜谱 App 给你的是搜索式列表大模型直接生成的是“香煎鸡胸肉配蒜蓉西兰花”这种看似合理、实则没考虑你家里还有多少调料、以及你是否在控糖的答案。而这个标题里的系统核心思路是先让 Neo4j 知识图谱把“食材—菜谱—营养—人群”的关系查清楚再让生成式 AI 在确定性的图谱结果之上写人话。也就是说图谱负责“答得对”大模型负责“说得好”。对正在做毕业设计或想搭建推荐服务的从业者而言它的价值不只是跑通一条 Python 链路而是学会一套“结构化知识约束生成文本”的企业级思路——这套思路放到工业知识图谱、电商导购、医疗问答里都能复用并不只服务于食谱推荐。2. Neo4j 知识图谱建模食谱领域的实体、关系与导入细节2.1 为什么图谱比关系型数据库更适合食谱推荐食谱数据天然是网状结构一份菜谱“包含”若干食材一种食材“属于”某个分类一道菜“适合”某些人群同时又“禁忌”另一些人群。如果全部塞进 MySQL 的二维表你需要设计五六张中间表并且每当推荐逻辑要增加一种关系比如“地域口味偏好”就要改表结构、加 JOINSQL 越写越重。而 Neo4j 这类图数据库把关系作为一等公民存储遍历“某个食材出现在哪些菜谱里”这种问题Cypher 只要写一行 MATCH底层走的是图的局部遍历而不是全表扫描尤其在数据实体达到数千、关系达到数万的规模后性能差别非常明显。另外一点常被忽略知识图谱对推荐结果的可解释性是天然支撑。用户问“为什么推荐这道菜”你可以直接回溯图谱路径——因为“冬瓜”和“排骨”都是你冰箱里的食材且“排骨冬瓜汤”属于“清淡类”适合当前选定的“减脂期”标签。这套血缘追溯是黑盒协同过滤给不了的也是做毕业设计答辩时的亮点素材。数据模型我一般会在白板上先画实体和关系用约定俗成的标签命名然后才落库。2.2 用 Cypher 建立食材—菜谱—人群三元组实体节点大概分四类Ingredient食材、Recipe菜谱、Tag营养/口味/烹饪方式标签、Crowd人群。关系有四条Recipe 包含 IngredientIngredient 属于 TagRecipe 适合/禁忌 CrowdCrowd 需要 Nutrition营养成分。建立约束和索引是第一步否则后续数据量一上来MERGE 性能就会让导入脚本变乌龟。CREATE CONSTRAINT ingredient_name IF NOT EXISTS FOR (n:Ingredient) REQUIRE n.name IS UNIQUE; CREATE CONSTRAINT recipe_id IF NOT EXISTS FOR (n:Recipe) REQUIRE n.id IS UNIQUE; CREATE INDEX tag_name_idx IF NOT EXISTS FOR (n:Tag) ON (n.name); CREATE INDEX crowd_name_idx IF NOT EXISTS FOR (n:Crowd) ON (n.name);逻辑说明唯一约束先于数据导入生效等同于关系型数据库里的主键。食材和菜谱用唯一约束标签和人群用普通索引即可因为它们是低基数维度不需要过度约束。这里的语法是 Neo4j 5.x 的写法使用IF NOT EXISTS可以保证脚本重复执行不报错这在后面反复调试建模脚本时非常重要。参数说明REQUIRE子句替代了老版本里的ON如果你用的是 Neo4j 4.x应该写成ON (n:Ingredient) ASSERT n.name IS UNIQUE。这一点在搜教程时经常被忽略很多老帖子的语法在 5.x 上直接报语法错误属于入门翻车率最高的点。2.3 Python 批量导入py2neo 与官方驱动怎么选py2neo 是老牌库写起来方便但它在 Neo4j 4.4 之后就不再积极维护和 5.x 的兼容性基本靠社区补丁。现在做新的毕设项目我更建议直接用官方neo4jPython 驱动它走 Bolt 协议性能稳定而且支持异步会话。批量导入时最忌讳的做法是一条一条执行 Cypher正确的做法是用session.execute_write配合参数化查询在单个事务里批量 MERGE。from neo4j import GraphDatabase class RecipeGraphImporter: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def close(self): self.driver.close() def upsert_recipe(self, tx, recipe): tx.run( MERGE (r:Recipe {id: $recipe_id}) SET r.name $name, r.cuisine $cuisine, r.difficulty $difficulty, r.cook_time $cook_time , recipe_idrecipe[id], namerecipe[name], cuisinerecipe[cuisine], difficultyrecipe[difficulty], cook_timerecipe[cook_time] ) for ing in recipe[ingredients]: tx.run( MERGE (i:Ingredient {name: $ing_name}) MERGE (r:Recipe {id: $recipe_id}) MERGE (r)-[:CONTAINS {amount: $amount, unit: $unit}]-(i) , ing_nameing[name], recipe_idrecipe[id], amounting[amount], uniting[unit] ) def import_batch(self, recipes): with self.driver.session() as session: for recipe in recipes: session.execute_write(self.upsert_recipe, recipe)逻辑说明upsert_recipe方法先把菜谱节点 MERGE 进去再遍历食材列表建立 CONTAINS 关系。注意第二个MERGE (r:Recipe {id: $recipe_id})里没有 SET 语句它的作用只是匹配已存在的菜谱节点来挂关系避免重复创建属性这是很多新手容易忽略的细节。批量导入时事务窗口由execute_write自动管理每调用一次该方法就提交一个事务。参数说明amount和unit是关系属性比如“200 克”“1 勺”。把数量放在关系上而不是节点上是图谱建模里的常见做法这样“宫保鸡丁里的鸡丁”和“白切鸡里的鸡”虽然是同一食材节点但各自菜谱中的用量可以追溯。cook_time我用整数分钟便于后续做“15 分钟快手菜”这种过滤查询。difficulty用 1-3 的整数档位避免字符串排序带来的混乱。2.4 查询模板冰箱里有什么能做什么菜图谱建模完成后最核心的查询是“给定食材列表找出能做的菜”。这里不能用简单的IN条件因为用户冰箱里可能只有 4 种食材而菜谱包含 8 种食材你需要的是“覆盖度”计算。MATCH (r:Recipe)-[:CONTAINS]-(i:Ingredient) WITH r, collect(i.name) AS all_ings WHERE size(all_ings) 6 MATCH (r)-[:CONTAINS]-(need:Ingredient) WHERE need.name IN $fridge WITH r, all_ings, collect(need.name) AS matched_ings WHERE size(matched_ings) toInteger($min_match) RETURN r.name AS recipe, size(matched_ings) * 1.0 / size(all_ings) AS coverage, matched_ings ORDER BY coverage DESC LIMIT 10逻辑说明第一段先收集每道菜的全部食材过滤掉总食材数大于 6 的复杂菜谱第二段只保留冰箱里能匹配到的食材最后的coverage是匹配数除以总食材数用这个比例把“能做”和“勉强能做”排序区分开。toInteger($min_match)让调用方控制最少满足几样食材才算候选默认我传 2。参数说明$fridge是数组参数驱动传参时用list类型。这里有个性能细节——如果数据量大第一段WITH r, collect(i.name)会全图扫描建议预先给 Recipe 节点添加ingredient_count属性并在导入时维护查询时直接WHERE r.ingredient_count 6把图遍历量降一个数量级。3. 生成式 AI 接入让模型基于图谱结果写菜谱3.1 图谱检索是 RAG 的“证据链”而不是聊天开场白直接拿用户输入去问大模型它会把“冰箱里有鸡胸肉和西兰花”当成开放题生成结果可能很惊艳但不稳定。常识是两套信息源无法对齐。而接入图谱后你先通过 Cypher 查到真正匹配的菜谱候选与食材关系把这些结果作为“检索证据”注入提示词模型的工作被限制为“润色和扩展”而不是凭空发明菜谱。检索增强生成RAG在这里的落地形态不是向量库而是图查询结果——这属于结构化 RAG比文本切片的非结构化 RAG 更适合确定性要求高的领域。具体工程上我用 LangChain 的Neo4jGraph组件连接数据库再配合一个简单的提示词模板。需要说明的是LangChain 的 neo4j 集成版本更新频繁比如热词里提到的dify neo4j 0.0.7是 Dify 某个特定版本对 Neo4j 的适配插件版本差异经常导致向量索引命名不一致。我建议不依赖这类插件自己直接调用官方驱动取查询结果把图谱摘出来的 JSON 拼进提示词最简单也最好排查。3.2 提示词模板的设计约束优先于自由发挥给模型注入的上下文必须包含三部分用户需求、图谱查询结果、安全与营养约束。生成菜谱的提示词模板写成这样。prompt_template 你是一名营养师兼家常菜厨师。 请根据以下已知信息推荐并描述一道菜。 用户需求{user_request} 候选菜谱来自知识图谱必须严格基于这些信息 {graph_candidates} 营养约束{dietary_rules} 禁忌提示{taboo_rules} 要求 1. 只能使用候选菜谱里的菜品组合不得自行发明菜名。 2. 给出做法步骤每步控制在 30 字以内。 3. 最后用一句说明为什么适合该用户。 请输出 逻辑说明graph_candidates是上一章 Cypher 查出来的结果集我把它格式化成“菜名 | 包含食材 | 覆盖度”的纯文本taboo_rules来自图谱中 Crowd 节点的禁忌关系例如“糖尿病前期 → 减少淀粉类主食”。把禁忌放在独立变量里是防止模型在长对话上下文里遗忘这条硬约束——这也是我踩过的坑放在正文里模型偶尔会在菜名上“聪明地”规避、但配菜的汤汁里依然加糖。分离变量并放最后强调实测出错概率低很多。参数说明温度推荐 0.3 到 0.5。太高模型会文采过于丰富而不自觉偏离图谱内容太低则生成文字接近干巴巴的查询结果展示。max_tokens控制在 500 到 800食谱类任务需要结构化输出太长容易让模型在结尾重复开头的内容。3.3 本地大模型与云端 API 的取舍毕业设计环境里最常见的两种部署方式调用商用大模型 API或者本地跑开源模型如 Qwen、ChatGLM。我的建议是优先用 API 跑通流程把所有调试精力放在图谱查询和提示词上因为本地模型光环境配置就可能消耗掉一周时间而且 7B 级别的模型在中文菜谱生成上的措辞明显不如 API。等答辩前如果老师关注数据隐私再换成本地部署的 Ollama Qwen2.5-7B代码层面只需要把ChatOpenAI换成ChatOllama。热词检索里出现的“无限制无审核生成式 AI”这种说法不在讨论范围内——正经的推荐系统必须带约束过滤尤其是食品营养场景一句禁忌漏掉造成的后果不是技术评分能挽回的。内容安全底线在这里不是政治问题是四处漏风的安全问题。4. 推荐链路合流从“能做的菜”到“该吃的菜”的完整管线4.1 系统架构分层把前面的模块拼起来整个系统可以分成四层数据存储层Neo4j 图谱 食材/菜谱 CSV 源数据、检索服务层Cypher 查询封装成 Python 函数、生成增强层提示词组装 大模型调用、API 接入层FastAPI 暴露接口给前端或 Web 页面。分层核心原则是让图谱查询结果与模型生成结果之间只通过 JSON 传递不共享内存对象否则后续任何一个环节升级都要连带改其他模块。4.2 核心接口实现推荐一份带理由的菜谱推荐接口接收用户冰箱食材、忌口标签、用餐人数返回三道菜及推荐理由。这个接口在毕设演示中足够撑起交互效果。from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List app FastAPI() class RecommendRequest(BaseModel): fridge: List[str] avoid: List[str] [] diet_tag: str 家常 app.post(/recommend) def recommend(req: RecommendRequest): candidates query_graph_candidates( fridgereq.fridge, min_match2, max_ingredients6 ) if not candidates: raise HTTPException(status_code404, detail没有找到足够匹配的菜谱请补充食材) taboo query_taboo_rules(req.avoid) prompt build_prompt( user_requestf冰箱里有{,.join(req.fridge)}忌口{,.join(req.avoid)}, graph_candidatesformat_candidates(candidates), dietary_rulesreq.diet_tag, taboo_rulestaboo ) result_text call_llm(prompt, temperature0.4) return { recommendations: result_text, evidence: candidates[:3], taboo_applied: taboo }逻辑说明接口先把用户输入传给query_graph_candidates内部执行第 2.4 节的 Cypher得到图谱候选再去查询禁忌规则最后把两者拼进提示词。这里刻意把taboo_applied单独返回给前端让用户看到“系统确实考虑了过敏源/忌口”这对答辩的展示效果很重要也是图谱可解释性的直接体现。参数说明min_match2表示至少两道食材匹配max_ingredients6表示不做食材超过 6 种的复杂菜。这两个参数是召回效果最敏感的两个旋钮调大 min_match推荐更精准但容易空结果调小 max_ingredients推荐的菜简单但可能忽略用户冰箱里已有的“硬菜”。4.3 候选排序策略覆盖率优先还是营养均衡优先图谱查询返回候选后需要一个排序打分环节。我建议设计一个简单可解释的加权公式得分 0.5 × 食材覆盖率 0.3 × 营养匹配度 0.2 × 烹饪时间反比。食材覆盖率直接用第 2.4 节算的 coverage营养匹配度需要预先在 Tag 节点上标记“高蛋白”“低脂”“低碳水”等属性与用户选择的 diet_tag 比对烹饪时间反比则是让快手菜优先。这个公式看起来简单但两个细节值得注意一是营养匹配度的计算必须在图谱查询阶段就过滤掉完全不匹配的菜否则模型生成的文本里会出现“虽然这道菜很下饭但不太符合您的控糖需求”这种自相矛盾的话二是不建议引入协同过滤做排序因为食谱领域的用户评分数据在毕设阶段很难拿到冷启动问题会让协同过滤变成摆设。知识图谱方法在这里的价值恰恰是不需要历史行为就能给出合理推荐。4.4 生成式 AI 输出解析与兜底逻辑大模型返回的文本再好看如果前端需要结构化展示食材清单就得做输出解析。常见做法是要求模型输出 JSON然后用json.loads解析但模型偶尔会在 JSON 前后加解释性文字导致解析异常。我的兜底方案是两个先尝试从文本中提取最外层花括号内容再解析如果解析失败直接返回图谱候选里的原始菜谱数据并用模板渲染不让用户看到接口报错。用户感知上只是推荐结果稍微生硬一点不会认为系统故障。import json, re def safe_parse_llm_output(text: str): match re.search(r\{.*\}, text, re.DOTALL) if not match: return None try: return json.loads(match.group()) except json.JSONDecodeError: return None逻辑说明正则里的.*配合re.DOTALL可以跨行匹配到完整 JSON 对象json.loads失败时返回 None外层调用再走兜底渲染路径。这一步强烈建议保留别偷懒——我见过很多次大模型返回内容里混入 Markdown 表格符号、或者把引号转义成中文全角导致解析失败。5. 避坑清单Neo4j 查询、Python 依赖与生成式 AI 的常见问题5.1 py2neo 连接 5.x 数据库报“无法执行语句”现象代码用 py2neo 的Graph()连接本地 Neo4j 5.x报错类似于“The Client is unauthorized”或者 Cypher 语法错误。原因py2neo 对 5.x 的密码认证机制支持不完整且 4.4 之后官方驱动才是最佳路径。解决把from py2neo import Graph全部替换为from neo4j import GraphDatabase按照第 2.3 节的写法做。血泪经验是别为了省事保留 py2neo 的写法后面任何一次graph.run(MATCH ...)都可能成为随机炸弹。5.2 MERGE 并发重复创建节点现象导入脚本多次执行后图谱里出现大量同名 Ingredient 节点。原因两个事务同时执行 MERGE 时唯一约束尚未命中导致并发插入重复。解决确认在导入前执行了第 2.2 节的CREATE CONSTRAINT ... IF NOT EXISTS另外不要让多个线程共用一个驱动实例写数据用session.execute_write自动串行化事务。5.3 反向关系查询不出来现象建立了(r)-[:CONTAINS]-(i)但查询MATCH (i)-[:CONTAINS]-(r)返回为空。原因方向写反或:CONTAINS忘了加冒号。解决没有捷径先MATCH (r:Recipe {id:001})-[rel]-(n) RETURN type(rel), labels(n)检查关系实际方向再调整查询。这类“玄学”问题九成是方向写错。5.4 大模型生成不存在的菜谱现象提示词里明明限制了“只能使用候选菜谱”但输出里出现了图谱中不存在的“私房秘制酱香鸡”。原因模型没有从图谱检索结果中获取足够多信息时会凭借训练记忆补全。解决把候选菜谱数量控制在 5 条以内并附上具体食材在提示词里加上“如果候选结果不足请直接告知用户无法推荐不要补充菜品”用较低温度重试。没有百分之百的解决方案这属于生成式 AI 的固有风险需要从工程上做兜底。5.5 中文食材名匹配不上现象图谱里存的是“番茄”用户输入“西红柿”。原因同义词未归一化。解决导入时建立“别名”属性数组或者建一个Synonym节点指向标准食材。查询时先用 Python 做一次同义词映射把用户输入替换成标准名再传参。5.6 Neo4j Desktop 内存占用过高现象笔记本上跑 Neo4j Desktop导入 3 万节点后系统卡死。原因默认堆内存配置可能设为 512M或者同时开了太多应用。解决在 neo4j.conf 里调整server.memory.heap.initial_size1G、server.memory.heap.max_size2G并用server.memory.pagecache.size1G留足页面缓存。毕设数据量根本不需要高配服务器但内存配置不当会让 4G 的机器比 8G 的机器更容易 OOM这比查询优化更影响体验。6. 验证方法与进阶用三个指标证明这套系统靠谱毕设答辩时老师最爱问的是“你的推荐准不准”或“你的系统比普通搜索好在哪”。给不出量化指标前面的图谱建模、生成式 AI 接入都会变成“听起来还行”。我建议做一套简单的离线评测准备 50 条测试输入每条包含冰箱食材与忌口标签人工标注“期望推荐的菜”和“期望排除的菜”然后跑一遍系统统计推荐可执行率推荐菜谱的食材全部在冰箱或允许采购清单内、禁忌排除率系统完全没有推荐触碰忌口标签的菜这是安全红线、以及模型幻觉率生成文本里出现图谱外部菜名的占比。def evaluate(test_cases, recommend_fn): safe 0 executable 0 hallucination 0 for case in test_cases: result recommend_fn(case.fridge, case.avoid) if case.taboo_recipe_id not in result: safe 1 if all(ing in case.fridge for ing in result.used_ingredients): executable 1 if result.contains_external_recipe: hallucination 1 total len(test_cases) print(f禁忌排除率: {safe/total:.2f}) print(f推荐可执行率: {executable/total:.2f}) print(f幻觉率: {hallucination/total:.2f})逻辑说明三个指标各有侧重。禁忌排除率必须做到 1.0做不到就说明图谱端的禁忌过滤失效应该先修这个可执行率不用追求 1.0因为用户冰箱食材确实有限但至少应该达到 0.8 以上幻觉率低于 0.1 就算合格。这套评测脚本独立于系统运行测试数据里的人工标注部分哪怕只有 50 条也会让答辩说服力上一个台阶。进阶方向有两个。第一个是把“人群禁忌”从生成式 AI 提示词挪到图谱查询层例如“糖尿病患者”直接通过MATCH (r:Recipe)-[:TABOO]-(c:Crowd {name:糖尿病})把结果排除而不是靠模型自觉这是最稳的约束方式。第二个是把用户历史点餐行为写入图谱增加User节点与PREFERS关系让推荐结果随偏好收敛——这一步能让系统从“通用推荐”升级为“个性化推荐”论文的创新点也就有了。我自己的教训是做这类系统时永远把“确定性过滤”放在“生成式润色”之前顺序反了指标会像过山车一样难以复现。希望今天这份拆解对你做毕设、做产品原型都有点实际的帮助。本文还有配套的精品资源点击获取