深入 Spring AI ChatClient:从一行代码到 LLM 调用的完整旅程

8 阅读16分钟

深入 Spring AI ChatClient:从一行代码到 LLM 调用的完整旅程

难度:★★★☆☆

当你写下 chatClient.prompt().user("Tell me a joke").call().content() 时,Spring AI 在背后做了什么?


目录

  1. 引言:一个问题
  2. 第一层:初见——最小可用示例
  3. 第二层:创建 ChatClient 的三种姿势
  4. 第三层:Fluent API 与响应类型全景
  5. 第四层:Prompt 模板与消息元数据
  6. 第五层:Builder 默认值——配置一次,处处生效
  7. 第六层:Advisor——Spring AI 的灵魂
  8. 第七层:聊天记忆 ChatMemory
  9. 总结

1. 引言:一个问题

在 Spring AI 中,最常用的"魔法"就是这段代码:

@RestController
class MyController {

    private final ChatClient chatClient;

    public MyController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/ai")
    String generation(String userInput) {
        return this.chatClient.prompt()
            .user(userInput)
            .call()
            .content();
    }
}

三行链式调用,一次 LLM 请求就完成了。没有 HTTP 客户端,没有 JSON 拼装,没有手动解析——就像在写 RestClient 一样自然。

问题来了:ChatClient 到底是接口还是实现?call()content() 谁才是真正发起模型调用的方法?为什么一个 ChatClient 能同时支持同步、流式、结构化输出、工具调用和 RAG?

本文将从官方参考文档出发,逐层拆解这个 API。你会发现:ChatClient 不是"又一个 HTTP 封装",而是一整套可组装、可观测、可插拔的 LLM 调用架构


2. 第一层:初见——最小可用示例

ChatClient 是 Spring AI 面向开发者的核心门面(Facade),提供:

  • 流式 API(Fluent API):用链式调用拼装 Prompt、调用模型、解析响应;
  • 同步 + 流式双编程模型:既能 call() 拿到完整结果,也能 stream() 拿到 Flux 增量流;
  • 统一的模型抽象:OpenAI、Anthropic、DeepSeek、Ollama……换模型不改业务代码;
  • Advisor 拦截链:把记忆、RAG、日志、工具调用、安全护栏等横切能力织入调用链。

最小闭环只有三个动作:

chatClient.prompt()      // ① 开启 Fluent 链(构造 Prompt)
    .user(userInput)      // ② 设置用户消息
    .call()               // ③ 声明同步调用
    .content();           // ④ 取出纯文本响应

其中 prompt() 负责把"用户消息、系统消息、工具、参数、Advisor"组装成一个 Prompt 对象——Prompt 从 API 角度上看就是一组消息的集合UserMessage(用户直接输入)与 SystemMessage(系统生成的对话引导),消息中常含占位符,运行时由用户输入替换。此外还有 Prompt 选项(如模型名称、控制随机性的 temperature)。


3. 第二层:创建 ChatClient 的三种姿势

3.1 姿势一:自动配置的 prototype Builder

最省事的方式:Spring AI 为每个 ChatModel 的自动配置都准备了一个 prototype 作用域ChatClient.Builder bean,直接注入:

@RestController
class MyController {

    private final ChatClient chatClient;

    public MyController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }
}

为什么是 prototype 作用域? 因为 Builder 是"一次性模具":build() 之后,builder 内部的默认配置会被复制进 ChatClient。如果 Builder 是单例,所有 ChatClient 会共享同一个 builder 实例,状态互相污染;prototype 保证每次注入都是全新实例

这也带来一个能力:同一个模型类型,可以轻松构建多个不同配置的 ChatClient:

@Configuration
class ChatClientConfig {

    @Bean
    ChatClient defaultChatClient(ChatClient.Builder builder) {
        return builder.build();
    }

    @Bean
    ChatClient customChatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("You are a helpful assistant.").build();
    }
}

3.2 姿势二:多模型场景的工程化方案

真实项目里几乎不会只有一个模型:复杂推理用大模型、简单任务用小模型、某个供应商挂了要降级……文档给出了多模型的完整工程方案。

新手最容易踩的坑:用 ChatClient.create(chatModel)ChatClient.builder(chatModel) 直接手工创建客户端——这会绕过自动配置,导致可观测性(Observability)和 ChatClientBuilderCustomizer 全部失效

