山东大学软件学院项目实训-基于语言大模型的智能居家养老健康守护系统-个人博客(九)
一、 项目背景与核心痛点
在开发这款面向养老健康场景的 AI 陪伴小程序时,我们面临的绝不是一个简单的“一问一答”玩具,而是一个需要处理复杂医疗上下文的多 Agent 对话系统。
真实场景中,用户(老人或家属)会连续追问:
-
老人:“最近血压有点高怎么办?” -> AI 给出建议 -> 老人:“那这个药还能吃吗?”(涉及多轮上下文中的指代问题)
-
家属:“我爸最近身体怎么样?”(需要严格的权限校验和后台数据聚合分析)
因此,后端的改造核心集中在对话历史上下文的管理、RAG(检索增强生成)与个人健康数据的注入,以及多个不同人设 Agent 的权限与状态隔离。同时,系统还包含个人资料维护、用药定时提醒等基础业务模块。
二、 总体架构与技术选型
整个 AI 调用链路概括为:
微信小程序 -> Controller -> Service 解析/创建会话 -> 聚合用户健康数据(DB) + 检索向量知识(RAG) -> 组装 System Prompt 与历史消息 -> 调用 DeepSeek -> 追加回复并保存 JSON 文件 -> 响应流(SSE)或普通文本
核心技术栈:
-
后端框架:Spring Boot 3.2 + MyBatis-Plus
-
AI 对话模型:DeepSeek Chat API(OpenAI 兼容接口,性价比高,temperature 根据 Agent 灵活调配)
-
向量框架与存储:Spring AI + PostgreSQL (pgvector)
-
对话存储:关系型数据库存储会话元数据 + 本地 JSON 文件存储完整消息正文
-
通信协议:RESTful API + SSE (Server-Sent Events) 流式推送
三、 对话历史记录:基于 JSON 文件的轻量级存储
为了让大模型理解“这个”、“刚才”等指代,必须引入统一的 sessionId 会话管理。
3.1 会话元数据表与消息文件分离
在存储选型上,我们坚持了元数据存 DB,消息正文存文件的策略:
-
chat_session 表:只保存 session_id、agent_type、elderly_id 等元数据,用于列表展示和权限过滤。
-
JSON 消息文件:将完整的消息数组 [{"role":"user", "content":"..."}] 序列化为 {sessionId}.json 文件保存到服务器。
这样做的好处是:数据库查询极轻,读写大段长文本直接走文件 I/O,结构简单。
3.2 填坑:硬编码目录导致的删除 Bug
在开发中发现一个 Bug:调用 DELETE /sessions/{sessionId} 返回成功,但列表里会话还在。
根因:早期只有 2 个 Agent,找文件时硬编码了 String[] subDirs = {"medication-safety", "companion"}。后来新增了家属辅诊等 Agent 却没有更新数组,导致找不到文件静默失败。
修复(坚持文件存储方案):为了保持文件存储的轻量优势,我重构了文件定位逻辑,取消了硬编码。改为直接读取会话表中的 agent_type,动态拼接路径 /storage-dir/{agentType}/{sessionId}.json,彻底解决了扩展 Agent 时的删除与读取问题。
3.3 流式接口 (SSE) 的会话持久化挑战
普通接口请求结束后直接保存会话即可,但情感陪伴 Agent需要流式逐字输出。
解决思路是:使用 StringBuilder 在后台默默收集完整回复。
-
流开始前:解析会话,将 user 消息写入 session。
-
流进行中:逐字向前端发送 SseEmitter.event(),同时用 StringBuilder.append() 收集片段。
-
流结束后([DONE]):将完整的 assistant 消息追加到 session 并覆写 JSON 文件。
(注:为解决并发追问导致的消息乱序,我们还在 Service 中对同一 sessionId 加了轻量级锁,确保追加串行化)
四、 让 AI 变聪明:上下文聚合与 RAG 实现
普通的 AI 不知道老人的病史和用药,这是不可接受的。我们需要把系统内的数据“喂”给大模型。
4.1 结构化数据的自动化注入 (ElderlyContext)
以用药安全审核 Agent为例,最初需要老人手填当前用药,体验极差。改造后,系统会自动从五张表聚合数据:
elderly(基本信息) + disease(疾病) + medication_plan(用药计划) + medical_record(病历) + health_data(体征)
关键经验:用自然语言替换 JSON 注入 Prompt
我们将聚合的数据转为如 【当前用药】降压药A,剂量:10mg/次(用于治疗:高血压) 这样的纯文本,拼接到 System Prompt 中。实测发现,大模型对中文自然语言结构(带【标签】)的理解和推理准确率,远高于直接塞入 JSON 字符串。同时,这允许我们将 temperature 调至极低的 0.3,保证医疗回答的严谨性。
4.2 双轨 RAG (检索增强生成)
对于非结构化的长文本,我们引入了 pgvector 向量检索:
-
病历 RAG:将长病历按 500 字分块入库,检索时严格带上 filterExpression("elderlyId == '...'"),确保不跨区泄露数据。
-
知识库 RAG:按 Agent 类型隔离。支持带上下文的检索:如果用户问“那这个要长期坚持吗?”,系统会先将历史消息中的医学关键词(血压、用药等)提取并拼接到 Query 中,再做向量召回,完美解决了多轮指代带来的检索失效问题。
五、 特殊 Agent 设计:家属辅诊的越权防御
家属辅诊 Agent 是唯一涉及跨用户数据访问的模块(家属看老人的数据),安全是第一位。
-
Service 层绝对拦截:我们在 FamilyAssistServiceImpl 中加入了双重绑定关系校验(查 elderly_family 表的 bindStatus)。这层校验放在 Service 而非 Controller,防止任何内部调用绕过。
-
SSE 的前置校验:曾踩坑把权限校验放在了异步线程里,导致 BusinessException 无法被全局处理器捕获。修复方案是:必须在建立 SseEmitter 之前(主线程)完成鉴权,失败则直接抛 403。
六、 支撑大模型的业务基石
除了 AI 核心链路,保障系统可用性的还有常规业务模块的设计。
6.1 个人资料编辑与 OSS 直传
老人和家属账户共享 /api/auth/profile 接口。
-
安全设计:DTO 仅允许传入 name 和 avatarUrl,后端通过 JWT 自动识别当前身份去更新表,防篡改。
-
文件存储坑点:处理用户头像上传时,使用了 Files.copy 和绝对路径流式写入服务器目录。这修复了使用 Spring MultipartFile.transferTo() 配合相对路径时偶发的 FileNotFoundException 异常。
更多推荐



所有评论(0)