Spring AI 是 JVM 上的 AI 工程抽象层,作用类似 Spring Data 之于数据库:它把 OpenAI、Anthropic、阿里通义千问、本地 Ollama 等模型提供商的差异统一成一套可移植的 API,业务代码面向 ChatClient 编程,而不是面向某个厂商的 HTTP 接口编程。我把它用进 AI 爆款文章创作器后,模型切换从改代码变成改配置,多阶段生成流程也收敛成了几个稳定抽象。下面按集成路径讲清楚它在项目中实际承担的职责。
一、不引入框架时,集成大模型要写多少东西
框架的定位可以从官网自述理解:把 Spring 生态的可移植、模块化设计原则延伸到 AI 领域。它不自研模型,也不与任何模型厂商竞争,只做 Java 应用与模型之间的集成层。这决定了它的使用边界——凡是跟”调模型”相关的横切问题,它都给出统一解法;凡是业务独有的流程编排,仍由项目自己的 Service 层负责。
写死 HTTP 调用的方案人人都能搭,成本藏在维护里。对比一个只有”问答”能力的调用差异:
| 环节 | 手写 HTTP 调用 | Spring AI |
|---|---|---|
| 请求构造 | 手拼 JSON、维护 URL 版本 | 流式 Builder API |
| 响应解析 | 手写反序列化与错误码映射 | 自动映射到实体或 record |
| 流式输出 | 手写字节流解析 | Flux 直接返回 |
| 厂商切换 | 重写调用层 | 换依赖与配置 |
| 工具调用 | 手写协议与结果回填 | Bean 方法直接暴露 |
手写方案在单模型原型阶段看不出差距,一旦要接第二个模型,或者要从同步改成流式,改动面会铺到所有调用点。
二、Spring AI 在爆款文章创作器中承担的核心职责
2.1 统一对话入口 ChatClient
ChatClient 是框架对外的主要门面,风格贴近 WebClient。创作器里所有大模型能力都收敛到这一个客户端上,业务层不再感知具体模型。Spring Boot 会自动装配一个 ChatClient.Builder,构造函数注入即可用。先引入对应 starter:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
换成通义或本地模型时,只改坐标与配置块,Service 代码不动:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.7
2.2 用 ChatClient 封装多阶段生成
爆款文章创作器的生成链路是选题、大纲、正文、标题润色四段,每段都是一次模型调用。我把它拆成独立的写作模板,共用同一个客户端:
@Service
public class ArticleGenerationService {
private final ChatClient chatClient;
public ArticleGenerationService(ChatClient.Builder builder) {
this.chatClient = builder.defaultSystem(
"你是资深新媒体编辑,输出中文爆款文章,标题党、水词一律禁用")
.build();
}
public String generateOutline(String topic) {
return chatClient.prompt()
.user(u -> u.text("围绕【{topic}】产出三版大纲,每版 5 个小节,含选题角度。")
.param("topic", topic))
.call()
.content();
}
}
system 提示词统一管住角色与约束,user 消息用参数模板填充,keyword 一变,整段逻辑无需改动。
多阶段编排本身放在编排层,每个阶段各持一份模板与一个回调,阶段失败可以单独重试,不会因为某一段超时把整篇文章推倒重来。这一步属于项目自己的设计,Spring AI 只管单次调用的稳定性。
2.3 上下文记忆与知识增强
创作器里”换一个风格重写””在上文基础上续写”这类需求,需要把历史片段带进对话。框架的 advisors 机制可以在每次请求时把对话历史自动注入上下文,业务代码不用手拼多轮消息。知识库类文章再叠加向量检索,把素材库文档先向量化,问答前按相似度取回 top-k 片段塞进 prompt,回答就锚定在自有数据上。
2.4 结构化输出直接进实体
大纲、标题列表这类结果,以前要写正则从文本里抠,现在一行声明目标类型:
public record OutlineResult(String angle, List<String> sections) {}
List<OutlineResult> results = chatClient.prompt()
.user("为【AI 写作工具】输出 3 个选题角度,JSON 数组格式")
.call()
.entity(new ParameterizedTypeReference<List<OutlineResult>>() {});
框架内部负责让模型按格式返回并完成反序列化,业务代码拿到的是强类型对象,后续存库、渲染都顺理成章。
三、流式与工具调用在业务代码里的位置
3.1 流式输出走 SSE 通道
长文生成耗时以十秒计,正文阶段我用流式把 token 逐段推给前端 Vue,接口直接返回 Flux:
@PostMapping(value = "/api/article/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamArticle(@RequestBody GenerateRequest request) {
return chatClient.prompt()
.user(u -> u.text("按大纲【{outline}】生成正文,分 5 段输出。")
.param("outline", request.outline()))
.stream()
.content();
}
3.2 工具调用把业务能力接进对话
写素材类文章时,模型需要查产品库或关键词热度,我把 Service 方法声明成工具,模型自己决定何时调用:
@Bean
@Description("查询素材库中与主题相关的文章标题与热度")
public Function<MaterialQuery, List<Material>> materialFetcher() {
return query -> materialService.search(query.topic(), query.limit());
}
方法名带 @Description 之后,调用点只需要在 prompt 里声明工具名,协议细节全部由框架处理。前端展示时,模型哪一步调了素材库、返回了什么,都能从调用记录还原,排错时链路清晰。
四、上生产前必须盯住的四个环节
- 超时与重试:模型接口偶发抖动,给调用配连接超时、读超时,配合退避重试,避免用户请求跟着失败;
- token 预算:流式生成要设 max-tokens,正文分段生成也能压低单次成本与首字延迟;
- 输出护栏:模型返回进入落库或渲染前做内容校验,防止注入类文本进入下游;
- 评估闭环:记录每次请求的 prompt 与响应,用固定测试集回归,模型或提示词变更后对比打分。
这四项在框架之外,却是生产可用与 Demo 的分水岭。框架解决了”怎么调”,这些环节决定”调得稳不稳”。
常见问题(FAQ)
Q1:Spring AI 和直接调 SDK 有什么区别?
多模型可移植。业务代码只依赖 ChatClient 抽象,切换或扩展模型厂商不动调用层。
Q2:接入自己的私有化模型要怎么做?
选 OpenAI 兼容协议的服务部署,starter 指向自定义 base-url 与模型名即可。
Q3:流式输出用什么类型返回前端?
接口返回 text/event-stream,方法返回 Flux,前端逐段读取渲染。