正确姿势是注入 ChatClientBuilderConfigurer——它会在内部应用所有 Customizer 并接好观测埋点,镜像自动配置的行为:

@Configuration
public class ChatClientConfig {

    @Bean
    @Primary
    public ChatClient openAiChatClient(OpenAiChatModel chatModel, ChatClientBuilderConfigurer configurer,
            ObjectProvider<ObservationRegistry> observationRegistry,
            ObjectProvider<ChatClientObservationConvention> chatClientObservationConvention,
            ObjectProvider<AdvisorObservationConvention> advisorObservationConvention,
            ObjectProvider<ToolCallingAdvisor.Builder<?>> toolCallingAdvisorBuilder) {
        ChatClient.Builder builder = ChatClient.builder(chatModel,
                observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP),
                chatClientObservationConvention.getIfUnique(),
                advisorObservationConvention.getIfUnique(),
                toolCallingAdvisorBuilder.getIfAvailable());
        return configurer.configure(builder).build();
    }
}

之后用 @Qualifier("openAiChatClient") / @Qualifier("anthropicChatClient") 注入即可按需切换模型。注意:当容器里存在多个 ChatModel bean 时,ChatModelChatClient 都需要显式标记 @Primary 以消解自动配置的歧义。

3.3 姿势三:多个 OpenAI 兼容端点

Groq、DeepSeek、vLLM 等大多兼容 OpenAI 协议,用 OpenAiChatModel.builder() 指定 baseUrl 就能一网打尽:

OpenAiChatModel groqModel = OpenAiChatModel.builder()
    .options(OpenAiChatOptions.builder()
        .baseUrl("https://api.groq.com/openai/v1")
        .apiKey(System.getenv("GROQ_API_KEY"))
        .model("llama3-70b-8192")
        .temperature(0.5)
        .build())
    .build();

String response = ChatClient.builder(groqModel).build().prompt(prompt).call().content();

4. 第三层:Fluent API 与响应类型全景

4.1 prompt() 的三个入口

prompt() 有三种重载,对应不同的拼装起点:

方法用途
prompt()最常用,自由拼装 user / system / tools / advisors
prompt(Prompt prompt)传入已构造好的 Prompt 对象
prompt(String content)便捷方法,直接以文本开头
// 便捷入口,等价于 prompt().user(text)
chatClient.prompt("What is the capital of France?").call().content();

4.2 call() 的惰性执行

关键认知.call() 本身不会触发模型调用——它只是在声明"我要用同步方式"。真正的模型调用发生在 .content().chatResponse() 等终止方法(Terminal Method)上。这与 WebClient 的延迟语义一脉相承。

call() 之后有这些返回值可选:

方法返回类型典型用途
content()String最常用,纯文本
chatResponse()ChatResponse完整响应对象,含多个 Generation 及元数据(如 token 用量——计费审计依据)
chatClientResponse()ChatClientResponseChatResponse + 执行上下文(可拿到 RAG 检索到的文档等 Advisor 中间产物)
entity(...)Java 类型结构化输出,直接映射成对象
responseEntity(...)ResponseEntity<T>ChatResponse 与结构化实体"双全"
// 完整响应对象:token 用量是计费的关键指标
ChatResponse chatResponse = chatClient.prompt()
    .user("Tell me a joke")
    .call()
    .chatResponse();

responseEntity() 需要同时拿到元数据和结构化实体时非常有用:

ResponseEntity<ActorFilms> re = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .responseEntity(ActorFilms.class);

4.3 entity():结构化输出

entity() 是生产环境最高频的能力——让 LLM 输出直接落入类型安全的 Record:

record ActorFilms(String actor, List<String> movies) {}

ActorFilms actorFilms = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorFilms.class);

// 泛型集合用 ParameterizedTypeReference
List<ActorFilms> films = chatClient.prompt()
    .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorFilms>>() {});

entity() 的所有重载:

entity(Class<T> type)
entity(ParameterizedTypeReference<T> type)              // 泛型集合
entity(StructuredOutputConverter<T> converter)          // 自定义转换器
entity(Class<T> type, Consumer<EntityParamSpec> spec)   // + 高级配置
entity(ParameterizedTypeReference<T> type, Consumer<EntityParamSpec> spec)
entity(StructuredOutputConverter<T> converter, Consumer<EntityParamSpec> spec)

4.4 EntityParamSpec:让结构化输出更"硬"

