Spring AI 核心架构全解析:从 ChatModel 到 Advisor Chain 的设计哲学

Spring AI 核心架构全解析:从 ChatModel 到 Advisor Chain 的设计哲学

系统提示词、记忆管理、工具调用、结构化输出——这些 AI Agent 的核心能力,在 Spring AI 中是如何统一编排的?答案藏在三个核心抽象里。

如果你读过本系列的前面几篇文章,应该已经对 AI Agent 的各个”器官”有了清晰的认识:ReAct 模式是它的大脑推理循环,Tool Use是它的双手,Memory是它的记忆系统,Prompt Engineering是它与人沟通的语言艺术。

但你有没有想过一个问题:这些能力是怎么被组装到一起的?谁负责把用户的输入变成 API 调用?谁负责在调用前后注入记忆、拦截敏感内容、格式化输出?

答案是 Spring AI 的核心架构——一个由 ChatModel、PromptTemplate 和 Advisor Chain 构成的精巧流水线。理解了这个架构,你才能真正掌控 Spring AI,而不是被它牵着鼻子走。

一、为什么需要理解架构,而不仅仅是 API?

很多开发者学 Spring AI 的方式是:翻文档 → 找到一个 ChatClient 的例子 → 跑通 → 以为自己会了。

这就像学开车只学了踩油门——你知道怎么让车往前跑,但不知道变速箱怎么工作、刹车系统怎么介入、ESP 在什么时候帮你稳住车身。一旦出了问题(比如响应格式不对、调用成本太高、链路延迟异常),你就完全抓瞎了。

Spring AI 的架构设计有一个核心思想:把 LLM 调用变成一次标准的 Spring 请求处理流程。这个流程里,ChatModel 是执行器,PromptTemplate 是模板引擎,Advisor Chain 是拦截器链——听起来是不是很像 Spring MVC 的 DispatcherServlet → HandlerMapping → HandlerAdapter → Interceptor

没错,Spring 团队就是故意这么设计的。他们希望 Java 开发者用已经熟悉的思维方式来构建 AI 应用。

二、ChatModel:一切调用的起点

2.1 ChatModel 接口的设计

先看核心接口定义:

1
2
3
4
5
6
7
8
9
10
11
public interface ChatModel extends Model<Prompt, ChatResponse> {

// 同步调用
ChatResponse call(Prompt prompt);

// 流式调用
Flux<ChatResponse> stream(Prompt prompt);

// 获取模型的默认选项
ChatOptions getDefaultOptions();
}

就这么简单的三个方法。但背后的设计考量远比看起来深刻。

为什么 call 接收 Prompt 而不是 String 因为一次 LLM 调用不仅仅是发送一段文字。一个 Prompt 对象封装了:

  • 一组 Message(系统消息、用户消息、助手消息、工具消息)
  • 一个 ChatOptions(模型名、温度、最大 token 等参数)

这就像 Spring MVC 里 HttpServletRequest 不仅仅是用户提交的表单数据,还包含了 headers、cookies、session 等上下文信息。

为什么 stream 返回 Flux<ChatResponse> 而不是 Flux<String> 因为流式响应不仅仅是文字片段。每次 chunk 可能携带:token 使用量、模型元数据、甚至工具调用的增量信息。用 ChatResponse 包装,上层代码可以统一处理。

2.2 消息类型体系

Spring AI 的消息体系是理解一切的基础:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Content {
String getText();
Map<String, Object> getMetadata();
List<Media> getMedia();
}

public interface Message extends Content {
MessageType getMessageType();
}

public enum MessageType {
SYSTEM, // 系统提示词
USER, // 用户输入
ASSISTANT, // 模型回复
TOOL // 工具调用结果
}

每种消息类型对应 OpenAI API 里的一个 role 字段。这个映射关系是这样的:

Spring AI OpenAI API
SystemMessage {"role": "system"}
UserMessage {"role": "user"}
AssistantMessage {"role": "assistant"}
ToolResponseMessage {"role": "tool"}

为什么要这么设计?因为不同厂商的 API 对消息角色的定义不完全一致。Spring AI 通过统一的消息类型体系,把底层差异屏蔽了。你写一套代码,换 OpenAI、换 Anthropic、换 Ollama,消息模型不用改。

2.3 一次调用的完整生命周期

让我用一个实际的代码片段来追踪一次调用的完整流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class ChatController {

private final ChatClient chatClient;

public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个专业的 Java 技术顾问")
.defaultOptions(ChatOptions.builder()
.model("gpt-4o")
.temperature(0.7)
.build())
.build();
}

