基于 Spring AI 的 DeepSeek 集成实战(第二版)

主题与目标

本文为“基于 Spring AI 的 DeepSeek 集成实战”系列博客的第二版,聚焦于用户体验的两大提升:

  1. 流式显示——让长文本响应边生成边可见,降低等待感;

  2. Markdown 渲染——以易读的富文本格式呈现 AI 返回的内容,而非原始 Markdown 代码。

目标读者为已有第一版接入基础、希望完善前后端交互、提升可用性的 Spring 开发者。


为什么要流式显示

  • 降低用户焦虑:长文本生成往往需要数秒甚至更久,用户若一动不动等待,体验欠佳。

  • 实时反馈检查:早期增量能让用户判断模型方向是否符合预期,及时中断或调整提示。

  • 带宽节省:浏览器无需等完整内容才渲染,节省首屏加载时间。

通过流式接口,后端将模型输出分段推送;前端收到一小段就立即渲染,给用户“滚动直播”般的生成体验。


为什么要 Markdown 展示

  • 可读性强:标题、列表、代码块、引用块,都能清晰区分层次和语法元素。

  • 格式统一:与 GitHub、文档平台保持一致,降低阅读成本。

  • 功能丰富:支持表格、任务列表、强调等多种富文本效果。

若将 AI 原始 Markdown 以纯文本显示,诸如 # 标题、`````` 代码块标记,都会干扰阅读。把文本先转换成 HTML 并安全渲染,即可获得最佳效果。


我是怎么实现的

后端:WebFlux + SSE 流式接口

  1. 依赖引入

    <!-- Spring WebFlux -->
    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <!-- Spring AI Starter -->
    <dependency>
     <groupId>org.springframework.ai</groupId>
     <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
     <version>1.0.0-M6</version>
    </dependency>
  2. 服务层(AiService)

    @Service
    public class AiService {
       private final StreamingChatModel model;
       public AiService(StreamingChatModel model) { this.model = model; }

       /** 流式生成返回 Flux<String> */
       public Flux<String> streamGenerate(String msg) {
           return model.stream(new UserMessage(msg));
      }
    }
  3. 控制层(AiController)

    @RestController
    @RequestMapping("/api/ai")
    public class AiController {
       private final AiService svc;
       public AiController(AiService svc) { this.svc = svc; }

       @GetMapping(path="/stream-chat", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
       public Flux<ServerSentEvent<String>> streamChat(@RequestParam String message) {
           return svc.streamGenerate(message)
                    .map(chunk -> ServerSentEvent.builder(chunk).build());
      }
    }
  • 使用 Reactor 的 Flux 管道将每次模型“增量输出”作为一条 SSE 事件推送。

  • 前端可通过标准的 EventSource 订阅 TEXT_EVENT_STREAM

前端:EventSource + Markdown 渲染

  1. EventSource 订阅 在用户输入后,先渲染 用户气泡,再插入一个空 Bot 占位气泡,然后:

    const es = new EventSource(`/api/ai/stream-chat?message=${encodeURIComponent(msg)}`);
    let buffer = '';
    es.onmessage = e => {
     buffer += e.data;
     botBubble.innerHTML = renderMarkdown(buffer);
     scrollToBottom();
    };
    es.onerror = () => {
     es.close();
     saveMessage('bot', buffer);
     enableInput();
    };
  2. 预处理 + GFM 渲染

    function renderMarkdown(raw) {
     // 1. 自动在 # 与文字之间补空格
     const fixed = raw.replace(/^(#{1,6})([^\s#])/gm, '$1 $2');
     // 2. marked 配置
     marked.setOptions({ gfm: true, headerIds: false });
     // 3. 安全渲染
     return DOMPurify.sanitize(marked.parse(fixed));
    }
  • 正则补空格:兼容模型直接输出的 #一级标题# 一级标题

  • GFM 模式:支持标题、列表、任务列表、表格等规范特性

  • DOMPurify:防止任何恶意 HTML 注入


额外优化与注意事项

  1. 连接管理EventSource 会自动重试,前端需在适当时机 es.close() 避免多次重连。

  2. 断点续传:可在 SSE 中加入 id 字段,实现断线后从最后一行继续。

  3. UI 动效:为占位气泡和代码块添加微交互动效,提升细节体验。

  4. 错误提示:在流式报错时,渲染一个红色提示气泡,并提供“重试”按钮。20250607142753.png

  5. 20250607142802.png

  • 微信
  • 赶快加我聊天吧
  • QQ
  • 赶快加我聊天吧
  • weinxin
三桂

发表评论 取消回复 您未登录,登录后才能评论,前往登录