当前位置: 首页 > news >正文

深入解析:AI帮写JD实践指南:Spring Boot中集成SseEmitter实现流式输出

需求背景

本次需求是需要实现AI帮写职位描述,并实现逐字输出的效果。

实现方式

大体实现的方向是后端接入大模型流式响应SDK,并通过SseEmitter将响应数据实时推送至前端。基于这个大体方向我就简单讲解一下我实现的过程、其中涵盖的知识点、期间遇到的一些大坑!

流式输出

常见的流式输出有以下几种,可以根据实际情况进行选择。

方式核心机制优点缺点典型场景
SseEmitter (Spring)Server-Sent Events (HTTP SSE协议)简单易用、浏览器原生支持、单向流仅支持文本推送实时日志、AI流输出
WebFlux Flux<T> (Reactive)响应式流(Reactor)背压、异步非阻塞学习曲线高高并发实时流
WebSocket 双向流长连接 + Frame 流双向通信、实时性最强需要握手与状态管理聊天、AI对话、消息推送

SseEmitter

1.特点
  • 仅支持单向通信(服务器 → 客户端)

  • 仅文本数据

  • 标准 SSE 协议,浏览器支持好

2.demo
//浏览器直接访问该接口,就可以看到页面逐字输出的效果
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() {SseEmitter emitter = new SseEmitter(0L);Executors.newSingleThreadExecutor().execute(() -> {try {for (int i = 0; i < 10; i++) {emitter.send(SseEmitter.event().data("chunk-" + i));Thread.sleep(500);}emitter.send(SseEmitter.event().data("done"));emitter.complete();} catch (Exception e) {emitter.completeWithError(e);}});return emitter;
}
3.核心API
  • 构造:

    • new SseEmitter() / new SseEmitter(timeoutMillis)(0 / -1 表示永不过期,建议明确超时)

  • 发送方法:

    • sseEmitter.send(Object data):发送默认事件(数据会被序列化)。

    • sseEmitter.send(SseEmitter.event().name("eventName").data("...")):发送具名事件(前端用 addEventListener 监听)。

  • 结束/异常:

    • sseEmitter.complete():正常结束(关闭连接)。

    • sseEmitter.completeWithError(Throwable):以错误结束。

  • 回调:

    • sseEmitter.onTimeout(Runnable)

    • sseEmitter.onCompletion(Runnable)

    • sseEmitter.onError(Consumer<Throwable>)

4.常用模式
  • 一次性流(短连接):AI 文本生成 → 流式返回 → 完成并 close(不保存 emitter)
  • 订阅推送(长连接):用户登录后建立 emitter 并保存在 Map(key=userid),后续服务可随时推送,需配合 ConcurrentHashMap、心跳与清理机制
  • 心跳/keep-alive:定期发送注释或空事件(SseEmitter.event().comment("keepalive"))防止代理关闭空闲连接
5.注意点
  • 不要用默认全局线程池处理长阻塞任务

    • CompletableFuture.runAsync(...) 默认使用 ForkJoinPool.commonPool() —— 在高并发场景会被占满。推荐自定义有界线程池(线程数、队列大小)。

  • 给 SseEmitter 设置合理超时

    • 永久超时会增加资源泄露风险。建议设置一个合理上限(例如 60s~5min),且在 onTimeout 里做清理和提示。

  • 心跳 / keep-alive

    • 代理(如 Nginx、Cloud Load Balancer)可能会在连接长时间无数据时断开。通过定时发送注释 event().comment("keep") 或空数据避免被断。

  • 防止资源泄露

    • onCompletion/onError/onTimeout 里从全局 Map 移除 emitter 并关闭。对异常关闭都要 complete()completeWithError()

  • 避免频繁小事件导致性能问题

    • 模型流或逐字符输出时,考虑合并 buffer(按时间或长度阈值 flush),减少网络/代理开销。

  • 不要把 API Key 等敏感信息写进日志

    • 任何异常日志打印前先过滤敏感字段。

  • 客户端事件语义

    • 使用具名事件(name("done")name("error"))便于前端处理。

  • 代理 & Nginx 配置 (踩坑点一:在测试环境前端总是一次性接收到所有数据,而不是分批接收的,就是因为Nginx有缓存)

    • 需确保 proxy_buffering off;proxy_read_timeout 足够大。否则 SSE 会被缓冲或超时。

  • Content-Type

    • MediaType.TEXT_EVENT_STREAM_VALUEtext/event-stream;charset=UTF-8)是 SSE 的标准 Content-Type。

WebFlux

1.特点
  • 完全异步、非阻塞,背压支持
  • 需要 WebFlux 环境(非传统 Spring MVC)
