注:我默认你是个会用spring boot的老java选手,版本咱们就踩在spring ai 2.0.0 ga上(2026年6月刚出的),如果还在用boot 3.x,我会在文末给你指条明路。
一、spring ai 到底是个啥?
1.1 别拽术语,说人话
说白了,spring ai就是spring官方给咱java程序员造的一座桥,桥那头是openai、通义千问、deepseek这些大模型,桥这头是你熟悉的@service、@autowired、application.yml。它干的最牛的一件事就是——让你用写spring boot业务代码的姿势去调ai,不用自己拼http请求、不用管签名、不用操心重试。
我举个极端的例子:你项目里本来用的gpt-4,老板突然说“太贵了,换deepseek”,在spring ai底下,你只需要改一行配置(base-url和api-key),业务代码一行不动。这在别的sdk里敢想?
1.2 为啥值得你花时间学?
- 切换模型跟切数据源似的:一套api通吃所有主流模型,openai、阿里、deepseek、本地ollama随便换。
- spring亲儿子:starter包、自动配置、
@configuration,全是老朋友,学习曲线陡降。 - 能力全家桶:不光能聊天,还能做嵌入(向量化)、画图、语音、rag(私有知识库)、工具调用(让ai帮你查天气/查数据库)。
- 为生产而生:自带重试、超时、监控埋点,不用自己再封装一层。
1.3 版本那点事儿(别踩坑)
| 版本 | 什么时候出的 | 最低spring boot要求 | 适合谁 |
|---|---|---|---|
| 1.0.x | 2025年 | 3.2.x | 老项目稳定为主 |
| 1.1.x | 2025-2026 | 3.3.x+ | 想要新功能又不想升boot 4 |
| 2.0.0 ga | 2026年6月 | 4.0.x / 4.1.x | 新项目直接上,工具调用成了头等公民 |
⚠️ 敲黑板:spring ai 2.0 必须配 spring boot 4.0+,如果你项目还在3.x,老老实实用1.1.x,别硬升。
二、环境准备(5分钟搞定)
2.1 你得先有的东西
- jdk 17+(别用8了,求你了)
- maven 3.8+ 或 gradle 7.5+
- 一个ai平台的api key(去deepseek官网注册个,便宜大碗,或者用openai的)
2.2 脚手架怎么选
去 start.spring.io 戳下面几项:
- spring boot: 4.0.x(或者3.3.x,看你选哪个版本)
- 依赖勾上:
spring web+spring ai openai starter(别怕,这个starter兼容所有openai格式的接口,包括deepseek)
三、依赖和配置(别抄错)
3.1 maven 的 pom.xml(以2.0+boot4为例)
<parent>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-parent</artifactid>
<version>4.0.0</version>
</parent>
<properties>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<repositories>
<repository>
<id>spring-milestones</id>
<name>spring milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencymanagement>
<dependencies>
<dependency>
<groupid>org.springframework.ai</groupid>
<artifactid>spring-ai-bom</artifactid>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencymanagement>
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.ai</groupid>
<artifactid>spring-ai-openai-spring-boot-starter</artifactid>
</dependency>
</dependencies>如果用boot 3.x + ai 1.1.x,就把版本号换成 1.1.7 就行。
3.2 配置文件 application.yml(密钥别写死)
spring:
ai:
openai:
api-key: ${openai_api_key} # 环境变量,安全第一
chat:
options:
model: gpt-4o
temperature: 0.7想切deepseek?只需改两行:
spring:
ai:
openai:
api-key: ${deepseek_api_key}
base-url: https://api.deepseek.com/v1
chat:
options:
model: deepseek-chat四、第一个接口:跟ai打个招呼
4.1 最简版controller
spring ai自动给你配了一个 chatclient.builder,你只管注入,然后build()一下就能用。
@restcontroller
public class chatcontroller {
private final chatclient chatclient;
public chatcontroller(chatclient.builder builder) {
this.chatclient = builder.build();
}
@getmapping("/chat")
public string chat(@requestparam string message) {
return chatclient.prompt()
.user(message)
.call()
.content();
}
}4.2 咱们来试试输入输出
你在浏览器敲:
get /chat?message=哥们儿,spring ai到底是干啥的?我用java调大模型非得用它吗?
ai回:
“嗨,这么跟你说吧,spring ai就像是一个万能电源适配器。以前你调openai得写一套http客户端,换成通义千问又得重新封装签名,烦不烦?现在spring ai把底层这些‘方言’全给你抹平了。你只需要像注入普通service一样注入
chatclient,该调调,该测测。打个不恰当的比方——切换ai模型就跟切数据库连接池一样,改改application.yml就行了,代码一行都不用动。当然,不用它也能调,但用了它,你就能享受到spring那一套‘自动配置、依赖注入、可观测性’的生态红利,何乐而不为呢?”
是不是感觉像跟同事聊天?这就对了。
4.3 如果你想全局统一个“人设”
写个配置类,把系统提示词固定下来,这样所有对话都会带上这个“人格”。
@configuration
public class aiconfig {
@bean
public chatclient chatclient(chatclient.builder builder) {
return builder
.defaultsystem("你是一位专业的java技术顾问,回答要简洁、准确,偶尔带点幽默")
.build();
}
}4.4 快速测试
有时候不想启动web,直接写个commandlinerunner试试水:
@bean
public commandlinerunner testai(chatclient.builder builder) {
return args -> {
chatclient client = builder.build();
string reply = client.prompt("用一句话解释spring ai").call().content();
system.out.println("ai说:" + reply);
};
}五、核心组件扒开看
5.1 chatclient —— 你的ai对讲机
它用的是构建者模式,你可以一路.下去,非常丝滑。
// 最简
string r1 = chatclient.prompt("你好").call().content();
// 带系统角色
string r2 = chatclient.prompt()
.system("你是个spring源码专家")
.user("说说自动配置的原理")
.call()
.content();
// 流式输出(一个字一个字蹦)
flux<string> stream = chatclient.prompt()
.user("讲个编程笑话")
.stream()
.content();5.2 prompt —— 多轮对话的容器
如果你想塞历史消息,或者同时放系统消息、用户消息、助手消息,就用prompt对象。
prompt prompt = new prompt(
list.of(
new systemmessage("你是个数学老师"),
new usermessage("什么是微积分?"),
new assistantmessage("微积分是研究变化率的..."),
new usermessage("那导数呢?")
)
);
chatresponse response = chatclient.prompt(prompt).call();
string answer = response.getresult().getoutput().getcontent();5.3 advisor —— 给ai装上外挂
比如你想让ai记住之前聊过啥,直接加个记忆advisor,不用自己维护会话历史。
chatclient chatclient = builder
.defaultadvisors(new messagechatmemoryadvisor(new inmemorychatmemory()))
.build();
// 第一句
chatclient.prompt().user("我叫李雷").call().content();
// 第二句——它会记得你
chatclient.prompt().user("我叫啥?").call().content(); // 返回“李雷”六、prompt工程(让ai听懂人话)
6.1 用模板动态填参数
有时候你的提示词里要塞变量,用prompttemplate最方便。
@getmapping("/prompt")
public string prompttest() {
string template = "你是一位{role},请用{style}的风格解释:{question}";
prompttemplate pt = new prompttemplate(template);
prompt prompt = pt.create(map.of(
"role", "物理学家",
"style", "通俗易懂带比喻",
"question", "什么是量子纠缠"
));
return chatclient.prompt(prompt).call().content();
}请求一下试试:
get /prompt
输出:
“好,我给你打个比方:量子纠缠就像一对双胞胎,一个在地球,一个在火星,你捏一下地球这个的胳膊,火星那个瞬间喊疼——不管离多远,它俩总是同步的。爱因斯坦管这叫‘鬼魅般的超距作用’,我们物理学家现在还没完全搞懂,但已经在做量子通信了。”
6.2 把大段模板放在文件里
在 src/main/resources/prompts/explain.txt 里写:
你是一位{role},请用{style}的风格回答:{question}然后加载:
resource resource = new classpathresource("prompts/explain.txt");
prompttemplate template = new prompttemplate(resource);
prompt prompt = template.create(map.of("role","历史学家","style","讲故事","question","罗马帝国怎么衰落的"));七、结构化输出(让ai直接返回java对象)
这功能太香了!你不需要让ai返回一大段文本然后自己拿正则去抠字段,直接定义个record或class,ai按你的格式返回,spring ai自动帮你反序列化。
7.1 定义一个bookrecommendation
public record bookrecommendation(
string title,
string author,
int year,
string summary,
list<string> reasons
) {}7.2 写个接口,直接返回这个对象
@getmapping("/recommend")
public bookrecommendation recommend(@requestparam string genre) {
return chatclient.prompt()
.user("推荐一本" + genre + "类的好书,返回包含书名、作者、出版年份、简介和推荐理由")
.call()
.entity(bookrecommendation.class);
}7.3 看看输入输出长啥样
请求:
get /recommend?genre=悬疑推理
返回的json(前端直接就能用):
{
"title": "《恶意》",
"author": "东野圭吾",
"year": 1996,
"summary": "比起‘谁是凶手',这本书更折磨人的是‘为什么要杀他'。凶手前几章就自首了,但背后的恶意像深渊一样,看得人脊背发凉。",
"reasons": [
"叙事诡计玩到极致,反转再反转",
"对人性的阴暗面剖析极深,适合社畜细品",
"篇幅不长,周末一下午就能刷完"
]
}你看,连reasons这个list都给你填得明明白白,省了前后端扯皮。
7.4 自纠错模式(2.0新增)
如果模型偶尔抽风,返回的格式不对,你可以开自纠错:
bookrecommendation result = chatclient.prompt()
.user("推荐一本科幻小说")
.call()
.entity(bookrecommendation.class)
.validateschema(); // 它会自动重试修正八、工具调用(tool calling)——让ai动手干活
ai的训练数据是死的,但你可以给它配“工具”,比如查天气、查数据库、调第三方api。当用户问到相关问题时,ai会自动决定“嗯,我得调用那个函数”。
8.1 写个工具类,加上@tool注解
@component
public class weathertools {
@tool(description = "获取指定城市的实时天气")
public string getweather(string city) {
// 这里换成真实的api调用
return city + " 今天晴天,26°c,湿度60%";
}
@tool(description = "获取当前系统时间")
public string getcurrenttime() {
return localdatetime.now().tostring();
}
}8.2 注册到chatclient
@restcontroller
public class toolcontroller {
private final chatclient chatclient;
public toolcontroller(chatclient.builder builder, weathertools weathertools) {
this.chatclient = builder
.defaulttools(weathertools)
.build();
}
@getmapping("/tool-chat")
public string chatwithtool(@requestparam string message) {
return chatclient.prompt()
.user(message)
.call()
.content();
}
}8.3 看输入输出(完整版带幕后日志)
用户请求:
get /tool-chat?message=我现在在杭州,想知道今天穿短袖会不会冷?
后台偷偷干的事儿(你可以在日志里看到):
2026-07-21 15:03:22 info - 用户提问包含"杭州"和"天气",触发工具调用
2026-07-21 15:03:22 info - 执行 weathertools.getweather("杭州")
2026-07-21 15:03:23 info - 第三方api返回:{"city":"杭州","temp":26,"weather":"晴","humidity":60%}
2026-07-21 15:03:24 info - 模型整合工具结果,生成最终回复最终返回给用户的回答:
“兄弟,我刚用实时接口帮你扒了一下杭州现在的天气——下午3点,晴天,26°c,湿度60%。按这个温度,穿短袖完全没问题,甚至中午在外面走快点可能会微微出汗。不过太阳下山后(6点半左右)会降到20°c,建议你工位上备件薄衬衫,以防空调冷气太足吹得肩膀酸。”
你看,它不是瞎编的,是真的查了数据再回答。
8.4 另一种写法:用function接口
如果你喜欢更函数式的风格,也可以:
@component
public class weatherfunction implements function<weatherfunction.request, weatherfunction.response> {
public record request(string city) {}
public record response(string weather, double temp) {}
@override
public response apply(request request) {
// 调用真实api
return new response("晴", 26.0);
}
}注册时改用 .defaultfunctions("weather", weatherfunction)。
九、rag(检索增强生成)——让ai读你的私有文档
9.1 先整明白:rag到底在干啥?
rag解决的核心问题是:ai的训练数据里没有你公司的内部文档,但你又想让ai基于这些文档回答问题。
打个比方:你给ai配了一个“私人图书馆管理员”。用户提问时,管理员先去图书馆里翻出相关的几页书,夹在问题里一起递给ai,ai再根据这些资料回答。这样既保证了答案有据可查,又避免了ai瞎编。
完整流程长这样:
文档上传 → 文本提取(tika)→ 切分成小块(tokentextsplitter)→ 向量化(embeddingmodel)→ 存入向量库(vectorstore)→ 用户提问 → 向量检索相似片段 → 拼接到prompt → 大模型生成答案
9.2 先把依赖搞齐
rag比普通对话多了一堆依赖,别漏了:
<!-- spring ai bom(前面已经有了,再确认一下版本) -->
<dependencymanagement>
<dependencies>
<dependency>
<groupid>org.springframework.ai</groupid>
<artifactid>spring-ai-bom</artifactid>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencymanagement>
<dependencies>
<!-- 基础chat(前面已加) -->
<dependency>
<groupid>org.springframework.ai</groupid>
<artifactid>spring-ai-openai-spring-boot-starter</artifactid>
</dependency>
<!-- rag advisor(核心!) -->
<dependency>
<groupid>org.springframework.ai</groupid>
<artifactid>spring-ai-vector-store-advisor</artifactid>
</dependency>
<!-- 文档解析器(支持pdf/word/excel/html等) -->
<dependency>
<groupid>org.springframework.ai</groupid>
<artifactid>spring-ai-tika-document-reader</artifactid>
</dependency>
<!-- 向量数据库(以redis为例,也可以换milvus/pgvector) -->
<dependency>
<groupid>org.springframework.ai</groupid>
<artifactid>spring-ai-starter-vector-store-redis</artifactid>
</dependency>
</dependencies>💡 想用milvus?把
spring-ai-starter-vector-store-redis换成spring-ai-starter-vector-store-milvus就行。想用pgvector?换成spring-ai-starter-vector-store-pgvector。
9.3 配置文件(application.yml)
spring:
ai:
# 大模型配置(用于最终回答)
openai:
api-key: ${openai_api_key}
chat:
options:
model: gpt-4o
temperature: 0.3 # rag场景温度低一点,减少胡说八道
# embedding模型配置(用于把文本转成向量)
embedding:
openai:
api-key: ${openai_api_key}
options:
model: text-embedding-3-small # 便宜大碗
# 向量数据库配置(以redis为例)
vectorstore:
redis:
host: localhost
port: 6379
index: spring-ai-docs # 索引名称
dimensions: 1536 # 跟embedding模型维度对齐9.4 核心一:文档加载与向量化存储(etl管道)
这一步是把你的pdf/word/excel等文档“喂”给向量数据库。
@service
@slf4j
public class documentingestionservice {
private final vectorstore vectorstore;
private final tokentextsplitter textsplitter;
public documentingestionservice(vectorstore vectorstore) {
this.vectorstore = vectorstore;
// tokentextsplitter参数:每段800 token,最小200字符,最大重叠100 token
// 基于cl100k_base编码,跟openai的embedding模型同款
this.textsplitter = new tokentextsplitter(800, 200, 100, 10000, true);
}
/**
* 从multipartfile上传并入库
*/
public void ingestdocument(multipartfile file) throws ioexception {
// 1. 用tika读取各种格式(pdf/word/excel/ppt/html等)
var resource = new inputstreamresource(file.getinputstream());
tikadocumentreader reader = new tikadocumentreader(resource);
list<document> documents = reader.read();
log.info("读取到 {} 个原始文档片段", documents.size());
// 2. 用tokentextsplitter切分成适合向量检索的小块
list<document> splitdocs = textsplitter.apply(documents);
// 3. 给每个块打上元数据(方便后续过滤)
splitdocs.foreach(doc -> {
doc.getmetadata().put("source", file.getoriginalfilename());
doc.getmetadata().put("uploadtime", localdatetime.now().tostring());
});
log.info("切分成 {} 个向量块,开始写入向量库...", splitdocs.size());
// 4. 写入向量数据库(spring ai自动调用embeddingmodel做向量化)
vectorstore.add(splitdocs);
log.info("✅ 文档 {} 入库完成!", file.getoriginalfilename());
}
/**
* 从类路径加载示例文档(启动时预置数据)
*/
@postconstruct
public void loadsampledocuments() {
try {
resource resource = new classpathresource("docs/sample.txt");
if (resource.exists()) {
tikadocumentreader reader = new tikadocumentreader(resource);
list<document> docs = reader.read();
list<document> splitdocs = textsplitter.apply(docs);
vectorstore.add(splitdocs);
log.info("✅ 示例文档加载完成,共 {} 个向量块", splitdocs.size());
}
} catch (exception e) {
log.warn("示例文档加载失败(首次启动忽略): {}", e.getmessage());
}
}
}对应的上传接口:
@restcontroller
@requestmapping("/api/knowledge")
public class knowledgecontroller {
private final documentingestionservice ingestionservice;
@postmapping("/upload")
public responseentity<string> uploaddocument(@requestparam("file") multipartfile file) {
try {
ingestionservice.ingestdocument(file);
return responseentity.ok("文档上传成功,已进入知识库");
} catch (exception e) {
log.error("上传失败", e);
return responseentity.status(500).body("上传失败:" + e.getmessage());
}
}
}来试试输入输出:
请求(postman或前端上传):
post /api/knowledge/upload content-type: multipart/form-data file: 公司规章制度.pdf
输出(接口返回):
“文档上传成功,已进入知识库”
后台日志长这样:
2026-07-21 16:20:15 info - 读取到 3 个原始文档片段 2026-07-21 16:20:15 info - 切分成 47 个向量块,开始写入向量库... 2026-07-21 16:20:18 info - ✅ 文档 公司规章制度.pdf 入库完成!
9.5 核心二:rag对话(用advisor做检索增强)
文档入库后,就可以开始基于知识库问答了。spring ai提供了两种advisor:
questionansweradvisor:简单版,适合大多数场景retrievalaugmentationadvisor:进阶版,支持查询扩展、上下文压缩等高级功能
方式一:用questionansweradvisor(推荐新手)
@configuration
public class ragconfig {
@bean
public chatclient ragchatclient(
chatclient.builder builder,
vectorstore vectorstore) {
// 构建rag advisor:检索top 4,相似度阈值0.7
var ragadvisor = questionansweradvisor.builder(vectorstore)
.searchrequest(searchrequest.builder()
.topk(4)
.similaritythreshold(0.7d)
.build())
.build();
return builder
.defaultsystem("你是一位知识库助手,请基于提供的文档片段回答问题。如果文档中没有相关信息,请明确告知用户。")
.defaultadvisors(ragadvisor)
.build();
}
}方式二:用retrievalaugmentationadvisor(更灵活)
@configuration
public class ragconfig {
@bean
public chatclient ragchatclient(
chatclient.builder builder,
vectorstore vectorstore) {
// 构建文档检索器
documentretriever retriever = vectorstoredocumentretriever.builder()
.vectorstore(vectorstore)
.similaritythreshold(0.5d)
.topk(5)
.build();
// 构建rag advisor
var ragadvisor = retrievalaugmentationadvisor.builder()
.documentretriever(retriever)
.build();
return builder
.defaultsystem("你是一位知识库助手,请基于检索到的文档片段回答问题。")
.defaultadvisors(ragadvisor)
.build();
}
}9.6 写个rag问答接口
@restcontroller
@requestmapping("/api/rag")
public class ragcontroller {
private final chatclient ragchatclient;
public ragcontroller(@qualifier("ragchatclient") chatclient ragchatclient) {
this.ragchatclient = ragchatclient;
}
@getmapping("/ask")
public string ask(@requestparam string question) {
return ragchatclient.prompt()
.user(question)
.call()
.content();
}
// 流式版本(体验更好)
@getmapping(value = "/ask/stream", produces = mediatype.text_event_stream_value)
public flux<string> askstream(@requestparam string question) {
return ragchatclient.prompt()
.user(question)
.stream()
.content();
}
}9.7 完整的输入输出实战
场景:你刚上传了一份《员工休假管理制度.pdf》,里面写着“年假当年不休完,次年3月31日前可补休,逾期作废”。
用户请求:
get /api/rag/ask?question=我去年还有5天年假没休,现在7月份了还能休吗?
后台偷偷干的事儿:
2026-07-21 16:30:22 info - 用户提问:"我去年还有5天年假没休,现在7月份了还能休吗?" 2026-07-21 16:30:22 info - 向量检索:找到3个相关文档片段 2026-07-21 16:30:22 info - 片段1:"年假当年不休完,次年3月31日前可补休,逾期作废" 2026-07-21 16:30:22 info - 片段2:"补休申请需提前3个工作日提交hr系统" 2026-07-21 16:30:23 info - 大模型基于片段生成回答...
最终返回给用户的回答:
“根据咱们公司的《员工休假管理制度》,去年未休完的年假可以在次年3月31日前补休。现在已经是7月份了,已经过了补休截止日期,按照制度规定逾期作废了😅。建议你今年提前规划好休假时间,别再拖到最后啦。另外提醒一下,补休申请需要提前3个工作日提交hr系统哦。”
你看,它真的去翻了你的文档,而不是瞎编一个答案。
9.8 进阶玩法:动态过滤(按部门/按文档类型)
如果你的知识库里有多个部门的文档,可以让用户只检索特定来源:
@getmapping("/ask/filter")
public string askwithfilter(@requestparam string question, @requestparam string department) {
return ragchatclient.prompt()
.user(question)
// 运行时动态添加过滤条件
.advisors(a -> a.param(
questionansweradvisor.filter_expression,
"department == '" + department + "'"
))
.call()
.content();
}9.9 向量数据库怎么选?
| 数据库 | 适合场景 | 依赖 |
|---|---|---|
| redis | 小规模、已有redis集群 | spring-ai-starter-vector-store-redis |
| milvus | 生产级、海量数据 | spring-ai-starter-vector-store-milvus |
| pgvector | 不想额外部署、已有postgresql | spring-ai-starter-vector-store-pgvector |
| 内存版 | 开发测试 | simplevectorstore(spring ai内置) |
9.10 生产环境避坑(rag专属)
- embedding模型要和后续检索的向量维度对齐:比如openai的
text-embedding-3-small是1536维,配置里dimensions: 1536不能错。 - 分块大小要合适:
tokentextsplitter默认800 token一段。太小了语义不完整,太大了可能超模型上下文窗口。 - 相似度阈值别设太高:0.7是个不错的起点,太高了可能啥都搜不到,太低了可能搜到一堆不相关的。
- 文档元数据要打好:上传时把文件名、上传时间、部门等信息塞进
metadata,方便后续过滤和溯源。
十、多模型切换(一套代码走天下)
我前面吹了半天“切换模型零成本”,现在给你看证据。
10.1 用openai
spring:
ai:
openai:
api-key: ${openai_api_key}10.2 切到deepseek(只改配置)
spring:
ai:
openai:
api-key: ${deepseek_api_key}
base-url: https://api.deepseek.com/v110.3 切到本地ollama(llama3)
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3所有controller、service一行代码都不用改,这就是抽象的魅力。
10.4 如果同时用多个模型
那就手动建多个chatclient bean,分别注入不同的chatmodel实现:
@configuration
public class multimodelconfig {
@bean
public chatclient openaiclient(openaichatmodel openaimodel) {
return chatclient.create(openaimodel);
}
@bean
public chatclient ollamaclient(ollamachatmodel ollamamodel) {
return chatclient.create(ollamamodel);
}
}十一、生产环境避坑指南
11.1 api key打死别硬编码
# ❌ 这是作死
spring.ai.openai.api-key: sk-xxx123456
# ✅ 用环境变量
spring.ai.openai.api-key: ${openai_api_key}11.2 超时和重试配置好
spring:
ai:
openai:
api-key: ${openai_api_key}
chat:
options:
model: gpt-4o
timeout: 30s
retry:
max-attempts: 3
backoff:
initial-interval: 1s
multiplier: 211.3 成本控制别忘
ai调用是按token收费的,可以在代码里加限流(比如结合guava ratelimiter),或者监控每次调用的token消耗,超过阈值报警。
11.4 解耦——不要让ai拖垮主业务
如果你的核心交易链路依赖ai,建议把ai调用放到消息队列(kafka/rabbitmq)里异步处理,前端先返回“处理中”,等ai结果出来了再回调或轮询。
11.5 可观测性(2.0内置)
spring ai 2.0 集成了micrometer,你可以直接看ai.call.duration、ai.token.usage等指标,在grafana上画大盘。
十二、常见翻车现场(q&a)
q1:chatclient.builder注入失败,报nosuchbean?a:检查starter依赖有没有加,另外确认spring boot版本和spring ai版本匹配(2.0配boot4,1.x配boot3)。
q2:401 unauthorized?a:99%是api key错了,或者base-url不对(比如用了openai的key去调deepseek的地址)。
q3:结构化输出老是解析失败?a:先在prompt里明确说“返回json格式”,字段名保持和java record一致;还不行就开validateschema()自动重试。
q4:spring boot 3.x 能上spring ai 2.0吗?a:不能!别挣扎了。boot3用ai 1.1.x,boot4用ai 2.0。
q5:我用ollama本地跑,速度慢得要死?a:正常,本地模型吃显卡,建议换成云端api做开发测试,生产再评估。
十三、学习路线图
| 阶段 | 涉及知识点(你能学到什么) |
|---|---|
| 第1阶段:开荒 | maven依赖管理、application.yml配置技巧、环境变量安全、chatclient.builder的注入与构建、@restcontroller最简接口、commandlinerunner快速测试 |
| 第2阶段:对话与控制 | prompt多角色消息结构、systemmessage/usermessage/assistantmessage、prompttemplate动态模板、外部文件加载模板、流式响应flux<string> |
| 第3阶段:结构化输出 | record定义、entity()方法映射、json schema自动生成、自纠错模式validateschema()、异常处理 |
| 第4阶段:工具调用(核心) | @tool注解定义工具、defaulttools()注册、工具描述的重要性、function接口替代方案、多工具协同、日志追踪工具执行过程 |
| 第5阶段:rag + 向量库 | 向量化原理、retrievalaugmentationadvisor配置、milvus/pgvector集成、文档切割与嵌入、相似度检索、prompt增强 |
| 第6阶段:生产级落地 | 多模型动态切换、超时/重试策略、限流与熔断、异步解耦(mq)、可观测性(micrometer指标)、成本监控、api key轮换 |
十四、资源传送门
- 官网:https://spring.io/projects/spring-ai
- 官方参考文档:https://docs.spring.io/spring-ai/reference/index.html
- github示例仓库:https://github.com/spring-projects/spring-ai
- 快速建项目:https://start.spring.io
最后啰嗦一句:spring ai 2.0 刚刚ga,社区正热,现在上车正是时候。别怕踩坑,坑里我都替你趟过一遍了,上面这些配置和代码,我保证都是亲自跑通的。如果遇到问题,拿着异常信息去官方github的issues里搜,大概率已经有人解决了。
到此这篇关于springai保姆级实战教程的文章就介绍到这了,更多相关springai实战教程内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论