@GetMapping("/chat")
public String chat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}

这短短几行代码,底层发生了什么?

  1. 构建阶段ChatClient.Builder 把系统提示词和默认选项存起来
  2. 组装阶段.user(message) 创建一个 UserMessage,与系统提示词一起组装成 Prompt 对象
  3. 执行阶段.call() 触发 ChatModel.call(prompt)
  4. 序列化阶段:Prompt 对象被序列化为 OpenAI 的 JSON 格式
  5. 网络阶段:通过 RestClient 发送 HTTP 请求
  6. 反序列化阶段:响应 JSON 被解析为 ChatResponse
  7. 提取阶段.content()ChatResponse 中提取文本

关键洞察:在这个流程中,步骤 3 和步骤 6 之间可以插入任意多个处理环节——这就是 Advisor Chain 的价值,我们后面详细讲。

2.4 源码剖析:OpenAiChatModel

看一个具体实现。OpenAiChatModel 是 Spring AI 对 OpenAI 的适配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class OpenAiChatModel extends AbstractToolCallSupport implements ChatModel {

private final OpenAiApi api;
private final OpenAiChatOptions defaultOptions;

@Override
public ChatResponse call(Prompt prompt) {
// 1. 将 Prompt 转换为 OpenAI API 请求格式
OpenAiApi.ChatCompletionRequest request = createRequest(prompt);

// 2. 发送 HTTP 请求
ResponseEntity<OpenAiApi.ChatCompletion> responseEntity =
this.api.chatCompletionEntity(request);

// 3. 将 OpenAI 响应转换为 Spring AI 的 ChatResponse
ChatResponse chatResponse = toChatResponse(responseEntity.getBody());

// 4. 处理工具调用(如果有)
if (chatResponse.hasToolCalls()) {
return handleToolCalls(prompt, chatResponse);
}

return chatResponse;
}
}

注意第 4 步——如果模型返回了工具调用请求,OpenAiChatModel 会自动执行工具、把结果封装为 ToolResponseMessage,然后再次调用模型。这个”自动循环”就是 Tool Use 的核心机制,它被封装在 AbstractToolCallSupport 基类里,所有模型实现都可以复用。

三、PromptTemplate:不仅仅是字符串替换

3.1 超越简单的模板

很多人以为 PromptTemplate 就是 String.format() 的高级版——往模板里填变量就完事了。但实际上,Spring AI 的 PromptTemplate 承担了更重要的职责。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PromptTemplate template = new PromptTemplate("""
你是一个{role}。
请根据以下上下文回答问题:

上下文:{context}
问题:{question}

请用{language}回答。
""");

Prompt prompt = template.create(Map.of(
"role", "Java 技术专家",
"context", retrievalResult,
"question", userQuestion,
"language", "中文"
));

这里 create() 方法返回的是一个完整的 Prompt 对象,而不是简单的字符串。它做了这些事情:

  1. 变量替换:把 {xxx} 占位符替换成实际值
  2. 消息拆分:根据模板结构判断哪些是系统消息、哪些是用户消息
  3. 选项合并:如果模板中定义了 ChatOptions,会与调用时的选项合并
  4. 验证:检查所有必填变量是否都提供了

3.2 与 RAG 的深度集成

在 RAG 场景下,PromptTemplate 的价值更加明显。还记得我们在 RAG 完整指南 中讲过的检索增强生成流程吗?Spring AI 把检索结果注入到 Prompt 中的方式非常优雅:

1
2
3
4
5
6
7
8
// RAG 场景下的 Prompt 构建
String systemPrompt = """
你是公司的内部知识助手。请严格根据以下参考资料回答用户问题。
如果参考资料中没有相关信息,请明确告知无法回答。

参考资料:
{context}
""";

{context} 的填充,通常通过 Advisor Chain 自动完成——这就是为什么理解 Advisor Chain 如此重要。

3.3 与 LangChain4j 的对比

LangChain4j 的模板系统叫 PromptTemplate(名字一样,但设计思路不同):

1
2
3
4
5
// LangChain4j 的方式
PromptTemplate template = PromptTemplate.from(
"你是{{role}},请回答:{{question}}"
);
String text = template.apply(Map.of("role", "专家", "question", "问题"));

关键差异

维度 Spring AI LangChain4j
返回类型 Prompt 对象(含消息列表+选项) String(纯文本)
消息角色感知 支持(可在模板中标记 system/user) 不支持(统一作为 user 消息)
与框架集成 通过 Advisor Chain 自动注入 需要手动组装 ChatMessage
类型安全 支持 ST(Structured Template)类型安全模板 纯字符串替换

