前端手记(一):技术选型——在 AI 驱动的医疗系统中,前端该承担什么?
在我们团队的架构方案中,后端核心是一个基于 ReAct 框架的单智能体(Single-Agent),负责从自然语言主诉中提取药物实体,调用本地医药知识图谱(Graph RAG)进行冲突检测,最终输出结构化的决策结果。这套 AI 决策链路的设计已经相当完善。如果 AI 的决策结果无法被医生或患者准确感知,那整套系统的医疗价值就会大打折扣。在普通 Web 应用里,前端的职责通常是"展示后端给的数据"。
项目: 面向全场景用药安全的医师助手Agent
团队: ColdX · 山东大学软件学院2026年春季项目实训
个人分工: 前端开发 & 界面设计
目录
一、重新定义:这个项目里的前端不只是写界面
在我们团队的架构方案中,后端核心是一个基于 ReAct 框架的单智能体(Single-Agent),负责从自然语言主诉中提取药物实体,调用本地医药知识图谱(Graph RAG)进行冲突检测,最终输出结构化的决策结果。这套 AI 决策链路的设计已经相当完善。
但在着手前端设计时,我意识到一个容易被忽视的问题:如果 AI 的决策结果无法被医生或患者准确感知,那整套系统的医疗价值就会大打折扣。
在普通 Web 应用里,前端的职责通常是"展示后端给的数据"。但在这个项目中,前端需要承担更复杂的角色——它是整条决策链路的最后一环,是系统与人之间唯一的信任界面。这个判断直接影响了我对技术选型和架构设计的思路。
二、摸清挑战:前端要解决哪些问题
选型之前,先把前端真正需要面对的技术挑战梳理清楚。
挑战一:数据格式的不确定性
后端的 Agent 虽然通过 System Prompt 约束输出为 JSON,但由于 LLM 的生成特性,不同输入下返回的字段结构会存在细微差异——某些字段可能为空,某些嵌套层级可能不同。前端必须具备健壮的容错解析能力,不能简单地假设后端返回的格式总是固定的。
挑战二:UI 状态远比想象中复杂
后端 Agent 的决策结果有三种完全不同的业务路径:
blocked:检测到高危冲突,直接阻断,前端渲染红色警告面板并锁定操作needs_clarification:关键信息缺失,Agent 无法给出确定性判断,前端需要挂起当前请求,触发追问弹窗(Human-in-the-loop)fallback:信息缺失且用户无法提供,系统柔性降级,前端渲染一份高亮标注盲区的防御性免责医嘱
这三个状态之间还存在合法的流转路径——例如用户在追问弹窗中选择"无法提供信息"后,状态应当从 needs_clarification 流转到 fallback。这不是简单的 if/else 判断,而是一个需要精确建模的有限状态机(FSM)。
挑战三:界面设计要服务于医疗决策,而不是观感
用药安全场景对 UI 的容错率极低。一个颜色语义模糊的警告、一个可以被意外关闭的追问弹窗、一段措辞不严谨的免责声明,都可能导致信息被误读。这意味着前端的每一个设计决策都需要以"信息传递的准确性"为第一优先级。
三、技术选型:技术栈选择的理由
基于以上三个挑战,最终确定的技术栈为:Vue 3 (Composition API) + Vite + Pinia + Element Plus。
3.1 Vue 3 —— Composition API 解决复杂逻辑的组织问题
选 Vue 3 而非 React,核心原因在于 Composition API 对复杂业务逻辑的封装方式更契合这个项目。
Vue 3 允许将同一块业务逻辑封装为独立的 composable 函数,在多个组件中复用。在我们的系统中,状态机的流转规则是高频被引用的核心逻辑——主界面需要知道当前是否处于追问状态,追问弹窗需要知道用户确认后应当流转到哪里,降级组件需要判断是否应该渲染。
如果把这些判断散落在各个组件里,后期维护会非常混乱。Composition API 允许我把整个状态机封装成一个 useConsultationStateMachine() 函数,所有状态变量和转移逻辑集中在一处:
// src/composables/useConsultationStateMachine.js
import { ref, computed } from 'vue'
const STATES = {
IDLE: 'idle',
LOADING: 'loading',
BLOCKED: 'blocked',
NEEDS_CLARIFICATION: 'needs_clarification',
FALLBACK: 'fallback',
SUCCESS: 'success',
}
export function useConsultationStateMachine() {
const currentState = ref(STATES.IDLE)
// 状态转移表:明确定义哪些流转是合法的
const transitions = {
[STATES.LOADING]: {
blocked: STATES.BLOCKED,
needs_clarification: STATES.NEEDS_CLARIFICATION,
fallback: STATES.FALLBACK,
success: STATES.SUCCESS,
},
[STATES.NEEDS_CLARIFICATION]: {
retry: STATES.LOADING, // 用户补充信息后重新发起请求
give_up: STATES.FALLBACK, // 用户无法提供信息,触发降级
},
}
function transition(event) {
const next = transitions[currentState.value]?.[event]
if (next) {
currentState.value = next
} else {
console.warn(`[FSM] 非法流转: ${currentState.value} --[${event}]--> ?`)
}
}
const isBlocked = computed(() => currentState.value === STATES.BLOCKED)
const needsClarification = computed(() => currentState.value === STATES.NEEDS_CLARIFICATION)
const isFallback = computed(() => currentState.value === STATES.FALLBACK)
return { currentState, STATES, transition, isBlocked, needsClarification, isFallback }
}
这个 composable 在任何需要感知会诊状态的组件里直接 import 即可,不需要重复编写状态判断。
3.2 Pinia —— 管理 AI 的半结构化输出
Agent 返回的 JSON 数据需要在多个组件之间共享:主界面要渲染患者信息,风险警告组件要读取冲突列表,追问弹窗要知道 Agent 的提问内容,降级组件要拿到免责医嘱文本。Pinia 作为全局状态管理库,是承载这份数据的最合适位置。
相比 Vuex,Pinia 没有繁琐的 mutations,action 里可以直接修改 state,Store 也可以按业务自由拆分。我把会诊数据和 UI 状态分成两个独立的 Store,职责边界清晰:
// src/stores/consultation.js
import { defineStore } from 'pinia'
export const useConsultationStore = defineStore('consultation', {
state: () => ({
patientProfile: {}, // 解析后的患者标签
conflicts: [], // 药物冲突列表
backendStatus: null, // 驱动状态机的状态码
clarificationQuestion: null, // needs_clarification 时的追问内容
fallbackAdvice: null, // fallback 时的免责医嘱文本
}),
actions: {
processAgentResponse(data) {
this.backendStatus = data.status
this.conflicts = data.conflicts ?? []
this.patientProfile = data.patient_profile ?? {}
this.clarificationQuestion = data.clarification_question ?? null
this.fallbackAdvice = data.fallback_advice ?? null
},
reset() { this.$reset() },
},
})
注意 ?? [] 和 ?? {} 的防御性处理——这正是应对 LLM 输出不稳定性的地方,后端偶尔漏掉某个字段时前端不会直接崩掉。
3.3 Vite —— 开发效率
Vite 基于原生 ES Module,冷启动近乎秒级,HMR 也非常灵敏。在频繁调整 UI 细节、反复验证状态机流转效果的开发阶段,和传统 Webpack 相比体验差距很明显。
另外,Vite 的代理配置极简,本地开发时跨域问题一行解决:
// vite.config.js
server: {
proxy: {
'/api': {
target: 'http://localhost:8000', // FastAPI后端地址
changeOrigin: true,
},
},
},
3.4 Element Plus —— 把精力放在业务逻辑上
Element Plus 提供了完整的企业级组件(对话框、表单、警告提示等),完全覆盖本项目的 UI 需求。不需要从零手写基础组件,可以把设计精力全部投入到医疗场景特有的交互逻辑上。
四、工程目录设计
选型确定后,按照关注点分离原则规划了工程目录:
src/
├── api/ # 网络层:Axios 实例与所有接口封装
├── composables/ # 业务逻辑层:状态机、数据解析等 composable
├── stores/ # 状态层:Pinia Store(会诊数据 + UI 状态)
├── views/ # 页面层:医生端主界面、患者端界面
└── components/
├── RiskAlert/ # 风险警告组件(阻断 / 建议)
├── ClarificationModal/ # 追问弹窗
└── FallbackAdvice/ # 降级免责医嘱组件
各层之间边界严格:api/ 只管请求,不写业务判断;composables/ 只管逻辑,不碰 UI;stores/ 是唯一的数据来源;components/ 只管渲染。这套结构的好处在后续会逐渐体现——当后端接口字段调整时,只需改动 stores/ 中的解析逻辑,不需要逐个修改组件。
五、阶段小结与下一步计划
当前阶段已完成:
- Vue3 + Vite 项目初始化,代理配置完成
- Pinia Store 骨架搭建完毕,字段设计已对齐后端接口契约
- 状态机 composable 基础框架完成
- 工程目录结构确定,组件拆分边界已与团队对齐
前端技术蓝图已经落定。下一篇将记录 Axios 请求层的封装细节——包括拦截器设计、统一错误处理策略等。
更多推荐



所有评论(0)