前言

如果你是一名 Java 后端工程师,过去这一年大概率遇到过这样的场景:产品说「我们要接入大模型」,于是你开始调研。Python 生态里 LangChain、LlamaIndex 风生水起,但你的团队是 Spring Boot 技术栈,引入 Python 意味着多语言运维、多份部署流水线、两套监控体系。你真正想要的,是把大模型当成一种「外部服务」,像接入数据库、消息队列一样,用熟悉的依赖注入、自动配置、Starter 体系把它接入进来。

Spring AI 就是为解决这个问题而生的。官方文档在 Introduction 里开宗明义地写到:Spring AI 项目的目标是简化 AI 功能的开发,避免不必要的复杂性(streamline the development of applications that incorporate artificial intelligence functionality without unnecessary complexity)。它借鉴了 LangChain 和 LlamaIndex 等知名 Python 项目的设计思路,但并不是它们的直接移植——官方文档特别强调,它诞生于一个信念:下一代生成式 AI 应用不会只属于 Python 开发者,而会普及到多种编程语言。

官方文档还点出了 Spring AI 要解决的根本挑战:把你的企业数据与 API 同 AI 模型连接起来(Connecting your enterprise Data and APIs with AI Models)。这句话看似平淡,实则概括了 AI 应用工程化的全部难点:模型本身是无状态的、封闭的,你的业务数据、私有 API、对话历史都在模型之外,如何把它们组织成模型能理解的输入,又如何把模型的输出结构化地接回你的系统。

这篇文章采用「官方文档带读」的方式,以当前稳定版(文档站点标注为 2.0.1 版本线)的 Spring AI Reference 为主线,逐段精读 Introduction、Getting Started 和 Chat Client API 三章中最核心的内容:ChatModel 与 ChatClient 两层抽象的设计差异、流式与非流式调用、Prompt 与消息角色、多模型统一抽象与 Starter 依赖、Spring Boot 自动配置,最后用一个可运行的对话服务端到端示例把知识串起来。所有 API 名称、配置项、代码示例均以官方文档为准,文档没有展开的细节我会显式标注。

值得系统学习它的理由还有一个:Spring AI 的 API 设计刻意向 Spring 开发者熟悉的形态靠拢——ChatClient 的流式 API 与 WebClient、RestClient 风格一致(idiomatically similar to the WebClient and RestClient APIs),自动配置遵循 Spring Boot 的惯例。学会它,你得到的不是一堆孤立的调用示例,而是一套可以长期演进的应用架构。

整体架构与核心概念

在读代码之前,先建立一张心智地图。Spring AI 官方文档把能力分成了几个清晰的层次,从下往上读会更清楚:

+---------------------------------------------------------------+
|                    你的应用代码(Controller / Service)          |
+---------------------------------------------------------------+
|  ChatClient(流式 Fluent API,面向用例)                        |
|    - prompt() / user() / system() / advisors()                |
|    - call() / stream() -> content() / entity() / chatResponse()|
+---------------------------------------------------------------+
|  Advisors(拦截与增强:记忆、RAG、日志、Tool Calling)          |
|    - MessageChatMemoryAdvisor / QuestionAnswerAdvisor / ...   |
+---------------------------------------------------------------+
|  ChatModel(模型交互的底层抽象,面向协议)                      |
|    - OpenAiChatModel / AnthropicChatModel / OllamaChatModel   |
+---------------------------------------------------------------+
|  Spring Boot Auto Configuration + Starters(装配层)           |
|    - ChatClient.Builder(prototype 作用域)自动装配            |
+---------------------------------------------------------------+
|  各 AI Provider(OpenAI / Anthropic / Ollama / ...)          |
+---------------------------------------------------------------+

这张图里有三个关键词需要吃透。

第一,可移植抽象(Portable API)。 官方文档反复强调:Spring AI 提供的抽象是开发 AI 应用的基础,这些抽象有多个实现,可以在最小化代码改动的前提下替换组件(enabling easy component swapping with minimal code changes)。对于 Chat、文生图、Embedding 模型,都提供同步和流式两种 API 选项,同时也保留了对模型厂商特有功能的访问能力。这就是 Spring 生态一贯的「面向接口编程」思路在 AI 领域的复刻。

