尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

知识图谱与大模型双引擎:中医养生问答系统从0到1实践

知识图谱与大模型双引擎:中医养生问答系统从0到1实践 简介这是一套面向中医养生知识服务场景的Python问答系统实现适用于对健康信息化、知识图谱应用或AI垂直领域感兴趣的开发者与学习者。系统融合结构化知识检索与大模型自然语言理解能力解决用户在中医养生领域精准提问、多角度解答的实际需求。资源包共344个文件含31个核心Python后端脚本含Flask/Django接口逻辑、7个HTML前端页面、24个PNG/JPG图标与界面素材、184个JS交互脚本含Bootstrap、jQuery Confirm、Layer等UI组件以及SQLite3数据库与CSV知识数据整体13.99MB技术栈清晰、模块边界明确。已有91人学习下载提供完整可运行源码、预置知识图谱数据、大模型调用接口封装及前后端联调说明便于快速部署、二次开发或教学演示。 我自己做过不少问答类的系统但真正把知识图谱和大模型两种路线放在一起揉进一个中医养生场景里的还是头一次这么系统地做。这个项目从零搭起来后端用 Python前端用纯 HTML实现在本地跑通图谱精准检索 大模型开放生成双引擎问答整个过程踩了不少坑也沉淀了不少经验分享一下完整的设计思路和实现细节。先说清楚这个系统解决什么问题传统的关键词搜索对阳虚体质冬天怎么调理这类问题几乎无能为力因为用户问的往往不是某个孤立实体而是一组关系推理。医疗健康类问题又讲究可溯源性大模型虽然能说会道但一本正经胡说八道也让人头疼。所以我把知识图谱问答和大模型问答并行接入图谱负责精确查询和关系推理大模型负责泛化语义理解和答案润色两者通过路由策略协同既保证回答的可靠性又保留自然语言交互的灵活性。整个项目对正在做智能问答、领域知识图谱落地或者想了解大模型和结构化知识怎么融合的开发者都挺有参考价值。1. 项目定位与整体设计思路1.1 为什么做中医养生问答系统中医养生这个领域太适合问答系统了。它的知识结构天然是实体 关系的网状形态比如体质、证候、经络、穴位、食材、药材、方剂、禁忌这些实体之间存在大量的养生、调理、关联、禁忌关系。你问脾胃虚寒能不能吃梨传统的搜索引擎给你一堆网页你得自己阅读判断图谱问答系统可以直接从梨节点的寒凉属性出发关联到脾胃虚寒的饮食禁忌关系给出明确答复。但光有图谱也不行。用户提问的方式是千奇百怪的同一句话可以表述成冬天手脚冰凉怎么调也可以说成阳虚怕冷吃什么如果图谱里没有完全匹配的路径就必须靠大模型来兜底。把两者结合起来之后图谱做确定性的知识检索大模型做语义理解 泛化生成这个系统才算真正能落地。1.2 为什么采用知识图谱 大模型双引擎这个问题我纠结了很久。一开始想的是只做知识图谱问答查起来精准每条回答都能溯源到具体的三元组。但测试了几百个问题之后发现真实用户的提问太碎了很多口语化表达根本没法直接映射到图谱查询语句上针对这类问题硬生生写规则规则越写越多效果越来越差。后来考虑干脆全交给大模型但医疗养生场景的底线是不能乱说大模型生成的回答里一旦掺入幻觉内容轻则误导用户重则出问题。双引擎的真正价值在于各取所长。我的设计是把图谱问答作为主引擎大模型作为语义兜底和增强层。一个 query 进来先走图谱查询流程如果命中率高、置信度够就直接输出答案如果图谱没命中或者命中路径太短就把问题交给大模型让它结合给定的领域上下文来回答。这样既保住了可解释性又保证了覆盖面体验上比单引擎顺滑很多。1.3 系统整体架构整个系统横向分四层数据层MySQL 存原始语料和日志Neo4j 存知识图谱向量数据库我用的是 Chroma存大模型检索用的文本切片。后端服务层Python 写的 FastAPI 应用内部拆成图谱问答服务、大模型问答服务、路由服务、日志服务四个模块。前端表现层纯 HTML CSS JavaScript做一个类聊天的交互页面支持问题的实时展示和流式输出。部署层本地开发环境直接跑生产环境用 Nginx 反代 Uvicorn。这个架构其实不复杂关键在设计细节上。每一层都多考虑了一步这个模块的数据从哪里来输出到哪里去整个系统串起来之后逻辑就非常顺。2. 知识图谱构建从语料到三元组2.1 中医养生领域的本体建模构建图谱之前第一件事是定义本体模型也就是这个图谱里有哪些实体类型和关系类型。我在这个项目里定义的核心实体有 12 类体质阳虚质、阴虚质、气虚质、痰湿质、湿热质、血瘀质、气郁质、特禀质、平和质证候脾胃虚寒、肝气郁结、气血两虚、肾阳虚等食材梨、羊肉、生姜、红枣、山药等药材当归、黄芪、枸杞、人参等穴位足三里、关元、涌泉、合谷等方剂四君子汤、六味地黄丸、归脾丸等经络胃经、脾经、督脉等症状手脚冰凉、失眠、乏力、口苦等季节春、夏、秋、冬人群老年人、孕妇、儿童等养生法艾灸、泡脚、八段锦、刮痧等禁忌饮食禁忌、生活禁忌等关系类型是图谱的灵魂我选用了十几种关系来连接这些实体调理体质(is_good_for)食材/药材/穴位 → 体质导致证候(causes)症状/习惯 → 证候治疗(cures)方剂/药材 → 证候禁忌(contraindicates)食材/药材 → 证候/体质归经(belongs_to)药材 → 经络穴位主治(treats)穴位 → 症状季节性养生(suited_for_season)食材/养生法 → 季节食材属性(has_property)食材 → 寒热温凉属性配伍禁忌(conflicts_with)药材 → 药材我这里再补一个很关键的点图谱关系的设计不要贪多贪全而是围绕用户可能会问哪些问题来反推。我整理了一百多个高频问题样本分析问题里涉及到哪些实体的哪些关系再按这个反向设计本体。如果你的图谱关系建了很多但用户根本问不到不仅浪费存储还会让查询语句越来越复杂。2.2 语料采集和三元组抽取本体模型建好之后最费时间的活来了数据准备。我从公开的中医养生书籍、药材百科、养生网站等来源整理了一万两千多条相关语料。这些语料不能直接塞进系统必须先结构化。三元组抽取我用了半自动的方式。纯手工标注太慢纯自动抽取质量没法保证。我的做法是分三步走第一步人工整理核心的规则模板比如XX性寒味甘具有清热解毒的功效这类半结构化的句子直接套正则抽出来。这一步能解决大概三成数据。第二步把剩余语料丢给大模型做信息抽取。我构造了一个抽取 prompt让大模型从一段话里抽取出符合本体定义的三元组输出成 JSON 格式。这一步能把抽取覆盖度提到八成以上。注意大模型抽取出来的一定要人工抽检我设置了 10% 的抽检率发现错误立即修正 prompt因为有的时候它会把不宜多吃抽成正向的适宜关系这种错误在养生场景里很危险。第三步人工审核修正。虽然费时间但这是保证图谱质量绕不开的一环。我审核的时候额外注意了数据的一致性比如梨不能在前一条数据里是寒性、在后一条数据里变成温性这种错误一旦进图之后所有的推理结果都会歪掉。2.3 Neo4j 图谱入库与索引设计三元组整理好之后就是导入 Neo4j。我用的是 Neo4j Community 5.x 版本数据库本身免费单机部署足够用。导入方式很简单数据整理成 CSV用LOAD CSV或者 py2neo 批处理写入都可以。我贴一个用 py2neo 批量导入的核心代码from py2neo import Graph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) def import_triples(triples): tx graph.begin() for subj, rel, obj in triples: # 确保实体节点存在MERGE 而不是 CREATE避免重复 subj_node tx.merge(Node(Entity, namesubj, entity_type未知)) obj_node tx.merge(Node(Entity, nameobj, entity_type未知)) # 关系也同样用 MERGE保证幂等 tx.merge(Relationship(subj_node, rel, obj_node)) tx.commit()导入的时候有个容易被坑的点Neo4j 的MERGE依赖唯一性约束。你要先给实体名建唯一约束否则并发导入时非常容易出现重复节点。建约束的语句是CREATE CONSTRAINT entity_name_unique IF NOT EXISTS FOR (n:Entity) REQUIRE n.name IS UNIQUE节点建好后索引也很关键。我在name字段上建了全文索引用于支持模糊查询同时在实体类型的字段上也建了索引。实际查询中用户输入的问题先经过实体识别识别出来的实体名再去图谱里精确匹配所以精准索引在很多时候比全文索引更重要。图谱的最终规模大概是 3200 实体节点和 18000 条关系这个规模对 Neo4j 来说是小意思查询响应基本都在几十毫秒级别。真正慢的反而不是图谱本身而是后面的实体识别和查询语句构造环节。2.4 实体识别与查询语句构造图谱问答的关键一步是实体识别。用户的问题是一段自然语言比如孕妇冬天适合吃什么你得先从中识别出孕妇和冬天这两个实体然后判断它们对应图谱里的人群和季节类型再去查询它们之间的关系。我做实体识别的方式是基于图谱里已有的实体名构建一个词典匹配的时候用正向最大匹配 同义词扩展。先按长度从长到短去匹配文本中的实体因为人参和人参果必须优先匹配更长的那个否则会误判如果匹配不到就查同义词表。同义词表是手工整理的比如拉肚子对应腹泻怕冷对应畏寒没力气对应乏力。整理同义词表的过程很琐碎但是它对提升命中率有立竿见影的效果。实体识别出来之后就要把这些实体和问题意图组装成 Cypher 查询语句。系统里我准备了几种查询模板根据用户在问题里出现的实体类型组合自动匹配// 示例1体质 食材 - 查询适合的食材 MATCH (e:Entity {name: 阳虚质}), (f:Entity {entity_type: 食材}) MATCH (f)-[r:is_good_for]-(e) RETURN f.name, r.relation_desc // 示例2症状 - 查询对应穴位 MATCH (s:Entity {name: 失眠}), (a:Entity {entity_type: 穴位}) MATCH (a)-[r:treats]-(s) RETURN a.name, r.relation_desc // 示例3食材 证候 - 判断是否禁忌 MATCH (f:Entity {name: 梨}), (s:Entity {name: 脾胃虚寒}) OPTIONAL MATCH (f)-[r:contraindicates]-(s) RETURN f.name, s.name, CASE WHEN r IS NULL THEN 可适量食用 ELSE 需忌口 END AS result查询语句的构造一定要考虑空结果的情况。用户问的问题可能命中实体但实体之间没有直接的关系。遇到这种情况我做了一跳扩展比如阳虚质和冬天之间没有直接关系那就分别查阳虚质适合吃什么冬天适合吃什么然后取交集如果交集非空就用交集生成答案如果交集为空就返回图谱未命中的提示转给大模型兜底。这个取交集、并集、差集的拓展策略能覆盖掉相当大一部分复杂查询场景。3. Python 后端FastAPI 搭建双引擎服务3.1 后端框架选型为什么选 FastAPI后端框架我在 Flask 和 FastAPI 之间对比了一下。这个系统本身不大用 Flask 完全够但最终选了 FastAPI 基于三点考虑第一后面有大模型问答模块大模型的响应是流式输出的FastAPI 对 SSEServer-Sent Events的支持更自然用StreamingResponse就能实现打字机效果第二FastAPI 自动生成 Swagger 文档调试接口非常方便第三接口用 Pydantic 做参数校验健康类问答的问题参数简单但未来要扩展多轮对话时状态管理会需要一个规范的请求结构。FastAPI 的项目结构我按模块拆得很清楚medical_qa/ ├── app.py # FastAPI 入口 ├── config.py # 全局配置 ├── models/ │ ├── schema.py # 请求/响应数据结构 │ └── entity.py # 实体识别模块 ├── services/ │ ├── kg_service.py # 知识图谱问答服务 │ ├── llm_service.py # 大模型问答服务 │ └── router.py # 双引擎路由服务 ├── utils/ │ ├── logger.py # 日志 │ └── cache.py # 缓存 └── static/ └── index.html # 前端页面这种分层结构的好处是每一个模块都有清晰的边界改动一个服务不影响其他模块。尤其是知识图谱的查询逻辑和大模型的调用逻辑如果混在一个文件里后面维护会非常痛苦。3.2 知识图谱问答模块实现kg_service.py是这个模块的核心。它的流程一共五步输入文本 → 实体识别 → 意图分类 → 查询语句构造 → 结果格式化。实体识别和查询语句构造上面已经说了这里重点说一下意图分类。意图分类我不用机器学习而是用规则判断。维护一个意图关键词表比如出现能不能吃/能吃吗/忌口 → 意图是饮食禁忌判断出现什么穴位/按哪里 → 意图是穴位推荐出现适合/推荐/吃什么 → 意图是养生推荐出现为什么/怎么回事/是什么 → 意图是原因解释出现泡脚/艾灸/按摩 → 意图是养生方法规则判断的准确率在这个封闭领域里其实很高我之前试过用 BERT 做意图分类效果提升有限但部署成本高了一个量级对这个场景没必要。规则分类加实体识别就能覆盖九成以上的图谱问答入口。查询结果格式化这一环节需要花心思。Neo4j 查出来的结果是节点和关系不能直接堆给用户要做两件事第一按关系类型分组让答案有条理。比如查询阳虚质适合吃什么返回结果按照食材推荐药材推荐穴位调理生活习惯分组呈现用户看起来一目了然。第二答案要带溯源也就是告诉用户这个结论依据是什么。我抽了一些图谱里关系实体上附加的属性描述比如某条羊肉 - is_good_for - 阳虚质关系上附加了一句羊肉性温能温中暖肾适合阳虚怕冷人群冬季食用这样答案就有了一定的解释性。3.3 大模型问答模块与 RAG 实现大模型问答模块选型上我考虑过两条路一是调用云厂商的 API一是本地部署开源模型。考虑到问答系统涉及用户健康数据很多场景不能把数据外发我最终选了本地部署方案。本地部署用的是 Ollama 拉取 Qwen2-7B-Instruct 模型7B 参数规模在普通消费级显卡上跑得动而且效果对中文医疗养生场景足够好。但直接拿大模型去回答问题效果并不理想。没有知识库约束的情况下Qwen 这样 7B 级别的小模型很容易一本正经编造养生知识什么每天吃三种水果能长寿这种话都能说出来。所以必须上 RAG检索增强生成先从自己的知识库里检索出与问题相关的文本片段再把这些文本片段作为上下文拼接进 prompt引导大模型基于这些上下文来回答。RAG 的检索我用了 Chroma 向量数据库。处理流程是把之前收集的一万两千多条语料按 300 字左右切块用嵌入模型转成向量存入 Chroma。用户提问时先对问题做同样的向量化然后从 Chroma 里按余弦相似度取 top 5 的相关片段拼进 prompt。我用的是 bge-large-zh-v1.5 嵌入模型中文效果比 OpenAI 的 text-embedding-ada-002 差不太多而且完全本地部署不需要调外部接口。向量化代码大致如下from sentence_transformers import SentenceTransformer import chromadb embed_model SentenceTransformer(BAAI/bge-large-zh-v1.5) client chromadb.PersistentClient(path./chroma_db) collection client.get_or_create_collection( nametcm_health, metadata{hnsw:space: cosine} ) def add_documents(docs, ids): embeddings embed_model.encode(docs).tolist() collection.add( idsids, documentsdocs, embeddingsembeddings ) def search_documents(query, top_k5): query_embedding embed_model.encode([query]).tolist() results collection.query( query_embeddingsquery_embedding, n_resultstop_k ) return results[documents]检索之后拼 prompt 有一个小技巧不是把检索出来的整段文本全部塞进去而是让大模型学会引用知识库内容。我在 system prompt 里强制要求你是一个中医学养生助手。请严格根据以下知识片段回答用户问题。如果知识片段中没有相关信息请直接回复根据现有知识库我无法准确回答这个问题不要编造。然后拼接知识片段和用户问题。这个约束对 7B 级别的小模型作用很大它能显著降低幻觉概率。实测下来加了 RAG 之后回答质量提升非常明显尤其是给出具体可操作建议这一类问题不再泛泛而谈而是能引用到具体的食材、穴位和禁忌。大模型回答还有一个体验问题生成速度。7B 模型单卡推理速度在 20 token/s 左右如果整段回答全部生成完再返回用户等待时间太长。所以一定要用流式输出。FastAPI 的写法如下from fastapi.responses import StreamingResponse app.post(/api/llm/chat) async def llm_chat(request: ChatRequest): async def generate(): async for chunk in llm_service.chat_stream(request.question): yield fdata: {json.dumps({answer: chunk}, ensure_asciiFalse)}\n\n return StreamingResponse(generate(), media_typetext/event-stream)前端 EventSource 监听这个流逐字显示回答交互体验会好很多。3.4 双引擎路由策略双引擎之间的调度逻辑我单独做了一个router.py。核心思路是图谱优先大模型兜底置信度判断。整个路由策略可以描述为下面的判断链第一步实体识别。如果问题里一个实体都识别不出来直接走大模型因为图谱查询无路可走。第二步图谱查询。查询返回的结果如果命中数大于等于 2 条关系且这 2 条关系置信度没有发生冲突就认为图谱结果可信直接把图谱答案返回。第三步如果图谱命中的结果只有 1 条或者这 1 条结果对应的关系是禁忌 / 适量这种需要综合判断的回答就把图谱结果作为参考上下文发给大模型让大模型生成一篇更自然、更完整的回答。这种情况下图谱的作用从回答者变成了知识约束者防止大模型跑偏。第四步如果图谱没有命中任何结果就只用 RAG 检索的上下文来问答。这里我特意把大模型回答和图谱回答区分展示如果答案来自图谱界面上会显示知识图谱检索结果如果来自大模型显示大模型综合回答。这个区分对用户来说其实是加分的因为养生知识用户天然对权威出处更信赖。路由策略里还有一个必要的细节问题缓存。同样的养生问题被重复问的概率非常高我加了一个简单的 Redis 缓存以问题的 MD5 作为 key缓存知识图谱查询结果 24 小时。大模型结果不做缓存因为生成式回答每次会有差异而且大模型调用本身有成本让用户每次问都看到不同措辞也有新鲜感。但其实大模型结果也可以考虑缓存一些高频标准化问题这可以视你的实际场景而定。4. 前端页面纯 HTML 实现聊天交互4.1 页面布局与聊天交互实现前端我刻意用了纯 HTML CSS JavaScript没有引入 Vue 或者 React 框架。原因是这个系统功能非常聚焦就是一个单页的聊天界面纯原生实现就能保证最轻量、部署最简单哪里都能跑。把index.html放进 FastAPI 的static目录后端一行代码就能把它服务出去。页面布局是这样的顶部系统标题和当前问答模式标识中部聊天消息区用户消息靠右系统消息靠左底部文本输入框和发送按钮聊天消息区需要支持两种消息类型普通文本消息和图谱卡片消息。图谱卡片消息展示的是知识图谱检索出的结构化结果包含实体名称、关系类型、关系描述和一个查看图谱路径的折叠按钮。这个折叠按钮打开之后可以看到图谱的路径描述比如羊肉 -[适合调理]- 阳虚质这部分其实比大模型生成的回答更有说服力。消息渲染的核心代码很简单主要是动态创建 DOM 节点function appendMessage(role, content, messageType) { const container document.getElementById(chat-messages); const msgDiv document.createElement(div); msgDiv.className message ${role}; if (messageType text) { msgDiv.innerHTML content; // 这里是纯文本防XSS } else if (messageType kg-card) { msgDiv.innerHTML renderKgCard(content); // 渲染图谱卡片 } container.appendChild(msgDiv); container.scrollTop container.scrollHeight; }务必注意innerHTML的安全问题。如果直接把用户输入的内容塞进去会引发 XSS 攻击。我的做法是对用户输入做一遍 HTML 转义把等特殊字符替换掉确保它不会被当作 HTML 执行。服务端返回的图谱内容虽然是可信的但也要做同样的转义双保险。4.2 流式输出与接口对接前端对接大模型流式输出这个环节我第一次实现的时候踩了不少坑。SSE 的EventSource只支持 GET 请求而我们的接口是 POST所以直接用EventSource是不行的。我最终用了fetch配合ReadableStream来做流式解析async function sendMessage(question) { const response await fetch(/api/llm/chat, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({question: question}) }); const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let answerText ; while (true) { const {done, value} await reader.read(); if (done) break; const chunk decoder.decode(value); // 解析 SSE 格式的数据 const lines chunk.split(\n); for (const line of lines) { if (line.startsWith(data: )) { const data JSON.parse(line.slice(6)); answerText data.answer; // 实时更新页面 document.getElementById(answer- currentId).textContent answerText; } } } }这里有个容易遇到的问题网络传输是分块的一块数据里可能包含多条 SSE 消息也可能一条消息被拆成两半所以解析时不能简单按行处理。我采用了一个简单而有效的方式用换行符分割然后维护一个缓冲字符串变量。如果最后一段数据不完整就先暂存等下一块数据来了拼起来再解析。否则你就会看到生成的回答时不时丢字或者出现JSON.parse报错。界面还有一个非常实用的细节加载状态提示。当问答引擎在处理时页面上会显示一个思考中的小动画用纯 CSS 实现三点闪烁。等拿到第一块数据之后再切回正常显示。这个小细节虽然简单但对用户体验的提升作用是肉眼可见的。4.3 常用快捷问题与展示优化界面底部我放了一排快捷提问标签比如阳虚体质如何调理冬天手脚冰凉怎么办脾胃虚寒能吃梨吗失眠按哪个穴位。用户点一下标签就自动往输入框填充对应的问题并触发发送。这批快捷问题都是我先前收集的高频问题样本中筛选出来的用户不知道怎么问的时候看到这些示例能更快上手。展示优化方面我做了几个细节图谱答案使用卡片式布局每条答案关联的关系清晰罗列配上小图标不是 emoji是 SVG 图标视觉上更直观。大模型答案使用流式打字效果输出配合引用知识库的角标说明该回答参考了知识库中的哪些文本片段。整个页面是响应式布局在手机屏幕上也能正常使用。这块我花了点时间调 CSS因为一开始桌面端正常一缩到手机宽度按钮就对不齐了。5. 双引擎协同细节与运行效果5.1 协同工作的边界划分双引擎协同最怕的是边界不清。我花了很多时间在定义一个问题到底该由哪个引擎来解决。我总结了一个判断标准问题是否可以被结构化。阳虚质适合吃哪些食材——适合图谱回答。因为阳虚质是一个实体适合吃是一个关系食材是一个类型整句话可以映射成一个结构化的图谱查询。阳虚质冬天怎么调理要注意什么——适合大模型结合图谱回答。因为怎么调理和注意什么不是单一关系而是一组关系加上常识知识的组合图谱回答会很干巴大模型能组织得更丰满。最近总是失眠是不是肝火旺——适合大模型兜底。这个问题涉及诊断推理图谱只能给到失眠对应哪些穴位、哪些食疗方但无法回答是不是肝火旺这种判断性的问题。这个边界划清楚了整个系统的回答质量才会有质的提升。我还加了一个机制如果用户连续两次问到同一个实体但是不同问题比如先问羊肉适合什么人吃再追问那我适合吃吗这种多轮对话场景单靠路由规则根本扛不住所以我在后端维护了一个简单的会话状态记录上一次问答涉及的核心实体。当检测到当前问题的实体缺失时尝试从会话状态里补全实体。这个机制在单轮问答系统里算是一个轻量的多轮增强。5.2 接口调用性能实测数据跑完一轮完整功能测试之后我记录了各环节的耗时数据环节耗时实体识别 图谱查询命中80-120ms实体识别 图谱查询未命中含兜底200-300msRAG 检索向量化 查询50-100ms大模型首 token 返回800-1500ms大模型完整回答约 200 字10-15s全流程图谱命中时100-150ms全流程大模型兜底时12-20s图谱命中时响应速度非常快基本上用户感觉不到等待大模型兜底时因为生成是流式的用户看到第一个字只要 800 多毫秒后续是逐字出现所以体感上还能接受。这个数据验证了把知识图谱和流式输出结合起来的价值哪怕最终要花 15 秒生成完整回答用户在头 1 秒就开始看到字往外蹦了心理等待时间大大缩短。5.3 缓存策略与并发考虑系统做了一些很实的性能优化不只是流式输出第一层是 Redis 缓存。知识图谱答案 24 小时有效期这个已经说过了。实体识别的结果也缓存了 30 分钟因为识别过程虽然快但每次都做一遍词典匹配没有必要尤其是高频词。第二层是 RAG 文档的向量化缓存。语料切片向量化之后直接存 Chroma不用每次重复计算。嵌入模型加载进内存之后保持常驻一开始加载模型需要两三秒之后就都是毫秒级。第三层是大模型的显存管理。Ollama 默认会把模型驻留在显存里如果不做任何配置模型第一次调用后会一直占着显存直到超时。这对单用户使用没问题但如果有多个请求排队就会导致显存占用太高。我用的是 Ollama 的OLLAMA_KEEP_ALIVE环境变量来设置模型在内存中的保留时间实测设为 10 分钟比较合理。并发方面单机部署这个项目其实不太需要考虑太高并发。但接口层我仍然用asyncio.Semaphore限制并发调用大模型的数量防止多个用户同时请求时显存溢出。这个限制设置为 2即同一时间最多两个大模型请求在运行其他的请求排队等待。知识图谱查询和 RAG 检索是 I/O 密集操作不受这个限制影响可以并发执行。6. 实操中遇到的典型问题与排查记录6.1 实体识别歧义问题刚开始测试时用户问体质差吃什么补实体识别结果把体质识别成了一个实体而不是体质差。这个问题的根源是词典匹配只关注实体名是否出现在文本中没有关注上下文语境。我的解决办法分两层第一层是增加前缀清洗。在匹配实体之前先剔除常见的修饰词比如体质差先把差去掉再去匹配体质。但这里体质本身在我的图谱里并不是实体实体应该是具体的阳虚质气虚质等所以匹配体质本身其实匹配不到走了大模型兜底效果还可以。第二层是上下文实体补全。类似体质差这种说法我整理了常见口语化表达和正式实体之间的映射表把体质差映射成气虚质阳虚质等多个可能体质的集合然后逐个去图谱验证哪一个有更丰富的关系路径取路径最多、关系最丰富的那一个来回答。这个思路在实测中效果很好没有完美的方案但用多候选 关系数量排序可以让系统在很多情况下都能蒙对用户的真实意图。6.2 大模型幻觉问题大模型幻觉是这类系统绕不开的坎。我第一次做完整测试的时候问系统孕妇能不能吃当归模型的回答是可以适量食用当归有补血活血的功效。如果对中医有点常识就知道这个回答是有问题的孕妇食用当归有活血风险一般建议慎用。排查之后发现问题是检索阶段没召回正确的知识片段。知识库里明明有当归活血孕妇慎用的内容但因为语义相似度不够高排在 top 5 之外没被召回。我的修复策略是多管齐下一是扩充同义词和近义表达。把孕妇和孕期妊娠期怀孕等词的向量距离拉近。具体做法是在这些文档切片中显式加入同义词让向量检索更容易命中。二是调整检索策略从纯向量检索改成向量 关键词混合检索。先按关键词精确匹配候选片段再跟向量检索结果做融合排序。这个思路跟 RAG 里常说的 Hybrid Search 一样虽然实现上多写一点代码但效果提升非常明显尤其是实体型问题因为实体名往往是高度特异的词。三是在 prompt 中增加一条硬规则如果检索到的知识片段之间有冲突必须明确告知用户资料中存在不同说法建议咨询专业中医师。这个规则让模型在无法判断时选择保守输出而不是强行给一个答案。6.3 多轮对话状态管理的坑系统一开始设计的时候没有考虑多轮对话但实测中发现用户天然会连续追问。比如先问阳虚体质怎么调理得到答案后再问那我可以吃西瓜吗这个问题本身没有实体阳虚质只有西瓜如果不带上文系统不知道用户是在问阳虚质能不能吃西瓜。我解决的方法是会话内存里保存最近五轮问答涉及的实体集合当当前问题实体识别不足时从历史实体里按问题相似度自动补齐。这个相似度我用的是最朴素的方法检查当前问题里有没有代词那它这以及语气词如果有就默认带上最近一轮的核心实体进行图谱查询。这一点在整个项目里是小细节但对体验的影响绝对不小。6.4 前端流式输出的黏包问题流式输出在本地都正常但部署到服务器之后用户反馈回答有时候会停顿很久然后突然跳出一大段字。排查发现这是 SSE 流解析问题。原因是 Nginx 默认会缓冲后端返回的数据导致流式响应没有逐字传到浏览器而是攒了一堆一起推送。解决方法是关闭 Nginx 对 SSE 接口的缓冲location /api/llm/chat { proxy_pass http://127.0.0.1:8000; proxy_buffering off; proxy_cache off; chunked_transfer_encoding on; proxy_http_version 1.1; proxy_set_header Connection ; }这个坑非常典型。如果你用了任何反向代理服务器都要确认它没有对 SSE 流做缓冲。另外如果服务器本身响应慢前端还可以配合AbortController做请求超时中断防止用户等太久。6.5 问题排查速查表根据这些调试经验我整理了一份排查速查表症状可能原因排查方法图谱答案为空实体未识别、查询路径不存在检查实体识别日志确认问题中实体是否被识别手工执行 Cypher 验证路径大模型答非所问RAG 检索召回不相关、prompt 约束不够打印检索命中的 top5 片段检查片段质量强化 system prompt大模型生成速度慢模型过大、显存不足、token 限制太长换更小的量化版模型减少 max_tokens检查显存占用前端不显示流式输出Nginx 缓冲、SSE 格式错误确认返回格式是data:开头关闭 Nginx 缓冲回答内容冲突知识图谱和知识库语料不一致检查图谱中三元组数据是否与源语料矛盾建立数据一致性审查流程问同一个问题每次答案不一样大模型温度参数太高把 temperature 调低到 0.3-0.5命中图谱的问题优先固定图谱答案服务启动后模型加载慢模型未预热启动时执行一次空请求完成模型预热6.6 一次完整的典型问答实录最后放一个完整的问答流程记录让大家直观感受一下系统到底跑成什么样。用户提问最近因为工作压力大晚上总是睡不着还容易口干舌燥喝了不少安神补脑液也没什么效果想知道中医会怎么看这个情况路由结果分析这个问题包含睡不着口干舌燥两个症状实体并且有明显的压力诱因描述。图谱查询路径比较弱实体识别命中后图谱只能返回失眠对应的穴位和食材但无法回答为什么没效果和整体怎么看所以路由判定走大模型兜底。RAG 检索到的关键片段包括失眠伴口干多见于阴虚火旺证常以滋阴降火为法阴虚体质人群常见症状有失眠、口干、五心烦热压力过大导致肝气郁结郁久化火暗耗阴液致阴虚火旺。大模型的最终回答是这样的您的情况在中医看来很可能属于阴虚火旺兼有肝郁。工作压力大长期思虑过度首先耗伤肝血导致肝气郁结郁结时间长了会化火这个火会进一步消耗体内阴液出现阴虚火旺于是就有了失眠和口干舌燥的表现。安神补脑液偏重于补益安神对于单纯的虚证有调理作用但您的情况里有火和郁光补不疏通效果确实会打折扣。建议可以试试以滋阴降火配合疏肝解郁的思路来调理日常可以适当食用百合、莲子、银耳这类清润滋阴的食材注意少熬夜、减少辛辣刺激。也可考虑按揉太冲穴配合三阴交来辅助缓解如果长期没有改善建议找专业中医师面诊辨证。这个回答结合了图谱和语料的信息又给了可操作的建议整体质量比纯大模型直接生成高了一个档次也比纯图谱回答丰满得多。7. 项目扩展与一些个人体会系统目前的基础功能已经完整跑通但回头再看可以继续深化的地方还不少。一是多轮对话的深度优化。当前会话状态只保存最近几轮的实体还没有真正意义上的对话记忆。后续如果要做成一个完整的养生顾问产品形态可以把多轮对话的上下文做成结构化状态把用户之前提到的所有体质信息、症状信息都沉淀下来并让图谱关系在这些历史实体上进行更深度的推理。二是知识更新通道。目前图谱数据是离线整理后一次性导入的后续如果遇到新的文献或新的养生知识没有一个便捷的更新管道。后续可以考虑做成人工审核 批量导入的半自动更新流程。三是大模型微调。RAG 方案已经很实用但如果想让回答的语言风格更贴近中医诊断的表述逻辑基于高质量的 QA 数据做一次 LoRA 微调会进一步提升回答质量。但这部分成本较高要根据实际需求评估收益。这个系统做完之后我感触最深的一点是知识图谱和大模型不是替代关系而是互补关系。知识图谱提供的是精确、可解释、可推理的结构化知识大模型提供的是灵活的语义理解和流畅的自然语言生成能力。当两者在一个系统里被设计成有序的协作关系时产出的效果远超任何一个单一路线。技术上并没有用到什么高深莫测的算法更多是把现有的工具合理地组合在一起并在工程细节上下足功夫。如果你也在做类似的领域问答系统我建议一开始就认真设计好路由策略——不要试图让某一个引擎解决所有问题。先梳理清楚你的用户会问哪些类型的问题然后把每个问题类型映射到最合适的回答路径上。这个设计做对了后面的开发会顺畅很多做反了你就会陷入反复调参和补丁的循环里。最后再分享一个实际开发中很容易被忽略的经验一定要从一开始就记录每一次问答的日志包括原始问题、路由决策、图谱命中情况、RAG 检索到的片段、最终回答内容、用户反馈。有了这批数据你才知道系统真实运行中哪些问题类型覆盖不足哪些地方频繁失败而不是凭感觉去优化。日志带来的判断支撑比任何主观经验都可靠。本文还有配套的精品资源点击获取
返回列表