spec 参数的形式是 Spring AI 2.0 的重点,它开启两个独立的高级行为:

① 原生结构化输出(useProviderStructuredOutput()

默认情况下,JSON Schema 是以文本指令的方式追加进用户消息("请按以下 JSON Schema 格式输出")。开启后,Schema 会作为结构化约束直接传给模型供应商 API,由模型端保证格式:

ActorFilms actorFilms = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorFilms.class, spec -> spec.useProviderStructuredOutput());

⚠️ 架构师提示:原生结构化输出默认关闭,因为各模型支持度参差不齐——比如 Ollama 的推理/思考模式可能返回纯文本而非 JSON;OpenAI 不支持顶层数组 Schema。生产环境按模型实测后再开启。它等价于在调用级设置 AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT,因此也可以用 .advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT) 全局开启。

② Schema 校验 + 自动重试(validateSchema()

模型返回的 JSON 不符合 Schema 时,校验失败信息会被追加回用户消息、让模型重新生成,最多重试 maxRepeatAttempts 次(默认 3):

ActorFilms actorFilms = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorFilms.class, spec -> spec
        .useProviderStructuredOutput()
        .validateSchema());

注意:validateSchema() 激活时不支持流式(校验需要完整响应)。其内部使用 StructuredOutputValidationAdvisor 实现。

两种模式对比:

维度默认 Prompt 指令模式Provider Native 模式
Schema 传递方式嵌入 Prompt 文本作为 API 参数传递
可靠性靠 LLM 自觉遵守模型 API 原生约束
适用性所有模型仅支持 StructuredOutputChatOptions 的模型
开启方式默认spec -> spec.useProviderStructuredOutput()

4.5 stream():流式响应

流式返回 Reactor 的 Flux,适合打字机效果的对话界面:

Flux<String> output = chatClient.prompt()
    .user("Tell me a joke")
    .stream()
    .content();          // Flux<String>

Flux<ChatResponse> responses = chatClient.prompt()
    .user("Tell me a joke")
    .stream()
    .chatResponse();     // 含元数据的完整流

流式 + 结构化输出目前没有 entity() 便捷方法,需借助 BeanOutputConverter 自行聚合:

var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<ActorsFilms>>() {});

Flux<String> flux = this.chatClient.prompt()
    .user(u -> u.text("Generate the filmography for a random actor. {format}")
            .param("format", this.converter.getFormat()))
    .stream()
    .content();

String content = flux.collectList().block().stream().collect(Collectors.joining());
List<ActorsFilms> actorFilms = converter.convert(content);

call() 与 stream() 对照:

终止方法call() 之后stream() 之后
纯文本String content()Flux<String> content()
完整响应ChatResponse chatResponse()Flux<ChatResponse> chatResponse()
含上下文ChatClientResponse chatClientResponse()Flux<ChatClientResponse> chatClientResponse()
结构化entity(...) / responseEntity(...)借助 BeanOutputConverter 聚合

5. 第四层:Prompt 模板与消息元数据

5.1 模板变量与 TemplateRenderer

不要用字符串拼接拼 Prompt。ChatClient 原生支持模板变量:

String answer = ChatClient.create(chatModel).prompt()
    .user(u -> u
            .text("Tell me the names of 5 movies whose soundtrack was composed by {composer}")
            .param("composer", "John Williams"))
    .call()
    .content();

内部由 TemplateRenderer 渲染,默认实现 StTemplateRenderer 基于开源 StringTemplate 引擎(另有 NoOpTemplateRenderer 用于完全跳过模板处理)。

架构师提示:如果你的 Prompt 里要内嵌 JSON(例如给模型输出格式示例),{} 分隔符会与 JSON 语法冲突。此时换用自定义分隔符:

String answer = ChatClient.create(chatModel).prompt()
    .user(u -> u
            .text("Tell me the names of 5 movies whose soundtrack was composed by <composer>")
            .param("composer", "John Williams"))
    .templateRenderer(StTemplateRenderer.builder()
            .startDelimiterToken('<').endDelimiterToken('>').build())
    .call()
    .content();

注意:.templateRenderer() 只作用于 ChatClient 链上直接定义的模板(.user() / .system()),不会影响 QuestionAnswerAdvisor 等 Advisor 内部使用的模板——它们有各自的模板定制机制。

5.2 消息元数据:给消息打标签

