最近把项目里的 Gemini API 调用迁移到了 Spring AI,踩了一些坑,也总结出一套还算顺手的用法,记录一下。
之前项目里调 LLM 用的是手写 HTTP 请求,代码大概长这样:
// 构造请求体
JSONObject body = new JSONObject();
body.put("model", "gemini-2.5-pro");
body.put("messages", messageList);
// 发请求
String response = HttpUtil.post(apiUrl, body.toJSONString());
// 解析响应
JSONObject result = JSONObject.parseObject(response);
String content = result.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content");
能用,但问题不少:
换个模型要改一堆代码
流式响应处理起来很麻烦
错误处理全靠 try-catch 硬扛
Spring AI 封装了这些底层细节,切换模型只需要改配置,代码基本不用动。
用 Spring Boot 3.3+ 的话,pom.xml 加这些:
<properties>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>
<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>
<!-- OpenAI 兼容接口,Gemini/DeepSeek 等都能用 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- 流式响应需要 webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
application.yml 配置:
spring:
ai:
openai:
api-key: ${GEMINI_API_KEY}
base-url: https://generativelanguage.googleapis.com/v1beta/openai
这里用的是 OpenAI 兼容接口。Gemini、DeepSeek、Moonshot 这些国产模型基本都支持这个格式,换模型只需要改 base-url 和 api-key。
最简单的用法,注入 ChatModel 直接调:
@Service
public class ChatService {
@Resource
private ChatModel chatModel;
public String chat(String userMessage) {
Prompt prompt = new Prompt(userMessage);
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getText();
}
}
实际业务里基本都要设置 system prompt:
public String chatWithSystem(String systemPrompt, String userMessage) {
List<Message> messages = List.of(
new SystemMessage(systemPrompt),
new UserMessage(userMessage)
);
Prompt prompt = new Prompt(messages);
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getText();
}
温度、最大 token 这些参数用 OpenAiChatOptions 设置:
public String chatWithOptions(String content, Double temperature) {
OpenAiChatOptions options = OpenAiChatOptions.builder()
.model("gemini-2.5-pro")
.temperature(temperature)
.maxTokens(4096)
.build();
Prompt prompt = new Prompt(content, options);
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getText();
}
打字机效果用 stream() 方法:
public Flux<String> streamChat(String content) {
Prompt prompt = new Prompt(content);
return chatModel.stream(prompt)
.map(response -> {
if (response.getResult() != null &&
response.getResult().getOutput() != null) {
String text = response.getResult().getOutput().getText();
return text != null ? text : "";
}
return "";
})
.filter(text -> !text.isEmpty());
}
Controller 里返回 SSE:
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamChat(@RequestBody ChatRequest request) {
return chatService.streamChat(request.getContent())
.map(content -> ServerSentEvent.<String>builder()
.data(content)
.build());
}
项目里不同租户用不同的 API Key,不能用自动注入的 ChatModel。写了个工厂类动态创建:
@Component
public class ChatClientFactory {
private final ConcurrentHashMap<Long, OpenAiChatModel> cache = new ConcurrentHashMap<>();
@Resource
private SiteConfigProvider configProvider;
public OpenAiChatModel getChatModel(Long siteId) {
return cache.computeIfAbsent(siteId, this::createChatModel);
}
private OpenAiChatModel createChatModel(Long siteId) {
// 从数据库读配置
ApiConfig config = configProvider.getApiConfig(siteId);
OpenAiApi api = OpenAiApi.builder()
.baseUrl(config.getApiUrl())
.apiKey(config.getApiKey())
.build();
return OpenAiChatModel.builder()
.openAiApi(api)
.defaultOptions(OpenAiChatOptions.builder()
.model("gemini-2.5-flash")
.temperature(0.7)
.build())
.build();
}
// 配置变更时刷新缓存
public void refresh(Long siteId) {
cache.remove(siteId);
}
}
配合 Spring 事件机制,配置变更时自动刷新:
@Component
public class ConfigChangeListener {
@Resource
private ChatClientFactory factory;
@EventListener
public void onConfigChanged(ConfigChangedEvent event) {
factory.refresh(event.getSiteId());
}
}
直接用 ChatModel 太底层,业务代码里写一堆消息构造、参数设置会很乱。封装一层 Service:
@Service
public class PlaybookAnalysisService {
@Resource
private ChatClientFactory factory;
/**
* 通用场景分析
*/
public JSONObject analyze(Long siteId, String scenario, String content) {
// 从数据库读取该场景的 prompt 配置
PromptConfig config = getPromptConfig(scenario);
OpenAiChatModel chatModel = factory.getChatModel(siteId);
List<Message> messages = List.of(
new SystemMessage(config.getSystemPrompt()),
new UserMessage(content)
);
OpenAiChatOptions options = OpenAiChatOptions.builder()
.model("gemini-2.5-pro")
.temperature(config.getTemperature())
.maxTokens(config.getMaxTokens())
.build();
Prompt prompt = new Prompt(messages, options);
ChatResponse response = chatModel.call(prompt);
String text = response.getResult().getOutput().getText();
return JSONObject.parseObject(extractJson(text));
}
/**
* 角色分析
*/
public JSONObject analyzeRole(Long siteId, String script) {
return analyze(siteId, "role_analysis", script);
}
/**
* 场景分析
*/
public JSONObject analyzeScene(Long siteId, String script) {
return analyze(siteId, "scene_analysis", script);
}
}
Controller 就很干净了:
@RestController
@RequestMapping("/analysis")
public class AnalysisController {
@Resource
private PlaybookAnalysisService service;
@PostMapping("/role")
public Result<?> analyzeRole(@RequestBody AnalysisRequest req) {
Long siteId = SecurityUtils.getSiteId();
JSONObject result = service.analyzeRole(siteId, req.getContent());
return Result.success(result);
}
}
LLM 响应慢是常态,默认超时时间可能不够用。yml 里加上:
spring:
mvc:
async:
request-timeout: 300000 # 5分钟
让 LLM 返回 JSON 时,经常会带 markdown 代码块:
```json
{"name": "张三"}
需要提取纯 JSON:
```java
public static String extractJson(String text) {
if (text == null) return null;
// 移除 markdown 代码块
String result = text.trim();
if (result.startsWith("```json")) {
result = result.substring(7);
} else if (result.startsWith("```")) {
result = result.substring(3);
}
if (result.endsWith("```")) {
result = result.substring(0, result.length() - 3);
}
return result.trim();
}
偶尔会遇到响应里没内容的情况,加个判断:
private String extractContent(ChatResponse response) {
if (response == null ||
response.getResult() == null ||
response.getResult().getOutput() == null) {
throw new ServiceException("AI 响应为空");
}
String text = response.getResult().getOutput().getText();
if (text == null || text.isBlank()) {
throw new ServiceException("AI 响应内容为空");
}
return text;
}
Spring AI 也支持 Agent 模式,可以让 LLM 自己决定调用什么工具。但对于这种"输入 → 处理 → 输出"的简单场景,用 Agent 属于过度设计。
Agent 适合的场景:
需要 LLM 自主决策调用哪些工具
多步骤推理任务
动态工作流
如果只是固定流程的文本处理,直接用 ChatModel 就够了。
Spring AI 确实让 LLM 调用变得更优雅,尤其是:
统一的抽象层,换模型成本很低
流式响应开箱即用
和 Spring 生态无缝集成
不过也别指望它能解决所有问题,像提示词工程、响应解析这些还是得自己处理。
写于 2025 年 1 月,基于 Spring AI 1.1.2 版本