Skip to content

Spring AI从环境搭建到首个生产化接口

本页不是“复制四行代码看见模型回答就结束”。它会带你从一个空 Spring Boot 项目开始,完成版本选择、依赖引入、密钥配置、自动配置验证、同步调用、SSE 流式调用、参数校验、异常转换和故障取证,并解释每一步为什么存在。

本页示例采用当前专栏统一基线:spring-ai-bom:2.0.0、2.x 风格的 spring-ai-starter-model-openai 和 JDK 21。这个组合只是本文可复现的教学基线,不代表任意 Spring Boot 版本都能与它混搭。已有项目必须先核对 Spring AI 发行说明和依赖兼容关系。

学习目标

完成本页后,你应该能够:

  1. 判断项目应使用 JDK 8、17 还是 21,以及为什么 JDK 8 不能直接运行当前 Spring AI。
  2. 解释 BOM、Starter、传递依赖和自动配置的关系。
  3. 解释 ChatClient.BuilderChatClientChatModel 和厂商 HTTP API 的职责边界。
  4. 写出可运行的同步接口和 SSE 流式接口。
  5. 说明一次请求从 Controller 到模型服务再返回浏览器的完整过程。
  6. 使用环境变量保存密钥,并区分本地、测试、预发和生产配置。
  7. 从依赖树、条件评估报告、配置绑定、HTTP 状态码和日志中定位启动或调用失败。
  8. 区分“Spring Boot 启动成功”“模型调用成功”和“达到生产可用”这三件不同的事。

一、先建立正确的系统边界

Spring AI 不在本机训练或运行大模型。下面这个 Demo 中,Spring Boot 应用是模型服务的客户端:

mermaid
flowchart TD
    A["浏览器或业务调用方"] --> B["Spring MVC参数绑定与校验"]
    B --> C["AiChatService"]
    C --> D["ChatClient"]
    D --> E["Prompt与Advisor链"]
    E --> F["ChatModel厂商适配器"]
    F --> G["HTTP客户端"]
    G --> H["云模型API或兼容推理服务"]
    H --> I["模型推理并生成Token"]
    I --> J["ChatResponse或Flux文本片段"]
    J --> K["HTTP JSON或SSE响应"]
组件负责什么不负责什么
Controller接收参数、校验、转换 HTTP 响应不保存 API Key,不编排复杂 AI 业务
Service组织业务 Prompt、调用策略和结果处理不绕过权限直接暴露模型能力
ChatClient方便地组装消息、Options、Advisor 和输出形式不是远程模型,也不训练模型
ChatModel统一模型调用抽象不保证所有厂商能力完全相同
Provider Starter提供配置属性、自动配置和具体适配器不替业务设计权限、限流和审计
模型服务Tokenize、推理、逐 Token 生成不天然知道企业实时数据和用户权限

不用 Spring AI 也可以手写 HTTP 请求。Spring AI 的价值不是“让 HTTP 消失”,而是把厂商协议、配置、消息对象、同步/流式调用、结构化输出、RAG 和工具调用放进统一的 Spring 工程边界。厂商差异依然存在,切换模型前仍要做能力和效果验证。

二、版本选择:先定运行基线再复制代码

2.1 JDK 8、17和21怎么选

项目情况建议原因
新建 Spring AI 服务优先 JDK 21当前 LTS,适合新服务并能使用现代 JVM 能力
已有 Spring Boot 3 项目至少 JDK 17,再按兼容矩阵选 Spring AISpring Boot 3 本身要求 Java 17+
已有 JDK 8 / Spring Boot 2 项目不要强行加入当前 Spring AI Starter字节码、Boot API、Jakarta 命名空间和依赖基线不兼容
JDK 8 主系统必须接 AI单独建设 AI 服务,主系统用受控 HTTP/RPC 调用隔离升级风险,主系统不必整体迁移 JDK

JDK 8 不能通过“在 pom.xml 中把 java.version 改成 8”兼容当前 Starter。编译器版本只是一个维度,依赖 Jar 本身可能已经使用更高版本字节码,并依赖 Spring Boot 3/4 和 Jakarta API。