第二,ChatModel 与 ChatClient 是两层不同的抽象。 ChatModel 是底层,代表「与一个具体聊天模型交互」的能力,例如 OpenAiChatModel、AnthropicChatModel;ChatClient 是上层,提供与 AI 模型通信的流式 API(fluent API),支持同步和流式两种编程模型。官方文档在 Chat Client API 一章开头定义得很清楚:ChatClient 拥有一组构建 Prompt 各个组成部分的方法,Prompt 作为输入传递给 AI 模型。选择哪一层,是后面主线章节要展开的第一个重要决策。

第三,横切能力走 Advisors。 官方文档把「调用模型时在 Prompt 上追加上下文数据」列为常见模式,这类上下文的典型类型包括你自己的数据(模型没训练过的私有数据)和对话历史(因为 chat model 的 API 是无状态的,你不把历史随请求发过去,模型就完全不记得你说过什么)。这些模式被封装成 Advisor,可以拦截、修改、增强每一次 AI 交互。Chat Memory(对话记忆)与 RAG(检索增强生成)都建立在这套机制之上。

除了这些,Introduction 还列举了 Spring AI 的完整能力清单,值得通读一遍建立全局观:跨厂商的可移植 Chat / 文生图 / Embedding API、Structured Outputs(把模型输出映射为 POJO)、全主流向量数据库支持(含一套 SQL 风格的元数据过滤 API)、Tools/Function Calling(允许模型请求执行客户端侧的工具)、可观测性(Observability)、文档摄取 ETL 框架、AI 模型评估、MCP(Model Context Protocol)集成,以及 ChatClient API 和 Advisors API 本身。如果你以后要做「对着文档问答」这类应用,官方文档明确说这套特性集就是为此准备的(implement common use cases, such as "Q&A over your documentation")。

你现在应该掌握了:Spring AI 的整体分层(应用 → ChatClient → Advisors → ChatModel → 自动配置 → 模型厂商)、可移植抽象的设计动机,以及「上下文注入走 Advisors」这条主线。

主线一:Spring AI 的定位与解决的问题

先把 Introduction 这页读透。官方文档给 Spring AI 下定义时有两句话值得逐字读。

第一句:「The project draws inspiration from notable Python projects, such as LangChain and LlamaIndex, but Spring AI is not a direct port of those projects.」这句话是在管理你的预期——不要期待 Spring AI 里有一个和 LangChain 的 Chain 一一对应的概念。它借鉴的是「把模型调用工程化」的思想,落地时完全按 Spring 的范式来做:Bean、自动配置、Builder、Fluent API。

第二句是问题定义:「Spring AI addresses the fundamental challenge of AI integration: Connecting your enterprise Data and APIs with AI Models.」注意这里是 Data 和 APIs 两个词。Data 对应 RAG、向量库、ETL 管线;APIs 对应 Tool/Function Calling,让模型可以反过来调用你暴露的客户端工具获取实时信息并采取行动(accessing necessary real-time information as required and taking action)。一个完整的 AI 应用,通常两条线都要走。

围绕这个定位,官方列出的特性可以按「模型侧 / 数据侧 / 工程侧」三类归档,方便记忆:

类别 官方特性 解决的问题
模型侧 跨厂商 Chat / Embedding / 文生图 / 语音转写 / TTS / 审核 API 换模型不改业务代码
模型侧 Structured Outputs、Tools/Function Calling 输出可用、模型可调用你的 API
数据侧 全主流向量库、SQL 式元数据过滤、ETL 框架、RAG 私有知识进上下文
工程侧 Chat Memory、Observability、模型评估、MCP、Boot 自动配置 记忆、监控、防幻觉、生态互通

官方文档还指出 ChatClient API 风格上与 WebClient、RestClient 一致。这一点不是修辞,而是设计承诺:你在 Spring 6 里怎么用 RestClient 发起一次类型安全的 HTTP 调用,在 Spring AI 里就怎么用 ChatClient 发起一次模型调用——Builder 注入、链式构建、响应类型转换,全部同构。

你现在应该掌握了:Spring AI 不是 LangChain 的 Java 版,而是「连接企业数据与 API 和 AI 模型」的工程化框架;它的能力清单可以按模型侧、数据侧、工程侧三类归档。

主线二:ChatModel 与 ChatClient,两层抽象的差异与选择

