Spring AI-参考-聊天客户端API
Spring AI-参考-聊天客户端API
Re-xy聊天客户端API
ChatClient 提供了一个流式 API(fluent API)用于与 AI 模型进行通信。它同时支持同步和流式两种编程模型。
请参阅本文档底部的“实现说明(https://docs.spring.io/spring-ai/reference/api/chatclient.html#_implementation_notes)”,其中涉及在 ChatClient 中结合使用命令式和响应式编程模型的相关内容。
这个流式 API 提供了一些方法,用于构建一个 Prompt(https://docs.spring.io/spring-ai/reference/api/prompt.html#_prompt)的各个组成部分,这个 Prompt 将作为输入传递给 AI 模型。Prompt 中包含了指导性文本,用以引导 AI 模型的输出和行为。从 API 的角度来看,提示是由一系列消息组成的。
AI 模型主要处理两种类型的消息:用户消息(user messages),即来自用户的直接输入;以及系统消息(system messages),由系统生成用以引导对话。
这些消息通常包含占位符,这些占位符会在运行时根据用户输入被替换,从而根据用户输入定制化 AI 模型的响应。
此外,还可以指定一些提示选项(Prompt options),例如要使用的 AI 模型的名称,以及用于控制生成输出的随机性或创造力的“温度”(temperature)设置。
核心组件:ChatClient
这个组件是用来和AI模型(例如OpenAI的GPT-4)聊天的接口。
两大编程模型:同步 vs. 流式
- 同步 (Synchronous):你问一个问题,然后一直等着,直到 AI 把完整的答案一次性全部给你。这就像下载一个文件,必须等它 100% 下载完才能打开。
- 流式 (Streaming):你问一个问题,AI 一边思考一边回答,把答案一个词一个词地吐给你。这就像看在线视频,视频内容是流式传输过来的,你不用等整个视频下载完就能开始看。ChatGPT 网站的打字机效果就是典型的流式响应。
核心交互方式:流式 API (Fluent API)
“Fluent API” 也常被称为“链式调用”。它的特点是你可以像链条一样把多个方法调用串在一起,让代码读起来像自然语言一样流畅。
以下写一个伪代码来方便理解:
1
2
3
4
5 String answer = chatClient.prompt()
.system("你是一个专业的诗人") // 先设置系统角色
.user("写一首关于春天的五言绝句") // 再提供用户问题
.call() // 发起调用
.content(); // 获取结果
- 核心输入结构:Prompt 和 Message
- Prompt(提示):可以理解为你要发送给 AI 的整个请求包裹。
- Message(消息):是这个包裹里的具体内容,可以有多条。
一个 Prompt 对象里,通常会包含一个或多个 Message 对象。
5. 两种主要消息类型:User vs. System
这是 Prompt Engineering 的核心概念:
- User Message (用户消息):就是你(用户)对 AI 说的话,比如“帮我翻译一下这句话”、“写一段代码”。
- System Message (系统消息):这是你给 AI 设定的**“人设”或“行为准则”**。它在对话开始前告诉 AI:“你接下来要扮演一个什么样的角色,遵守什么样的规则”。比如:
- “你是一个从不讲笑话的严肃历史学家。”
- “你的所有回答都必须使用中文,并且格式为 JSON。”
- 动态化:占位符 (Placeholders)
你可以在消息文本里预留一些“坑”(占位符),比如 Tell me a joke about {topic}.。在实际调用时,再把 {topic} 替换成具体的值,比如 “cats” 或者 “programmers”。这让你的提示可以被复用。- 精细控制:提示选项 (Prompt Options)
除了对话内容,你还可以设置一些“超参数”来微调 AI 的行为:
- model (模型名称):指定你要用哪个 AI 模型,比如 gpt-4o 还是 gpt-3.5-turbo。
- temperature (温度):这是一个非常重要的参数,控制 AI 回答的创造性或随机性。
- 低温 (比如 0.1):AI 的回答会非常确定、严谨、重复性高,适合需要事实和精确答案的场景。
- 高温 (比如 0.9):AI 的回答会更有创意、更天马行空、更多样化,适合头脑风暴、写故事等场景。
创建ChatClient
ChatClient 是使用 ChatClient.Builder 对象创建的。您可以为任何 ChatModel(https://docs.spring.io/spring-ai/reference/api/chatmodel.html) Spring Boot 自动配置获取自动配置的 ChatClient.Builder 实例,或者以编程方式创建一个实例。
使用自动配置的 ChatClient.Builder
在最简单的用例中, Spring AI 提供 Spring Boot 自动配置,创建一个原型 ChatClient.Builder bean 供你注入到你的类中。下面是检索对简单用户请求的 String 响应的简单示例。
1 |
|
在这个简单的示例中,用户输入设置用户消息的内容。call()方法向 AI 模型发送请求,content()方法将 AI 模型的响应作为 String 返回。
我现在来详细注释一下这个代码:
1 | // @RestController 是一个组合注解,它结合了 @Controller 和 @ResponseBody。 |
使用多个聊天模型
在以下几种情况下,您可能需要在单个应用程序中使用多个聊天模型:
- 对不同类型的任务使用不同的模型(例如,用于复杂推理的强大模型和用于简单任务的更快、更便宜的模型)
- 当一个模型服务不可用时实现回退机制
- A/B 测试不同的型号或配置
- 为用户提供基于其偏好的模型选择
- 组合专用模型(一个用于代码生成,另一个用于创意内容等)
默认情况下,Spring AI 会自动配置单个 ChatClient.Builder Bean。但是,您可能需要在应用程序中使用多个聊天模型。以下是处理此情况的方法:
在所有情况下,您都需要通过设置属性 spring.ai.chat.client.enabled=false 来禁用 ChatClient.Builder 自动配置。
这允许您手动创建多个 ChatClient 实例。
解读:
当默认的“一个应用对应一个 AI 模型”的简单模式不够用时,Spring AI 提供了灵活的手动配置方式来支持更复杂的“多模型”场景。
为什么要用多个模型?
原文列举了几个非常经典的理由:
成本与性能的权衡 (Cost vs. Performance):场景:做一个智能客服。对于简单的问候、查询订单等任务,用一个便宜快速的模型(如 GPT-3.5-Turbo)就够了。但如果用户提出复杂的投诉或需要深度分析的问题,就切换到更强大但更贵的模型(如 GPT-4o)。核心思想:好钢用在刀刃上,不同任务用不同工具。
高可用性与容错 (High Availability & Fault Tolerance):场景:你的主要 AI 服务商(比如 OpenAI)突然宕机了。为了不影响用户,系统可以自动切换到备用的服务商(比如 Anthropic 的 Claude 或 Google 的 Gemini)。核心思想:不要把所有鸡蛋放在一个篮子里。
实验与优化 (A/B Testing):场景:你不确定用模型 A 还是模型 B 的效果更好。你可以让 50% 的用户流量走模型 A,另外 50% 走模型 B,然后比较哪个的用户满意度更高或业务指标更好。核心思想:用数据说话,找到最优解。
用户自定义 (User Customization):场景:在你的应用里,像一个设置选项一样,让用户自己选择喜欢用“聪明但慢一点的 GPT-4o”还是“快一点但可能没那么聪明的 GPT-3.5”。核心思想:把选择权交给用户。
专业分工 (Specialization):场景:你的应用既能帮你写代码,又能帮你写诗。你可以配置一个专门为代码优化过的模型(如 codestral)来处理代码生成请求,同时用另一个擅长创意的模型(如 gpt-4o)来写诗。核心思想:让专业的人做专业的事。
如何实现?(The “How”)
理解了“为什么”之后,“怎么做”就成了关键。Spring AI 的解决方案非常直接:第一步:关闭默认的“傻瓜模式”
默认行为:Spring AI 默认会自动配置好一个 ChatClient.Builder,让用户开箱即用。这很方便,但也限制了用户只能有一个“默认”的客户端。
需要做的:在application.properties 或 application.yml 文件里,加上这行配置:
1 spring.ai.chat.client.enabled=false它的作用:这行配置等于告诉 Spring AI:“谢谢你的好意,但别再帮我自动创建那个默认的 ChatClient.Builder 了。接下来的事情,我自己来!”
第二步:自己动手,丰衣足食
一旦关闭了自动配置,你就获得了完全的自由。
你现在可以像创建任何普通的 Spring Bean 一样,在你的配置类 (@Configuration) 中,手动创建任意多个 ChatClient 实例。
每个实例都可以绑定到不同的底层 ChatModel(比如一个连 OpenAI,一个连 Ollama),或者绑定到同一个 ChatModel 但使用不同的默认设置(比如一个高温,一个低温)。总结
这段内容是 Spring AI 从“入门”到“进阶”的一个重要转折点。
入门模式:依赖自动配置,快速搞定单个模型的集成。
进阶模式:
- 自动配置 (spring.ai.chat.client.enabled=false)。
- 手动创建和配置多个 ChatClient Bean,以满足复杂的业务需求。
针对单一模型类型的多个 ChatClient
本节介绍一个常见的用例,即您需要创建多个 ChatClient 实例,这些实例都使用相同的底层模型类型,但具有不同的配置。
1 | // 以编程方式创建 ChatClient 实例 |
第一种方法:
核心目的: 快速、直接地创建一个基础的 ChatClient 实例,不包含任何预设的默认配置。
实现方式: 通过 ChatClient 接口的静态工厂方法 create(ChatModel model)。
最终产物: 一个“标准版”或“裸版”的 ChatClient。它是一个功能完备的客户端,但没有任何个性化设置。
适用场景: 当你的 AI 请求每次都完全不同,或者你倾向于在每次调用时都明确指定所有参数(如系统提示、模型选项等),不希望有任何隐藏的默认行为时。
第二种方法:
核心目的: 创建一个带有预设默认配置(例如默认系统提示、默认模型参数等)的、可复用的 ChatClient 实例。
实现方式: 采用建造者设计模式 (Builder Pattern)。
- ChatClient.builder(myChatModel): 启动构建过程,返回一个 Builder 对象。
- .defaultSystemPrompt(…): 在 Builder上进行链式调用,以声明所需的默认配置。
- .build(): 完成构建,生成最终的 ChatClient 实例。
最终产物: 一个“定制版”或“智能版”的 ChatClient。这个实例内部封装了你在构建时设定的默认值。
适用场景: 当你有一类相似的 AI 请求(例如,所有的客服对话、所有的代码翻译任务),它们总是需要共享相同的背景指令或参数时。使用这种方式可以极大地简化后续的调用代码,避免重复设置。
用于不同模型类型的 ChatClient
当使用多个 AI 模型时,您可以为每个模型定义单独的 ChatClient bean:
1 | import org.springframework.ai.chat.ChatClient; |
然后,您可以使用 @Qualifier 注解将这些 bean 注入到您的应用程序组件中:
1 |
|
一句话总结 @Qualifier(…):
它是一个“消除歧义”的注解。在有多个同类型 Bean 的情况下,通过指定 Bean 的名字,来精确地注入你想要的那一个。
来讲解一下这个步骤:
String response = chat.prompt(input).call().content();
第一步:chat.prompt(input)
例如我想问ai,今天的天气如何?
prompt 这个词,在 AI 领域,它的标准翻译是“提示”或“提示语”。它不仅仅是关键词,而是你希望 AI 对其作出回应的完整指令或上下文。
chat.prompt(input) 这段代码,其实是一个语法糖(shorthand),它等价于 chat.prompt().user(input)。
chat.prompt(): 动作是“准备一封新信件”(创建一个新的 Prompt 请求对象)。
.user(input): 动作是“在这封信的用户提问区,写上 input 的内容”。
所以,当你写 chat.prompt(“今天的天气如何?”) 时,你实际上是在创建一个请求,这个请求里包含了一条用户消息(User Message),内容就是 “今天的天气如何?”。第二步:.call()
call 的意思是**“调用”或“执行”。它是整个流程的“发送”按钮**。
它的作用:告诉 ChatClient:“我的信(Prompt)已经准备好了,现在请你把它发送给 AI 模型,然后在原地等待,直到你收到 AI 完整的回信。”
它的性质:这是一个同步阻塞(Synchronous & Blocking)的操作。
同步:意味着它会一次性返回完整的结果,而不是一点一点地返回。
阻塞:意味着你的程序代码在执行到 .call() 时会暂停,就像卡住了一样,直到网络另一头的 AI 模型把所有文字都生成完毕,并将结果传回你的程序,你的代码才会继续往下走。
它的返回值:.call() 执行完毕后,返回的不是一个简单的字符串,而是一个包含了所有响应信息的“包裹”,这个包裹的类型是 ChatResponse。这个包裹里除了有 AI 回答的文本内容,可能还有其他信息,比如这次调用消耗了多少 Token、调用是否成功、AI 决定停止回答的原因等等。第三步:.content()
content 的意思是“内容”。它是整个流程的“拆信封”动作。
它的作用:从上一步 .call() 返回的那个响应“包裹” (ChatResponse) 中,提取出我们最关心的核心部分——AI 生成的文本内容。
它的性质:这是一个简单的数据提取操作。
它的返回值:.content() 方法最终返回一个我们最熟悉的 java.lang.String 对象,这个字符串就是 AI 对你问题的回答,比如 “很抱歉,我无法获取实时的天气信息…”。
多个兼容 OpenAI 的 API 端点
OpenAiApi 和 OpenAiChatModel 类提供了一个 mutate() 方法,它允许您创建现有实例的、具有不同属性的变体。当您需要与多个兼容 OpenAI 的 API 协同工作时,这个功能特别有用。
1 |
|
这段代码解决了一个非常有趣的问题:现在市面上有很多新兴的 AI 服务(比如 Groq、Together AI、Perplexity 等),它们为了方便开发者迁移,都提供了兼容 OpenAI API 规范的接口。这意味着,你可以用和调用 OpenAI 完全相同的代码结构去调用它们,只需要更换 API 地址 (Base URL) 和 API 密钥 (API Key) 即可。
这段代码的核心就是展示如何利用 Spring AI 的 .mutate() 方法,优雅地实现这种动态切换。
核心概念:.mutate() 方法
- mutate() 的意思是“变异”或“派生”。它是一种“以我为模板,创建一个稍作修改的新副本”的设计模式(类似于原型模式)。
- 当你调用 someObject.mutate() 时,它会返回一个 Builder 对象,这个 Builder 的初始状态完全继承了 someObject 的所有属性。
- 然后,你可以对这个 Builder 进行局部修改,最后调用 .build() 来生成一个全新的、修改后的实例,而原始的 someObject 保持不变。
ChatClient 流式 API
ChatClient 流式 API 允许您通过一个重载的 prompt 方法,以三种不同的方式来创建一个提示(Prompt),从而启动流式 API 的调用链:
- prompt(): 这个无参数的方法让您可以开始使用流式 API,允许您逐步构建用户消息、系统消息以及提示的其他部分。
- prompt(): 完全自定义点餐
代码:chatClient.prompt()
比喻:你走到点餐台前,对服务员说:“我要开始点餐了”。然后你开始一步步地告诉他:“我要一个汉堡(.user()),不要酸黄瓜,加双份芝士(各种 .option()),再来一杯可乐(另一个 .user() 或工具调用),对了,记得提醒厨师我是你们的超级会员,口味要重一点(.system())。”
使用场景:这是最灵活、最常用的方式。你需要从零开始构建一个复杂的请求,包含系统消息、用户消息、各种选项等。链式调用的所有功能都可以从这个起点开始。
1
2
3
4 >chatClient.prompt() // <--- 起点
.system("你是一个专业的翻译家")
.user("请把 'Hello, World!' 翻译成法语")
.call().content();
- prompt(Prompt prompt): 这个方法接受一个 Prompt 对象作为参数,让您可以传入一个已经通过 Prompt 类的非流式 API 创建好的 Prompt 实例。
- prompt(Prompt prompt): 拿着写好的菜单直接点餐
代码:chatClient.prompt(myPrompt)
比喻:你在来餐厅的路上,就已经用 App 提前把你的菜单(一个 Prompt 对象)编辑好了。到了餐厅,你直接把手机上的菜单二维码给服务员一扫,说:“就照这个单子做!”
使用场景:当你因为某些原因(比如代码解耦、从其他模块传来等)已经有了一个构建好的、完整的 Prompt 对象时,用这个方法可以最直接地把它交给 ChatClient。
1
2
3
4
5
6 >// 在别的地方,你已经费心构建好了一个 Prompt 对象
>Prompt myDetailedPrompt = new Prompt(...);
>// 现在直接把它扔给 ChatClient
>chatClient.prompt(myDetailedPrompt) // <--- 起点
.call().content();
- prompt(String content): 这是一个类似于上一个重载方法的便捷方法。它直接接受用户的文本内容。
代码:chatClient.prompt(“你好吗?”)
比喻:你赶时间,直接对服务员喊:“来一份最简单的单人套餐!”(这里的套餐内容就是你提供的那段字符串)。
使用场景:这是最简单、最便捷的方式,专门用于那种“只有一个用户问题,没有其他任何要求”的场景。它在内部会自动帮你把这个字符串包装成一个只包含单条用户消息的 Prompt 对象。
1
2
3 // 背后相当于 chatClient.prompt().user("你好吗?")
chatClient.prompt("你好吗?") // <--- 起点
.call().content();
ChatClient 响应 (ChatClient Responses)
ChatClient API 通过其流式 API 提供了多种方式来格式化来自 AI 模型的响应。
解读:当用户调用 AI 模型后(通过 .call() 或 .stream()),ChatClient 不仅仅是给用户一个简单的字符串。它提供了一套丰富的“工具”,让用户能以不同的形式提取和转换 AI 的响应结果。
返回一个 ChatResponse
来自 AI 模型的响应是一个由 ChatResponse 类型定义的丰富结构。它包含了关于响应是如何生成的元数据 (metadata),并且还可以包含多个被称为 Generations 的响应结果,每个结果都有其自己的元数据。这些元数据包含了用于创建响应的令牌(token)数量(每个 token 大约相当于 3/4 个单词)。这个信息很重要,因为托管的 AI 模型是根据每次请求使用的 token 数量来收费的。
下面展示了一个示例,通过在 call() 方法后调用 chatResponse() 来返回包含元数据的 ChatResponse 对象。
1 | ChatResponse chatResponse = chatClient.prompt() |
解读:AI 的响应远不止一串文本那么简单。它是一个包含丰富“附加信息”的复杂对象,而 .chatResponse() 方法就是获取这个完整对象的钥匙。
方法 返回类型 用途 比喻 .content() String 只想要核心文本内容,简单快捷 只看电影正片 .chatResponse() ChatResponse 想要全部,包括元数据、Token 消耗等 看蓝光典藏版影碟
返回一个实体 (Entity)
您通常希望返回一个从返回的字符串映射而来的实体类。entity() 方法提供了此功能。
例如,给定以下 Java record:
1 | record ActorFilms(String actor, List<String> movies) {} |
您可以使用 entity() 方法轻松地将 AI 模型的输出映射到这个 record,如下所示:
1 | ActorFilms actorFilms = chatClient.prompt() |
此外,还有一个重载的 entity 方法,其签名为 entity(ParameterizedTypeReference
1 | List<ActorFilms> actorFilms = chatClient.prompt() |
解读:
核心就是:不要再手动解析 AI 返回的 JSON 字符串了!Spring AI 可以自动帮你把 AI 的文本回答直接转换成你想要的 Java 对象。
比喻:手动翻译 vs. 同声传译
- 传统方式 (手动解析):
- 你告诉 AI:“请用 JSON 格式返回演员汤姆·汉克斯的电影列表。”
- AI 返回给你一长串文本:”{"actor":"Tom Hanks","movies":["Forrest Gump","Saving Private Ryan"]}”
- 你拿到这个字符串后,需要自己找一个 JSON 解析库(比如 Jackson 或 Gson),写一堆代码(objectMapper.readValue(…))来把它手动转换成你的 ActorFilms Java 对象。
- 这就像:你听到一段外语,需要先用笔把它逐字记下来,然后再查字典一个词一个词地翻译。过程繁琐且容易出错。
- Spring AI 的 .entity() 方式 (结构化输出):
- 你用同样的方式告诉 AI 你想要什么。
- 然后你直接在调用链的末尾加上 .entity(ActorFilms.class)。
- Spring AI 在背后默默地帮你完成了所有工作:
- 它会优化你的 prompt,确保 AI 更大概率返回正确的 JSON 格式。
- 它拿到 AI 返回的文本。
- 它自动调用内置的 JSON 解析器,将文本转换成你指定的 ActorFilms.class 实例。
- 你直接就得到了一个类型安全、立即可用的 Java 对象!
- 这就像:你戴上了一副同声传译耳机。你听到外语的同时,耳机里已经传来了翻译好的母语。过程无缝、高效、无需动手。
两种 entity() 方法的用法
- entity(Class
type): 用于简单的、非泛型的对象
- 代码: .entity(ActorFilms.class)
何时使用: 当你期望的结果是一个单一的、具体的类的实例时,比如 ActorFilms、User、Product。
示例:
1
2
3
4
5
6
7
8
9
10
11 // 定义一个简单的 Java 对象 (用 record 更简洁)
record ActorFilms(String actor, List<String> movies) {}
// 发起调用并直接获取对象
ActorFilms actorFilms = chatClient.prompt()
.user("为随机一位演员生成电影作品列表。")
.call()
.entity(ActorFilms.class); // <-- 直接指定类
// 现在可以直接使用 actorFilms 对象了
System.out.println("演员: " + actorFilms.actor());
- entity(ParameterizedTypeReference
type): 用于复杂的、带泛型的对象
- 代码: .entity(new ParameterizedTypeReference<List
>() {})
为何需要这么复杂? 因为 Java 的泛型有类型擦除的特性。在运行时,List
和 List 都会被“擦除”成同一个 List.class。只传入 List.class,程序就不知道列表里面应该装什么类型的对象。ParameterizedTypeReference 是 Spring 提供的一种“技巧”,它能在运行时保留并传递完整的泛型信息(“我想要的不仅是个 List,而是个装满了 ActorFilms 的 List”)。 何时使用: 当你期望的结果是一个泛型集合时,最常见的就是 List
、Map<K, V> 等。 示例:
1
2
3
4
5
6
7
8
9
10 // 目标是获取一个 ActorFilms 对象的列表
List<ActorFilms> actorFilmsList = chatClient.prompt()
.user("为汤姆·汉克斯和比尔·默里各生成5部电影的作品列表。")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {}); // <-- 保留泛型信息
// 现在可以直接遍历这个列表了
for (ActorFilms af : actorFilmsList) {
System.out.println("演员: " + af.actor());
}
方法 用途 关键点 .entity(YourClass.class) 将 AI 响应映射到单个普通 Java 对象。 简单直接,适用于非泛型类。 .entity(new ParameterizedTypeReference<…>() {}) 将 AI 响应映射到泛型集合(如 List)。 解决了 Java 泛型擦除问题,必须用这种写法。 .entity() 是 Spring AI 的“杀手级”功能之一。它将大语言模型的非结构化文本输出,与 Java 的强类型、结构化编程世界无缝地连接了起来,是构建健壮、可靠 AI 应用的基石。
流式响应 (Streaming Responses)
stream() 方法让您能够获得一个异步的响应,如下所示:
1 | Flux<String> output = chatClient.prompt() |
您也可以使用 Flux
未来,我们将提供一个便捷的方法,让您可以通过响应式的 stream() 方法直接返回一个 Java 实体。在此期间,您应该使用结构化输出转换器 (Structured Output Converter) 来显式地转换聚合后的响应,如下所示。这也演示了在流式 API 中使用参数的方法,这将在文档的后续部分进行更详细的讨论。
1 | // 创建一个 BeanOutputConverter,指定期望的输出类型 |
解读:
这段话的核心是告诉你如何处理像 ChatGPT 网站上那种“一个词一个词往外蹦”的响应效果,并点出了当前在流式场景下获取结构化数据(Java 对象)的临时解决方案。
- 什么是流式响应? (stream())
- .call() (同步):你问问题,然后一直等,直到 AI 把完整的一大段话说完,你才收到所有内容。
- .stream() (流式/异步):你问问题,AI 一边想一边说。它每想出一个词或一小段话,就立刻发给你。你收到的是一个持续不断的数据流,而不是一次性的完整结果。
- 返回类型 Flux
:stream() 方法返回的是一个 Flux 对象。Flux 是 Project Reactor (Spring 的响应式编程库) 中的一个核心类型,代表一个可以发出 0 到 N 个元素的异步序列(数据流)。
简单的流式文本 (.stream().content())
1
2
3
4 Flux<String> output = chatClient.prompt()
.user("Tell me a joke")
.stream() // <-- 关键:从这里开始,一切都是异步的、流式的
.content(); // <-- 从流中的每个小块(Chunk)里提取文本内容
- 工作流程:
- .stream() 发起请求,但不会阻塞,而是立即返回一个 Flux
。
- AI 模型开始生成响应,比如 “Why”, “ did”, “ the”, “ scarecrow”, “ win”, “ an”, “ award?”
- Flux 会依次发出这些字符串片段。
- 你可以“订阅”(subscribe) 这个 Flux 来处理每一个到来的片段,例如在界面上实时显示它们,实现打字机效果。
流式场景下的结构化输出 (一个当前的“权宜之计”)
这是本段最复杂也最重要的部分。
问题所在: .entity() 这个便捷的方法在 stream() 模式下尚不可用(原文说“未来会提供”)。你不能直接 .stream().entity(…)。因为流是一个个碎片,你无法在收到第一个碎片时就把它转换成一个完整的 Java 对象。
当前的解决方案: 分三步走:
准备工作:创建转换器并修改 Prompt
-var converter = new BeanOutputConverter<>(…):手动创建一个“转换器”,告诉它你最终想要什么类型的 Java 对象(比如 List
)。 .param(“format”, this.converter.getFormat()):最关键的一步!这个转换器内部有一个 getFormat() 方法,它能生成一段特殊的指令文本,告诉 AI “请务必以我指定的 JSON 格式返回你的答案”。你必须把这个指令作为参数塞进你的 prompt 里。
执行与收集:发起流式请求,并把所有碎片重新拼起来
chatClient.prompt(…).stream().content():像之前一样,获取一个包含 JSON 字符串碎片的 Flux。
flux.collectList().block()…:这是一个响应式编程的操作。
- collectList(): “把流里的所有碎片都收集起来,放到一个列表里。”
- .block(): “一直等,直到所有碎片都收集完毕。”(这里把异步操作又变回了同步等待,因为你需要完整的 JSON 才能解析)。
- .stream().collect(Collectors.joining()): 把列表里的所有字符串碎片无缝拼接成一个完整的 JSON 字符串。
- 手动转换:使用转换器完成最后一步
- List
actorFilms = this.converter.convert(this.content); - 现在你有了完整的 JSON 字符串 content,最后调用转换器的 convert 方法,手动完成从字符串到 Java 对象的转换。
场景 方法链 解释 获取流式文本 .stream().content() 简单直接,获取 Flux ,用于实现打字机效果。 获取流式结构化对象 (当前) (复杂三步法) 1. 创建 Converter 并修改 Prompt。
2. .stream() 后收集并拼接成完整字符串。
3. 手动调用 converter.convert()。虽然 Spring AI 的目标是让一切都简单,但在某些功能(如流式结构化输出)尚未完全成熟时,它也为你提供了底层的、稍微复杂一些的手动工具 (BeanOutputConverter) 作为过渡方案。
提示模板 (Prompt Templates)
前情提要:
什么是提示模板?为什么需要它?
没有模板 (硬编码):
“Tell me a joke about a cat.”
如果你想问关于狗的笑话,你得重写整个字符串。使用模板:
“Tell me a joke about a {topic}.”
这是一个可复用的模板。你可以保持模板不变,只在运行时改变 {topic} 的值(比如 “cat”, “dog”, “programmer”),就能生成不同的提示。
ChatClient 流式 API 允许您将用户和系统的文本作为带有变量的模板来提供,这些变量会在运行时被替换。
这段话的核心是告诉你:不要把你的提示写死!Spring AI 内置了一个强大的模板引擎,让你可以在提示中预留“坑位”(占位符),然后在运行时动态地填入内容。
1 | String answer = ChatClient.create(chatModel).prompt() |
.text(): 定义模板字符串。
.param(“key”, “value”): 为模板中名为 key 的占位符填充 value。在发送给 AI 之前,Spring AI 会自动把它们拼接成最终的提示:”Tell me the names of 5 movies whose soundtrack was composed by John Williams”。
在内部,ChatClient 使用 PromptTemplate 类来处理用户和系统的文本,并依赖一个给定的 TemplateRenderer 实现来用运行时提供的值替换变量。默认情况下,Spring AI 使用 StTemplateRenderer 实现,它基于由 Terence Parr 开发的开源 StringTemplate 引擎。
- PromptTemplate: Spring AI 内部用来表示一个“模板化提示”的对象。
- TemplateRenderer: 这是**“渲染器”或“模板引擎”**的接口。它的工作就是接收一个带占位符的模板和一堆参数,然后输出最终的、被填充好的字符串。
- StTemplateRenderer: 这是 Spring AI 默认的渲染器实现。它背后使用的是一个叫 StringTemplate 的成熟、强大的开源库。
默认情况下,无需关心这些内部实现,Spring AI 会自动用 StringTemplate 帮你处理 {…} 格式的占位符。
Spring AI 也提供了一个 NoOpTemplateRenderer,用于不需要任何模板处理的情况。
直接在 ChatClient 上配置的 TemplateRenderer (通过 .templateRenderer()) 仅适用于直接在 ChatClient 构建器链中定义的提示内容(例如,通过 .user(), .system())。它不影响像 QuestionAnswerAdvisor 这样的“顾问”(Advisors) 内部使用的模板,这些顾问有它们自己的模板定制机制(请参阅“自定义顾问模板”)。
如果您倾向于使用不同的模板引擎,您可以直接向 ChatClient 提供一个自定义的 TemplateRenderer 接口实现。您也可以继续使用默认的 StTemplateRenderer,但使用自定义的配置。
这里提到了两种常见的定制需求:
- a) 根本不想要模板功能
**场景:**你的提示内容本身就包含 {},比如你正在向 AI 提问关于 Java 泛型或 JSON 的问题。默认的模板引擎可能会误以为这些是占位符,从而导致错误。
**解决方案:**使用 NoOpTemplateRenderer。NoOp 是 “No Operation” 的缩写,意思是“啥也别干”。这个渲染器会原封不动地返回你的文本,不做任何替换。
- b) 更换占位符的分隔符
**场景:**和上面类似,为了避免 {} 与你的提示内容(特别是 JSON)冲突,你想换一种更安全的分隔符,比如 <…>。
**解决方案:**你可以不更换整个引擎,而是重新配置默认的 StTemplateRenderer。
1
2
3
4
5
6 .templateRenderer( // 告诉 ChatClient:“别用默认的渲染器了,用我这个定制版的!”
StTemplateRenderer.builder() // 获取一个 StTemplateRenderer 的建造者
.startDelimiterToken('<') // 把开始符号从 '{' 改成 '<'
.endDelimiterToken('>') // 把结束符号从 '}' 改成 '>'
.build() // 创建这个定制版的渲染器实例
)重要提示: 当你这么做之后,你的模板文本也必须同步修改,把 { composer } 改成 < composer >。
.templateRenderer() 仅适用于直接在 ChatClient 构建器链中定义的提示内容…
**这句话的意思是:**你在一次调用中通过 .templateRenderer() 设置的渲染器,只对这一次调用生效。它是一个局部的、一次性的配置。
它不会影响更高级的、封装了自身逻辑的组件(比如 QuestionAnswerAdvisor,即 RAG 的核心组件)。那些组件有自己独立的模板配置方式,因为它们的模板通常更复杂。
例如,默认情况下,模板变量是通过 {} 语法来识别的。如果您计划在提示中包含 JSON,您可能想要使用不同的语法以避免与 JSON 语法冲突。例如,您可以使用 < 和 > 作为定界符。
1 | String answer = ChatClient.create(chatModel).prompt() |
小总结:
基础用法: 使用 {} 作为占位符,并通过 .param() 方法传值,实现动态提示。
核心原理: 背后由 StringTemplate 引擎驱动。
高级定制:
可以通过 .templateRenderer() 在单次调用中更换渲染器。
可以换成 NoOpTemplateRenderer 来禁用模板功能。
可以重新配置默认的 StTemplateRenderer 来更换分隔符(比如 <…>),以避免与 JSON 等语法冲突。
.call() 的返回值(call() return values)
在 ChatClient 上指定 call() 方法后,对于响应类型,有几种不同的选项。
这段话的核心是:在发起同步调用 .call() 之后,Spring AI 为你提供了多条“取货通道”,你可以根据自己的需求,选择最合适的那一条来获取 AI 的响应。
我们用一个“去餐厅吃饭”的比喻来梳理这些选项,假设你已经点完餐(.prompt())并告诉服务员“可以上菜了”(.call())。现在,你要决定如何“享用”这顿饭。
String content(): 返回响应的 String 内容。
content():只吃主菜
比喻:你对服务员说:“别的都别管,直接把主菜(比如牛排)端上来就行!”
返回:String
用途:最简单、最常用。当你只关心 AI 回答的核心文本内容时,用这个就够了。ChatResponse chatResponse(): 返回 ChatResponse 对象,该对象包含多个生成结果(Generations)以及关于响应的元数据,例如用于创建该响应的 token 数量。
chatResponse(): 不仅吃主菜,还要看菜单和账单
比喻:你对服务员说:“把主菜端上来,顺便把详细的菜单(包含成分说明)和本次消费的账单(Token 使用量)也一起拿来。”
返回:ChatResponse
用途:当你需要监控成本(看 token)、调试问题(看元数据)、或处理 AI 返回的多个候选答案时使用。ChatClientResponse chatClientResponse(): 返回一个 ChatClientResponse 对象,该对象包含 ChatResponse 对象和 ChatClient 的执行上下文,让您能够访问在“顾问”(Advisors) 执行期间使用的附加数据(例如,在 RAG 流程中检索到的相关文档)。
chatClientResponse(): 不仅要菜和账单,还要知道厨师是谁、采购员从哪买的菜
比喻:你对服务员说:“除了菜和账单,我还要知道这道菜是哪个厨师团队(Advisor)做的,他们为了做这道菜,特意去哪个市场采购了哪些新鲜食材(RAG 检索到的文档)。”
返回:ChatClientResponse
用途:这是一个非常高级的选项。当你使用了更复杂的 Spring AI 组件,比如 QuestionAnswerAdvisor (RAG的核心),并且你需要获取这些组件在执行过程中的内部数据(比如 RAG 到底引用了哪些文档来生成答案)时,才会用到它。普通用例几乎接触不到。entity() 用于返回一个 Java 类型:
entity(ParameterizedTypeReference
type): 用于返回一个实体类型的集合(Collection)。
entity(Classtype): 用于返回一个特定的实体类型。
entity(StructuredOutputConverterstructuredOutputConverter): 用于指定一个 StructuredOutputConverter 的实例,以将字符串转换为实体类型。 entity(…): 让服务员把菜按我要求摆盘
这是结构化输出的功能,它有几种不同的“摆盘”方式:entity(ParameterizedTypeReference< T > type): 按家庭分享餐摆盘
比喻:“请把菜摆成一个‘家庭分享餐’(List< ActorFilms >)的样子,里面要有多份单人套餐。”
用途:将 AI 响应转换成一个泛型集合,比如 List< YourClass >。entity(Class< T > type): 按单人套餐摆盘
比喻:“请把菜直接摆成一个‘单人套餐’(ActorFilms 对象)的样子给我。”
用途:将 AI 响应直接转换成一个普通的 Java 对象。entity(StructuredOutputConverter< T > structuredOutputConverter): 按我自带的图纸摆盘
比喻:“别用你们餐厅的盘子了,用我自带的这个设计图纸(StructuredOutputConverter 实例)来摆盘。”
用途:这是一个更底层的定制选项。当你有一个自己创建或配置的转换器实例时,可以用它来指导转换过程。这在使用流式API处理结构化输出时见过。
您也可以调用 stream() 方法来替代 call()。
最后,别忘了,所有这些都是在 .call()(同步)之后使用的。如果你想用异步流式的方式,那就得把 .call() 换成 .stream(),然后使用流式 API 提供的对应方法。
.stream() 的返回值(stream() return values)
在 ChatClient 上指定 stream() 方法后,对于响应类型,有几个选项:
.stream() 方法把 .call() 的所有返回类型都装进了一个叫 Flux 的“异步管道”里。你不再是一次性收到一个完整的结果,而是会通过这个管道,持续不断地收到一个个数据块。
我们继续用“去餐厅吃饭”的比喻,但这次,餐厅的服务模式升级了,变成了“回转寿司”。你坐在传送带旁边,菜品会一片一片、一盘一盘地传送到你面前。
Flux< String > content(): 返回一个 Flux,该 Flux 会持续发出由 AI 模型正在生成的字符串片段。
content(): 只拿传送带上的寿司肉
返回: Flux< String >
比喻: 传送带上源源不断地传来一小片一小片的生鱼片(字符串片段)。你只关心吃肉,所以你只拿这些鱼片。比如,AI 的回答是 “Hello World”,传送带可能会依次传来 “He”, “llo”, “ Wo”, “rld”。
用途: 实现打字机效果。这是最常见的流式用例。你“订阅”(subscribe) 这个 Flux< String >,每当一小段文本传来,你就在界面上把它追加显示出来。Flux< ChatResponse > chatResponse(): 返回一个 Flux,该 Flux 会发出 ChatResponse 对象,其中包含了关于响应的附加元数据。
chatResponse(): 传送带上不仅有寿司,还有带说明的小盘子
返回: Flux< ChatResponse >
比喻: 传送带上传来的不再是裸露的鱼片,而是一小盘一小盘的寿司。每个盘子(ChatResponse)上不仅有寿司(文本片段),还可能贴着标签,写着“这是今天第一批金枪鱼”、“这批的 token 消耗是 5”等等(元数据)。
用途: 当你需要在流式处理的过程中,实时获取每一小块响应的元数据时使用。比如,你可能想在流结束时,累加所有中间过程的 token 消耗。这比等到最后才拿到总的 ChatResponse 要更实时。Flux< ChatClientResponse > chatClientResponse(): 返回一个 Flux,该 Flux 会发出 ChatClientResponse 对象,该对象包含 ChatResponse 对象和 ChatClient 的执行上下文,让您能够访问在“顾问”(Advisors) 执行期间使用的附加数据(例如,在 RAG 流程中检索到的相关文档)。
chatClientResponse(): 传送带上不仅有盘子,还有厨师的签名和采购记录
返回: Flux
比喻: 传送带上传来的盘子更加豪华了。除了寿司和标签,盘子底下还附带了一张卡片,上面有主厨(Advisor)的签名,以及这张卡片所用的鱼是今天早上从哪个码头(RAG 检索到的文档)采购的记录。
用途: 非常高级且罕见。用于在流式 RAG 场景下,实时地想知道“当前生成的这段话,是参考了哪些文档片段”。这可以用来在界面上实现一种效果:当 AI 的回答逐渐出现时,旁边同步高亮显示它正在参考的原文。
| 同步 (call()) 之后 | 异步 (stream()) 之后 | 区别是什么? |
|---|---|---|
| .content() -> String | .content() -> Flux< String > | 一次性拿到完整字符串 vs. 通过管道持续收到字符串片段。 |
| .chatResponse() -> ChatResponse | .chatResponse() -> Flux< ChatResponse > | 一次性拿到完整的带元数据的响应 vs. 通过管道持续收到带元数据的响应片段。 |
| .chatClientResponse() -> ChatClientResponse | .chatClientResponse() -> Flux< ChatClientResponse > | 一次性拿到完整的带执行上下文的响应 vs. 通过管道持续收到带上下文的响应片段。 |
关键记忆点:.stream() 只是把 .call() 的所有返回结果,都用 Flux<…> 这个异步管道包装了起来。选择哪个,取决于你是想“等所有菜都上齐了再吃”(同步 call),还是想“菜上来一道吃一道”(异步 stream)。
Using Defaults(使用默认值)
在一个 @Configuration 类中创建一个带有默认系统文本的 ChatClient,可以简化运行时的代码。通过设置默认值,您在调用 ChatClient 时只需要指定用户文本,从而无需在您的运行时代码路径中为每个请求都设置系统文本。
这段话的核心思想是:不要在每次对话时都重复告诉 AI 它的“人设”。在应用启动时一次性配置好,以后每次调用时,它就自动带上这个人设了。
默认系统文本 (Default System Text)
在下面的示例中,我们将配置系统文本,使其总是以海盗(“You are a friendly chat bot that answers question in the voice of a Pirate”–>”你是一个友好的聊天机器人,要用一个海盗(Pirate)的口吻来回答问题。”)的口吻回复。为了避免在运行时代码中重复设置系统文本,我们将在一个 @Configuration 类中创建一个 ChatClient 实例。
1 | // @Configuration 注解告诉 Spring:“这个类是专门用来做配置的,请在启动时扫描它。” |
- 从 Builder 到 Client: 这个 @Bean 方法的输入是通用的 ChatClient.Builder,输出是一个经过定制的、具体的 ChatClient 实例。
- @Bean 的魔力: Spring 容器现在有了一个名为 chatClient 的特定 Bean。当其他组件请求注入一个 ChatClient 类型的对象时,Spring 会把这个“海盗版”的实例提供给它。
- 集中配置: 所有关于“海盗”这个角色的设定,都封装在了 Config 类里,与业务逻辑完全分离。
以及一个用于调用它的 @RestController:
1 |
|
- 注入的是“成品”,不是“工具”: AIController 直接注入了最终产品 ChatClient,而不是用于构建的工具 ChatClient.Builder。这让 Controller 的职责更单一,它只管使用,不管创建。
- 运行时代码简化: completion 方法的实现非常干净,只关注核心业务逻辑——获取用户输入并调用 AI。所有关于“角色扮演”的模板代码都被消除了。
- 结果验证: curl 的返回结果 “To hear some arrr-rated jokes! Arrr, matey!” 完美地证明了这一点。AI 的回答充满了海盗的口吻(”arrr-rated”, “Arrr, matey!”),说明我们设置的默认系统提示确实生效了。
当通过 curl 调用应用程序端点时,结果是:
1 | ❯ curl localhost:8080/ai/simple |
总结
这个例子是理解和应用 Spring AI 依赖注入和集中化配置思想的绝佳范本。
- 分离关注点 (Separation of Concerns):Config 类负责如何创建和配置 AI 客户端,AIController 类负责如何使用 AI 客户端。两者各司其职。
- 代码整洁 (Clean Code):通过将通用配置(如 System Prompt)提取到配置类中,极大地简化了业务代码,使其更易读、更易维护。
- 依赖注入的力量 (Power of DI):通过 @Bean 和构造函数注入,Spring 框架像一个智能的管道工,自动地将配置好的“海盗版” ChatClient 连接到了需要它的地方。
带参数的默认系统文本 (Default System Text with parameters)
在下面的示例中,我们将在系统文本中使用一个占位符,以便在运行时而不是在设计时指定回答的口吻。
这个例子的核心思想是:我们可以预设一个“半成品”的人设模板,然后在每次对话时,再动态地填入最关键的那部分信息。
1 |
|
- 关键变化: defaultSystem 的内容从一个固定的字符串 “…” 变成了包含占位符 {voice} 的模板字符串。
- 现在我们创建的 ChatClient 不再是“海盗版”,我们可以称之为**“演员版”**。它知道自己需要扮演一个角色,但具体扮演谁,它在等待指令。
1 |
|
- .system(…): 这不再是像 .system(“some text”) 那样直接提供一个全新的系统提示。
- 它的作用是: “我要对默认的系统提示进行补充或修改”。
- sp -> …: 这是一个 Lambda 表达式,sp 代表 SystemPromptConfigurer,一个专门用来配置系统提示的工具。
- sp.param(“voice”, voice): 这句代码的意思是:“请找到默认系统提示模板里的那个叫 {voice} 的占位符,然后用我从 URL 参数里拿到的 voice 变量的值去填充它。”
当通过 httpie 调用应用程序端点时,结果是:
1 | http localhost:8080/ai voice=='Robert DeNiro' |
【完整流程】
- 用户访问 …/ai?voice=Robert DeNiro。
- AIController 接收到请求,voice 变量的值是 “Robert DeNiro”。
- .system(sp -> sp.param(“voice”, voice)) 这行代码执行,它把 “Robert DeNiro” 填入到了默认模板 “You are a friendly chat bot that answers question in the voice of a {voice}” 中。
- 最终,发送给 AI 的实际系统提示变成了:”You are a friendly chat bot that answers question in the voice of a Robert DeNiro”。
- AI 接收到这个指令,开始模仿著名演员罗伯特·德尼罗的经典台词(”You talkin’ to me?”)来回答问题。
总结
这个例子展示了一个非常强大和实用的模式:
- 在配置层 (@Configuration):定义一个通用的、带占位符的默认模板,设定好框架。
- 在业务层 (@RestController):只关注填充模板中的动态部分,而不需要重复整个模板的框架。
这种方式兼顾了代码的简洁性和运行时的灵活性。你可以用同样的代码,通过改变 voice 参数,让 AI 模仿海盗、莎士比亚、牛仔、宇航员……而无需修改一行后端代码。这是构建高度可定制化 AI 应用的关键技巧。
其他默认值 (Other defaults)
在 ChatClient.Builder 级别,您可以指定默认的提示配置。
ChatClient.Builder 是一个强大的“预设工作台”。你不仅可以预设系统提示(人设),还可以预设模型参数、可用工具、默认问题、甚至是 RAG 功能。
我们可以把 ChatClient 想象成一个高度定制的“智能助理机器人”。通过 Builder 的 defaultXxx() 系列方法,你可以在它出厂前(build() 之前)为它安装各种“默认配件”和“默认程序”。
defaultOptions(ChatOptions chatOptions): 传入定义在 ChatOptions 类中的可移植选项,或者像 OpenAiChatOptions 中那样的模型特定选项。有关模型特定的 ChatOptions 实现的更多信息,请参考 JavaDocs。
defaultOptions(…): 预设大脑的“运行参数”
- 比喻: 预设机器人的**“思考模式”**。比如,默认是“严谨模式”(temperature=0.2)还是“创意模式”(temperature=0.8),默认使用哪个型号的大脑(model=”gpt-4o”)。
- 用途: 统一应用的 AI 响应风格,避免在每次调用时都重复设置 temperature, model 等参数。
defaultFunction(String name, String description, java.util.function.Function< I, O > function): name 用于在用户文本中引用该函数。description 解释了函数的目的,帮助 AI 模型选择正确的函数以获得准确响应。function 参数是一个 Java 函数实例,模型会在必要时执行它。
defaultFunctions(String… functionNames): 在应用程序上下文中定义的 java.util.Function 的 bean 名称。
defaultFunction(…) / defaultFunctions(…): 给机器人预装“工具”和“技能”
- 比喻: 给机器人安装一些默认就能使用的工具,比如一个“天气查询器”或者“计算器”。你还要给每个工具一份说明书(description),让机器人知道什么时候该用哪个工具。
- 用途: 这是实现 Tool Calling / Function Calling 的关键。让 AI 能够调用你写的 Java 代码来获取外部信息或执行操作。预设之后,这个机器人就默认拥有了这些超能力。
defaultUser(String text), defaultUser(Resource text), defaultUser(Consumer< UserSpec > userSpecConsumer): 这些方法让您可以定义默认的用户文本。Consumer< UserSpec > 允许您使用 lambda 表达式来指定用户文本和任何默认参数。
defaultUser(…): 预设一个“开场白”或“默认问题”
- 比喻: 预设机器人每次启动时,默认要处理的第一个任务。
- 用途: 比较少见,但可能用于某些特定场景。比如,创建一个专门用于“每日新闻总结”的 ChatClient,它的 defaultUser 提示就是“请为我总结今天的头条新闻”。这样调用时甚至连 .user() 都可以省略。
defaultAdvisors(Advisor… advisor): “顾问”(Advisors) 允许修改用于创建 Prompt 的数据。QuestionAnswerAdvisor 的实现通过将与用户文本相关的上下文信息附加到提示中,来启用检索增强生成(RAG)模式。
defaultAdvisors(Consumer< AdvisorSpec > advisorSpecConsumer): 此方法允许您定义一个 Consumer 来使用 AdvisorSpec 配置多个顾问。顾问可以修改用于创建最终 Prompt 的数据。Consumer< AdvisorSpec > 让您可以指定一个 lambda 来添加顾问,例如 QuestionAnswerAdvisor,它通过根据用户文本将相关上下文信息附加到提示中,来支持检索增强生成。
defaultAdvisors(…): 给机器人配备一个“外脑”或“研究助理” (RAG 功能)
- 比喻: 这是最强大的配件!你给机器人配备了一个研究团队(Advisor)。当机器人遇到它知识范围外的问题时,这个研究团队(QuestionAnswerAdvisor)会先去你的知识库(向量数据库)里查找相关资料,然后把资料整理好,交给机器人,让它根据这些新资料来回答问题。
- 用途: 启用 RAG (检索增强生成)。通过预设一个 QuestionAnswerAdvisor,你创建的 ChatClient 就天生具备了结合你私有数据来回答问题的能力。
运行时覆盖默认值
您可以在运行时使用相应的、去掉了 default 前缀的方法来覆盖这些默认值。
- options(ChatOptions chatOptions)
- function(String name, String description, java.util.function.Function< I, O > function)
- functions(String… functionNames)
- user(String text), user(Resource text), user(Consumer< UserSpec > userSpecConsumer)
- advisors(Advisor… advisor)
- advisors(Consumer< AdvisorSpec > advisorSpecConsumer)
核心原则:default vs. non-default
这是最需要记住的一点!
defaultXxx(…): 在 ChatClient.Builder 上调用,用于配置时设置全局默认值。这是“一次性岗前培训”。
例如:builder.defaultSystem(“你是海盗”)
xxx(…) (没有 default 前缀): 在 chatClient.prompt() 的调用链中调用,用于运行时提供单次调用的特定值。这个特定值会覆盖本次调用的全局默认值。这是“下达临时指令”。
例如:chatClient.prompt().system(“这次别当海盗了,扮演个牛仔”)
这个设计提供了完美的平衡:
- 对于通用行为:在配置中用 defaultXxx() 设置一次,一劳永逸。
- 对于特殊情况:在运行时用 xxx() 灵活地进行单次覆盖,而不影响全局默认设置。
总结
ChatClient.Builder 不仅仅是一个简单的建造者,它是一个功能丰富的配置中心。通过它的 defaultXxx 系列方法,你可以创建一个高度定制化、功能强大的 ChatClient 实例,它天生就具备:
- 特定的性格 (defaultSystem)
- 固定的思考模式 (defaultOptions)
- 强大的外部工具 (defaultFunction)
- 连接私有知识库的能力 (defaultAdvisors)
掌握了这些,你就可以根据不同的业务场景,创建出多个各司其职、高度专业化的“AI 智能助理” Bean,让你的应用架构变得清晰、强大且易于维护。