2.demo
@GetMapping(value = "/reactive", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux reactiveStream() {return Flux.interval(Duration.ofSeconds(1)).map(seq -> "chunk-" + seq).take(10);
}

WebSocket

1.特点
  • 需要状态管理,不适合短时流式接口
  • 真正的双向实时通信

基于以上流式输出框架,本次需求选择使用SeeEmitter

大模型

本次需求使用的大模型是阿里云的百炼大模型,这里附上流式响应的官方文档。

https://bailian.console.aliyun.com/?tab=doc#/doc/?type=model&url=2866129

注意点

1.是否启动增量输出

DashScope 协议支持增量与非增量式流式输出:

  • 增量(推荐):每个数据块仅包含新生成的内容,设置incremental_outputtrue启动增量式流式输出。

    示例:["我爱","吃","苹果"]
  • 非增量:每个数据块都包含之前已生成的内容,造成网络带宽浪费和客户端处理压力。设置incremental_outputfalse启动非增量式流式输出。

    示例:["我爱","我爱吃","我爱吃苹果"]

2.参坑点二:streamCall方法传参使用GenerationParam,而非ApplicationParam

调用方式是否需要代码传提示词说明
调用 Application(推荐)❌ 不需要后台已配置角色和技能,代码只传输入,但是streamCall方法不支持传入该参数
调用 Generation(直接模型)✅ 需要没有后台模板,代码里要附上完整提示词。通过设置messages参数实现

3.角色讲解

SYSTEM(系统角色)

系统角色是对话的“设定者”或“引导者”。

  • 作用:告诉模型「你是谁」「你要怎么回答」「有哪些约束」。

  • 通常位置:放在消息列表的第一条。

  • 示例用途:控制语气、身份、输出格式。

  • 理解方式:就像在聊天开始前给 AI 一段背景设定或任务指令。

Message.builder().role("system").content("你是一位经验丰富的招聘文案专家,擅长撰写吸引求职者的职位描述。").build();

USER(用户角色)

用户角色是由实际用户(或业务系统)输入的消息。

  • 作用:告诉模型“我现在要你帮我做什么”。

ASSISTANT(助手角色)

助手角色是模型自己之前生成的内容。

  • 作用:用于带上下文的多轮对话(让模型知道它之前说过什么)

BOT(机器人角色)

有些 SDK(例如阿里百炼、阿里云灵积 DashScope)中,bot 是与 assistant 类似的角色。

  • 作用:表示模型生成的回复,有时在聊天机器人上下文里更常见。

  • 区别

    • assistant 一般指助手模型(ChatGPT类对话);

    • bot 则更接近系统内置机器人(比如客服机器人、模板任务型 bot)。

  • 在百炼里,二者可根据不同 API 互换使用。

ATTACHMENT(附件角色)

附件角色 用于携带非文本内容,例如文件、图片、音频、视频等。

  • 作用:为对话附加素材或上下文。

  • 常用于:图文混合生成、多模态场景。

根据以上角色和本次业务确定下来使用SYSTEM和USER

//构建AI帮写职位描述调用百炼参数
Map contentMap = buildAiWriteParam(param);
//获取大模型提示语
String aiTips = ApplicationContextHolder.getProperty("position.position.description.ai.write.tips",String.class);
List messages = Arrays.asList(com.alibaba.dashscope.common.Message.builder().role(Role.SYSTEM.getValue()).content(aiTips).build(),com.alibaba.dashscope.common.Message.builder().role(Role.USER.getValue()).content(JSON.toJSONString(contentMap)).build());

总结

到这里,调用大模型流式响应API,通过SseEmitter实时输出模型产生的数据就可以实现该功能啦!!!对了,在补充一下最后一个踩坑点。

踩坑点三:SSE流式接口不能通过Feign调用,所以在微服务架构中一定要注意!我的解决方案直接暴力在上游服务引入了百炼SDK进行调用的

http://www.jsqmd.com/news/322700/

相关文章:

  • 中英文按视觉长度分割
  • C# 泛型编译后究竟长啥样?
  • 目标检测数据集 - 饮用水垃圾检测数据集下载
  • 为啥“泛型”非得在编译期把类型参数定死?——大白话讲透 C# 泛型背后的规矩(含很多生活比喻)
  • 1月30号
  • 反射调用为何疯狂GC?揭秘装箱与锯齿图
  • 文件在模型服务化中的各个状态IncomingFile➡FileItem;项目异常抛出体系;环境变量url与普通常量url区别;
  • 中英文、中英标点及数字按视觉长度分割
  • 2026简单易用的PPT智能生成工具及实操指南
  • 揭秘电商企业降本60%的SQL优化黄金法则
  • 超轻量图片水印添加工具:13.5KB绿色版,支持自定义内容与位置
  • 告别熬夜做PPT!5款高性价比AI生成工具,效率翻倍不踩坑
  • 考执业医师哪个课程好?小编推荐你选阿虎医考!
  • 爆了!关于2026开年3位程序员接连猝死事件对普通人的启示录一
  • 视频批量智能分割工具:一键自动剪辑与镜头识别教程
  • 考中医执业医师,到底哪个老师讲得好?
  • 告别熬夜做PPT!3款AI一键生成神器,学生党职场人闭眼冲
  • 备课效率翻倍!2026教师专用PPT工具全攻略:传统神器+AI黑科技一网打尽
  • 告别PPT排版焦虑!4个宝藏模板平台,覆盖全场景需求
  • 告别熬夜做PPT!4款宝藏生成工具实测,小白也能秒变设计大神
  • 三维激光扫描与comsol
  • 高低温冲击试验箱哪家质量好?精选厂家对比
  • 备课神器|6款教师专属PPT生成工具,5分钟搞定精美课件!
  • 2026学生党PPT神器推荐:3款AI工具实测横评,告别熬夜赶稿!
  • 备考中医执医考试,我为什么推荐阿虎医考
  • 揭秘!6款宝藏PPT制作工具,适配所有人群,新手也能轻松上手
  • 学生党PPT神器全攻略:免费工具+AI助手,轻松搞定课堂汇报与小组作业
  • 1.29.codeforces div2 C,D 个人题解 - CUC
  • 2025-2026 AI PPT工具实测解析:百度文库PPT全场景能力拆解
  • 2026职场PPT工具天花板推荐:AI赋能+高效出稿,打工人必备指南