JDK 8 主系统的合理边界:

mermaid
flowchart TD
    A["JDK 8核心业务系统"] --> B["内部鉴权网关或AI适配接口"]
    B --> C["JDK 21 Spring AI服务"]
    C --> D["模型、RAG和工具编排"]
    D --> E["模型服务与向量库"]

这样做不是为了追求微服务数量,而是隔离两套不兼容的运行时。AI 服务接口必须有认证、超时、限流、幂等和审计,不能因为是内网就裸奔。

2.2 版本必须成套

Spring AI 版本演进中出现过不同 Starter 名称:

教程来源可能看到的名称处理方式
较早版本教程spring-ai-openai-spring-boot-starter只能与该教程对应版本线使用
本专栏 2.x 基线spring-ai-starter-model-openai由 2.x BOM 管理版本

不要在同一个项目中同时加入新旧 Starter,也不要给每个 Spring AI 模块手写不同版本。BOM 的作用是提供一组经过配套管理的模块版本;Maven 导入 BOM 后,具体依赖通常不再写 <version>

2.3 先检查本机环境

bash
java -version
mvn -version

两条命令中的 Java 路径和版本都要检查。常见错误是 IDE 使用 JDK 21,而终端中的 JAVA_HOME 仍指向 JDK 8,导致 IDE 能运行、命令行构建失败。

Windows PowerShell:

powershell
$env:JAVA_HOME
Get-Command java
java -version
mvn -version

三、创建最小项目

示例目录:

text
spring-ai-first-demo/
├── pom.xml
└── src/
    ├── main/
    │   ├── java/com/example/ai/
    │   │   ├── SpringAiApplication.java
    │   │   ├── config/AiClientConfig.java
    │   │   ├── controller/AiChatController.java
    │   │   ├── dto/ChatRequest.java
    │   │   ├── dto/ChatResponseDto.java
    │   │   ├── exception/AiExceptionHandler.java
    │   │   └── service/AiChatService.java
    │   └── resources/
    │       ├── application.yml
    │       ├── application-local.yml
    │       └── application-prod.yml
    └── test/

分层的原因:Controller 只处理 HTTP;Service 表达业务调用;配置类集中构造客户端。未来加入 RAG、Tool Calling、限流或模型路由时,不需要把 Controller 改成巨型类。

四、完整Maven配置

pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>spring-ai-first-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>21</java.version>
        <spring-ai.version>2.0.0</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>
        <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>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

这里使用 Spring Boot 4.0.0 与 Spring AI 2.0.0 作为同一教学基线。若项目仓库的实际版本、官方兼容矩阵或依赖解析结果不同,应整体调整版本线,而不是只改一个 Starter 名称。

验证依赖:

bash
mvn dependency:tree
mvn dependency:tree -Dincludes=org.springframework.ai
mvn help:effective-pom

应该确认:

  • 只有一套 Spring AI 版本。
  • 没有同时出现新旧 OpenAI Starter。
  • 没有被业务依赖偷偷覆盖 Spring、Jackson、Reactor 或 HTTP 客户端版本。
  • Maven 实际使用的 JDK 与项目目标一致。

五、配置模型服务和密钥

5.1 公共配置

application.yml

yaml
spring:
  application:
    name: spring-ai-first-demo
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:local}

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: when_authorized

server:
  shutdown: graceful

5.2 本地配置

application-local.yml

