news 2026/4/20 8:54:09

山东大学软件学院项目实训-基于语言大模型的智能居家养老健康守护系统-个人博客(二)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
山东大学软件学院项目实训-基于语言大模型的智能居家养老健康守护系统-个人博客(二)

情感陪伴 Agent 开发博客 (SSE 流式响应)

一、需求背景

老年人群体普遍面临孤独感就医焦虑两大心理问题。子女不在身边时,老人遇到身体不适往往感到无助;去医院就诊时面对复杂的流程和专业术语也容易紧张。

本模块构建了一个「情感陪伴 Agent」,提供两种模式:

  • 心理慰藉模式 (companion):扮演一位温暖的陪伴者,倾听老人的心声,给予情感支持
  • 陪诊助手模式 (diagnosis):帮助老人整理症状描述、解释医疗流程、缓解就医焦虑

与上一个用药审核 Agent 不同,本模块的核心技术特性是SSE (Server-Sent Events) 流式响应——AI 的回复像打字一样逐字推送到前端,大幅提升交互体验。

二、技术选型

技术选型理由
流式推送Spring MVCSseEmitter无需引入 WebFlux 响应式框架,与现有 MVC 架构兼容
流式 HTTP 客户端Java 内置java.net.http.HttpClientJDK 11+ 原生支持,零额外依赖,支持 InputStream 流式读取
异步处理ExecutorService线程池避免流式读取阻塞 Servlet 线程
大模型 APIDeepSeek Chat API (stream=true)OpenAI 兼容的 SSE 流式接口

为什么不用 WebFlux?项目已经是 Spring MVC 架构,引入 WebFlux 需要全面改造(Mono/Flux 返回类型、响应式数据库驱动等)。SseEmitter是 Spring MVC 原生提供的 SSE 支持,可以在不改变项目架构的前提下实现流式推送。

三、流式响应架构设计

3.1 流式 vs 非流式对比

【非流式】用户发送 → 等待 5-10 秒 → 一次性收到完整回复 【流式】 用户发送 → 0.5 秒后开始逐字显示 → 持续 5-10 秒 → 显示完成

流式的优势在于首字延迟极低,用户几乎立刻就能看到 AI 开始回复,心理上不会觉得「卡住了」。

3.2 端到端流式链路

┌──────────┐ POST请求 ┌───────────────┐ stream=true ┌───────────┐ │ 前端 │ ──────────→ │ Controller │ ──────────────→ │ DeepSeek │ │ │ │ (SseEmitter) │ │ API │ │ │ ←─ SSE 流 ── │ ↑ │ ←── SSE 流 ──── │ │ │ 逐字渲染 │ │ Service层 │ data: {...} │ │ │ │ │ (异步线程池) │ │ │ └──────────┘ └───────────────┘ └───────────┘

关键流程:

  1. Controller 创建SseEmitter并立即返回(非阻塞)
  2. Service 在异步线程中向 DeepSeek 发起stream=true的请求
  3. 通过InputStream逐行读取 DeepSeek 返回的 SSE 流
  4. 解析每个data: {...}JSON 中的delta.content字段
  5. 通过emitter.send()将内容片段推送给前端
  6. 收到[DONE]信号后调用emitter.complete()关闭连接

四、详细实现

4.1 DTO 层:请求参数

@Data@Schema(description="情感陪伴对话请求")publicclassCompanionChatRequest{@NotBlank(message="消息内容不能为空")@Schema(description="用户消息",example="我今天觉得胸口有点闷")privateStringmessage;@NotBlank(message="对话模式不能为空")@Schema(description="对话模式: companion(心理慰藉) / diagnosis(陪诊)",example="companion")privateStringmode;}

mode字段决定 Agent 的「人格」——同一个服务根据 mode 切换不同的 System Prompt。

4.2 Service 层:双模式人设 Prompt

