Neo4j知识图谱与生成式AI融合的推荐系统实战:从Cypher查询到大模型重排

发布时间:2026/9/12 17:32:40
Neo4j知识图谱与生成式AI融合的推荐系统实战:从Cypher查询到大模型重排 简介一套基于 Python、知识图谱Neo4j与生成式 AI 的智能食谱推荐系统毕业设计源码适合正在准备毕业设计、期末大作业或课程设计的开发者学习使用。项目以 Python 脚本为程序入口前端由 tsx、less、css 文件构成交互界面后端配置与依赖通过 json、yaml 等文件管理附带的说明文档与 sh 部署脚本可帮助快速安装、运行和发布。压缩包内共 52 个文件整体大小约 686KB体量适中便于本地调试和阅读。源码已通过本地编译评审分达 98 分难度适中且经助教老师审定内容质量有保障从内容预览可见项目覆盖健康记录、今日分析、食谱管理、我的食谱等模块并附赠额外资源。目前已有 89 人学习下载。通过分析和运行这套源码可以深入理解知识图谱如何存储食材与菜谱的关系、生成式 AI 如何根据用户行为推荐个性化菜谱对提升 Python 开发与智能推荐系统落地能力很有帮助。1. 为什么一个毕业设计敢同时叠Python、知识图谱和生成式AI推荐系统这个方向大多数学生选题时第一反应是协同过滤或者深度学习排序靠用户–物品打分矩阵做预测。而这份源码走了一条不太一样的路用Neo4j把食材、菜品、健康标签建成知识图谱再让生成式AI在图谱召回的候选集上做重排和解释生成。换句话说它不是把生成式AI当作推荐主模型而是当成推荐链路中负责加分的那一层。这样设计的好处很实际——图谱负责可解释、可查询、可控制生成式AI负责多样性和个性化表达两者各管一段出问题也好定位。源码里能看到完整的前后端工程Python入口、TypeScript前端、umi配置、发布脚本一应俱全评审分能达到98分说明不是空壳Demo。适合三种人看正在选毕业设计题目的本科生、想快速跑通知识图谱LLM推荐链路的研究生以及后端想了解前端工程长什么样的开发。接下来我按技术选型、图谱构建、生成层实现、前后端联调这条线逐层拆。2. 技术选型剖析Neo4j存关系、生成式AI做重排中间夹着Python2.1 三层架构入口、图谱、生成层的分工拆开这份源码的目录结构能明显看到三个层次。最上层是main.py作为后端入口负责HTTP接口和业务编排中间层是Neo4j图数据库存储菜品、食材、营养标签之间的语义关系最底层或者说最外围的是生成式AI服务负责把图谱的输出加工成用户容易理解的推荐结果和解释文本。前端部分由umi框架支撑pages目录下能看到recipes、healthRecord、todayAnalysis、myfood这些页面对应食谱浏览、健康档案、今日分析、我的食物等模块。NavBottom.tsx是底部导航组件说明这是一个偏向移动端H5形态的应用。三层之间的调用关系是这样的前端发起推荐请求到main.py后者通过Cypher查询Neo4j拿到候选集再把候选集拼进提示词模板请求生成式AI模型最终把结果返回前端展示。整个链路里Python是粘合层Neo4j是数据底座生成式AI是增强层。这样分工的好处是图谱查询结果稳定可复现生成式AI只负责锦上添花即便模型输出偶尔不稳定也不会导致推荐主流程崩溃。2.2 为什么是Neo4j而不是MySQL菜谱场景的查询模式差异菜谱推荐有一个很典型的查询需求我冰箱里有鸡蛋、番茄、豆腐能做什么菜如果用MySQL你得先查食材表再join菜品食材关联表最后按匹配度排序。这个逻辑在数据量小的时候没问题但一旦出现哪些菜和糖醋排骨用的调料高度重叠这类多跳关系查询SQL写起来会非常痛苦。Neo4j把这种查询变成了纯粹的图遍历。菜品、食材、营养标签各自是节点它们之间的关系是边CREATE (d:Dish {name: 番茄炒蛋, cook_time: 15, difficulty: 简单}), (i1:Ingredient {name: 番茄}), (i2:Ingredient {name: 鸡蛋}), (i3:Ingredient {name: 盐}), (d)-[:HAS_INGREDIENT {weight_g: 200}]-(i1), (d)-[:HAS_INGREDIENT {weight_g: 150}]-(i2), (d)-[:HAS_INGREDIENT {weight_g: 5}]-(i3)这里cook_time表示烹饪时长difficulty表示难度等级weight_g是配料用量字段设计直接对应推荐时的过滤条件。Cypher语句的意图很直白创建菜品节点、食材节点然后建立关系。相比SQL中多表join这条路在多跳关系查询和按路径过滤两种场景下优势明显——图数据库天然支持变长路径比如想查和番茄炒蛋共用两种以上食材的菜一个MATCH就能解决。2.2.1 关系型数据在图里的表达方式关系型数据库建模要先设计表结构字段定死之后加列很麻烦。图数据库天然是schema-less的菜品节点可以随时加season、spicy_level这类属性不需要改表结构。在这个项目中健康档案healthRecord需要根据用户的血糖、过敏原等信息做过滤新增属性不会影响已有查询逻辑这在迭代频繁的毕设场景里非常省事。2.3 生成式AI在推荐链路里的真实位置很多人以为生成式AI推荐就是用ChatGPT直接给菜谱其实这是误区。直接让大模型生成菜谱存在两个问题一是幻觉模型会编造不存在的食材搭配二是不可控同一个提示词每次输出都可能不一样。这份源码的做法是先用知识图谱锁定合法的候选菜品再让生成式AI在候选集内做事情。生成式AI在链路里承担三个具体任务第一根据用户当下输入的自然语言做意图解析比如低脂高蛋白的晚餐第二对图谱召回的结果做重排调整推荐顺序第三生成推荐理由告诉用户为什么推荐这道菜。也就是说图谱负责划定疆域生成式AI负责在疆域内做精细耕作。这种架构在工程上是合理的。哪怕生成式AI接口超时或者返回空推荐结果依然可以从图谱里出最多是少一段解释文案。系统中todayAnalysis页面就是在做这件事——把用户当天的饮食记录送入图谱分析营养结构再由生成式AI给出解读。2.4 环境准备与目录映射拿到源码先别急着跑先确认环境。项目涉及两套运行时Python环境和Node.js环境。Python版本建议3.9或3.10依赖管理看requirements.txt前端需要pnpm用pnpm-lock.yaml锁版本。# 安装后端依赖 pip install -r requirements.txt # 安装前端依赖 pnpm install # 启动 Neo4j 数据库Docker 方式 docker run -d -p 7474:7474 -p 7687:7687 \ -e NEO4J_AUTHneo4j/your_password \ neo4j:5-community启动Neo4j时注意Neo4j 5.x 的默认Bolt端口从7687开始浏览器管理界面在7474。密码要记牢.umirc.ts或后端配置里会用到。这一步完成后可以看说明.txt里提到的初始化脚本通常包含导入菜谱数据、创建索引的操作。Python基础语法不用额外补能看懂main.py里的路由和函数调用就够了真正的难点在Cypher和提示词工程上。3. 知识图谱构建与Cypher查询实战3.1 食材、菜品、健康标签的图谱模型知识图谱构建的第一步是定义本体。这份源码里的实体类型比一般Demo丰富Dish菜品、Ingredient食材、Nutrient营养成分、HealthTag健康标签、User用户和HealthRecord健康档案。关系类型则定义了菜品与食材的HAS_INGREDIENT、菜品与营养成分的CONTAINS_NUTRIENT、菜品与健康标签的HAS_TAG以及用户与菜品的ATE、FAVORITE等。这样的模型设计覆盖了推荐系统的核心查询场景按食材推荐菜品、按营养需求过滤菜品、按用户偏好排序。healthRecord模块之所以能工作靠的是User节点和HealthRecord节点之间的关联关系。比如用户有糖尿病史图谱上就能通过HAS_TAG过滤掉高糖菜品而不需要在业务代码里写一堆if-else判断。3.2 导入与构建从半结构化数据到Cypher数据来源通常是爬虫抓取的公开食谱网站或者是手工整理的Excel表。拿到数据后第一件事是清洗和去重。这一步要处理的问题很典型同一道菜在不同来源里叫番茄炒蛋和西红柿炒鸡蛋。我的做法是先建唯一性约束让重名数据在导入阶段就报错而不是事后排查。CREATE CONSTRAINT dish_name_unique IF NOT EXISTS FOR (d:Dish) REQUIRE d.name IS UNIQUE; CREATE CONSTRAINT ingredient_name_unique IF NOT EXISTS FOR (i:Ingredient) REQUIRE i.name IS UNIQUE;CREATE CONSTRAINT用于创建唯一性约束IF NOT EXISTS保证脚本可重复执行。这样即便重复导入数据Neo4j也会在写入时报错不会产生脏数据。配合MERGE语句可以实现存在则匹配、不存在则创建的幂等导入逻辑。LOAD CSV WITH HEADERS FROM file:///dishes.csv AS row MERGE (d:Dish {name: row.dish_name}) SET d.cuisine row.cuisine, d.cook_time toInteger(row.cook_time), d.difficulty row.difficulty WITH d, row MERGE (i:Ingredient {name: row.ingredient_name}) MERGE (d)-[:HAS_INGREDIENT {weight_g: toFloat(row.weight_g)}]-(i)LOAD CSV从Neo4j的import目录加载数据MERGE先尝试匹配已有节点再决定是否创建。toInteger和toFloat是类型转换函数因为CSV里所有字段默认都是字符串。这个脚本的边界情况要注意如果一道菜有十种食材CSV里就有十行数据WITH d, row就是为了让食材归属到正确的菜品上。3.3 推荐召回阶段的查询模板知识图谱推荐的核心价值体现在召回阶段。常规的按食材查菜用一条Cypher就能完成MATCH (u:User {id: $user_id})-[:HEALTH_RECORD]-(hr:HealthRecord) WITH hr MATCH (d:Dish)-[:HAS_INGREDIENT]-(i:Ingredient) WHERE i.name IN $ingredients AND NOT EXISTS { MATCH (d)-[:HAS_INGREDIENT]-(bad:Ingredient) WHERE bad.name IN hr.allergens } WITH d, count(DISTINCT i) AS matched_count ORDER BY matched_count DESC, d.cook_time ASC LIMIT 20 RETURN d.name, d.cook_time, matched_count这个查询分三段先取用户的健康档案再匹配包含目标食材的菜品然后过滤掉包含过敏原的菜品最后按匹配度排序并限制返回20条。$ingredients和$user_id是参数占位符在实际代码中通过驱动传参。这里比纯SQL强的地方在于NOT EXISTS子查询直接扫描的是图上的关系不需要反复join食材表。匹配度排序是一个值得做文章的细节。食材匹配数相同的情况下烹饪时间短的优先这符合用户下班后懒得做饭的真实场景。如果想要更精细的排序可以把matched_count改成加权分数比如主料权重1.0、辅料权重0.5。3.4 查询性能与浏览器显示限制排错Neo4j自带浏览器里有一个坑默认查询结果超过25个标签时会提示只显示25个标签让人误以为数据没导入完整。这个限制不是数据量问题而是浏览器渲染逻辑避免节点爆炸拖垮前端。排查方法很简单用RETURN count(d)汇总统计即可。另一个常见坑是Cypher执行计划退化。如果ORDER BY matched_count直接跑全表数据量过万后响应时间会飙升。解决办法是在导入阶段建立索引CREATE INDEX dish_cuisine_idx IF NOT EXISTS FOR (d:Dish) ON (d.cuisine);dish_cuisine_idx是针对cuisine属性的索引。Neo4j在Cypher执行时会自动选择索引前提是查询里用了WHERE d.cuisine $cuisine这类精确匹配。用CONTAINS做模糊匹配时索引会失效这也是为什么导入阶段就要做名称归一化把西红柿统一改成番茄。图谱构建这块做到位之后接下来就看生成式AI层怎么把召回结果变成用户读得懂的内容。4. 生成式AI推荐层从召回结果到一句话解释4.1 推荐管线召回-过滤-增强-重排拿到图谱的候选集后不能直接丢给大模型。推荐管线分四步召回、过滤、上下文增强、重排与生成。召回由Cypher完成过滤在Python里做上下文增强是把用户健康档案和候选菜品组装成结构化文本最后才是重排和生成。def build_prompt(user_profile: dict, candidates: list[dict]) - str: dish_text \n.join( f- {c[name]}{c[cook_time]}分钟难度{c[difficulty]} for c in candidates[:10] ) prompt f 你是一个健康的饮食规划助手。根据用户的健康档案和候选菜品推荐3道最适合的菜。 用户档案 - 目标{user_profile.get(goal, 均衡饮食)} - 过敏原{, .join(user_profile.get(allergens, []) or [无])} - 忌口{, .join(user_profile.get(avoid, []) or [无])} 候选菜品 {dish_text} 请按以下格式输出 1. 菜名 | 推荐理由 | 匹配点 .strip() return promptbuild_prompt()把用户档案和候选菜品拼进提示词。user_profile里的goal、allergens、avoid字段来自healthRecord页面录入的数据。这里的关键是只传候选菜品的前10个避免token超限。菜名后面附带的烹饪时间和难度信息让大模型在重排时有依据可循而不是纯靠训练语料里的印象。4.2 提示词模板与参数调节4.2.1 温度与top_p的控制逻辑调用生成式AI接口时temperature和top_p这两个参数直接影响输出质量。temperature控制随机性取值越低输出越确定top_p控制累积概率截断取值越低越保守。推荐解释的场景下temperature设置0.3到0.5比较合适——太低会显得机械太高会让推荐理由天马行空。import httpx async def generate_recommendations(prompt: str) - str: async with httpx.AsyncClient(timeout30) as client: resp await client.post( http://localhost:8000/v1/chat/completions, json{ model: qwen2.5:7b, messages: [{role: user, content: prompt}], temperature: 0.3, top_p: 0.85, max_tokens: 300, }, ) resp.raise_for_status() data resp.json() return data[choices][0][message][content]httpx.AsyncClient的timeout30表示请求上限30秒防止模型推理过慢拖垮接口。model字段是本地部署的模型名称来源于你在Ollama或vLLM里拉取的模型。temperature0.3让推荐理由偏向稳定top_p0.85保留一定多样性避免多次请求结果完全一致。max_tokens300限制生成长度中文推荐解释一般两三百字足够。4.3 Python调用生成接口的异步实现生成式AI接口如果同步调用用户在前端页面会一直转圈。异步化是最基本的工程要求。上面的示例函数用了async/await语法配合httpx和FastAPI的配合很顺畅。from fastapi import FastAPI from fastapi.concurrency import run_in_threadpool app FastAPI() app.get(/api/recommend) async def recommend(user_id: str, ingredients: str): candidates await query_graph(user_id, ingredients.split(,)) prompt build_prompt(user_profile, candidates) result await generate_recommendations(prompt) return {candidates: candidates, explanation: result}run_in_threadpool是把同步代码放进线程池运行避免阻塞事件循环。这个场景里query_graph走的是Neo4j驱动本身是同步的放在run_in_threadpool里执行不会卡住其他请求。candidates字段返回图谱原始结果explanation字段返回生成式AI的解释文案前端拿到之后可以同时渲染列表和推荐语。4.4 流式输出与前端SSE对接推荐理由如果等大模型全部生成完才返回体验会很差。用流式输出可以做到生成一个字显示一个字的效果。后端用SSEServer-Sent Events前端用EventSource接收。async def stream_recommendation(prompt: str): async with httpx.AsyncClient(timeout60) as client: async with client.stream( POST, http://localhost:8000/v1/chat/completions, json{model: qwen2.5:7b, messages: [{role: user, content: prompt}], stream: True}, ) as resp: async for line in resp.aiter_lines(): if line.startswith(data: ): yield line[6:]aiter_lines()按行迭代响应体yield逐条产出数据。FastAPI里把返回值声明成StreamingResponse即可将流透传给前端。前端的todayAnalysis和recipes页面配一个sse.js工具类处理断线重连和缓冲就能实现打字机效果。流式接入之后整个推荐链路的工程部分就完整了接下来看前端怎么接、发布脚本做了什么。5. 前端联动、构建发布与上线验证5.1 umi页面与状态管理前端目录里出现的.umirc.ts是umi框架的配置文件。umi在React生态里属于约定式路由pages目录下的文件名会自动映射成路由路径。recipes.tsx对应/recipes页面healthRecord.tsx对应/healthRecord页面不需要手动注册路由。状态管理这块项目没有引入重量级方案组件之间用props传值页面级状态通过umi的useModel或者React内置的useState管理。NavBottom.tsx作为底部导航组件用history.push做页面跳转配合selectedKeys实现Tab高亮。移动端H5场景下这种轻量方案比Redux好维护得多。5.2 接口代理与跨域配置本地开发时前端跑在8000端口后端FastAPI跑在9000端口跨域问题靠umi代理解决// .umirc.ts export default { proxy: { /api: { target: http://localhost:9000, changeOrigin: true, }, }, };/api前缀的请求全部转发到9000端口changeOrigin: true让服务端认为请求来自同一源。这个配置只对开发环境生效生产环境走后端直接部署或者Nginx反向代理。如果你在后端看到CORSMiddleware那是给直接访问后端接口的场景兜底的。5.3 构建发布publish.sh到底做了什么源码里带publish.sh和publish_oss.sh两个脚本前者是通用发布后者是发布到对象存储OSS。打开publish.sh核心流程就三步前端构建、后端打包、产物上传。#!/bin/bash source ~/.bashrc set -e pnpm build tar -czf dist.tar.gz dist scp dist.tar.gz userserver:/opt/recipes/ ssh userserver cd /opt/recipes tar -xzf dist.tar.gz systemctl restart recipes.serviceset -e让脚本在任一步骤失败时立即退出避免带着半成品继续发布。pnpm build会生成dist目录打包上传后解压重启服务。publish_oss.sh的逻辑差别只在最后一步用ossutil cp命令把产物同步到OSS的静态网站Bucket。5.4 上线前的五步验证清单改动代码后按这个顺序自测基本能覆盖大部分问题验证项操作预期结果图谱连通性Neo4j Browser执行MATCH (n) RETURN count(n)返回数字与导入总量一致接口连通性curl http://localhost:9000/api/recommend?user_id1ingredients鸡蛋返回JSON结构与前端类型定义匹配生成服务健康curl http://localhost:8000/v1/models返回模型列表代理转发浏览器访问http://localhost:8000/api/recommend能看到后端响应头发布回滚保留上一版dist.tar.gz异常时立即恢复服务5分钟内恢复可用5.5 高频报错对照表报错场景可能原因快速定位前端页面白屏接口代理配置缺失或后端未启动打开浏览器Network面板看请求是否返回200Cypher语法报错Neo4j版本差异比如4.x和5.x的关系语法不同查看Neo4j官方文档对应版本替换CREATE为MERGE生成接口超时模型加载慢或max_tokens设置过大先请求/v1/models确认模型就绪再把max_tokens从500降到200推荐结果重复temperature过高且未做去重添加Set去重逻辑把temperature降到0.3最后提一个偏方如果生成式AI返回的解释和推荐菜品对不上检查提示词里候选菜品是否带了编号。大模型对推荐第3道菜这类指代容易混淆让它在输出里带上菜名全称准确率会明显提升。本文还有配套的精品资源点击获取