跳到主要内容

数据截至 (上游 commit c988e72ab728)

Spring AI — 架构与原理

30 秒导读: Spring AI 让"调大模型"变成 Spring 应用里最普通的一件事——注入一个 ChatClient,像写 RestClient 那样把话说完、拿到结果。要加记忆、加检索、加工具、加日志,就往 Advisor 责任链上插一环;要换供应商(OpenAI → Anthropic → 本地 Ollama),换 starter 和配置即可,业务代码不动。


1. 这是什么(零基础也能懂)

一句话定义: Spring AI 是 Spring 官方出的 AI 应用框架,它给"大模型、向量库、工具调用"这些新东西套上了 Spring 一贯的端口 + 自动配置 + 拦截链三件套。

解决什么问题、给谁用。 假设你是个 Java 后端,老板让你在现有 Spring Boot 服务里加个"问答助手":要能读公司文档、要能调你已有的几个 Service 方法、要记住对话、上线后还得有监控。裸调 OpenAI SDK 你会写出一堆一次性胶水;换成 Anthropic 就得重写一遍。Spring AI 就是把这堆胶水做成框架。

它对外提供这些能力(as-of 本 commit,按仓库目录点数):

能力具体是什么落在哪个模块
聊天 / 嵌入 / 图像 / 语音 / 审核五类模型的统一接口spring-ai-model
多供应商适配15 个模型模块(OpenAI、Anthropic、Ollama、Bedrock、Google GenAI、DeepSeek…)models/
向量存储22 个向量库模块(PGVector、Qdrant、Redis、Milvus…)vector-stores/
开箱即用装配48 个 Spring Boot starterstarters/
工具调用 / MCP@Tool 注解、MCP 客户端与服务端spring-ai-modelmcp/
RAG 流水线查询改写 → 检索 → 拼接 → 增强spring-ai-rag

用起来什么样。 最小可用的一段业务代码长这样:

// 示意,非源码
@RestController
class AskController {

private final ChatClient chatClient;

AskController(ChatClient.Builder builder) { // starter 已经把 Builder 装好了
this.chatClient = builder
.defaultSystem("你是公司知识库助手,只用中文回答") // 每次请求都带上的系统提示
.build();
}

@GetMapping("/ask")
String ask(String q) {
return chatClient.prompt() // 开始攒一个请求
.user(q) // 用户这句话
.tools(new OrderTools()) // 把带 @Tool 的方法交给模型调用
.call() // 同步发出去(.stream() 就是流式)
.content(); // 只要文本
}
}

重点看:prompt()content() 是一条流式(fluent)链,中间任何一步都只是"往请求上加料",真正发请求发生在 call() 之后。

一句话直觉: 如果你写过 Spring Web——ChatClient 之于 ChatModel,约等于 RestClient 之于 HTTP 客户端;Advisor 之于一次模型调用,约等于 Filter 之于一次 HTTP 请求。(这只是帮你建立心智模型,后文一律用 ChatClient / Advisor 这些原名。)

版本前提: 本仓库主干是 2.0.x(pom.xml:82.0.1-SNAPSHOT),要求 Spring Boot 4.x;1.1.x 分支才配 Spring Boot 3.5.x(README.md:16-18)。


2. 顶层全景(它大概怎么转)

怎么读这张图: 从上往下就是一次 call() 的走向,四个格子按先后顺序执行;箭头右侧是这一步产出的东西。

你的应用代码
│ chatClient.prompt().user(q).tools(...).call().content()

┌──────────────────────────────────────┐
│ ① 组装:攒出一个不可变请求 │ ──▶ ChatClientRequest
│ system → 历史消息 → user 依次排好 │ (Prompt + 一个 context 字典)
└──────────────────────────────────────┘


┌──────────────────────────────────────┐
│ ② Advisor 责任链(按 order 排队) │ ──▶ 记忆 / RAG / 工具循环 / 日志
│ 每环都能改请求、也能改响应 │ 都是链上的一环
└──────────────────────────────────────┘
│ 链底固定是"真的去调模型"这一环