心理慰藉模式 Prompt
privatestaticfinalStringCOMPANION_PROMPT=""" 你是一位温暖、耐心的老年人情感陪伴助手,名叫"暖心"。 你的职责是为老年人提供心理慰藉和情感支持。 你的性格特点: - 说话温柔、亲切,像一位关心长辈的晚辈 - 善于倾听,会先共情再给建议 - 用简单易懂的语言交流,避免专业术语 - 适当使用鼓励性的话语 - 关注老人的情绪变化,及时安抚 对话原则: 1. 先表达理解和关心 2. 温和地引导老人表达感受 3. 给出积极正面的回应 4. 必要时建议老人与家人或专业人士沟通 5. 绝不给出医疗诊断或用药建议,涉及身体不适时建议就医 请用中文回答,语气要亲切自然。 """;
陪诊助手模式 Prompt
privatestaticfinalStringDIAGNOSIS_PROMPT=""" 你是一位专业的老年人陪诊助手,名叫"安心"。 你的职责是帮助老年人在就医过程中理解病情、整理问题,并提供陪伴支持。 你的职责: 1. 帮助老人整理和描述症状,以便就诊时更清晰地向医生表达 2. 解释常见的医学检查项目和流程 3. 帮助老人理解医嘱和注意事项 4. 提醒就诊前的准备事项(空腹、带证件等) 5. 缓解就医紧张情绪 重要原则: - 你不是医生,绝不做诊断或开处方 - 始终建议老人遵循医生的专业意见 - 遇到紧急情况立即建议拨打120或前往急诊 - 用简单易懂的语言解释医学概念 - 保持耐心和温和的态度 请用中文回答,语气要专业但亲切。 """;

通过 mode 字段动态切换:

privateStringgetSystemPrompt(Stringmode){return"diagnosis".equalsIgnoreCase(mode)?DIAGNOSIS_PROMPT:COMPANION_PROMPT;}

Prompt 设计要点:两个模式的关键差异在于——「暖心」侧重情感共情(先理解再建议),「安心」侧重信息整理(帮助老人与医生高效沟通)。两者都设置了安全边界:绝不做医疗诊断

4.3 Service 层:SSE 流式调用(核心难点)

这是本模块的技术核心——如何实现从 DeepSeek 到前端的端到端流式传输。