ChatClient 支持给 user / system 消息附加元数据,用于链路追踪、日志分析、下游处理:

String response = chatClient.prompt()
    .user(u -> u.text("What's the weather like?")
        .metadata("messageId", "msg-123")
        .metadata("userId", "user-456")
        .metadata("priority", "high"))
    .call()
    .content();

// 系统消息同理
String response2 = chatClient.prompt()
    .system(s -> s.text("You are a helpful assistant.")
        .metadata("version", "1.0")
        .metadata("model", "gpt-4"))
    .user("Tell me a joke")
    .call()
    .content();

元数据会体现在生成的 UserMessage / SystemMessagegetMetadata() 中,在自定义 Advisor 里读取消息元数据是常见的埋点手段。

校验规则(防御性设计,早失败优于静默丢失):key 不能为 null / 空,value 不能为 null,Map 中任何 null 元素都会抛 IllegalArgumentException


6. 第五层:Builder 默认值——配置一次,处处生效

在生产项目里,系统提示词、默认工具、RAG Advisor 不应散落在每个 Controller 里,而应在 ChatClient.Builder 层固化。Builder 提供全套 default* 方法:

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
            .defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).build())
            .defaultTools(myToolCallbacks)
            .defaultOptions(OpenAiChatOptions.builder().temperature(0.7).build())
            .build();
    }
}

默认配置 API 全景:

方法作用
defaultSystem(String/Resource/Consumer)默认系统提示词(可带模板占位符)
defaultUser(String/Resource/Consumer)默认用户消息
defaultOptions(ChatOptions)默认模型参数(temperature、model 等)
defaultTools(Object...)默认工具注册,每次请求都可用
defaultToolContext(Map)工具执行默认上下文
defaultTemplateRenderer(TemplateRenderer)默认模板渲染器
defaultAdvisors(Advisor...) / defaultAdvisors(Consumer<AdvisorSpec>)默认 Advisor 链

运行时按需覆盖同名的default 前缀方法即可——运行时配置优先,二者自动合并:

// 系统提示词中的占位符在运行时动态注入
Map<String, String> completion = chatClient.prompt()
    .system(sp -> sp.param("voice", voice))   // 覆盖 defaultSystem 的 {voice}
    .user(message)
    .call()
    .content();

mutate() 还能从已有 ChatClient(或一次请求配置)派生新客户端,继承全部默认设置再微调:

ChatClient.Builder derived = chatClient.mutate().defaultSystem("...").build();
ChatClient.Builder fromRequest = chatClient.prompt().user("...").mutate();

7. 第六层:Advisor——Spring AI 的灵魂

如果说 ChatClient 是门面,Advisor 就是 Spring AI 的拦截器(Interceptor)体系——类似 Spring MVC 的 HandlerInterceptor 或 Servlet 的 Filter。它是 RAG、对话记忆、工具调用、日志、安全护栏等一切横切能力的载体。

7.1 设计思想:拦截器体系

一次调用会经过一条getOrder() 排序的 Advisor 链,链尾是框架自动添加的"发请求给模型"的终结点。核心接口(org.springframework.ai.chat.client.advisor.api 包):

// 同步
public interface CallAdvisor extends Advisor {
    ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain);
}

// 响应式
public interface StreamAdvisor extends Advisor {
    Flux<ChatClientResponse> adviseStream(ChatClientRequest request, StreamAdvisorChain chain);
}

栈式执行模型(与 AOP 环绕通知一致):

  • 低 order 值(高优先级)先执行
  • 第一个处理请求的 Advisor,最后处理响应;
  • 链上的 ChatClientRequestChatClientResponse 都携带一个共享的 advise-context,用于跨 Advisor 传递状态(比如 RAG 检索到的文档);
  • Advisor 可以选择阻断请求(不调用 chain.next*()),此时它必须自行填充响应——这正是安全护栏类 Advisor 的拦截原理。
用户 Prompt
   │
   ▼
[Advisor 1  order=最低]  ← 最先处理请求
   │  ... 可修改 Prompt / 阻断请求
[Advisor 2]
   │
[ChatModelCallAdvisor]   ← 框架自动添加,真正调用 LLM
   │
   ▼  响应原路返回
[Advisor 2]  ← 后处理响应
[Advisor 1]  ← 最后处理响应

7.2 AdvisorSpec 配置与顺序语义

