简介面向计算机专业毕业设计及项目实战学习者内含一套基于知识图谱与生成式AI的智能食谱推荐系统完整源码。项目以知识图谱组织食材、菜谱与营养标签关系结合生成式模型输出个性化推荐方案覆盖从需求分析、数据库设计到前后端联调的完整流程适合用于毕设、课程设计或期末大作业。资源包共42个文件约681KB以tsx、less、ts等前端源码为主包含React页面与样式模块另有Python后端处理脚本、JSON与YAML配置、shell部署脚本等兼顾开发调试与发布环境。目前已有404人学习或下载。项目经导师指导并评审98分本地调试可运行内部按前端、后端、公共组件等模块划分便于阅读和二次扩展对理解知识图谱落地及生成式AI应用有参考价值。1. 基于知识图谱和生成式AI的智能食谱推荐系统到底做成了什么打开你自己的前端页面输入“冰箱里有一块鸡胸肉、半颗西兰花、两个鸡蛋”系统不会只给你十条菜名列表而是会根据健康目标返回“低卡鸡胸肉炒西兰花”并附上一句“这道菜蛋白质含量约22g脂肪低于6g符合你的减脂需求”。这就是基于知识图谱和生成式AI的智能食谱推荐系统知识图谱把食材、菜谱、营养标签和用户偏好之间的关联存成一张可跳转的图生成式AI再把推荐结果包装成有人味、有理由的话术。这份Python毕业设计源码是一套完整的前后端分离项目前端工程是food-react-master后端入口是main.py能跑通从用户输入到图谱查询、再到AI生成推荐文案的整条链路。它适合正在做毕设的计算机专业学生也适合想练全栈实践、又不想从零开始写推荐逻辑与前端页面的学习者。2. 架构与技术选型为什么要把“食材关系”和“推荐文案”拆开处理2.1 标签推荐的问题与知识图谱的选型理由如果只用贴标签的方式做推荐比如给鸡胸肉贴“低脂”“高蛋白”那用户问“我花生过敏能吃什么”时系统就只能靠人工维护一份过敏名单根本解释不了“因为宫保鸡丁里有花生碎而你点了过敏体质标签所以它被排除了”这种多级关系。知识图谱把实体抽象成节点把“菜谱包含食材”“食材适合体质”“食材禁忌体质”这些关系抽象成边查询时可以从任意节点出发沿着关系路径跳转这是普通关系型数据库和标签体系做不到的。从实现成本来看在毕设周期里用图数据库Neo4j存食材关系并不算重。Neo4j自带Cypher查询语言支持事务、索引和批量写入后端Python通过py2neo就能直接连接。相比用二维表硬维护多对多关系图结构天然适合食材、菜谱、营养、体质这几个实体之间的复杂关联。而且答辩时画一张“鸡胸肉—西兰花—减脂菜谱”的关系子图比贴一大堆SQL表直观得多。更重要的是后续加新关系类型不需要改表结构只要在Cypher里新增一种关系就能往上挂数据。2.2 生成式AI在系统里的职责边界第一次做这类系统的人最容易犯的错是让AI直接生成菜谱。我见过最离谱的输出是“香蕉炒茄子是传统菜肴”。原因很简单没有知识图谱约束的生成式AI本质是在做续写不是在查事实。所以在这份源码的推荐链路里生成式AI不是决策器只是文案包装器——先由知识图谱确定候选菜谱再由自然语言模型生成推荐理由和适配说明。这种分工有两个直接好处候选集可控推荐结果不会出现不存在的菜推荐语有解释性用户能看到“为什么给我推这道菜”。我在调试时踩过这个坑一开始让模型直接输出菜名跑十条有两条是瞎编的。后面改成“图谱选菜、AI说话”稳定性立刻上来了。你答辩时也可以抓住这个点知识图谱保证推荐的菜真实存在生成式AI保证推荐的理由像人话两者缺一不可。如果方向反了系统就成了一个披着AI外套的抽奖机。2.3 前端与后端工程结构从 .umirc.ts 到 main.py这份源码的目录能看出它是真实的前后端分离工程不是拿单页HTML糊出来的。前端基于React和UmiJS后端是Python整体结构都在压缩包里。为了让你启动时心里有数我把关键文件和目录的作用列成了一张表。文件/目录作用main.pyPython后端入口包含API接口与推荐服务food-react-master/.umirc.tsUmiJS配置文件路由、代理、打包参数都在这food-react-master/typings.d.tsTypeScript类型声明food-react-master/src/pages前端页面比如推荐页、食谱详情页food-react-master/src/components可复用组件比如菜谱卡片、食材输入框food-react-master/src/common公共工具函数和请求封装food-react-master/src/layouts页面整体布局package.json / pnpm-lock.yaml依赖声明与锁文件注意用pnpm安装publish.sh / publish_oss.sh前端部署和OSS上传脚本从main.py看后端更像FastAPI风格带类型注解和接口文档。如果你打开过FastAPI项目会看到路由装饰器、Query参数、Response Model都可以写在同一个文件里。启动命令通常是uvicorn main:app --reload启动后浏览器访问/docs就能看到接口列表这个特性在答辩演示时很有用不用写测试工具就能手动调接口。前端那边UmiJS有约定式路由在src/pages下建一个文件就自动生成对应路由所以看到目录里有多个tsx页面文件时基本上对应了页面导航上的几个Tab。3. 把食材关系写成图知识图谱建模与 Cypher 查询实战3.1 实体与关系设计一份能跑的图模型知识图谱建模不需要把菜谱所有属性都塞进去只留推荐系统真正用得到的属性就好。我按五个实体理解这源码里的图谱食材、菜谱、营养标签、体质标签、用户洞察。食材是关键节点菜谱通过“包含”关系指向食材营养标签通过“属于”关系挂在菜谱上体质标签通过“适合”或“禁忌”关系连接食材。用户那边则通过“偏好”关系与菜谱关联这样推荐时可以把用户做过的菜、收藏过的菜考虑进来。关系类型建议用英文命名避免中文关系在JSON和代码拼接时出现转义问题。下面这组关系是我调试时常用的可以直接当建库脚本参考。实体A关系实体B含义菜谱(Recipe)CONTAINS食材(Ingredient)菜谱包含某食材食材(Ingredient)SUITABLE_FOR体质(Tag)食材适合某体质食材(Ingredient)FORBIDDEN_FOR体质(Tag)某体质禁忌该食材菜谱(Recipe)HAS_TAG标签(Tag)菜谱带低脂、高蛋白等标签用户(User)PREFERS菜谱(Recipe)用户偏好某菜谱注意不要把所有属性都堆在关系上。比如“菜谱包含食材”的用量可以写在CONTAINS关系属性里方便之后计算营养用户最后点击时间写在PREFERS关系属性上查询偏好时就能按时间倒序。我把这个设计原则总结成一句话节点存稳定基本信息关系存交互行为和场景量值。3.2 用 py2neo 建节点与关系写图数据库前建议先确认Neo4j已经启动Bolt端口可通。然后用py2neo批量写入几千个节点完全不需要走官方Import工具直接用Subgraph一次提交几百条就够了。from py2neo import Graph, Subgraph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, password123)) graph.run(MATCH (n) DETACH DELETE n) # 调试时重置库 nodes [] rels [] # 建食材和菜谱节点 ingredients { chicken_breast: {name: 鸡胸肉, protein_g: 22, calorie: 118}, broccoli: {name: 西兰花, fiber_g: 2.6, calorie: 34}, } recipes { stir_fry_broccoli_chicken: {name: 西兰花炒鸡胸肉, difficulty: 低}, } for key, attrs in ingredients.items(): nodes.append(Node(Ingredient, **attrs)) for key, attrs in recipes.items(): nodes.append(Node(Recipe, **attrs)) # 把菜谱和食材关联起来 rels.append(Relationship(recipes[stir_fry_broccoli_chicken], CONTAINS, ingredients[chicken_breast])) rels.append(Relationship(recipes[stir_fry_broccoli_chicken], CONTAINS, ingredients[broccoli])) graph.create(Subgraph(nodes, rels))这段代码有两个容易翻车的地方。一是Relationship构造时直接用字典里的Node对象不要只用字符串名字否则Neo4j可能找不到对应节点二是graph.create是自动提交事务的写入量太大时建议分批提交比如每500个节点一个Subgraph避免内存突然涨起来。写完以后在Neo4j浏览器执行MATCH p(n:Recipe)--() RETURN p就能看到关系图长什么样。连接参数里bolt://localhost:7687是Neo4j的Bolt协议端口auth里第一个是用户名第二个是安装时设置的密码。如果你的Neo4j跑在Docker里端口可能映射到别的宿主机端口记得同步改main.py里的连接配置否则后端起服务时会报连接失败。3.3 查询候选菜谱Cypher 写法与参数解释当用户在前端输入几个食材后后端要查“这些食材能做哪些菜”。Cypher里先定位食材节点再通过CONTAINS反查菜谱最后返回菜谱和全部配料方便后续做营养计算和缺料提示。MATCH (r:Recipe)-[:CONTAINS]-(i:Ingredient) WHERE i.name IN $userIngredients WITH r, collect(i.name) AS matched MATCH (r)-[:CONTAINS]-(all:Ingredient) WITH r, matched, collect(all.name) AS allIngredients WHERE size(matched) toInteger($minMatch) RETURN r.name AS recipe, matched, allIngredients ORDER BY size(matched) DESC LIMIT toInteger($limit)这段查询的逻辑是先搜集用户输入的食材名找出所有包含至少minMatch个食材的菜谱再把菜谱的全部配料带出来按命中数量排序。minMatch一般设成2意思是至少两个食材对得上推荐结果才不太偏limit设成5到10后端拿到候选后再交给生成式AI包装。如果用户输入的食材在知识图谱里完全没有匹配节点返回就是空列表前端需要处理“未找到候选菜谱”的情况不能让它直接传到AI那边。实际执行时用graph.run(query, parameters{userIngredients: [鸡胸肉, 西兰花], minMatch: 2, limit: 5})传参。千万不要用f-string直接拼用户输入进Cypher一旦引号没处理好就会报语法错误而且有注入风险。我习惯把所有查询参数都放进字典这样Neo4j驱动会自动做类型检查和转换。4. 生成式AI接进推荐链路Prompt 设计、参数设置与结果校验4.1 推荐链路里生成式AI的“位置”整个推荐接口的执行顺序是接收用户请求 → 解析食材和偏好 → 查询知识图谱拿到候选菜谱 → 组装Prompt → 调用生成式AI接口 → 解析AI返回的JSON → 把推荐理由附在菜品结果里返回前端。这个顺序意味着生成式AI永远位于图谱查询之后只做翻译不做决定。很多翻车案例都是因为流程反了让AI先给菜名再去图数据库里找这个菜是否存在。一旦找不到程序只能报错或者返回预置文案。把图谱候选塞进Prompt后AI的任务变成“从候选里挑一个最合适的并写理由”它没有机会自创菜谱推荐结果的可靠性就高了不少。4.2 调用生成式AI的Python封装后端里一般会有一个build_prompt函数和一个call_llm函数。下面是简化后的骨架可以放到main.py里跑通。def build_prompt(user_input: str, candidates: list) - str: candidate_lines \n.join( f{i1}. {item[recipe]}食材{,.join(item[ingredients])} for i, item in enumerate(candidates) ) return f 你是家常菜推荐助手。依据下面的候选菜谱从用户需求出发挑一个最合适的。 只允许从候选里选不要自创菜名。输出JSON格式 {{recommendation: 菜名, reason: 推荐理由, replacements: 可替代食材如有}} 候选菜谱 {candidate_lines} 用户需求{user_input} def call_llm(prompt: str): # 实际项目中换成你自己的大模型接口配置 response openai.ChatCompletion.create( modelgpt-3.5-turbo, messages[ {role: system, content: 你只输出JSON不要输出多余文本。}, {role: user, content: prompt}, ], temperature0.3, max_tokens256, request_timeout15, ) return response[choices][0][message][content]这里几个参数对毕设项目很重要。temperature0.3是为了降低随机性让同一道菜在不同次调用里的推荐理由差异不要太大max_tokens256限制输出长度防止模型为了凑字数写成长篇大论request_timeout15避免模型接口卡住时整个推荐接口无响应。如果你没有openai库也可以直接用requests调HTTP接口但最好在服务端做一层超时控制并且用json参数严格解析返回内容。4.3 防止AI瞎编食材前置约束与后置兜底前置约束是把候选菜谱名单写进Prompt并且强调“只允许从候选里选”。但即使这样模型还是有概率把理由里的食材写得和候选不一致比如候选里根本没有牛奶模型却因为语言惯性写了一句“加入牛奶更顺滑”。所以后置校验一定要做校验菜名在不在候选集里校验理由里出现的食材在不在菜谱配料单里。import json def parse_llm_output(text: str, candidates: list, fallback_reason: str) - dict: try: data json.loads(text) recipe_name data.get(recommendation, ) if not any(c[recipe] recipe_name for c in candidates): raise ValueError(模型输出不在候选集中) return { recipe: recipe_name, reason: data.get(reason, fallback_reason), replacements: data.get(replacements, []) } except Exception: return {recipe: candidates[0][recipe], reason: fallback_reason}兜底逻辑里我默认把匹配食材最多的候选菜放第一位作为AI输出非法时的替代。理由文案用“该菜谱使用的食材最贴近你的输入且整体营养搭配均衡”这种通用句保证前端不会展示空内容。如果你觉得一次生成失败概率高可以加一次重试最多重试两次再不行就走兜底。毕设阶段我建议不要做过多重试因为演示时等待时间太长体验反而变差。如果你担心模型API费用又想离线演示可以换transformers加载本地模型。本地模型同样要用低temperature并且把max_new_tokens设到100到200同时注意模型权重要预加载不要在每个请求里重复加载否则一次请求可能要等十几秒。5. 避坑记录从 Neo4j 连不上到前端打包失败的五个问题这个课题的坑主要集中在环境、连接和AI输出三块。我把自己调试时踩过的五条记录按“现象→原因→解决”写出来你照着排查会快很多。5.1 Neo4j 连接失败Failed to establish connection现象运行main.py时直接报py2neo.connector或Bolt连接超时浏览器里却打不开Neo4j的Web管理页面。原因八成是Neo4j服务没启动或者Bolt端口没有监听。新版Neo4j Desktop启动后还需要手动打开当前数据库实例不是装完就自动运行。还有一种是密码不对Neo4j 4.x后初始密码不会再用neo4j/neo4j安装时必须设置很多人拿旧教程里的默认密码去连自然报错。解决先打开Neo4j Browser确认数据库状态是RUNNING。再用命令行检查端口是否监听Linux和macOS下执行lsof -i:7687Windows下用netstat -ano | findstr 7687。如果端口没起来去Neo4j配置里看server.bolt.listen_address是不是默认localhost:7687。最后确认main.py里Graph(bolt://localhost:7687, auth(neo4j, 密码))的写法。我见过有人把bolt写成http那是Neo4j Browser的端口不是驱动连接的端口。5.2 npm install 和 pnpm-lock.yaml 一起用导致依赖崩溃现象在food-react-master目录下执行npm install报了各种extraneous、peer dependency冲突哪怕删掉node_modules重装还是不行。原因源码里写的是pnpm-lock.yaml说明前端的依赖锁定是基于pnpm生成的。直接用npm install会按npm自己的解析规则重新计算版本遇到锁定文件不兼容就会出现依赖树混乱。解决优先使用pnpm。先确认全局安装了pnpm然后执行cd food-react-master pnpm install pnpm dev如果公司或学校网络拉取慢可以先把.npmrc文件里的registry换成本地镜像再重新安装。我不建议删除pnpm-lock.yaml去强行兼容npm因为锁文件里的版本组合是作者验证过的删了之后前端很可能会出现样式错乱或组件编译不过去。5.3 前端请求后端接口 404 或跨域失效现象前端页面能打开但一点“开始推荐”就报404或者浏览器提示CORS policy拦截。原因开发阶段前后端端口不同浏览器默认跨域。UmiJS项目里应该在.umirc.ts中配置proxy把/api开头的请求转发到Python后端。如果缺少这段配置请求直接打到前端开发服务器当然会404。解决在.umirc.ts里增加代理配置proxy: { /api: { target: http://127.0.0.1:8001, changeOrigin: true, pathRewrite: { ^/api: }, }, }注意pathRewrite这段如果后端接口本身没有/api前缀必须把/api去掉否则后端收到的路径里带着/api匹配不上路由。如果你后端main.py里定义的是app.get(/recommend)前端请求路径就应该是/api/recommend然后代理重写为/recommend。改完配置要重启pnpm devCtrlC再重新运行一次让配置生效。5.4 推荐理由里出现不存在的食材现象AI给的推荐理由写“这道菜加入了牛奶更香”但知识图谱里这道菜根本不含牛奶甚至用户还点了乳制品不耐受标签。原因模型在生成理由时会顺着语言惯性补充细节把“牛奶”这种常见食材写进去。中文大模型的幻觉概率比想象中高很多只靠Prompt约束无法根治。解决后置校验不能只查菜名还得校验理由里的每个食材词是否都在菜谱配料里。我在4.3节提到了这个思路具体实现时可以把理由按词切分后和配料做交集。更简单的方法是额外加一个字段“严格依据给定配料”同时把temperature降到0.2大部分情况下能避免。如果校验失败就丢弃模型输出使用模板文案。我一般设置最多重试一次因为连续两次生成同样幻觉的概率很低确保演示流畅。5.5 知识图谱更新后查询结果不对现象往Neo4j里新增了菜谱和食材但推荐接口怎么都查不到新数据重启后端也没用。原因可能是创建节点和关系时没有提交事务也可能是没有给name属性建索引。Neo4j在数据量变大后按属性查询会变慢甚至出现长时间扫描导致超时。py2neo的graph.create虽然是自动事务但如果你把写入逻辑分散在多个函数里某个函数中途抛异常会导致提交不完整。解决到Neo4j Browser执行一条索引创建命令CREATE INDEX FOR (i:Ingredient) ON (i.name); CREATE INDEX FOR (r:Recipe) ON (r.name);建完索引后再跑查询速度会明显改善。同时检查写入代码有没有在batch末尾统一调用graph.commit。如果用的是py2neo的autocommit模式要确保最后出现graph.create(subgraph)而不是只调了Graph()却忘写create。我自己的习惯是每写一批就立刻查询一次确认入图成功再继续下一批这样能从源头定位漏写的数据。这五条基本覆盖了从环境安装到推荐输出的闭环问题。按我的经验遇到报错先看日志其次查端口再看配置文件最后才怀疑业务逻辑。不要一上来就改main.py里的推荐算法那是效率最低的做法。6. 进阶技巧用“解释回溯法”验证推荐质量让毕业演示更可信如果你已经把推荐链路跑通先不要急着加用户登录、收藏这些边角功能而是要做一次“解释回溯”验证。方法很简单对每次AI返回的推荐理由检查理由里出现的食材是否都能在知识图谱中找到对应关系。这一步能自动发现AI输出里的幻觉也能在答辩时展示完整的逻辑闭环。我在源码基础上写过这样一个验证脚本给一个菜名从图谱里取出它包含的全部食材然后和AI返回的理由做比对。def verify_recommendation(graph, recipe_name): result graph.run( MATCH (r:Recipe {name: $name})-[:CONTAINS]-(i:Ingredient) RETURN collect(i.name) AS ingredients, namerecipe_name ).data() return result[0][ingredients] if result else []你可以把这脚本嵌进测试模块每次改完Prompt就运行一次随机喂20组不同的用户输入把推荐结果和验证结果一起输出到Excel。答辩演示时打开这个表格左边是AI给出的理由右边是图谱里实际存在的食材清单两者能对得上说明系统没有自说自话。如果再想加深印象可以加一个“缺料替代”功能让AI返回一个replacements字段字段里的替代食材也可以通过图谱里同体质标签的食材去生成整个项目就从“能跑”升级成了“有逻辑”。这套验证方法看起来简单但实际能拦住不少问题。我记得有一次改完Prompt后理由里出现了“虾仁”可对应菜谱是纯素的幸好跑了回溯脚本才在答辩前发现。从那以后我每次改完Prompt或更新图谱数据都会强制跑一遍脚本至少检查20条推荐结果再演示给别人看。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?