今天学习下Spring AI 2.0.0:

  • 第一个Spring AI Hello World
  • 结构化输出
  • Tool Calling
  • RAG
  • Advisor

环境要求

  • JDK : temurin-lts-jdk (25.0.3-9.0.LTS)
  • Spring Ai : 2.0.0
  • SpringBoot : 4.1.0
  • Gradle : 9.5.1

创建项目

创建正常的Spring项目即可,添加的依赖有:

  • spring-boot-starter-web
  • spring-boot-starter-webflux
  • spring-ai-starter-model-openai
  • lombok
  • spring-boot-devtools
  • junit
  • spring-ai-bom

我选择的是Gradle,下面就贴一下build.gradle,创建好应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
plugins {  
id 'java'
id 'org.springframework.boot' version '4.1.0'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'cn.net.dev'
version = '0.0.1-SNAPSHOT'
description = 'springai'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}

repositories {
mavenCentral()
}

ext {
set('springAiVersion', "2.0.0")
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.ai:spring-ai-starter-model-openai' //换模型可以换这个依赖
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testCompileOnly 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'
}

dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
}
}

tasks.named('test') {
useJUnitPlatform()
}

Hello Spring Ai

先来第一个SpringAI程序

application.yml

1
2
3
4
5
6
7
8
9
spring:  
application:
name: springai
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com/v1
chat:
model: deepseek-v4-flash

即使添加了OpenAI的模块,但其实只要是兼容OpenAI协议的都可以配置上去。

ChatController

创建controller包,在里面新建一个ChatController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package cn.net.dev.springai.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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
public class ChatController {

private final ChatClient chatClient;
// 使用构造器自动注入
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个个专业的Java技术顾问,回答问题简洁准确")
.build();
}

/**
* 普通模式
* @param message
* @return
*/
@RequestMapping("/chat")
public String chat(@RequestParam String message) {
return chatClient
.prompt() // 提示词
.user(message) // 用户输入的内容
.call() // 调用模型
.content(); // 返回数据
}

/**
* 流式
* @param message
* @return
*/
@GetMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8")
public Flux<String> streamChat(@RequestParam String message) {
return chatClient.prompt().user(message).stream().content();
}
}

对于IDEA,打开运行/调试配置,点击修改选项(alt+M),点击环境变量(alt+E),在添加的环境变量表单里,输入如下内容:

1
DEEPSEEK_API_KEY=你的DeepSeek API KEY

或者创建.env文件,也可以直接设置系统级环境变量。总之,能读取出来即可。

启动项目后,在浏览器输入http://127.0.0.1:8080/chat?message=JVM的内存结构是怎样的?,此时浏览器就会返回如下数据:

1
JVM 内存主要分为以下几个区域: - **程序计数器**:线程私有,记录当前线程执行的字节码行号。 - **虚拟机栈**:线程私有,存储局部变量表、操作数栈、动态链接、方法出口等。每个方法调用对应一个栈帧。 - **本地方法栈**:线程私有,为 Native 方法服务。 - **堆**:线程共享,存放对象实例和数组。是垃圾回收的主要区域,可细分为新生代(Eden、Survivor)和老年代。 - **方法区(元空间)**:线程共享,存储已被加载的类信息、常量、静态变量等。JDK 8 后元空间取代永久代,使用本地内存。 此外,还有**运行时常量池**(属于方法区的一部分)和**直接内存**(NIO 使用的堆外内存)。

同样的方式,请求http://127.0.0.1:8080/stream?message=JVM的内存结构是怎样的?。这样,一个Spring AIHello World就完成了。

结构化输出

Spring AI 2.0支持把模型输出映射到Java对象,地产自动处理JSON解析和类型转换。

单对象输出

ChatController中,继续加入如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
record Product(  
String name,
List<String> prop,
List<String> cons,
int score,
String recommendation
) {
}

