简介:这是一个基于知识图谱和生成式AI的智能食谱推荐系统完整工程,面向正在做毕业设计的计算机专业学生,也适合需要项目实战练习的入门者作为课程设计、期末大作业使用。项目采用前后端分离结构,前端以TypeScript/React技术栈呈现,包含19个tsx页面组件、9个less样式文件和类型定义,负责菜品展示、交互与推荐结果呈现;后端包含Python入口、YAML配置和依赖锁定信息,另有shell发布脚本,用于本地运行、调试与部署。整个压缩包共42个文件,仅681KB,目录层次清晰,解压后即可对照学习。源码经过严格调试,评审分为98分,内容经助教老师审定,难度适中,能够帮助读者理解知识图谱在食谱推荐中的应用以及生成式AI能力的接入方式。目前已有403人学习下载,适合作为毕业设计选题方案、系统搭建参考和功能复现素材。
1. 从“猜你喜欢”到“告诉你为什么并教会你做”
推荐系统最常见的尴尬,是推给用户一道“理论上相似”的菜,用户却完全没听过、不知道食材去哪买、更别说怎么做。基于知识图谱和生成式AI的智能食谱推荐系统,解法是把推荐从二维的“用户-物品”相似度,升级为“食材、菜系、营养、做法、口味”组成的知识网络,再让生成式AI为推荐结果补上人类能读懂的“理由”和“步骤”。换句话说,它不只是告诉你“推荐什么”,还回答“为什么推荐”和“你会不会做”。
这个项目非常适合Python毕业设计——它不依赖大规模算力,mainblock是Neo4j图数据库加一套推荐算法,再配一个生成式AI接口就能串起来;更难得的是,它天然分成数据、算法、应用三层,工作量便于分段推进,答辩时故事线也好讲。本文按我通常的做法,从知识图谱建模、推荐计算、生成式AI接入,到毕业设计工程化落地,拆成一条可以照做的路径。前置知识需要Python和基本SQL/数据库概念,推荐系统理论有了解更好,没有也能跟着代码走通。
2. 知识图谱建模与食材数据的预处理方案
2.1 为什么是知识图谱而不是关系型数据库
传统食谱系统用三张表——菜谱表、食材表、关联表——也能工作,但遇到“我想吃清淡的、含虾、30分钟内能做完的川菜”这类组合条件时,SQL关联查询会越写越复杂,还要处理“番茄炒蛋”和“西红柿炒鸡蛋”这种同义食材问题。知识图谱把万物建模为“实体-关系-实体”,查询模式天然是图路径,跨关系推理(食材→营养→菜系→做法)只需一次遍历,不需要多次JOIN。
知识图谱对推荐系统的另一个关键贡献是可解释性。协同过滤告诉你“与你相似的人喜欢这道菜”,但说不出为什么;知识图谱可以返回一条路径:用户偏好“高蛋白”→ 虾仁的蛋白质属性 → 虾仁属于海鲜 → 海鲜常出现于粤菜 → 推荐白灼虾。这条路径本身就是推荐理由,后面接生成式AI时,素材就来自这里。
2.2 实体、关系与属性设计
智能食谱推荐系统的知识图谱,建议从四种核心实体和五类边关系开始,别贪多,毕设规模控制在一千道菜、三百种食材,就能跑通全链路。
实体类型:
| 实体 | 属性建议 | 示例 |
|---|---|---|
| Dish(菜谱) | name, cuisine, cooking_time, difficulty, steps, flavor_profile | 麻婆豆腐 / 川菜 / 20分钟 / 入门 / 麻辣 |
| Ingredient(食材) | name, category, season, shelf_life | 豆腐 / 豆制品 / 全年 / 冷藏3天 |
| Nutrient(营养标签) | name, unit, health_benefit | 蛋白质 / g / 肌肉修复 |
| UserPreference(用户偏好) | user_id, taste, allergy, diet_style | u_1024 / 偏辣 / 花生过敏 / 高蛋白 |
五类边关系:
关系边一般用CQL创建,核心语句是:
CREATE (d:Dish {name: '麻婆豆腐', cuisine: '川菜', cooking_time: 20}) CREATE (i:Ingredient {name: '豆腐', category: '豆制品'}) CREATE (n:Nutrient {name: '蛋白质', unit: 'g'}) CREATE (d)-[:CONTAINS {amount_g: 300}]->(i) CREATE (i)-[:HAS_NUTRIENT]->(n) CREATE (u:UserPreference {user_id: 'u_1024', taste: '辣'}) CREATE (u)-[:LIKES {weight: 0.8}]->(d)这段代码创建了菜谱、食材、营养标签、用户偏好四个节点,以及“菜谱包含食材”“食材具有营养”“用户喜欢菜谱”三种关系。CONTAINS关系上的amount_g属性,会在后续做营养过滤时派上用场,比如“蛋白质含量低于30g的不要推荐”。
节点融合是构建图谱时第一个坑。同一食材在不同菜谱里叫法不一,常见做法是:
import re def normalize_ingredient(raw): text = raw.strip() text = re.sub(r'\d+(克|g|克/kg)?$', '', text) text = text.replace('西红柿', '番茄') return text我一般还会准备一个手工映射表synonym_map.json,覆盖“土豆/马铃薯”“青椒/柿子椒”这类高频同义食材。清洗规则不必追求百分百,毕业设计能覆盖top 80的食材就足够支撑推荐效果了。
2.3 向量化与嵌入存储
知识图谱构建好之后,推荐计算还需要一个向量化步骤。两个常用选择:
- TransE / DistMult 等图嵌入:把每个实体映射成低维向量,保留图结构语义。
- 自建同现矩阵加 Word2Vec:把“菜谱-食材”视为文档-词,用Word2Vec学习食材向量,实现成本低,效果也不差。
毕设场景我更推荐后者,数据量小、训练快,还能复用gensim的成熟接口:
from gensim.models import Word2Vec # corpus_by_dish: 每道菜的食材列表 dish_ingredient_corpus = [ ['豆腐', '牛肉', '豆瓣酱'], ['番茄', '鸡蛋'], ['虾仁', '西兰花', '蒜'], ] model = Word2Vec(sentences=dish_ingredient_corpus, vector_size=64, window=3, min_count=1, epochs=50) # 验证语义相似度 similar_ingredients = model.wv.most_similar('豆腐', topn=5) print(similar_ingredients)向量存哪?两个选择:直接压成.npy文件供内存调用;或者写进Neo4j节点属性。毕设建议选前者,省去图数据库扩容操作;答辩时可以表达“大数据量场景可替换为向量数据库”。
3. 融合知识图谱的智能食谱推荐算法设计
3.1 推荐主流程分层与热启动策略
一张图把推荐主流程拆清楚:
冷启动阶段(用户没有任何历史行为):先从知识图谱做面向前端的基础推荐,包含大众口味菜、当季食材推荐、低难度菜。内容侧保证质量,实现方式是查图谱:
MATCH (d:Dish) WHERE d.difficulty = '入门' AND d.cooking_time <= 30 RETURN d.name AS name, d.cuisine AS cuisine ORDER BY d.name LIMIT 10热启动阶段(用户有了评分、点击、搜索、收藏等行为):进入偏好画像 → 图谱路径召回 → 排序 → 重排 → 生成式AI理由五步流水线。这条路径在毕业设计里足够撑起推荐系统的完整论域。
3.2 基于图路径的偏好画像构建
用户偏好不只在显式评分里。基于点击行为就能建轻量画像。这里给出一个可运行的画像构建脚本核心片段:
def build_user_profile(user_id, clicks): profile = {'ingredients': [], 'cuisines': [], 'tastes': []} for dish, weight in clicks.items(): # 从Neo4j查询这道菜的食材、菜系、口味 cypher_query = """ MATCH (d:Dish {name: $dish_name}) OPTIONAL MATCH (d)-[:CONTAINS]->(i:Ingredient) OPTIONAL MATCH (d)-[:HAS_CUISINE]->(c:Cuisine) RETURN collect(i.name) AS ingredients, c.name AS cuisine """ # 累加权重到profile profile['ingredients'].extend( [(ing, weight * 0.8) for ing in row['ingredients']] ) return profile这里的要点是:不是简单记“看过麻婆豆腐”,而是把它拆成“牛肉、豆腐、川菜、麻辣、下饭菜”等标签,每个标签携带权重。点击权重默认1.0,收藏权重1.5,完整吃教程步骤则加权到2.0。这样向量化后,即使用户下一秒的兴趣漂移,也能实时反映出来。
3.3 DeepWalk / Node2Vec 排序与 EE 策略
偏好画像出来了,推荐排序有两种进阶组合:
方案A:规则路径加分。基于知识图谱的个性化路径查询,直接当作推荐结果召回:
MATCH (u:UserPreference {user_id: 'u_1024'})-[:LIKES]->(d1:Dish) MATCH (d1)-[:CONTAINS]->(i:Ingredient)<-[:CONTAINS]-(d2:Dish) WHERE d2 <> d1 RETURN d2.name AS recommended, count(*) AS score ORDER BY score DESC LIMIT 10这条查询做了基于共同食材的推荐,但它只是“相同食材”维度,推荐结果会陷入“吃过麻婆豆腐就只推豆腐菜”的尴尬。
方案B:图嵌入向量加相似度排序。把用户画像的向量和菜谱向量做点积或余弦相似度,再结合随机游走生成的Node2Vec特征,得到综合排序分:
from node2vec import Node2Vec from gensim.models import KeyedVectors # 用node2vec在Neo4j导出的边列表上学习图嵌入 node2vec = Node2Vec(edges_path, dimensions=128, walk_length=20, num_walks=10, workers=4) model = node2vec.fit(window=10, min_count=1, epochs=30) model.wv.save_word2vec_format('kg_embeddings.vec')这里walk_length=20表示每次随机游走20步,num_walks=10表示每个节点做10次游走。两者决定邻居探索深度:太大让远亲节点都算近邻,太小则只覆盖直接相连节点。对于一千到两千节点的图谱,20的步长和10的游走次数足够。嵌入维度128,对应多分类任务足够,图规模扩大后再考虑256。
排序分数计算公式,我一般用:
final_score = alpha * content_score + beta * graph_path_score + gamma * user_item_sim参数配置参考:
| 参数 | 推荐初始值 | 调节方向 |
|---|---|---|
| alpha | 0.4 | 内容质量冷启动时加大 |
| beta | 0.4 | 用户历史丰富后加大 |
| gamma | 0.2 | 有评分矩阵时可加大至0.3 |
| walk_length | 20 | 冷启动初期降低,数据多可提高 |
| top_k 候选池 | 50 | 最终展示取8~12条 |
EE策略(探索-利用平衡)建议用“随机ε”实现:有10%的概率从候选池里随机抽一道菜,不按分数排序。这么做的好处是防反馈闭环,避免只推荐相似口味让用户腻味;同时能攒到负反馈数据用于下一轮训练。毕业设计里在排序结果上做这个随机覆盖层,代码工作量不大,答辩讲清楚原理就能拿分。
3.4 从候选到结果:重排与去重
图谱推荐的常见问题是一堆菜高度重复。我一般会先按菜系或食材品类做MMR(最大边际相关)重排。核心逻辑是:每选一道菜,不仅要看它的推荐分数,还要惩罚与已选菜重叠度过高的候选。伪代码如下:
def mmr_rerank(candidates, already_selected, lambda_=0.7, top_k=8): result = [] while len(result) < top_k and candidates: best_score = -float('inf') best_item = None for item in candidates: relevance = item.score # 重复度: 与已选菜共享食材的比例 overlap = compute_ingredient_overlap(item, already_selected) mmr_score = lambda_ * relevance - (1 - lambda_) * overlap if mmr_score > best_score: best_score = mmr_score best_item = item result.append(best_item) already_selected.append(best_item) candidates.remove(best_item) return resultlambda_=0.7偏重相关性,lambda_=0.3偏重多样性。建议冷启动时用0.5~0.6,给新用户多展示不同菜系;老用户保持0.7。
这一层的算法要从“用户-菜谱”二元关系里挖掘:重复推同食材菜,会让用户觉得“系统没别的东西了”。MMR重排环节就是对多样化推荐的直接兑现。
4. 生成式AI接入:推荐理由与动态菜谱生成
4.1 生成式AI在食谱推荐系统里扮演什么角色
知识图谱擅长查关系,不擅长写文案。生成式AI要干的活有两件:
- 把图谱路径翻译成自然语言推荐理由:“因为你偏好高蛋白、低脂,且多次浏览海鲜类菜谱,虾仁西兰花和您的饮食方向匹配度较高。”
- 动态生成定制食谱细节:用户说“最近在减脂,能不能少油做这道菜”,生成式AI基于原菜谱改写,生成替代食材和烹饪建议。
这两个输出都直接面向用户,是系统展现“智能感”的关键入口。生成式AI部分完成度的高低,往往决定毕业设计打分天花板。
4.2 接入生成式AI的最小可运行方案
2025年这个时间点,主流接入方式已经是OpenAI兼容接口走统一调用。以国产大模型为例,结构化输入尤为关键。我的常见做法是:
import os import json from openai import OpenAI client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL") ) def generate_recommendation_reason(dish_info, user_profile, graph_path): prompt = f""" 你是资深美食推荐官。请用不超过80字写一段推荐理由。 菜名: {dish_info['name']} 菜系: {dish_info['cuisine']} 主要食材: {', '.join(dish_info['ingredients'])} 用户偏好: {json.dumps(user_profile, ensure_ascii=False)} 图谱路径依据: {graph_path} 要求: 说清楚“为什么推荐”,引用图谱路径中的偏好,不写空话,不用emoji。 """ resp = client.chat.completions.create( model="your-model-name", messages=[{"role": "user", "content": prompt}], temperature=0.7, max_tokens=150, ) return resp.choices[0].message.content这段代码的关键参数说明:temperature=0.7让文案有变化但不过于发散;max_tokens=150限制推荐词控制在合理长度;graph_path是前一步图谱查询出来的路径文本,例如“李四偏好高蛋白 → 虾仁蛋白质含量19g/100g → 虾仁→鳕鱼同属海鲜 → 推荐鳕鱼西兰花”。没有它,生成式AI就只能依赖菜品名瞎编,推荐理由的可信度会断崖式下降。
4.3 生成式AI与知识图谱的两种结合模式
第一种叫“图谱引导生成”:
用户请求 → 查询知识图谱得到图谱路径 → 把路径作为prompt上下文 → 调用大模型生成自然语言第二种叫“生成内容图谱回写”:
用户请求 → 大模型输出新菜谱 → 解析结构化字段 → 写回Neo4j图谱作为新的节点毕设建议重点做第一种,第二种作为扩展。理由:第一种是推荐系统核心能力闭环,不需要面对生成菜谱质量不稳定的问题;第二种涉及新菜谱节点、关系边、以及食材标准化映射,做不好会污染图谱,评分反而扣分。
4.4 生成式AI的稳定性和成本控制
两条必须提前处理的坑:
一是非结构化输出导致前端渲染崩。大模型偶尔会输出Markdown表格、代码块或换行混乱的文本。解决的常用做法是让模型输出JSON结构,再用Pydantic做约束解析:
from pydantic import BaseModel class RecipeAdvice(BaseModel): reason: str substitute_ingredients: list[str] cooking_tips: list[str] # 在prompt中要求: 直接输出JSON,不要Markdown代码块二是调用失败降级。毕设现场演示时,最怕大模型API挂了或者网络不稳。降级策略是必须的,保留一份本地规则模板:
def fallback_reason(dish_info, user_profile): return (f"为您推荐{dish_info['cuisine']}风味的{dish_info['name']}," f"主要食材包含{ '、'.join(dish_info['ingredients'][:3]) }," f"符合您当前偏好的{'高蛋白' if user_profile.get('taste') == '清淡' else '重口味'}方向。")最后再补充一次本地写入提示语优化:如果接入模型支持system prompt,在system层写明“你是DietGPT,一个基于知识图谱的膳食推荐文案助手”,输出质量会明显稳定。
5. 毕业设计的工程落地:从源码到可演示系统
5.1 技术栈选择与目录结构
毕业设计源码要能当场跑起来,技术选型千万别贪新。推荐组合:
后端 FastAPI + Neo4j + Python3.11;前端 Vue3 + Element Plus;嵌入服务用 gensim + node2vec;API层装openai库兼容大模型。如果不想写前端,FastAPI直接带一个 /docs Swagger界面也能演示接口。
目录组织方式参考:
recipe-recommendation-system/ ├── app/ │ ├── api/ # 后端路由 │ ├── core/ # 配置与依赖注入 │ ├── db/ # Neo4j连接与数据初始化 │ ├── models/ # Pydantic数据模型 │ ├── services/ │ │ ├── graph_service.py # 图谱查询 │ │ ├── rec_engine.py # 推荐引擎(召回/排序) │ │ ├── rerank.py # MMR重排 │ │ └── llm_service.py # 生成式AI接入 │ └── data/ # 菜谱csv / 图谱dump ├── notebooks/ # 数据处理与模型训练实验 └── scripts/ ├── build_graph.py # 从csv构建知识图谱 └── train_embeddings.py # 训练图嵌入/食材向量每个文件职责建议在代码文件头写清楚docstring,毕业设计源码查重和答辩讲解都省事。
5.2 知识图谱构建脚本的完整示例
构建Python知识图谱到Neo4j是最占工作量的一步。用一个纯Python脚本搞定数据管道:
import csv from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password")) def load_dish(csv_path): with driver.session() as session: with open(csv_path, encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: # 用MERGE防重复导入 session.run(""" MERGE (d:Dish {id: $id}) SET d.name = $name, d.cuisine = $cuisine, d.cooking_time = toInteger($cooking_time), d.difficulty = $difficulty """, **row)MERGE是这里的关键,它做“有则更新,无则创建”,避免重复跑脚本时产生脏数据。另外注意csv的编码用utf-8-sig,很多Windows导出的csv带BOM头,不解码会出现第一个列名带“\ufeff”字符的问题。
数据量适中时,每批用事务提交。推荐在脚本里加一个batch_size=200的控制项,Neo4j单事务执行大量MERGE会慢到怀疑人生。
5.3 演示环节的稳定性和答辩技巧
毕设演示最怕冷场和卡壳。教授最常见的问题是:“你如何证明推荐有效?”所以离线评估得准备。在源数据中留出20%的用户行为作为测试集,用 recall@10 和 hit_rate@10 作为指标:
def recall_at_k(recommended, actual, k=10): rec_top_k = set(recommended[:k]) if not actual: return 0.0 return len(rec_top_k & actual) / len(actual)只跑一个baseline不充分,更好的是对比两个方案:协同过滤只用UserCF vs 知识图谱+生成式AI的组合。预期结果知识图谱方案在包含图谱路径信息的新菜上命中率更高,冷启动场景优势明显。答辩PPT里放一张柱状对比图,胜过讲10页理论。
另一个高频答辩问题:“生成式AI在推荐里到底解决了什么问题?”回答要点:传统推荐系统只能给“推荐了什么”,生成式AI给“为什么推荐”和“怎么做”,两者是互补关系,不是替代关系。知识图谱给生成式AI提供事实基础,生成式AI把事实翻译成用户可感知的语言——这个回答框架可应对大部分追问。
5.4 一个必须做的性能优化
最后一章落到具体技巧:给Neo4j查询加查询缓存。毕业设计现场演示,用户反复点击查询,如果每次都走CQL,响应时间是200~500ms,但演示时连续操作10次,Neo4j的IO开销会让体验变差。
用Python一个functools装饰器就能解决:
from functools import lru_cache @lru_cache(maxsize=256) def graph_recommend(user_id_key: str, top_k: int): """带缓存的图谱推荐,user_id_key格式: user_id|top_k""" with driver.session() as session: result = session.run(RECOMMEND_CYPHER, user_id=user_id_key.split('|')[0]) return list(result)注意lru_cache的key必须是hashable,所以参数拼成字符串传入。用户行为一更新,需要手动刷新缓存:调用graph_recommend.cache_clear()或者在写入行为动作后重新构造key。这个细节写进文档、演示时提一嘴“我做了缓存设计”,是低成本的加分点。
数据规模再往上走时,才需要引入Redis或内存缓存层——毕业设计阶段一个函数级缓存已经足够,后面这句话留给答辩时展示你对扩展性的认识。
本文还有配套的精品资源,点击获取