这是带读的核心章节。官方文档在 Chat Client API 一章开头的描述给出了选择依据:ChatClient 提供与 AI 模型通信的 fluent API,支持同步与流式两种编程模型;而 ChatModel 是自动配置和底层交互的基础。

两条路都走得通,差异在于心智负担和可维护性:

维度 直接用 ChatModel 用 ChatClient
API 形态 构造 Prompt 对象,手动组装 Message 流式构建:prompt().user().call().content()
默认配置 每次调用重复写 system、options builder 级 defaultSystem / defaultOptions 一次配置
横切能力 手动挂 Advisor advisor 链是一等公民
响应处理 拿到 ChatResponse 自己解析 content() / entity() / responseEntity() 直接出结果
典型场景 框架扩展、特殊协议细节控制 业务代码的常规模型调用

官方文档的立场很明确:绝大多数业务代码应该用 ChatClient。一个最简示例(来自官方文档,此处完整引用以便对照阅读):

@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();
    }
}

短短十几行里有三个官方想让你记住的点。其一,ChatClient 不是 new 出来的,而是通过 ChatClient.Builder 创建;在 Spring Boot 应用里,这个 Builder 是自动配置好的原型(prototype)作用域 Bean,注入到你的类里再 build()。其二,.user(userInput) 设置的是 user 消息的内容;其三,call() 发送请求,content() 把模型的响应以 String 返回。

注意一个容易误解的细节:官方文档明确说明,调用 call() 并不会真正触发模型执行,它只是告诉 Spring AI 使用同步还是流式调用;真正的模型调用发生在 content()、chatResponse()、responseEntity() 这类终结方法被调用时。同理,stream() 也只是声明意图。这是典型的惰性执行设计,理解了它,后面排查「为什么请求没有发出去」类的问题会快很多。

那什么时候需要回头操作 ChatModel?官方文档在「多个模型类型」一节给出了一种场景:当你用 ChatClient.builder(chatModel) 为不同厂商的模型手工创建 ChatClient Bean 时,实际上绕过了自动配置的 ChatClient.Builder,可观测性和 ChatClientBuilderCustomizer 会失效——除非你通过 ChatClientBuilderConfigurer 来构建。这说明 ChatModel 层更适合框架级定制,而业务代码保持在 ChatClient 层是官方推荐的默认姿势。

你现在应该掌握了:两层抽象的分工——ChatModel 面向协议与装配,ChatClient 面向用例与流式构建;call() 只是声明同步意图、终结方法才真正发起调用;业务代码优先用 ChatClient。

主线三:流式与非流式调用

官方文档对这两种模式的说法是:ChatClient 同时支持 synchronous 和 streaming 编程模型。同步调用前面已经见过,流式调用的官方示例如下:

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

stream() 与 call() 平级,之后可以接 content() 得到 Flux(模型生成的字符串流),也可以接 chatResponse() 得到 Flux(带元数据的响应流)。如果你想要实体对象,官方文档提示:未来会提供流式返回 Java 实体的便捷方法,目前应当使用 Structured Output Converter 显式转换聚合后的响应,官方给出的写法是用 BeanOutputConverter 配合流式输出,先把流聚合、再转换。

流式方案背后有一个重要的工程约束,官方文档在 Implementation Notes 一节讲得很直白:流式只通过 Reactive 栈支持(Streaming is only supported via the Reactive stack),命令式应用必须为此引入 Reactive 栈(例如 spring-boot-starter-webflux);反过来,非流式只通过 Servlet 栈支持,响应式应用必须引入 Servlet 栈(例如 spring-boot-starter-web)。听起来矛盾,但官方文档解释过这是这套 API 的独特之处——多数应用要么响应式要么命令式,不会两者兼有,而 ChatClient 把两种编程模型组合在了一起。

这意味着落地时你需要做一个架构决策:

场景 推荐做法
传统 Servlet 应用 + 同步接口 call() + content(),最简单
需要打字机效果的 SSE 接口 引入 WebFlux,stream() + Flux 返回
已全面 Reactive 注意部分调用仍是阻塞的(见下文)

还有一个官方特意提醒的坑:Tool Calling 是命令式的,会导致阻塞工作流(Tool calling is imperative, leading to blocking workflows),并且在 Micrometer 观测上会造成部分不完整的 span(ChatClient 的 span 与 tool calling 的 span 无法关联,前者保持未完成状态)。另外内置的 Advisor 在标准调用里执行阻塞操作、在流式调用里执行非阻塞操作;流式 Advisor 调用使用的 Reactor Scheduler 可以通过每个 Advisor 类上的 Builder 配置。

