深度剖析Spring AI源码(三):ChatClient详解,优雅的流式API设计
“The best APIs are those that make simple things simple and complex things possible.” —— Alan Kay (计算机科学巨匠)
Spring AI的
ChatClientAPI正是这句话的完美诠释。它既能让新手在几分钟内快速上手,发出第一个AI请求,又能通过丰富的扩展点满足企业级应用的复杂需求。今天,让我们深入这个被誉为“AI界的WebClient/RestTemplate”的流式API,看看它是如何做到优雅与强大并存的。
开场:为什么需要ChatClient?
在上一章中,我们已经了解了Model抽象层的底层设计。但如果直接使用Model接口进行AI调用,代码往往是这样的:
// 原始的Model调用方式
ChatModel chatModel = new OpenAiChatModel(openAiApi);
Prompt prompt = new Prompt(List.of(
new SystemMessage("You are a helpful assistant"),
new UserMessage("Hello, how are you?")
));
ChatResponse response = chatModel.call(prompt);
String content = response.getResult().getOutput().getContent();
这种方式虽然功能完整,但暴露了几个痛点:
- 冗长繁琐:仅仅为了发送一个简单的请求,就需要手动构建
Prompt和Message列表,样板代码过多。 - 不够直观:核心的业务意图(比如“问一个问题”)被大量的技术细节所掩盖。
- 难以组合:在需要动态构建Prompt、添加工具调用或应用重试等复杂场景下,代码会迅速变得混乱,可读性极差。
ChatClient的诞生,正是为了解决这些痛点,它提供了一个更高层次的抽象,让AI调用变得像使用Spring的WebClient调用REST API一样简单、直观和强大。
ChatClient接口体系架构
在深入代码之前,我们先通过一张类图来鸟瞰ChatClient的整体设计。这套接口体系遵循了流式API的设计思想,引导开发者一步步构建请求和处理响应。
核心接口深度解析
1. ChatClient主接口
ChatClient是所有交互的入口,它定义了发起AI对话的起点。让我们深入其接口设计(位于spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java):
public interface ChatClient {
// 静态工厂方法 - 简单创建
static ChatClient create(ChatModel chatModel) {
return create(chatModel, ObservationRegistry.NOOP);
}
// 带监控的创建方法
static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry) {
return create(chatModel, observationRegistry, null);
}
// 完整的创建方法
static ChatClient create(ChatModel chatModel,
ObservationRegistry observationRegistry,
@Nullable ChatClientObservationConvention observationConvention) {
Assert.notNull(chatModel, "chatModel cannot be null");
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
return builder(chatModel, observationRegistry, observationConvention).build();
}
// Builder模式入口
static Builder builder(ChatModel chatModel) {
return builder(chatModel, ObservationRegistry.NOOP, null);
}
// 核心方法 - 开始构建请求
ChatClientRequestSpec prompt();
ChatClientRequestSpec prompt(String content);
ChatClientRequestSpec prompt(Prompt prompt);
// 可变构建器 - 基于当前实例创建新的Builder
Builder mutate();
}
这个接口的设计充满了Spring的味道,精妙之处在于:
- 静态工厂方法:提供了多种
create和builder方法,无论是简单场景还是需要深度定制的场景,都能找到合适的创建方式。 - 流式接口:所有
prompt()方法都返回ChatClientRequestSpec接口,这使得方法调用可以像链条一样串联起来,代码一气呵成。 - 不可变性:
ChatClient实例一旦通过build()创建,其默认配置就是不可变的,这使得它在多线程环境下可以被安全地共享和复用。mutate()方法则提供了一种“写时复制”的机制来创建新的、可修改的构建器。
2. 流式请求构建:ChatClientRequestSpec
ChatClientRequestSpec是ChatClient流式API的核心,它负责定义一次具体的AI请求。这个接口提供了丰富的方法来构建一个完整的Prompt。
public interface ChatClientRequestSpec {
// 可变构建器
Builder mutate();
// Advisor支持 - AOP模式的体现
ChatClientRequestSpec advisors(Consumer<AdvisorSpec> consumer);
ChatClientRequestSpec advisors(Advisor... advisors);
ChatClientRequestSpec advisors(List<Advisor> advisors);
// 消息构建
ChatClientRequestSpec messages(Message... messages);
ChatClientRequestSpec messages(List<Message> messages);
// 模型选项
<T extends ChatOptions> ChatClientRequestSpec options(T options);
// 工具调用支持
ChatClientRequestSpec toolNames(String... toolNames);
ChatClientRequestSpec tools(Object... toolObjects);
ChatClientRequestSpec toolCallbacks(ToolCallback... toolCallbacks);
ChatClientRequestSpec toolContext(Map<String, Object> toolContext);
// 系统消息 - 设定AI行为
ChatClientRequestSpec system(String text);
ChatClientRequestSpec system(Resource textResource, Charset charset);
ChatClientRequestSpec system(Consumer<PromptSystemSpec> consumer);
// 用户消息 - 实际的问题或指令
ChatClientRequestSpec user(String text);
ChatClientRequestSpec user(Resource text, Charset charset);
ChatClientRequestSpec user(Consumer<PromptUserSpec> consumer);
// 模板渲染器
ChatClientRequestSpec templateRenderer(TemplateRenderer templateRenderer);
// 执行调用
CallResponseSpec call(); // 同步调用
StreamResponseSpec stream(); // 流式调用
}
3. 响应处理:CallResponseSpec与StreamResponseSpec
当请求构建完毕,通过.call()或.stream()执行后,返回的响应处理接口同样设计得非常考究:
public interface CallResponseSpec {
// 结构化输出 - 直接转换为Java对象
@Nullable
<T> T entity(ParameterizedTypeReference<T> type);
@Nullable
<T> T entity(StructuredOutputConverter<T> structuredOutputConverter);
@Nullable
<T> T entity(Class<T> type);
// 包装响应 - 获取完整的响应信息
ChatClientResponse chatClientResponse();
// 原始响应
@Nullable
ChatResponse chatResponse();
// 简单文本内容
@Nullable
String content();
// 响应实体包装
<T> ResponseEntity<ChatResponse, T> responseEntity(Class<T> type);
}
public interface StreamResponseSpec {
// 流式响应
Flux<ChatClientResponse> chatClientResponse();
Flux<ChatResponse> chatResponse();
Flux<String> content();
}
使用体验:从简单到复杂
1. 最简单的使用方式
对于最常见的“一问一答”场景,ChatClient的使用简单到了极致:
// 创建ChatClient
ChatClient chatClient = ChatClient.create(chatModel);
// 最简单的调用
String response = chatClient
.prompt("给我讲个笑话")
.call()
.content();
System.out.println(response);
这就是ChatClient的魅力所在——让简单的事情变得极其简单,开发者无需关心底层的Prompt和Message结构,专注于传递核心信息即可。
2. 稍微复杂的场景
当需要为AI设定角色时,可以链式调用.system()和.user():
String response = chatClient
.prompt()
.system("你是一个专业的Java开发者")
.user("解释一下Spring Boot的自动配置原理")
.call()
.content();
3. 企业级复杂场景
在真实的企业级应用中,AI调用往往伴随着安全、日志、重试、多模态输入和结构化输出等复杂需求。ChatClient通过其强大的Builder和可扩展的Advisor机制,从容应对:
// 使用Builder预配置
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是一个AI助手,专门帮助解决技术问题")
.defaultAdvisors(
new SafeGuardAdvisor(), // 安全防护
new LoggingAdvisor(), // 日志记录
new RetryAdvisor() // 重试机制
)
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-4")
.temperature(0.7)
.maxTokens(1000)
.build())
.build();
// 复杂的调用
PersonInfo person = chatClient
.prompt()
.user(userSpec -> userSpec
.text("分析这个简历:{resume}")
.param("resume", resumeText)
.media(MimeType.IMAGE_JPEG, resumeImage))
.advisors(new ResumeAnalysisAdvisor())
.tools(resumeAnalysisService) // 工具调用
.call()
.entity(PersonInfo.class); // 结构化输出
核心实现解析
1. DefaultChatClient的实现
让我们深入DefaultChatClient的内部,看看它是如何工作的(位于spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java):
public class DefaultChatClient implements ChatClient {
private final ChatModel chatModel;
private final ChatClientRequestSpec defaultRequestSpec;
private final ObservationRegistry observationRegistry;
// 构造函数 - 依赖注入
DefaultChatClient(ChatModel chatModel,
ChatClientRequestSpec defaultRequestSpec,
ObservationRegistry observationRegistry) {
this.chatModel = chatModel;
this.defaultRequestSpec = defaultRequestSpec;
this.observationRegistry = observationRegistry;
}
@Override
public ChatClientRequestSpec prompt() {
// 返回默认请求规范的副本
return this.defaultRequestSpec.mutate().build();
}
@Override
public ChatClientRequestSpec prompt(String content) {
return prompt().user(content);
}
@Override
public ChatClientRequestSpec prompt(Prompt prompt) {
return prompt().messages(prompt.getInstructions());
}
}
2. 请求构建的实现:DefaultChatClientRequestSpec
DefaultChatClientRequestSpec是流式API的核心实现,它像一个状态机,一步步收集构建请求所需的所有信息。
class DefaultChatClientRequestSpec implements ChatClientRequestSpec {
private final ChatModel chatModel;
private final List<Message> messages = new ArrayList<>();
private final List<Advisor> advisors = new ArrayList<>();
private ChatOptions chatOptions;
private Map<String, Object> toolContext = new HashMap<>();
@Override
public ChatClientRequestSpec system(String text) {
this.messages.add(new SystemMessage(text));
return this;
}
@Override
public ChatClientRequestSpec user(String text) {
this.messages.add(new UserMessage(text));
return this;
}
@Override
public ChatClientRequestSpec advisors(Advisor... advisors) {
this.advisors.addAll(Arrays.asList(advisors));
return this;
}
@Override
public CallResponseSpec call() {
return new DefaultCallResponseSpec(buildRequest());
}
@Override
public StreamResponseSpec stream() {
return new DefaultStreamResponseSpec(buildRequest());
}
// 构建最终的请求对象
private ChatClientRequest buildRequest() {
return ChatClientRequest.builder()
.prompt(new Prompt(this.messages, this.chatOptions))
.advisors(this.advisors)
.toolContext(this.toolContext)
.build();
}
}
3. Advisor链的执行
ChatClient最精彩、最能体现其AOP思想的部分,莫过于Advisor链的执行逻辑:
class DefaultCallResponseSpec implements CallResponseSpec {
private final ChatClientRequest request;
@Override
public String content() {
// 执行Advisor链
ChatClientResponse response = executeAdvisorChain(request);
return response.getResult().getOutput().getContent();
}
private ChatClientResponse executeAdvisorChain(ChatClientRequest request) {
// 构建Advisor链
List<CallAdvisor> callAdvisors = request.getAdvisors()
.stream()
.filter(CallAdvisor.class::isInstance)
.map(CallAdvisor.class::cast)
.sorted(AnnotationAwareOrderComparator.INSTANCE) // 按Order排序
.toList();
// 添加默认的ChatModel调用Advisor
List<CallAdvisor> allAdvisors = new ArrayList<>(callAdvisors);
allAdvisors.add(new ChatModelCallAdvisor(chatModel));
// 执行链式调用
CallAdvisorChain chain = new DefaultCallAdvisorChain(allAdvisors);
return chain.nextCall(request);
}
}
设计模式的精妙运用
1. 建造者模式的完美实现
ChatClient的Builder设计堪称教科书级别,它将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
public interface Builder {
// 默认配置方法
Builder defaultAdvisors(Advisor... advisor);
Builder defaultOptions(ChatOptions chatOptions);
Builder defaultUser(String text);
Builder defaultSystem(String text);
// 模板渲染器
Builder defaultTemplateRenderer(TemplateRenderer templateRenderer);
// 工具相关
Builder defaultToolNames(String... toolNames);
Builder defaultTools(Object... toolObjects);
Builder defaultToolCallbacks(ToolCallback... toolCallbacks);
// 克隆当前Builder
Builder clone();
// 构建最终对象
ChatClient build();
}
使用示例:
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是一个专业的技术顾问")
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-4")
.temperature(0.3)
.build())
.defaultAdvisors(
new LoggingAdvisor(),
new SafeGuardAdvisor()
)
.build();
2. 责任链模式的Advisor实现
Advisor的执行巧妙地运用了责任链模式,每个Advisor都是链上的一个节点,它既可以处理请求,也可以将请求传递给链上的下一个节点。
public interface CallAdvisorChain {
ChatClientResponse nextCall(ChatClientRequest request);
}
class DefaultCallAdvisorChain implements CallAdvisorChain {
private final List<CallAdvisor> advisors;
private final int index;
@Override
public ChatClientResponse nextCall(ChatClientRequest request) {
if (index >= advisors.size()) {
throw new IllegalStateException("No more advisors in chain");
}
CallAdvisor advisor = advisors.get(index);
CallAdvisorChain nextChain = new DefaultCallAdvisorChain(advisors, index + 1);
// 每个Advisor都可以决定是否继续执行链
return advisor.adviseCall(request, nextChain);
}
}
3. 模板方法模式的应用
BaseAdvisor接口(我们将在后续章节深入探讨)则提供了模板方法的实现,定义了before和after两个钩子方法,让开发者可以轻松地在AI调用的前后织入自定义逻辑。
public interface BaseAdvisor extends CallAdvisor, StreamAdvisor {
@Override
default ChatClientResponse adviseCall(ChatClientRequest request,
CallAdvisorChain chain) {
// 模板方法:before -> 执行链 -> after
ChatClientRequest processedRequest = before(request, chain);
ChatClientResponse response = chain.nextCall(processedRequest);
return after(response, chain);
}
// 子类实现具体的前置和后置逻辑
ChatClientRequest before(ChatClientRequest request, AdvisorChain chain);
ChatClientResponse after(ChatClientResponse response, AdvisorChain chain);
}
流式处理的优雅实现
流式处理是现代AI应用的标配,ChatClient通过与Project Reactor的深度集成,提供了非常优雅的实现:
@Override
public Flux<String> content() {
return executeStreamAdvisorChain(request)
.map(response -> response.getResult().getOutput().getContent())
.filter(Objects::nonNull);
}
private Flux<ChatClientResponse> executeStreamAdvisorChain(ChatClientRequest request) {
// 构建流式Advisor链
List<StreamAdvisor> streamAdvisors = request.getAdvisors()
.stream()
.filter(StreamAdvisor.class::isInstance)
.map(StreamAdvisor.class::cast)
.sorted(AnnotationAwareOrderComparator.INSTANCE)
.toList();
// 添加默认的流式调用Advisor
List<StreamAdvisor> allAdvisors = new ArrayList<>(streamAdvisors);
allAdvisors.add(new ChatModelStreamAdvisor(chatModel));
// 执行流式链
StreamAdvisorChain chain = new DefaultStreamAdvisorChain(allAdvisors);
return chain.nextStream(request);
}
使用示例:
// 流式输出,逐字显示
chatClient
.prompt("写一首关于Spring的诗")
.stream()
.content()
.subscribe(System.out::print);
// 或者收集完整响应
List<String> chunks = chatClient
.prompt("解释量子计算")
.stream()
.content()
.collectList()
.block();
结构化输出的魔法
ChatClient最令人惊艳的功能之一,就是能将AI的非结构化文本输出,直接转换为类型安全的Java对象,这极大地简化了后续的数据处理。
// 定义输出结构
public record WeatherInfo(
String city,
double temperature,
String condition,
List<String> forecast
) {}
// 直接获取结构化对象
WeatherInfo weather = chatClient
.prompt("北京今天的天气如何?请以JSON格式返回")
.call()
.entity(WeatherInfo.class);
System.out.println("城市:" + weather.city());
System.out.println("温度:" + weather.temperature() + "°C");
这个“魔法”的背后,是Spring AI内置的StructuredOutputConverter在默默工作,它会智能地在Prompt中添加指令,引导AI输出指定的JSON格式,并自动完成反序列化。
@Override
public <T> T entity(Class<T> type) {
ChatClientResponse response = chatClientResponse();
// 使用Jackson进行JSON转换
StructuredOutputConverter<T> converter =
new BeanOutputConverter<>(type);
String content = response.getResult().getOutput().getContent();
return converter.convert(content);
}
最佳实践与使用技巧
1. 合理使用Builder预配置
在真实项目中,你可能会针对不同业务场景使用不同的AI配置。通过@Bean和@Qualifier,你可以预先配置好多个ChatClient实例,供不同服务注入使用。
// 为不同场景创建专门的ChatClient
@Configuration
public class ChatClientConfig {
@Bean
@Qualifier("technical")
public ChatClient technicalChatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是一个资深的技术专家")
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-4")
.temperature(0.3) // 技术问答需要更准确
.build())
.build();
}
@Bean
@Qualifier("creative")
public ChatClient creativeChatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是一个富有创意的作家")
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-4")
.temperature(0.9) // 创意写作需要更多随机性
.build())
.build();
}
}
2. 善用Advisor增强功能
Advisor是ChatClient的“超级外挂”。你可以自定义Advisor来实现各种横切关注点,比如下面这个用于控制Token成本的例子:
// 自定义Advisor
public class CostControlAdvisor implements BaseAdvisor {
private final TokenCounter tokenCounter;
private final int maxTokensPerRequest;
@Override
public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
// 检查token数量
int estimatedTokens = tokenCounter.estimate(request.getPrompt());
if (estimatedTokens > maxTokensPerRequest) {
throw new IllegalArgumentException("Request too large: " + estimatedTokens);
}
return request;
}
@Override
public ChatClientResponse after(ChatClientResponse response, AdvisorChain chain) {
// 记录实际使用的token
Usage usage = response.getMetadata().getUsage();
tokenCounter.record(usage.getTotalTokens());
return response;
}
}
3. 错误处理和重试
结合Advisor和Spring Retry,可以轻松实现健壮的错误处理和重试机制。
// 带重试的调用
String response = chatClient
.prompt("复杂的技术问题")
.advisors(new RetryAdvisor(3, Duration.ofSeconds(2))) // 配置重试3次,间隔2秒
.call()
.content();
// 推荐使用Spring AI内置的异常体系进行捕获
try {
String result = chatClient
.prompt("可能失败的请求")
.call()
.content();
} catch (ChatClientException e) {
log.error("AI调用失败,根本原因: ", e.getRootCause());
return "抱歉,AI服务暂时不可用,请稍后再试。";
}
小结
ChatClient API是Spring AI最亮眼的“门面”之一,它完美地践行了Spring生态的设计哲学:
- 流式接口:让API调用变得直观和优雅
- Builder模式:提供了灵活的配置方式
- Advisor机制:通过AOP和责任链模式,提供了强大的、非侵入式的横切关注点处理能力。
- 响应式支持:与Project Reactor无缝集成,天然支持流式处理和背压控制。
- 结构化输出:将AI的自然语言输出直接转换为Java对象,打通了AI与业务逻辑的“最后一公里”。
这套精心设计的API不仅极大地降低了AI应用的开发门槛,也为构建复杂的企业级场景提供了坚实、可扩展的基础。它就像是AI世界的WebClient,让Java开发者能够用最熟悉、最舒适的方式,驾驭强大的生成式AI。
下一章,我们将深入向量存储的世界,探索Spring AI是如何统一五花八门的向量数据库的。这将是构建RAG应用的基石,也是企业AI应用的核心组件,敬请期待!
思考题:如果让你设计一个AI调用的流式API,你会如何平衡易用性和灵活性?ChatClient的设计给了你什么启发?

1346

被折叠的 条评论
为什么被折叠?



