
简介基于知识图谱和生成式AI的智能食谱推荐系统毕业设计源码面向计算机相关专业正在准备毕设的学生及需要项目实战练习的学习者。项目以知识图谱组织食谱数据结合生成式AI提供智能推荐难度适中适合作为课程设计或期末大作业。压缩包共42个文件约681KB其中以19个tsx、9个less等前端类型文件为主辅以3个ts、2个json配置文件以及main.py、yaml等后端与部署相关文件结构清晰便于快速定位前端页面、组件、布局与后端入口。项目经导师指导并通过评审得分98分源码全部本地编译可运行经过严格调试。目前已有404人学习下载适合需要参考完整项目结构、理解知识图谱与AI推荐结合方式或希望直接获取可运行源码进行二次开发的读者。1. 这个基于知识图谱和生成式AI的智能食谱推荐系统解决的是什么问题在各种推荐系统里最容易翻车的场景是用户说“今晚想吃得清淡点蛋白质高一点但不能碰花生最好十五分钟能端上桌”。协同过滤在没有用户行为数据时直接失效纯标签匹配又答不了这种组合约束。这个基于知识图谱和生成式AI的智能食谱推荐系统就是冲这类需求来的毕业设计源码先用图数据库把食材、菜谱、营养标签、场景之间的关联结构化再把图查询召回的结果交给大模型生成带理由的推荐文案。项目主干分成 Python 后端、React 前端和知识图谱数据层源码本地编译可运行适合计算机相关专业做毕设、课程设计或期末大作业有 Web 基础就能改。2. 知识图谱层四类节点和五条关系如何把推荐约束变成图查询2.1 推荐为什么需要一个图结构从冷启动和组合约束说起先说结论传统推荐在这个场景下的短板不在算法本身而在数据表达。协同过滤依赖“用户—物品”交互记录新用户进来没有任何行为推荐结果只能靠全局热门这就是冷启动问题。关键词标签匹配稍微好一点但它是平面结构回答不了“低卡但要有鸡胸肉、还要适合减脂期”这种带多约束的查询——标签系统里这些条件只能靠 AND 硬拼拼出来的结果往往既不准也不可解释。知识图谱把“菜谱包含哪些食材”“菜谱打了什么营养标签”“菜谱适合什么场景”“哪些食材互相冲突”这些关系变成图上的路径。查询条件变成一次图遍历比如“从鸡胸肉食材节点出发走到所有菜谱节点再过滤掉含花生的路径”这就是天然的可解释推荐。后面接大模型时图谱已经给出了结构化候选集大模型只负责润色文案、补充理由而不是凭空生成菜谱这样既避免了 AI 幻觉也保住了推荐结果可控。2.2 节点与关系设计四类实体和五条关系照着建就行这个系统里我按使用频率把图谱设计成四类节点Recipe即菜谱节点记录菜名、热量、烹饪时间、难度Ingredient食材节点记录名称、分类比如肉类、蔬菜、豆制品NutrientTag营养标签节点比如低卡、高蛋白、少油Scene场景节点比如减脂期、学生餐、节日聚餐。关系也固定成五条Recipe 指向 Ingredient 的 CONTAINS表示菜谱包含某食材Recipe 指向 NutrientTag 的 HAS_TAG表示菜谱具备某营养属性Recipe 指向 Scene 的 SUIT_FOR表示适合某场景Ingredient 指向自身的 IS_A表示上下级分类比如“鸡胸肉 IS_A 禽肉”Ingredient 之间用 CONFLICT_WITH 表示禁忌冲突比如“花生 CONFLICT_WITH 对坚果过敏人群”。对应建库时先给节点字段建唯一约束避免导入重复数据。这里给出初始化结构的 Cypher 语句// 为两类核心节点建立唯一约束防止重复导入 CREATE CONSTRAINT recipe_id IF NOT EXISTS FOR (r:Recipe) REQUIRE r.id IS UNIQUE; CREATE CONSTRAINT ingredient_id IF NOT EXISTS FOR (i:Ingredient) REQUIRE i.id IS UNIQUE; // 建立索引按名称查食材时避免全图扫描 CREATE INDEX ingredient_name IF NOT EXISTS FOR (i:Ingredient) ON (i.name);逻辑说明第一段给菜谱和食材的 id 字段建唯一约束这是后续用 MERGE 做幂等写入的前提重复执行导入脚本不会产生双份数据。第二段给食材名称建索引推荐接口里大量查询以食材名称为入口没有索引时节点多了会明显变慢。参数说明约束名 recipe_id、ingredient_id 是自定义的可改成你习惯的命名REQUIRE 语法要求当前图数据库版本支持如果用的是老版本需要把约束语法换成 CREATE CONSTRAINT ON (r:Recipe) ASSERT r.id IS UNIQUE 这种旧写法。2.3 召回查询一个 Cypher 同时过滤热量、食材与场景图模型立住以后核心召回逻辑就变成一条 Cypher 查询。后端拿到用户输入“鸡胸肉 低卡 减脂期”后把约束翻译成图遍历召回符合条件的菜谱并按热量升序返回// 召回含鸡胸肉、带低卡标签、适合减脂期的菜谱 MATCH (r:Recipe)-[:CONTAINS]-(i:Ingredient {name: 鸡胸肉}) WITH r MATCH (r)-[:HAS_TAG]-(t:NutrientTag {name: 低卡}) MATCH (r)-[:SUIT_FOR]-(s:Scene {name: 减脂期}) RETURN r.name AS recipe_name, r.calories AS calories, r.cook_time AS cook_time ORDER BY r.calories ASC LIMIT 10逻辑说明第一段先锁定包含鸡胸肉的菜谱这一步把候选集收窄WITH r 把当前结果传递到下一段避免后续 MATCH 产生笛卡尔积膨胀。后面两段分别校验“低卡”标签和“减脂期”场景三个条件全部满足的菜谱才进入返回结果。参数说明name 的值是中文时注意保证 Neo4j 数据库存储编码为 UTF-8否则匹配不上LIMIT 10 控制召回数量实际项目里我建议调到 20给后面规则过滤和 LLM 排序留一些余量。ORDER BY r.calories ASC 是简单的热量升序如果你想优先推荐口味更重的菜可以改成按评分字段降序。2.4 数据初始化从 CSV 到图数据库的常规导入路径这个项目里的菜谱和食材数据量不大常见做法是用 CSV 存放在后端某个 data 目录里首次启动时一次性导入。导入脚本长这样// 从 CSV 导入食材节点MERGE 保证重复执行不产生重复节点 LOAD CSV WITH HEADERS FROM file:///ingredients.csv AS row MERGE (i:Ingredient {id: row.id}) SET i.name row.name, i.category row.category;逻辑说明LOAD CSV WITH HEADERS 会把 CSV 第一行当字段名row.id、row.name 对应每一列。MERGE 会先按 id 查找节点存在则只更新属性不存在才创建配合前面建的唯一约束这个脚本可以反复执行。参数说明file:///ingredients.csv 是相对路径文件必须放在图数据库安装目录的 import 文件夹下否则会报找不到文件的错误。导入中文 CSV 时我犯过一个低级错误用 Excel 另存的 CSV 默认是 ANSI 编码进图数据库后中文全变乱码解决方法是把文件另存为 UTF-8 编码或者用 Python 脚本先做一次编码转换再导入。3. Python 后端main.py 里的混合推荐主链路从路由到模型调用3.1 从入口看整体流程FastAPI 风格的路由与中间件打开 main.py从目录结构和路由写法能看出这个工程用的是 FastAPI 风格。整个后端流程是前端把用户需求 POST 到 /recommend 接口服务端先连接图数据库做召回再做规则过滤最后调用大模型生成推荐文案。下面这段代码是这个工程里最核心的应用初始化部分from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel app FastAPI(titlerecipe-recommender) # 前后端分离开发时必须配 CORS否则浏览器会拦截请求 app.add_middleware( CORSMiddleware, allow_origins[http://localhost:8000], allow_methods[*], allow_headers[*], ) class RecommendRequest(BaseModel): ingredients: list[str] # 用户手头有的食材 max_calories: int 600 # 热量上限默认 600 千卡 tags: list[str] [低卡] # 营养标签可多选 scene: str 减脂期 # 场景 avoid_food: list[str] [] # 禁忌食材逻辑说明CORSMiddleware 加在应用实例上用于解决前端页面在 localhost 不同端口调用接口时的跨域拦截问题。RecommendRequest 是一个 Pydantic 模型FastAPI 会自动校验请求体结构前端漏传字段时用默认值兜底避免接口直接报 422 错误。参数说明allow_origins 里的地址要写成前端实际跑起来的地址比如前端用 8000 端口这里就放 http://localhost:8000。max_calories、tags、scene 的默认值是我按“减脂推荐”这个典型场景设置的实际使用中前端会显式传值覆盖。avoid_food 是禁忌食材列表对应图谱里的 CONFLICT_WITH 关系下面过滤阶段会用到。3.2 图召回、规则过滤、大模型解读三段式推荐逻辑推荐主链路我习惯拆成三个函数每个函数只干一件事出了问题也好排查。第一段 graph_recall 负责把请求参数拼成 Cypher 查询执行并返回候选菜谱第二段 rule_filter 负责把候选里含禁忌食材的菜谱剔除掉顺带做去重第三段 llm_interpret 把结构化候选交给大模型生成带理由的推荐文案。def recommend(req: RecommendRequest): # 第一步按请求参数查知识图谱召回候选菜谱 candidates graph_recall(req) # 第二步剔除含禁忌食材的菜谱并做基础去重 filtered rule_filter(candidates, req.avoid_food) # 第三步调用大模型把结构化候选转成推荐文案 result llm_interpret(filtered, req) return {items: result}逻辑说明候选人少时rule_filter 里直接用 Python 集合操作判断菜谱包含的食材是否和 avoid_food 有交集即可不需要再查图。llm_interpret 放在最后一步是因为大模型调用耗时且可能失败先尽量把候选集缩到最小再调用能显著降低超时概率和调用成本。参数说明graph_recall 内部把 req.ingredients 里的食材名称用参数化查询拼进 Cypher避免字符串拼接注入风险。rule_filter 注意保留原始顺序因为图查询结果已经是按热量排好序的过滤后如果重新排序会丢失这一层优先级。3.3 大模型调用封装temperature、timeout 和降级策略大模型在这个系统里的定位不是生成菜谱而是把图谱召回结果“翻译”成用户能直接看的推荐理由。所以 prompt 里要明确告诉模型只能基于给定菜谱列表回答不要自己编新菜。下面这段是这类调用比较稳的封装方式import requests def call_llm(prompt: str) - str: resp requests.post( LLM_ENDPOINT, headers{Authorization: fBearer {LLM_API_KEY}}, json{ model: LLM_MODEL, messages: [{role: user, content: prompt}], temperature: 0.3, max_tokens: 800, }, timeout20, ) resp.raise_for_status() return resp.json()[choices][0][message][content] def llm_interpret(candidates, req): if not candidates: return {message: 没有找到匹配的菜谱, items: []} try: prompt build_prompt(candidates, req) content call_llm(prompt) return parse_llm_reply(content) except Exception: # 大模型挂了也不让接口挂降级返回结构化候选 return {items: candidates[:3], source: graph_fallback}逻辑说明call_llm 把 prompt 和参数包成 POST 请求拿到响应后只取 choices 里的文本内容。llm_interpret 里 try 包裹整个调用过程任何异常都走降级分支直接返回图谱候选保证接口始终有响应不会因为外部服务不稳定而整体不可用。参数说明temperature 设为 0.3是为了让模型在“基于事实改写”时少发散数值越高越容易发挥但越容易偏离候选菜谱。max_tokens 限制回复长度按 800 够输出三四条带理由的推荐了。timeout 设 20 秒是这个场景的临界值低于 10 秒在模型服务忙时基本必超时高于 30 秒会让前端等到失去耐心。3.4 配置管理密钥、数据库连接串不要写进仓库这个项目运行时需要两类敏感配置大模型 API 密钥和图数据库连接信息。常见做法是单独建一个 config.py从环境变量读取而不是把真实密钥硬编码进 main.py。我见过太多临时毕设项目把密钥写死在代码里最后打包发布时跟着源码一起泄露。import os LLM_ENDPOINT os.getenv(LLM_ENDPOINT, ) LLM_API_KEY os.getenv(LLM_API_KEY, ) LLM_MODEL os.getenv(LLM_MODEL, default-model) NEO4J_URI os.getenv(NEO4J_URI, bolt://localhost:7687) NEO4J_USER os.getenv(NEO4J_USER, neo4j) NEO4J_PASSWORD os.getenv(NEO4J_PASSWORD, )逻辑说明每个变量用 os.getenv 读取带默认值的适合本地开发比如数据库地址默认连本机不带默认值的强制要求环境变量存在缺失时启动阶段就应该抛错提醒。这个文件本身建议写进 .gitignore。参数说明NEO4J_URI 默认 bolt://localhost:7687这是图数据库的 Bolt 协议端口注意不要和浏览器访问用的 7474 端口混淆Python 驱动用的是前者。LLM_MODEL 里的默认值按你实际接的模型服务商文档填不同厂商的模型标识符差异很大。4. React 前端food-react-master 的工程结构、启动与联调4.1 前端页面与组件职责从 pages 到 common 的目录分工前端部分是一个标准的 umi 工程目录结构里 src 下面分了 pages、components、assets、layouts、common 几类。pages 是页面级路由组件components 放在页面里复用的业务组件layouts 是整体布局common 主要放请求封装、常量定义这类跨页面共享的逻辑。按这个工程实际页面来对我判断至少有四类页面首页展示推荐入口和菜品卡片流推荐结果页展示后端返回的菜谱列表和推荐理由知识图谱可视化页用关系图组件展示食材和菜谱的关联结构AI 对话页用户输入需求后端返回生成式文案。组件层面会拆出菜谱卡片、食材标签选择器、搜索框、加载状态这几个高频复用件。4.2 pnpm 安装与启动一次跑通的命令序列项目里带 pnpm-lock.yaml说明依赖管理用的是 pnpm不是 npm。新机器上第一次跑命令顺序和版本匹配是两个最容易踩坑的点。按下面顺序来通常能直接跑通# 进入前端工程目录 cd food-react-master # 安装依赖使用 lock 文件锁定版本 pnpm install # 启动开发服务器 pnpm start逻辑说明pnpm install 严格按 pnpm-lock.yaml 解析依赖版本同一个 lock 文件在不同机器上装出来的依赖树是一致的这是保证“本地编译可运行”的关键。pnpm start 会读取 .umirc.ts 里的配置默认起一个开发服务器并监听代理转发规则。参数说明如果本机没装 pnpm先通过系统包管理器或 Node 自带的 corepack 启用启动前检查 Node 版本和工程开发时用的版本差异过大时pnpm install 可能会因为某些原生依赖编译失败而报错。另外注意不要直接删掉 pnpm-lock.yaml 改成 npm install这会把依赖树整个打散容易触发版本不兼容。4.3 请求封装与跨域代理前端怎么和后端连通前后端分离开发时最省事的连通方式不是在后端配 CORS而是在前端开发服务器里配代理。.umirc.ts 里通常有一段 proxy 配置把前端发出的 /api 请求转发到后端地址export default { npmClient: pnpm, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, }, }, };逻辑说明开发服务器收到路径以 /api 开头的请求时会转发到 target 指定的 8000 端口。这样浏览器里页面请求的同源地址实际上被开发服务器代理到了后端天然绕开跨域限制。参数说明target 要写后端实际监听地址如果后端跑在别的机器上就改成对应 IPchangeOrigin 设为 true 后后端收到请求时的 Host 头会是 target 地址否则某些框架会因 Host 校验拒绝请求。上线时这段代理配置不再生效需要由部署端 Web 服务器统一做反向代理。请求调用的封装通常放在 common 目录统一走 umi 自带的 request 方法import { request } from umi; // 把后端 /recommend 接口封装成可复用函数 export async function fetchRecommend(params: RecommendParams) { return request(/api/recommend, { method: POST, data: params, }); }逻辑说明umi 的 request 自动带上 JSON 序列化和 Content-Type 头页面组件里只需要调 fetchRecommend 拿到 Promise。返回体结构要和后端保持一致推荐接口的返回是 { items: [...] }页面拿到 items 后渲染成菜谱卡片列表。参数说明params 是调用方传入的对象对应后端 RecommendRequest 的字段名建议和 Python 侧的字段保持完全一致比如 ingredients、max_calories否则后端解析会得到空值或报 422。4.4 知识图谱可视化与 AI 对话页的数据流图谱可视化页不是重点功能但做好了答辩效果会好看不少。数据通常来自后端一个只读接口返回图谱的节点和关系数组前端用关系图组件渲染。数据流是页面加载时 GET /graph 拿到 { nodes: [...], links: [...] }传给图表组件生成力导向图。注意数据量知识图谱节点几百个时问题不大几千个时浏览器会明显卡顿此时后端要做节点数截断。AI 对话页的数据流稍有不同用户输入一句话前端 POST 到 /recommend 或单独的 /chat 接口后端查图拿到候选后交给大模型返回的文案带流式效果。如果没有实现流式输出可以退而求其次把大模型响应整个拿回来后一次性渲染交互略笨但功能完整。5. 避坑记录安装、图数据与接口联调的 5 个典型翻车点5.1 依赖安装与版本类现象前端目录执行 pnpm install中途报错 panic或者装完以后 pnpm start 直接白屏控制台一堆模块找不到。原因大多数情况是本机 Node 版本和工程开发时期的主版本不一致。pnpm-lock.yaml 里锁了部分依赖的安装包版本但 Node 运行时的原生模块需要重新编译版本差异大就编译失败。解决先看 package.json 里的 engines 字段或 README 里是否标注了兼容 Node 版本没有标注就查看 lock 文件首部的 lockfileVersion 判断年代把本机 Node 切到该时代的主流版本区间。切换版本用 nvm 这类版本管理工具最省事装好以后重新跑 pnpm install不要在原报错目录上反复重试。5.2 数据库连接类现象后端启动时报连接失败提示 localhost 对应端口没有服务监听或认证失败无法连接数据库。原因两个端口容易搞混——图数据库的浏览器界面走 HTTP 端口 7474Python 驱动连的是 Bolt 端口 7687。后端配置里写的是 bolt 端口但有人用浏览器先打开了 7474 确认服务在跑后端却还是连不上另一种情况是数据库服务压根没启动或密码和 Neo4j 安装时设置的不一致。解决先用浏览器访问 7474 端口确认数据库面板能打开再用命令行或 Python 脚本测试 7687 端口通不通。认证错误时到数据库配置文件里临时改回初始密码或者通过命令行工具执行重置密码语句。最保险的做法是把 NEO4J_PASSWORD 配置通过环境变量传入别写死在 config.py 里。5.3 食材名称匹配不上现象接口返回空列表但明明图数据库里有这道菜直接在图查询工具里执行同样的 Cypher 却查得到。原因中文名称匹配不上基本都是编码或字段值不一致。最常见的是导入 CSV 时编码不是 UTF-8图数据库里存的是乱码其次是查询参数里带空格比如“鸡胸肉 ”和“鸡胸肉”不匹配还有一种情况是导入时某个食材把“鸡胸肉”写成了“鸡小胸”名称字段不统一。解决把导入用的 CSV 全部转成 UTF-8 编码再导入在查询前对请求参数做 strip 去空格名称统一问题先在项目里建一个食材别名映射表把“鸡小胸”“鸡里脊”这类近似食材映射到标准名再进图查询。5.4 大模型接口超时导致推荐失败现象前端点推荐按钮后转圈几十秒最后接口报错页面弹网络错误后端日志显示 requests.post 一直卡住直到抛异常。原因大模型服务响应慢是常态尤其在调用高峰时段。没设 timeout 时requests 库默认会一直等到底层 TCP 超时用户体验就是白等。更隐蔽的问题是重试机制缺失一次偶发超时就直接让整个推荐链路失败而图召回阶段的结果本来是可用的。解决调用层统一加 timeout并且把重试逻辑做成两层——第一层针对连接错误快速重试一次第二层在最终失败时走降级直接返回图谱候选集。这样即使大模型完全不可用推荐功能也只是少了文案不会整体崩溃。5.5 跨域与请求路径不一致现象前端页面能打开但所有推荐请求都报 CORS error 或 404。CORS error 说明请求发出去了但响应被浏览器拦截404 说明请求路径和后端路由对不上。原因CORS error 通常是后端中间件没配或 allow_origins 没包含前端地址404 大部分是代理配置里前端发 /api/recommend后端路由却定义在 /recommend代理没有做路径重写。解决路径不一致时在代理配置里加一层路径重写把 /api 前缀剥掉再转发或者在 FastAPI 侧把路由统一加 /api 前缀。两种做法选一种但前后端要约定一致不要出现前端发 /api/recommend、后端只接受 /recommend 的割裂状态。6. 验证推荐质量用一份需求清单把系统调到能答辩的状态系统跑通只是第一步推荐质量才是答辩时被追问的重点。我习惯在项目里维护一份“需求清单”包含 5 类典型用户需求低卡减脂、高蛋白增肌、少油清淡、不吃某类食材、节日聚餐。每轮改完推荐逻辑后不用人工去页面点直接用 curl 把清单里的需求逐条打进接口看返回结果是否符合硬约束curl -X POST http://127.0.0.1:8000/recommend \ -H Content-Type: application/json \ -d {ingredients:[鸡胸肉],max_calories:600,tags:[低卡],scene:减脂期,avoid_food:[花生]}判断返回结果时我只看三件事硬约束有没有违反比如含花生却被推荐排序是否符合用户预期低卡需求下热量低的排前面生成的推荐理由是不是从候选菜谱事实出发有没有出现模型凭空编造的菜名。三条都满足这一轮调优就算过了。这类“清单自测”适合开发期反复执行比页面点点点快得多。真正答辩前再补一次链路走查启动后端确认图数据库服务在跑再启动前端从页面完成一次完整推荐。后端日志里重点看两个指标Cypher 查询执行时间是否在百毫秒级大模型调用是否稳定。太慢就检查图谱数据量是否过大或查询是否没有走索引。这套代码本身已经把三种关键技术串起来了知识图谱保证推荐可解释可约束生成式 AI 保证推荐文案像真人写的前后端分离方便各部分单独调试。如果你想在毕设里加点差异化改进我建议优先做一层缓存把相同查询参数的推荐结果缓存几分钟图查询和大模型调用都省了系统响应速度会有一个明显提升——这个改动要动的地方不多后端推荐函数外面包一层缓存逻辑就行。这项目我先后跑过两遍第一遍就是没注意 Node 版本卡在前端白屏上大半天第二遍老老实实按清单先检查环境再启动半小时就跑通了全部链路。从那以后我每次拿到新源码都强制先看一眼 lock 文件、再确认数据库服务状态才动手执行任何安装命令。希望帮到你。本文还有配套的精品资源点击获取