yaml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      # 使用官方服务时通常不需要覆盖base-url;兼容服务按平台文档配置。
      base-url: ${OPENAI_BASE_URL:https://api.openai.com}
      chat:
        options:
          model: ${OPENAI_CHAT_MODEL:gpt-4o-mini}
          temperature: 0.2

配置层次:

mermaid
flowchart TD
    A["操作系统或Secret系统提供环境变量"] --> B["Spring Environment"]
    B --> C["spring.ai.openai配置属性绑定"]
    C --> D["OpenAI自动配置读取属性"]
    D --> E["创建API客户端和ChatModel"]
    E --> F["ChatClient.Builder使用ChatModel"]

${OPENAI_API_KEY} 没有默认值,意味着缺少密钥时应尽早失败,而不是误用一个测试密钥。模型名和 Base URL 可以为本地开发提供默认值,但生产环境应显式配置并纳入发布清单。

Windows PowerShell:

powershell
$env:OPENAI_API_KEY="替换成真实密钥"
$env:OPENAI_CHAT_MODEL="替换成账号实际可用的模型名"
$env:SPRING_PROFILES_ACTIVE="local"

Linux/macOS:

bash
export OPENAI_API_KEY='替换成真实密钥'
export OPENAI_CHAT_MODEL='替换成账号实际可用的模型名'
export SPRING_PROFILES_ACTIVE='local'

不要把真实密钥写进:

  • Git 仓库中的 YAML。
  • Dockerfile 的 ENV 或构建层。
  • Controller 返回值。
  • 异常堆栈和 DEBUG 请求日志。
  • 前端 JavaScript。
  • 截图、群聊和公开工单。

浏览器不能直接持有服务端模型密钥。浏览器请求自己的后端,后端完成身份认证、额度控制后再调用模型。

5.3 生产配置

application-prod.yml 可以只保留环境相关策略:

yaml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: ${OPENAI_BASE_URL}
      chat:
        options:
          model: ${OPENAI_CHAT_MODEL}

logging:
  level:
    org.springframework.ai: INFO

生产密钥应来自 Kubernetes Secret、Vault、云密钥系统或受控配置平台,并具备最小权限、轮换、审计和泄露吊销流程。

六、Spring Boot自动配置到底做了什么

加入 Provider Starter 后,不是 Maven 直接创建了 Bean。Maven 只把类和自动配置元数据放进 Classpath;真正创建 Bean 发生在 Spring Boot 启动时:

mermaid
flowchart TD
    A["SpringApplication.run"] --> B["创建并准备Environment"]
    B --> C["加载application与环境变量"]
    C --> D["扫描自动配置候选"]
    D --> E["判断ConditionalOnClass等条件"]
    E --> F{"条件是否满足"}
    F -->|"否"| G["记录未匹配原因"]
    F -->|"是"| H["绑定spring.ai.openai属性"]
    H --> I["创建厂商API客户端"]
    I --> J["创建OpenAiChatModel"]
    J --> K["创建ChatClient.Builder"]
    K --> L["业务配置构建ChatClient"]

常见条件包括:

  • 对应类是否存在于 Classpath。
  • 自动配置开关是否启用。
  • 是否已经有用户自定义的同类型 Bean。
  • 必要属性是否存在并成功绑定。
  • 当前应用类型和依赖是否满足要求。

因此,ChatClient.Builder 注入失败时,排查顺序应该是“依赖与条件”,而不是先更换 API Key。密钥错误通常影响客户端创建或远程调用,但 Starter 根本没进入 Classpath 时连相应自动配置类都不存在。

查看条件评估:

bash
mvn spring-boot:run -Dspring-boot.run.arguments=--debug

或在本地临时开启:

yaml
debug: true

搜索 CONDITIONS EVALUATION REPORT,分别看 Positive matches 和 Negative matches。不要在生产长期打开全局 DEBUG,它可能产生大量日志并扩大敏感数据暴露面。

七、编写可运行代码

7.1 启动类

java
package com.example.ai;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringAiApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringAiApplication.class, args);
    }
}

启动类放在 com.example.ai 根包,子包中的 Controller、Service 和配置类才能被默认组件扫描发现。

7.2 集中构建ChatClient

java
package com.example.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiClientConfig {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("""
                        你是企业内部技术助手。
                        回答必须使用简体中文;不确定时明确说明不确定;
                        不得编造系统中不存在的数据,也不得声称已经执行未实际执行的操作。
                        """)
                .build();
    }
}

