ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于知识图谱与大模型的Python中医养生问答系统构建指南

2026/8/26 11:13:02 拓冰建站 浏览量
基于知识图谱与大模型的Python中医养生问答系统构建指南 简介在垂直领域问答系统中单一技术方案往往难以兼顾准确性与交互体验大模型擅长自然语言生成却容易产生幻觉知识图谱能提供结构化事实却缺乏语言组织能力。为解决这一矛盾工程上常采用“知识图谱大模型”的混合架构由知识图谱负责事实兜底大模型负责语义理解与表达生成从而在医疗、法律、养生等对准确性要求高的场景中实现可靠且自然的智能问答。本文以Python FastAPI为后端、Neo4j为图谱存储、本地Qwen模型为生成引擎完整演示了从领域本体建模、知识图谱构建与清洗、后端服务设计到前端交互实现的落地过程并给出混合问答策略与性能优化方案适合希望构建垂直领域问答系统的开发者参考。 去年我在做中医养生问答方向的项目时碰到一个典型矛盾纯靠大模型回答语气很流畅但细节特别容易“一本正经地胡说八道”纯靠知识图谱又没法应付用户那些绕弯子的问题比如“最近总是夜里醒手心发热是不是该补点什么”。这个python中医养生问答系统就是围绕这个矛盾做的后端用Python搭服务前端用HTML写单页集成了基于知识图谱的问答大模型问答双通道能力。知识图谱负责给准确事实兜底大模型负责组织语言和理解上下文两者组合起来比我之前单独用任何一条路线都稳得多。这篇文章适合两类人看一是想自己撸一个垂直领域问答系统的开发者尤其医疗、养生、法律这类对事实准确性要求高的场景二是对知识图谱和大模型工程结合感兴趣想找一个完整参考实现的同学。我会把从领域建模、Neo4j建库、后端接口设计到前端页面实现再到混合问答策略的完整过程都过一遍顺带把我踩过的坑也列出来。1. 整体设计为什么非要把知识图谱和大模型拼在一起1.1 单一技术路线的局限性先说说为什么不能只用其中一种方案。只用大模型最直接的问题就是幻觉。通用大模型在中医养生领域虽然有大量训练语料但它不具备查证能力。你问“阳虚体质能不能吃西瓜”它可能给你一个听起来很有道理、实际上却是模棱两可甚至错误的答案。中医养生又很讲究体质辨别和食材宜忌一个推荐错误用户看了觉得没事但长期照做很可能影响健康这个责任项目方承担不起。只用知识图谱也有问题。知识图谱本质上是“实体-关系-实体”的三元组存储它擅长回答“阳虚体质宜食哪些食材”这种结构化查询但不擅长处理自然语言。用户不会总按标准实体名提问他可能说“我手脚冰凉冬天特别怕冷”你得先把这句话映射到“阳虚体质”这个实体上再做图谱查询。而且图谱只能给你事实列表没法生成一段像人话一样的完整建议。所以单独用图谱会把交互体验做得特别生硬。因此我做的是双通道图谱管事实大模型管表达图谱查不到的时候再由大模型兜底并明确提示用户“以下是基于通用知识的参考”。这样既控制了幻觉风险也让回答更像一个真正的养生顾问。1.2 双通道架构与核心流程整个系统从数据流上可以分成四层第一层是前端页面纯HTMLCSSJavaScript实现跑在浏览器里负责收集用户问题、展示回答、渲染知识图谱卡片的实体关系。第二层是Python后端服务我用FastAPI搭建提供/api/chat接口内部做意图识别、图谱查询、大模型调用的路由。第三层是知识图谱存储用Neo4j保存中医养生领域的实体和关系比如“体质-宜食-食材”“穴位-主治-症状”。第四层是大模型服务我用本地的Qwen模型提供生成能力避免调用外部API导致数据外泄和响应不可控。用户发起一个问题后后端先做意图识别如果问题里能抽出“体质”“食材”“穴位”等实体就去Neo4j查询相关三元组查询结果塞进大模型的提示词上下文里让模型基于“知识图谱事实通用养生知识”生成回答。如果图谱没查到大模型就用自身知识生成一个低置信度回答并附上“建议咨询中医师”的免责提示。整个流程用一条链路串起来不是两个独立接口也不是简单的“先图谱后大模型”而是在提示词层面深度融合。1.3 技术选型的几个关键决策技术选型上我做了几个对比。后端框架我在Flask和FastAPI之间选择了FastAPI原因是它对异步支持好自动生成API文档而且用Pydantic做参数校验很方便。在一个问答系统里图谱查询和大模型调用都是耗时长、适合并发的操作异步协程能明显提升系统吞吐量。Flask本身也很优秀如果团队只熟悉Flask用它也能做但对于新项目我建议直接上FastAPI。前端不用Vue或React而是用纯HTML这其实是刻意为之。这个项目的核心价值在后端前端只需要一个聊天窗口加几个卡片用原生HTMLJS足够还能避免引入Node.js构建链路的复杂度。如果你想快速验证想法或者给后端Demo用纯HTML是最省事的方案。核心存储我选了Neo4j。知识图谱本来就是Neo4j的主场Cypher查询做多跳关系遍历非常自然。比如查询“阳虚体质宜食的食材以及这些食材对应的食谱”用MySQL要写好几层JOIN用Cypher只要一行MATCH。大模型推理服务选本地Qwen主要考虑是中医健康数据敏感用户问的问题可能涉及身体状态我不希望数据经过第三方API。用Ollama把Qwen跑在本地服务器上虽然对硬件有一定要求但在可控成本和数据安全之间是合理的平衡。2. 中医养生知识图谱从领域建模到落地入库2.1 实体和关系怎么定义知识图谱不能上来就灌数据第一步是定义本体。我先梳理了中医养生场景里最常用的几类实体最核心的是体质类型、食材、穴位、经络、症状、养生方法、食谱。这些实体覆盖了大部分日常养生问题用户问“我是什么体质、该吃什么、该按哪个穴位”底层都能落到这些实体上。关系方面我设计了以下几组最常用的体质 - 宜食 - 食材比如“阳虚体质 - 宜食 - 羊肉”体质 - 忌食 - 食材比如“阴虚体质 - 忌食 - 辣椒”体质 - 易感 - 症状比如“痰湿体质 - 易感 - 身体沉重”症状 - 宜按 - 穴位比如“失眠 - 宜按 - 神门穴”穴位 - 归经 - 经络比如“足三里 - 归经 - 足阳明胃经”养生方法 - 适用 - 体质比如“艾灸 - 适用 - 阳虚体质”实体属性也要想清楚。食材节点要存“性味”“归经”“功效简介”体质节点要存“典型表现”“调理原则”穴位节点要存“定位”“操作方法”。这些属性在后续大模型生成回答时非常重要它们能作为事实依据直接塞进提示词。2.2 数据来源与清洗数据来源我主要用了三类中医体质分类与判定标准、公开的中医药膳资料、经典中医养生书籍中涉及食疗和穴位的内容。需要注意这些公开资料的版权和适用性要自己把关少量摘录用于知识整理还好不能全文拷贝商用。项目里我特意加了一句免责声明所有内容仅供参考不构成医疗诊断身体不适请及时就医这句话放在前端页脚和每次回答的末尾。清洗是工作量最大的环节。我碰到最多的坑就是同一实体的不同叫法比如“红枣”和“大枣”其实是一个东西“阳虚”和“阳气虚”也指向同一体质。如果不做实体对齐图谱会变成一个分裂的迷宫。我的做法是先建一个“实体别名表”把所有同义词统一映射到标准名入库时一律用标准名。比如大枣、红枣、干枣统统映射到“红枣大枣”然后在节点属性里存别名列表方便前端搜索。2.3 Neo4j建库实操Neo4j建库我用的是Cypher批量导入的方式。比如导入食材节点和“体质-宜食”关系代码大概是这样CREATE (f:Food {name: 羊肉, nature: 温, flavor: 甘, meridian: 脾经、肾经, effect: 温中暖肾益气补虚}); CREATE (b:BodyType {name: 阳虚体质, typical: 畏寒怕冷手足不温, principle: 温阳散寒}); MATCH (b:BodyType {name: 阳虚体质}), (f:Food {name: 羊肉}) CREATE (b)-[:宜食]-(f);对于批量导入我写了一个Python脚本读取CSV然后调用Neo4j的批量接口避免在Cypher里逐条手写。脚本里有一个关键步骤导入前先做实体名归一化再通过MERGE而不是CREATE来避免重复节点。MERGE相当于“存在则匹配不存在则创建”对实体对齐很有用。还有一个容易忽略的点关系方向。中医养生里“体质宜食食材”和“食材宜用于体质”是反方向但语义不同。我统一约定主体在前体质 - 宜食 - 食材查询时只按这个方向查避免后续代码里方向混乱。2.4 图谱数据校验与补全建完图谱后我做了几轮校验。第一轮是查孤立节点比如没有任何关系的食材节点要检查是不是漏建了关系第二轮是查重复关系比如同一对实体之间出现两条“宜食”需要去重第三轮是逻辑校验比如“阳虚体质”的忌食列表里不能出现“生姜”这种温性食材虽然这种校验不能完全自动化但可以靠规则筛选明显冲突的数据。补全则靠迭代。用户问过的高频问题里如果发现图谱没有覆盖我就手动补充。比如有人问“湿气重怎么办”我当时图谱里只有“痰湿体质”没有“湿气重”这个症状实体后来就补了“湿气重 - 相关体质 - 痰湿体质”的关系。知识图谱是越用越完整的最开始不必追求大而全先覆盖核心场景再按真实提问慢慢扩展。3. 后端服务Python如何把图谱和大模型串起来3.1 后端接口设计我用FastAPI写了三个核心接口。第一个是POST /api/chat接收用户消息返回最终回答、命中的实体、来源类型第二个是GET /api/entities?queryxx给前端做搜索联想第三个是GET /api/graph?entityxx返回指定实体的周边关系前端用小卡片展示。/api/chat的请求体定义如下from pydantic import BaseModel class ChatRequest(BaseModel): message: str session_id: str default返回结构我设计成{ reply: 阳虚体质的人冬天怕冷宜吃羊肉、韭菜、桂圆等温性食材……, entities: [阳虚体质, 羊肉, 韭菜], source: knowledge_graphllm, confidence: high }这样前端可以不仅显示文本还能把命中的实体渲染成可以点击的知识卡片用户点了就能看图谱关系。3.2 知识图谱查询模块图谱查询模块我封装了一个KnowledgeGraphService核心方法是根据用户问题抽取的实体名到Neo4j查关系。例如用户说“阳虚体质吃什么”后端先用一个简单的规则抽取“阳虚体质”然后执行Cypherfrom py2neo import Graph class KnowledgeGraphService: def __init__(self): self.graph Graph(bolt://localhost:7687, auth(neo4j, password)) def get_food_by_physique(self, physique_name: str) - list: query MATCH (b:BodyType {name: $name})-[:宜食]-(f:Food) RETURN f.name AS name, f.nature AS nature, f.effect AS effect data self.graph.run(query, namephysique_name).data() return data查完后不是直接返回而是组装成一个“知识片段”的字符串比如“羊肉性温功效温中暖肾益气补虚”。这个片段后面会被拼进大模型提示词。3.3 大模型问答模块大模型我通过Ollama提供的HTTP接口调用模型用Qwen。为了不让模型离题我设计了一个提示词模板先把真实知识图谱结果放进去再加约束。system_prompt 你是一位有经验的中医养生顾问。请根据下面提供的知识图谱事实回答用户问题。 要求 1. 优先引用知识图谱中的内容不要编造图谱里不存在的食材、穴位或功效。 2. 回答要口语化、有温度但不要给人“包治百病”的感觉。 3. 如果用户症状严重请提醒及时就医不要耽误诊疗。 4. 如果知识图谱中没有相关信息请明确说“这部分内容我掌握得不够准确”。 知识图谱事实 {kg_facts} 这里的关键是“知识图谱事实”不能太长。我一开始把所有查询结果都塞进去结果大模型反而找不到重点回答变得啰嗦。后来做了剪裁最多保留5条食材、3个穴位、2条食谱并且按“宜食”“忌食”“穴位”“食谱”分块排列。3.4 混合问答策略混合问答不是简单的“图谱优先”。我实际用的是三条分支第一分支如果规则抽取出明确实体并且图谱查询结果非空就走“图谱大模型”增强路径系统提示词中强制要求回答时引用图谱事实置信度高。第二分支如果实体抽取不到但用户描述偏向症状就用症状关键词做模糊匹配尝试找关联穴位或食材如果关联到了再放大模型生成这个路径置信度中等。第三分支如果完全匹配不到就直接让大模型回答但在回复末尾附上“以上内容来自通用知识建议您线下咨询中医师”置信度低。我把这三条分支的路由逻辑写成了一个简单的词典分类器加规则。你也可以用训练好的意图分类模型但对于垂直领域规则加词表在初期已经足够稳定而且方便快速调整。4. 前端页面纯HTML也能做出好用的聊天界面4.1 页面整体布局与交互设计前端虽然是纯HTML但我没有做得很简陋。整体布局分左右两栏左侧是聊天窗口右侧是知识图谱实体卡片区。用户没有提问时右侧展示一些推荐问题比如“易疲劳是哪种体质”“哪些食材适合寒性体质的人”用户提问后右侧根据返回的实体动态加载图谱关系点击实体名称可以继续展开。交互上我只有一个关键点不要让用户干等。聊天消息发送后前端马上显示一个“正在结合知识图谱查询……”的加载状态。虽然实际上后端是大模型生成可能耗时几秒到十几秒但这个提示能让用户知道系统正在工作而不是卡死了。4.2 关键前端代码实现页面核心是调用后端接口并渲染回复。我写了一个sendMessage函数async function sendMessage() { const input document.getElementById(chatInput); const message input.value.trim(); if (!message) return; appendMessage(user, message); input.value ; showLoading(); const response await fetch(/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message: message }) }); const data await response.json(); hideLoading(); appendMessage(assistant, data.reply); renderEntities(data.entities); }这里一定要处理两个细节一是fetch请求后端时如果前后端分开部署会有跨域问题需要在FastAPI里加 CORS 中间件二是前端拿到的中文内容如果出现乱码要检查后端返回时是否设置了正确的Content-Type: application/json; charsetutf-8。FastAPI默认用UTF-8但我曾因为用了旧版本Py2neo返回的字符串编码异常而踩过坑。4.3 前后端联调细节联调阶段最容易出问题的就是路径和请求格式。我本地后端起在127.0.0.1:8000前端HTML直接用浏览器打开此时跨域是必现的。后端加中间件解决from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*], )调试时我习惯先用Postman测试后端接口确认返回结构后再去写前端渲染这样能隔离问题。前端渲染如果返回结构和预期不符优先打开浏览器F12的Network面板看真实响应而不是猜代码。5. 完整实操从零搭一个“体质鉴别养生推荐”示例5.1 场景拆解我用一个高频需求来完整走一遍流程用户输入“我冬天特别怕冷手脚老是冰凉应该吃点啥”这个问题里的核心信息是“怕冷”“手脚冰凉”它们都能映射到“阳虚体质”。这个例子很适合说明混合问答的价值。如果只靠图谱直接查“阳虚体质宜食食材”系统也能给答案但给不出针对“手脚冰凉”的完整解释如果只靠大模型又容易出现推荐不准确的情况。现在系统会先通过图谱把“阳虚体质”相关的食材和穴位查出来再让大模型基于这些事实组织成一段贴心的回答。5.2 后端处理流程后端接收到这条消息后先走实体识别。我用了一个关键词表physique_keywords { 阳虚体质: [怕冷, 手脚冰凉, 畏寒, 四肢不温], 阴虚体质: [手心发热, 失眠, 口干, 盗汗], 痰湿体质: [身体沉重, 容易胖, 舌苔厚], }根据句子里的“怕冷”“手脚冰凉”系统判断命中“阳虚体质”。然后执行图谱查询查出宜食食材羊肉、韭菜、桂圆、生姜忌食食材苦瓜、绿豆、西瓜推荐食谱当归生姜羊肉汤、韭菜炒核桃仁推荐穴位关元穴、命门穴、足三里这些结果被组装成知识片段拼进大模型提示词。5.3 实际返回结果与前端展示大模型生成后的回答大致是“从您说的冬天怕冷、手脚冰凉来看偏向阳虚体质。阳虚的人阳气不足温煦功能减弱所以容易怕冷。饮食上适合吃羊肉、韭菜、桂圆这类温性食材每天可以煮一点当归生姜羊肉汤像苦瓜、绿豆这类寒凉食物尽量少吃。日常还可以艾灸或按揉关元穴、命门穴帮助温阳散寒。如果这种怕冷持续加重或者伴随其他不舒服建议去中医科做个系统调理。”返回结果里带着entities: [阳虚体质, 羊肉, 当归生姜羊肉汤, 关元穴]前端把这些实体渲染成右侧卡片用户点击“关元穴”会触发一次/api/graph?entity关元穴的查询展示穴位归经、主治等相关关系。5.4 性能优化实际部署后我发现两个性能问题。第一个是Neo4j查询耗时。随着数据量增加未加索引的模糊查询会变慢。解决办法是在name属性上建索引CREATE INDEX FOR (b:BodyType) ON (b.name); CREATE INDEX FOR (f:Food) ON (f.name);第二个是大模型并发问题。Ollama默认单实例处理请求如果多个用户同时问后面的请求会排队。我在后端做了一个简单的请求队列限制大模型调用并发为1或2避免Ollama内存溢出。同时在应用层加了Redis缓存把高频问题的回答缓存30分钟重复问题就直接走缓存明显降低了整体响应时间。6. 踩坑记录与排查思路6.1 中文编码与字符集问题我最早遇到最频繁的问题就是中文乱码。有一次前端明明收到了正确JSON但页面显示却是\u9633\u865a。后来发现是Python的json.dumps默认把中文转成Unicode转义序列。FastAPI内部会自动处理但我在调试阶段用json.dumps(data, ensure_asciiFalse)打印日志时没注意导致日志里的内容没法看。所以排查时一定要确认工具链里每一层的编码设置。6.2 Neo4j导入慢或内存不足如果一次性导入几千个节点和关系用逐个CREATE会非常慢。我改成用UNWIND批量写入比如把数据读成列表后UNWIND $rows AS row MERGE (f:Food {name: row.food_name}) SET f.nature row.nature, f.effect row.effect同时控制每次写入条数不要超过500条。如果Neo4j内存不足优先调整JVM堆内存配置之前默认堆内存只有512M我调到2G以后导入速度明显提升也更稳定。6.3 大模型回答幻觉和偏离专业问题这个坑比较难解决。即使我给了知识图谱事实模型偶尔还是会在推荐食谱的剂量上“自由发挥”比如“当归生姜羊肉汤里放当归20克”。中医里当归用量有讲究我作为系统开发者没有资格确认这个剂量是否适合所有人因此在提示词里直接加了硬性约束不要给出具体的药物剂量只提食材和大致做法。这个规则能大大降低风险。6.4 前后端联调时的CORS和路径问题CORS问题前面提过但还有一个容易被忽略的点如果把前端HTML放到后端静态目录下则不需要跨域却要注意静态资源路径。我在FastAPI里挂载了静态目录访问http://127.0.0.1:8000/就能打开首页此时前端请求/api/chat是同源请求不需要CORS。这比每次打开本地文件再跨域顺畅很多。6.5 常见问题速查表我把几个高频问题整理成了一张表方便以后排查问题现象可能原因排查与解决前端返回乱码后端编码或Content-Type不对检查是否返回UTF-8 JSON浏览器Network面板看响应头大模型回答与图谱不符提示词约束不足调整提示词加强“优先引用知识图谱事实”的指令减少自由发挥Neo4j查询很慢缺少索引或数据量过大给常用属性建索引用批量Cypher写入多个用户同时问时卡死Ollama并发能力有限后端加大模型调用队列限制并发数前端请求跨域失败后端未配置CORS加CORSMiddleware或把前端挂到后端静态目录实体识别不准用户表述口语化持续补充同义词表和触发规则加入更多别名7. 项目落地后的一些体会我做完这套系统最深的感受是知识图谱和大模型不是替代关系而是互相补位的关系。尤其在中医养生这种垂直领域用户既要准确的事实又要自然的交流体验只有一个技术栈就很难两全。如果让我从零重做一遍我会在项目最开始就规划好“实体别名表”的维护机制而不是等数据多到开始乱才回头清洗。另外想提醒一句这类项目往深了做一定会碰到医疗合规问题。系统的定位必须明确是“养生科普”而不是“医疗诊断”界面提示、回答文案、免责声明都要做到位。这个底线守住了技术上的尝试反而能更放开手脚。最后分享一个实用的小技巧如果你没有足够数据建完整知识图谱可以先跑一个“大模型少量结构化JSON”的最小版本把高频问题的答案做成JSON片段再让大模型基于JSON生成回答。等数据积累多了再把这些JSON片段迁移到Neo4j。这样能缩短项目落地时间也方便观察用户真正问什么避免一开始就过度设计。本文还有配套的精品资源点击获取