ChatClient 调用链路

Spring AI 调用大模型的完整链路:

ChatClient(客户端入口)
  → Advisor 链(拦截器,可插入日志、RAG等)
    → ChatModel(模型抽象层,如 DashScopeChatModel)
      → 内部 HTTP 请求 → 大模型 API(如阿里百炼)

ChatClient 是核心客户端,相当于调用大模型的"RestTemplate"。ChatModel 是模型抽象层,屏蔽不同大模型的 API 差异,你配了哪个厂商就自动用对应的实现类,不需要自己管。

两种调用方式

// 非流式:等大模型全部生成完,一次性返回
String answer = chatClient.prompt()
    .user("你好")
    .call()
    .content();

// 流式:大模型每生成一点就推一点过来
Flux<String> stream = chatClient.prompt()
    .user("你好")
    .stream()
    .content();

.call() 是同步等待完整结果,.stream() 是响应式数据流,适合"逐字蹦出来"的聊天效果。

流式调用的完整链路

return this.chatClient.prompt()
    .system(promptSystem -> promptSystem
        .text(this.systemPromptConfig.getChatSystemMessage().get())  // 系统提示词
        .param("now", DateUtil.now())  // 模板变量
    )
    .user(question)
    .stream()
    .chatResponse()
    .map(chatResponse -> {
        String text = chatResponse.getResult().getOutput().getText();
        return ChatEventVO.builder()
            .eventData(text)
            .eventType(ChatEventTypeEnum.DATA.getValue())
            .build();
    })
    .concatWith(Flux.just(ChatEventVO.builder()
        .eventType(ChatEventTypeEnum.STOP.getValue())
        .build()));

逐方法拆解:

方法 所属 作用
prompt() ChatClient 创建一次请求
system(...) ChatClient 设置系统提示词
user(question) ChatClient 设置用户问题
stream() ChatClient 发起流式调用,返回 Flux
chatResponse() ChatClient 取出完整响应对象(含元数据)
map(...) Flux (Reactor) 逐段转换数据类型
concatWith(...) Flux (Reactor) 在流末尾追加数据
Flux.just(...) Flux (Reactor) 把单个对象包装成流

注意区分:prompt() 到 chatResponse() 是 ChatClient 的方法,从 map() 开始都是 Reactor 框架的 Flux 方法。ChatClient 负责调大模型,Flux 负责处理数据流。

Lambda 表达式在这里的作用

.system(promptSystem -> promptSystem.text("...").param("now", DateUtil.now()))

-> 是 Java 的 Lambda 语法。用 Lambda 是因为对象不直接给你用。如果框架直接把对象给你,你就不需要 Lambda,直接操作就行。正因为框架内部创建对象、内部管理,才通过 Lambda 回调的方式让你"借用"一下配置。

跟 StringBuilder 类似,都是构建器模式,但 StringBuilder 是你自己拿着对象操作,Lambda 方式是你把操作逻辑交给框架。

参数名随便取,promptSystem、s、p 都一样。

为什么封装成 JSON 对象而不是纯文本

// 简单写法:返回纯文本
Flux<String> stream = chatClient.prompt().user("你好").stream().content();
// 前端收到:你好

// 项目写法:返回 JSON 对象
Flux<ChatEventVO> stream = ...
// 前端收到:{"eventData":"你好","eventType":"DATA"}

封装成 JSON 是行业通用做法。好处是前端能通过 eventType 区分数据类型:

{"eventData":"你好","eventType":"DATA"}    ← 渲染文字
{"eventData":",我是","eventType":"DATA"}  ← 渲染文字
{"eventData":"AI助手","eventType":"DATA"}  ← 渲染文字
{"eventData":null,"eventType":"STOP"}      ← 输出结束

后续要扩展也方便,比如加 THINKING(思考中)、ERROR(出错)等类型,直接加枚举值就行,不用改协议。

Java 对象怎么变成 JSON 的

代码里全程操作的都是 Java 对象(ChatEventVO),没有写任何 JSON 转换代码。Spring 看到返回类型是对象,自动用 Jackson 序列化成 JSON 发出去:

ChatEventVO 对象 → Spring 自动序列化 → {"eventData":"你好","eventType":"DATA"} → HTTP 发给前端

如果返回 Flux<String>,Spring 就直接发纯文本,不走 JSON 序列化。

响应式编程(Reactor)

Flux 是 Reactor 框架的核心类,代表一个持续产出数据的数据流。响应式编程的思想是:数据不是一次性给你,而是像水流一样持续推过来,你定义"收到数据时怎么处理"。

常用操作符:

.map(x -> ...)        // 转换流中的每个元素
.concatWith(...)      // 在流末尾拼接另一个流
Flux.just(...)        // 创建只有一个元素的流

AI 流式对话场景天然适合响应式编程,因为大模型生成内容本身就是逐步产出的。不需要专门系统学 Reactor,项目里用到什么记什么,够用。

前后端配合

流式接口需要前后端约定:

  • 传输协议:Spring WebFlux 的 Flux 默认用 SSE(Server-Sent Events)推送,前端用 EventSource 接收
  • 事件格式:前端要知道 ChatEventVO 的字段名和含义
  • 结束标记:前端收到 STOP 事件就知道输出结束,可以关闭连接

系统提示词

系统提示词是大模型收到的第一条消息,定义大模型的角色和行为规范,不会显示给用户。对回答质量和风格影响很大。

.system(s -> s.text("你是一个专业的Java老师"))
.user("什么是多态?")

大模型实际收到的消息结构:

[系统] 你是一个专业的Java老师 [用户] 什么是多态? [助手] 多态是指..

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