深度剖析Spring AI源码(三):ChatClient详解,优雅的流式API设计

ComfyUI

ComfyUI

AI应用
ComfyUI

ComfyUI是一款易于上手的工作流设计工具,具有以下特点:基于工作流节点设计,可视化工作流搭建,快速切换工作流,对显存占用小,速度快,支持多种插件,如ADetailer、Controlnet和AnimateDIFF等

深度剖析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();

这种方式虽然功能完整,但暴露了几个痛点:

  • 冗长繁琐:仅仅为了发送一个简单的请求,就需要手动构建PromptMessage列表,样板代码过多。
  • 不够直观:核心的业务意图(比如“问一个问题”)被大量的技术细节所掩盖。
  • 难以组合:在需要动态构建Prompt、添加工具调用或应用重试等复杂场景下,代码会迅速变得混乱,可读性极差。

ChatClient的诞生,正是为了解决这些痛点,它提供了一个更高层次的抽象,让AI调用变得像使用Spring的WebClient调用REST API一样简单、直观和强大。

ChatClient接口体系架构

在深入代码之前,我们先通过一张类图来鸟瞰ChatClient的整体设计。这套接口体系遵循了流式API的设计思想,引导开发者一步步构建请求和处理响应。

«interface»
ChatClient
+prompt() : ChatClientRequestSpec
+prompt(String) : ChatClientRequestSpec
+prompt(Prompt) : ChatClientRequestSpec
+mutate() : Builder
«interface»
ChatClientRequestSpec
+system(String) : ChatClientRequestSpec
+user(String) : ChatClientRequestSpec
+advisors(Advisor...) : ChatClientRequestSpec
+call() : CallResponseSpec
+stream() : StreamResponseSpec
«interface»
CallResponseSpec
+content() : String
+entity(Class) : T
+chatResponse() : ChatResponse
«interface»
StreamResponseSpec
+content() : Flux<String>
+chatResponse() : Flux<ChatResponse>
«interface»
Builder
+defaultSystem(String) : Builder
+defaultAdvisors(Advisor...) : Builder
+build() : ChatClient

核心接口深度解析

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的味道,精妙之处在于:

  • 静态工厂方法:提供了多种createbuilder方法,无论是简单场景还是需要深度定制的场景,都能找到合适的创建方式。
  • 流式接口:所有prompt()方法都返回ChatClientRequestSpec接口,这使得方法调用可以像链条一样串联起来,代码一气呵成。
  • 不可变性ChatClient实例一旦通过build()创建,其默认配置就是不可变的,这使得它在多线程环境下可以被安全地共享和复用。mutate()方法则提供了一种“写时复制”的机制来创建新的、可修改的构建器。

2. 流式请求构建:ChatClientRequestSpec

ChatClientRequestSpecChatClient流式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的魅力所在——让简单的事情变得极其简单,开发者无需关心底层的PromptMessage结构,专注于传递核心信息即可。

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. 建造者模式的完美实现

ChatClientBuilder设计堪称教科书级别,它将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。

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接口(我们将在后续章节深入探讨)则提供了模板方法的实现,定义了beforeafter两个钩子方法,让开发者可以轻松地在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增强功能

AdvisorChatClient的“超级外挂”。你可以自定义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生态的设计哲学:

  1. 流式接口:让API调用变得直观和优雅
  2. Builder模式:提供了灵活的配置方式
  3. Advisor机制:通过AOP和责任链模式,提供了强大的、非侵入式的横切关注点处理能力。
  4. 响应式支持:与Project Reactor无缝集成,天然支持流式处理和背压控制。
  5. 结构化输出:将AI的自然语言输出直接转换为Java对象,打通了AI与业务逻辑的“最后一公里”。

这套精心设计的API不仅极大地降低了AI应用的开发门槛,也为构建复杂的企业级场景提供了坚实、可扩展的基础。它就像是AI世界的WebClient,让Java开发者能够用最熟悉、最舒适的方式,驾驭强大的生成式AI。

下一章,我们将深入向量存储的世界,探索Spring AI是如何统一五花八门的向量数据库的。这将是构建RAG应用的基石,也是企业AI应用的核心组件,敬请期待!


思考题:如果让你设计一个AI调用的流式API,你会如何平衡易用性和灵活性?ChatClient的设计给了你什么启发?

您可能感兴趣的与本文相关的镜像

ComfyUI

ComfyUI

AI应用
ComfyUI

ComfyUI是一款易于上手的工作流设计工具,具有以下特点:基于工作流节点设计,可视化工作流搭建,快速切换工作流,对显存占用小,速度快,支持多种插件,如ADetailer、Controlnet和AnimateDIFF等

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

THMAIL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值