所以当你设计「流式输出 + 工具调用」的 Agent 时,要知道观测链路和阻塞模型都是特殊的,监控指标对不上时不要意外——以当前版本官方文档的说明为准。

你现在应该掌握了:stream() 返回 Flux,惰性执行同样适用;流式依赖 Reactive 栈、非流式依赖 Servlet 栈这一硬性约束;以及工具调用是阻塞式、会影响 Micrometer 观测这一实现细节。

主线四:Prompt 与消息角色

官方文档对 Prompt 的定义值得抄下来细读:「The Prompt contains the instructional text to guide the AI model's output and behavior. From the API point of view, prompts consist of a collection of messages.」API 视角下,Prompt 就是一批消息的集合;模型处理两类主要消息:来自用户的 user messages,和由系统生成、用于引导对话的 system messages。这些消息通常包含占位符,在运行期根据用户输入做替换,从而让模型的回应贴合具体输入。此外还有 Prompt options,比如指定使用的模型名称、控制输出随机性的 temperature。

把这段翻译成工程语言:一条模型请求 = 消息列表 + 选项。Spring AI 把这两件事都对象化了。消息角色上,官方文档点名的两类是 user 和 system(消息体系里还有 assistant 等角色,用于表达模型自己的历史输出,这是多轮对话记忆的基础,具体以当前版本官方文档的概念章节为准)。两者的分工是:system 定基调、定边界(「你是一个用海盗口音回答问题的助手」),user 承载具体输入。

Prompt Template 是把这套机制用活的钥匙。官方示例:

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();

文档进一步揭示了实现:ChatClient 内部使用 PromptTemplate 类处理 user 和 system 文本,基于某个 TemplateRenderer 实现在运行期替换变量。默认实现是 StTemplateRenderer——基于 Terence Parr 开发的开源 StringTemplate 引擎;如果不需要模板处理,Spring AI 还提供 NoOpTemplateRenderer。

一个实用细节:默认模板变量用 {} 语法标识。如果你的 Prompt 里要嵌入 JSON,花括号会和 JSON 语法冲突,官方给出的解法是通过 StTemplateRenderer.builder() 更换定界符,例如改成 < 和 >:

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();

另外要注意作用域:配置在 ChatClient 上的 TemplateRenderer(通过 .templateRenderer())只作用于构建链里直接定义的 prompt 内容(如 .user()、.system()),不影响 Advisor 内部使用的模板——比如 QuestionAnswerAdvisor 有自己独立的模板定制机制。这条边界划得很清楚,避免你误以为换了一个渲染器就能统一全部模板。

system 文本也可以带参数,做到「设计期写模板、运行期填参数」。官方示例在 Builder 上定义 defaultSystem("You are a friendly chat bot that answers question in the voice of the {voice}"),运行时在请求上 .system(sp -> sp.param("voice", voice)) 传入具体值,这样同一个 ChatClient 可以按请求切换人设,不必为每种人设建一个 Bean。

你现在应该掌握了:Prompt = 消息集合 + 选项;user/system 两类核心消息的角色分工;{} 占位符 + StTemplateRenderer 的默认模板机制、JSON 冲突时的定界符改法、以及 ChatClient 级渲染器不作用于 Advisor 内部模板这条边界。

主线五:多模型统一抽象与 Starter 依赖

「可移植」三个字最终要落在依赖管理上。官方 Getting Started 一章给出了清晰的路径。

首先是版本基线:Spring AI 2.0.x 支持 Spring Boot 4.0.x 和 4.1.x。正式版构件发布在 Maven Central,无需额外配置仓库。版本对齐建议用 BOM——spring-ai-bom 是一个纯 BOM 构件,只包含依赖管理,没有插件声明:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>2.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

如果要用快照版本(例如 2.0.0-SNAPSHOT),官方要求显式添加两个快照仓库:Spring 自己的 https://repo.spring.io/snapshot 和 Sonatype 中央门户快照仓库。官方还贴心提示了一个 Maven 坑:如果你的 settings.xml 用 mirrorOf=* 配置了公司镜像,会把快照仓库请求也劫持走,需要改成 *,!spring-snapshots,!central-portal-snapshots 排除掉 Spring 仓库。这类「公司镜像导致依赖拉不下来」的问题在中文社区问答里出现频率极高,根因就在这里。

