Vue2 + FastAPI + Dify 实现 AI 医疗预检分诊助手:从问诊追问到医生审核闭环
本文介绍了一个基于Vue2+FastAPI+Dify的AI医疗预检分诊助手项目。该系统通过AI收集患者症状信息并生成初步分诊建议,再由医生审核确认,形成完整的医疗问诊闭环。项目采用前后端分离架构,前端使用Vue2+ElementUI,后端采用FastAPI+MySQL,AI功能通过Dify工作流实现
本文适合刚学完 Vue、Python 后端和数据库,想做一个完整 AI 应用项目的同学。 项目仅用于学习演示,不能直接作为真实医疗诊断系统使用。
项目已经开源,源码地址: https://gitee.com/hn2004214/medical-consultation-system
这个地址就是项目的 Gitee 页面,读者可以打开查看源码,也可以通过 git clone 拉取到本地运行。
1. 这个项目解决什么问题
很多 AI 项目只做了一个聊天框:
用户输入一句话 -> 调用大模型 -> 返回一段话
这样能演示 AI 效果,但还不像一个真正的业务系统。我的理解是,AI 应用真正难的地方不是“调一次接口”,而是要处理用户、数据、流程、状态和人工审核。
医疗问诊场景更明显。患者说“我头疼”,医生还需要继续了解:疼多久了、有没有发热、有没有呕吐、有没有基础疾病、最近有没有外伤或感染。如果这些信息不完整,AI 直接给建议就不可靠。
所以这个项目的设计思路是:
AI 先追问症状 -> AI 整理预检分诊建议 -> 医生审核确认
重点不是让 AI 替代医生,而是让 AI 做前置信息收集和初步整理,医生负责最终判断。
2. 业务流程
项目分为患者端和医生端,主流程如下:
患者注册 / 登录 ↓ 填写健康档案 ↓ 创建问诊会话 ↓ 患者描述症状 ↓ AI 根据症状继续追问 ↓ 患者点击“生成分诊建议” ↓ AI 输出结构化分诊结果 ↓ 会话进入“待医生审核” ↓ 医生查看患者资料、聊天记录、AI 建议 ↓ 医生选择:通过 / 打回重诊 / 打回追问 ↓ 患者查看结果或继续补充信息
这里有三个关键角色:
| 角色 | 作用 |
|---|---|
| 患者 | 填写档案、描述症状、查看结果 |
| AI | 追问症状、生成预分诊建议 |
| 医生 | 审核 AI 建议,决定通过或打回 |
这个闭环是项目的核心。它比普通 AI 聊天多了一层“医生审核”,所以业务边界更清楚。
3. 技术栈与模块分工
项目技术栈如下:
| 模块 | 技术 | 负责什么 |
|---|---|---|
| 前端 | Vue2、Vite、Element UI、Axios | 页面展示、表单、接口请求 |
| 后端 | FastAPI、Pydantic、SQLAlchemy Async | 接口、业务逻辑、数据库操作 |
| 数据库 | MySQL | 保存用户、档案、会话、消息、审核记录 |
| 登录 | JWT | 区分当前操作用户 |
| AI | Dify Workflow | 追问症状、生成分诊建议 |
| 报告 | jsPDF、html2canvas | 导出审核后的报告 |
几个概念简单理解:
-
FastAPI:Python 写接口的框架。
-
Pydantic:校验前端传来的参数。
-
SQLAlchemy:用 Python 操作 MySQL。
-
JWT:登录后的身份凭证。
-
Dify Workflow:把 AI 流程拆成可配置节点。
前端不直接调用 Dify,而是走:
前端 -> 后端 -> Dify
这样 Dify Key 不会暴露在浏览器里。
4. 数据库和状态机
这个项目的核心表可以理解为五类:
| 表 | 作用 |
|---|---|
| users | 用户账号,包含患者和医生 |
| user_profiles | 患者健康档案 |
| conversations | 一次问诊会话 |
| messages | 会话里的每一条聊天消息 |
| admin_reviews | 医生审核记录 |
为什么要把 conversations 和 messages 分开?
因为一次问诊会有很多轮聊天。conversations 表示“一次问诊”,messages 表示“这次问诊里的一句话”。
会话状态是项目里很重要的业务控制:
class ConversationStatus(str, enum.Enum): ACTIVE = "active" # 进行中 PENDING_REVIEW = "pending_review" # 待审核 REVIEWED = "reviewed" # 已审核
三个状态的含义:
| 状态 | 含义 |
|---|---|
| active | 患者可以继续和 AI 对话 |
| pending_review | AI 已生成分诊建议,等待医生审核 |
| reviewed | 医生已经审核完成 |
没有状态控制,患者可能在医生审核时继续改症状,医生看到的数据就不稳定。这个状态机就是项目的业务保护线。
5. 患者发送消息:前端代码链路
前端接口封装在 frontend/src/api/triage.js:
export function sendMessage(data) {
return request.post('/triage/send-message', data);
}
患者聊天页会调用这个接口。核心逻辑可以简化成:
async sendToAi(content, targetNode) {
const resp = await triageApi.sendMessage({
conversation_id: this.conversation.id,
content,
...(targetNode ? { target_node: targetNode } : {}),
});
await this.loadConversation(this.conversation.id);
if (resp.is_final) {
this.$message.success('AI 已生成分诊建议,正在转交医生审核');
}
}
这里有两个关键字段:
-
conversation_id:告诉后端这条消息属于哪次问诊。 -
target_node:告诉后端这次走followup还是triage。
普通聊天默认走 followup,也就是继续追问。患者点击“生成分诊建议”时,前端会传:
await this.sendToAi(content, 'triage');
所以 target_node 可以理解成前端、后端、Dify 之间的流程开关。
6. 后端入口:FastAPI 路由和参数校验
前端请求会进入 backend/app/api/v1/triage.py:
@router.post(
"/send-message",
response_model=SendMessageResponse,
summary="发送消息(触发 AI 工作流)",
)
async def send_message(
payload: SendMessageRequest,
current_user: User = Depends(require_patient),
db: AsyncSession = Depends(get_async_db),
):
return await triage_service.send_message(db, current_user, payload)
这段代码只做三件事:
-
接收
/triage/send-message请求。 -
通过
require_patient确认当前用户是患者。 -
把业务交给
triage_service.send_message。
请求参数定义在 backend/app/schemas/triage.py:
class SendMessageRequest(BaseModel):
conversation_id: int = Field(..., gt=0, description="会话 ID")
content: str = Field(..., min_length=1, max_length=2000, description="患者消息内容")
target_node: Literal["triage", "followup"] | None = Field(default=None)
这里限制了:
-
conversation_id必须大于 0。 -
content不能为空,最长 2000 字。 -
target_node只能是triage或followup。
我的理解是,参数校验越靠前,后面的业务代码越干净。
如果有人传错参数,比如 target_node="abc" 或消息为空,请求会在进入业务逻辑前就被拦住。这样 triage_service 不需要再处理一堆脏数据,只专心写问诊业务。
7. 后端核心:triage_service.send_message
患者发送一条消息后,后端主流程在 backend/app/services/triage_service.py。
可以概括为:
查会话 -> 校验归属 -> 校验状态 -> 保存患者消息 -> 查询患者档案和历史消息 -> 调用 Dify -> 保存 AI 回复 -> 如果是最终分诊结果,更新为待审核
先看状态校验:
if conv.status == ConversationStatus.PENDING_REVIEW:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="会话正在等待医生审核,暂不可继续问诊",
)
if conv.status == ConversationStatus.REVIEWED:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该会话已审核完成,请创建新会话",
)
这保证了两件事:
-
等待医生审核时,患者不能继续改内容。
-
医生审核完成后,这次会话不能再被修改。
然后保存患者消息:
await message_repo.create(
db,
conversation_id=conv.id,
role="user",
content=payload.content,
)
接着准备传给 Dify 的上下文:
profile = await profile_repo.get_by_user_id(db, user.id)
profile_summary = build_profile_summary(profile)
history_msgs = await message_repo.list_by_conversation(db, conv.id)
history_payload = [
{"role": m.role, "content": m.content} for m in history_msgs
]
AI 问诊不是单轮问答,它需要知道患者档案和历史对话。否则患者前面已经回答过的问题,AI 后面可能又问一遍。
最后决定走哪个工作流节点:
target_node = payload.target_node or conv.current_node or "followup"
优先级是:
前端显式指定 > 会话当前节点 > 默认 followup
8. Dify 工作流:followup 和 triage
项目里 Dify 主要有两个分支:
| 节点 | 作用 |
|---|---|
| followup | 继续追问,补全症状信息 |
| triage | 综合已有信息,生成分诊建议 |
后端调用 Dify 的代码封装在 backend/app/services/dify_client.py。核心请求体是:
payload = {
"inputs": {
"user_message": user_message,
"conversation_id": str(conversation_id),
"patient_profile": patient_profile or "",
"target_node": target_node,
"history": json.dumps(history or [], ensure_ascii=False),
"doctor_reason": doctor_reason or "",
},
"response_mode": "blocking",
"user": f"patient-{conversation_id}",
}
注意这几个点:
-
inputs里的字段名必须和 Dify 开始节点变量一致。 -
target_node决定走followup还是triage。 -
history被转成 JSON 字符串传入工作流。 -
blocking表示后端等待 Dify 返回完整结果。
如果没有配置真实 Dify Key,项目会走 Mock 模式,方便先跑通前后端和数据库流程。
Dify 返回后,后端会整理成统一格式:
return {
"is_final": is_final,
"assistant_text": assistant_text,
"triage_payload": triage_payload,
}
三个字段的含义:
| 字段 | 含义 |
|---|---|
| is_final | 是否已经生成最终分诊建议 |
| assistant_text | 展示给患者看的 AI 回复 |
| triage_payload | 结构化分诊结果 |
如果是 followup 分支,后端会强制保持非最终态:
if target_node == "followup" and explicit_final is None:
is_final = False
这样可以避免追问节点误把会话推进到医生审核。
分诊节点最好让 Dify 输出固定 JSON。比如:
{
"recommended_dept": "神经内科",
"possible_causes": ["偏头痛", "上呼吸道感染相关头痛"],
"advice": "补水休息,监测体温和血压,症状加重及时线下就医。",
"urgency_level": "一般",
"is_final": true,
"answer": "已根据您的描述生成初步分诊建议。"
}
这样后端才能稳定保存 recommended_dept、urgency_level 和完整 triage_result。如果让 AI 自由输出一大段自然语言,页面展示和数据库保存都会变麻烦。
9. 生成分诊建议后,如何进入待审核
当 Dify 返回 is_final=True,后端会保存结构化分诊结果,并把会话状态改成 pending_review。
核心代码:
if dify_result["is_final"]:
raw_payload = dify_result["triage_payload"] or {}
triage_payload = TriageResultPayload(**raw_payload)
await conversation_repo.update_triage_result(
db,
conv,
recommended_dept=triage_payload.recommended_dept,
urgency_level=triage_payload.urgency_level,
triage_result=raw_payload,
)
await conversation_repo.update_status(
db,
conv,
status=ConversationStatus.PENDING_REVIEW,
current_node="triage",
)
这段代码完成两件事:
-
把
recommended_dept、urgency_level、完整triage_result保存到会话。 -
把状态从
active改成pending_review。
从这一步开始,患者不能继续修改当前问诊,直到医生审核或打回。
10. 医生审核闭环
医生审核页支持三种动作:
| 动作 | 含义 |
|---|---|
| approve | 审核通过 |
| reject_retriage | 打回重新分诊 |
| reject_followup | 打回继续追问 |
前端提交审核时,会传 review_type 和审核意见:
const payload = {
review_type: reviewType,
review_text: this.reviewForm.review_text,
};
if (
reviewType === 'reject_retriage' &&
this.reviewForm.regenerate_fields.length > 0
) {
payload.regenerate_fields = this.reviewForm.regenerate_fields;
}
await doctorApi.submitReview(this.detail.id, payload);
review_text 不只是备注,也可以作为医生给 AI 的修正意见。比如医生认为推荐科室不准确,就可以写明原因,让 AI 重新生成。
后端处理逻辑在 doctor_service.py:
if review_type == ReviewType.APPROVE:
await conversation_repo.update_status(
db, conv, status=ConversationStatus.REVIEWED
)
else:
target_node = (
"triage" if review_type == ReviewType.REJECT_RETRIAGE else "followup"
)
await conversation_repo.update_status(
db,
conv,
status=ConversationStatus.ACTIVE,
current_node=target_node,
)
await _retrigger_dify(
db,
conv=conv,
target_node=target_node,
doctor_reason=payload.review_text,
regenerate_fields=payload.regenerate_fields,
)
这段逻辑可以这样理解:
-
医生通过:会话变成
reviewed。 -
打回重诊:会话回到
active,Dify 走triage。 -
打回追问:会话回到
active,Dify 走followup。
如果医生只想让 AI 改某几个字段,前端会传 regenerate_fields。后端会把医生选择的字段拼进提示词,并在新结果回来后只合并这些字段。这个设计比“全部重来”更稳。
字段合并的思路可以简化成:
old_payload = dict(conv.triage_result or {})
merged = dict(old_payload)
for field in regenerate_fields:
if field in raw_payload:
merged[field] = raw_payload[field]
比如医生只勾选 recommended_dept,那后端只采纳 AI 新生成的推荐科室,其它字段继续保留原来的结果。这样能减少 AI 重新生成时带来的不稳定。
11. 前端如何配合状态变化
后端负责真正限制操作,前端负责把状态解释给用户。
患者端需要展示:
-
当前会话状态。
-
AI 聊天记录。
-
分诊建议。
-
医生审核意见。
-
是否还能继续输入。
当会话是 pending_review 时,页面应该提示“正在等待医生审核”,并禁用输入框。这样用户不会误以为系统卡住了。
医生端需要展示:
-
患者基本资料。
-
健康档案。
-
历史聊天记录。
-
AI 分诊结果。
-
审核操作按钮。
医生审核不能只看 AI 结论,还要能看到患者上下文,否则审核没有意义。
PDF 导出也应该放在 reviewed 之后,因为 pending_review 只是 AI 建议,还不是医生确认后的结果。
12. 本地运行步骤
先创建数据库:
CREATE DATABASE medical_consultation DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
启动后端:
cd backend python -m venv venv venv\Scripts\activate pip install -r requirements.txt copy .env.example .env uvicorn app.main:app --reload
.env 里重点配置:
DATABASE_URL=你的数据库连接 SECRET_KEY=你自己生成的随机字符串 DIFY_API_KEY=你的 Dify API Key DIFY_WORKFLOW_ID=你的 Dify Workflow ID
如果还没有配置 Dify,可以先用占位值走 Mock 模式。
启动前端:
cd frontend npm install npm run dev
如果要接真实 Dify,需要在 Dify 导入项目 DSL,绑定自己的模型,发布工作流,然后把 API Key 和 Workflow ID 写入后端 .env。
建议第一次运行按这个顺序测试:
1. 先启动 MySQL 和后端 2. 再启动前端 3. 注册患者账号并填写档案 4. 创建问诊会话,发送一条症状描述 5. 点击生成分诊建议 6. 登录医生端审核 7. 回到患者端查看审核结果
如果这条链路能走通,再去接真实 Dify。这样排查问题更简单,不会把前端、后端、数据库、AI 配置混在一起查。
13. 常见问题
后端连接不上数据库
先检查 MySQL 是否启动、数据库名是否是 medical_consultation、.env 中的连接字符串是否正确。
前端请求接口失败
先确认后端是否启动,再看浏览器控制台和后端终端日志。404 多半是接口地址问题,500 多半要看后端报错。
AI 没有真实回复
如果 Dify Key 还是占位符,项目会走 Mock 模式。要使用真实 AI,需要检查 DIFY_API_KEY、DIFY_WORKFLOW_ID、DIFY_BASE_URL,以及 Dify 工作流是否已发布。
生成分诊建议后不能继续聊天
这是正常设计。会话进入 pending_review 后,患者需要等待医生审核。医生打回追问后,状态才会回到 active。
Dify 输出解析失败
分诊节点最好要求 AI 输出固定 JSON,例如包含:
recommended_dept、possible_causes、advice、urgency_level、is_final、answer
AI 输出越规范,后端越容易稳定解析和保存。
14. 开源和医疗边界
这个项目已经开源到 Gitee:
https://gitee.com/hn2004214/medical-consultation-system
读者可以直接打开这个网站查看项目代码,也可以使用下面的命令拉取:
git clone https://gitee.com/hn2004214/medical-consultation-system.git
开源时不要提交:
-
.env -
数据库密码
-
Dify API Key
-
真实患者数据
-
venv -
node_modules -
构建产物
可以提交源码、Dify DSL 示例、README、SECURITY、LICENSE 和 .env.example。
另外要明确说明:这个项目是学习演示项目,不能直接用于真实医疗诊断。它的设计重点是“AI 辅助整理 + 医生审核”,不是“AI 自动看病”。
15. 总结
这个项目可以总结成四句话:
Vue2 负责页面和交互 FastAPI 负责接口和业务规则 MySQL 负责保存会话数据 Dify 负责 AI 追问和分诊建议
最核心的链路是:
患者发消息 -> 后端保存消息并调用 Dify -> Dify 根据 target_node 追问或分诊 -> is_final=True 时进入 pending_review -> 医生 approve / reject_retriage / reject_followup
我的理解是,AI 应用开发真正重要的能力,是把 AI 放进一个清晰、可控、能落地的业务系统里。这个项目不复杂,但前端、后端、数据库、AI 工作流和医生审核都串起来了,很适合作为 AI 应用入门练习。
更多推荐


所有评论(0)