4.3.1 创建 SseEmitter 并异步执行
@OverridepublicSseEmitterchatStream(CompanionChatRequestrequest){// 创建 SseEmitter,超时时间 120 秒SseEmitteremitter=newSseEmitter(120_000L);// 在异步线程中执行流式调用,不阻塞 Servlet 线程executor.execute(()->{try{streamFromDeepSeek(request,emitter);}catch(Exceptione){log.error("流式对话异常",e);try{emitter.send(SseEmitter.event().name("error").data("对话服务暂时不可用,请稍后重试"));}catch(Exceptionignored){}emitter.completeWithError(e);}});// 注册超时和异常回调emitter.onTimeout(emitter::complete);emitter.onError(t->log.warn("SSE 连接异常断开",t));returnemitter;}

为什么需要异步线程池?SseEmitter的工作原理是:Controller 方法返回SseEmitter后,Servlet 线程立即释放;后续通过其他线程调用emitter.send()推送数据。如果在 Servlet 线程中同步读取 DeepSeek 的流,会长时间占用线程,在高并发时耗尽线程池。

4.3.2 流式读取 DeepSeek SSE 响应
privatevoidstreamFromDeepSeek(CompanionChatRequestrequest,SseEmitteremitter)throwsException{Stringurl=deepSeekConfig.getBaseUrl()+"/v1/chat/completions";// 构建请求体,关键是 "stream": trueMap<String,Object>body=Map.of("model",deepSeekConfig.getModel(),"messages",List.of(Map.of("role","system","content",getSystemPrompt(request.getMode())),Map.of("role","user","content",request.getMessage())),"temperature",0.8,"max_tokens",1500,"stream",true// 开启流式响应);StringjsonBody=objectMapper.writeValueAsString(body);// 使用 Java 内置 HttpClient 发起请求HttpRequesthttpRequest=HttpRequest.newBuilder().uri(URI.create(url)).header("Content-Type","application/json").header("Authorization","Bearer "+deepSeekConfig.getApiKey()).POST(HttpRequest.BodyPublishers.ofString(jsonBody)).timeout(Duration.ofSeconds(60)).build();// 关键:使用 InputStream 方式接收响应,实现流式读取HttpResponse<java.io.InputStream>response=httpClient.send(httpRequest,HttpResponse.BodyHandlers.ofInputStream());// 逐行读取 SSE 数据try(BufferedReaderreader=newBufferedReader(newInputStreamReader(response.body(),StandardCharsets.UTF_8))){Stringline;while((line=reader.readLine())!=null){if(line.isBlank())continue;if(!line.startsWith("data: "))continue;Stringdata=line.substring(6).trim();// [DONE] 表示流结束if("[DONE]".equals(data)){emitter.send(SseEmitter.event().name("done").data("[DONE]"));break;}// 解析 JSON,提取 delta.contenttry{JsonNodenode=objectMapper.readTree(data);JsonNodedelta=node.path("choices").get(0).path("delta");Stringcontent=delta.path("content").asText("");if(!content.isEmpty()){emitter.send(SseEmitter.event().name("message").data(content));}}catch(Exceptione){log.debug("跳过无法解析的 SSE 数据: {}",data);}}}emitter.complete();}

DeepSeek 流式响应格式解析

DeepSeek 开启stream=true后,响应是标准的 SSE 格式,每行格式为:

data: {"id":"...","choices":[{"delta":{"content":"你"},"index":0}]} data: {"id":"...","choices":[{"delta":{"content":"好"},"index":0}]} ... data: [DONE]

非流式响应中内容在choices[0].message.content,流式响应中每个 chunk 的增量内容在choices[0].delta.content,这是 OpenAI 格式流式与非流式的关键区别。

4.3.3 参数选择说明
参数说明
temperature0.8比用药审核 Agent 的 0.3 更高,让对话更自然、有温度感
max_tokens1500陪伴对话不需要太长的回复,控制在合理范围
streamtrue开启流式输出
SseEmitter 超时120,000ms2 分钟超时,足够完成一轮对话
HttpClient 超时60sAPI 调用超时保护

4.4 Service 层:非流式兜底接口

为不支持 SSE 的客户端(如微信小程序)提供传统的同步接口:

@OverridepublicStringchat(CompanionChatRequestrequest){Stringurl=deepSeekConfig.getBaseUrl()+"/v1/chat/completions";HttpHeadersheaders=newHttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);headers.setBearerAuth(deepSeekConfig.getApiKey());Map<String,Object>body=Map.of("model",deepSeekConfig.getModel(),"messages",List.of(Map.of("role","system","content",getSystemPrompt(request.getMode())),Map.of("role","user","content",request.getMessage())),"temperature",0.8,"max_tokens",1500);try{HttpEntity<Map<String,Object>>entity=newHttpEntity<>(body,headers);ResponseEntity<String>response=restTemplate.exchange(url,HttpMethod.POST,entity,String.class);JsonNoderoot=objectMapper.readTree(response.getBody());returnroot.path("choices").get(0).path("message").path("content").asText();}catch(Exceptione){log.error("调用 DeepSeek API 失败",e);thrownewRuntimeException("情感陪伴服务暂时不可用,请稍后重试: "+e.getMessage());}}

4.5 Controller 层:双端点设计

@RestController@RequestMapping("/api/companion")@RequiredArgsConstructor@Tag(name="情感陪伴 Agent",description="基于 AI 的老年人情感陪伴与陪诊助手,支持流式响应")publicclassEmotionalCompanionController{privatefinalEmotionalCompanionServiceemotionalCompanionService;// 流式接口:produces 声明为 text/event-stream@PostMapping(value="/chat/stream",produces=MediaType.TEXT_EVENT_STREAM_VALUE)@Operation(summary="流式对话",description="SSE 流式响应,逐字输出 AI 回复")publicSseEmitterchatStream(@RequestBody@ValidCompanionChatRequestrequest){returnemotionalCompanionService.chatStream(request);}// 非流式接口:标准 JSON 响应@PostMapping("/chat")@Operation(summary="普通对话",description="非流式,等待完整回复后一次性返回")publicResult<String>chat(@RequestBody@ValidCompanionChatRequestrequest){Stringresult=emotionalCompanionService.chat(request);returnResult.success(result);}}

注意produces = MediaType.TEXT_EVENT_STREAM_VALUE:这告诉 Spring MVC 该接口返回的 Content-Type 是text/event-stream,浏览器/客户端据此识别为 SSE 流。

五、前端接入指南

5.1 Web 前端(fetch + ReadableStream)

asyncfunctionchatStream(message,mode){constresponse=awaitfetch('/elderlycare/api/companion/chat/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message,mode})});constreader=response.body.getReader();constdecoder=newTextDecoder();letbuffer='';while(true){const{done,value}=awaitreader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});constlines=buffer.split('\n');buffer=lines.pop();for(constlineoflines){if(line.startsWith('data:')){constdata=line.substring(5);if(data==='[DONE]')return;document.getElementById('chat-output').textContent+=data;}}}}

5.2 微信小程序(使用非流式接口)

wx.request({url:'https://your-server/elderlycare/api/companion/chat',method:'POST',header:{'Content-Type':'application/json'},data:{message:'我今天觉得胸口有点闷',mode:'companion'},success(res){that.setData({reply:res.data.data});}});

六、SSE 协议细节

6.1 事件类型定义

事件名data 内容说明
messageAI 回复的文本片段前端应拼接到对话区域
done[DONE]流结束信号,前端应关闭连接
error错误描述文本异常时发送,前端应展示错误提示

6.2 实际 SSE 数据示例

event:message data:您 event:message data:好 event:message data:,听到您说 event:message data:胸口有点闷 event:message data:,我很关心 event:message data:您的感受。 ... event:done data:[DONE]

七、测试方法

curl 流式测试(推荐)

curl-XPOST http://localhost:8080/elderlycare/api/companion/chat/stream\-H"Content-Type: application/json"\-d'{"message": "我今天觉得胸口有点闷", "mode": "companion"}'\--no-buffer

--no-buffer可以实时看到逐字输出效果。

curl 普通测试

curl-XPOST http://localhost:8080/elderlycare/api/companion/chat\-H"Content-Type: application/json"\-d'{"message": "明天要去医院做心电图,需要注意什么?", "mode": "diagnosis"}'

八、文件结构总结

src/main/java/com/xxx/elderlycare/ ├── controller/ │ └── EmotionalCompanionController.java # 双端点:/chat/stream (SSE) + /chat (普通) ├── dto/ │ └── CompanionChatRequest.java # 请求 DTO(message + mode) ├── service/ │ ├── EmotionalCompanionService.java # 接口(chatStream + chat) │ └── impl/ │ └── EmotionalCompanionServiceImpl.java # 核心实现 │ ├── COMPANION_PROMPT # 心理慰藉人设 │ ├── DIAGNOSIS_PROMPT # 陪诊助手人设 │ ├── chatStream() # SseEmitter + 异步线程 │ ├── streamFromDeepSeek() # HttpClient 流式读取 │ └── chat() # RestTemplate 同步调用

九、总结与思考

技术亮点

  1. 零依赖流式方案:没有引入 WebFlux 或 OkHttp,使用 Java 内置HttpClient+ Spring MVCSseEmitter就实现了完整的 SSE 流式传输,与现有 MVC 架构完美兼容。

  2. 双模式 Prompt 切换:通过mode字段在运行时切换 System Prompt,一个 Service 支撑两种截然不同的对话风格,避免了代码重复。

  3. 异步非阻塞SseEmitter+ 线程池的组合让流式读取不占用 Servlet 线程,保证了服务在高并发下的响应能力。

  4. 双端点兜底:同时提供流式和非流式接口,SSE 不可用时(如小程序)可无缝降级。

局限性与改进方向

  • 无多轮对话上下文:当前每次请求都是独立的单轮对话。后续可引入会话 ID + 消息历史管理,实现连续对话。
  • 无情绪识别:可以在 Service 层增加情绪分析前处理,根据检测到的情绪状态动态调整回复风格。
  • 线程池参数:当前使用CachedThreadPool,生产环境建议替换为固定大小的线程池并配置合理的队列长度。

作者简介:YOU优,一名专注 AI 应用开发的程序员。
项目源码:https://gitee.com/cai-xukun1111/elder-guard-ai
觉得有趣的话,点个赞再走吧 ❤️

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/20 8:48:15

魔兽争霸3优化终极指南:5分钟解决Windows 11兼容性问题

魔兽争霸3优化终极指南&#xff1a;5分钟解决Windows 11兼容性问题 【免费下载链接】WarcraftHelper Warcraft III Helper , support 1.20e, 1.24e, 1.26a, 1.27a, 1.27b 项目地址: https://gitcode.com/gh_mirrors/wa/WarcraftHelper WarcraftHelper是一款专为经典游戏…

作者头像 李华
网站建设 2026/4/20 8:46:48

标定结果从2像素到0.13:一次双目标定失败到成功的“硬核”复盘

图准科技-HH 图准精度视界 在立体视觉的世界里&#xff0c;标定&#xff08;Calibration&#xff09;是最基础也是最关键的一步&#xff0c;很多人往往忽略了它的重要性。然而&#xff0c;正是那些被忽视的细节往往隐藏着最致命的问题&#xff0c;特别是在面对真实数据时——尤…

作者头像 李华
网站建设 2026/4/20 8:43:26

ncmdumpGUI完全指南:解锁网易云音乐NCM加密格式的终极解决方案

ncmdumpGUI完全指南&#xff1a;解锁网易云音乐NCM加密格式的终极解决方案 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换&#xff0c;Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI 在数字音乐版权保护日益严格的今天&…

作者头像 李华
网站建设 2026/4/20 8:42:40

Dubbo 超时机制与集群容错机制详解:防止雪崩的利器

Dubbo 超时机制与集群容错机制详解&#xff1a;防止雪崩的利器 一、引言 在分布式系统中&#xff0c;服务间的远程调用充满不确定性——网络延迟、服务端GC停顿、瞬间流量洪峰等都可能导致调用失败或响应缓慢。如果没有合理的保护机制&#xff0c;一个服务的不稳定会像多米诺骨…

作者头像 李华