ChatClient 通过 AdvisorSpec 配置 Advisor:

interface AdvisorSpec {
    AdvisorSpec param(String k, Object v);
    AdvisorSpec params(Map<String, Object> p);
    AdvisorSpec advisors(Advisor... advisors);
    AdvisorSpec advisors(List<Advisor> advisors);
}

顺序就是语义——每个 Advisor 都会修改 Prompt 或上下文,修改结果传递给下一个。看官方示例:

ChatClient.builder(chatModel)
    .build()
    .prompt()
    .advisors(a -> a
        .advisors(
            MessageChatMemoryAdvisor.builder(chatMemory).build(),   // 先:注入对话历史
            QuestionAnswerAdvisor.builder(vectorStore).build()      // 后:基于历史做检索
        )
        .param(ChatMemory.CONVERSATION_ID, conversationId))         // 记忆必需的会话 ID
    .user(userText)
    .call()
    .content();

MessageChatMemoryAdvisor 先把历史拼进 Prompt,QuestionAnswerAdvisor 再基于"用户问题 + 历史"做向量检索——顺序反了,检索质量就会下降。

⚠️ 使用记忆类 Advisor 时,ChatMemory.CONVERSATION_ID 必须在每次调用通过 .param() 提供,否则运行期抛 IllegalArgumentException

7.3 内置 Advisor 一览

类别Advisor作用
记忆MessageChatMemoryAdvisor从记忆库取历史,作为消息列表并入 Prompt
记忆VectorStoreChatMemoryAdvisor从向量库检索历史,并入系统提示词
RAGQuestionAnswerAdvisor朴素 RAG:向量检索 + 上下文注入
RAGRetrievalAugmentationAdvisor模块化 RAG 架构的完整实现
推理ReReadingAdvisorRE2 技术:让模型重读问题提升推理
工具ToolCallingAdvisor工具调用循环(默认自动注册)
安全SafeGuardAdvisor防止生成有害/不当内容
日志SimpleLoggerAdvisor打印请求/响应,调试利器

日志 Advisor 推荐放在链尾,并配合配置开启 DEBUG:

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

也可以定制日志内容(控制敏感信息不外泄):

SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
    request -> "Custom request: " + request.prompt().getUserMessage(),
    response -> "Custom response: " + response.getResult(),
    0
);

7.4 自定义 Advisor

只需实现 CallAdvisor / StreamAdvisor(或 BaseAdvisor),用 before 修改请求、after 修改响应。下面是一个官方风格的 RE2(Re-Reading)推理增强 Advisor——它把用户输入改造成"问题 + 再读一遍":

public class ReReadingAdvisor implements BaseAdvisor {

    private static final String DEFAULT_RE2_ADVISE_TEMPLATE = """
            {re2_input_query}
            Read the question again: {re2_input_query}
            """;

    @Override
    public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
        String augmentedUserText = PromptTemplate.builder()
            .template(DEFAULT_RE2_ADVISE_TEMPLATE)
            .variables(Map.of("re2_input_query",
                chatClientRequest.prompt().getUserMessage().getText()))
            .build()
            .render();

        return chatClientRequest.mutate()
            .prompt(chatClientRequest.prompt().augmentUserMessage(augmentedUserText))
            .build();
    }

    @Override
    public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
        return chatClientResponse;
    }

    @Override
    public int getOrder() { return 0; }
}

最佳实践(官方文档提炼):

  1. 每个 Advisor 职责单一,保持模块化;
  2. advise-context 在 Advisor 间共享状态;
  3. 尽量同时实现流式与非流式两个版本;
  4. 仔细编排链上的 order,确保数据流正确;
  5. 需要"请求/响应两头都是第一个"的 Advisor 时,拆成两个 Advisor 并分别设置 order,用 context 传状态。

7.5 工具调用:ToolCallingAdvisor

ChatClient自动注册 ToolCallingAdvisor(默认 order 为 Ordered.HIGHEST_PRECEDENCE + 300),确保即使工具是在运行时由其他 Advisor 动态注入的,也能被正确处理:

String response = ChatClient.builder(chatModel)
    .build()
    .prompt("What day is tomorrow?")
    .tools(new DateTimeTools())   // ToolCallingAdvisor 自动注册
    .call()
    .content();

按需禁用:

