一、项目介绍
1、概述
-
本项目基于 Neo4j 图数据库构建知识图谱与向量索引,实现了一个前沿的检索增强生成 RAG 系统,用于提升大语言模型在私有知识库问答场景中的准确性、逻辑推理能力与可控性
-
项目核心流程包括:文档加载与清洗、实体与关系抽取(IE)、文本分块(Chunking)与向量化嵌入(Embedding)、知识图谱构建与图结构存储。在用户提问时,系统不仅利用向量进行语义相似度检索,更结合 Neo4j 的图关联查询能力,精准捕捉核心实体及其外延的关联知识,融合形成多维度的上下文。最后,将这些富含拓扑关系的知识片段与问题一同输入大语言模型,从而生成基于事实和强逻辑关系的高质量回答
-
在模型层面,项目选用通义千问(Qwen)大模型,通过引入 Graph RAG 架构,深度融合结构化知识与非结构化文本,有效缓解了大模型的“幻觉”问题,显著提升了跨文档复杂关联问答的准确性与可追溯性
-
项目最终实现了一个可用于复杂知识库问答、企业全域文档检索与智能客服的、具备深度推理能力的轻量级 Graph RAG 系统
2、技术栈
- Python 基础语法
- PyTorch 深度学习库
- Transformer 架构
- LangChain 框架
- Neo4j 图数据库
- FastAPI 后端服务框架
- Vue-cli 前端脚手架
- MySQL 关系型数据库
二、项目开发
1、知识库搭建
-
知识库的搭建非常重要,知识库的质量直接影响 RAG 系统检索效果
-
知识库的搭建属于离线行为,即在我们构建 RAG 系统的客户端和服务器之前,就应该把知识库构建好
-
以医疗数据集为案例演示:
-
数据集以及解析后存储代码:【F:\workspace\AI\neo4j_rag\feifan_rag_server\create_data\datasets\构建节点和关系-写入数据.py】
-
测试:
import os
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnableMap, RunnablePassthrough
from langchain_neo4j import Neo4jGraph
from langchain_openai import ChatOpenAI
from langchain_neo4j import GraphCypherQAChain
# 初始化 Neo4j 连接
graph = Neo4jGraph(
url="bolt://127.0.0.1:7687",
username="neo4j",
password="rootroot",
database="neo4j"
)
# 初始化 LLM
llm = ChatOpenAI(
model="qwen3.6-27b",
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
# 构建 GraphCypherQAChain
chain = GraphCypherQAChain.from_llm(
llm=llm,
graph=graph,
allow_dangerous_requests=True,
verbose=True,
top_k=10,
validate_cypher=True
)
# 问题
questions = [
"感冒有哪些常见症状?",
"感冒的疾病详情是什么?",
"咳嗽症状对应哪些疾病?"
]
answers = []
for q in questions:
result = chain.invoke({"query": q})
answers.append(f"问题:{q}\n回答:{result['result']}")
# 用 LLM 做综合总结
prompt = PromptTemplate(
input_variables=["answers"],
template="""
请将以下问答进行综合归纳,生成一段完整、连贯的中文回答:
{answers}
"""
)
# 构建管道
summary_pipeline = (
RunnableMap({
"answers": RunnablePassthrough() # 直接传入 answers
})
| prompt # 将 answers 放入模板
| llm # LLM 生成文本
| StrOutputParser() # 输出为字符串
)
# 执行
final_summary = summary_pipeline.invoke({
"answers": "\n\n".join(answers)
})
print("综合回答:\n", final_summary)
- 输出结果
综合回答:
感冒是一种常见呼吸道疾病,其典型症状主要包括发烧、鼻塞、发热伴寒战、头痛、全身忽冷忽热、咽痛、咽喉干燥及灼热感以及流鼻涕等。从疾病详情来看,感冒主要分为普通感冒与流行性感冒两大类:普通感冒(中医称“伤风”)多由鼻病毒等多种病毒引起(约占30%–50%),而流感主要由流感病毒导致,传染性强且病毒易变异,即使曾患病仍可能再次感染。约70%–80%的感冒病例由病毒引发,细菌感染多为继发;受凉、淋雨、过度疲劳等因素会降低机体或呼吸道局部防御功能,老幼体弱或患有慢性呼吸道疾病者尤为易感。该病主要通过呼吸道飞沫或接触被污染物品传播,全年皆可发病,冬春季节高发;因病毒种类多、人体免疫力短暂且无交叉免疫,加之健康人群中存在隐性携带者,一年内可能多次发病。一般病程为7至14天,治愈率高达97%。在日常预防方面,建议坚持冷水洗脸与热水泡脚、早晚及餐后淡盐水漱口,可辅助饮用生姜红糖水或萝卜醋汤;同时应注意补充维生素C和E,保证每日8小时充足睡眠以维持免疫水平,并经常进行鼻部按摩,预防鼻咽部初始感染。如需前往三甲医院就诊,治疗费用通常在500至1000元左右,该病一般不在医保报销范围内。此外,咳嗽作为感冒常见的伴随症状,若持续不缓解、加重或性质改变,需警惕其可能提示的其他疾病,包括肺炎、支气管炎、肺结核、哮喘、禽流感等呼吸系统疾病,以及粒细胞缺乏症、气管肿瘤、膀胱移行细胞癌、睾丸肿瘤或药物性哮喘等。若症状复杂、迁延不愈或伴有其他异常表现,建议及时就医进行专业评估与针对性治疗。
2、服务器开发
2.1 搭建服务器项目
- 创建一个 FastAPI 项目
2.2 聊天接口配置
- 创建一个聊天接口子路由,功能代码先不写,先实现模板构建
- 在项目根目录下创建
api/controller文件夹,然后创建文件ChatController.py
from fastapi import APIRouter, Request
# 创建路由对象
chat_router = APIRouter()
# 聊天接口
@chat_router.get("/chat")
def chat(request: Request,question: str):
pass
main.py中配置跨域、注册子路由、配置启动
import uvicorn
from fastapi import FastAPI
from api.controller.ChatController import chat_router
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# 跨域
app.add_middleware(
CORSMiddleware,
allow_origins=["http://127.0.0.1:8000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册子路由
app.include_router(chat_router)
# 启动服务
if __name__ == '__main__':
uvicorn.run(
app="main:app",
host="127.0.0.1",
port=8080,
reload=True,
)
2.3 工具封装
- 在项目根目录下创建
api/utils文件夹,然后创建文件Neo4jUtil.py,封装一个获取 Neo4j 连接的工具
import os
from langchain_neo4j import Neo4jGraph
def get_neo4j_conn():
return Neo4jGraph(
url=os.getenv("NEO4J_URL", "bolt://127.0.0.1:7687"),
username=os.getenv("NEO4J_USERNAME", "neo4j"),
password=os.getenv("NEO4J_PASSWORD", "rootroot"),
database=os.getenv("NEO4J_DATABASE", "neo4j"),
)
if __name__ == '__main__':
print(get_neo4j_conn())
- 在项目根目录下创建
api/utils文件夹,然后创建文件MyModel.py,封装一个加载模型的工具
import os
from langchain_openai import ChatOpenAI
def load_model():
return ChatOpenAI(
model="qwen3.6-27b",
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
2.4 聊天接口实现
-
主要采取 MVC 分层思想,在业务逻辑层完成对应的聊天功能代码处理
-
在项目根目录下创建
api/service文件夹,然后创建文件ChatService.py
from api.utils.Neo4jUtil import get_neo4j_conn
from api.utils.MyModel import load_model
from langchain_neo4j import GraphCypherQAChain
from langchain_core.prompts import PromptTemplate
CYPHER_GENERATION_TEMPLATE = """Task:Generate Cypher statement to query a graph database.
Instructions:
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided.
Schema:
{schema}
# 节点标签及含义
| 节点标签 | 含义 |
|--------------|------------------|
| Disease | 疾病(核心节点) |
| Symptom | 症状 |
| Check | 检查项目 |
| Cureway | 治疗方式 |
| Drug | 药物 |
| Department | 就诊科室 |
| Food | 食物 |
| Dishes | 菜肴 |
| Category | 疾病分类 |
# 关系类型及含义
| 关系类型 | 含义 | 起始节点 | 目标节点 |
|----------------------|-------------------|----------|-------------|
| DISEASE_SYMPTOM | 疾病症状 | Disease | Symptom |
| DISEASE_CHECK | 相关检查项目 | Disease | Check |
| DISEASE_CUREWAY | 治疗方式 | Disease | Cureway |
| DISEASE_DRUG | 治疗或相关药物 | Disease | Drug |
| DISEASE_DEPARTMENT | 就诊科室 | Disease | Department |
| DISEASE_DO_EAT | 推荐进食的食物 | Disease | Food |
| DISEASE_NOT_EAT | 不推荐进食的食物 | Disease | Food |
| DISEASE_DISHES | 适合疾病的菜肴 | Disease | Dishes |
| DISEASE_ACOMPANY | 并发症 / 伴随疾病 | Disease | Disease |
| DISEASE_CATEGORY | 疾病所属类别 | Disease | Category |
# 查询示例
# 问:高血压有哪些症状?
MATCH (d:Disease {{name:"高血压"}})-[:DISEASE_SYMPTOM]->(s:Symptom) RETURN s.name AS symptom
# 问:感冒吃什么药?
MATCH (d:Disease {{name:"感冒"}})-[:DISEASE_DRUG]->(dr:Drug) RETURN dr.name AS drug
# 问:糖尿病不宜吃什么?
MATCH (d:Disease {{name:"糖尿病"}})-[:DISEASE_NOT_EAT]->(f:Food) RETURN f.name AS food
# 问:肺炎需要做什么检查?
MATCH (d:Disease {{name:"肺炎"}})-[:DISEASE_CHECK]->(c:Check) RETURN c.name AS check_item
# 问:高血压挂什么科?
MATCH (d:Disease {{name:"高血压"}})-[:DISEASE_DEPARTMENT]->(dep:Department) RETURN dep.name AS department
# 问:感冒的并发症有哪些?
MATCH (d:Disease {{name:"感冒"}})-[:DISEASE_ACOMPANY]->(a:Disease) RETURN a.name AS complication
# 问:糖尿病属于哪类疾病?
MATCH (d:Disease {{name:"糖尿病"}})-[:DISEASE_CATEGORY]->(c:Category) RETURN c.name AS category
# 问:高血压可以吃什么菜?
MATCH (d:Disease {{name:"高血压"}})-[:DISEASE_DISHES]->(dishes:Dishes) RETURN dishes.name AS dishes
# 问:哪些疾病会有头痛症状?
MATCH (d:Disease)-[:DISEASE_SYMPTOM]->(s:Symptom {{name:"头痛"}}) RETURN d.name AS disease
Note: Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything else than for you to construct a Cypher statement.
Do not include any text except the generated Cypher statement.
The question is:
{question}"""
QA_TEMPLATE = """你是一名专业的医疗智能问答助手,基于 Neo4j 疾病知识图谱为用户提供准确的健康咨询。
# 第一步:意图识别
判断用户问题是否属于【医疗疾病类】,包括但不限于:
- 疾病症状、病因、并发症
- 检查项目、就诊科室
- 治疗方式、用药建议
- 饮食宜忌、推荐菜肴
- 疾病分类与归属
# 第二步:按类别处理
## 情形 1:属于医疗疾病类 → 基于以下图谱查询结果作答
- 严格基于查询结果作答,不得编造疾病、药物或诊疗方案
- 若查询结果为空,回复:"知识库中未收录该疾病的相关信息,建议咨询专业医生"
## 情形 2:不属于医疗疾病类
- 忽略知识图谱查询结果
- 基于自身通用知识自然作答
- 回复中不得出现"知识库""图谱""上下文"等字样
# 输出要求
- 直接给出最终答案,不复述问题、不解释判断过程
- 涉及用药、治疗、剂量等敏感内容时,附加一句:"具体方案请遵医嘱"
- 语言简洁、准确、通俗易懂
---
【图谱查询结果】
{context}
【用户问题】
{question}
【回答】
"""
def chat(question: str):
graph = get_neo4j_conn()
llm = load_model()
cypher_prompt = PromptTemplate(
input_variables=["schema", "question"],
template=CYPHER_GENERATION_TEMPLATE,
)
qa_prompt = PromptTemplate(
input_variables=["context", "question"],
template=QA_TEMPLATE,
)
rag_chain = GraphCypherQAChain.from_llm(
llm=llm,
graph=graph,
cypher_prompt=cypher_prompt,
qa_prompt=qa_prompt,
verbose=True,
allow_dangerous_requests=True,
)
return rag_chain.invoke({"query": question})
- 修改
api/controller/ChatController.py代码
import json
import time
from fastapi import APIRouter, Request
from starlette.responses import StreamingResponse
from api.service import ChatService
chat_router = APIRouter()
@chat_router.get("/chat")
def chat(request: Request, question: str):
def streaming_content():
try:
result = ChatService.chat(question)
answer = result.get("result", "") if isinstance(result, dict) else str(result)
for ch in answer:
payload = json.dumps({"content": ch}, ensure_ascii=False)
yield f"data: {payload}\n\n"
time.sleep(0.02)
yield "data: [DONE]\n\n"
except Exception as e:
err = json.dumps({"error": str(e)}, ensure_ascii=False)
yield f"data: {err}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
content=streaming_content(),
media_type="text/event-stream",
)
2.5 测试
- 这里的返回值没解析,是正确的

3、客户端开发
3.1 搭建客户端项目
- 创建以 vue-cli 项目
- 下载以下库
"axios": "^1.3.3",
"dompurify": "^3.3.3",
"element-ui": "^2.15.14",
"marked": "^3.0.8",
3.2 对话页面设计
- 套模板或者 AI 基于 element-ui 生成一个
<template>
<div id="chat-container">
<!-- 消息区 -->
<div id="messages">
<div
v-for="(msg, index) in messages"
:key="index"
class="message"
:class="msg.role"
>
<div class="avatar">
{{ msg.role === 'assistant' ? '🤖' : '👤' }}
</div>
<div class="content">
<div
class="text-message"
v-html="$renderMarkdown(msg.content)">
</div>
</div>
</div>
</div>
<!-- 输入区 -->
<div id="input-area">
<div class="input-wrapper">
<el-input
type="textarea"
v-model="input"
:rows="2"
placeholder="请输入你的问题..."
@keydown.enter.native.prevent="sendMessage"
>
</el-input>
<el-button
type="primary"
@click="sendMessage">
发送
</el-button>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
export default {
name: "Index",
data() {
return {
serverUrl: "http://127.0.0.1:8080/",
input: "",
messages: [
{
role: "assistant",
content: "你好 👋,我是 AI 助手"
}
]
};
},
methods: {
// 滚动到底部
scrollToBottom() {
this.$nextTick(() => {
const box = document.getElementById("messages");
box.scrollTop = box.scrollHeight;
});
},
// 发送消息
sendMessage() {
if (this.input.trim() === "") return;
const userMsg = this.input.trim();
this.input = "";
// 用户消息
this.messages.push({
role: "user",
content: userMsg
});
// AI占位消息
this.messages.push({
role: "assistant",
content: "AI 正在思考..."
});
this.scrollToBottom();
const index = this.messages.length - 1;
let aiContent = "";
let finished = false;
// SSE请求
const params = new URLSearchParams({
question: userMsg
});
const es = new EventSource(
this.serverUrl + `chat?${params}`
);
let lastContent = "";
es.onmessage = e => {
if (e.data === "[DONE]") {
finished = true
es.close()
return
}
let data = JSON.parse(e.data)
if (data.content == "\n" && lastContent == "\n") {
return
}
aiContent += data.content;
Vue.set(this.messages, index, {
"role": "assistant",
"type": "text",
"content": aiContent
})
this.$nextTick(() => {
this.scrollToBottom()
})
lastContent = data.content;
}
es.onerror = () => {
if (finished) return;
es.close();
Vue.set(this.messages, index, {
role: "assistant",
content: aiContent || "连接中断,请重试"
});
};
}
}
};
</script>
<style scoped>
/* ===== 页面 ===== */
body {
margin: 0;
background: #f5f7fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
}
#chat-container {
width: 900px;
max-width: 95%;
height: 90vh;
margin: 5vh auto;
background: white;
border-radius: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}
/* ===== 消息区 ===== */
#messages {
flex: 1;
overflow-y: auto;
padding: 24px;
background: #f7f8fa;
}
/* ===== 消息 ===== */
.message {
display: flex;
gap: 10px;
max-width: 75%;
margin-bottom: 18px;
padding: 12px 16px;
border-radius: 14px;
line-height: 1.6;
font-size: 14px;
}
.message.user {
margin-left: auto;
background: #409EFF;
color: white;
flex-direction: row-reverse;
}
.message.assistant {
background: white;
border: 1px solid #ebeef5;
}
/* ===== 头像 ===== */
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: white;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* ===== 内容 ===== */
.content {
flex: 1;
}
.text-message {
white-space: pre-wrap;
word-break: break-word;
}
/* ===== 输入区 ===== */
#input-area {
padding: 16px;
border-top: 1px solid #ebeef5;
background: white;
}
.input-wrapper {
display: flex;
align-items: flex-end;
}
.el-input {
flex: 1;
margin-right: 10px;
}
.el-button {
height: 52px;
}
</style>
3.3 测试

浙公网安备 33010602011771号