为什么注入 Builder 再构建:

  • Builder 已连接自动配置创建的 ChatModel
  • 可以集中设置默认 System Prompt、Advisor 和 Options。
  • 业务代码依赖稳定的 ChatClient,不重复创建配置。
  • 测试或多模型路由时可以显式构建不同客户端。

System Prompt 只是模型行为约束,不是安全边界。用户仍可能诱导模型忽略它,所以数据权限、工具权限和输出校验必须由后端代码执行。

7.3 请求和响应对象

java
package com.example.ai.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record ChatRequest(
        @NotBlank(message = "message不能为空")
        @Size(max = 4000, message = "message不能超过4000个字符")
        String message
) {
}
java
package com.example.ai.dto;

public record ChatResponseDto(String answer) {
}

长度限制不仅防止数据库字段越界,还用于:

  • 控制 Token 和费用。
  • 防止一个请求占用过长推理时间。
  • 降低日志、网关和序列化压力。
  • 为限流和容量估算提供边界。

字符数不等于 Token 数。中文、英文、代码和不同模型的 Tokenizer 都会产生不同换算,生产还要在模型调用前做 Token 预算。

7.4 Service中的同步与流式调用

java
package com.example.ai.service;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
public class AiChatService {
    private final ChatClient chatClient;

    public AiChatService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    public String chat(String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }

    public Flux<String> stream(String message) {
        return chatClient.prompt()
                .user(message)
                .stream()
                .content();
    }
}

同步调用会等待完整响应,适合短内容、后台分类和结构化结果;流式调用在模型逐步生成时向下游发送片段,主要改善首 Token 时间,不会自动降低总生成时间和费用。

7.5 Controller

java
package com.example.ai.controller;

import com.example.ai.dto.ChatRequest;
import com.example.ai.dto.ChatResponseDto;
import com.example.ai.service.AiChatService;
import jakarta.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/ai")
public class AiChatController {
    private final AiChatService aiChatService;

    public AiChatController(AiChatService aiChatService) {
        this.aiChatService = aiChatService;
    }

    @PostMapping("/chat")
    public ChatResponseDto chat(@Valid @RequestBody ChatRequest request) {
        return new ChatResponseDto(aiChatService.chat(request.message()));
    }

    @PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> stream(@Valid @RequestBody ChatRequest request) {
        return aiChatService.stream(request.message());
    }
}

示例为了突出调用链而省略登录代码,商业系统必须在 Controller 前完成用户认证,并在 Service 中执行租户、数据权限、额度和审计策略。

7.6 统一处理输入错误

java
package com.example.ai.exception;

import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class AiExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> handleValidation(MethodArgumentNotValidException exception) {
        String message = exception.getBindingResult().getFieldErrors().stream()
                .findFirst()
                .map(error -> error.getDefaultMessage())
                .orElse("请求参数不合法");
        return Map.of("code", "INVALID_ARGUMENT", "message", message);
    }
}

模型服务异常不能原样返回给前端,否则可能暴露 Provider、URL、请求结构和内部配置。生产应把异常分类为认证、限流、超时、上游不可用、内容安全和内部错误,并为每次请求生成 requestId/traceId。

八、启动和验证

8.1 启动前检查

bash
mvn clean test
mvn dependency:tree -Dincludes=org.springframework.ai

确认密钥环境变量存在,但不要打印值:

PowerShell:

powershell
if ([string]::IsNullOrWhiteSpace($env:OPENAI_API_KEY)) { "missing" } else { "configured" }

Linux/macOS:

bash
test -n "$OPENAI_API_KEY" && echo configured || echo missing

启动:

bash
mvn spring-boot:run

启动成功只证明 Spring Context 已刷新和 Web Server 已监听,不一定证明模型账号、网络和模型名可用。很多 Provider 客户端在第一次业务调用时才真正发起远程请求。

8.2 验证同步接口

bash
curl -X POST "http://127.0.0.1:8080/api/ai/chat" \
  -H "Content-Type: application/json" \
  -d '{"message":"用零基础能理解的方式解释什么是RAG"}'

