Spring AI简介
Spring AI 是 Spring 官方提供的 AI 应用开发框架,目标是用熟悉的 Spring 编程方式接入大语言模型、Embedding 模型和向量数据库。
日常开发中常用的能力主要有:
| 能力 | 作用 |
|---|---|
| Chat Model | 统一不同模型厂商的对话调用方式 |
| ChatClient | 使用链式 API 构造提示词并调用模型 |
| Chat Memory | 保存多轮对话需要的上下文 |
| Advisor | 在模型调用前后统一处理日志、记忆和 RAG |
| Tool Calling | 让模型按需调用 Java 方法 |
| Embedding Model | 将文本转换为向量 |
| Vector Store | 保存向量并进行相似度检索 |
| RAG | 检索私有知识并作为上下文交给模型回答 |
本文代码以 Spring AI 1.1.0 为例。Spring AI 不同版本之间的依赖名称和部分 API 可能发生变化,升级版本时需要同时查看对应版本的参考文档和 Javadoc。
基本使用
引入Spring AI BOM
使用 BOM 统一管理 Spring AI 组件版本,后续引入 Spring AI 依赖时不需要再单独填写版本号。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
引入模型依赖
下面使用 OpenAI Starter。它同时提供 Chat Model、Embedding Model、ChatClient 自动配置和 Tool Calling 支持。
<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>
如果使用 Anthropic、Ollama、Azure OpenAI 等模型,需要替换成对应的 Starter。业务代码尽量依赖 ChatModel、EmbeddingModel 和 ChatClient 等通用接口,切换模型时改动会更小。
配置模型
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
# 使用 OpenAI 兼容接口时再配置
# base-url: ${OPENAI_BASE_URL}
chat:
options:
model: ${CHAT_MODEL}
temperature: 0.7
embedding:
options:
model: ${EMBEDDING_MODEL}
API Key 应通过环境变量、配置中心或密钥管理服务提供,不要直接提交到代码仓库。
chat.options.model 和 embedding.options.model 是两种不同类型的模型,不能混用:
- Chat Model 负责生成回答。
- Embedding Model 负责把文本转换成向量。
创建ChatClient
Spring Boot 会自动创建一个 ChatClient.Builder,可以直接注入并构建 ChatClient。
@Configuration
public class AiConfiguration {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultSystem("你是一个严谨、简洁的技术助手")
.build();
}
}
基本调用:
@Service
public class AiChatService {
private final ChatClient chatClient;
public AiChatService(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String chat(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
call() 表示同步调用,content() 只返回最终文本。如果还需要 Token 使用量、Generation 或模型元数据,应获取完整的 ChatResponse。
ChatClient更多使用
设置System和User消息
System 消息用于定义角色、边界和输出要求,User 消息用于传递本次请求。
String answer = chatClient.prompt()
.system("""
你是一个 Java 代码审查助手。
只指出能够确认的问题,并给出修改建议。
""")
.user("""
请检查下面的方法:
public int divide(int a, int b) {
return a / b;
}
""")
.call()
.content();
需要动态替换提示词参数时,可以使用模板变量:
String answer = chatClient.prompt()
.system(system -> system
.text("你是一个技术助手,回答必须使用 {language}")
.param("language", "中文"))
.user("解释什么是向量检索")
.call()
.content();
不要直接用字符串拼接来自用户的内容构造 System Prompt。固定规则和用户输入应放在不同消息中,降低提示词注入带来的影响。
获取完整响应
ChatResponse response = chatClient.prompt()
.user("解释 Spring AI Advisor")
.call()
.chatResponse();
只需要回答正文时使用 content();需要 Token 使用情况或其他模型信息时使用 chatResponse()。
返回Java对象
可以让 ChatClient 将模型输出转换为 Java 类型:
public record BookRecommendation(
String title,
String author,
String reason
) {
}
BookRecommendation recommendation = chatClient.prompt()
.user("推荐一本适合 Java 开发者学习分布式系统的书")
.call()
.entity(BookRecommendation.class);
结构化输出仍由模型生成,反序列化成功不代表内容一定正确。重要业务字段仍需要校验。
流式输出
流式调用返回 Flux,需要 Reactive Web 支持:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
public Flux<String> stream(String question) {
return chatClient.prompt()
.user(question)
.stream()
.content();
}
流式响应适合聊天页面逐字展示。Tool Calling 本身可能包含阻塞操作,使用流式接口不代表整条调用链都是非阻塞的。
Chat Memory
大语言模型本身是无状态的。需要多轮对话时,应用必须在每次请求中把相关历史消息重新发送给模型。
Chat Memory 用于维护模型当前需要的上下文,不等同于完整聊天记录。需要审计或永久保存全部消息时,应单独设计聊天历史表。
内存会话
MessageWindowChatMemory 只保留指定数量的最近消息:
@Configuration
public class ChatMemoryConfiguration {
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
}
@Bean
public ChatClient memoryChatClient(
ChatClient.Builder builder,
ChatMemory chatMemory) {
return builder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
}
内存存储会在应用重启后丢失,适合本地开发、测试和不需要持久化的临时会话。
指定会话ID
每次使用 Memory Advisor 时都应传入明确的 conversation ID:
public String chat(String conversationId, String question) {
return chatClient.prompt()
.user(question)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
}
会话 ID 的设计原则:
- 同一段多轮对话使用相同 ID。
- 不同用户、租户或业务会话使用不同 ID。
- 不要把可猜测的会话 ID 直接当成权限凭证。
- 服务端应校验当前用户是否有权访问该会话。
清理指定会话:
chatMemory.clear(conversationId);
JDBC持久化
需要在应用重启后保留会话上下文时,可以使用 JDBC Chat Memory。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- 根据实际数据库替换 JDBC Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always
platform: postgresql
使用自动配置的 JdbcChatMemoryRepository:
@Bean
public ChatMemory chatMemory(
JdbcChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(20)
.build();
}
生产环境通常应使用 Flyway 或 Liquibase 管理表结构,并将 initialize-schema 设置为 never,避免应用启动时自动修改数据库。
Advisor
Advisor 类似 ChatClient 的拦截器,可以在请求发送给模型之前补充内容,也可以在模型响应后执行统一处理。
常见用途:
- 添加多轮对话记忆。
- 记录请求和响应。
- 执行 RAG 检索。
- 添加公共上下文。
- 统一修改 Prompt。
注册默认Advisor
@Bean
public ChatClient chatClient(
ChatClient.Builder builder,
ChatMemory chatMemory) {
return builder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
new SimpleLoggerAdvisor()
)
.build();
}
默认 Advisor 会参与该 ChatClient 的每一次请求。
只在当前请求使用
String answer = chatClient.prompt()
.user(question)
.advisors(new SimpleLoggerAdvisor())
.call()
.content();
Advisor 会根据 getOrder() 排序:
- order 数值越小,请求阶段越先执行。
- 响应会沿 Advisor 链反向返回。
- 多个 Advisor 存在依赖关系时,应明确设置顺序。
开启 SimpleLoggerAdvisor 日志:
logging:
level:
org.springframework.ai.chat.client.advisor: DEBUG
日志可能包含用户问题、模型响应和 RAG 上下文。生产环境需要脱敏并限制日志访问权限。
Tool Calling
Tool Calling 允许模型根据工具描述决定是否调用 Java 方法。模型负责生成工具名称和参数,真正的方法执行发生在应用本地。
定义Tool
@Component
public class TimeTools {
@Tool(description = "获取用户所在时区的当前日期和时间")
public String getCurrentDateTime() {
ZoneId zoneId = LocaleContextHolder
.getTimeZone()
.toZoneId();
return ZonedDateTime.now(zoneId).toString();
}
}
带参数的工具:
@Tool(description = "查询指定城市的天气")
public String getWeather(
@ToolParam(description = "城市名称,例如:杭州")
String city) {
return weatherClient.query(city);
}
工具描述应说明:
- 什么情况下调用。
- 每个参数的含义和格式。
- 返回结果代表什么。
描述不清楚时,模型可能不调用工具,也可能生成错误参数。
注册Tool
为 ChatClient 的所有请求注册:
@Bean
public ChatClient chatClient(
ChatClient.Builder builder,
TimeTools timeTools) {
return builder
.defaultTools(timeTools)
.build();
}
只为当前请求注册:
String answer = chatClient.prompt()
.user("现在几点?")
.tools(timeTools)
.call()
.content();
Tool异常处理
spring:
ai:
tools:
throw-exception-on-error: false
| 配置 | 行为 |
|---|---|
false |
将可处理的工具错误交回模型,由模型继续生成响应 |
true |
工具异常直接向调用方抛出 |
Tool 本质上是本地代码,必须像普通业务接口一样处理参数校验、权限检查、超时、幂等和审计。删除数据、支付、发消息等有副作用的操作,不应仅依赖模型判断是否执行。
Embedding
Embedding Model 会把文本转换为数值向量。语义越接近的文本,其向量通常也越接近。
常见用途:
- 语义搜索。
- 文本聚类。
- 相似内容推荐。
- RAG 知识检索。
文本转向量
spring-ai-starter-model-openai 已经提供 EmbeddingModel:
@Service
public class TextVectorizer {
private final EmbeddingModel embeddingModel;
public TextVectorizer(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
public float[] embed(String text) {
return embeddingModel.embed(text);
}
}
向量维度由 Embedding Model 决定。更换模型后维度可能变化,已有向量索引通常需要重新创建并重新写入数据。
创建Document
Spring AI 使用 Document 表示需要向量化和检索的内容:
Document document = new Document(
"Spring AI 提供了统一的模型和向量数据库抽象。",
Map.of(
"source_id", "article-001",
"category", "spring",
"title", "Spring AI"
)
);
metadata 用于保存来源、分类和业务 ID。后续需要按 metadata 过滤或删除时,应在写入前设计好字段。
文档切块
长文档不应整体生成一个向量。通常需要先拆成较小的文本块:
TokenTextSplitter splitter =
new TokenTextSplitter(2000, 100, 5, 10000, true);
List<Document> chunks = splitter.apply(documents);
切块参数需要根据内容和模型上下文调整:
- 块太大:检索命中后携带的无关内容较多。
- 块太小:语义不完整,回答缺少上下文。
- 不同业务文档应分别评估切块大小,不要只使用固定经验值。
按Token分批
切块决定检索粒度,分批只决定一次 Embedding 请求发送多少内容。
@Bean
public BatchingStrategy batchingStrategy() {
TokenCountEstimator estimator =
new JTokkitTokenCountEstimator(
EncodingType.CL100K_BASE
);
return new TokenCountBatchingStrategy(
estimator,
8000,
0.1,
Document.DEFAULT_CONTENT_FORMATTER,
MetadataMode.NONE
);
}
List<List<Document>> batches =
batchingStrategy.batch(chunks);
上面的 0.1 表示预留 10% Token 空间,降低请求接近模型上限时被拒绝的概率。MetadataMode.NONE 表示 metadata 不参与嵌入文本,但仍可以保存在向量文档中用于过滤。
Vector Store
Vector Store 用于保存文本向量并执行相似度检索。Spring AI 提供统一的 VectorStore 接口,底层可以使用 Redis、PGVector、Milvus、Qdrant、Pinecone 等实现。
基本流程:
- 将原始内容封装成
Document。 - 对长文档进行切块。
- 使用 Embedding Model 生成向量。
- 写入 Vector Store。
- 将用户问题转换为向量并检索相似文档。
Redis Vector Store
Redis 方案需要 Redis Stack,普通 Redis 只支持基础数据结构,不能直接使用向量索引和相似度搜索。
引入依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
基本配置:
spring:
data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
ai:
vectorstore:
redis:
initialize-schema: true
index-name: spring-ai-index
prefix: "spring-ai:"
需要声明可过滤的 metadata 字段时,可以手动创建 RedisVectorStore:
@Configuration
public class RedisVectorConfiguration {
@Bean
public JedisPooled jedisPooled(
@Value("${spring.data.redis.host}") String host,
@Value("${spring.data.redis.port}") int port) {
return new JedisPooled(host, port);
}
@Bean
public RedisVectorStore redisVectorStore(
JedisPooled jedisPooled,
EmbeddingModel embeddingModel,
@Value("${spring.ai.vectorstore.redis.index-name}")
String indexName,
@Value("${spring.ai.vectorstore.redis.prefix}")
String prefix) {
return RedisVectorStore
.builder(jedisPooled, embeddingModel)
.indexName(indexName)
.prefix(prefix)
.initializeSchema(true)
.metadataFields(
RedisVectorStore.MetadataField.tag("source_id"),
RedisVectorStore.MetadataField.tag("category")
)
.build();
}
}
如果 Redis 开启了用户名、密码或 TLS,应使用相应的 JedisClientConfig 创建连接,不要把密码写在 Java 代码中。
写入文档
Document document = new Document(
"Spring AI 可以通过 Advisor 组合聊天记忆和 RAG。",
Map.of(
"source_id", "article-001",
"category", "spring"
)
);
vectorStore.add(List.of(document));
写入时会调用 Embedding Model,因此需要同时保证模型服务和 Vector Store 可用。
写入长文档:
List<Document> chunks = splitter.apply(documents);
for (List<Document> batch : batchingStrategy.batch(chunks)) {
vectorStore.add(batch);
}
相似度搜索
SearchRequest request = SearchRequest.builder()
.query("如何给对话增加历史记忆?")
.topK(5)
.similarityThreshold(0.7)
.filterExpression("category == 'spring'")
.build();
List<Document> results =
vectorStore.similaritySearch(request);
常用参数:
| 参数 | 作用 |
|---|---|
query |
需要检索的自然语言问题 |
topK |
最多返回多少条文档 |
similarityThreshold |
最低相似度阈值 |
filterExpression |
按 metadata 过滤 |
检索为空时,先去掉 metadata 过滤条件,再降低相似度阈值排查。不要一开始就把阈值设置得过高。
删除文档
已知文档 ID 时可以按 ID 删除:
vectorStore.delete(List.of(documentId));
需要删除一篇原始文档产生的全部切块时,使用稳定的业务 ID:
vectorStore.delete("source_id == 'article-001'");
不要使用空字符串相似度搜索后再根据 topK 删除,检索结果可能被截断,导致部分切块残留。
更新文档
大多数 Vector Store 没有通用的原地更新语义。更新原始文档通常采用“删除旧切块,再写入新切块”:
String sourceId = "article-001";
vectorStore.delete(
"source_id == '" + sourceId + "'"
);
Document replacement = new Document(
newContent,
Map.of(
"source_id", sourceId,
"category", "spring"
)
);
List<Document> newChunks =
splitter.apply(List.of(replacement));
vectorStore.add(newChunks);
生产代码需要考虑“删除成功但新增失败”的情况,可以使用重试、补偿任务、版本字段或新旧索引切换降低数据不一致风险。
metadata字段变更
initializeSchema(true) 通常只负责创建不存在的索引,不会自动修改已经存在的索引结构。
新增可过滤 metadata 字段后,需要:
- 修改 Vector Store 中的 metadata 字段声明。
- 创建新索引或重建旧索引。
- 重新写入文档向量。
生产环境不建议直接删除正在使用的索引,可以创建带版本号的新索引,完成数据迁移和验证后再切换。
RAG
RAG(Retrieval Augmented Generation,检索增强生成)的核心流程是:
- 接收用户问题。
- 从 Vector Store 检索相关文档。
- 将文档作为上下文加入 Prompt。
- 调用 Chat Model 生成回答。
RAG 不会自动保证答案正确。检索质量、切块策略、Prompt 约束和模型能力都会影响最终结果。
QuestionAnswerAdvisor
只需要常规的“检索 → 注入上下文 → 回答”流程时,可以使用 QuestionAnswerAdvisor。
引入依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
QuestionAnswerAdvisor qaAdvisor =
QuestionAnswerAdvisor.builder(vectorStore)
.build();
String answer = chatClient.prompt()
.user("Spring AI 如何保存多轮对话?")
.advisors(qaAdvisor)
.call()
.content();
自定义 RAG Prompt:
PromptTemplate promptTemplate = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder()
.startDelimiterToken('<')
.endDelimiterToken('>')
.build())
.template("""
<query>
请只根据下面的资料回答问题。
如果资料中没有答案,请直接回答“不知道”。
---------------------
<question_answer_context>
---------------------
""")
.build();
QuestionAnswerAdvisor qaAdvisor =
QuestionAnswerAdvisor.builder(vectorStore)
.promptTemplate(promptTemplate)
.build();
Spring AI 1.1.0 的自定义模板需要保留 <query> 和 <question_answer_context> 占位符,否则 Advisor 无法正确注入问题和检索结果。
RetrievalAugmentationAdvisor
需要自定义查询改写、文档检索和上下文增强流程时,使用模块化 RAG。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>
RewriteQueryTransformer queryTransformer =
RewriteQueryTransformer.builder()
.chatClientBuilder(
ChatClient.builder(chatModel)
)
.build();
DocumentRetriever documentRetriever =
VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.5)
.topK(4)
.build();
Advisor ragAdvisor =
RetrievalAugmentationAdvisor.builder()
.queryTransformers(queryTransformer)
.documentRetriever(documentRetriever)
.queryAugmenter(
ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.build()
)
.build();
String answer = chatClient.prompt()
.user(question)
.advisors(ragAdvisor)
.call()
.content();
查询改写最好使用独立的裸 ChatClient:
ChatClient.builder(chatModel)
如果从业务 ChatClient 调用 mutate(),查询改写可能继承默认 System Prompt、Chat Memory、Tools 或其他 Advisor,从而干扰改写结果。
空上下文策略:
| 设置 | 行为 |
|---|---|
allowEmptyContext(false) |
没有检索到资料时,不允许模型脱离知识库自由回答 |
allowEmptyContext(true) |
没有检索结果时仍继续调用模型 |
企业知识、法规、财务等强约束场景通常应设置为 false,并明确设计“未找到答案”的响应。
常见问题
| 现象 | 常见原因 | 处理方法 |
|---|---|---|
| ChatClient 无法注入 | 没有引入模型 Starter,或存在多个 Chat Model | 检查依赖;为主要模型设置 @Primary 或手动创建 Builder |
| 模型调用返回 401 | API Key 无效或没有传入环境变量 | 检查密钥、环境变量和模型平台权限 |
| 模型名称不存在 | Chat Model 与 Embedding Model 配置混用 | 分别检查 chat 和 embedding 下的 model |
| 流式调用无法工作 | 没有引入 WebFlux,或使用的模型不支持流式输出 | 引入 WebFlux 并检查模型能力 |
| 多轮对话没有上下文 | 没有注册 Memory Advisor,或 conversation ID 不一致 | 检查默认 Advisor 和每次请求的会话 ID |
| 不同用户对话串线 | 多个用户共用 conversation ID | 按用户和业务会话生成隔离的 ID,并进行权限校验 |
| Tool 没有执行 | 模型不支持 Tool Calling、工具未注册或描述不清 | 检查模型能力、.tools()/.defaultTools() 和工具描述 |
| 普通 Redis 可用但向量检索失败 | Redis 不包含 Search 和 Vector 能力 | 使用 Redis Stack 或支持向量检索的 Redis 服务 |
| Redis 报索引不存在 | 索引未初始化或索引名不一致 | 检查 initialize-schema 和 index-name |
| metadata 过滤无效 | 字段没有加入索引,或旧索引结构未更新 | 声明 metadata 字段并重建索引 |
| 向量检索一直为空 | 未写入数据、阈值过高或过滤条件不匹配 | 确认数据,去掉过滤条件并逐步降低阈值 |
| RAG 不使用知识库 | 没有注册 Advisor、检索为空或模板占位符错误 | 检查检索结果、Advisor 和 PromptTemplate |
| RAG 产生无依据回答 | 允许空上下文,或 Prompt 没有限制 | 禁止空上下文并明确无答案策略 |
| 日志包含敏感内容 | 开启了 Advisor DEBUG 日志 | 关闭生产 DEBUG 日志并进行脱敏 |
开发注意事项
- API Key、数据库密码和 Redis 密码不要写入代码或提交到仓库。
- Prompt、Tool 参数和模型结构化输出都需要做业务校验。
- Tool 具有本地代码权限,涉及副作用时必须增加鉴权、幂等和审计。
- Chat Memory 只保存模型所需上下文,完整聊天记录应独立存储。
- Embedding Model 变更后,应检查向量维度并重新生成已有向量。
- 文档写入 Vector Store 前,应先确定业务 ID、metadata 和切块策略。
- metadata 索引结构发生变化时,应执行索引迁移。
- RAG 上线前,应准备固定问题集评估召回率和回答准确率。
- 模型、数据库和向量库调用都可能失败,应设置超时、重试和降级策略。
- 对用户输入、RAG 上下文和模型输出进行日志脱敏。