@GetMapping("/analyze")
public Product analyzeProduct(@RequestParam String productName) {
return chatClient.prompt()
.user("分析产品:" + productName + ",给出优缺点和推荐指数")
.call()
.entity(Product.class);
}

然后打开浏览器,输入http://127.0.0.1:8080/analyze?productName=美的热水壶,此时就会输出如下内容跟:

1
{"name":"美的热水壶","prop":["加热速度快","自动断电防干烧","双层防烫设计","大容量适合家庭使用"],"cons":["部分型号壶盖密封圈易老化","烧水噪音相对较大"],"score":9,"recommendation":"性价比高,适合注重安全和耐用性的家庭用户"}

列表输出

ChatController中,继续加入如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
record Menu(  
String name,
String ingredient,
String calories
){}

@RequestMapping("/menu")
public List<Menu> recommendMenu(@RequestParam String scene) {
return chatClient.prompt()
.user("为以下人群推荐5种菜谱:" + scene)
.call()
.entity(new ParameterizedTypeReference<List<Menu>>() {
});
}

启动项目后,在浏览器输入:http://127.0.0.1:8080/menu?scene=减脂,此时就会输出如下:

1
[{"name":"蒜蓉西兰花炒鸡胸","ingredient":"鸡胸肉、西兰花、蒜、橄榄油","calories":"150 kcal"},{"name":"柠檬烤三文鱼配芦笋","ingredient":"三文鱼、芦笋、柠檬、黑胡椒","calories":"200 kcal"},{"name":"番茄菠菜豆腐汤","ingredient":"豆腐、菠菜、番茄、洋葱","calories":"180 kcal"},{"name":"凉拌黄瓜鸡蛋丝","ingredient":"黄瓜、鸡蛋、醋、盐","calories":"120 kcal"},{"name":"双椒炒海鲜","ingredient":"虾仁、鱿鱼、青椒、红椒","calories":"160 kcal"}]

Tool Calling

Tool Calling是让大模型具备执行力的关键。模型判断什么情况下会调用工具,Spring AI负责执行并把结果返回给模型.
下面模拟两个场景,定义两个工具的实际功能:
service/WeatherService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package cn.net.dev.springai.service;  

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class WeatherService {
public String getWeather(String cityName) {
log.info("大模型调用本工具........");
return """
🌍 城市: %s (%s)
☁️ 天气数据: %s
""".formatted("北京", "中国", "晴");
}
}
service/EmailService.java
package cn.net.dev.springai.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class EmailService {

public String send(String destEmail, String subject, String body) {
log.info("大模型调用发送邮件工具...");
log.info("Subject " + subject + " body " + body);
return "邮件已发送给" + destEmail;
}
}

定义工具
tools/MyTools.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package cn.net.dev.springai.tools;  

import cn.net.dev.springai.service.EmailService;
import cn.net.dev.springai.service.WeatherService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyTools {

@Autowired
private WeatherService weatherService;

@Autowired
private EmailService emailService;

@Tool(description = "查询指定城市的实时天气信息")
public String getWeather(@ToolParam(description = "城市名称,例如:北京") String cityName) {
System.out.println(cityName);
;
return weatherService.getWeather(cityName);
}

@Tool(description = "发送邮件")
public String sendMail(@ToolParam(description = "收件人邮箱") String destEmail,
@ToolParam(description = "邮件标题") String subject,
@ToolParam(description = "邮件内容") String body) {
return emailService.send(destEmail, subject, body);

}
}

注册工具到ChatClient
controller/ChatController

1
2
3
4
5
6
7
8
9
10
11
private final ChatClient chatClient;  

private MyTools tools;

public ChatController(ChatClient.Builder builder,MyTools tools) {
this.chatClient = builder
.defaultSystem("你是一个个专业的Java技术顾问,回答问题简洁准确")
.build();

this.tools = tools;
}

然后新增一个接口

1
2
3
4
5
6
7
8
@RequestMapping("/testTools")  
public String testTools(@RequestParam String question) {
return chatClient
.prompt()
.user(question)
.tools(tools) // 注册工具类,由模型判断是否调用
.call().content();
}