PowerShell 推荐:

powershell
$body = @{ message = "用零基础能理解的方式解释什么是RAG" } |
    ConvertTo-Json

Invoke-RestMethod `
    -Method Post `
    -Uri "http://127.0.0.1:8080/api/ai/chat" `
    -ContentType "application/json; charset=utf-8" `
    -Body $body

8.3 验证SSE流式接口

bash
curl -N -X POST "http://127.0.0.1:8080/api/ai/chat/stream" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"message":"分步骤解释Spring AI一次请求的流程"}'

-N 禁止 curl 缓冲输出。若后端已经逐块发送但 Nginx、网关或浏览器客户端继续缓冲,用户仍会在最后一次性看到结果,因此排查流式问题要逐跳检查。

九、一次同步请求内部发生了什么

mermaid
flowchart TD
    A["POST JSON进入Web Server"] --> B["Jackson反序列化ChatRequest"]
    B --> C["Bean Validation校验"]
    C --> D["Controller调用Service"]
    D --> E["ChatClient创建Request Spec"]
    E --> F["合并默认System与本次User消息"]
    F --> G["合并默认和请求级Options"]
    G --> H["Advisor调用前处理"]
    H --> I["ChatModel转换为厂商请求"]
    I --> J["HTTP连接、TLS、认证和发送"]
    J --> K["模型Tokenizer与推理"]
    K --> L["厂商返回响应JSON"]
    L --> M["适配为ChatResponse"]
    M --> N["Advisor调用后处理"]
    N --> O["content提取文本"]
    O --> P["Jackson序列化HTTP响应"]

每一段都可能失败:

阶段常见失败第一证据
JSON反序列化JSON格式错误、字段类型错误400响应、MVC异常日志
参数校验空问题、长度超限Validation错误字段
Bean与配置Bean不存在、属性没绑定启动日志、Condition Report
DNS/TCP/TLS域名解析失败、代理阻断、证书错误异常cause、DNS和TLS验证
HTTP认证API Key错误、组织无权限401/403、Provider requestId
模型路由Base URL或模型名错误404、Provider响应体
配额限流RPM/TPM/并发或余额不足429、Retry-After、配额面板
推理超时、内容安全拒绝、上游5xxHTTP状态、耗时、finish reason
输出处理空候选、截断、格式不合法ChatResponse元数据、usage

只调用 .content() 很方便,但排障和计费时还要保留完整 ChatResponse 中的 usage、finish reason、模型标识和 Provider 元数据。不要记录完整 Prompt 和回答后再声称“已经可观测”,因为它们可能含敏感数据。

十、流式调用为什么能边生成边返回

模型不是先在内存中生成完整文章再必然一次返回。很多服务可以在解码阶段逐 Token 产生增量事件:

mermaid
flowchart TD
    A["模型生成第一个Token"] --> B["Provider发送增量事件"]
    B --> C["HTTP客户端解析事件"]
    C --> D["ChatModel发出Flux元素"]
    D --> E["Controller写入SSE响应"]
    E --> F["网关立即转发"]
    F --> G["浏览器追加显示"]
    G --> H{"是否结束或取消"}
    H -->|"继续"| A
    H -->|"结束"| I["关闭流并记录usage和状态"]

流式调用要额外处理:

  • 客户端断开后的取消传播,否则后端和模型可能继续计费。
  • 网关缓冲和压缩造成的“假流式”。
  • 首 Token 超时与整个流的总超时。
  • 中途错误时不能再随意改 HTTP 状态码,需要协议内错误事件。
  • 并发连接、文件描述符和内存占用。
  • 最终 usage、finish reason 和审计信息的收集。

完整原理见 ChatClient同步、流式与Advisor

十一、从哪里看自动配置是否成功

11.1 启动日志

启动时首先确认:

text
应用使用了哪个Profile
Web Server监听哪个端口
ApplicationContext是否刷新成功
是否存在UnsatisfiedDependencyException
是否有ConfigurationProperties绑定错误

