Appearance
Spring Boot + Spring AI:最小接入、聊天接口与流式输出
这一篇只聚焦第一阶段:把 Spring Boot 项目里的大模型调用和前端交互主线跑通。
也就是说,这里先不展开 RAG、Agent 和复杂工具编排,而是把下面几件事做扎实:
- 依赖怎么加
- 配置怎么写
- 聊天接口怎么暴露
- 流式输出怎么接
- 为什么建议按
Controller -> Service分层
1. 最小接入通常要准备什么
最小接入通常至少包括:
spring-boot-starter-web- 一个
Spring AI模型 Starter
例如:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>这个依赖组合的重点不是“只能用 OpenAI”,而是先建立一个最容易理解的最小入口。
2. 配置项最小长什么样
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini这样写最想表达的是:
- Key 走环境变量
- 模型默认值走配置
- 业务代码里少写硬编码
3. 一个最小聊天接口
java
package com.example.ai.web;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 最小聊天接口示例。
*/
@RestController
public class AiChatController {
private final ChatClient chatClient;
public AiChatController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
/**
* 接收用户问题并返回模型回答。
*
* @param message 用户输入
* @return 模型生成的文本结果
*/
@GetMapping("/ai/chat")
public String chat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}4. 为什么不建议把 Prompt 都写在 Controller 里
更自然的做法是:
- Controller 收请求
- Service 组织 Prompt 和模型调用
- 后续再往外拆日志、工具、RAG 和安全治理
4.1 一个更稳的分层示例
java
package com.example.ai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
/**
* AI 对话服务。
*/
@Service
public class AiAssistantService {
private final ChatClient chatClient;
public AiAssistantService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
/**
* 同步返回模型回答。
*
* @param question 用户问题
* @return 模型回答
*/
public String ask(String question) {
return chatClient.prompt()
.system("你是一名后端技术助手,回答尽量准确、简洁、贴近工程。")
.user(question)
.call()
.content();
}
/**
* 流式返回模型增量内容。
*
* @param question 用户问题
* @return 增量文本流
*/
public Flux<String> stream(String question) {
return chatClient.prompt()
.system("你是一名后端技术助手,回答尽量准确、简洁、贴近工程。")
.user(question)
.stream()
.content();
}
}java
package com.example.ai.web;
import com.example.ai.service.AiAssistantService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
/**
* AI 对话接口。
*/
@RestController
public class AiAssistantController {
private final AiAssistantService aiAssistantService;
public AiAssistantController(AiAssistantService aiAssistantService) {
this.aiAssistantService = aiAssistantService;
}
@GetMapping("/ai/ask")
public String ask(@RequestParam String question) {
return aiAssistantService.ask(question);
}
@GetMapping(value = "/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String question) {
return aiAssistantService.stream(question);
}
}5. 流式输出的链路怎么理解
很多人第一次接流式接口时,最容易只记住 Flux<String> 这一行。但真正要看清的是整条链路。
5.1 一张流程图看清
mermaid
sequenceDiagram
participant F as 前端
participant C as Controller
participant S as AiAssistantService
participant M as 模型
F->>C: GET /ai/stream?question=...
C->>S: stream(question)
S->>M: 发起流式生成请求
M-->>S: 持续返回增量内容
S-->>C: Flux<String>
C-->>F: text/event-stream这条链路最想表达的是:
- 前端拿到的不是一次性完整 JSON
- 后端也不是“先生成完再统一返回”
Controller -> Service -> 模型这条链路都要支持流式
6. 一个聚合接口骨架
当你刚开始搭项目时,最实用的往往是先准备一个统一的 demo 接口层,方便前端或测试快速验证。
java
package com.example.ai.web;
import com.example.ai.service.AiAssistantService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
/**
* AI 基础能力示例接口。
*/
@RestController
public class AiDemoController {
private final AiAssistantService aiAssistantService;
public AiDemoController(AiAssistantService aiAssistantService) {
this.aiAssistantService = aiAssistantService;
}
@GetMapping("/demo/ai/chat")
public String chat(@RequestParam String q) {
return aiAssistantService.ask(q);
}
@GetMapping(value = "/demo/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String q) {
return aiAssistantService.stream(q);
}
}7. 第一阶段最容易踩的坑
7.1 Prompt 全写在 Controller 里
这样会很快变得:
- 不好复用
- 不好测试
- 不好扩展工具调用和 RAG
7.2 只会同步返回,不考虑流式体验
只要回答一长,用户体验通常就会明显变差。
7.3 把模型名、Key、系统提示写死在业务代码里
这种写法在:
- 换模型
- 区分环境
- 做灰度
时都会很难受。
8. 这一篇最重要的结论
第一阶段最重要的不是“把所有 AI 能力一口气做完”,而是把 Spring Boot 里的最小聊天主线跑顺:配置清楚、分层清楚、同步和流式接口都能稳定暴露。只有这一层打稳,后面的工具调用、RAG 和 Agent 才不会越来越乱。