尝试发送:

127.0.0.1:8080/testTools?question=今天北京天气怎么样?,此时,会返回硬编码的天气:

1
2
3
4
北京今天天气晴朗 ☀️,适合外出活动。

# 控制台也会打印日志:
大模型调用本工具........

127.0.0.1:8080/testTools?question=给contact@dev.net.cn发送邮件,说明天早晨9点要开产品调研会?,此时返回:

1
2
3
4
5
6
7
8
邮件已成功发送至 **contact@dev.net.cn**。 **邮件内容摘要:** - **主题:** 产品调研会通知 - **正文:** 提醒明天早晨9点准时参加产品调研会 如有需要调整会议时间或补充其他信息,请随时告知。

2026-07-19T14:57:47.028+08:00 INFO 18316 --- [springai] [nio-8080-exec-5] c.net.dev.springai.service.EmailService : 大模型调用发送邮件工具...
2026-07-19T14:57:47.029+08:00 INFO 18316 --- [springai] [nio-8080-exec-5] c.net.dev.springai.service.EmailService : Subject 产品调研会通知 body 您好,

通知您明天(即次日)早晨9点将召开产品调研会,请准时参加。

谢谢!

RAG

RAG(检索增强生成),解决大模型因训练数据时效问题,导致出现幻觉,胡乱回答,同时也是解决内部资料能够被大模型读取的场景。

安装向量数据库

就是加了个vector类型的PostgreSQL 17

1
2
3
4
5
6
7
docker run -d \
--name pgvector-db \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=testdb \
-p 5432:5432 \
pgvector/pgvector:pg17

添加依赖

打开build.gradle,新增依赖:

1
2
3
4
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.ai:spring-ai-vector-store-advisor'
implementation 'org.springframework.ai:spring-ai-starter-vector-store-pgvector'
implementation 'org.postgresql:postgresql:42.7.11'

修改application.yml

修改支持embedding的模型,目前国内支持embedding的有:qwen3.7-text-embeddingBGE-M3m3e-base。目前我使用的是qwen

然后加入datasourcevectorstore的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
spring:  
application:
name: springai
datasource:
url: jdbc:postgresql://127.0.0.1:5432/testdb
username: postgres
password: postgres

ai:
openai:
api-key: ${QWEN_API_KEY}
chat:
model: glm-5.2
# 改成你自己的工作空间地址
base-url: https://ws-xxxxx.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
embedding:
model: qwen3.7-text-embedding
# 改成你自己的工作空间地址
base-url: https://ws-xxxxxxxx.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
# api-key: ${DEEPSEEK_API_KEY}
# base-url: https://api.deepseek.com/v1
# chat:
# model: deepseek-v4-flash
vectorstore:
pgvector:
initialize-schema: true
index-type: HNSW
distance-type: cosine_distance
# 注意这个维度,和建表的时候要一致
dimensions: 1024
schema-name: public
table-name: vector_store

