简介:这份资源是面向高校学生、教师及科研人员的高分期末大作业级美食问答系统源码,基于Python与知识图谱技术实现,适合作为课程设计、毕业设计或自学练手项目,也支持在此基础上二次开发。压缩包共18个文件,约162KB,包含6个py核心脚本、7个txt词表数据、2个json图谱数据,以及md说明、ico图标和jpg配图,覆盖问答解析、图谱构建与界面展示等模块。项目围绕美食领域展开,内置菜品、食材、口味、菜系、餐厅、厨师等多类实体词表,通过问题分类、问句解析与答案检索完成智能问答,并配有可视化界面。目前已有163人学习下载,可作为知识图谱入门与问答系统实战的参考案例。读者可从中获取完整可运行源码、图谱构建思路、问答流程拆解及数据组织方式,便于快速理解项目结构并迁移到其他垂直领域。
1. 美食问答系统:从一份期末大作业源码看知识图谱怎么落地
拿到「美食问答系统-基于python知识图谱的美食问答系统源码」这个题目时,很多人第一反应是把它当成一份交完就删的课程作业。但如果你正在做毕业设计、准备面试项目,或者想找一个能跑通的知识图谱入门案例,这套东西的价值远不止「高分」两个字。它本质上是一个垂直领域的 KBQA(Knowledge Base Question Answering)最小闭环:把菜系、食材、口味、烹饪方式这些美食实体抽出来,建成图数据库,再让用户用自然语言提问,系统解析意图后去图里查答案。适合谁?适合已经会写 Python、但没真正搭过知识图谱的开发者,也适合想用「python + 知识图谱 + 问答」三个关键词撑起简历项目的同学。下面我按自己复现这类系统的顺序,把选型、构建、问答、避坑一条线讲清楚。
2. 知识图谱美食问答系统的技术选型:为什么是 Neo4j 加 jieba 而不是纯 MySQL
2.1 关系型数据库做美食问答的三个硬伤
用 MySQL 存「宫保鸡丁属于川菜、用鸡胸肉和花生、口味微辣」这类信息,表结构会迅速膨胀。一道菜对应多个食材,一个食材又出现在多道菜里,这是典型的多对多关系,靠中间表 JOIN 能查,但一旦用户问「川菜里用到花椒的菜有哪些」,SQL 就要嵌套三层以上,可读性和扩展性都很差。更麻烦的是推理:用户问「和麻婆豆腐口味相似的菜」,关系型数据库没有天然的路径查询能力,你得自己写递归逻辑。知识图谱的核心优势就在这里——实体是节点,关系是边,多跳查询就是图遍历,天然贴合美食领域「菜—食材—菜系—口味」这种网状结构。
2.2 Neo4j 与 Python 驱动的组合逻辑
选 Neo4j 不是因为它名气大,而是它的 Cypher 查询语言对「路径」的表达最直观。Python 侧用官方驱动neo4j连接,配合jieba做中文分词、py2neo或原生 driver 执行查询。整个链路的逻辑是:用户输入问题 → jieba 分词并匹配实体词典 → 识别问句模板(比如「X的食材是什么」)→ 转成 Cypher → 查图 → 组装自然语言答案。这套组合的好处是每一层都能单独替换,比如分词换成 HanLP、图库换成 Nebula,不影响整体架构。
2.3 环境搭建的最小命令集
先确认 Python 版本,建议 3.8 以上,然后装依赖。Neo4j 用 Desktop 版或社区版都行,本地起一个实例,默认端口 7687。
# 创建虚拟环境,避免污染全局 python -m venv foodkg source foodkg/bin/activate # Windows 用 foodkg\Scripts\activate # 安装核心依赖 pip install neo4j jieba pandas装完后验证驱动能连上:
from neo4j import GraphDatabase # 本地 Neo4j 默认地址,密码是你首次启动时设的 driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "你的密码")) def test_conn(tx): result = tx.run("RETURN 'connected' AS msg") return result.single()["msg"] with driver.session() as session: print(session.execute_read(test_conn)) # 输出 connected 说明通了参数说明:bolt://是 Neo4j 的二进制协议,比 HTTP 快;auth第一个参数是用户名,社区版固定 neo4j,第二个是你自己设的密码,忘了就得改配置重启。这一步翻车的常见原因是端口被占或密码没改,报AuthError就去 Neo4j Browser 里重置。
3. 美食知识图谱构建:从 CSV 到 Neo4j 的实体关系入库
3.1 实体与关系的 schema 设计
动手写代码前先把 schema 定死,不然后面改起来是血泪经验。美食领域我一般抽五类节点:Dish(菜品)、Ingredient(食材)、Cuisine(菜系)、Flavor(口味)、CookingMethod(烹饪方式)。关系对应:Dish-[:BELONGS_TO]->Cuisine、Dish-[:CONTAINS]->Ingredient、Dish-[:HAS_FLAVOR]->Flavor、Dish-[:COOKED_BY]->CookingMethod。节点属性至少留一个name,菜品再加description。这个 schema 决定了后面能回答哪些问题,比如没有CookingMethod节点,用户问「清蒸的菜有哪些」就答不了。
3.2 用 pandas 读数据并批量写入
假设你有一份dishes.csv,字段是菜品、菜系、食材(分号分隔)、口味、烹饪方式。批量写入比逐条 CREATE 快一个数量级。
import pandas as pd from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "你的密码")) df = pd.read_csv("dishes.csv") def create_dish(tx, name, cuisine, ingredients, flavor, method): # MERGE 保证节点不重复,SET 更新属性 tx.run(""" MERGE (d:Dish {name: $name}) SET d.description = $name + '是一道美食' MERGE (c:Cuisine {name: $cuisine}) MERGE (d)-[:BELONGS_TO]->(c) MERGE (f:Flavor {name: $flavor}) MERGE (d)-[:HAS_FLAVOR]->(f) MERGE (m:CookingMethod {name: $method}) MERGE (d)-[:COOKED_BY]->(m) WITH d UNWIND $ingredients AS ing_name MERGE (i:Ingredient {name: ing_name}) MERGE (d)-[:CONTAINS]->(i) """, name=name, cuisine=cuisine, ingredients=ingredients.split(";"), flavor=flavor, method=method) with driver.session() as session: for _, row in df.iterrows(): session.execute_write(create_dish, row["菜品"], row["菜系"], row["食材"], row["口味"], row["烹饪方式"])逻辑说明:MERGE是「有则匹配、无则创建」,避免同一食材被建成多个节点;UNWIND把食材列表展开成多行,一次性建好所有CONTAINS关系。参数上,ingredients.split(";")要求 CSV 里食材用分号隔开,如果你用逗号,pandas 读进来会串列,这是新手最常踩的坑。写完跑一句统计验证:
with driver.session() as session: count = session.run("MATCH (d:Dish) RETURN count(d) AS c").single()["c"] print(f"共导入 {count} 道菜")3.3 建索引让查询不卡
数据量上千后,不建索引的MATCH (d:Dish {name: '宫保鸡丁'})会全图扫描。给常用查询字段加索引:
with driver.session() as session: session.run("CREATE INDEX dish_name IF NOT EXISTS FOR (d:Dish) ON (d.name)") session.run("CREATE INDEX ing_name IF NOT EXISTS FOR (i:Ingredient) ON (i.name)")IF NOT EXISTS保证重复执行不报错。索引建在name上是因为问答时实体匹配几乎都靠名字。注意 Neo4j 5.x 的索引语法和 4.x 不同,4.x 用CREATE INDEX ON :Dish(name),版本不对会直接语法错误,先CALL dbms.components()看版本。
4. 问答模块实现:jieba 分词加模板匹配怎么把问句变成 Cypher
4.1 问句意图识别的两条路线
一条是模板匹配,一条是模型分类。期末作业级别、数据量几百条的场景,模板匹配足够且可控。做法是先把用户问题用 jieba 切词,再拿词去实体词典里比对,命中菜品名就锁定查询主体,命中「食材」「做法」「口味」这类关键词就锁定查询意图。比如「宫保鸡丁用什么做的」切出「宫保鸡丁」「用」「什么」「做」,匹配到菜品实体和「食材」意图,就生成查CONTAINS关系的 Cypher。模型路线要标注数据、训练分类器,投入产出比在这个规模下不划算,除非你要冲更高级的课设。
4.2 实体词典与意图模板的代码实现
import jieba from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "你的密码")) # 从图库拉出所有菜品名,构建实体词典 with driver.session() as session: dishes = [r["name"] for r in session.run("MATCH (d:Dish) RETURN d.name AS name")] for d in dishes: jieba.add_word(d) # 把菜品名加进分词词典,避免被切碎 # 意图模板:关键词 -> Cypher 模板 INTENT_MAP = { "食材": "MATCH (d:Dish {name: $name})-[:CONTAINS]->(i:Ingredient) RETURN i.name AS ans", "口味": "MATCH (d:Dish {name: $name})-[:HAS_FLAVOR]->(f:Flavor) RETURN f.name AS ans", "菜系": "MATCH (d:Dish {name: $name})-[:BELONGS_TO]->(c:Cuisine) RETURN c.name AS ans", "做法": "MATCH (d:Dish {name: $name})-[:COOKED_BY]->(m:CookingMethod) RETURN m.name AS ans", } def answer(question): words = list(jieba.cut(question)) dish = next((w for w in words if w in dishes), None) intent = next((k for k in INTENT_MAP if k in question), None) if not dish or not intent: return "没听懂,换个问法试试" with driver.session() as session: records = session.run(INTENT_MAP[intent], name=dish) vals = [r["ans"] for r in records] return f"{dish}的{intent}是:" + "、".join(vals) if vals else "图里没这条数据" print(answer("宫保鸡丁的食材是什么"))逻辑说明:jieba.add_word是关键一步,不加的话「宫保鸡丁」可能被切成「宫保」「鸡丁」,实体就匹配不上。INTENT_MAP用字典把意图和 Cypher 绑定,扩展新意图只需加一行。参数$name是 Neo4j 的参数化查询,防止注入,别用字符串拼接。返回结果用、连接,因为一道菜的食材是多个值。
4.3 多跳问题的处理边界
模板匹配能覆盖单跳查询,但「川菜里用到花椒的菜有哪些」这种要先查菜系再筛食材的两跳问题,模板就得单独写。常见做法是给这类问题建复合模板,Cypher 写成MATCH (c:Cuisine {name:$cuisine})<-[:BELONGS_TO]-(d:Dish)-[:CONTAINS]->(i:Ingredient {name:$ing}) RETURN d.name。边界在于:模板越多维护成本越高,超过二十个模板就该考虑上意图分类模型了。这个判断线是我做了几个类似项目后总结的,不是拍脑袋。
5. 避坑与排查:美食问答系统复现时最容易翻车的五件事
5.1 中文分词把菜名切碎导致实体匹配失败
现象:问「麻婆豆腐什么口味」,系统答「没听懂」。原因:jieba 默认词典没有「麻婆豆腐」,切成「麻婆」「豆腐」,实体词典里两个词都匹配不上。解决:启动时把所有菜品名jieba.add_word加进自定义词典,或者用jieba.load_userdict("dishes.txt")批量加载,每行一个菜名。
5.2 Neo4j 密码或端口不对报 AuthError
现象:一运行就抛neo4j.exceptions.AuthError。原因:要么密码不是默认的 neo4j,要么 7687 端口被别的进程占了。解决:先去 Neo4j Browser(7474 端口)确认能登录,改过密码就用新密码;端口冲突就改neo4j.conf里的dbms.connector.bolt.listen_address,同时改 Python 里的连接串。
5.3 MERGE 写成 CREATE 导致节点重复
现象:查「鸡肉」相关菜品,结果里同一道菜出现好几次。原因:写入时用了CREATE而不是MERGE,每次导入都新建节点,图里全是重复。解决:所有节点创建统一用MERGE,写完跑MATCH (i:Ingredient {name:'鸡肉'}) RETURN count(i)检查,大于 1 就是重复了,清库重导。
5.4 CSV 编码问题让 pandas 读出乱码
现象:导入后菜品名全是问号或方块。原因:CSV 存成了 GBK,pandas 默认按 UTF-8 读。解决:pd.read_csv("dishes.csv", encoding="gbk"),或者用编辑器把文件转成 UTF-8。这个坑在 Windows 上尤其常见,Excel 另存为 CSV 默认就是 GBK。
5.5 意图关键词和实体名撞车
现象:问「川菜的口味」,系统把「川菜」当菜品去查HAS_FLAVOR,返回空。原因:「川菜」是菜系实体,但模板里「口味」意图只认菜品。解决:实体匹配时区分节点类型,先判断命中的词属于 Dish 还是 Cuisine,再选对应模板。简单做法是维护两个词典,匹配时按优先级判断。
6. 把问答准确率提上去:三个我实际用过的调优技巧
第一个技巧是给实体匹配加同义词表。用户不会总说标准菜名,「宫保鸡丁」可能被说成「宫爆鸡丁」,「西红柿」和「番茄」混用。做法是建一个synonym.csv,两列「别名、标准名」,启动时读进来,匹配前先做一次替换。这一步能把实体命中率拉高不少,成本却很低。
synonym = dict(pd.read_csv("synonym.csv").values) def normalize(word): return synonym.get(word, word)第二个技巧是给答案加置信度提示。模板匹配难免误判,与其硬答,不如在结果为空或匹配到多个实体时返回「你是想问 A 还是 B」。实现上就是在实体匹配阶段收集所有候选,超过一个就反问。这比强行给个错答案体验好得多。
第三个技巧是定期用真实问句回归测试。我会攒一个test_questions.txt,每行一个问题加期望答案,改完代码跑一遍对比。下面这个脚本能直接算准确率:
correct = 0 with open("test_questions.txt", encoding="utf-8") as f: for line in f: q, expected = line.strip().split("||") if expected in answer(q): correct += 1 print(f"准确率:{correct} / 总条数")这套回归测试是我踩过「改一处崩三处」的坑之后养成的习惯,没有它,每次调模板都像开盲盒。知识图谱问答系统看着简单,真正决定好不好用的就是这些边角功夫。希望帮到你。
本文还有配套的精品资源,点击获取