原创 编程疏影 路条编程 2024年09月12日 07:30 河北
SpringBoot 3.3 + Ollama + Thymeleaf,实现基于通义千问Qwen-2模型的EventStream数据解析
在当今快速发展的技术环境中,实时数据处理已成为许多应用的核心需求。无论是消息推送、实时通知,还是用户与智能对话系统的交互,这些场景都依赖于服务器与客户端之间的持续数据流(EventStream)。传统的HTTP请求-响应模式已无法满足这些需求,而EventStream提供了一种高效的解决方案,使服务器能够在连接保持活跃的情况下向客户端推送多条消息。
在本文中,我们将通过Spring Boot 3.3结合Ollama的Qwen-2模型和Thymeleaf模板引擎,构建一个支持EventStream数据解析的Web应用。Ollama作为一款强大的本地AI模型推理工具,其Qwen-2模型具有卓越的自然语言处理能力,能够实时生成对话或其他文本数据。我们将利用这一能力,通过Spring Boot的WebFlux模块接收来自Ollama的EventStream数据,并使用Thymeleaf和JavaScript在前端进行实时展示。
运行效果:
若想获取项目完整代码以及其他文章的项目源码,且在代码编写时遇到问题需要咨询交流,欢迎加入下方的知识星球。
项目结构
项目的基本结构如下:
springboot-eventstream/
│
├── src/main/
│ ├── java/com/example/eventstream/
│ │ ├── controller/
│ │ │ └── StreamController.java
│ │ ├── service/
│ │ │ └── EventStreamService.java
│ │ ├── model/
│ │ │ └── EventMessage.java
│ ├── resources/
│ │ ├── templates/
│ │ │ └── index.html
│ │ └── application.yml
│ └── resources/static/
│ └── js/
│ └── stream.js
└── pom.xml
项目依赖配置(pom.xml)
在pom.xml
中配置项目所需的依赖,包括Spring Boot、Thymeleaf、Lombok,以及用于与Ollama接口进行交互的依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>springboot-eventstream</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-eventstream</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring WebFlux for reactive programming -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Jackson for JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
YAML 配置(application.yml)
在application.yml
中配置Ollama的本地接口信息,确保能够正确连接并接收Qwen-2模型生成的EventStream数据。
server:
port: 8080
ollama:
api-url: http://localhost:5000/eventstream
model: qwen2
核心实现
1. EventMessage 模型类
EventMessage
类用于存储从EventStream接收到的消息数据。通过Lombok自动生成必要的getter和setter方法。
package com.icoderoad.eventstream.entity;
import lombok.Data;
@Data
public class EventMessage {
private String model;
private String created_at;
private String response;
private boolean done;
}
2. EventStreamService 服务类
EventStreamService
类负责与Ollama的Qwen-2模型进行交互,获取并解析来自EventStream的数据流。
package com.icoderoad.eventstream.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import com.icoderoad.eventstream.entity.EventMessage;
import reactor.core.publisher.Flux;
@Service
public class EventStreamService {
private final WebClient webClient;
@Value("${ollama.model}")
private String model;
public EventStreamService(WebClient.Builder webClientBuilder, @Value("${ollama.api-url}") String apiUrl) {
this.webClient = webClientBuilder
.baseUrl(apiUrl)
.defaultHeader("Accept", "text/event-stream")
.build();
}
public Flux<EventMessage> getEventStream(String userInput) {
return webClient.post()
.uri("/api/generate")
.header("Content-Type", "application/json") // 设置内容类型
.bodyValue("{\"model\":\"" + model + "\",\"prompt\": \"" + userInput + "\"}")
.retrieve()
.bodyToFlux(EventMessage.class);
}
}
3. StreamController 控制器类
StreamController
类负责处理前端请求,并通过EventStreamService返回实时数据流。
在此方法中,我们使用produces = MediaType.TEXT_EVENT_STREAM_VALUE
来明确指定返回的是一个EventStream。这一步非常重要,因为它告诉Spring和客户端,数据是流式传输的,而不是一次性发送的。这种方式非常适合处理长连接或实时更新的场景,如聊天应用、股票行情、传感器数据等。
package com.example.eventstream.controller;
import com.example.eventstream.model.EventMessage;
import com.example.eventstream.service.EventStreamService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class StreamController {
@Autowired
private EventStreamService eventStreamService;
@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<EventMessage> streamEvents(@RequestParam String input) {
return eventStreamService.getEventStream(input);
}
}
说明:
-
@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
:通过指定produces
属性为MediaType.TEXT_EVENT_STREAM_VALUE
,明确告知客户端我们将以文本事件流的形式发送数据。文本事件流是一种轻量级的流式数据格式,适用于Web应用中的实时数据更新场景。 -
Flux<EventMessage>
:使用Reactor提供的Flux
类型来表示数据流,这样我们可以在Spring WebFlux中异步处理流式数据。
视图控制类
package com.icoderoad.eventstream.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
前端实现
在前端,我们使用Thymeleaf模板和JavaScript来展示从服务器端推送的事件消息,并提供一个输入框让用户输入内容。
1. Thymeleaf 模板(index.html)
在src/main/resources/templates
目录下创建文件 index.html
,设计一个页面,用于输入用户内容并展示来自Qwen-2模型的实时消息。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Qwen-2 模型实时数据流</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" >
<style>
body {
background-color: #f8f9fa;
}
.chat-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 10px;
background: #ffffff;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
height: 100vh;
justify-content: space-between;
}
.chat-box {
flex: 1;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 10px;
background: #f9f9f9;
padding: 10px;
margin-bottom: 10px;
}
.message {
margin-bottom: 15px;
}
.message.user {
text-align: right;
}
.message.bot {
text-align: left;
}
.message-content {
display: inline-block;
padding: 10px;
border-radius: 10px;
}
.message.user .message-content {
background-color: #007bff;
color: #ffffff;
}
.message.bot .message-content {
background-color: #e9ecef;
color: #000000;
}
.input-group {
margin-top: 10px;
display: flex;
align-items: center;
}
.input-group input {
border-radius: 20px;
border: 1px solid #ddd;
flex: 1;
}
.input-group button {
border-radius: 20px;
margin-left: 10px;
}
</style>
</head>
<body>
<div class="chat-container">
<h1>Qwen-2 模型实时数据流</h1>
<div class="chat-box" id="event-container">
<!-- 消息内容会动态插入到这里 -->
</div>
<div class="input-group">
<input type="text" class="form-control" id="user-input" placeholder="输入你的内容">
<button id="send-button" class="btn btn-primary">发送</button>
</div>
</div>
<script src="/js/stream.js"></script>
</body>
</html>
JavaScript 实现(stream.js)
在``src/main/resources/static/js/stream.js中,使用
EventSource`对象与服务器端建立连接,根据用户输入动态更新页面内容。
document.addEventListener("DOMContentLoaded", function () {
const eventContainer = document.getElementById("event-container");
const userInput = document.getElementById("user-input");
const sendButton = document.getElementById("send-button");
let eventSource = null;
let cachedResponse = ''; // 用于缓存接收到的部分内容
sendButton.addEventListener("click", function () {
const input = userInput.value;
// 清空之前的 EventSource(如果存在)
if (eventSource) {
eventSource.close();
}
// 清空 eventContainer 中的内容
eventContainer.innerHTML = '';
// 创建一个新的 EventSource 实例
eventSource = new EventSource(`/events?input=${encodeURIComponent(input)}`);
// 创建一个新的 div 元素来显示所有事件消息
const responseDiv = document.createElement("div");
responseDiv.classList.add("message", "bot");
const responseContent = document.createElement("div");
responseContent.classList.add("message-content");
responseDiv.appendChild(responseContent);
eventContainer.appendChild(responseDiv);
eventSource.onmessage = function (event) {
const eventData = JSON.parse(event.data);
// 将新的响应追加到缓存中
cachedResponse += eventData.response;
// 将缓存内容转换为 HTML 并显示
let htmlContent = cachedResponse;
// 替换 Markdown 标题格式为 HTML 标题标签
htmlContent = htmlContent
.replace(/^###### (.+)$/gm, '<h6>$1</h6>')
.replace(/^##### (.+)$/gm, '<h5>$1</h5>')
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// 替换 **xxxx** 为 <b>xxxx</b>
const boldTextRegex = /\*\*(.*?)\*\*/g;
htmlContent = htmlContent.replace(boldTextRegex, '<b>$1</b>');
// 将转换后的 HTML 内容更新到 responseContent 中
responseContent.innerHTML = htmlContent;
};
eventSource.onerror = function () {
console.error("EventSource failed.");
eventSource.close();
};
eventSource.onopen = function () {
console.log("Connection to /events opened.");
};
// 在流结束时清空缓存并处理最终内容
eventSource.addEventListener('end', function () {
let finalHtmlContent = cachedResponse;
// 替换 Markdown 标题格式为 HTML 标题标签
finalHtmlContent = finalHtmlContent
.replace(/^###### (.+)$/gm, '<h6>$1</h6>')
.replace(/^##### (.+)$/gm, '<h5>$1</h5>')
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// 替换 **xxxx** 为 <b>xxxx</b>
finalHtmlContent = finalHtmlContent.replace(boldTextRegex, '<b>$1</b>');
// 将转换后的 HTML 内容更新到 responseContent 中
responseContent.innerHTML = finalHtmlContent;
});
});
});
启动项目并测试
启动Spring Boot应用,访问http://localhost:8080/
,输入内容并点击发送按钮,您将看到Qwen-2模型生成的实时消息流展示在页面上。
结论
通过Spring Boot 3.3结合Ollama的Qwen-2模型和Thymeleaf,我们成功实现了一个实时输入解析与显示的Web应用。本文展示了如何使用EventStream技术在现代Web应用中处理实时数据,并通过简单的前后端整合,提供流畅的用户交互体验。希望这篇文章能为您在实现类似功能时提供有价值的参考。
今天就讲到这里,如果有问题需要咨询,大家可以直接留言或扫下方二维码来知识星球找我,我们会尽力为你解答。