创建表(实际上配置中配置了自动创建的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE IF NOT EXISTS public.vector_store (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
metadata JSONB,
embedding VECTOR(1024) --和配置文件中一致
);

-- 创建 HNSW索引 还有一个是IVFFlat(估计是生产用的吧)
CREATE INDEX IF NOT EXISTS vector_store_embedding_idx
ON public.vector_store
USING HNSW (embedding vector_cosine_ops);
--

RAG入库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
controller/DocumentIngestionController
package cn.net.dev.springai.controller;

import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class DocumentIngestionController {

@Autowired
private VectorStore vectorStore;

@Autowired
private TokenTextSplitter tokenTextSplitter;

@PostMapping("/load")
public String loadDocument() {
// 模拟读取文档
var doc1 = new Document("Spring AI 2.0 与 2026年7月31日正式发布,支持Java 25 和虚拟线程");
var doc2 = new Document("詹姆斯高斯林最喜欢的语言是Go语言");

// 文本切分、将长文档切分成适合模型处理的快
List<Document> splitDocs = tokenTextSplitter.apply(List.of(doc1, doc2));
// 向量化存储:将每个文本块转换为向量并存入向量数据库
vectorStore.add(splitDocs);
return "%d 个文档已经存入向量数据库pgvector".formatted(splitDocs.size());
}
}

启动项目后,调用该接口,尝试将模拟的文档写入到向量数据库。

1
2
3
POST http://localhost:8080/load

# 2 个文档已经存入向量数据库pgvector

查看SQL中是否存在

1
2
3
4
5
6
7
SELECT 
id,
LEFT(content, 50) as content_preview,
metadata,
embedding
FROM vector_store
LIMIT 3;

RAG问答

接下来就试试,利用RAG知识库去回答问题
controller/RagController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package cn.net.dev.springai.controller;  

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RagController {


private final ChatClient chatClient;
@Autowired
private VectorStore vectorStore;

public RagController(final ChatClient.Builder builder) {
this.chatClient = builder.build();
}

@GetMapping("/ask")
public String askRagQuestion(String question) {

var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(3).similarityThreshold(0.7).build())
.build();

return chatClient.prompt().user(question)
.advisors(qaAdvisor)
.call()
.content();

}
}

调用接口,问问题:

1
http://127.0.0.1:8080/ask?question=詹姆斯高斯林最喜欢的语言是什么

按照之前入库的信息,接口会返回:

1
根据提供的信息,詹姆斯高斯林最喜欢的语言是Go语言。

Advisor链

Advisors API提供了一种灵活且强大的方式,拦截、修改和增强Spring应用中的AI驱动交互。调用带有用户文本的AI模型时,可以添加或补充上下文数据,就像拦截器一样。

创建一个自己的Advisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
component/AuditAdvisor.java
package cn.net.dev.springai.compoent;

import cn.net.dev.springai.repository.AuditLogRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import java.util.Objects;
import java.util.UUID;

@Slf4j
@Component
public class AuditAdvisor implements CallAdvisor {

private final AuditLogRepository auditLog;

public AuditAdvisor(AuditLogRepository auditLog) {
this.auditLog = auditLog;
}

@Override
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
log.info("====== AuditAdvisor ======");
// 前置请求
String requestId = UUID.randomUUID().toString();
auditLog.logRequest(requestId, chatClientRequest.prompt().toString());
// 执行链中的下一个Advisor
ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);

if (chatClientResponse.chatResponse() != null) {
// 后置:记录响应
auditLog.logResponse(requestId, Objects.requireNonNull(chatClientResponse.chatResponse().getResult()).getOutput().getText());
}
return chatClientResponse;
}

@Override
public String getName() {
return "AuditAdvisor";
}

@Override
public int getOrder() {
// 最先执行
return Ordered.HIGHEST_PRECEDENCE;
}
}

注册AuditAdvisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
config/ChatClientConfig.java
package cn.net.dev.springai.config;

import cn.net.dev.springai.compoent.AuditAdvisor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfig {

@Bean
public ChatClient chatClientWithMemory(
ChatClient.Builder builder,
VectorStore vectorStore,
// 注入自己定义的Advisor
AuditAdvisor auditAdvisor
) {
ChatMemory chatMemory = MessageWindowChatMemory.builder().maxMessages(20).build();
return builder.defaultAdvisors(
// 别忘了注入进来
auditAdvisor,
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(5).build()
).build()
).defaultSystem("我是李大爷,一名专业摸鱼工程师").build();
}

}

创建一个repositoryrepository/AuditLogRepository.java

1
2
3
4
5
6
package cn.net.dev.springai.repository;  

public interface AuditLogRepository {
void logRequest(String requestId, String content);
void logResponse(String requestId, String content);
}

实现类 repository/InMemoryAuditLogRepository.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.net.dev.springai.repository;  

import org.springframework.stereotype.Component;