其次是对厂商的抽象。官方 Chat Models 一节列出了所有聊天模型集成:Amazon Bedrock Converse、Anthropic、Azure OpenAI、DeepSeek、Google GenAI、Groq、Mistral AI、MiniMax、NVIDIA、Ollama、Perplexity AI、OCI Generative AI、OpenAI 等。它们共享同一套上层抽象,各自的接入方式是添加对应的 Starter 依赖。官方 Gradle 示例里点名的命名规则是 spring-ai-starter-model-openai(OpenAI)、依此类推 Anthropic、Ollama 等均为 spring-ai-starter-model-{provider} 形态。Starter 会带入模型客户端、自动配置和必要的选项绑定。

换个厂商是什么体验?理想情况下是:改一个依赖、改一组以 spring.ai 为前缀的配置项,业务代码里的 ChatClient 调用一行不动。这就是「abstractions have multiple implementations, enabling easy component swapping with minimal code changes」的完整闭环。官方也特别说明:除了可移植 API,同时保留了对模型特有功能的访问——你并不为抽象付出「只能用最小公分母」的代价。

真正需要多模型并存时,官方文档给了三条路线。其一,同一模型类型、不同配置的多个 ChatClient:自动配置的 ChatClient.Builder 是 prototype 作用域,每个注入点拿到新实例,直接定义多个 @Bean 即可:

@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();
    }
}

其二,不同模型类型并存:不要图省事用 ChatClient.create(chatModel) 或 ChatClient.builder(chatModel) 绕过自动配置——官方明确警告这会忽略可观测性和 ChatClientBuilderCustomizer。正确姿势是注入 ChatClientBuilderConfigurer(它会应用所有已注册的 Customizer 并接好观测),配合 @Primary 解决多个 ChatModel 候选的注入歧义。其三,多个 OpenAI 兼容端点共存:直接用 OpenAiChatModel.builder() 配不同 baseUrl(比如 Groq 和 OpenAI 官方各建一个实例),Builder 模式在这里很顺手。

你现在应该掌握了:BOM 管理版本、Maven Central 即发即用的仓库策略、快照仓库与公司镜像的冲突处理;spring-ai-starter-model-{provider} 的 Starter 命名规律;以及多模型场景下「prototype Builder 多 Bean」「ChatClientBuilderConfigurer 保观测」「OpenAiChatModel.builder 直连兼容端点」三条官方路线。

主线六:Spring Boot 自动配置

Spring AI 的自动配置遵循 Boot 惯例:引入 Starter 后,核心 Bean 由 spring.ai.* 配置属性驱动装配。对聊天场景而言,最重要的自动产物就是前面反复出现的 ChatClient.Builder——官方文档说得很具体:Spring Boot 自动配置为你创建一个 prototype 作用域的 ChatClient.Builder Bean,可以注入到任意类里再 build() 出自己的 ChatClient。

为什么强调 prototype?因为在 Builder 上做的每次定制(defaultSystem、defaultOptions、defaultAdvisors)都是构建时固化的,单例 Builder 会导致不同配置诉求互相污染;prototype 保证每个注入点拿到干净的新 Builder。理解了这一点,「为什么官方示例总是在构造函数里 chatClientBuilder.build()」就豁然开朗了。

配置项侧,各 Starter 对应一组 spring.ai.{provider}.* 属性。以 OpenAI 为例,spring.ai.openai.api-key 和 spring.ai.openai.chat.options.model 这类属性绑定到 OpenAiChatOptions,进而在 ChatModel 上生效。官方文档在 Builder 章节列出的 defaultOptions 方法说明中也印证了这一点:你可以传入 ChatOptions 中定义的可移植选项,或 OpenAiChatOptions 这类模型特有选项,二者都可以通过 Builder 设默认值。具体属性清单随版本演进,以当前版本官方文档的附录为准。

