Spring AI从环境搭建到首个生产化接口
本页不是“复制四行代码看见模型回答就结束”。它会带你从一个空 Spring Boot 项目开始,完成版本选择、依赖引入、密钥配置、自动配置验证、同步调用、SSE 流式调用、参数校验、异常转换和故障取证,并解释每一步为什么存在。
本页示例采用当前专栏统一基线:spring-ai-bom:2.0.0、2.x 风格的 spring-ai-starter-model-openai 和 JDK 21。这个组合只是本文可复现的教学基线,不代表任意 Spring Boot 版本都能与它混搭。已有项目必须先核对 Spring AI 发行说明和依赖兼容关系。
学习目标
完成本页后,你应该能够:
- 判断项目应使用 JDK 8、17 还是 21,以及为什么 JDK 8 不能直接运行当前 Spring AI。
- 解释 BOM、Starter、传递依赖和自动配置的关系。
- 解释
ChatClient.Builder、ChatClient、ChatModel和厂商 HTTP API 的职责边界。 - 写出可运行的同步接口和 SSE 流式接口。
- 说明一次请求从 Controller 到模型服务再返回浏览器的完整过程。
- 使用环境变量保存密钥,并区分本地、测试、预发和生产配置。
- 从依赖树、条件评估报告、配置绑定、HTTP 状态码和日志中定位启动或调用失败。
- 区分“Spring Boot 启动成功”“模型调用成功”和“达到生产可用”这三件不同的事。
一、先建立正确的系统边界
Spring AI 不在本机训练或运行大模型。下面这个 Demo 中,Spring Boot 应用是模型服务的客户端:
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 AI | Spring 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 主系统的合理边界:
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 先检查本机环境
java -version
mvn -version两条命令中的 Java 路径和版本都要检查。常见错误是 IDE 使用 JDK 21,而终端中的 JAVA_HOME 仍指向 JDK 8,导致 IDE 能运行、命令行构建失败。
Windows PowerShell:
$env:JAVA_HOME
Get-Command java
java -version
mvn -version三、创建最小项目
示例目录:
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 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 名称。
验证依赖:
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:
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: graceful5.2 本地配置
application-local.yml:
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配置层次:
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:
$env:OPENAI_API_KEY="替换成真实密钥"
$env:OPENAI_CHAT_MODEL="替换成账号实际可用的模型名"
$env:SPRING_PROFILES_ACTIVE="local"Linux/macOS:
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 可以只保留环境相关策略:
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 启动时:
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 时连相应自动配置类都不存在。
查看条件评估:
mvn spring-boot:run -Dspring-boot.run.arguments=--debug或在本地临时开启:
debug: true搜索 CONDITIONS EVALUATION REPORT,分别看 Positive matches 和 Negative matches。不要在生产长期打开全局 DEBUG,它可能产生大量日志并扩大敏感数据暴露面。
七、编写可运行代码
7.1 启动类
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
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 请求和响应对象
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
) {
}package com.example.ai.dto;
public record ChatResponseDto(String answer) {
}长度限制不仅防止数据库字段越界,还用于:
- 控制 Token 和费用。
- 防止一个请求占用过长推理时间。
- 降低日志、网关和序列化压力。
- 为限流和容量估算提供边界。
字符数不等于 Token 数。中文、英文、代码和不同模型的 Tokenizer 都会产生不同换算,生产还要在模型调用前做 Token 预算。
7.4 Service中的同步与流式调用
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
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 统一处理输入错误
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 启动前检查
mvn clean test
mvn dependency:tree -Dincludes=org.springframework.ai确认密钥环境变量存在,但不要打印值:
PowerShell:
if ([string]::IsNullOrWhiteSpace($env:OPENAI_API_KEY)) { "missing" } else { "configured" }Linux/macOS:
test -n "$OPENAI_API_KEY" && echo configured || echo missing启动:
mvn spring-boot:run启动成功只证明 Spring Context 已刷新和 Web Server 已监听,不一定证明模型账号、网络和模型名可用。很多 Provider 客户端在第一次业务调用时才真正发起远程请求。
8.2 验证同步接口
curl -X POST "http://127.0.0.1:8080/api/ai/chat" \
-H "Content-Type: application/json" \
-d '{"message":"用零基础能理解的方式解释什么是RAG"}'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 $body8.3 验证SSE流式接口
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、网关或浏览器客户端继续缓冲,用户仍会在最后一次性看到结果,因此排查流式问题要逐跳检查。
九、一次同步请求内部发生了什么
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、配额面板 |
| 推理 | 超时、内容安全拒绝、上游5xx | HTTP状态、耗时、finish reason |
| 输出处理 | 空候选、截断、格式不合法 | ChatResponse元数据、usage |
只调用 .content() 很方便,但排障和计费时还要保留完整 ChatResponse 中的 usage、finish reason、模型标识和 Provider 元数据。不要记录完整 Prompt 和回答后再声称“已经可观测”,因为它们可能含敏感数据。
十、流式调用为什么能边生成边返回
模型不是先在内存中生成完整文章再必然一次返回。很多服务可以在解码阶段逐 Token 产生增量事件:
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 启动日志
启动时首先确认:
应用使用了哪个Profile
Web Server监听哪个端口
ApplicationContext是否刷新成功
是否存在UnsatisfiedDependencyException
是否有ConfigurationProperties绑定错误11.2 Actuator Beans与Conditions
只在受控环境临时暴露,不能向公网开放:
management:
endpoints:
web:
exposure:
include: health,info,beans,conditions然后查询:
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断言
本地诊断可以临时加入:
@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
现象通常是 NoSuchBeanDefinitionException 或 UnsatisfiedDependencyException。
按顺序检查:
mvn dependency:tree -Dincludes=org.springframework.ai是否真的有 Provider Starter。- 是否混入旧 Starter 或版本冲突。
mvn -version使用的 JDK 是否满足当前 Boot。- Condition Evaluation Report 中相关自动配置为什么没有匹配。
- 是否手工排除了自动配置。
- 是否定义了一个不完整的自定义
ChatModel,使@ConditionalOnMissingBean退让。 - 配置属性是否因名称错误或类型错误绑定失败。
错误思路:反复换 API Key。没有 Builder 时请求甚至还没走到远程认证。
12.2 启动时报占位符无法解析
例如:
Could not resolve placeholder 'OPENAI_API_KEY'说明 Spring Environment 中没有该值。检查启动进程真正继承的环境变量,而不是只看另一个终端窗口。IDE Run Configuration、Windows 服务、Docker 和 Kubernetes 都有各自的环境注入边界。
12.3 HTTP 401或403
401通常表示凭据缺失、格式错误或无效;403通常表示凭据已识别但没有模型、项目、区域或组织权限。检查:
密钥是否属于当前环境
是否多出引号或空格
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 timeout | TCP连接或代理 |
| 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_PROXY、HTTPS_PROXY 和 NO_PROXY。curl 成功不能自动证明 Java 客户端也使用了同一代理和 TrustStore。
证据至少包括:
应用容器内DNS结果
目标Host和端口
代理地址与NO_PROXY规则
TCP和TLS握手结果
Java异常cause链
网关或代理访问日志12.9 返回空文本或内容被截断
不要只打印 .content() 后猜测。查看完整响应中的:
- Generation 数量。
- AssistantMessage 内容。
- finish reason。
- 内容安全拒绝信息。
- usage。
- 模型和 Provider 元数据。
可能是最大输出 Token 太小、内容安全拒绝、Tool Call 尚未完成、上游返回空候选或流在中途断开。
12.10 SSE最后一次性返回
逐层检查:
模型Provider是否发送增量
ChatModel是否持续发出Flux元素
Controller是否声明text/event-stream
Servlet容器是否及时flush
Nginx/网关是否开启响应缓冲
压缩是否聚合小块
前端是否使用流式读取而不是等待完整JSON在每层记录时间戳可以找到哪一跳开始聚合,不能只看前端现象。
十三、环境隔离和发布要求
| 环境 | 模型与数据策略 |
|---|---|
| 本地 | 低额度测试密钥、脱敏样例、严格预算 |
| 单元测试 | 不访问真实模型,使用测试替身验证业务分支 |
| 集成测试 | 使用专用账号验证协议、认证和超时 |
| 预发 | 与生产相近版本,使用合成或脱敏数据做评估 |
| 生产 | 独立密钥、最小权限、限流、审计、SLO和成本告警 |
不能把真实模型输出写成完全固定的单元测试断言。模型有随机性,Provider 也会升级。测试应分层:
- 单元测试验证参数校验、权限、Prompt 模板和错误映射。
- 契约测试验证请求/响应协议和结构化解析。
- 小规模在线评估验证真实模型效果。
- 回归评估集验证升级前后的质量、安全、延迟和成本。
十四、这个Demo证明了什么
跑通同步和流式接口可以证明:
- JDK、Maven、Spring Boot 和 Spring AI 依赖至少能在当前环境启动。
- 自动配置创建了可用的 ChatModel 和 ChatClient Builder。
- API Key、Base URL、模型名、网络和基础协议能够完成一次调用。
- JSON 和 SSE 基础链路可以工作。
它不能证明:
- 模型回答始终正确。
- 系统不会越权或泄密。
- 高并发下不会触发 429、线程或连接耗尽。
- RAG 召回准确。
- Tool Calling 安全、幂等且可审计。
- 成本满足预算。
- 故障时能够降级和恢复。
- 更换模型后质量完全一致。
从 Demo 到商业生产至少还要补:认证、租户隔离、权限、Prompt 版本、结构化校验、超时、限流、重试预算、熔断、模型路由、内容安全、观测、评估、成本和审计。
十五、商业场景中的第一版边界
以“医疗数据采集异常解释助手”为例,第一版不能让用户直接把任意数据库内容发给模型。合理流程是:
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 主系统通过受控接口调用。
十八、学习验收
不要看答案,完成以下实验:
- 用
java -version和mvn -version证明构建使用同一个 JDK。 - 用依赖树证明项目只存在一套 Spring AI 版本。
- 在不打印密钥值的情况下证明环境变量已注入。
- 画出从 HTTP 请求到 ChatModel 再到 Provider 的调用链。
- 分别跑通同步与 SSE 接口,并记录首 Token 和总耗时。
- 故意移除 Starter,根据 Condition Report 解释 Builder 为什么消失。
- 故意配置错误模型名,区分 404 路径错误和模型不存在。
- 用无效密钥制造 401,并保存脱敏后的 Provider requestId。
- 解释为什么 429 不能无脑立即重试。
- 解释这个 Demo 为什么还不能直接用于医疗、支付或生产运维决策。