@Component
public class InMemoryAuditLogRepository implements AuditLogRepository {
@Override
public void logRequest(String requestId, String content) {
System.out.println("[REQUEST][" + requestId + "] " + content);
}

@Override
public void logResponse(String requestId, String content) {
System.out.println("[RESPONSE][" + requestId + "] " + content);
}
}

接入Controller

改一下之前的ChatController就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package cn.net.dev.springai.controller;  

import cn.net.dev.springai.tools.MyTools;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.List;

@RestController
public class ChatController {

private final ChatClient chatClient;

// 这里必须用刚才配置的,否则不生效
private final ChatClient memoryChatClient;

private MyTools tools;

public ChatController(ChatClient.Builder builder, MyTools tools, ChatClient memoryChatClient) {
// 相当于重新创建的
this.chatClient = builder
.defaultSystem("你是一个个专业的Java技术顾问,回答问题简洁准确")
.build();

this.tools = tools;
// 注入ChatClientConfig定义的
this.memoryChatClient = memoryChatClient;
}

/**
* 普通模式
*
* @param message
* @return
*/
@GetMapping("/chat")
public String chat(@RequestParam String message, HttpServletRequest request) {
String sessionId = request.getSession().getId();
// return chatClient.prompt().user(message).call().content();
return memoryChatClient.prompt().user(message)
.advisors(a -> a.param(
ChatMemory.CONVERSATION_ID,
sessionId
))
.call().content();
}

/**
* 流式
*
* @param message
* @return
*/
@GetMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8")
public Flux<String> streamChat(@RequestParam String message) {
return chatClient.prompt().user(message).stream().content();
}


record Product(
String name,
List<String> prop,
List<String> cons,
int score,
String recommendation
) {
}
@GetMapping("/analyze")
public Product analyzeProduct(@RequestParam String productName) {
return chatClient.prompt()
.user("分析产品:" + productName + ",给出优缺点和推荐指数")
.call()
.entity(Product.class);
}

record Menu(
String name,
String ingredient,
String calories
) {
}
@RequestMapping("/menu")
public List<Menu> recommendMenu(@RequestParam String scene) {
return chatClient.prompt()
.user("为以下人群推荐5种菜谱:" + scene)
.call()
.entity(new ParameterizedTypeReference<List<Menu>>() {
});
}

@RequestMapping("/testTools")
public String testTools(@RequestParam String question) {
return chatClient
.prompt()
.user(question)
.tools(tools) // 注册工具类,由模型判断是否调用
.call().content();
}
}

调配用接口:http://127.0.0.1:8080/chat?message=你是谁?,接口返回:

1
我是李大爷,一名专业摸鱼工程师。不过,根据提供的上下文信息,我无法回答“你是谁”这个问题,因为上下文中并未包含关于我身份的相关内容,仅提供了关于詹姆斯·高斯林和Spring AI 2.0的信息。

此时,控制台会打印:

1
2
3
4
2026-07-19T19:03:38.908+08:00  INFO 21532 --- [springai] [nio-8080-exec-1] c.n.dev.springai.compoent.AuditAdvisor   : ====== AuditAdvisor ======
[REQUEST][6689000b-85f0-41ab-a34f-9b82cdef2681] Prompt{messages=[SystemMessage{textContent='我是李大爷,一名专业摸鱼工程师', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='你是谁?', metadata={messageType=USER}, messageType=USER}], modelOptions=org.springframework.ai.openai.OpenAiChatOptions@3571c333}
[RESPONSE][6689000b-85f0-41ab-a34f-9b82cdef2681] 我是李大爷,一名专业摸鱼工程师。不过,根据提供的上下文信息,我无法回答“你是谁”这个问题,因为上下文中并未包含关于我身份的相关内容,仅提供了关于詹姆斯·高斯林和Spring AI 2.0的信息。
已与地址为 ''127.0.0.1:62287',传输: '套接字'' 的目标虚拟机断开连接