【免费】LLM大模型 微信小程序AI智能医疗问诊平台系统(RAG+LangChain+Neo4j知识图谱+FastAPI+Vue3)多模态,支持文本,语音,图片,视频 ,AI大模型毕业设计
大家好,我是Java1234_小锋老师,分享一套锋哥原创的LLM大模型 微信小程序AI智能医疗问诊平台系统(RAG+LangChain+Neo4j知识图谱+FastAPI+Vue3)多模态,支持文本,语音,图片,视频 。

项目介绍
随着人工智能技术与移动互联网的深度融合,居民对便捷、低成本、可随时获取的健康咨询服务需求日益增长。传统线下问诊存在挂号排队时间长、优质医疗资源分布不均、轻症患者占用专家号源等突出问题;而现有的在线问诊产品多以人工图文咨询为主,响应依赖医生在线时间,且普遍只支持纯文本描述,难以处理皮疹、患处、检查单等视觉信息,也无法为老年用户提供语音输入通道。如何借助大语言模型的语义理解与多模态感知能力,构建一个既具备专业知识约束、又可随时响应的智能问诊平台,成为智慧医疗领域值得研究的课题。
本文设计并实现了一个微信小程序 AI 智能医疗问诊平台系统。系统采用前后端分离架构,以微信原生小程序作为患者端载体,以 Vue3 配合 Element Plus、ECharts 构建管理员后台与医生工作台,后端使用 Python 语言与 FastAPI 框架提供 RESTful 接口及 SSE 流式通道,数据层由 MySQL 关系数据库、Chroma 向量数据库与 Neo4j 图数据库共同承担。在智能问诊内核方面,系统以 LangChain 作为大模型应用编排框架,通过通义千问 qwen3.5-omni-plus 全模态大模型实现对文本、图片、视频、语音四类输入的统一理解;同时构建了检索增强生成(RAG)与知识图谱推理双通道:一方面将医学文档经递归字符切分后由 text-embedding-v4 模型转换为 2048 维向量存入 Chroma,在问诊时召回最相关的知识片段;另一方面在 Neo4j 中建立以疾病为中心的症状、科室、药品、检查、饮食等关联网络,通过 Cypher 语句依据症状共现度推理候选疾病与推荐科室。两路检索结果经提示模板拼装为上下文后交由大模型生成回答,有效抑制了大模型的事实性幻觉,并使每一条结论都可回溯到具体知识来源。
系统共设计 14 张数据表,实现了用户注册登录、AI 多模态问诊、症状自查推理、预约挂号、在线人工咨询、健康档案、健康资讯、个人中心,以及后台数据统计、用户与医疗资源管理、知识库管理、知识图谱可视化、问诊记录监管等功能模块。经功能测试与性能测试验证,22 项功能用例全部通过,AI 问诊首字返回时间约 1.8 秒,知识检索与图谱推理平均耗时分别为 313 毫秒与 97 毫秒,系统运行稳定、交互流畅,达到了预期设计目标,对基于大模型的智慧医疗应用具有一定的参考价值。
源码下载
链接: https://pan.baidu.com/s/1sdArTFu7_YNwouyyYqy1rg?pwd=1234
提取码: 1234
系统展示












