Skip to content

Spring Boot + Spring AI:最小接入、聊天接口与流式输出

这一篇只聚焦第一阶段:把 Spring Boot 项目里的大模型调用和前端交互主线跑通。

也就是说,这里先不展开 RAGAgent 和复杂工具编排,而是把下面几件事做扎实:

  1. 依赖怎么加
  2. 配置怎么写
  3. 聊天接口怎么暴露
  4. 流式输出怎么接
  5. 为什么建议按 Controller -> Service 分层

1. 最小接入通常要准备什么

最小接入通常至少包括:

  1. spring-boot-starter-web
  2. 一个 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

这样写最想表达的是:

  1. Key 走环境变量
  2. 模型默认值走配置
  3. 业务代码里少写硬编码

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 里

更自然的做法是:

  1. Controller 收请求
  2. Service 组织 Prompt 和模型调用
  3. 后续再往外拆日志、工具、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

这条链路最想表达的是:

  1. 前端拿到的不是一次性完整 JSON
  2. 后端也不是“先生成完再统一返回”
  3. 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 里

这样会很快变得:

  1. 不好复用
  2. 不好测试
  3. 不好扩展工具调用和 RAG

7.2 只会同步返回,不考虑流式体验

只要回答一长,用户体验通常就会明显变差。

7.3 把模型名、Key、系统提示写死在业务代码里

这种写法在:

  1. 换模型
  2. 区分环境
  3. 做灰度

时都会很难受。

8. 这一篇最重要的结论

第一阶段最重要的不是“把所有 AI 能力一口气做完”,而是把 Spring Boot 里的最小聊天主线跑顺:配置清楚、分层清楚、同步和流式接口都能稳定暴露。只有这一层打稳,后面的工具调用、RAG 和 Agent 才不会越来越乱。

基于 VitePress 构建的个人技术笔记。