Spring AI 的设计更贴近 Spring 的哲学——模板不是孤立的字符串处理工具,而是整个请求处理管线的一环。这也是为什么 Spring AI 的 PromptTemplate 可以和 Advisor Chain 无缝配合,而 LangChain4j 需要更多手动编排。

四、Advisor Chain:Spring AI 的灵魂

如果说 ChatModel 是发动机,PromptTemplate 是油门踏板,那 Advisor Chain 就是整辆车的控制系统——它决定了调用前做什么、调用后做什么、出错了怎么办。

4.1 什么是 Advisor?

1
2
3
4
5
6
7
8
9
10
11
12
13
@FunctionalInterface
public interface CallAdvisor extends Advisor {

// 围绕 ChatModel.call() 的增强逻辑
ChatResponse adviseCall(Prompt prompt, CallAdvisorChain chain);
}

@FunctionalInterface
public interface StreamAdvisor extends Advisor {

// 围绕 ChatModel.stream() 的增强逻辑
Flux<ChatResponse> adviseStream(Prompt prompt, StreamAdvisorChain chain);
}

每个 Advisor 拦截一次 LLM 调用,可以在调用前后做任何事情:

  • 调用前:注入记忆、注入检索结果、压缩 prompt、添加安全过滤
  • 调用后:格式化输出、提取结构化数据、记录日志、计算成本

这和 Spring MVC 的 HandlerInterceptor、Servlet 的 Filter 是完全一样的设计模式——责任链模式

4.2 Advisor 的执行顺序

1
2
3
4
5
6
7
8
ChatClient chatClient = builder
.defaultSystem("你是助手")
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory), // 1. 注入记忆
new QuestionAnswerAdvisor(vectorStore), // 2. RAG 检索
new SafeGuardAdvisor(blockedWords) // 3. 安全过滤
)
.build();

执行时,请求像剥洋葱一样穿过每一层:

1
2
3
4
5
6
7
8
用户请求
→ SafeGuardAdvisor (检查输入是否安全)
→ QuestionAnswerAdvisor (检索相关文档,注入到 prompt)
→ MessageChatMemoryAdvisor (注入历史对话记忆)
→ ChatModel.call() (真正的 LLM 调用)
← 拿到响应
← SafeGuardAdvisor (检查输出是否安全)
← 返回给用户

注意方向:Advisors 按添加顺序从外到内执行(前处理),响应从内到外返回(后处理)。这和 Spring AOP 的 Around Advice 是一个道理。

4.3 内置 Advisor 详解

Spring AI 提供了几个开箱即用的 Advisor,每一个都对应一个核心场景:

MessageChatMemoryAdvisor——记忆注入

1
2
3
4
5
6
7
// 创建记忆存储
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build();

// 作为 Advisor 挂载
MessageChatMemoryAdvisor memoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);

内部实现的核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public ChatResponse adviseCall(Prompt prompt, CallAdvisorChain chain) {
// 1. 从存储中取出历史消息
List<Message> memoryMessages = chatMemory.get(conversationId);

// 2. 把历史消息注入到 prompt 的消息列表前面
List<Message> combinedMessages = new ArrayList<>(memoryMessages);
combinedMessages.addAll(prompt.getInstructions());

// 3. 创建新的 Prompt,带着历史记忆继续传递
Prompt augmentedPrompt = new Prompt(combinedMessages, prompt.getOptions());

// 4. 调用链中的下一个 Advisor(最终到达 ChatModel)
ChatResponse response = chain.nextCall(augmentedPrompt);

// 5. 把本次的用户消息和助手回复存入记忆
chatMemory.add(conversationId, userMessage);
chatMemory.add(conversationId, assistantMessage);

return response;
}

设计亮点:记忆注入是在 Advisor 层完成的,ChatModel 完全不知道记忆的存在。这意味着你可以轻松替换记忆策略(窗口记忆、摘要记忆、向量记忆),而不用改任何模型调用代码。

QuestionAnswerAdvisor——RAG 检索增强

1
2
3
4
5
6
7
QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.query("...")
.topK(5)
.similarityThreshold(0.7)
.build())
.build();

这个 Advisor 在 知识检索引擎 那篇文章中有详细讲解。它的核心流程:

  1. 拿到用户的原始问题
  2. 用问题去向量数据库检索相关文档
  3. 把检索到的文档拼接到 prompt 的 {context} 占位符中
  4. 把增强后的 prompt 传给下一个 Advisor

