后端

pom.xml


<properties>    
  <java.version>17</java.version>    
  <spring-ai.version>1.0.0-M5</spring-ai.version>
</properties>
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
          <!-- 公司内部Ollama模型 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>
        
         <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

注意:此时的jdk版本需要17及以上!

application.yaml


server:
  port: 8080

spring:
  application:
    name: ai-demo
  ai:
    ollama:
      base-url: http://服务ip:11434
      chat:
        model: deepseek-r1:7b

AIChatController.java

	@Resource
    private OllamaChatModel ollamaChatModel;
    
    /**
     * 全文本输出格式
     *
     * @param message
     * @return
     */
    @GetMapping("/chat")
    public String chat(String message) {
        Prompt prompt = new Prompt(new UserMessage(message));
        ChatResponse chatResponse = ollamaChatModel.call(prompt);
        String content = chatResponse.getResult().getOutput().getContent();
        content = content.replace("<think>", "").replace("</think>", "").trim();
        log.info("输出结果: {}", content);
        return content;
    }
    
    /**
     * 流式输出格式
     *
     * @param message
     * @return
     */
    @GetMapping("/stream")
    public SseEmitter streamChat(@RequestParam String message) {
        // 60秒超时时间
        SseEmitter emitter = new SseEmitter(60_000L);

        // 使用异步线程处理流式响应
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> {
            try {
                // 创建流式Prompt
                Prompt prompt = new Prompt(new UserMessage(message));

                // 获取响应流
                Flux<ChatResponse> responseFlux = ollamaChatModel.stream(prompt);

                responseFlux.subscribe(
                        chunk -> {
                            // 处理每个响应块
                            // String content = processChunk(chunk);
                            String content = chunk.getResult().getOutput().getContent();
                            log.info("content: {}", content);
                            try {
                                // 发送数据到客户端
                                emitter.send(content);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        },
                        error -> {
                            emitter.completeWithError(error);
                            log.error("流式输出异常: {}", error.getMessage());
                        },
                        () -> {
                            emitter.complete();
                            log.info("流式输出完成");
                        }
                );
            } catch (Exception e) {
                emitter.completeWithError(e);
                log.error("流式处理初始化失败: {}", e.getMessage());
            }
        });

        executor.shutdown();
        return emitter;
    }

前端

package.json

{
  "name": "chat",
  "private": true,
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "lucide-vue-next": "^0.263.0",
    "marked": "^4.2.12",
    "vue": "^3.3.4",
    "vue-router": "^4.2.4"
  },
  "devDependencies": {
    "@tailwindcss/typography": "^0.5.15",
    "@vitejs/plugin-vue": "^4.2.3",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.49",
    "tailwindcss": "^3.4.17",
    "vite": "^4.4.9"
  }
}

核心代码


const sendMessage = async () => {
            const message = userMessage.value.trim()
            if (!message || isTyping.value) return

            messages.value.push({
                text: message,
                sender: 'user',
                timestamp: new Date().toISOString()
            })

            userMessage.value = ''
            isTyping.value = true
            scrollToBottom()

            try {
                const response = await fetch(
                    // `${baseUrl}/ollama/chat?message=${encodeURIComponent(message)}`,
                    `${baseUrl}/ollama/stream?message=${encodeURIComponent(message)}`,
                    { method: 'GET' }
                )

                if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`)

                 // 创建新的消息对象并立即添加到消息列表
                let botMessage = ref({
                    text: '',
                    sender: 'bot',
                    timestamp: new Date().toISOString(),
                })

                messages.value.push(botMessage.value)  // 立即添加消息对象
                // 创建 ReadableStream 的读取器
                const reader = response.body.getReader()
                const decoder = new TextDecoder()

                 // 循环读取流数据
                while (true) {
                    const { done, value } = await reader.read()
                    if (done) break

                    // 解码并处理文本
                    const text = decoder.decode(value)
                    // 处理 SSE 格式,移除 "data: " 前缀和 think 标签
                    const cleanText = text
                        .replace(/^data:\s*/gm, '')
                        .replace(/<think>|<\/think>/g, '')
                        .trim()

                    console.log('cleanText---', cleanText)
                    if (cleanText) {
                        // 直接更新消息对象的文本内容
                        botMessage.value.text += cleanText
                        newMessage.value += cleanText
                        scrollToBottom()
                    }
                }

            } catch (error) {
                messages.value.push({
                    text: '请求失败,请稍后再试',
                    sender: 'bot',
                    timestamp: new Date().toISOString()
                })
            } finally {
                isTyping.value = false
                scrollToBottom()
            }
        }

目录结构
在这里插入图片描述

运行效果
在这里插入图片描述

在这里插入图片描述
至此,流式效果已实现,需要代码的同学可以加入技术群获取!

本人正在打造技术交流群,欢迎志同道合的朋友一起探讨,一起努力,通过自己的努力,在技术岗位这条道路上走得更远。QQ群号:925317809 备注:技术交流 即可通过!

加入技术群可以获取资料,含AI资料、Spring AI中文文档等,等你加入~

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