欢迎光临
我们一直在努力

【Spring Boot 3 + Spring AI】 实战:十分钟集成 OpenAI API 构建智能应用

Spring Boot 3 + Spring AI 实战:十分钟集成 OpenAI API 构建智能应用

    • 🚀 前言:为什么选择 Spring AI?
    • 🧠 一、Spring AI 核心架构与原理
      • 1.1 整体架构原理图
      • 1.2 AI 交互流程图
    • ⚙️ 二、环境准备与项目初始化
      • 2.1 技术栈要求
      • 2.2 Maven 依赖配置
      • 2.3 配置文件设置
    • 💬 三、快速入门:你的第一个 AI 对话机器人
      • 3.1 创建 Service 层
      • 3.2 创建 Controller 层
    • 🚀 四、进阶实战:提示词工程与流式响应
      • 4.1 提示词模板
      • 4.2 流式响应
    • 🔍 五、核心应用:RAG(检索增强生成)实战
      • 5.2 Spring AI 实现 RAG
    • 🛠️ 六、Function Calling:赋予 AI 调用 Java 方法的能力
      • 6.1 Function Calling 流程图
      • 6.2 实战:查询天气机器人
    • 🛡️ 七、生产环境最佳实践
      • 7.1 安全性
      • 7.2 Token 计费控制
      • 7.3 异常处理
      • 7.4 性能优化
    • 🎯 八、总结

摘要:随着生成式 AI 的爆发,Java 开发者不再需要羡慕 Python 的丰富生态。Spring AI 作为 Spring 家族的新成员,为 Java 生态带来了类似 LangChain 的标准化抽象。本文将基于 Spring Boot 3,深入剖析 Spring AI 的核心架构,通过实战案例带你十分钟集成 OpenAI API,构建具备流式输出、提示词模板甚至 RAG(检索增强生成)能力的智能应用。


🚀 前言:为什么选择 Spring AI?

