
简介面向计算机相关专业学生与开发者围绕基于Python、知识图谱Neo4j和生成式AI的智能食谱推荐系统这份资源提供了一套可直接用于毕业设计、课程设计或项目立项的高分完整项目尤其适合软件工程、人工智能等方向的学生参考。资源内包含前端页面、组件、公共布局、Python后端入口及部署脚本等模块并附详细文档、全部数据资料与图片素材代码结构清晰目录划分明确便于快速定位推荐逻辑、知识图谱构建、接口调用与界面渲染等关键部分。压缩包共43个文件核心为19个tsx、9个less等前端组件与样式文件搭配ts逻辑、Python脚本、YAML配置与Shell部署文件整包约682KB下载后即可在本地环境运行调试。目前已有293人浏览学习。项目在mac及Windows 10/11上验证通过答辩评审分达95分既可直接提交使用也可针对推荐算法、图谱查询或AI交互模块进行二次扩展是兼顾完成度与学习价值的实用范例。1. 智能食谱推荐系统为什么需要知识图谱和生成式AI一起上当用户说“今天想吃点清淡的最好能控糖”普通推荐系统能做的只是把“清淡”拆成关键词剩下全靠猜。基于Python知识图谱(Neo4j)的做法则把食材GI值、烹饪方式、用户忌口都建模成显式关系推荐问题变成一条带约束的图查询生成式AI补的是最后一环把结构化查询结果翻译成有做法、有步骤的人话。三者合在一起就是这个智能食谱推荐系统的核心链路Python做编排Neo4j做事实层LLM做语义转换。适合两类人看准备毕业设计或课程项目的同学想把图谱建模、图查询调优和LLM输出控制一次练完以及想了解推荐系统如何从行为矩阵切到语义约束的工程师。这个题目最大的价值不在算法多深而在于每条推荐都能用一条关系路径解释给评委听。2. 食谱知识图谱怎么建实体关系、约束索引与 Neo4j 导入 CSV 的三种姿势2.1 为什么选知识图谱而不是协同过滤或向量检索食谱推荐最常见的错误是照搬商品推荐的协同过滤套路给用户-菜品行为矩阵算相似度。但食谱场景里绝大多数用户不会留下足够密集的评分一道菜只被吃过两三次就进了稀疏矩阵的角落向量检索能解决文本召回却处理不了“不含花生”“小于30分钟”“要蒸的”这类反向与范围约束。知识图谱把约束放在边上和属性上查询时用NOT EXISTS和范围过滤直接完成推理过程也能回溯给用户看。三者的定位差异如下方案数据依赖反向约束推荐理由可解释性冷启动表现协同过滤用户行为矩阵不支持要额外做过滤层弱差向量检索菜谱文本向量需要后处理中中知识图谱食材-菜品关系原生NOT EXISTS支持强中这不是说知识图谱万能。它的代价在构建阶段食材别名、单位换算、分类树都要整理过一遍才能支撑查询。毕业设计的数据规模通常在一千至几千道菜建图成本完全可控还能顺便展示本体建模能力。所以这个标题把知识图谱(Neo4j)放在C位是成立的——建模成本低、查询表达力强、演示效果好三者同时成立。2.2 实体与关系设计把用量放在关系属性上设计食谱图谱时第一件事是定节点与关系的粒度。最常见的结构是三层Dish表示菜品Ingredient表示食材Category表示烹饪方式或菜系分类User表示用户。关系上Dish到Ingredient用INCLUDESDish到Category用BELONGS_TOUser与Dish之间保留HISTORYUser与Category或Ingredient之间放LIKES/DISLIKES。先建约束和索引保证后面MERGE幂等CREATE CONSTRAINT dish_name IF NOT EXISTS FOR (d:Dish) REQUIRE d.name IS UNIQUE; CREATE CONSTRAINT ingredient_name IF NOT EXISTS FOR (i:Ingredient) REQUIRE i.name IS UNIQUE; CREATE INDEX category_name_index IF NOT EXISTS FOR (c:Category) ON (c.name);约束在这里同时是唯一索引能让MERGE按名称定位节点避免重复导入产生“同菜多名”。下面这一段体现关键设计决策MERGE (d:Dish {name: 清蒸鲈鱼}) MERGE (i:Ingredient {name: 鲈鱼}) MERGE (c:Category {name: 蒸菜}) MERGE (u:User {id: u1001}) MERGE (d)-[:INCLUDES {amount: 600, unit: g, role: main}]-(i) MERGE (d)-[:BELONGS_TO]-(c) MERGE (u)-[:HISTORY {rating: 5, at: datetime()}]-(d);注意amount、unit、role放在INCLUDES关系上而不是Ingredient节点上。理由同一食材在不同菜里的用量和主辅角色不同放属性上才能直接过滤“主料必须包含鲈鱼”而不必去读食材的全局属性。另外User到Dish的HISTORY带着评分和时间戳评分供排序时间戳用来剔除近三个月重复推荐。2.3 Neo4j 导入 CSV 的三种姿势与字段清洗细节食材和菜品数据最常以CSV提供Neo4j导入CSV文件有两条主流路径交互式清洗用LOAD CSV首次建库的百万级导入用neo4j-admin import脚本管道里也可以交给APOC的apoc.load.csv。三者的边界如下方式适用量级事务性是否支持MERGE典型用途LOAD CSV几十万行以内每批自动提交支持日常补数据、带清洗的导入neo4j-admin import百万级离线全量不支持重建数据库apoc.load.csv中等可控制支持导入同时调用其他过程最常见的作业场景是LOAD CSV。注意CSV所有字段进入Cypher后都是字符串数值必须显式转换同时养成用trim清理空格的习惯LOAD CSV WITH HEADERS FROM file:///dishes.csv AS row WITH row WHERE trim(row.name) MERGE (d:Dish {name: trim(row.name)}) SET d.cooking_time toInteger(row.cooking_time), d.difficulty row.difficulty, d.rating toFloat(row.rating);这段导入逻辑里WHERE先过滤掉空行避免MERGE建出空节点toInteger和toFloat负责类型转换。若字段分隔符与菜名里的逗号冲突把CSV导出为管道符分隔然后在LOAD CSV后加FIELDTERMINATOR |。关于批量提交在Neo4j 5.x里LOAD CSV按事务自动分批不需要额外加USING PERIODIC COMMIT如果环境是4.x则在LOAD CSV之前写USING PERIODIC COMMIT 500并配合内存参数避免事务膨胀。3. 生成式 AI 参与推荐的链路LLM 解析用户意图图谱查询出事实再生成菜谱文案3.1 把自然语言解析成固定 JSONPrompt 白名单与 temperature 参数生成式AI在这套系统里最稳的位置是“语义解析器”和“文案生成器”而不是事实来源。第一步把用户自由文本转成结构化约束。常见做法是调用兼容OpenAI协议的接口本地可以接ollama这类服务线上则填对应的API地址关键是Prompt要定义字段白名单和输出格式。我用temperature0和json_object响应格式保证相同输入不抖动import json from openai import OpenAI client OpenAI(base_urlhttp://localhost:11434/v1, api_keyollama) SYSTEM_PROMPT 你是一条菜谱查询解析器只输出JSON不要输出解释。 规则 1. 字段只允许 methods, avoid_ingredients, max_cooking_time, taste, servings 2. methods 取值只能是 蒸、煮、炒、烤、炖、拌、炸 3. 用户没提到的约束不要凭空添加 4. 食材名使用常见中文名 def parse_intent(text: str) - dict: resp client.chat.completions.create( modelqwen2.5:7b, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: text}, ], response_format{type: json_object}, temperature0, ) return json.loads(resp.choices[0].message.content)这里最容易被忽略的是第2条白名单把methods限制死大模型就不会生成图谱里根本不存在的分类名后面的Cypher查询才能稳定命中。response_format和temperature0是组合使用的只开一个都不够。首次运行前记得在VS Code里配好Python虚拟环境并执行pip install openai neo4j fastapi uvicorn依赖装齐再调试。3.2 编排代码先让 Neo4j 出事实再交给生成式 AI 措辞推荐引擎的编排顺序决定了系统可信度。我一般会先查图谱拿候选菜品再做文案生成反过来先让LLM生成食谱再接图谱校验会把幻觉扩散到最终展示层。参与各环节的职责分配如下环节输入输出事实责任LLM意图解析用户文本JSON约束语义转换Neo4j图谱查询JSON约束菜品候选与食材事实事实来源LLM文案生成候选列表与食材事实完整菜谱文案措辞润色核心代码如下from neo4j import GraphDatabase class RecipeEngine: def __init__(self, uri, user, password, llm_client): self.driver GraphDatabase.driver(uri, auth(user, password)) self.llm llm_client def recommend(self, text: str, user_id: str): intent parse_intent(text) with self.driver.session() as session: dishes session.execute_read(self._query_dishes, intent, user_id) return self._render(dishes, intent) staticmethod def _query_dishes(tx, intent, user_id): cypher MATCH (d:Dish)-[:BELONGS_TO]-(c:Category) WHERE c.name IN $methods AND (d.cooking_time IS NULL OR d.cooking_time $max_time) AND NOT EXISTS { MATCH (d)-[:INCLUDES]-(i:Ingredient) WHERE i.name IN $avoid } RETURN d.name AS name, d.cooking_time AS cooking_time, [(d)-[:INCLUDES]-(i) | i.name] AS ingredients ORDER BY d.rating DESC LIMIT 10 result tx.run( cypher, methodsintent.get(methods, []), max_timeintent.get(max_cooking_time, 120), avoidintent.get(avoid_ingredients, []), ) return [record.data() for record in result]这段代码有四个值得照抄的点。一是查询用execute_read而不是execute_writeNeo4j Python Driver 5.x对读事务压力小也便于连接池复用。二是NOT EXISTS子查询直接从候选里剔除含忌口食材的菜反向约束一次完成。三是列表推导式[(d)-[:INCLUDES]-(i) | i.name]一次性取食材名避免二次查询。四是ORDER BY用图谱里的rating字段而不是让LLM拍脑袋排序。3.3 幻觉控制食材集合必须是图谱事实的子集生成式AI写“做法步骤”时最容易凭空加食材或编造用量。如果让LLM自由发挥校验环节就要兜底。我通常把生成结果和图谱已查到的食材事实做子集比对再检查步骤数量和营养数值区间def validate_recipe(llm_recipe: dict, graph_facts: dict) - bool: if ingredients not in llm_recipe or steps not in llm_recipe: return False gen_set {item[name] for item in llm_recipe[ingredients]} graph_set set(graph_facts[ingredients]) if not gen_set.issubset(graph_set): missing gen_set - graph_set raise ValueError(f生成内容包含图谱外食材: {missing}) if not (3 len(llm_recipe[steps]) 8): raise ValueError(f步骤数异常: {len(llm_recipe[steps])}) return True这段校验的优点是纯集合运算加长度判断毫秒级完成。原则就一条图谱负责事实生成式AI负责润色生成结果永远不能超出图谱已知范围。这样即使LLM输出一次跑偏最多是文案难看不会出现推荐菜里混入过敏原食材的硬伤。4. 把推荐引擎调得能扛演示连接池参数、Cypher 索引和冷启动兜底策略4.1 项目结构先对齐Python 工程里图谱与LLM各自分层一个能交付的智能食谱推荐系统工程目录不应该只有一个脚本。把图谱访问、LLM调用、推荐编排、接口四层分开调试时能单独替换任意一层而不会互相牵连recipe-ai/ ├── app/ │ ├── main.py # FastAPI 入口 │ ├── graph/driver.py # Neo4j 驱动初始化 │ ├── graph/cypher.py # Cypher 查询语句集中管理 │ ├── llm/parser.py # LLM 意图解析 │ ├── llm/rendering.py # 菜谱文案生成 │ └── services/recommender.py ├── data/*.csv └── tests/在graph/driver.py里驱动初始化不要写在每次请求里模块级创建单例即可from neo4j import GraphDatabase _driver None def get_driver(uribolt://localhost:7687, userneo4j, passwordNone): global _driver if _driver is None: _driver GraphDatabase.driver( uri, auth(user, password), max_connection_pool_size50, connection_acquisition_timeout60, ) return _driver4.2 3 个必调参数连接池、事务超时与索引命中演示现场最怕两种状况接口卡死、内存不足。所以参数设置要提前做。连接池本身不影响单查询速度但它决定并发度事务超时防止某条坏Cypher把整个进程拖住索引则决定查询从全表扫描变成索引查找。三个必调参数如下参数建议值配置位置说明max_connection_pool_size50Python驱动并发峰值决定本地演示50足够db.transaction.timeout10sneo4j.conf超过10秒的事务被终止约束/索引Dish.name, Ingredient.nameCypher DDLMERGE和等值查询命中唯一索引参数调完还要用EXPLAIN确认执行计划看是否走了索引EXPLAIN MATCH (d:Dish)-[:BELONGS_TO]-(c:Category) WHERE c.name 蒸菜 RETURN d.name LIMIT 20;看到计划里出现Index seeks而不是NodeByLabelScan才算真正命中索引。如果查询仍慢优先检查WHERE条件里的属性是否都建了索引以及LIMIT是否被放在ORDER BY之前。常见误区是只给Dish.name建索引却用Category.name过滤结果全表扫了Category节点。另一个常见误区是急着在Neo4j上跑图神经网络演示场景的数据量撑不起训练效果反而把工程复杂度拉高图谱在这里是存储与查询引擎不是模型训练场。4.3 冷启动没行为数据时用知识图谱和 LLM 兜底用户第一次进系统HISTORY关系为空协同过滤直接失效知识图谱的兜底逻辑是把意图里的食材做种子先生成候选再个性化。混合策略一般是这样def hybrid_recommend(user_id: str, text: str): if user_has_history(user_id): return recommend_by_history(user_id, text) intents parse_intent(text) if intents.get(avoid_ingredients): return recommend_with_dietary_filter(text, intents) return recommend_by_popularity(intents.get(methods, []))这里的分支逻辑排在意图解析之后因为生成式AI已经把“少油”“清淡”这类语义转成了methods和max_cooking_time冷启动查询就有了精确入参。注意别在用户没历史时硬套协同过滤否则返回的是空列表演示效果会很难看。5. 排错与数据一致性Neo4j 连接问题、只显示 25 个标签和知识图谱脏数据5.1 连接测试驱动版本、认证和超时排查Neo4j安装与配置完后最常见的任务是确认应用能连通。先在Python里做一次最小连通性测试from neo4j import GraphDatabase driver GraphDatabase.driver( bolt://localhost:7687, auth(neo4j, password), ) try: driver.verify_connectivity() print(connected) finally: driver.close()verify_connectivity会主动发起握手。失败的常规原因不多按下面的表逐项查报错形式常见原因先查什么ServiceUnavailableNeo4j没启动或Bolt端口写错浏览器访问7474是否正常AuthenticationError密码与数据库账号不一致NEO4J_AUTH或auth传参DatabaseNotFounddatabase名不对驱动里database参数是否写了库名注意驱动版本要和Neo4j服务端匹配Neo4j 5.x配neo4j Python Driver 5.x不能用4.x的驱动连5.x的库。端口这里也容易混7474是HTTP管理端口驱动走7687的Bolt协议。5.2 知识图谱只显示 25 个标签是数据没入库还是浏览器限制很多人在Neo4j Browser里看到左侧只出现25个标签就以为建图失败。这不是数据丢失是Browser的可视化上限默认只展示前25个标签和相应关系类型更多标签被折叠。想验证真实数量用Cypher直接计数MATCH (n) RETURN labels(n) AS label, count(*) AS count ORDER BY count DESC;这条查询会列出全部标签及节点数和可视化面板无关。如果需要调整Browser的显示在设置里修改Graph Visualization的标签数量上限但更建议把Browser当调试工具业务展示用Web端Neo4j JavaScript Driver按需取子图这样不再受“只显示25个标签”的限制。这个坑对经验者也常见因为标签数量超过25大概率是节点类型被拆碎回头检查建模是否合理。5.3 脏数据校验孤立节点、重复菜名与单位规范图谱导入完毕先跑三组校验再进推荐链路。孤立菜品没有关联食材会当选入候选后展示空食材列表MATCH (d:Dish) WHERE NOT (d)-[:INCLUDES]-(:Ingredient) RETURN d.name AS empty_dish LIMIT 20;重复菜名在约束建好前可能已经混入Python侧快速查重from collections import Counter names [r[name] for r in run_query(MATCH (d:Dish) RETURN d.name AS name)] duplicates [name for name, n in Counter(names).items() if n 1]单位不统一比菜名重复更隐蔽同一食材有的存g有的存克有的存毫升。导入阶段就要做映射把“克/公克/g”都归一为g否则LLM生成文案里的用量会和图谱事实冲突触发上一章的校验失败。单位映射表建议和CSV放在同一个data目录清洗脚本和导入脚本分开方便评委查看数据血缘。6. 把“高分项目”的演示讲到评委心里三条链路、Docker 部署和两个扩展方向6.1 演示链路先排好再开 Neo4j Browser毕业设计答辩时演示顺序比代码更影响观感。我会固定排三条链路第一输入“想吃蒸菜、不要花生、30分钟内”展示LLM解析出的JSON再展示图谱返回的候选最后展示生成的完整菜谱这条链路覆盖标题里三个关键词。第二点一道推荐菜切换到图谱子图视角把“为什么推荐”解释成一条关系路径这一步是知识图谱项目独有的加分项。第三换一个没有历史记录的新用户ID重复第一次输入展示冷启动兜底逻辑。正式演示前把候选结果先跑一遍并缓存到接口层避免现场等LLM推理耗时Neo4j Browser则提前执行一次预热查询后续点击响应会明显变快。6.2 Docker Compose 一份配置同时启动 Neo4j 和 API本地环境最好一次拉起而不是让评委看安装过程。用Compose把Neo4j和FastAPI绑在同一套配置里services: neo4j: image: neo4j:5-community ports: - 7474:7474 - 7687:7687 environment: NEO4J_AUTH: neo4j/password NEO4J_PLUGINS: [apoc] volumes: - ./neo4j-data:/data api: build: . depends_on: - neo4j ports: - 8000:8000NEO4J_AUTH设置初始密码NEO4J_PLUGINS让社区版启用APOCAPI服务的depends_on只保证容器启动顺序还要在启动脚本里对7687端口做健康检查轮询防止Neo4j未就绪时API反复重连。6.3 扩展点过敏原约束和食材替代答辩被问“还能做什么”时不要说空话直接讲可落地的扩展。一是把“用户对某食材过敏”建模成(User)-[:ALLERGIC_TO]-(Ingredient)在_query_dishes的NOT EXISTS里追加一个条件剔除任何包含过敏食材的菜比在应用层过滤更早挡住风险二是食材替代推荐用同一Category或营养属性近似的Ingredient做替换候选查询写成MATCH (i:Ingredient)-[:SUBSTITUTES]-(alt)即可。这两个扩展都建立在已有的INCLUDES、BELONGS_TO结构上新增的ALLERGIC_TO和SUBSTITUTES两类关系各用一条MERGE查询端只是给NOT EXISTS子句追加一行图谱的语义边界却在演示中清楚展示出来了。本文还有配套的精品资源点击获取