上线我的 2.0

上线我的 2.0

马图图

岁月变迁何必不悔,尘世喧嚣怎能无愧。

17 文章数
1 评论数

Spring AI 实战:从 HTTP 调用到优雅封装

Matuto
2026-01-06 / 0 评论 / 11 阅读 / 0 点赞

最近把项目里的 Gemini API 调用迁移到了 Spring AI,踩了一些坑,也总结出一套还算顺手的用法,记录一下。

为什么选 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分钟

JSON 响应解析

让 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;
}

要不要用 Agent?

Spring AI 也支持 Agent 模式,可以让 LLM 自己决定调用什么工具。但对于这种"输入 → 处理 → 输出"的简单场景,用 Agent 属于过度设计。

Agent 适合的场景:

  • 需要 LLM 自主决策调用哪些工具

  • 多步骤推理任务

  • 动态工作流

如果只是固定流程的文本处理,直接用 ChatModel 就够了。

总结

Spring AI 确实让 LLM 调用变得更优雅,尤其是:

  • 统一的抽象层,换模型成本很低

  • 流式响应开箱即用

  • 和 Spring 生态无缝集成

不过也别指望它能解决所有问题,像提示词工程、响应解析这些还是得自己处理。


写于 2025 年 1 月,基于 Spring AI 1.1.2 版本

上一篇 下一篇
评论
来首音乐
光阴似箭
今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月
文章目录
每日一句