项目: 面向全场景用药安全的医师助手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 请求层的封装细节——包括拦截器设计、统一错误处理策略等。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