深度剖析Spring AI源码(三):ChatClient详解,优雅的流式API设计
深度剖析Spring AI源码(三):ChatClient详解,优雅的流式API设计
“The best APIs are those that make simple things simple and complex things possible.” —— Alan Kay (计算机科学巨匠)
Spring AI的
ChatClient
API正是这句话的完美诠释。它既能让新手在几分钟内快速上手,发出第一个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);// 可变构建器 - 基于当前实例创建新的BuilderBuilder 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();// 原始响应@NullableChatResponse chatResponse();// 简单文本内容@NullableString 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;}@Overridepublic ChatClientRequestSpec prompt() {// 返回默认请求规范的副本return this.defaultRequestSpec.mutate().build();}@Overridepublic ChatClientRequestSpec prompt(String content) {return prompt().user(content);}@Overridepublic 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<>();@Overridepublic ChatClientRequestSpec system(String text) {this.messages.add(new SystemMessage(text));return this;}@Overridepublic ChatClientRequestSpec user(String text) {this.messages.add(new UserMessage(text));return this;}@Overridepublic ChatClientRequestSpec advisors(Advisor... advisors) {this.advisors.addAll(Arrays.asList(advisors));return this;}@Overridepublic CallResponseSpec call() {return new DefaultCallResponseSpec(buildRequest());}@Overridepublic 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;@Overridepublic 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调用AdvisorList<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);// 克隆当前BuilderBuilder 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;@Overridepublic 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 {@Overridedefault ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {// 模板方法:before -> 执行链 -> afterChatClientRequest 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();// 添加默认的流式调用AdvisorList<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;@Overridepublic 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;}@Overridepublic ChatClientResponse after(ChatClientResponse response, AdvisorChain chain) {// 记录实际使用的tokenUsage 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的设计给了你什么启发?