自动配置还覆盖了一条隐蔽但关键的链路:ToolCallingAdvisor 的自动注册。官方文档说明,ChatClient 默认总是在 Advisor 链中自动注册一个 ToolCallingAdvisor,除非显式关闭。对应的配置项是 spring.ai.chat.client.tool-calling.enabled(默认 true),以及控制其在链中位置的 spring.ai.chat.client.tool-calling.advisor-order(默认 Ordered.HIGHEST_PRECEDENCE + 300)。官方对 advisor-order 的调优建议很专业:这个值必须低于「需要在工具调用循环内部运行」的 Advisor 的顺序(即每次迭代都要重复执行),同时高于「只在循环外运行」的 Advisor(每个用户请求只执行一次)。要做深度定制(比如换 ToolCallingManager),可以声明一个 ToolCallingAdvisor.Builder<?> 类型的 Bean,自动配置上的 @ConditionalOnMissingBean 会让你的 Bean 优先。

再补一块自动配置拼图:Chat Memory。ChatMemory 接口代表对话记忆的存储,内置实现是 MessageWindowChatMemory——它维护一个不超过最大窗口(默认 20 条消息)的消息集合,超出时淘汰旧消息但保留 system 消息;若新 system 消息加入,则移除所有旧的 system 消息。它底层走 ChatMemoryRepository 抽象,官方列出的实现包括 InMemory、Jdbc、Cassandra、Neo4j、Mongo、Redis 等多种。使用时 ChatMemory.CONVERSATION_ID 必须每次通过 .param() 传入,否则运行期直接抛 IllegalArgumentException,官方明确说没有默认会话 ID——生产上这个参数常常和登录用户或会话标识绑定,千万别漏。

你现在应该掌握了:自动配置的核心产物是 prototype 作用域的 ChatClient.Builder;配置属性走 spring.ai.* 前缀绑定;ToolCallingAdvisor 自动注册及其两个关键配置项;MessageWindowChatMemory 的窗口淘汰规则和 CONVERSATION_ID 必填约束。

主线七:一个可运行的对话服务端到端示例

把前面六节串成一个最小但完整的工程。目标是:一个能记住对话历史、支持流式输出、通过配置切换模型的 REST 服务。

第一步,初始化与依赖。到 start.spring.io 选择需要的 AI Models(官方推荐入口),或手工加 BOM 加 Starter:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

第二步,配置文件 application.yaml:

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4
          temperature: 0.7

如果本地用 Ollama,把依赖换成 spring-ai-starter-model-ollama,配置换成 spring.ai.ollama 前缀的对应项即可,代码零改动——这正是统一抽象的价值所在。

第三步,装配带记忆和日志的 ChatClient。利用 Builder 的默认值能力,把 system 人设、记忆 Advisor、日志 Advisor 固化在 Bean 定义里:

@Configuration
class ChatConfig {
    @Bean
    ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
        return builder
                .defaultSystem("你是海盗助手,用海盗腔回答。")
                .defaultAdvisors(
                        MessageChatMemoryAdvisor.builder(chatMemory).build(),
                        new SimpleLoggerAdvisor())
                .build();
    }

    @Bean
    ChatMemory chatMemory() {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(new InMemoryChatMemoryRepository())
                .maxMessages(20)
                .build();
    }
}

第四步,Controller 提供同步与流式两个端点。同步端点复用默认记忆 Advisor,通过 param 传入会话 ID:

@RestController
class ChatController {
    private final ChatClient chatClient;

    ChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @GetMapping("/chat")
    Map<String, String> chat(@RequestParam String message,
                             @RequestParam String conversationId) {
        String reply = chatClient.prompt()
                .user(message)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
                .call()
                .content();
        return Map.of("reply", reply);
    }

    @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    Flux<String> stream(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .stream()
                .content();
    }
}

第五步,启动验证:

curl "http://localhost:8080/chat?message=我叫小明&conversationId=u-1"
curl "http://localhost:8080/chat?message=我叫什么&conversationId=u-1"
curl -N "http://localhost:8080/chat/stream?message=讲个笑话"

第二条请求能答出「小明」,说明记忆链路通了(Advisor 把历史拼进了 Prompt);第三条请求按 SSE 逐段吐字,说明流式链路通了。想看 Advisor 生效前后的 Prompt 细节,把 application.yaml 里 logging.level.org.springframework.ai.chat.client.advisor 调成 DEBUG,SimpleLoggerAdvisor 会打印请求与响应数据——官方同时提醒:生产环境小心日志里的敏感信息。

整条链路读下来,你会发现自己没有写任何 HTTP 客户端代码、没有手写 JSON、没有自己拼历史消息——全部沉到了抽象层以下。这,就是官方文档那句「streamline the development... without unnecessary complexity」的实证。

