Spring AI:让大模型住进 Spring 生态(二)

专栏:Spring AI 探索手札

个人主页:手握风云

目录

一、ChatClient

[1.1. 实现简单对话](#1.1. 实现简单对话)

[1.2. 角色预设](#1.2. 角色预设)

[1.3. 结构化输出](#1.3. 结构化输出)

[1.4. 实现流式输出](#1.4. 实现流式输出)

[1.5. 打印日志](#1.5. 打印日志)

二、流式编程

[2.1. SSE 协议介绍](#2.1. SSE 协议介绍)

[2.2. Spring 中 SSE 实现](#2.2. Spring 中 SSE 实现)

三、ChatModel

[3.1. 概述](#3.1. 概述)

[3.2. 实现简单对话](#3.2. 实现简单对话)

[3.3. 角色预设](#3.3. 角色预设)

[3.4. 实现流式输出](#3.4. 实现流式输出)

[3.5. ChatModel 与 ChatClient 的核心区别](#3.5. ChatModel 与 ChatClient 的核心区别)


一、ChatClient

ChatClient 是 Spring AI 框架中封装了复杂交互流程的高阶 API 接口,旨在简化开发者与大语言模型的集成过程。它提供了一套极具表现力的流式(Fluent)API,支持同步和响应式编程模型,能让你开箱即用,避免处理底层组装 Prompt 的繁琐逻辑。

1.1. 实现简单对话

最基础的用法就是一问一答。我们只需要传入用户的输入内容,调用 call() 方法发送请求,并通过 content() 提取返回的字符串即可:

java 复制代码
package com.yang.ai.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/client")
public class ChatClientController {
    private final ChatClient chatClient;

    public ChatClientController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/call")
    public String generation(String userInput) {
        return this.chatClient.prompt()
            // 用户输入的信息
            .user(userInput)
            // 请求大模型
            .call()
            // 返回文本
            .content();
    }
}

访问接口:http://127.0.0.1:8080/client/call?userInput=你是谁?

1.2. 角色预设

如果你想给你的 AI 助手设定一个"人设"(比如一个专属的编码助手),可以通过 ChatClient.Builder 中的 defaultSystem() 方法来配置默认的系统消息。这样配置后,这段文本会作为基础角色设定,注入到每次对话的上下文中:

java 复制代码
public ChatClientController(ChatClient.Builder chatClientBuilder) {
    this.chatClient = chatClientBuilder.
            defaultSystem("你叫编码助手,擅长 Java 语法和集合框架。").
            build();
}

我们依然访问接口:http://127.0.0.1:8080/client/call?userInput=你是谁?

1.3. 结构化输出

在业务开发中,我们通常需要大模型返回 JSON 或特定的数据结构,而不是纯文本。借助 entity() 方法和 JDK 16 引入的 record,可以轻松实现结构化转换:

java 复制代码
package com.yang.ai.configuration;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfiguration {
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
        return chatClientBuilder
                .build();
    }
}
java 复制代码
// 1. 定义实体类
record Recipe(String dish, List<String> ingredients) {
}

// 2. 调用模型并映射
@RequestMapping("/entity")
public String entity(String userInput) {
    Recipe recipe = chatClient.prompt()
            .user(String.format("请帮我生成%s的食谱", userInput))
            .call()
            .entity(Recipe.class); // 自动将模型输出转为自定义实体

    return recipe.toString();
}

1.4. 实现流式输出

用户和大模型进⾏交互时,由于大模型⼀次输出内容较多,等待全部内容⽣成完毕会导致用户等待时间过长,这对用户的体验⾮常不友好。可以采用流式输出的方式。ChatClient 作为高阶 API,通过链式调用封装了流式请求的所有细节,核心是用 stream() 替代同步调用的 call(),直接返回 Flux<String>,无需手动解析响应。

java 复制代码
// 防止出现乱码,采用 UTF-8
@GetMapping(value = "/stream", produces = "text/html;charset=utf-8")
public Flux<String> stream(String userInput) {
    return this.chatClient.prompt()
            .user(userInput)
            .stream()
            .content();
}

1.5. 打印日志

Advisors(拦截/增强器): 它是介于用户请求与 AI 模型之间的中间件组件。其核心功能借鉴了 Spring AOP 的思想,以链式结构运行,能够对请求进行拦截、过滤和增强(例如在调用 API 前后处理参数和结果、敏感词过滤、管理对话上下文等)。

SimpleLoggerAdvisor: 它是 Spring AI 内置的 Advisor 之一,专门用于记录日志。只需将其添加到 Advisor 链中,它就能自动记录所有经过该 Advisor 的聊天请求(Request)和响应(Response)。

开发人员可以通过两种方式配置日志增强器:1. 全局配置,默认作用于所有对话,在使用 ChatClient.Builder 构建客户端时,通过 defaultAdvisors() 方法全局挂载。这样设置后,该 Advisor 会作用于 ChatClient 发起的每一次对话。2. 请求级别配置(作用于单次对话): 可以在发起具体的请求时通过 .advisors() 单独配置。注意: 如果在全局和单次对话中都设置了同一类型的 Advisor,单次对话(请求级别)的优先级高于全局默认配置。

由于 SimpleLoggerAdvisor 的日志默认是以 Debug 级别输出的,因此必须在 Spring Boot 的配置文件中调整对应包的日志打印级别:

java 复制代码
logging:
  level:
    org.springframework.ai.chat.client.advisor: debug
java 复制代码
package com.yang.ai.configuration;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfiguration {
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
        return chatClientBuilder
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}

二、流式编程

2.1. SSE 协议介绍

SSE(Server-Sent Events,服务器发送事件)是一种基于 HTTP 的轻量级实时通信协议,主要用于解决传统 HTTP 无状态且无法由服务器主动推送消息的问题,从而实现流式传输(Streaming)。

核心运行机制与特点:

  • 基于 HTTP 协议:它直接复用标准的 HTTP/HTTPS 协议进行通信,不需要引入额外的端口或复杂的协议,因此兼容性极好且易于部署。
  • 单向数据推送 :SSE 采用的是服务器向客户端的单向通信机制。一旦客户端通过普通 HTTP 请求建立了连接,服务器就可以在这个通道上持续不断地推送数据流,但客户端无法通过该连接向服务器反向发送数据。
  • 持久连接不断开:服务器会在响应时声明发送的是流消息,此时客户端收到声明后不会主动关闭连接,而是保持连接开放,一直等待接收新的数据流。
  • 自动重连机制:当网络或连接意外中断时,浏览器具备自动断线重连的能力,服务器还可以通过特殊的字段指定重连的时间间隔。

要成功实现 SSE,服务端必须遵循特定的数据格式标准:服务端在响应时,必须设置 Content-Type: text/event-stream;charset=utf-8,以此告知客户端这是一个事件流。消息组装与分隔:每次发送内容由若干 message 组成,不同 message 间用 \n\n 严格分隔。每个 message 内包含若干行,每行标准格式为 [field]: value\n 。支持的字段:data [必需]:实际推送的数据内容。event [非必需]:表示自定义的事件类型(默认情况下是 message 事件)。id [非必需]:数据的唯一标识符,类似于数据的编号。retry [非必需]:用于告诉浏览器在断开连接后,重新发起连接需要等待的时间间隔。

复制代码
event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: end\n
data: a bar event\n\n

客户端 API 的处理:在前端浏览器中,处理 SSE 数据流非常简便,主要依赖于浏览器内置的 EventSource API。 开发者只需实例化一个 EventSource 对象并指向服务端的流接口,然后通过挂载 onmessage 事件(或监听自定义的 event 事件),即可实时获取 event.data 中的数据并渲染到页面上。

服务端代码实现:

java 复制代码
package com.yang.ai.controller;

import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

@RestController
@RequestMapping("/sse")
public class SseController {
    @RequestMapping("/data")
    public void data(HttpServletResponse response) throws IOException, InterruptedException {
        response.setContentType("text/event-stream;charset=utf-8");
        PrintWriter writer = response.getWriter();
        for (int i = 0; i < 20; i++) {
            String s = "data: " + new Date() + "\n\n";
            writer.write(s);
            writer.flush();
            Thread.sleep(1000L);
        }
    }
}

客户端 API:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SSE</title>
</head>
<body>
    <div id="sse"></div>
    <script>
        let eventSource = new EventSource('/sse/data');
        eventSource.onmessage = function(event) {
            document.getElementById('sse').innerHTML += event.data + '<br/>';
        }
    </script>
</body>
</html>
java 复制代码
@RequestMapping("/retry")
public void retry(HttpServletResponse response) throws IOException {
    log.info("发起请求: retry");
    response.setContentType("text/event-stream;charset=utf-8");
    PrintWriter writer = response.getWriter();
    String s = "retry: 2000\n";
    s += "data: " + new Date() + "\n\n";
    writer.write(s);
    writer.flush();
}
html 复制代码
<div id="sse"></div>
<script>
    let eventSource = new EventSource('/sse/retry');
    eventSource.onmessage = function(event) {
        document.getElementById('sse').innerHTML += event.data + '<br/>';
    }

    eventSource.onmessage
</script>
java 复制代码
@RequestMapping("/event")
public void event(HttpServletResponse response) throws IOException, InterruptedException {
    log.info("发起请求: event");
    response.setContentType("text/event-stream;charset=utf-8");
    PrintWriter writer = response.getWriter();
    for (int i = 0; i < 10; i++) {
        String s = "event: foo\n";
        s += "data: " + new Date() + "\n\n";
        writer.write(s);
        writer.flush();
        Thread.sleep(1000L);
    }
}
html 复制代码
<script>
    let eventSource = new EventSource('/sse/event');
    eventSource.addEventListener("foo", (event) => {
        document.getElementById('sse').innerHTML += event.data + '<br/>';
    })
</script>

2.2. Spring 中 SSE 实现

Spring 从 4.2 版本开始就已经支持 SSE。从 Spring 5 开始,推荐使用 WebFlux 框架来更优雅地实现 SSE 协议,其核心 API 是 Flux。

在 WebFlux 中,可以将 Flux 想象成一条数据传送带,它具备以下三大核心特点:

  1. 异步传送:数据就像快递包裹一样逐个到达,接收方不需要等待所有数据全部到齐即可开始处理。
  2. 灵活加工:支持在中途通过操作符对数据进行修改,如过滤、转换等。
  3. 弹性控制:接收方可以通过"背压机制"来调控数据的接收速度。

使用 Flux 进行流式编程通常遵循以下固定流程:

  1. 创建 Flux:建立一个 Flux 数据流并指定其数据源。
  2. 处理数据:调用相关的操作符(如 map 等)对流转中的数据进行加工。
  3. 订阅数据:通过 subscribe() 订阅 Flux 来消费数据。注意: 只有触发订阅动作后,数据才会真正开始流动。

以下是 Flux 流中常见操作符:

操作符 作用 示例代码片段
map() 元素一对一转换 .map(String::toUpperCase)
filter() 条件过滤 .filter(s -> s.length() > 5)
take() 限制元素数量 .take(2)
merge() 合并多个 Flux (不保证顺序) Flux.merge(Flux.just("A"), Flux.just("B"))
concat() 顺序拼接多个 Flux (保证顺序) Flux.concat(Flux.just("A"), Flux.just("B"))
delayElements() 延迟元素发射 .delayElements(Duration.ofSeconds(1))
java 复制代码
@RequestMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
    return Flux.interval(Duration.ofSeconds(1)).map(s -> new Date().toString());
}
html 复制代码
<script>
    let eventSource = new EventSource('/sse/stream');
    eventSource.onmessage = function(event) {
        document.getElementById('sse').innerHTML += event.data + '<br/>';
    }
</script>

三、ChatModel

3.1. 概述

java 复制代码
package com.yang.ai.controller;

import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/ds")
public class DeepSeekChatModel {
    @Autowired
    private OpenAiChatModel chatModel;

    @GetMapping("/chat")
    public String chat(String message) {
        return chatModel.call(message);
    }
}

ChatModel 抽象了应用程序与 AI 模型交互的过程。它的工作原理是接收 Prompt(提示词)作为输入,将请求发送给后端大模型,并返回 ChatResponse 对象作为输出。虽然它提供了一个简单的 String call(String message) 方法,但这个方法本质上是对传入的字符串进行了封装,底层依然是调用核心的 ChatResponse call(Prompt prompt) 方法。这样设计是为了简化初学者的使用,避免处理复杂的输入输出解析。

以下是 ChatModel 的定义:

java 复制代码
public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingChatModel {
    default String call(String message) {
        Prompt prompt = new Prompt(new UserMessage(message));
        Generation generation = this.call(prompt).getResult();
        return generation != null ? generation.getOutput().getText() : "";
    }

    default String call(Message... messages) {
        Prompt prompt = new Prompt(Arrays.asList(messages));
        Generation generation = this.call(prompt).getResult();
        return generation != null ? generation.getOutput().getText() : "";
    }
......
}

3.2. 实现简单对话

如果不使用简化的 call(String) 方法,开发者需要手动构建 Prompt 对象来进行交互。通过 new Prompt(message) 将用户文本包装为提示词对象,调用 call() 后,从返回的 ChatResponse 对象中层层解析获取纯文本结果。

java 复制代码
@GetMapping("/chatByPrompt")
public String chatByPrompt(String message) {
    Prompt prompt = new Prompt(message);
    ChatResponse response = chatModel.call(prompt);
    return response
            .getResult()
            .getOutput()
            .getText();
}

测试接口:http://127.0.0.1:8080/ds/chatByPrompt?message=如果让奥格威给特斯拉汽车写广告语,他会怎么写?

3.3. 角色预设

ChatModel 支持通过 Prompt 来预设角色,这是引导大模型输出特定风格或专业内容的核心手段。首先需要分别创建 SystemMessage 和 UserMessage 。将这两种消息组合成一个 List,并封装进 Prompt 中传递给 ChatModel 执行。模型会结合整个对话历史(包括系统指令)生成符合上下文的连贯回复。

java 复制代码
@GetMapping("/role")
public String role(String message) {
    SystemMessage systemMessage = new SystemMessage("你叫编码助手,擅长 Java 语法和集合框架。");
    UserMessage userMessage = new UserMessage(message);

    Prompt prompt = new Prompt(systemMessage, userMessage);
    ChatResponse response = chatModel.call(prompt);

    return response
            .getResult()
            .getOutput()
            .getText();
}

测试接口:http://127.0.0.1:8080/ds/role?message=你是谁?

3.4. 实现流式输出

为了解决大段文本生成导致的长时间等待问题,提升用户体验,ChatModel 提供了流式输出能力。

java 复制代码
@RequestMapping(value = "/stream", produces = "text/html;charset=utf-8")
public Flux<String> stream(String message) {
    Prompt prompt = new Prompt(message);
    Flux<ChatResponse> response = chatModel.stream(prompt);
    return response.map(x -> x.getResult().getOutput().getText());
}

测试接口:http://127.0.0.1:8080/ds/stream?message=你是谁?

3.5. ChatModel 与 ChatClient 的核心区别

这两个接口是 Spring AI 与大模型交互的两大基石,但设计理念和适用场景不同:

  • ChatModel(底层与灵活):它是底层的通信接口,直接与大模型(如通义千问、OpenAI 等)交互。它只提供基础的 call 和 stream 方法,开发者需要手动处理提示词组装、参数配置和响应解析等细节。优势在于高度灵活,适合深度定制。
  • ChatClient(高阶与高效):它是基于 ChatModel 进行的封装和增强。通过 Fluent API(流式 API),它屏蔽了底层复杂的交互细节,自动集成了提示词管理、响应格式化、结构化输出映射(如自动转为实体类)等能力,能大幅提高日常业务开发效率。
相关推荐
不会写DN1 小时前
Golang中实时推送的功臣 - WebSocket
开发语言·后端·golang
星辰_mya1 小时前
无锁编程:并发的“珠穆朗玛峰”与 F1 的“无缝换挡”
java·开发语言·面试
温柔一只鬼.1 小时前
Java GUI 制作 贪吃蛇小游戏
java·开发语言
昵称只能一个月修改一次。。。1 小时前
并发服务器、多路IO复用
java·服务器·网络
Yvonne爱编码2 小时前
二叉树高频题精讲 | 从入门到熟练掌握二叉树操作
java·开发语言·数据结构·链表·二叉树
wuqingshun3141592 小时前
说说java中实现多线程有几种方法
java·开发语言·jvm
于眠牧北2 小时前
重写RedisTemplate后在lua脚本中传递参数不需要二次转换
java·junit·lua
深蓝轨迹2 小时前
SQL优化及实战分享
java·数据库·sql
毕业设计-小慧2 小时前
计算机毕业设计springboot电影选座与订票系统 基于SpringBoot的影院在线票务管理平台 基于SpringBoot的智能影厅座位预约系统
spring boot·后端·课程设计