关键设计:检索结果是通过 PromptTemplate 的变量注入的,而不是直接拼接字符串。这保证了模板的一致性和可维护性。

SafeGuardAdvisor——安全过滤

1
2
3
SafeGuardAdvisor safeGuard = new SafeGuardAdvisor(
List.of("密码", "内部机密", "SQL注入")
);

简单但有效——在输入和输出两端检查是否有敏感内容。在 AI Agent 安全防线 那篇文章中我们详细讨论了 Prompt 注入防御,SafeGuardAdvisor 是第一道防线的实现。

4.4 自定义 Advisor 实战

理解了 Advisor 的机制后,你可以编写自己的 Advisor 来扩展任何能力。比如一个 Token 用量监控 Advisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Component
public class TokenUsageAdvisor implements CallAdvisor {

private static final Logger log = LoggerFactory.getLogger(TokenUsageAdvisor.class);
private final MeterRegistry meterRegistry;

public TokenUsageAdvisor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}

@Override
public ChatResponse adviseCall(Prompt prompt, CallAdvisorChain chain) {
long startTime = System.currentTimeMillis();

// 调用链继续执行
ChatResponse response = chain.nextCall(prompt);

long latency = System.currentTimeMillis() - startTime;

// 记录 Token 使用量
Usage usage = response.getMetadata().getUsage();
meterRegistry.counter("ai.tokens.input").increment(usage.getPromptTokens());
meterRegistry.counter("ai.tokens.output").increment(usage.getGenerationTokens());
meterRegistry.timer("ai.latency").record(latency, TimeUnit.MILLISECONDS);

log.info("LLM调用完成: inputTokens={}, outputTokens={}, latency={}ms",
usage.getPromptTokens(), usage.getGenerationTokens(), latency);

return response;
}

@Override
public String getName() {
return "TokenUsageAdvisor";
}

@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE; // 最外层,统计完整耗时
}
}

这个 Advisor 挂在链的最外层,包裹整个调用过程,记录了输入 token、输出 token 和延迟。配合 Micrometer + Prometheus,你就能在 Grafana 里看到 AI 调用的实时监控面板——这正是 AI Agent 可观测性 中讲的方案的底层实现。

五、ChatClient:统一的门面

理解了上面三个核心组件后,ChatClient 就很好理解了——它是把这些组件组合在一起的 门面(Facade)

5.1 Builder 模式的精妙设计

1
2
3
4
5
6
7
8
9
10
11
12
13
ChatClient client = ChatClient.builder(chatModel)
.defaultSystem("你是一个 Java 架构师")
.defaultOptions(ChatOptions.builder()
.model("gpt-4o")
.temperature(0.3)
.maxTokens(4096)
.build())
.defaultAdvisors(
new MessageChatMemoryAdvisor(memory),
new QuestionAnswerAdvisor(vectorStore)
)
.defaultFunctions("getWeather", "searchDocs") // 注册工具
.build();

Builder 的每个方法都在做一件事:往调用管线的不同层注入配置。最终 build 出来的 ChatClient 就是一个预配置好的调用管线。

5.2 调用时的动态覆盖

1
2
3
4
5
6
7
8
9
10
// 可以在每次调用时覆盖默认配置
String answer = client.prompt()
.system("这次用英文回答") // 覆盖系统提示词
.user("什么是 Spring AI?") // 设置用户消息
.options(ChatOptions.builder() // 覆盖模型参数
.temperature(0.9)
.build())
.advisors(a -> a.param("chat_memory_conversation_id", "user-123")) // 设置记忆上下文
.call()
.content();

为什么要有”默认+覆盖”的设计? 因为大多数场景下配置是固定的(系统提示词、模型选择、Advisor 链),但偶尔需要在特定请求中调整参数(比如不同的对话需要不同的 conversation ID)。

六、与 LangChain4j 架构的横向对比

作为 Java 生态的两大 AI 框架,Spring AI 和 LangChain4j 的架构设计哲学有本质差异:

维度 Spring AI LangChain4j
核心模式 Advisor Chain(责任链) Chain(顺序调用)
扩展机制 Advisor(声明式,可插拔) Chain 步骤(代码式,需修改)
记忆管理 Advisor 自动注入 需手动注入 ChatMemory
RAG 集成 Advisor 自动检索+注入 需手动编排 EmbeddingStoreRetriever
工具调用 模型层自动处理循环 AiServices 代理层处理
Spring 生态集成 原生(DI、AOP、Actuator) 需要桥接(CDI 适配)
流式支持 Flux 原生集成 需要额外配置
学习曲线 熟悉 Spring 的人上手快 更接近 Python AI 框架的思路