核心代码
"""认证服务"""
from fastapi import HTTPException
from sqlalchemy.orm import Session
from core.security import verify_password, create_access_token
from models.admin import Admin
from models.doctor import Doctor
from models.user import User
from schemas.common import LoginRequest, RegisterRequest, TokenResponse
class AuthService:
"""认证业务逻辑"""
@staticmethod
def login(db: Session, req: LoginRequest) -> TokenResponse:
"""
三角色登录
:param req: 登录请求
:return: 令牌响应
"""
if req.role == "admin":
user = db.query(Admin).filter(Admin.username == req.username).first()
nickname = user.nickname if user else None
elif req.role == "doctor":
user = db.query(Doctor).filter(Doctor.username == req.username).first()
nickname = user.real_name if user else None
elif req.role == "user":
user = db.query(User).filter(User.username == req.username).first()
nickname = user.real_name if user else None
else:
raise HTTPException(status_code=400, detail="无效的角色类型")
if not user or not verify_password(req.password, user.password):
raise HTTPException(status_code=401, detail="用户名或密码错误")
if hasattr(user, "status") and user.status == 0:
raise HTTPException(status_code=403, detail="账号已被禁用")
token = create_access_token({
"sub": user.username,
"user_id": user.id,
"role": req.role,
})
return TokenResponse(
access_token=token,
role=req.role,
user_id=user.id,
username=user.username,
nickname=nickname,
avatar=getattr(user, "avatar", None),
)
@staticmethod
def register(db: Session, req: RegisterRequest) -> TokenResponse:
"""
患者注册(含确认密码校验)
"""
if req.password != req.confirm_password:
raise HTTPException(status_code=400, detail="两次密码输入不一致")
existing = db.query(User).filter(User.username == req.username).first()
if existing:
raise HTTPException(status_code=400, detail="用户名已存在")
user = User(
username=req.username,
password=req.password,
real_name=req.real_name,
phone=req.phone,
gender=1,
)
db.add(user)
db.commit()
db.refresh(user)
token = create_access_token({"sub": user.username, "user_id": user.id, "role": "user"})
return TokenResponse(
access_token=token,
role="user",
user_id=user.id,
username=user.username,
nickname=user.real_name,
)
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
import { formatDateTime, parseListData } from '@/utils/format'
const list = ref([])
const loading = ref(false)
const keyword = ref('')
const statusFilter = ref('')
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
/** 加载咨询列表 */
async function loadList() {
loading.value = true
try {
const params = {
page: page.value,
page_size: pageSize.value,
keyword: keyword.value.trim(),
}
if (statusFilter.value !== '') {
params.status = statusFilter.value
}
const res = await request.get('/consult/admin/list', { params })
list.value = parseListData(res)
total.value = res.data?.total ?? list.value.length
} catch {
list.value = []
total.value = 0
} finally {
loading.value = false
}
}
/** 搜索咨询 */
function handleSearch() {
page.value = 1
loadList()
}
/** 重置搜索 */
function handleReset() {
keyword.value = ''
statusFilter.value = ''
page.value = 1
loadList()
}
/** 分页切换 */
function handlePageChange(p) {
page.value = p
loadList()
}
/** 删除咨询 */
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
`确定删除该咨询记录吗?患者:${row.user_name || '未知'},主诉:${row.chief_complaint || ''}`,
'提示',
{ type: 'warning' },
)
await request.delete(`/consult/admin/${row.id}`)
ElMessage.success('删除成功')
if (list.value.length === 1 && page.value > 1) {
page.value -= 1
}
loadList()
} catch { /* */ }
}
onMounted(loadList)
</script>
<template>
<div>
<h2 class="page-title">咨询管理</h2>
<div class="modern-card">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="keyword"
placeholder="搜索主诉 / 患者 / 医生"
clearable
style="width: 280px"
@keyup.enter="handleSearch"
/>
<el-select
v-model="statusFilter"
placeholder="状态筛选"
clearable
style="width: 140px"
>
<el-option label="待回复" :value="0" />
<el-option label="已回复" :value="1" />
</el-select>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<el-table :data="list" v-loading="loading" class="adaptive-table" stripe>
<el-table-column prop="id" label="ID" min-width="60" />
<el-table-column prop="chief_complaint" label="主诉" min-width="200" show-overflow-tooltip />
<el-table-column prop="user_name" label="患者" min-width="100" />
<el-table-column prop="doctor_name" label="医生" min-width="100" />
<el-table-column prop="status" label="状态" min-width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'warning'" size="small">
{{ row.status === 1 ? '已回复' : '待回复' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="时间" min-width="170">
<template #default="{ row }">{{ formatDateTime(row.create_time || row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" min-width="100" fixed="right">
<template #default="{ row }">
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && !list.length" description="暂无咨询数据" />
<!-- 分页 -->
<div v-if="total > 0" class="pagination-wrap">
<el-pagination
v-model:current-page="page"
:page-size="pageSize"
:total="total"
layout="total, prev, pager, next"
background
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</template>
<style scoped>
.search-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.pagination-wrap {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>
更多推荐


所有评论(0)