┌──────────────────────────────────────┐
│ ③ ChatModel 端口(可移植抽象) │ ──▶ ChatResponse
└──────────────────────────────────────┘


OpenAI / Anthropic / Ollama / Bedrock … 的 HTTP API

部件一句话职责:

部件干什么在哪个文件
ChatClient面向用户的流式 API,负责"把话说完"spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java:61
ChatClientRequest组装完成的不可变请求:Prompt + 一个 context 字典(advisor 之间传数据用)spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClientRequest.java:36
DefaultAroundAdvisorChain责任链本体:从队列里弹一个 advisor 执行,顺带打一层 observationspring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java:59
ToolCallingAdvisor链上的"工具调用循环":调模型 → 执行工具 → 带结果再调模型,直到模型不再要工具spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/ToolCallingAdvisor.java:65
ChatModelCallAdvisor链底那一环,真正 chatModel.call(prompt);order 是最低优先级,保证排最后spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/ChatModelCallAdvisor.java:43
ChatModel / EmbeddingModel / VectorStore三个可移植端口,每家供应商各写一个实现spring-ai-model/src/main/java/org/springframework/ai/chat/model/ChatModel.java:30
自动配置按 classpath 和配置项把上面这些装成 beanauto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java:72

主线走一遍(不进代码):

  1. 收集。 你在 prompt()... 上写的 system 文本、消息、user 文本、options、tools,全被暂存在一个 request spec 里。
  2. 组装。 call() 触发 toChatClientRequest,把 system 放第一条、历史消息放中间、user 放最后,渲染完模板、合并完 options,产出 ChatClientRequest(DefaultChatClientUtils.java:50-137)。
  3. 建链。 同时建出这次调用专用的 advisor 链:你注册的 advisor + 自动补上的 ToolCallingAdvisor + 链底的 ChatModelCallAdvisor/ChatModelStreamAdvisor(DefaultChatClient.java:1189-1203)。
  4. 穿链。 advisorChain.nextCall(request) 一环环往里走;每环可以在调下一环之前改请求、之后改响应(DefaultAroundAdvisorChain.java:98-121)。
  5. 触底。 走到 ChatModelCallAdvisor,它把结构化输出的格式说明拼进 user 消息,然后 chatModel.call(prompt)(ChatModelCallAdvisor.java:53-58)。
  6. 回卷。 响应沿原路返回,每环按相反顺序做"后处理"(记忆落库、日志、用量累加),最后 content() / entity() 把它变成字符串或 POJO。

3. 阅读地图

建议按下表顺序读;每章都能独立打开,但 01 和 02 是后面几章的地基。

顺序章节讲什么什么时候读它
1ChatClient:从流式 API 到一个 ChatClientRequest流式 API 的三层结构、消息排序规则、options 怎么合并、entity() 怎么走想搞清"我写的那串点号最后变成了什么"
2Advisor 责任链:Spring AI 的拦截器模型CallAdvisor/StreamAdvisor/BaseAdvisor、order 语义、链的弹栈执行与 copy()想自己写一个拦截器,或搞清多个 advisor 的先后
3工具调用:从 @Tool 注解到多轮循环@Tool 如何变成 ToolCallbackToolCallingManager 如何执行、循环为何搬上链做 agent、做函数调用、排查工具没被调用
4可移植模型层:一套抽象罩住十几家供应商ChatModel/ChatOptions 端口设计、供应商特有选项怎么留、结构化输出两条路径要换供应商,或要把回答转成 POJO
5记忆、RAG 与向量存储:把企业数据接进模型ChatMemory 与会话 id、RAG 六段流水线、VectorStore 端口与过滤 DSL做知识库问答、做多轮对话
6落到 Spring Boot:starter、可观测性、MCP 与工具检索自动配置装了什么、gen_ai.* 观测指标、MCP 工具桥接、工具检索(tool search)要上线、要接监控、要接 MCP 生态

4. 巧妙之处(可借鉴的技术)