补充一个前面刻意留到这里的决策点:call() 之后到底选哪个终结方法。官方文档把可选项列得很全,核心对照如下:

终结方法 返回内容 适用场景
content() 纯文本 String 只要答案正文,最常见的默认选择
chatResponse() 带元数据的完整 ChatResponse,可含多个 Generation 需要 token 用量等计费与诊断信息
chatClientResponse() ChatResponse 加上 Advisor 执行上下文 需要读取 RAG 流程中检索出的相关文档
entity(Class) / entity(ParameterizedTypeReference) 映射好的 Java 实体或实体集合 结构化输出,如把结果直接落成 record
responseEntity(Class) 同时拿到完整 ChatResponse 与实体 既要元数据又要结构化数据的一次调用

其中 chatResponse() 值得多说一句:官方文档解释,ChatResponse 是模型返回的富结构,可以包含多个被称为 Generation 的响应,各自携带元数据;元数据里包括生成响应所用的 token 数量(文档特别提示,每个 token 大约是四分之三个单词),而托管模型按请求消耗的 token 计费——所以做成本管控时,chatResponse() 是唯一能让你拿到账单依据的入口。entity() 系列还有一个可组合的 EntityParamSpec 开关:validateSchema() 会按实体 schema 校验 JSON 响应并在失败时带错误反馈自动重试,useProviderStructuredOutput() 则把 schema 作为 API 级约束发给厂商而非塞进 Prompt 文本,两个开关可以独立也可以组合使用。

你现在应该掌握了:五个终结方法的选择依据、token 元数据与成本核算的关系,以及 EntityParamSpec 两个可靠性开关的语义。

你现在应该掌握了(主线七):从依赖、配置、Bean 装配到双端点验证的完整闭环,以及记忆链路与流式链路的验证方法。

踩坑经验

以下每组都来自官方文档的显式警告或隐含约束,是带读过程中最容易翻车的点。

第一组:绕过自动配置。

❌ 错误做法:需要多个模型时,直接用 ChatClient.create(chatModel) 或 ChatClient.builder(chatModel) 手工建 Bean。
✅ 正确做法:注入 ChatClientBuilderConfigurer 来构建自定义 Builder——官方文档明确说 configurer 会应用所有已注册的 ChatClientBuilderCustomizer 并接好可观测性,镜像自动配置的内部行为;多个 ChatModel 候选时再配合 @Primary 消除注入歧义。

第二组:误以为 call() 会立即发请求。

❌ 错误做法:在编排逻辑里调用 call() 之后立刻假设模型已经执行完毕,基于这个假设做副作用操作或并发控制。
✅ 正确做法:记住官方定义——call() 只声明同步/流式意图,真正的执行发生在 content()、chatResponse()、responseEntity() 等终结方法上;排查「请求没发出去」时先看终结方法有没有被调用。

第三组:Prompt 里嵌 JSON 用默认花括号定界符。

❌ 错误做法:在 .user() 文本里直接混入 JSON,同时用 {} 作为模板变量定界符,导致 JSON 被模板引擎误解析。
✅ 正确做法:官方给出的方案是用 StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>') 更换定界符(或者用 NoOpTemplateRenderer 关掉模板处理),从根上避开语法冲突。

第四组:流式场景缺少 Reactive 栈。

❌ 错误做法:在纯 Servlet(spring-boot-starter-web)应用里直接调 stream(),期望它能流式吐数据。
✅ 正确做法:官方 Implementation Notes 写明流式只由 Reactive 栈支持,命令式应用必须引入 Reactive 栈(如 spring-boot-starter-webflux);反之响应式应用引入 Servlet 栈并容忍部分阻塞调用。架构选型时就把这条栈依赖定下来。

第五组:记忆 Advisor 漏传会话 ID。

❌ 错误做法:配好了 MessageChatMemoryAdvisor,却忘了在请求上提供 ChatMemory.CONVERSATION_ID。
✅ 正确做法:官方明确写「Omitting it throws an IllegalArgumentException. There is no default conversation ID」——每次走记忆链路的调用都必须 .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)),建议在 Controller 层把会话 ID 做成必填参数。

第六组:生产环境裸开 DEBUG 日志。

