Spring AI 摸鱼指南:ChatClient 是接线员,PromptTemplate 是剧本,组合起来就是开发神器
作者说在前头:这篇文章不是让你背 API 文档的,而是让你笑着学会怎么用 Spring AI 开发应用。如果你看完还觉得 Spring AI 是个玄学,那一定是我写得不够接地气,欢迎来打我(开玩笑的)。
重要提示:本文基于 Spring AI 1.1.x 稳定版本编写,代码示例可直接运行。使用里程碑版本(如 M4)可能存在 API 变更,请谨慎使用。
一、开篇:Spring AI 是什么?——你的"翻译官+保姆"
想象一下这个场景:你是个 Java 程序员,老板突然说"咱们接个大模型,做个智能客服吧"。你打开 ChatGPT 的 API 文档,看着一堆 HTTP 请求、JSON 解析、错误处理,瞬间头大——这玩意儿比写 CRUD 复杂多了!
Spring AI 就是来拯救你的。它不是什么高大上的黑科技,而是 Spring 官方推出的一个框架,专门帮 Java 程序员"驯服"大模型。你可以把它理解成:
- 翻译官:把 Java 代码翻译成大模型能听懂的话,不用你手写 HTTP 请求
- 保姆:自动处理连接、重试、错误处理这些脏活累活,你只管写业务逻辑
今天我们要聊的两个核心概念——ChatClient 和 PromptTemplate,就是 Spring AI 里的"黄金搭档":
- PromptTemplate:大模型的"剧本模板",负责把提示词写得规范、可复用
- ChatClient:你的"大模型专属接线员",负责帮你传话、把回复带回来
有了这俩,开发 AI 应用就像点外卖一样简单:选好模板(剧本),告诉接线员(ChatClient)要什么,坐等结果。
二、Spring AI 基础准备:先有锅和油,才能炒菜
2.1 环境要求:版本匹配很重要
就像炒菜要先有锅和油,用 Spring AI 之前,你得先准备好这些:
- JDK 17+:别拿 JDK 8 硬刚,Spring AI 最低要求 JDK 17,不然启动就报错,就像用平底锅做爆炒,会翻车的
- Spring Boot 3.1+:Spring AI 是基于 Spring Boot 3.x 的,2.x 版本不支持
版本匹配关系(重要!):
- Spring AI 1.0.x → 适配 Spring Boot 3.1.x - 3.2.x
- Spring AI 1.1.x → 适配 Spring Boot 3.2.x - 3.3.x
- 里程碑版本(如 1.0.0-M4)仅适配特定 Spring Boot 版本,且 API 可能变更,生产环境不推荐使用
Maven 依赖配置(使用稳定版本):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<!-- Spring AI 全家桶套餐,一键配齐所有工具 -->
<!-- 使用稳定版本,避免 API 变更导致的问题 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
<!-- 如果要用其他大模型,比如智谱 AI -->
<!--
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
-->
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Gradle 配置:
dependencies {
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.1.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
关键依赖说明:
spring-ai-openai-spring-boot-starter:这是 Spring AI 的"全家桶套餐",包含了 ChatClient、PromptTemplate 等所有核心组件,还自动配置好了连接池、重试机制等,不用你手写- 版本选择:优先使用稳定版本(1.1.x),里程碑版本(M4、M5)仅用于尝鲜,生产环境请使用稳定版
2.2 配置 API 密钥:别把密钥写在代码里,钱包会哭
重要警告:千万别把 API 密钥直接写在代码里!我见过好几个程序员把密钥硬编码在代码里,结果代码一上传 GitHub,第二天就收到"大模型欠费账单",几百块说没就没。
正确做法:用环境变量或配置文件
在 application.yml 中配置:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY} # 从环境变量读取,安全第一
# 自定义 API 端点 URL(可选,默认使用官方端点)
# 使用场景:① 通过代理访问(国内访问 OpenAI);② 使用自建大模型服务;③ 使用不同区域的 API 端点
base-url: https://api.openai.com # 默认值,可改为代理地址或自建服务地址
chat:
options:
model: gpt-3.5-turbo # 指定模型,gpt-3.5-turbo 或 gpt-4
temperature: 0.7 # 生成随机性(0-2),0.7 是平衡值
max-tokens: 500 # 最大回复令牌数(注意:gpt-3.5-turbo 默认上限 4096,gpt-4 上限更高)
或者在 application.properties 中:
spring.ai.openai.api-key=${OPENAI_API_KEY}
# 自定义 API 端点 URL(可选)
spring.ai.openai.base-url=https://api.openai.com
spring.ai.openai.chat.options.model=gpt-3.5-turbo
spring.ai.openai.chat.options.temperature=0.7
spring.ai.openai.chat.options.max-tokens=500
自定义 API URL 的使用场景:
-
通过代理访问 OpenAI(国内开发者常用):
spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: https://your-proxy-server.com # 代理服务器地址 -
使用自建大模型服务(如本地部署的模型):
spring: ai: openai: api-key: your-local-api-key # 自建服务的密钥 base-url: http://localhost:8080/v1 # 自建服务的 API 地址注意:自建服务需要兼容 OpenAI API 格式,否则可能无法正常工作。
-
使用不同区域的 API 端点:
spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: https://api.openai.com # 默认美国区域 # 或使用其他区域(如果 OpenAI 提供) # base-url: https://api-eu.openai.com # 欧洲区域(示例)
设置环境变量(Linux/Mac):
export OPENAI_API_KEY=sk-xxxxxxxxxxxxx
Windows 设置:
set OPENAI_API_KEY=sk-xxxxxxxxxxxxx
生产环境配置密钥的落地方法:
-
使用配置中心(Nacos):
spring: cloud: nacos: config: server-addr: localhost:8848 file-extension: yml在 Nacos 控制台创建配置,key 为
spring.ai.openai.api-key,value 为加密后的密钥。 -
使用 Kubernetes Secret:
# 在 Spring Boot 中通过环境变量读取 env: - name: OPENAI_API_KEY valueFrom: secretKeyRef: name: openai-secret key: api-key -
API 密钥权限最小化:
- OpenAI:在 API 密钥设置中,仅开通
chat.completions接口权限 - 设置调用额度限制(如每月 100 美元),避免异常调用导致巨额费用
- 定期轮换密钥,降低泄露风险
- OpenAI:在 API 密钥设置中,仅开通
三、核心概念 1:PromptTemplate——大模型的"剧本模板"
3.1 什么是 PromptTemplate?——拍电影的剧本模板
PromptTemplate 就像拍电影的剧本模板:固定好台词框架,只需要填演员名字、场景就能直接用。
没有模板时的痛点:
想象一下,每次写提示词都要重新拼接字符串:
String prompt = "请帮我写一段关于" + topic + "的朋友圈文案,要求" + length + "字以内,带点小幽默";
这样写有几个问题:
- 容易出错:少个引号、多个空格,大模型就"一脸懵"
- 不可复用:换个场景又要重新写,就像"每次写信都要重新发明信封格式"
- 难维护:提示词改个标点,要在代码里找半天
有了 PromptTemplate 后:
import org.springframework.ai.chat.prompt.PromptTemplate;
import java.util.HashMap;
import java.util.Map;
PromptTemplate template = new PromptTemplate(
"请帮我写一段关于 {topic} 的朋友圈文案,要求 {length} 字以内,带点小幽默,别太官方"
);
就像填快递单,固定格式,只换收件人信息。
3.2 核心特性:占位符语法和模板复用
占位符语法:用 {} 包裹变量名
PromptTemplate template = new PromptTemplate(
"请给 {name} 写一句 {festival} 祝福语"
);
渲染模板:传入参数,生成最终提示词
Map<String, Object> variables = new HashMap<>();
variables.put("name", "张三");
variables.put("festival", "春节");
String renderedPrompt = template.render(variables);
// 结果:请给 张三 写一句 春节 祝福语
模板复用:一个模板可以给不同的业务场景用,就像"万能检讨书模板",改个名字就能交差。
3.3 高级特性:默认值、多段模板、模板校验
1. 设置默认值:如果某个参数未传入,使用默认值
PromptTemplate template = PromptTemplate.create(
"请帮我写一段关于 {topic} 的朋友圈文案,要求 {length} 字以内"
)
.withDefault("length", 50); // 如果未传入 length,默认使用 50
Map<String, Object> variables = new HashMap<>();
variables.put("topic", "摸鱼学 Spring AI");
// 不传 length,会自动使用默认值 50
String prompt = template.render(variables);
2. 多段模板拼接:组合多个模板片段
PromptTemplate systemTemplate = new PromptTemplate("你是一个专业的朋友圈文案写手");
PromptTemplate userTemplate = new PromptTemplate("请写一段关于 {topic} 的文案");
// 在实际使用中,systemTemplate 会作为系统提示,userTemplate 作为用户提示
// 这涉及到 Prompt 对象的多角色消息,后面会详细说明
3. PromptTemplate 与 Prompt 的关系:
PromptTemplate渲染后生成Prompt对象Prompt包含多个Message(系统提示、用户提示等),支持多角色对话- 简单场景下,
PromptTemplate.render()返回字符串即可;复杂场景需要构建Prompt对象
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
// 构建多角色 Prompt
Prompt prompt = new Prompt(
Arrays.asList(
new SystemMessage("你是一个专业的朋友圈文案写手"),
new UserMessage("请写一段关于摸鱼学 Spring AI 的文案")
)
);
3.4 实战案例:朋友圈文案生成器(完整版,含异常处理)
完整代码实现:
package com.example.springai.service;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
@Service
public class WeChatMomentService {
// 定义模板:固定格式,只换话题
private final PromptTemplate template = PromptTemplate.create(
"帮我写一段关于 {topic} 的朋友圈文案,要求 {length} 字以内,带点小幽默,别太官方"
)
.withDefault("length", 50); // 设置默认长度
/**
* 生成朋友圈文案提示词
* @param topic 话题,比如"摸鱼学 Spring AI"
* @param length 文案长度(可选,默认 50)
* @return 渲染后的提示词(可以传给大模型)
* @throws IllegalArgumentException 如果 topic 为空
*/
public String generatePrompt(String topic, Integer length) {
// 参数校验:生产环境必须做
if (!StringUtils.hasText(topic)) {
throw new IllegalArgumentException("话题不能为空");
}
Map<String, Object> variables = new HashMap<>();
variables.put("topic", topic);
if (length != null && length > 0) {
variables.put("length", length);
}
try {
// 渲染模板,生成最终提示词
return template.render(variables);
} catch (Exception e) {
// 模板渲染失败(如占位符错误),记录日志并抛出异常
throw new RuntimeException("模板渲染失败: " + e.getMessage(), e);
}
}
}
测试代码(含完整导入):
package com.example.springai;
import com.example.springai.service.WeChatMomentService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class WeChatMomentServiceTest {
@Autowired
private WeChatMomentService service;
@Test
void testGeneratePrompt() {
String prompt = service.generatePrompt("摸鱼学 Spring AI", null);
System.out.println(prompt);
assertNotNull(prompt);
assertTrue(prompt.contains("摸鱼学 Spring AI"));
}
@Test
void testGeneratePromptWithLength() {
String prompt = service.generatePrompt("摸鱼学 Spring AI", 100);
System.out.println(prompt);
assertNotNull(prompt);
}
@Test
void testGeneratePromptWithEmptyTopic() {
// 测试异常处理
assertThrows(IllegalArgumentException.class, () -> {
service.generatePrompt("", null);
});
}
}
运行结果:
帮我写一段关于 摸鱼学 Spring AI 的朋友圈文案,要求 50 字以内,带点小幽默,别太官方
吐槽:有了模板,再也不用每次写提示词都薅秃头发了。一个模板,改个参数就能用,爽!
四、核心概念 2:ChatClient——你的"大模型专属接线员"
4.1 什么是 ChatClient?——你和大模型之间的接线员
ChatClient 就像你和大模型之间的接线员:你不用直接拨号(调用原生 API),只需要告诉接线员"我要发啥内容",它就帮你转接、传话、把对方的回复带回来。
没有 ChatClient 时的痛点:
以前调用大模型,你得手写 HTTP 请求:
// 手写 HTTP 请求(累死)
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
body.put("model", "gpt-3.5-turbo");
body.put("messages", Arrays.asList(
Map.of("role", "user", "content", prompt)
));
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
"https://api.openai.com/v1/chat/completions",
request,
Map.class
);
// 还要从 JSON 里抠字段(更累)
String content = ((Map)((List)((Map)response.getBody().get("choices")).get(0)).get("message")).get("content").toString();
写十几行代码,就为了发一条消息,累不累?
有了 ChatClient 后(正确用法):
import org.springframework.ai.chat.client.ChatClient;
// Spring AI 1.1.x 标准用法:链式调用
String response = chatClient.prompt()
.text(prompt)
.call()
.content();
一行链式调用搞定,爽!
4.2 核心特性:自动配置、链式调用、响应处理
自动配置:只要引入依赖,Spring Boot 就自动给你生成一个 ChatClient 实例,就像点外卖时自动送筷子,不用额外要。
@Service
public class AIService {
// 直接注入,不用自己 new
@Autowired
private ChatClient chatClient;
public String chat(String prompt) {
// Spring AI 1.1.x 标准用法:链式调用
return chatClient.prompt()
.text(prompt)
.call()
.content();
}
}
链式调用的完整流程:
chatClient.prompt():开始构建提示.text(prompt):设置用户消息内容.call():调用大模型 API.content():提取回复的文本内容
与 PromptTemplate 的联动:剧本写好了,接线员负责递剧本。
@Service
public class WeChatMomentService {
@Autowired
private ChatClient chatClient; // 接线员
private final PromptTemplate template = PromptTemplate.create(
"帮我写一段关于 {topic} 的朋友圈文案,要求 {length} 字以内,带点小幽默,别太官方"
)
.withDefault("length", 50);
/**
* 生成朋友圈文案(完整版,含异常处理)
*/
public String generateMoment(String topic, Integer length) {
// 1. 参数校验
if (!StringUtils.hasText(topic)) {
throw new IllegalArgumentException("话题不能为空");
}
try {
// 2. 渲染模板,生成提示词
Map<String, Object> variables = new HashMap<>();
variables.put("topic", topic);
if (length != null && length > 0) {
variables.put("length", length);
}
String prompt = template.render(variables);
// 3. 调用大模型,使用链式调用
String response = chatClient.prompt()
.text(prompt)
.call()
.content();
// 4. 校验响应(大模型可能返回空内容)
if (!StringUtils.hasText(response)) {
throw new RuntimeException("大模型返回内容为空");
}
return response;
} catch (Exception e) {
// 5. 异常处理:API 调用失败、网络异常、密钥过期等
// 生产环境应记录日志,并根据异常类型做不同处理
throw new RuntimeException("生成朋友圈文案失败: " + e.getMessage(), e);
}
}
}
响应处理:直接提取文本,不用从 JSON 里抠字段,就像外卖直接送上门,不用自己去后厨端。
4.3 自定义 ChatClient:何时需要自定义?
默认 ChatClient 已足够大多数场景,但在以下情况需要自定义:
- 指定特定模型(如强制使用 gpt-4)
- 设置默认参数(如全局 temperature、max-tokens)
- 配置重试策略(如失败重试 3 次)
自定义 ChatClient 示例:
package com.example.springai.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class SpringAIConfig {
/**
* 自定义 ChatClient:指定模型和默认参数
* 说明:Spring AI 的自动配置会扫描 spring.ai.openai.api-key,自动创建 ChatClient Bean,
* 一般无需手动声明。本示例仅演示「为什么需要自定义」:比如强制指定模型、默认温度、令牌上限或重试策略。
* @param chatModel 自动注入的 ChatModel(由 Spring AI 自动配置)
* @return 自定义的 ChatClient
*/
@Bean
@Primary // 标记为默认 Bean,避免多个 ChatClient 冲突
public ChatClient customChatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是一个专业的内容创作助手") // 设置默认系统提示
.defaultOptions(opts -> opts
.withTemperature(0.7f) // 设置默认温度
.withMaxTokens(500) // 设置默认最大令牌数
)
.build();
}
}
使用场景说明:
- 如果只是简单调用,直接用自动配置的
ChatClient即可 - 如果需要全局设置参数,才需要自定义 Bean
4.4 实战案例:朋友圈文案生成器(完整版,含异常处理)
完整代码实现(已在上文展示,这里补充测试代码):
package com.example.springai;
import com.example.springai.service.WeChatMomentService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class WeChatMomentServiceTest {
@Autowired
private WeChatMomentService service;
@Test
void testGenerateMoment() {
try {
String result = service.generateMoment("摸鱼学 Spring AI", null);
System.out.println("生成的朋友圈文案:");
System.out.println(result);
assertNotNull(result);
assertTrue(result.length() > 0);
} catch (Exception e) {
// 如果 API 调用失败(如密钥错误、网络异常),会抛出异常
System.err.println("生成失败: " + e.getMessage());
// 生产环境应记录日志,这里仅做演示
}
}
}
运行结果(成功时):
生成的朋友圈文案:
今天摸鱼学了 Spring AI,发现这玩意儿比写 CRUD 有意思多了!ChatClient 一行代码搞定大模型调用,PromptTemplate 让提示词写起来像填表,以后开发 AI 应用,别人还在写 HTTP 请求,我已经摸完鱼下班了 😂
异常处理场景:
- API 密钥错误:抛出
401 Unauthorized异常 - 网络异常:抛出
ConnectionException - 大模型返回空内容:抛出自定义异常
- 模板渲染失败:抛出
RuntimeException
吐槽:以前调用大模型要写十几行 HTTP 代码,现在一行链式调用搞定,爽!这就是 Spring AI 的魅力——把复杂的事情变简单。
五、进阶实践:黄金搭档组合拳——实现"摸鱼代码解释器"
5.1 场景设计:程序员刚需功能
你有没有遇到过这种情况:祖传代码里一行 lambda 写了 50 个字符,看了半小时,结果是个简单的遍历?或者接手别人的代码,看到一段嵌套的 stream 操作,一脸懵?
这个工具帮你搞定:输入一段看不懂的代码,返回"人话解释+摸鱼小贴士"。
5.2 完整代码实现(生产级,含异常处理)
1. 定义 PromptTemplate:
package com.example.springai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
@Service
public class CodeExplainerService {
@Autowired
private ChatClient chatClient;
// 定义模板:要求用大白话解释,别讲术语,还要给摸鱼小贴士
private final PromptTemplate template = PromptTemplate.create(
"请解释这段 Java 代码:\n{code}\n\n" +
"要求:\n" +
"1. 用大白话解释,别讲术语\n" +
"2. 给一句摸鱼小贴士,比如'这段代码可以用 Stream 简化,省出时间摸鱼'\n" +
"3. 如果代码有问题,指出问题并给出优化建议"
);
/**
* 解释代码(生产级,含完整异常处理)
* @param code 要解释的代码
* @return 解释结果
* @throws IllegalArgumentException 如果代码为空
* @throws RuntimeException 如果 API 调用失败或返回空内容
*/
public String explainCode(String code) {
// 1. 参数校验
if (!StringUtils.hasText(code)) {
throw new IllegalArgumentException("代码不能为空");
}
// 2. 代码长度校验(避免超出令牌限制)
if (code.length() > 2000) {
throw new IllegalArgumentException("代码过长,请分段解释(单次不超过 2000 字符)");
}
try {
// 3. 渲染模板
Map<String, Object> variables = new HashMap<>();
variables.put("code", code);
String prompt = template.render(variables);
// 4. 调用大模型
String response = chatClient.prompt()
.text(prompt)
.call()
.content();
// 5. 校验响应
if (!StringUtils.hasText(response)) {
throw new RuntimeException("大模型返回内容为空");
}
return response;
} catch (org.springframework.ai.chat.client.UnknownApiException e) {
// API 调用失败(如密钥错误、模型不存在)
throw new RuntimeException("API 调用失败,请检查密钥和模型配置: " + e.getMessage(), e);
} catch (java.net.ConnectException e) {
// 网络异常
throw new RuntimeException("网络连接失败,请检查网络: " + e.getMessage(), e);
} catch (Exception e) {
// 其他异常(模板渲染失败、响应解析失败等)
throw new RuntimeException("解释代码失败: " + e.getMessage(), e);
}
}
}
2. 测试方法(含完整导入):
package com.example.springai;
import com.example.springai.service.CodeExplainerService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class CodeExplainerServiceTest {
@Autowired
private CodeExplainerService service;
@Test
void testExplainCode() {
// 输入一段复杂代码(嵌套的 stream 操作)
String code = """
List<String> result = list.stream()
.filter(s -> s.length() > 5)
.map(s -> s.toUpperCase())
.sorted()
.collect(Collectors.toList());
""";
try {
String explanation = service.explainCode(code);
System.out.println("代码解释:");
System.out.println(explanation);
assertNotNull(explanation);
} catch (Exception e) {
System.err.println("解释失败: " + e.getMessage());
// 生产环境应记录日志
}
}
@Test
void testExplainCodeWithEmptyInput() {
// 测试空值校验
assertThrows(IllegalArgumentException.class, () -> {
service.explainCode("");
});
}
@Test
void testExplainCodeWithLongCode() {
// 测试长度校验
String longCode = "a".repeat(3000);
assertThrows(IllegalArgumentException.class, () -> {
service.explainCode(longCode);
});
}
}
运行结果(成功时):
代码解释:
这段代码做了三件事:
1. 过滤:从列表里挑出长度大于 5 的字符串(就像筛子筛豆子,只留大的)
2. 转大写:把每个字符串都转成大写(就像把"hello"变成"HELLO")
3. 排序:按字母顺序排好(就像字典排序)
4. 收集:把结果装进新列表里
摸鱼小贴士:这段代码可以用 parallelStream() 并行处理,如果数据量大,能省出不少时间摸鱼。不过要注意线程安全,别把数据搞乱了。
优化建议:如果 list 是空的,可以加个判空,避免不必要的操作。
异常处理场景:
- 代码为空:抛出
IllegalArgumentException - 代码过长:抛出
IllegalArgumentException(避免超出令牌限制) - API 调用失败:捕获
UnknownApiException,提示检查密钥和模型配置 - 网络异常:捕获
ConnectException,提示检查网络 - 大模型返回空内容:抛出
RuntimeException
吐槽:看,大模型不仅帮你解释了代码,还教你摸鱼,这波血赚!而且代码里加了完整的异常处理,生产环境也能用。
六、常见问题与避坑指南(完整版)
6.1 依赖冲突:多 starter 冲突的完整解决方案
问题:同时引入了 OpenAI 和智谱 AI 的 starter,启动报错。
Error: Multiple ChatClient beans found. Expected single matching bean.
原因:Spring 懵了:你到底想找哪个大模型聊天?
完整解决方案:
方案 1:使用 @Primary 指定默认 ChatClient
@Configuration
public class SpringAIConfig {
@Bean
@Primary // 标记为默认 Bean
public ChatClient openAIChatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}
方案 2:使用 @Qualifier 注入特定 ChatClient
如果同时需要多个大模型的 ChatClient,可以这样:
@Configuration
public class SpringAIConfig {
@Bean("openAIChatClient")
public ChatClient openAIChatClient(
@Qualifier("openAiChatModel") ChatModel chatModel
) {
return ChatClient.builder(chatModel).build();
}
@Bean("zhipuAIChatClient")
public ChatClient zhipuAIChatClient(
@Qualifier("zhipuAiChatModel") ChatModel chatModel
) {
return ChatClient.builder(chatModel).build();
}
}
使用时通过 @Qualifier 注入:
@Service
public class AIService {
@Autowired
@Qualifier("openAIChatClient")
private ChatClient openAIChatClient;
@Autowired
@Qualifier("zhipuAIChatClient")
private ChatClient zhipuAIChatClient;
}
6.2 API 密钥错误:完整排查指南
问题:报错 invalid api key 或 401 Unauthorized。
完整排查步骤:
- 检查密钥格式:别把
sk-xxx写成ks-xxx,我见过好几个程序员栽在这 - 检查环境变量:
echo $OPENAI_API_KEY(Linux/Mac)或echo %OPENAI_API_KEY%(Windows) - 检查配置文件:确认
application.yml中的spring.ai.openai.api-key配置正确 - 检查密钥权限:
- OpenAI:确认密钥有
chat.completions接口权限 - 如果仅开通 GPT-3.5 却调用 GPT-4,会报权限错误
- OpenAI:确认密钥有
- 检查区域限制:
- OpenAI 密钥可能不支持国内 IP,需要使用代理或 VPN
- 智谱 AI 等国内大模型无此限制
- 检查密钥是否过期:去大模型官网重新生成一个密钥(如果旧的泄露了)
6.2.1 API URL 配置问题:代理和自建服务的常见错误
问题:配置了自定义 base-url,但调用失败,报错 Connection refused 或 404 Not Found。
常见错误场景:
-
代理 URL 配置错误:
# 错误:URL 末尾多了斜杠或路径不对 base-url: https://your-proxy.com/v1/ # 错误:末尾斜杠 # 正确:只配置基础域名,Spring AI 会自动拼接路径 base-url: https://your-proxy.com -
自建服务 API 格式不兼容:
- Spring AI 期望的 API 格式:
POST /v1/chat/completions - 如果自建服务不兼容 OpenAI API 格式,需要修改服务端或使用其他适配方案
- Spring AI 期望的 API 格式:
-
URL 协议错误:
# 错误:缺少协议 base-url: your-proxy.com # 正确:包含协议 base-url: https://your-proxy.com
完整排查步骤:
- 检查 URL 格式:确保包含协议(
https://或http://),末尾不要加斜杠 - 测试代理连通性:用
curl测试代理是否可用curl -X POST https://your-proxy.com/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}' - 检查代理配置:如果使用 Nginx 等反向代理,确保正确转发请求头(特别是
Authorization) - 查看日志:Spring AI 会记录请求 URL,检查日志确认实际请求的地址是否正确
国内访问 OpenAI 的推荐方案:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
# 使用代理服务器(需要自己搭建或购买代理服务)
base-url: https://your-proxy-server.com
# 如果需要设置代理的认证信息,可以通过系统属性或环境变量设置
# 注意:Spring AI 本身不直接支持代理认证,需要在代理服务器层面配置
6.3 提示词渲染失败:占位符错误的完整排查
问题:模板渲染失败,报错 Template variable not found。
原因:占位符写错了,比如把 {code} 写成了 [code],大模型:你猜我认不认识这个符号?
完整排查步骤:
- 占位符格式:必须用
{}包裹,不能用[]或() - 变量名匹配:变量名要和传入的 Map key 一致(区分大小写)
// 错误示例
PromptTemplate template = new PromptTemplate("请解释 [code]"); // 错误:用了 []
// 正确示例
PromptTemplate template = new PromptTemplate("请解释 {code}"); // 正确:用了 {}
- 变量名规范:建议使用英文变量名,符合 Java 编码规范
// 不推荐:中文变量名
variables.put("姓名", "张三");
// 推荐:英文变量名
variables.put("name", "张三");
6.4 响应超时:完整配置指南
问题:大模型回复慢,或者超时。
原因分析:
- 问题太复杂:比如"帮我写一个淘宝",大模型:你猜我要写多久?
- 令牌数过多:输入 + 输出超过模型限制
- 网络延迟:国内访问 OpenAI 可能较慢
完整解决方案:
1. 配置超时时间(正确配置):
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-3.5-turbo
temperature: 0.7
max-tokens: 500
# 超时配置(注意:这是连接超时,不是生成超时)
timeout: 30s # 30 秒超时
2. 优化提示词:
- 避免过于复杂的问题(如"帮我写一个淘宝")
- 明确输出格式和长度(如"50 字以内")
- 使用简单问题测试(如"帮我写一个 Hello World")
3. 令牌数限制说明:
gpt-3.5-turbo:默认最大 4096 令牌(输入 + 输出)gpt-4:默认最大 8192 令牌- 如果输入过长,应分段处理或使用更高版本模型
重要提示:temperature 和 max-tokens 不是超时配置!
temperature:生成随机性参数(0-2),0.7 是平衡值max-tokens:回复最大令牌数,不是超时时间- 真正的超时配置是
timeout(如spring.ai.openai.chat.timeout=30s)
6.5 版本兼容性问题:完整升级指南
问题:用了过时的 API,代码跑不起来。
版本选择建议:
- 生产环境:使用稳定版本(1.1.x),别用
-M开头的里程碑版本(除非你想当小白鼠) - 查询版本适配表:访问 Spring AI 官方文档,查看版本与 Spring Boot 的匹配关系
旧版本升级到正式版的改动点:
-
ChatClient 用法变更:
// 旧版本(M4):简化调用 String response = chatClient.call(prompt); // 新版本(1.1.x):链式调用 String response = chatClient.prompt() .text(prompt) .call() .content(); -
依赖版本更新:
<!-- 旧版本 --> <version>1.0.0-M4</version> <!-- 新版本 --> <version>1.1.0</version> -
配置项变更:部分配置项名称可能调整,请参考官方文档
6.6 多模块项目 Bean 扫描问题
症状:在多模块项目中注入 ChatClient 时报「No qualifying bean」。
排查清单:
- 子模块依赖:确认业务子模块已引入
spring-ai-openai-spring-boot-starter(或对应模型的 starter) - 包扫描范围:启动类的
@SpringBootApplication是否能扫描到业务类所在包;如不在同级,可通过scanBasePackages显式指定 - 配置文件可见性:子模块能否读取到
spring.ai.openai.api-key等配置(多模块的资源合并是否正确)
示例:启动类跨模块扫描
@SpringBootApplication(scanBasePackages = {
"com.example.springai", // 公共配置与自动配置
"com.example.business.module" // 业务子模块包名
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
七、拓展方向:多轮对话、函数调用、RAG 落地指引
7.1 多轮对话:使用 ChatMemory 记住上下文
场景:就像和大模型唠嗑,记住之前的对话内容。
实现步骤:
- 引入依赖(如果使用内存存储):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store</artifactId>
<version>1.1.0</version>
</dependency>
- 配置 ChatMemory:
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemory chatMemory() {
// 使用内存存储(简单场景)
return new InMemoryChatMemory();
// 或使用 Redis 存储(生产环境推荐)
// return new RedisChatMemory(redisTemplate);
}
}
- 使用多轮对话:
@Service
public class ConversationService {
@Autowired
private ChatClient chatClient;
@Autowired
private ChatMemory chatMemory;
public String chat(String userMessage, String conversationId) {
// 1. 获取历史对话
List<Message> history = chatMemory.get(conversationId);
// 2. 构建多轮对话 Prompt
Prompt prompt = new Prompt(
Stream.concat(
history.stream(), // 历史消息
Stream.of(new UserMessage(userMessage)) // 当前消息
).collect(Collectors.toList())
);
// 3. 调用大模型
String response = chatClient.prompt()
.messages(prompt.getInstructions())
.call()
.content();
// 4. 保存对话历史
chatMemory.add(conversationId, new UserMessage(userMessage));
chatMemory.add(conversationId, new AssistantMessage(response));
return response;
}
}
7.2 函数调用:让大模型调用本地方法
场景:让大模型帮你调用本地方法,如查询数据库、调用外部 API。
实现步骤:
- 定义函数接口:
public interface WeatherFunction {
@Function(description = "获取指定城市的天气")
String getWeather(@Parameter(description = "城市名称") String city);
}
- 实现函数:
@Component
public class WeatherFunctionImpl implements WeatherFunction {
@Override
public String getWeather(String city) {
// 调用天气 API 或查询数据库
return "北京:晴天,25°C";
}
}
- 配置函数调用:
@Configuration
public class FunctionCallConfig {
@Bean
public ChatClient functionCallChatClient(
ChatModel chatModel,
WeatherFunction weatherFunction
) {
return ChatClient.builder(chatModel)
.defaultFunctions(weatherFunction) // 注册函数
.build();
}
}
- 使用函数调用:
@Service
public class FunctionCallService {
@Autowired
private ChatClient chatClient;
public String chat(String userMessage) {
// 大模型会自动判断是否需要调用函数
String response = chatClient.prompt()
.text(userMessage)
.call()
.content();
return response;
}
}
7.3 RAG(检索增强生成):结合向量数据库
场景:让大模型记住你的数据,做知识库问答。
实现步骤:
- 引入向量数据库依赖(以 PostgreSQL + pgvector 为例):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store</artifactId>
<version>1.1.0</version>
</dependency>
- 配置向量数据库:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/vectordb
username: postgres
password: postgres
- 存储文档向量:
@Service
public class VectorStoreService {
@Autowired
private VectorStore vectorStore;
public void storeDocument(String document) {
// 将文档转换为向量并存储
Document doc = new Document(document);
vectorStore.add(List.of(doc));
}
}
- RAG 查询:
@Service
public class RAGService {
@Autowired
private ChatClient chatClient;
@Autowired
private VectorStore vectorStore;
public String query(String question) {
// 1. 从向量数据库检索相关文档
List<Document> relevantDocs = vectorStore.similaritySearch(question);
// 2. 构建 RAG Prompt
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n"));
String prompt = String.format(
"基于以下上下文回答问题:\n%s\n\n问题:%s",
context, question
);
// 3. 调用大模型
return chatClient.prompt()
.text(prompt)
.call()
.content();
}
}
八、生产环境最佳实践
8.1 核心准则总结
- 模板复用:将常用提示词封装成 PromptTemplate,避免重复编写
- 异常重试:API 调用失败时,实现重试机制(如重试 3 次,每次间隔 1 秒)
- 限流降级:避免频繁调用导致 API 限流,实现请求限流和降级策略
- 日志埋点:记录 API 调用日志、响应时间、令牌消耗等,便于监控和优化
- 参数校验:所有输入参数必须校验,避免无效请求
- 响应校验:校验大模型返回内容,避免空内容或格式错误
8.2 完整生产级代码示例
package com.example.springai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class ProductionAIService {
@Autowired
private ChatClient chatClient;
private final PromptTemplate template = PromptTemplate.create(
"帮我写一段关于 {topic} 的朋友圈文案,要求 {length} 字以内"
)
.withDefault("length", 50);
/**
* 生产级方法:含参数校验、异常重试、日志埋点
*/
@Retryable(
value = {Exception.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public String generateMoment(String topic, Integer length) {
// 1. 参数校验
if (!StringUtils.hasText(topic)) {
log.warn("话题为空,拒绝请求");
throw new IllegalArgumentException("话题不能为空");
}
long startTime = System.currentTimeMillis();
try {
// 2. 渲染模板
Map<String, Object> variables = new HashMap<>();
variables.put("topic", topic);
if (length != null && length > 0) {
variables.put("length", length);
}
String prompt = template.render(variables);
// 3. 调用大模型
String response = chatClient.prompt()
.text(prompt)
.call()
.content();
// 4. 响应校验
if (!StringUtils.hasText(response)) {
log.error("大模型返回内容为空,topic: {}", topic);
throw new RuntimeException("大模型返回内容为空");
}
// 5. 记录日志(生产环境必须)
long duration = System.currentTimeMillis() - startTime;
log.info("生成朋友圈文案成功,topic: {}, length: {}, duration: {}ms",
topic, length, duration);
return response;
} catch (Exception e) {
// 6. 异常日志
long duration = System.currentTimeMillis() - startTime;
log.error("生成朋友圈文案失败,topic: {}, duration: {}ms, error: {}",
topic, duration, e.getMessage(), e);
throw e;
}
}
}
九、总结与思考
9.1 核心知识点回顾
- PromptTemplate:大模型的"剧本模板",负责把提示词写得规范、可复用
- ChatClient:你的"大模型专属接线员",负责帮你传话、把回复带回来
- 两者组合:开发大模型应用的"快捷键",一行链式调用搞定
9.2 生产环境使用要点
- 版本选择:使用稳定版本(1.1.x),避免里程碑版本
- 异常处理:完整的参数校验、响应校验、异常捕获
- 日志埋点:记录调用日志、响应时间、令牌消耗
- 安全配置:API 密钥用环境变量或配置中心,别写代码里
- 性能优化:合理设置超时、令牌数限制,避免无效请求
9.3 最后的话
学会这俩玩意,以后开发 AI 应用,别人还在写 HTTP 请求,你已经摸完鱼下班了。
记住:
- 模板写得好,提示词不用愁
- 接线员(ChatClient)帮你传话,代码写得爽
- 密钥别写代码里,钱包会哭
- 生产环境加异常处理和日志,别让线上出问题
十、思考与练习
练习题目:实现一个"古诗生成器"(完整版)
需求:
- 输入主题(比如"摸鱼")
- 生成一首七言绝句
- 要求押韵、有意境
- 含完整的异常处理和参数校验
提示:
- 改改 PromptTemplate 就行,超简单
- 模板示例:
"请以 {theme} 为主题,生成一首七言绝句,要求押韵、有意境" - 用 ChatClient 链式调用大模型,返回结果
- 注意令牌数限制:七言绝句约 28 字,加上提示词,总令牌数应控制在 500 以内
参考代码框架:
package com.example.springai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class PoemGeneratorService {
@Autowired
private ChatClient chatClient;
private final PromptTemplate template = PromptTemplate.create(
"请以 {theme} 为主题,生成一首七言绝句,要求押韵、有意境"
);
/**
* 生成古诗(完整版,含异常处理)
* @param theme 主题
* @return 生成的古诗
*/
public String generatePoem(String theme) {
// 1. 参数校验
if (!StringUtils.hasText(theme)) {
throw new IllegalArgumentException("主题不能为空");
}
try {
// 2. 渲染模板
Map<String, Object> variables = new HashMap<>();
variables.put("theme", theme);
String prompt = template.render(variables);
// 3. 调用大模型(链式调用,符合 1.1.x 规范)
String poem = chatClient.prompt()
.text(prompt)
.call()
.content();
// 4. 响应校验
if (!StringUtils.hasText(poem)) {
throw new RuntimeException("大模型返回内容为空");
}
return poem;
} catch (Exception e) {
// 5. 异常兜底:超时、密钥错误、网络异常等
log.warn("生成古诗失败,theme: {}, err: {}", theme, e.getMessage());
return "生成失败:" + e.getMessage();
}
}
}
测试代码:
@SpringBootTest
class PoemGeneratorServiceTest {
@Autowired
private PoemGeneratorService service;
@Test
void testGeneratePoem() {
String poem = service.generatePoem("摸鱼");
System.out.println("生成的古诗:");
System.out.println(poem);
assertNotNull(poem);
// 验证:七言绝句应为 4 句,每句 7 字
String[] lines = poem.split("\n");
assertEquals(4, lines.length);
}
}
试试吧:改改模板,换个主题,看看大模型能生成什么好玩的诗!记得加异常处理和日志哦。
文章结束:如果你看完这篇文章,还觉得 Spring AI 是个玄学,那一定是我写得不够接地气。欢迎在评论区吐槽,或者直接来打我(开玩笑的)。
祝你摸鱼愉快! 🎉