4.1 把工具循环从模型里搬到链上,再用 copy(this) 防自递归

妙在哪: 1.x 时代"模型说要调工具 → 执行 → 再问模型"这个循环藏在每个 ChatModel 实现内部,链上的 advisor 只看得见最终答案,拦不到中间轮次。2.0 把它抽成 ToolCallingAdvisor,循环体变成链上的一个 do-while,于是每一轮都会重新穿过链的下半截,日志/记忆/审计 advisor 因此能看见每一轮(ToolCallingAdvisor.java:149-216)。

关键一行是 callAdvisorChain.copy(this).nextCall(...):它复制一条排除自己之后的链,否则每轮都会再次进入自己而无限递归(ToolCallingAdvisor.java:167DefaultAroundAdvisorChain.java:178-195)。

配套证据:全仓非测试代码里 executeToolCalls 的调用点只剩 advisor 和 manager,任何 ChatModel 实现都不再调它;OpenAiChatModel 只用 resolveToolDefinitions 把工具描述塞进请求(models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java:879)。

4.2 用 order 数值让"记忆"天然套在"工具循环"外面

妙在哪: 一个真实的坑是——工具循环跑三轮,记忆 advisor 如果在循环内,就会把三轮中间态全写进历史。Spring AI 不靠文档提醒,而是靠两个默认常量把层次固定死:

常量效果
Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDERHIGHEST_PRECEDENCE + 200记忆 advisor 先执行 → 在外层
ToolCallingAdvisor.DEFAULT_ORDERHIGHEST_PRECEDENCE + 300工具循环在内层,中间轮次不落进记忆

依据:advisor/api/Advisor.java:39advisor/ToolCallingAdvisor.java:75

更细的一手:如果检测到有 MemoryAdvisor 排在自己下游(order 更大,即在循环里面),ToolCallingAdvisor 会主动关掉自己维护的中间历史,把记账权交出去,避免两边重复(DefaultChatClient.java:1232-1237,配合 doGetNextInstructionsForToolCall,ToolCallingAdvisor.java:222-234)。

4.3 结构化输出走 context 字典,而不是塞进方法签名

妙在哪: 你调 .entity(Order.class),格式说明并不是当场拼进 prompt 的。entity() 先把格式字符串放进请求的 context 字典,等链走到底,ChatModelCallAdvisor 才决定怎么用它:

  • 模型不支持原生结构化输出 → 把格式说明追加到 user 消息末尾;
  • 支持原生(选项实现了 StructuredOutputChatOptions)→ 把 JSON Schema 写进 options,不动 prompt。

依据:DefaultChatClient.java:592-607(写 context)、ChatModelCallAdvisor.java:66-101(两条分支)、键名见 ChatClientAttributes.java:29-33

好处是中间任何 advisor 都能看见甚至改写这个意图,而不需要在链的每一层增加一个参数。

4.4 给循环留 protected 钩子,扩展不用改源码

妙在哪: ToolCallingAdvisor 把循环的四个时刻开成 protected 空方法——doInitializeLoop / doBeforeCall / doAfterCall / doFinalizeLoop(ToolCallingAdvisor.java:236-252);流式侧另有同款钩子 doInitializeLoopStream / doBeforeStream / doAfterStream / doFinalizeLoopStream(ToolCallingAdvisor.java:413-451)。

这不是摆设:仓库自带的工具检索功能就是靠继承实现的——ToolSearchToolCallingAdvisor extends ToolCallingAdvisor,在 doBeforeCall 里按当前对话动态换掉这一轮要暴露给模型的工具集(advisors/spring-ai-tool-search-advisor/src/main/java/org/springframework/ai/chat/client/advisor/toolsearch/ToolSearchToolCallingAdvisor.java:76,146-173)。当你有几百个工具、塞不进上下文时,这就是解法。

4.5 "至多一个 ToolAdvisor" 用启动期校验兜住