❌ 错误做法:为了排查问题,把 org.springframework.ai.chat.client.advisor 的日志级别调成 DEBUG 后长期留在生产配置里。
✅ 正确做法:SimpleLoggerAdvisor 默认打印完整请求与响应数据,官方原话是「Be cautious about logging sensitive information in production environments」——调试期可以开,上线前要么关掉,要么用带自定义 requestToString/responseToString 的构造器做脱敏裁剪。

你现在应该掌握了:六组高频坑的触发条件与官方背书的标准解法。

最佳实践清单

综合全文,落地 Spring AI 时建议按这份清单自查:

  1. 版本基线对齐:Spring AI 2.0.x 配 Spring Boot 4.0.x / 4.1.x;版本管理一律用 spring-ai-bom,不写散版本号。
  2. 仓库策略:正式版只用 Maven Central;要用快照版再显式加 spring-snapshots 与 central-portal-snapshots,并检查公司 Maven 镜像的 mirrorOf 是否把快照仓库劫持了。
  3. 依赖最小化:按厂商选 spring-ai-starter-model-{provider},不要图省事把多个厂商 Starter 全引进来拖慢启动、增加攻击面。
  4. 分层纪律:业务代码只碰 ChatClient;只有做框架级定制时才下沉到 ChatModel,且下沉时走 ChatClientBuilderConfigurer 保住可观测性。
  5. 默认值收敛:人设(defaultSystem)、默认选项(defaultOptions)、默认工具(defaultTools)、默认 Advisor(defaultAdvisors)都固化在 Builder 上,运行时代码只传 user 输入和会话参数。
  6. 记忆即 Advisor:对话记忆走 MessageChatMemoryAdvisor + ChatMemoryRepository 组合,JDBC/Redis 等持久化实现按部署环境选择;CONVERSATION_ID 每次必传。
  7. 流式先定栈:动手写 stream() 之前先确认 Reactive 栈在依赖里,并和团队对齐「工具调用阻塞、观测 span 不完整」的实现现状。
  8. 模板早规划:Prompt 里嵌 JSON 的场景提前换定界符;记得 ChatClient 级 TemplateRenderer 管不到 Advisor 内部模板。
  9. 日志有边界:SimpleLoggerAdvisor 只进联调环境,生产要么移除要么脱敏;观测走 Micrometer 体系而不是靠裸日志。
  10. 配置即文档:spring.ai.* 属性与代码默认值的最终生效关系,以当前版本官方文档的属性附录为准,升级版本时逐条核对。

总结

  1. Spring AI 的定位是把企业数据与 API 同 AI 模型连接起来,它是 Spring 范式的 AI 工程框架,而不是 LangChain 的 Java 移植;学习它就是在学一套可长期演进的接入架构。
  2. 核心抽象分两层:ChatModel 面向协议,ChatClient 面向用例;业务代码默认用 ChatClient,call() 只声明同步意图、终结方法才真正触发模型调用。
  3. 流式与非流式不是简单的 API 开关,而是与 Reactive / Servlet 技术栈绑定的架构决策;工具调用是阻塞式的,并会影响 Micrometer 观测的完整性。
  4. Prompt = 消息集合 + 选项;system 定基调、user 载输入,StTemplateRenderer 提供 {} 占位符模板能力,JSON 场景需更换定界符。
  5. 多模型统一抽象靠 Starter 依赖 + spring.ai.* 配置 + prototype 作用域的 ChatClient.Builder 自动配置三者合力;多模型并存时记住 @Primary、ChatClientBuilderConfigurer 与 builder 直连三条路线。
  6. 记忆、RAG、日志、工具调用都是 Advisor 链上的节点;会话 ID 必传、生产日志脱敏、Advisor 顺序决定执行次序——这三条是官方文档里用警告语气写下的纪律。

参考资料

  • Spring AI Reference - Introduction(项目定位与特性总览):https://docs.spring.io/spring-ai/reference/index.html
  • Spring AI Reference - Chat Client API(本文核心带读章节):https://docs.spring.io/spring-ai/reference/api/chatclient.html
  • Spring AI Reference - Getting Started(仓库、BOM、依赖与版本基线):https://docs.spring.io/spring-ai/reference/getting-started.html
  • Spring AI 文档版本说明:本文撰写时文档站点稳定版标注为 2.0.1;文中未逐条展开的属性清单与实现细节,以当前版本官方文档为准。