在过去的一年里,如果你想用 Java 开发 AI 应用,可能需要直接调用 OpenAI 的 HTTP 接口,或者移植 Python 的 LangChain 库。这不仅繁琐,而且缺乏 Java 生态的“味道”。 Spring AI 的诞生改变了这一切。它并非 LangChain 的简单移植,而是遵循 Spring 设计哲学(可移植性、模块化、面向接口)构建的 AI 应用开发框架。它提供了以下核心价值:

  • API 抽象:一套代码支持 OpenAI、Azure OpenAI、HuggingFace 等多种模型提供商。
  • 同步与流式:原生支持流式响应,不再需要手动处理 Server-Sent Events (SSE)。
  • Spring Boot 集成:自动配置、外部化配置,开箱即用。

  • 🧠 一、Spring AI 核心架构与原理

    在写代码之前,理解 Spring AI 的架构设计至关重要,这有助于我们理解它的扩展性。

    1.1 整体架构原理图

    Spring AI 将 AI 模型交互抽象为多个层次,从底层的 HTTP 通信到上层的 POJO 转换。

    #mermaid-svg-AOAml1yD54pv6wcZ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AOAml1yD54pv6wcZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AOAml1yD54pv6wcZ .error-icon{fill:#552222;}#mermaid-svg-AOAml1yD54pv6wcZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AOAml1yD54pv6wcZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AOAml1yD54pv6wcZ .marker.cross{stroke:#333333;}#mermaid-svg-AOAml1yD54pv6wcZ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AOAml1yD54pv6wcZ p{margin:0;}#mermaid-svg-AOAml1yD54pv6wcZ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster-label text{fill:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster-label span{color:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster-label span p{background-color:transparent;}#mermaid-svg-AOAml1yD54pv6wcZ .label text,#mermaid-svg-AOAml1yD54pv6wcZ span{fill:#333;color:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .node rect,#mermaid-svg-AOAml1yD54pv6wcZ .node circle,#mermaid-svg-AOAml1yD54pv6wcZ .node ellipse,#mermaid-svg-AOAml1yD54pv6wcZ .node polygon,#mermaid-svg-AOAml1yD54pv6wcZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AOAml1yD54pv6wcZ .rough-node .label text,#mermaid-svg-AOAml1yD54pv6wcZ .node .label text,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape .label,#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-AOAml1yD54pv6wcZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AOAml1yD54pv6wcZ .rough-node .label,#mermaid-svg-AOAml1yD54pv6wcZ .node .label,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape .label,#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape .label{text-align:center;}#mermaid-svg-AOAml1yD54pv6wcZ .node.clickable{cursor:pointer;}#mermaid-svg-AOAml1yD54pv6wcZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AOAml1yD54pv6wcZ .arrowheadPath{fill:#333333;}#mermaid-svg-AOAml1yD54pv6wcZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AOAml1yD54pv6wcZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AOAml1yD54pv6wcZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AOAml1yD54pv6wcZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AOAml1yD54pv6wcZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AOAml1yD54pv6wcZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AOAml1yD54pv6wcZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster text{fill:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster span{color:#333;}#mermaid-svg-AOAml1yD54pv6wcZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AOAml1yD54pv6wcZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AOAml1yD54pv6wcZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape p,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape rect,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AOAml1yD54pv6wcZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AOAml1yD54pv6wcZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AOAml1yD54pv6wcZ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    基础设施与外部服务

    模型提供商适配层

    Spring AI 抽象层

    应用层

    Spring Boot Application

    Chat Client / Image Client

    Prompt Templates

    Output Parser

    Embedding Model

    OpenAI Adapter

    Azure Adapter

    HuggingFace Adapter

    OpenAI API

    Vector Databasee.g. Redis/PGVector

    核心组件解析:

    • Client 层:最顶层的入口,如 ChatClient,开发者通过它与 AI 交互。
    • Prompt 层:负责将用户输入、系统指令和变量组装成 AI 能理解的格式。
    • Model 层:具体的模型实现,负责将 Prompt 发送给 API 并解析响应。

    1.2 AI 交互流程图

    当我们调用 chatClient.call() 时,Spring AI 内部发生了什么?

    Converter

    OpenAI HTTP API

    Converter Jackson/JSON

    Prompt Template

    ChatClient

    Spring Boot App

    Converter

    OpenAI HTTP API

    Converter Jackson/JSON

    Prompt Template

    ChatClient

    Spring Boot App

    #mermaid-svg-7MFetsnPb1s8BG24{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7MFetsnPb1s8BG24 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7MFetsnPb1s8BG24 .error-icon{fill:#552222;}#mermaid-svg-7MFetsnPb1s8BG24 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7MFetsnPb1s8BG24 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7MFetsnPb1s8BG24 .marker.cross{stroke:#333333;}#mermaid-svg-7MFetsnPb1s8BG24 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7MFetsnPb1s8BG24 p{margin:0;}#mermaid-svg-7MFetsnPb1s8BG24 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7MFetsnPb1s8BG24 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7MFetsnPb1s8BG24 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-7MFetsnPb1s8BG24 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-7MFetsnPb1s8BG24 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-7MFetsnPb1s8BG24 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-7MFetsnPb1s8BG24 .sequenceNumber{fill:white;}#mermaid-svg-7MFetsnPb1s8BG24 #sequencenumber{fill:#333;}#mermaid-svg-7MFetsnPb1s8BG24 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-7MFetsnPb1s8BG24 .messageText{fill:#333;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7MFetsnPb1s8BG24 .labelText,#mermaid-svg-7MFetsnPb1s8BG24 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .loopText,#mermaid-svg-7MFetsnPb1s8BG24 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7MFetsnPb1s8BG24 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7MFetsnPb1s8BG24 .noteText,#mermaid-svg-7MFetsnPb1s8BG24 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7MFetsnPb1s8BG24 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7MFetsnPb1s8BG24 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7MFetsnPb1s8BG24 .actorPopupMenu{position:absolute;}#mermaid-svg-7MFetsnPb1s8BG24 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-7MFetsnPb1s8BG24 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7MFetsnPb1s8BG24 .actor-man circle,#mermaid-svg-7MFetsnPb1s8BG24 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-7MFetsnPb1s8BG24 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    .prompt().user("…").call()

    组装 Prompt (System/User)

    String / ChatRequest

    序列化为 JSON

    JSON Body

    HTTP POST (/v1/chat/completions)

    JSON Response

    反序列化为 ChatResponse

    ChatResponse 对象

    返回 Entity / String


    ⚙️ 二、环境准备与项目初始化

    2.1 技术栈要求

    硬性要求:

    • JDK 17+:Spring Boot 3 强制要求。
    • Spring Boot 3.2.x+:确保兼容性。
    • OpenAI API Key:你需要一个密钥。

    2.2 Maven 依赖配置

    Spring AI 的版本更新较快,建议使用 BOM(Bill of Materials)管理版本。

    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-bom</artifactId>
    <version>1.0.0-M2</version> <!– 请检查最新版本 –>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>
    <dependencies>
    <!– Spring AI OpenAI Starter –>
    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>

    <!– Web Starter –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    </dependencies>

    注意:由于 Spring AI 尚未放入 Maven 中央仓库的 release 版本,你需要确保添加了 Spring Milestone 或 Snapshot 仓库。

    <repositories>
    <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/milestone</url>
    <snapshots>
    <enabled>false</enabled>
    </snapshots>
    </repository>
    </repositories>

    2.3 配置文件设置

    在 application.yml 中配置 API Key 和基础选项:

    spring:
    ai:
    openai:
    api-key: ${OPENAI_API_KEY} # 建议使用环境变量
    chat:
    options:
    model: gpt4turbo # 或 gpt-3.5-turbo
    temperature: 0.7 # 控制随机性,0.0-2.0


    💬 三、快速入门:你的第一个 AI 对话机器人

    我们将构建一个简单的 REST API,接收用户问题并返回 AI 回答。

    3.1 创建 Service 层

    @Service
    public class ChatService {
    private final ChatClient chatClient;
    public ChatService(ChatClient.Builder chatClientBuilder) {
    this.chatClient = chatClientBuilder.build();
    }
    public String chat(String message) {
    // 简单调用
    return chatClient.prompt()
    .user(message)
    .call()
    .content();
    }
    }

    代码解析:

    • ChatClient.Builder:Spring Boot 自动配置注入的构建器。
    • .prompt():开始构建对话请求。
    • .user():添加用户消息。
    • .call():同步阻塞调用(等待完整响应)。
    • .content():提取返回文本内容。

    3.2 创建 Controller 层

    @RestController
    @RequestMapping("/api/ai")
    public class ChatController {
    private final ChatService chatService;
    public ChatController(ChatService chatService) {
    this.chatService = chatService;
    }
    @GetMapping("/chat")
    public Map<String, String> chat(@RequestParam String message) {
    String response = chatService.chat(message);
    return Map.of("answer", response);
    }
    }

    运行项目,访问 http://localhost:8080/api/ai/chat?message=Hello%20Spring%20AI,你将看到 AI 的回复。

    🚀 四、进阶实战:提示词工程与流式响应

    简单的字符串拼接无法胜任复杂业务。我们需要提示词模板来管理上下文,并使用流式传输来提升用户体验。

    4.1 提示词模板

    假设我们要做一个“写诗助手”,需要固定的角色设定。 创建 prompts/poet-template.st 文件(放在 src/main/resources 下):

    你是一位古代诗人,擅长唐诗宋词。
    请根据以下主题:{topic},创作一首七言绝句。
    要求:
    1. 韵律工整
    2. 意境优美
    3. 字数严格

    Service 代码升级:

    @Service
    public class PoetService {
    private final ChatClient chatClient;
    // Spring AI 自动加载 resources 目录下的 .st 文件
    public PoetService(ChatClient.Builder chatClientBuilder) {
    this.chatClient = chatClientBuilder
    .defaultSystem("你是一位专业的文学创作助手") // 默认系统提示
    .build();
    }
    public String writePoem(String topic) {
    return chatClient.prompt()
    .user(userSpec -> userSpec
    .text("请根据主题:{topic} 写一首诗") // 内联模板
    .param("topic", topic)
    )
    .call()
    .content();
    }
    }

    4.2 流式响应

    流式响应让用户感觉像是在实时打字,而不是等待几秒钟后一次性蹦出一段文字。这在 Web 界面中尤为重要。 流式接口原理图:

    #mermaid-svg-YMN60JdmtVtlwVjk{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YMN60JdmtVtlwVjk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YMN60JdmtVtlwVjk .error-icon{fill:#552222;}#mermaid-svg-YMN60JdmtVtlwVjk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YMN60JdmtVtlwVjk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YMN60JdmtVtlwVjk .marker.cross{stroke:#333333;}#mermaid-svg-YMN60JdmtVtlwVjk svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YMN60JdmtVtlwVjk p{margin:0;}#mermaid-svg-YMN60JdmtVtlwVjk .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster-label text{fill:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster-label span{color:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster-label span p{background-color:transparent;}#mermaid-svg-YMN60JdmtVtlwVjk .label text,#mermaid-svg-YMN60JdmtVtlwVjk span{fill:#333;color:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .node rect,#mermaid-svg-YMN60JdmtVtlwVjk .node circle,#mermaid-svg-YMN60JdmtVtlwVjk .node ellipse,#mermaid-svg-YMN60JdmtVtlwVjk .node polygon,#mermaid-svg-YMN60JdmtVtlwVjk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YMN60JdmtVtlwVjk .rough-node .label text,#mermaid-svg-YMN60JdmtVtlwVjk .node .label text,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape .label,#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape .label{text-anchor:middle;}#mermaid-svg-YMN60JdmtVtlwVjk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YMN60JdmtVtlwVjk .rough-node .label,#mermaid-svg-YMN60JdmtVtlwVjk .node .label,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape .label,#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape .label{text-align:center;}#mermaid-svg-YMN60JdmtVtlwVjk .node.clickable{cursor:pointer;}#mermaid-svg-YMN60JdmtVtlwVjk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YMN60JdmtVtlwVjk .arrowheadPath{fill:#333333;}#mermaid-svg-YMN60JdmtVtlwVjk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YMN60JdmtVtlwVjk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YMN60JdmtVtlwVjk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YMN60JdmtVtlwVjk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YMN60JdmtVtlwVjk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YMN60JdmtVtlwVjk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YMN60JdmtVtlwVjk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster text{fill:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster span{color:#333;}#mermaid-svg-YMN60JdmtVtlwVjk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YMN60JdmtVtlwVjk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YMN60JdmtVtlwVjk rect.text{fill:none;stroke-width:0;}#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape p,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape rect,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YMN60JdmtVtlwVjk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YMN60JdmtVtlwVjk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YMN60JdmtVtlwVjk :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    SSE Request

    Flux

    Async HTTP

    Chunks

    Chunks

    SSE Events

    Client

    Spring Controller

    ChatClient

    OpenAI API

    代码实现: Spring AI 的 stream() 方法返回的是 Reactor 的 Flux<String>,这可以完美适配 Spring WebFlux 或者 Spring MVC 的流式响应。

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamChat(@RequestParam String message) {
    return chatClient.prompt()
    .user(message)
    .stream() // 关键:开启流式
    .content(); // 返回 Flux<String>
    }

    前端测试体验: 使用 curl 测试可以看到数据一行行吐出:

    curl -N http://localhost:8080/api/ai/stream?message=讲个笑话


    🔍 五、核心应用:RAG(检索增强生成)实战

    大模型有两个致命弱点:知识幻觉(瞎编乱造)和知识截止日期(不知道今天发生的事)。 RAG (Retrieval-Augmented Generation) 是解决这些问题的银弹:先去外部知识库检索相关信息,然后扔给 LLM 让它基于这些信息回答。

    5.2 Spring AI 实现 RAG

    我们需要 EmbeddingModel(将文本转向量)和 VectorStore(存向量)。为了简化演示,我们使用基于内存的向量存储(生产环境建议用 PgVector 或 Milvus)。 添加依赖:

    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
    </dependency>
    <!– 注意:Transformers 比较重,适合本地演示。生产环境建议用 OpenAI 的 Embedding API –>

    RAG Service 代码:

    @Service
    public class RagService {
    private final ChatClient chatClient;
    private final VectorStore vectorStore;
    public RagService(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
    this.chatClient = chatClientBuilder.build();
    this.vectorStore = vectorStore;

    // 初始化时加载一些文档
    loadKnowledgeBase();
    }
    private void loadKnowledgeBase() {
    List<Document> documents = List.of(
    new Document("Spring Boot 3 最低要求 JDK 17。"),
    new Document("Spring AI 支持函数调用,可以连接外部 API。"),
    new Document("GraalVM 可以将 Java 应用编译为本地镜像,实现毫秒级启动。")
    );
    vectorStore.add(documents);
    }
    public String ask(String question) {
    // 1. 检索相关文档
    List<Document> similarDocs = vectorStore.similaritySearch(SearchRequest.query(question).withTopK(2));

    // 2. 构建包含上下文的 Prompt
    String context = similarDocs.stream()
    .map(Document::getContent)
    .collect(Collectors.joining("\\n"));

    String systemPrompt = """
    你是一个助手。请仅使用以下提供的上下文信息来回答用户的问题。
    如果上下文中不包含答案,请说"我不知道"。

    上下文:
    {context}
    """;
    // 3. 发送给 LLM
    return chatClient.prompt()
    .system(systemPrompt)
    .user(question)
    .call()
    .content();
    }
    }

    关键点解析:

  • VectorStore:Spring AI 抽象了向量数据库接口。我们调用 similaritySearch 即可找到语义最相近的文本片段。
  • Prompt 注入:我们将检索到的文档塞进 System Prompt,这是 RAG 成功的关键。

  • 🛠️ 六、Function Calling:赋予 AI 调用 Java 方法的能力

    这是 AI 应用最强大的功能之一。AI 不再只是“聊天机器人”,它可以理解意图并调用后端业务逻辑,比如“查询天气”、“下单”。

    6.1 Function Calling 流程图

    Java Function

    Spring AI

    LLM

    用户

    Java Function

    Spring AI

    LLM

    用户

    #mermaid-svg-aNDcoXXGcYgRWlaD{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aNDcoXXGcYgRWlaD .error-icon{fill:#552222;}#mermaid-svg-aNDcoXXGcYgRWlaD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aNDcoXXGcYgRWlaD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aNDcoXXGcYgRWlaD .marker.cross{stroke:#333333;}#mermaid-svg-aNDcoXXGcYgRWlaD svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aNDcoXXGcYgRWlaD p{margin:0;}#mermaid-svg-aNDcoXXGcYgRWlaD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aNDcoXXGcYgRWlaD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aNDcoXXGcYgRWlaD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD .sequenceNumber{fill:white;}#mermaid-svg-aNDcoXXGcYgRWlaD #sequencenumber{fill:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD .messageText{fill:#333;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aNDcoXXGcYgRWlaD .labelText,#mermaid-svg-aNDcoXXGcYgRWlaD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .loopText,#mermaid-svg-aNDcoXXGcYgRWlaD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aNDcoXXGcYgRWlaD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-aNDcoXXGcYgRWlaD .noteText,#mermaid-svg-aNDcoXXGcYgRWlaD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aNDcoXXGcYgRWlaD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aNDcoXXGcYgRWlaD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aNDcoXXGcYgRWlaD .actorPopupMenu{position:absolute;}#mermaid-svg-aNDcoXXGcYgRWlaD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-aNDcoXXGcYgRWlaD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aNDcoXXGcYgRWlaD .actor-man circle,#mermaid-svg-aNDcoXXGcYgRWlaD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-aNDcoXXGcYgRWlaD :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    帮我查询北京现在的天气

    识别意图 ->> 需要调用 getWeather(city)

    返回 Function Call 指令

    执行 getWeather("Beijing")

    返回 "晴, 25度"

    将结果作为上下文再次请求

    北京现在天气晴朗,气温25度

    6.2 实战:查询天气机器人

    步骤 1:定义 Java Function

    public record WeatherRequest(String city) {}
    public record WeatherResponse(String city, double temperature, String condition) {}
    @Component
    public class WeatherService {
    @Bean
    public Function<WeatherRequest, WeatherResponse> weatherFunction() {
    return request -> {
    // 模拟调用第三方天气 API
    if ("Beijing".equalsIgnoreCase(request.city())) {
    return new WeatherResponse("Beijing", 25.5, "Sunny");
    }
    return new WeatherResponse(request.city(), 0, "Unknown");
    };
    }
    }

    步骤 2:启用 Function Calling

    String response = chatClient.prompt()
    .user("北京现在天气怎么样?")
    .functions("weatherFunction") // 指定 Bean 名称
    .call()
    .content();

    原理: Spring AI 会自动扫描容器中注册的 Function Bean,并在调用 OpenAI 时将函数的 JSON Schema 发送给模型。如果模型判断需要调用该函数,它会暂停生成,返回一个工具调用指令,Spring AI 拦截该指令,执行 Java 方法,将结果再次喂给模型,模型最后组织语言回复用户。

    🛡️ 七、生产环境最佳实践

    将 Demo 级应用投入生产,需要注意以下几点:

    7.1 安全性

    绝对不要将 API Key 提交到 Git

    • 方案 A:使用环境变量(推荐)。export OPENAI_API_KEY=sk-xxxx...
    • 方案 B:使用 Spring Cloud Config 或 Vault。

    7.2 Token 计费控制

    LLM 按输入和输出的 Token 计费。为了防止用户滥用导致账单爆炸:

  • 限制 Max Tokens:chatClient.prompt()
    .options(OpenAiChatOptions.builder().withMaxTokens(500).build())
    ...
  • 输入长度截断:在发送给 AI 前,截断过长的 Prompt。
  • 7.3 异常处理

    网络波动或 OpenAI 服务不可用是常态。

    try {
    return chatClient.prompt().user(msg).call().content();
    } catch (OpenAiApiException e) {
    if (e.getStatusCode().is5xxServerError()) {
    // 服务端错误,重试
    return retryLogic(msg);
    }
    // 其他错误
    return "抱歉,AI 服务暂时不可用。";
    }

    7.4 性能优化

    • 连接池配置:Spring AI 底层使用 WebClient 或 RestTemplate,确保配置了适当的连接池大小。
    • 缓存:对于常见问题(如 FAQ),可以使用 Redis 缓存 AI 的回答,直接返回,避免重复计费。

    🎯 八、总结

    通过 Spring Boot 3 和 Spring AI,Java 开发者现在拥有了构建企业级 AI 应用的强力武器。本文涵盖了从基础架构、简单对话、流式交互到复杂的 RAG 和 Function Calling 的完整链路。 技术演进路径: 手工 HTTP 调用 ➡️ Spring AI 标准化 Client ➡️ RAG + Function Calling 的智能 Agent Spring AI 的优势在于它完美融合了 Spring 生态的非侵入式设计理念,让 Java 开发者能专注于业务逻辑,而不是繁琐的 HTTP 编解码。未来的 AI 应用开发,将不仅仅是算法模型的比拼,更是工程化落地能力的较量。 现在,打开你的 IDE,开始构建你的第一个 Spring AI 应用吧!

    赞(0)
    未经允许不得转载:171主机测评 » 【Spring Boot 3 + Spring AI】 实战:十分钟集成 OpenAI API 构建智能应用
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址