11.2 Actuator Beans与Conditions

只在受控环境临时暴露,不能向公网开放:

yaml
management:
  endpoints:
    web:
      exposure:
        include: health,info,beans,conditions

然后查询:

bash
curl http://127.0.0.1:8080/actuator/conditions
curl http://127.0.0.1:8080/actuator/beans

查找 ChatModel、OpenAI 自动配置和 ChatClient Builder。conditions 能说明某个自动配置为什么匹配或没匹配,beans 能说明最终创建了什么对象;二者分别回答“为什么”和“结果是什么”。

11.3 启动时做最小Bean断言

本地诊断可以临时加入:

java
@Bean
ApplicationRunner verifyAiBeans(ApplicationContext context) {
    return args -> {
        String[] names = context.getBeanNamesForType(
                org.springframework.ai.chat.model.ChatModel.class);
        System.out.println("ChatModel bean count=" + names.length);
    };
}

不要打印 Bean 的 toString(),其中可能间接包含客户端配置或敏感字段。诊断完成后移除这种标准输出,改用受控健康检查和指标。

十二、故障排查Runbook

12.1 找不到ChatClient.Builder Bean

现象通常是 NoSuchBeanDefinitionExceptionUnsatisfiedDependencyException

按顺序检查:

  1. mvn dependency:tree -Dincludes=org.springframework.ai 是否真的有 Provider Starter。
  2. 是否混入旧 Starter 或版本冲突。
  3. mvn -version 使用的 JDK 是否满足当前 Boot。
  4. Condition Evaluation Report 中相关自动配置为什么没有匹配。
  5. 是否手工排除了自动配置。
  6. 是否定义了一个不完整的自定义 ChatModel,使 @ConditionalOnMissingBean 退让。
  7. 配置属性是否因名称错误或类型错误绑定失败。

错误思路:反复换 API Key。没有 Builder 时请求甚至还没走到远程认证。

12.2 启动时报占位符无法解析

例如:

text
Could not resolve placeholder 'OPENAI_API_KEY'

说明 Spring Environment 中没有该值。检查启动进程真正继承的环境变量,而不是只看另一个终端窗口。IDE Run Configuration、Windows 服务、Docker 和 Kubernetes 都有各自的环境注入边界。

12.3 HTTP 401或403

401通常表示凭据缺失、格式错误或无效;403通常表示凭据已识别但没有模型、项目、区域或组织权限。检查:

text
密钥是否属于当前环境
是否多出引号或空格
Base URL是否属于同一个平台
账号是否被禁用或密钥是否过期
是否需要项目或组织头
目标模型是否已授权

记录 Provider 返回的 requestId 和错误码,不记录完整密钥。

12.4 HTTP 404或“模型不存在”

404不一定是模型不存在,也可能是 Base URL 路径拼错。OpenAI 兼容服务对 /v1 是否包含在 Base URL 中可能有不同要求。必须以平台协议为准,分别确认:

  • 最终请求 Host 和路径。
  • API 版本。
  • 模型名称是否是 API 标识而不是控制台展示名。
  • 当前区域是否部署该模型。
  • 兼容服务是否完整实现了对应接口。

12.5 HTTP 429

429可能来自:

  • 每分钟请求数 RPM。
  • 每分钟 Token 数 TPM。
  • 并发请求上限。
  • 账号余额或预算。
  • 自己的网关限流。

先读取错误体、Retry-After、配额面板和本地请求指标。不能对所有 429 立即无限重试,否则重试请求会继续占用配额并形成放大风暴。生产重试要有指数退避、随机抖动、最大次数和全局重试预算。

12.6 超时

先区分:

超时发生位置
DNS超时域名解析
Connect timeoutTCP连接或代理
TLS handshake timeout证书、代理、加密握手
Read/response timeout模型迟迟没有返回数据
First-token timeout流式请求首个增量太慢
Total timeout整个生成超过业务截止时间