# 全局禁用(所有调用):工具定义仍会发给模型,但模型发起的工具调用不再自动执行
spring.ai.chat.client.tool-calling.enabled=false
// 单次调用禁用(例如想自己驱动工具调用循环、逐轮转发给前端)
chatClient.prompt("What day is tomorrow?")
    .tools(new DateTimeTools())
    .advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
    .call()
    .content();

小知识ToolAdvisor 是一个标记接口——只要链上已存在实现了它的 Advisor,框架就不会再自动注册第二个 ToolCallingAdvisor。所以传入自定义 ToolAdvisor 也会自动抑制默认注册。


8. 第七层:聊天记忆 ChatMemory

模型 API 是无状态的——告诉模型你的名字,下一轮它就忘了。对话记忆必须由应用层维护。Spring AI 提供了 ChatMemory 接口与开箱实现 MessageWindowChatMemory

  • 维护固定大小消息窗口(默认 20 条),超出后淘汰旧消息;
  • 系统消息永远保留;新系统消息加入时会清掉旧系统消息;
  • 底层存储由 ChatMemoryRepository 抽象,提供多种实现:InMemoryChatMemoryRepositoryJdbcChatMemoryRepositoryCassandraChatMemoryRepositoryNeo4jChatMemoryRepositoryMongoChatMemoryRepositoryRedisChatMemoryRepository——从单机到分布式按需取用。

配合 MessageChatMemoryAdvisor 使用,即实现了"带记忆的多轮对话":

ChatClient.builder(chatModel)
    .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
    .build()
    .prompt()
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
    .user(userText)
    .call()
    .content();

9. 总结

从一行 chatClient.prompt().user(...).call().content() 到底层机制,ChatClient 体系经历了 8 层抽象

  1. 门面层ChatClient):声明式 Fluent API,开发者只关心"问什么、拿什么"
  2. 创建层ChatClient.Builder):prototype 作用域,自动配置 / 多模型 / 多端点三姿势
  3. 请求层prompt()):三种入口,统一组装 Prompt 的各个组成部分
  4. 响应层call() / stream()):惰性执行,五种返回值按需取用
  5. 结构化层entity() / EntityParamSpec):JSON Schema 的两种传递策略 + 校验重试
  6. 模板层TemplateRenderer):占位符渲染,可自定义分隔符
  7. 拦截层Advisor):栈式执行链,横切能力可插拔
  8. 记忆层ChatMemory):有界窗口 + 可插拔存储

关键设计思想

设计体现
门面模式ChatClient 统一屏蔽底层 ChatModel 差异
惰性执行call() 声明模式,content() 等终止方法才真正调用
Builder 模式prototype 作用域 + default* 默认配置,配置一次处处生效
拦截器模式Advisor 链栈式执行,横切能力与业务解耦
责任链每个 Advisor 修改 Prompt/上下文后传递给下一个
标记接口ToolAdvisor / MemoryAdvisor 抑制重复自动注册
策略模式EntityParamSpec 控制 JSON Schema 的传递策略

架构师速查

诉求用哪个能力
一次性问答prompt().user().call().content()
打字机效果stream().content() + WebFlux
解析模型输出为对象entity(Class) / entity(ParameterizedTypeReference)
计费审计 / 质量监控chatResponse() 拿 token 元数据
多轮对话MessageChatMemoryAdvisor + ChatMemory.CONVERSATION_ID
私有知识库问答QuestionAnswerAdvisor / RetrievalAugmentationAdvisor
让模型调用你的 APItools(...) + ToolCallingAdvisor
拦截 / 观测 / 增强自定义 CallAdvisor / StreamAdvisor

三条铁律

  1. 能用 Builder 层配置的,绝不放运行时——defaultSystem / defaultAdvisors / defaultOptions 是团队约定,运行时覆盖是例外;
  2. 多模型场景务必走 ChatClientBuilderConfigurer——否则可观测性和 Customizer 全部丢失,线上排查困难;
  3. 把 Advisor 顺序当业务逻辑对待——它是栈式执行,顺序错了,RAG 检索质量、记忆注入、工具调用都可能出错。

下次当你写下 .call().content() 时,希望你能想起:这条链上可能正跑着记忆注入、向量检索、敏感词守卫、工具调用循环——而它们,都只是这条 Advisor 链上按 order 排列的普通一环。


本文基于 Spring AI 2.0.0 官方参考文档(Chat Client API 与 Advisors API)整理,参考:docs.spring.io/spring-ai/r…