怎么选? 如果你的团队是 Spring 技术栈,Spring AI 是更自然的选择——你不需要学一套新的编程范式,用已经熟悉的拦截器、模板引擎、依赖注入就能构建完整的 AI 应用。如果你的项目不依赖 Spring,或者你更喜欢 LangChain 的”链式调用”风格,LangChain4j 也很好。

关键不是哪个”更好”,而是理解它们的设计哲学差异:Spring AI 把 AI 调用当作一次 HTTP 请求处理,LangChain4j 把它当作一条数据处理流水线。

七、生产环境的最佳实践

7.1 Advisor 的顺序很重要

1
2
3
4
5
6
7
8
9
10
11
// ❌ 错误顺序:记忆在最外层,导致安全过滤无法检查注入了记忆的 prompt
.defaultAdvisors(
new MessageChatMemoryAdvisor(memory), // 外层:注入记忆
new SafeGuardAdvisor(blockedWords) // 内层:只检查原始输入
)

// ✅ 正确顺序:安全过滤在最外层,能检查完整的 prompt
.defaultAdvisors(
new SafeGuardAdvisor(blockedWords), // 外层:检查完整 prompt
new MessageChatMemoryAdvisor(memory) // 内层:注入记忆
)

经验法则:安全类 Advisor 放最外层,数据增强类(RAG、记忆)放中间,格式化类放最内层。

7.2 异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class FallbackAdvisor implements CallAdvisor {

@Override
public ChatResponse adviseCall(Prompt prompt, CallAdvisorChain chain) {
try {
return chain.nextCall(prompt);
} catch (ModelNotAvailableException e) {
// 主模型不可用,切换到备用模型
return fallbackModel.call(prompt);
} catch (RateLimitExceededException e) {
// 限流,返回友好提示
return new ChatResponse(List.of(
new Generation("当前请求量较大,请稍后再试。")
));
}
}
}

7.3 成本控制

结合 Token Usage Advisor 和预算守卫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class BudgetGuardAdvisor implements CallAdvisor {

private final AtomicInteger dailyTokens = new AtomicInteger(0);
private static final int DAILY_LIMIT = 1_000_000;

@Override
public ChatResponse adviseCall(Prompt prompt, CallAdvisorChain chain) {
if (dailyTokens.get() >= DAILY_LIMIT) {
throw new BudgetExceededException("今日 Token 预算已用完");
}

ChatResponse response = chain.nextCall(prompt);
dailyTokens.addAndGet(response.getMetadata().getUsage().getTotalTokens());
return response;
}
}

这个思路在 评估与优化 那篇有更详细的讨论。

八、架构的边界与未来

8.1 当前的局限

Spring AI 的 Advisor Chain 设计虽然优雅,但也有局限:

  1. 编排能力有限:目前的 Advisor Chain 是线性的,不支持条件分支(”如果用户问的是代码问题,走代码专用模型;否则走通用模型”)。这种复杂编排需要自己实现。
  2. 多模型协作不直接:虽然可以用不同的 ChatModel 创建不同的 ChatClient,但没有内置的”模型路由”机制。
  3. 调试体验:链路中间环节的调试还不够方便,没有内置的 Advisor 可视化工具。

8.2 未来方向

Spring AI 正在快速演进,几个值得关注的方向:

  • Agent 抽象:目前 Spring AI 更偏向”模型调用框架”,Agent 能力(自主规划、循环推理)还在演进中
  • 多模态统一:图片、音频、视频的处理正在整合到统一的 Advisor 体系中
  • 评估框架:内置的模型评估和 A/B 测试能力
  • 与 Spring 生态的深度融合:Spring Batch 处理大批量 AI 任务、Spring Integration 处理事件驱动的 AI 流程

总结

Spring AI 的核心架构可以用一句话概括:用 Spring 开发者熟悉的方式,把 LLM 调用变成可管理、可扩展、可观测的企业级组件

  • ChatModel 是执行器——负责与 LLM 通信
  • PromptTemplate 是模板引擎——负责构建结构化的提示词
  • Advisor Chain 是拦截器链——负责在调用前后注入各种横切关注点
  • ChatClient 是门面——负责把这些组件组装成一个易用的 API

理解了这个架构,你就不再是”调 API 的人”,而是”设计系统的人”。你可以根据业务需求选择合适的 Advisor 组合,编写自定义 Advisor 处理特殊场景,用监控 Advisor 掌握系统运行状况。

这就是从”会用框架”到”理解框架”的跨越。


相关阅读