同时记录输入 Token、输出上限、模型、Region、Provider requestId 和每阶段耗时。只把总超时从 30 秒改成 300 秒会长期占用线程、连接和额度,不能解释根因。

12.7 TLS证书错误

典型异常包括 SSLHandshakeException、PKIX path building failed。常见原因:

  • 企业代理做 TLS 解密,其根证书不在 JVM TrustStore。
  • 使用了私有模型服务的自签名证书。
  • 容器镜像 CA 证书过旧。
  • 系统时间错误导致证书未生效或过期。
  • SNI、域名和证书 SAN 不匹配。

正确做法是把受信任 CA 纳入镜像和证书管理流程。不要通过“信任所有证书”或关闭主机名校验解决生产问题。

12.8 代理环境下连接失败

先确认请求究竟应该直连还是走代理,再检查 JVM/HTTP 客户端是否读取系统代理、HTTP_PROXYHTTPS_PROXYNO_PROXYcurl 成功不能自动证明 Java 客户端也使用了同一代理和 TrustStore。

证据至少包括:

text
应用容器内DNS结果
目标Host和端口
代理地址与NO_PROXY规则
TCP和TLS握手结果
Java异常cause链
网关或代理访问日志

12.9 返回空文本或内容被截断

不要只打印 .content() 后猜测。查看完整响应中的:

  • Generation 数量。
  • AssistantMessage 内容。
  • finish reason。
  • 内容安全拒绝信息。
  • usage。
  • 模型和 Provider 元数据。

可能是最大输出 Token 太小、内容安全拒绝、Tool Call 尚未完成、上游返回空候选或流在中途断开。

12.10 SSE最后一次性返回

逐层检查:

text
模型Provider是否发送增量
ChatModel是否持续发出Flux元素
Controller是否声明text/event-stream
Servlet容器是否及时flush
Nginx/网关是否开启响应缓冲
压缩是否聚合小块
前端是否使用流式读取而不是等待完整JSON

在每层记录时间戳可以找到哪一跳开始聚合,不能只看前端现象。

十三、环境隔离和发布要求

环境模型与数据策略
本地低额度测试密钥、脱敏样例、严格预算
单元测试不访问真实模型,使用测试替身验证业务分支
集成测试使用专用账号验证协议、认证和超时
预发与生产相近版本,使用合成或脱敏数据做评估
生产独立密钥、最小权限、限流、审计、SLO和成本告警

不能把真实模型输出写成完全固定的单元测试断言。模型有随机性,Provider 也会升级。测试应分层:

  1. 单元测试验证参数校验、权限、Prompt 模板和错误映射。
  2. 契约测试验证请求/响应协议和结构化解析。
  3. 小规模在线评估验证真实模型效果。
  4. 回归评估集验证升级前后的质量、安全、延迟和成本。

十四、这个Demo证明了什么

跑通同步和流式接口可以证明:

  • JDK、Maven、Spring Boot 和 Spring AI 依赖至少能在当前环境启动。
  • 自动配置创建了可用的 ChatModel 和 ChatClient Builder。
  • API Key、Base URL、模型名、网络和基础协议能够完成一次调用。
  • JSON 和 SSE 基础链路可以工作。

它不能证明:

  • 模型回答始终正确。
  • 系统不会越权或泄密。
  • 高并发下不会触发 429、线程或连接耗尽。
  • RAG 召回准确。
  • Tool Calling 安全、幂等且可审计。
  • 成本满足预算。
  • 故障时能够降级和恢复。
  • 更换模型后质量完全一致。

从 Demo 到商业生产至少还要补:认证、租户隔离、权限、Prompt 版本、结构化校验、超时、限流、重试预算、熔断、模型路由、内容安全、观测、评估、成本和审计。

十五、商业场景中的第一版边界

以“医疗数据采集异常解释助手”为例,第一版不能让用户直接把任意数据库内容发给模型。合理流程是:

