SpringBoot 3.3 + Ollama 实现基于通义千问Qwen-2模型的EventStream数据解析

原创 编程疏影 路条编程 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应用中处理实时数据,并通过简单的前后端整合,提供流畅的用户交互体验。希望这篇文章能为您在实现类似功能时提供有价值的参考。

今天就讲到这里,如果有问题需要咨询,大家可以直接留言或扫下方二维码来知识星球找我,我们会尽力为你解答。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值