妙在哪: 框架会自动补一个 ToolCallingAdvisor,而用户也可能手动注册一个(比如上面的工具检索版)。两个循环叠在一起会产生难查的重复调用。Spring AI 的处理是两段式:先看链里有没有 ToolAdvisor 标记接口的实现,有就不自动补(DefaultChatClient.java:1217-1238);建链前再数一遍,超过一个直接抛异常并打印各自的 order(DefaultChatClient.java:1240-1248)。

可借鉴的模式: 用一个空标记接口(ToolAdvisorMemoryAdvisor)表达"这一环属于哪一类",框架就能对链做语义级校验,而不必反射类名。


5. 代码地图(导航索引)

按主题跳源码。行号 as-of c988e72a;行号漂移时用「符号名」列 grep 更稳。

主题文件路径符号名锚点
流式 API 入口spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.javaChatClientprompt()ChatClientRequestSpec:61:128:386
请求组装(消息排序 + options 合并)spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientUtils.javatoChatClientRequest:50-137
建链 + 自动注册工具 advisorspring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.javabuildAdvisorChainautoRegisterToolCallingAdvisorvalidateSingleToolAdvisor:1189:1217:1240
结构化输出的意图传递spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.javadoSingleWithBeanOutputConverter:592-607
请求 / 响应载体spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClientRequest.javaChatClientRequest(record):36
责任链执行与复制spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.javanextCallnextStreamcopyAdvisorsAfter:98:124:178
advisor 基类(before/after 模板)spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/api/BaseAdvisor.javaBaseAdvisorbeforeafter:42:84:89
advisor 顺序常量spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/api/Advisor.javaDEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER:39
工具调用循环(同步 + 流式)spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/ToolCallingAdvisor.javaadviseCalladviseStreamhandleToolCallRecursionDEFAULT_ORDER:123:258:331:75
链底调模型spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/ChatModelCallAdvisor.javaadviseCallaugmentWithFormatInstructions:53:66
工具解析与执行spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.javaresolveToolDefinitionsexecuteToolCalls:137:147
@Tool 注解 → 回调spring-ai-model/src/main/java/org/springframework/ai/tool/method/MethodToolCallbackProvider.javagetToolCallbacks:86:124
工具注解定义spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.javaToolreturnDirect():37:64
聊天模型端口spring-ai-model/src/main/java/org/springframework/ai/chat/model/ChatModel.javaChatModelcall(Prompt):30:45
供应商实现样板models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.javacallinternalCallresolveToolDefinitions 调用点:196:208:879
结构化输出转换器spring-ai-model/src/main/java/org/springframework/ai/converter/BeanOutputConverter.javaBeanOutputConverter:51
对话记忆端口spring-ai-model/src/main/java/org/springframework/ai/chat/memory/ChatMemory.javaChatMemoryCONVERSATION_ID:31:36
记忆 advisorspring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/MessageChatMemoryAdvisor.javaMessageChatMemoryAdvisorbeforeafter:47:74:137
RAG 流水线spring-ai-rag/src/main/java/org/springframework/ai/rag/advisor/RetrievalAugmentationAdvisor.javaRetrievalAugmentationAdvisorbefore:62:107
向量检索器spring-ai-rag/src/main/java/org/springframework/ai/rag/retrieval/search/VectorStoreDocumentRetriever.javaVectorStoreDocumentRetriever:57
向量存储端口spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/VectorStore.javaVectorStoreaddgetNativeClient:40:51:100
Boot 自动配置auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.javaChatClientAutoConfigurationchatClientBuilder:72:115
观测指标命名spring-ai-model/src/main/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConvention.javaDEFAULT_NAME(gen_ai.client.operation):40
MCP 工具桥接mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.javaSyncMcpToolCallback:47
工具检索扩展advisors/spring-ai-tool-search-advisor/src/main/java/org/springframework/ai/chat/client/advisor/toolsearch/ToolSearchToolCallingAdvisor.javaToolSearchToolCallingAdvisordoBeforeCall:76:156

引用约定: 上表及全文行号均相对克隆根,as-of sourceCommit: c988e72ab7282bcc352b2155e8cb27ff4b5e0fda