mermaid
flowchart TD
    A["运维人员提交任务ID和问题"] --> B["后端认证与数据权限"]
    B --> C["按任务ID读取允许展示的脱敏事实"]
    C --> D["构造版本化Prompt"]
    D --> E["Token与费用预算"]
    E --> F["调用模型"]
    F --> G["校验回答与引用"]
    G --> H["返回解释和建议"]
    H --> I["记录requestId、版本、耗时和费用"]

模型只负责基于已授权事实生成解释,不能直接决定重跑任务、删除数据或修改资产状态。高风险动作必须进入 Tool Calling 的后端鉴权、参数校验、幂等和审计链路。

十六、常见误区

误区1:能启动就说明模型可用

很多远程连接在第一次调用时才建立。必须执行受控的真实调用或专用健康探测。

误区2:OpenAI兼容就能无缝替换

兼容通常只覆盖部分字段。Tool Calling、JSON Schema、图片、usage、错误码和流式事件可能不同。

误区3:temperature为0就绝对确定

低温度通常更稳定,但底层模型、服务实现、并行计算和版本升级仍可能带来差异。关键业务不能把模型自由文本当确定性规则引擎。

误区4:System Prompt可以防越权

Prompt 是软约束。后端权限过滤和工具授权才是强边界。

误区5:流式一定更快、更便宜

流式主要降低用户等待首段内容的时间,总 Token 和总推理时间不一定下降,还增加长连接治理成本。

误区6:捕获Exception后统一重试

参数错误、401、403和确定性内容拒绝通常不应重试。只有可恢复错误才进入受预算约束的重试。

十七、面试标准回答

Spring AI项目怎么从零接入模型

先确定 JDK、Spring Boot 和 Spring AI 的兼容版本,用 BOM 统一模块版本并引入一个 Provider Starter;通过环境变量或 Secret 配置 API Key、Base URL 和模型名。Spring Boot 根据 Classpath、配置属性和条件装配创建厂商客户端、ChatModel 和 ChatClient Builder。业务配置用 Builder 构建 ChatClient,Service 组织 Prompt 并选择 call 或 stream,Controller 只做鉴权、校验和响应转换。上线前还要补超时、限流、重试预算、权限、审计、评估、成本和故障降级。

ChatClient.Builder为什么可以直接注入

Provider Starter 把自动配置类带入 Classpath。Spring Boot 启动时读取 spring.ai.* 属性并判断条件,先创建厂商 API 客户端和 ChatModel,再基于 ChatModel 提供 ChatClient Builder。若注入失败,应先检查依赖树、版本和 Condition Evaluation Report,而不是先怀疑业务 Prompt。

call和stream有什么区别

call 等完整模型响应后一次返回,适合短回答、结构化结果和后台任务;stream 把增量内容作为 Flux/SSE 持续返回,主要改善首 Token 时间。流式还必须处理网关缓冲、断连取消、首 Token 与总超时、并发连接和中途错误。

为什么JDK 8项目不能直接加入当前Spring AI

当前 Spring AI 所依赖的 Spring Boot、Jakarta API 和依赖字节码基线高于 JDK 8,改编译参数无法让高版本依赖在 JDK 8 上运行。通常应升级主项目,或把 AI 能力拆成 JDK 17/21 的独立服务,让 JDK 8 主系统通过受控接口调用。

十八、学习验收

不要看答案,完成以下实验:

  1. java -versionmvn -version 证明构建使用同一个 JDK。
  2. 用依赖树证明项目只存在一套 Spring AI 版本。
  3. 在不打印密钥值的情况下证明环境变量已注入。
  4. 画出从 HTTP 请求到 ChatModel 再到 Provider 的调用链。
  5. 分别跑通同步与 SSE 接口,并记录首 Token 和总耗时。
  6. 故意移除 Starter,根据 Condition Report 解释 Builder 为什么消失。
  7. 故意配置错误模型名,区分 404 路径错误和模型不存在。
  8. 用无效密钥制造 401,并保存脱敏后的 Provider requestId。
  9. 解释为什么 429 不能无脑立即重试。
  10. 解释这个 Demo 为什么还不能直接用于医疗、支付或生产运维决策。

关联知识点