简介:基于知识图谱与生成式AI的智能食谱推荐系统,是一套Python毕业设计源码,面向计算机相关专业准备毕业设计的学生,也可用于课程设计、期末大作业和项目实战练习。项目经导师指导并通过评审,得分98分,源码均已本地编译运行并严格调试,难度适中。资源压缩包为zip格式,共42个文件,约681KB;文件类型以19个tsx组件、9个less样式和3个ts类型文件为主,另含Python入口、yaml配置与shell部署脚本,覆盖前端页面、样式定义、类型声明及基础后端逻辑。目前已有404人学习/下载。读者可借此理解知识图谱在食谱关联检索中的实际应用,并实践生成式AI生成菜品的流程;完整的前端工程目录、配置文件和部署脚本也有助于二次开发、排错和毕业设计答辩演示。
1. 先理解这条链路:知识图谱加生成式AI,救的是推荐的可解释性
深夜食堂场景,用户给出一条很具体的诉求:夜宵想吃辣但又怕胖。传统标签推荐系统大概率先按“辣”捞出火锅冒菜,再按“低卡”捞出蔬菜沙拉,最后给你一个说不清依据的拼接结果。标题里的这套方案思路不一样:先用知识图谱把菜谱、食材、口味、营养、场景做成实体和关系,回答“辣且低热量在图谱里有哪几道菜、证据路径是什么”,再由生成式AI基于这些结构化候选生成一段像人话的推荐理由和微调建议。这条链路说白了就是两段式推荐系统:知识图谱负责召回、约束和解释,生成式AI负责表达和补全。适合正在选题的毕业生、想快速搭一套演示原型的开发者,以及想给现有推荐系统加可解释性的人。
2. 食谱知识图谱设计与构建:五类实体、六类关系与入库脚本
知识图谱落到这个系统里,其实就是一张有向图:节点是菜、食材、营养素、口味、场景,边表示“包含”“属于”“适用”这类语义关系。我一般在毕业设计阶段建议把图先建在本地内存里,用 NetworkX 建模,导出成 JSON 供后端和前端共用。等数据量超过几万节点再迁移到正式图数据库,迁移时只要把边表原样导入就行,业务代码几乎不用动。
2.1 实体关系设计:菜谱、食材、营养、口味、人群
先定实体类型,我习惯用节点 ID 前缀区分类型,避免“土豆”既是食材又是菜名时撞到一起。
| 实体类型 | 节点ID前缀 | 说明 |
|---|---|---|
| 菜谱 | dish:: | 一道完整菜品,如宫保鸡丁 |
| 食材 | ingredient:: | 鸡胸肉、花生米、干辣椒 |
| 营养素 | nutrition:: | 蛋白质、热量、脂肪、碳水 |
| 口味 | taste:: | 辣、麻、清淡、甜口 |
| 场景人群 | scene:: | 减脂、夜宵、儿童、加班餐 |
关系用六类就够撑起整个推荐逻辑:菜谱包含食材、食材拥有营养素、菜谱有口味标签、菜谱属于菜系、菜谱适配场景、食材间可替换。下面这张关系表直接对应代码里的边类型。
| 头实体 | 关系 | 尾实体 | 含义 |
|---|---|---|---|
| 菜谱 | contains | 食材 | 菜谱用到某食材 |
| 食材 | has_nutrition | 营养素 | 食材的营养构成 |
| 菜谱 | has_taste | 口味 | 味道标签 |
| 菜谱 | belongs_to | 菜系 | 川菜、粤菜、家常菜 |
| 菜谱 | suitable_for | 场景 | 减脂、夜宵、儿童餐 |
| 食材 | substitute_of | 食材 | 可替换食材关系 |
为什么用图而不是一张标签表?因为“夜宵想吃辣但怕胖”这类问题本质是多跳查询:菜谱到口味一跳,菜谱到食材一跳,食材再到营养素一跳。图结构能把这个路径原样查出来,而标签表只能做等值匹配,解释不了中间经过的食材和营养约束。后面给生成式AI写推荐理由时,这条路径就是证据。
2.2 图存储选型:本地 NetworkX 还是图数据库服务
先给结论:数据量在几千到几万个节点时,我强烈建议用 NetworkX 加 JSON 文件,别一上来就上数据库服务。原因有三个。
第一,毕业设计演示环境经常只有一台普通笔记本,起一个图数据库服务对内存占用不小,还多一道安装排错过程。第二,NetworkX 的node_link_data能直接导出 JSON,前端可视化、后端召回逻辑都共用同一份数据,调试时肉眼可见。第三,等导师问“换到生产环境怎么扩展”,再借这个结构说明如何把边表导入正式图数据库,话术反而更好展开。
如果非要上服务,记得保留边表的 CSV 导出逻辑,这是迁移时唯一需要的东西。后续所有推荐算法都只依赖“节点-关系-属性”这套抽象,不依赖某个存储的具体 API。
2.3 从半结构化数据抽三元组:build_kg.py 脚本
常见做法是自己整理一份菜谱台账,字段涵盖菜名、食材、菜系、口味、营养标签、适用场景。下面这份 CSV 是模拟数据的格式约定:
dish_id,dish_name,ingredients,cuisine,taste,nutrition,suitable,calories D001,宫保鸡丁,鸡胸肉,花生米,干辣椒,川菜,辣,蛋白质,高热量,减脂,280 D002,白灼虾,基围虾,姜,蒜,粤菜,清淡,蛋白质,低热量,减脂,150对应的构建脚本如下,重点是把每一行变成多条三元组边。
# build_kg.py # 读取菜谱台账,抽取知识图谱三元组,导出 graph.json import csv import json import networkx as nx from collections import Counter G = nx.MultiDiGraph() def split_list(text: str) -> list: """把中文顿号、逗号统一按英文逗号切分。""" if not text: return [] return [x.strip() for x in text.replace("、", ",").split(",") if x.strip()] def add_node(etype: str, name: str) -> str: """带类型前缀的节点ID,重复节点只建一次。""" key = f"{etype}::{name.strip()}" if not G.has_node(key): G.add_node(key, etype=etype, name=name.strip()) return key def add_edge(head: str, rel: str, tail: str, **props) -> None: G.add_edge(head, tail, relation=rel, **props) def build(csv_path: str) -> None: with open(csv_path, encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: dish = add_node("dish", row["dish_name"]) # 1. 菜谱 -> contains -> 食材 for ing in split_list(row["ingredients"]): ing_node = add_node("ingredient", ing) add_edge(dish, "contains", ing_node, calories=row["calories"]) # 2. 食材 -> has_nutrition -> 营养素 for ntr in split_list(row["nutrition"]): ntr_node = add_node("nutrition", ntr) add_edge(ing_node, "has_nutrition", ntr_node) # 3. 菜谱 -> suitaable_for -> 场景 for scene in split_list(row["suitable"]): add_edge(dish, "suitable_for", add_node("scene", scene)) # 4. 菜谱 -> has_taste -> 口味 for taste in split_list(row["taste"]): add_edge(dish, "has_taste", add_node("taste", taste)) # 5. 菜谱 -> belongs_to -> 菜系 if row["cuisine"].strip(): add_edge(dish, "belongs_to", add_node("cuisine", row["cuisine"])) if __name__ == "__main__": build("recipes.csv") data = nx.node_link_data(G, edges="edges") with open("graph.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) print("nodes:", G.number_of_nodes(), "edges:", G.number_of_edges())代码里add_node先查再插,避免同一食材被反复建节点;第 13 行把食材和营养素串起来后,后面才能回答“哪些菜里有高蛋白食材”。split_list同时处理中文顿号和英文逗号,很多网上表格导出数据都是混着的,这一步能省不少清洗时间。calories先挂在“包含食材”边上,后面想做热量排序可以直接取边的属性。
导出时ensure_ascii=False是关键,否则中文全变成\uXXXX,前端渲染出乱码就麻烦了。node_link_data产出的结构自带nodes和links两部分,Flask 后端读取后可以直接转成networkx图对象,不用重新解析一遍。
2.4 图谱质量检查:重复实体与悬空节点
图建完不能直接拿去跑推荐,先做一轮质量检查。最容易翻车的两类问题:同名食材因为空格、全角符号被建了两个节点;以及某些节点没有任何边连接,召回时永远命不中。
# inspect_kg.py # 检查悬空节点和重复实体,输出问题清单 import json import networkx as nx from collections import Counter with open("graph.json", encoding="utf-8") as f: data = json.load(f) G = nx.node_link_graph(data, edges="edges") # 检查1:孤立节点 iso = [n for n, d in G.degree() if d == 0] print("isolated nodes:", len(iso), iso[:10]) # 检查2:同名同类型节点重复出现 name_counter = Counter() for n, attr in G.nodes(data=True): name_counter[(attr["etype"], attr["name"])] += 1 dups = {k: v for k, v in name_counter.items() if v > 1} print("duplicated keys:", len(dups), list(dups.items())[:10])悬空节点多半是某一行 CSV 数据不完整,只建了节点没建边。重复节点多半来自数据源写法不一致,比如“鸡胸肉”和“鸡胸肉 ”就差一个空格。真遇到类似问题,统一做一次归一化:去掉首尾空格、把全角逗号换半角、把繁体简单映射到简体。这一层清洗直接影响前端图谱可视化效果,花二十分钟处理,比后期排查推荐不准要划算得多。
3. 推荐主流程串起来:图谱召回后再让生成式AI补全和表达
图建好后,推荐主流程分四步:把用户输入转成标签权重、在图谱里召回候选菜、为候选菜抽取证据路径、让生成式AI基于证据生成推荐语。每一步之间用数据接口解耦,生成式AI挂了,前面的召回结果还能用模板兜底,不至于整个系统瘫掉。
3.1 用户意图先落成带权重的标签集合
用户不会说“taste 等于辣”,只会说“想吃辣但又怕胖”。常见做法是做一张关键词映射表,把口语映射成{口味: {辣: 2}, 场景: {夜宵: 1}, 营养: {低热量: 1}}这样的结构化对象。权重表示约束强度,数值越大在召回时越优先。
这一段不用做大模型意图识别,否则等于用大炮打蚊子。毕设阶段做二十个关键词的规则映射就够撑起演示效果。等后面有精力,再把这个规则函数替换成大模型函数,输入输出接口都保持一致,不影响到下游。
3.2 图谱子图召回:带权重的多标签匹配
召回脚本的核心是遍历所有菜谱节点,按用户标签给每道菜打分。下面这份代码按口味、场景、食材、营养四个维度算分,最后取 Top N 作为生成式AI的候选集。
# recall.py # 根据用户标签权重,从知识图谱中召回候选菜谱 def recall_by_profile(G, profile, top_n=20): scores = {} for n, attr in G.nodes(data=True): if attr.get("etype") != "dish": continue score = 0.0 # 口味匹配,权重最高 for taste, w in profile.get("taste", {}).items(): if G.has_edge(n, f"taste::{taste}"): score += 2.0 * w # 场景匹配,如夜宵、减脂 for scene, w in profile.get("scene", {}).items(): if G.has_edge(n, f"scene::{scene}"): score += 1.5 * w # 食材匹配 for ing, w in profile.get("ingredient", {}).items(): if G.has_edge(n, f"ingredient::{ing}"): score += 1.0 * w # 营养约束:遍历该菜的食材,检查食材是否挂到目标营养标签 for ntr, w in profile.get("nutrition", {}).items(): for ing in G.successors(n): if G.nodes[ing].get("etype") != "ingredient": continue if G.has_edge(ing, f"nutrition::{ntr}"): score += 0.8 * w break # 一个食材命中就算该菜命中营养约束 if score > 0: scores[n] = score ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [n for n, _ in ranked[:top_n]]has_edge直接按节点ID判断边是否存在,前提是标签节点ID和用户输入名称经过了同一种归一化。口味权重给到 2.0,因为口味是用户最主观的感受;食材权重 1.0,食材换掉还能勉强吃,口味不对直接推翻整道菜。营养约束用了break提前跳出,一道菜只要有一个食材命中高蛋白就算满足,不用所有食材都高蛋白。
top_n=20不是越大越好。候选太多会把提示词撑爆,大模型还要花更多时间取舍,推荐结果反而分散。我一般控制在 8 到 12 个,既能保证多样性,又能让生成式AI聚焦在真正匹配的菜谱上。
3.3 抽取证据路径:让“为什么”有迹可循
召回只给出菜名还不行,生成式AI要能解释“为什么推这道菜”。所以下一步要从图里抽出证据路径,比如“白灼虾 -> 基围虾 -> 蛋白质 -> 低热量”。
# evidence.py # 为一道候选菜抽取图谱证据路径,用于拼接提示词 def build_evidence(dish_key, G, max_items=5): evidence = [] dish_name = G.nodes[dish_key]["name"] # 食材 -> 营养 路径 for ing in G.successors(dish_key): if G.nodes[ing].get("etype") != "ingredient": continue ing_name = G.nodes[ing]["name"] for ntr in G.successors(ing): if G.nodes[ntr].get("etype") == "nutrition": evidence.append(f"{ing_name}->{G.nodes[ntr]['name']}") if len(evidence) >= max_items: break # 口味标签 for taste in G.successors(dish_key): if G.nodes[taste].get("etype") == "taste": evidence.append(f"口味:{G.nodes[taste]['name']}") return evidence证据路径的格式统一成食材->营养和口味:标签,文本简单直接,大模型很容易理解。这个函数要在拼接提示词之前调用,把结果和菜名一起塞给大模型,模型就只能沿着证据说话,而不是自己编营养知识。
3.4 拼装推荐回复:把图谱证据交给生成式AI
下面这段代码把候选菜、证据路径和用户需求拼成一个提示词,要求大模型输出 JSON 数组,每个推荐项包含菜名、理由和调整建议。
# recommend.py def make_recommendation(user_text, profile, G, llm_chat): candidates = recall_by_profile(G, profile, top_n=8) lines = [] for dish_key in candidates: dish_name = G.nodes[dish_key]["name"] evidence = build_evidence(dish_key, G) evidence_text = "; ".join(evidence) if evidence else "无额外证据" lines.append(f"- {dish_name} | 图谱证据: {evidence_text}") prompt = f"""你是食谱推荐助手。 用户需求:{user_text} 以下是从知识图谱召回的真实候选菜谱,只能从里面选,不得新增菜品,不得虚构食材。 候选清单: {chr(10).join(lines)} 请输出 JSON,格式严格如下: {{"recommendations": [{{"dish": "菜名", "reason": "50字以内推荐理由", "adjust": "一句微调建议"}}]}} """ reply = llm_chat(prompt) return parse_reply(reply)注意提示词里写的是“真实候选菜谱”“不得新增菜品”,这两句话是用来给大模型上紧箍咒的。生成式AI有很强的幻觉倾向,如果你不限制食材范围,它会自信地推荐“荔枝配香肠”。把图谱证据放在候选清单里,等于把模型的回答空间先切好,它只是在边界里做润色,而不是放飞。
到这里,图谱只是搜索引擎,大模型只是话术包装器,两者各司其职。千万别反过来让大模型直接生成菜,再拿图谱去验证,那会得到一堆查无此菜的推荐。
4. 生成式AI接入参数:让输出稳定、像样、不跑题
接入生成式AI本身不难,难的是一次返回就能用。毕业设计答辩现场心态本来就紧,如果模型返回一次卡住或吐出一串乱格式,整个人会慌。这一章分享一组稳定的参数组合和一套降级逻辑,让线上的黑匣子尽量听话。
4.1 用兼容协议封装一个最小请求函数
不管底层跑的是什么模型服务,大部分都提供 OpenAI 兼容接口。直接拿 requests 请求,不依赖某个 SDK,换服务商时只需要改 URL 和模型名。
# llm_client.py import requests import json def llm_chat(messages, temperature=0.3, max_tokens=800, timeout=30): url = "http://127.0.0.1:8000/v1/chat/completions" payload = { "model": "local-model", "messages": messages, "temperature": temperature, "top_p": 0.85, "max_tokens": max_tokens, "stream": False } resp = requests.post(url, json=payload, timeout=timeout) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]messages是一个列表,包含 system 和 user 两种角色。timeout 设成 30 秒是折中值,太短容易在推理慢时误杀请求,太长会让前端一直转圈。raise_for_status会铺开非 200 状态码,方便快速定位问题。
4.2 四个关键参数与调参经验
| 参数 | 范围 | 作用 | 本系统建议 |
|---|---|---|---|
| temperature | 0~2 | 控制随机性,越大越发散 | 0.3 |
| top_p | 0~1 | 核采样,控制候选词概率累加度 | 0.85 |
| max_tokens | 正整数 | 输出最大长度,超出会被截断 | 700~800 |
| frequency_penalty | -2~2 | 抑制重复用词 | 0.2 |
temperature 是第一个要调的参数。推荐理由需要稳定可复现,0.3 是经验起点;如果发现理由千篇一律,往 0.6 方向调;如果开始胡言乱语,往 0.2 方向压。top_p 和 temperature 可以同时调,但不要在同一轮里大改,一次只动一个参数,不然分不清是谁引起的。
max_tokens 设太大没有意义,推荐理由控制在 50 字以内,加 JSON 结构,总量 700 足够。截断后输出缺了右括号,解析必炸,所以宁可放宽到 800。frequency_penalty 给 0.2 是为了避免模型每句都以“这道菜”开头,但不要拉太高,太高会破坏语义连贯性。
4.3 提示词要拆成 system 和 user 两部分
把系统约束写进 system,用户问题写进 user,模型对边界的感知会更清晰。下面这段模板可以直接复用。
SYSTEM_PROMPT = "你是烹饪知识图谱问答助手,只允许使用知识图谱提供的菜品、食材、营养标签作答,禁止返回图谱以外的内容。" USER_TEMPLATE = ( "用户需求:{user_text}\n" "候选菜谱与证据:\n{evidence}\n" "请输出 JSON,格式:{{\"recommendations\": [{{\"dish\": \"菜名\", \"reason\": \"推荐理由\", \"adjust\": \"调整建议\"}}]}}" )这样拆的好处是,如果后面要换多轮对话场景,system 不用改,每次追加 user 消息即可。证据文本拼在最前面,模型会优先参考上下文里的实体,降低幻觉概率。注意 JSON 的字段名固定下来,解析脚本才能稳定取到dish和reason。
4.4 超时重试与降级:不能在答辩现场掉链子
生成式AI有时候会超时或返回纯文本错误,所以在调用层套一次重试,再不行就降级到模板推荐。模板推荐不依赖模型,只用图谱召回的 Top1 菜名拼接固定话术。
import time, random def robust_llm_chat(messages, retries=2, llm_chat=llm_chat): for attempt in range(retries): try: return llm_chat(messages, timeout=30) except (requests.Timeout, requests.ConnectionError): if attempt == retries - 1: return None time.sleep(1 + attempt) return None def fallback_reply(user_text, top_dish_name): return { "recommendations": [{ "dish": top_dish_name, "reason": "根据你的偏好标签,这道菜在图谱中的匹配度最高。", "adjust": "如果口味不够,可以增加同口味菜谱的候选范围。" }] }重试间隔用1 + attempt做退避,避免两台设备同时重打接口。降级回答虽然朴素,但能保证系统不黑屏。答辩现场最怕的不是回答不够精彩,而是整个服务无响应,宁可给一个保守答案,也不能让用户等一个超时页面。
5. 本地复现避坑指南:乱码、瞎编、超时与图谱膨胀
标题里的源码包拿下来后,真正跑通只是第一步。我见过太多人在本地环境栽在数据编码、模型幻觉和依赖冲突上。这里整理五条踩坑记录,每条都是“现象、原因、解决”的路子,照着检查能省一整晚的调试时间。
5.1 图谱里的中文变成一堆问号或乱码
现象:前端图谱加载出来,节点名全是?或\uXXXX。
原因:CSV 文件编码不是 UTF-8,或者图 JSON 导出时ensure_ascii没设成 False。Windows 下用 Excel 另存的 CSV 经常是 GBK,读取时按 UTF-8 解析就崩了。
解决:读取 CSV 时统一加encoding="utf-8-sig",它能自动吃掉 BOM 头。导出 JSON 时固定ensure_ascii=False,写路径时也用绝对路径,防止打包后工作目录变化找不到文件。
5.2 生成式AI推荐了一道图谱里根本不存在的菜
现象:候选清单里只有十道菜,模型却回复“推荐红酒炖牛肉”,图谱里完全没这个节点。
原因:提示词约束不够硬。模型看到用户说“想吃点好的”,就自由发挥补了一道常见的菜。
解决:把候选菜名做成枚举,提示词里加一句“只能从候选清单选择,若所有候选都不合适,返回空数组”。同时在输出校验时做一次硬校验:解析 JSON 后检查每个菜名是否真的存在于图节点集合中,不存在就把该条推荐删掉。校验代码在parse_reply里补一段拦截逻辑,不要依赖模型自觉。
5.3 图谱节点太多,召回一次要等好几秒
现象:数据量上千后,recall_by_profile扫描全图明显变慢,前端卡顿。
原因:每次请求都遍历所有节点,且候选生成后没有做缓存。食材和营养匹配又涉及多跳查询,复杂度叠上去就慢了。
解决:建一个内存索引,把口味、场景、食材到菜谱的倒排关系先建立好,召回时直接查倒排表再合并分数。节点量再大,也能在几十毫秒内返回。实在不行就限制一次请求只扫描当前用户最活跃的 500 道菜,毕业设计演示根本感觉不到差别。
5.4 JSON 响应被 markdown 围栏包住,解析失败
现象:json.loads抛Expecting value,打印原始响应发现是```json ... ```。
原因:部分模型服务默认输出 markdown 风格,尤其微调过的模型很喜欢在 JSON 外面包围栏。
解决:解析前先清理字符串,去掉开头和结尾的```json和```。下面这段是百试百灵的处理:先定位第一个{和最后一个},截取中间部分再json.loads。如果还失败,就尝试json5库解析,它能容忍尾逗号。
import json, re def extract_json(reply: str) -> str: if not reply: return "" # 去掉 ```json 围栏 reply = re.sub(r"^```(?:json)?\s*", "", reply.strip()) reply = re.sub(r"\s*```$", "", reply) # 截取最外层花括号 start, end = reply.find("{"), reply.rfind("}") if start == -1 or end == -1: return reply return reply[start:end + 1] def parse_reply(reply: str) -> dict: cleaned = extract_json(reply) try: return json.loads(cleaned) except json.JSONDecodeError as e: print("JSON 解析失败:", e, "原始内容:", reply) return {"recommendations": []}这里把extract_json单拎出来,是因为后面调试任何 JSON 输出都会用到。遇到模型输出被截断的情况,至少返回空列表,让前端显示“暂无推荐”,而不是直接抛异常。
5.5 接口超时,前端一直转圈
现象:推荐接口偶尔要等 60 秒以上,然后前端报错。
原因:部署大模型的本机推理速度受显存和上下文长度影响,用户请求太长时推理时间成倍增加。提示词里塞了过多候选菜,也增加了 prefill 时间。
解决:三层手段一起用。第一,候选菜数压到 8 个以内,证据路径每条限制在 5 个以内;第二,HTTP 请求设置 30 秒超时,超过就走降级模板;第三,生成式AI的调用放在线程池里,前端先返回“正在分析”,等后台完成再轮询结果。演示现场推荐把超时缩短到 20 秒,宁可降级也不要让评委对着转圈页面等一分钟。
6. 从能跑到能答辩:验证方法、演示点与打包习惯
很多毕设代码跑通后就直接拿去答辩了,结果被问“你怎么证明推荐结果靠谱”时答不上来。我的经验是提前准备一套轻量人工验证集,不用写论文那么正式,但能让导师看出你认真做了评估。
准备十组测试输入,覆盖不同组合条件:夜宵想吃辣但怕胖、高蛋白健身餐、儿童午餐不辣、加班后快手菜、低热量家常菜等等。每组输入跑一遍完整流程,记录召回数量、生成结果是否合理、有没有虚构食材。表格形式放一页 PPT 里,评委一眼就懂。
| 测试输入 | 召回菜数 | 推荐理由有依据 | 有无虚构食材 |
|---|---|---|---|
| 夜宵想吃辣但怕胖 | 8 | 是 | 无 |
| 高蛋白健身餐 | 6 | 是 | 无 |
| 儿童午餐不辣 | 5 | 是 | 无 |
| 加班快手菜 | 7 | 是 | 无 |
| 低热量家常菜 | 9 | 是 | 无 |
演示时,前端页面加一个“图谱路径”展开区域,把菜的食材节点、营养节点、口味标签用彩色连线显示出来。不需要复杂可视化框架,用 JSON 里的节点坐标和边关系画成简单树状图就行。这一步比任何文字都能说明“为什么推荐这道菜”。我自己的经验是,理论知识讲五分钟不如现场点开一条图谱路径给大家看直观。
最后是打包习惯。写一个run.py作为统一入口,启动时先加载graph.json,再拉起 Flask 服务和生成式AI客户端。依赖导出用pip freeze > requirements.txt,同时另存一份requirements-dev.txt只放核心依赖,方便别人直接复现。模型服务地址写进环境变量,别硬编码在代码里,换机器演示时改环境变量即可,不用动代码。
做完这些,回头最大的教训是:生成式AI参数调了三天,最后发现让系统稳定的不是某个参数,而是把知识图谱作为硬约束的边界守住了。模型只负责说话,不负责决策,这个边界不守住,参数调得再漂亮也白搭。希望这个思路能帮你在毕设里少走一段弯路,做到既有深度又稳定可演示。
祝你答辩顺利。
本文还有配套的精品资源,点击获取