乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • Spring AI 学习路径
  • 第1章 Spring AI 概述与核心理念

    • 章节导读
    • Spring AI 概述与可移植 API
    • 核心模块与依赖关系
  • 第2章 快速入门与第一个AI应用

    • 章节导读
    • 环境准备与配置
    • 第一个 AI 对话应用
  • 第3章 聊天模型与ChatClient

    • 章节导读
    • ChatClient 详解
    • ChatModel 底层抽象
    • 多轮对话与 ChatMemory
  • 第4章 提示词管理与模板

    • 章节导读
    • Prompt 与 Message 体系
    • 提示词模板与动态构建
  • 第5章 输出解析与结构化响应

    • 章节导读
    • 输出解析器与 BeanOutputConverter
  • 第6章 嵌入模型与向量存储

    • 章节导读
    • EmbeddingModel 与文本向量化
    • ETL 数据注入流水线
    • 向量存储抽象与配置
  • 第7章 检索增强生成(RAG)

    • 章节导读
    • RAG 核心机制与流程
    • QuestionAnswerAdvisor 详解
    • RAG 实战案例
  • 第8章 函数调用(Function Calling)

    • 章节导读
    • Tool 注解与函数声明
    • 函数调用实战
  • 第9章 多模态

    • 章节导读
    • 图像输入与视觉模型
    • 图像生成
  • 第10章 Advisor拦截器链

    • 章节导读
    • Advisor 链与内置拦截器
  • 第11章 MCP 协议与跨语言工具集成

    • 章节导读
    • MCP 协议深入
  • 第12章 可观测性测试与最佳实践

    • 章节导读
    • 可观测性与指标监控
    • 测试策略与 Mock 实践
    • 最佳实践与面试考点汇总

白歌对小崔说:"ChatClient 好用,但你要理解底下那层 ChatModel。当你的消息流需要完全手动控制时,直接拿 ChatModel 会更灵活。"

ChatModel 底层抽象

定义与作用

ChatModel 是 Spring AI 中与 AI 供应商通信的底层抽象接口。它定义了与任何 LLM 交互的标准化契约:

public interface ChatModel {
    ChatResponse call(Prompt prompt);
    Flux<ChatResponse> stream(Prompt prompt);
}

ChatClient 在内部将请求委托给 ChatModel,ChatModel 再通过各提供商的 HTTP 客户端(OpenAI、Ollama 等)发送请求。

核心原理:ChatClient 与 ChatModel 的分工

图释:ChatClient 负责所有"智能"功能(Advisor、Tool Calling、记忆管理),ChatModel 只负责"传输"——把构造好的 Prompt 发给 LLM 并返回 Response。

ChatClient vs ChatModel 对比

维度ChatClientChatModel
定位高层门面,开发者主接口底层抽象,与 LLM 直接通信
API 风格Fluent API 链式调用call(Prompt) / stream(Prompt)
Advisor自动链式处理不参与,需手动封装
Tool Calling自动调度不参与,需手动管理 ToolResponseMessage
ChatMemory通过 Advisor 自动注入不参与,需手动管理消息列表
适用场景90% 的日常对话场景底层消息流操控、自定义调度逻辑
创建方式ChatClient.Builder 注入各 Starter 自动配置,直接 @Autowired

何时直接使用 ChatModel

以下场景 ChatClient 不够灵活,应直接操作 ChatModel:

  1. 手动管理 Tool Calling 循环:需要精确控制工具调用何时结束
  2. 动态构造复杂 Prompt:消息列表需要在运行时动态拼接多种类型
  3. 自定义重试/降级逻辑:需要在一次调用失败后切换备用模型
  4. 精细的 Token 预算控制:手动计算消息 Token 数并裁剪历史

完整示例一:直接使用 ChatModel 进行底层调用

场景说明

小崔需要手动构建 Message 列表,精确控制每一条消息的内容和角色。

关键代码

@RestController
public class ModelController {

    private final ChatModel chatModel;

    public ModelController(ChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @GetMapping("/model-chat")
    public String modelChat(@RequestParam String question) {
        // 手动构造完整的 Prompt
        Prompt prompt = new Prompt(List.of(
                new SystemMessage("""
                    你是飞翔科技大学的学生助手。
                    如果不知道答案,请直接说"我不确定"。
                    """),
                new UserMessage(question)
        ));

        ChatResponse response = chatModel.call(prompt);
        return response.getResult().getOutput().getContent();
    }
}

运行结果

GET /model-chat?question=计算机科学专业需要修多少学分

回复:根据飞翔科技大学2024级培养方案,计算机科学专业要求修满170学分,
其中通识教育40学分、学科基础45学分、专业核心50学分、实践环节35学分。

完整示例二:手动管理多轮对话(无 ChatMemory)

场景说明

在没有 ChatMemory 的情况下,小崔手动维护对话历史列表。

关键代码

@RestController
public class ManualMemoryController {

    private final ChatModel chatModel;
    // 手动维护的消息历史
    private final List<Message> history = new ArrayList<>();

    public ManualMemoryController(ChatModel chatModel) {
        this.chatModel = chatModel;
        // 初始化 System Message
        history.add(new SystemMessage("你是飞翔科技大学的学生助手。"));
    }

    @GetMapping("/manual-chat")
    public String chat(@RequestParam String message) {
        // 追加用户消息到历史
        history.add(new UserMessage(message));

        // 用完整历史构造 Prompt
        Prompt prompt = new Prompt(new ArrayList<>(history));
        ChatResponse response = chatModel.call(prompt);

        // 追加 AI 回复到历史
        String answer = response.getResult().getOutput().getContent();
        history.add(new AssistantMessage(answer));

        return answer;
    }

    @GetMapping("/manual-chat/clear")
    public String clearHistory() {
        history.clear();
        history.add(new SystemMessage("你是飞翔科技大学的学生助手。"));
        return "对话历史已清除";
    }
}

运行结果

GET /manual-chat?message=我叫张三
→ "你好张三!有什么可以帮助你的?"

GET /manual-chat?message=我叫什么名字
→ "你叫张三。"   ← 手动维护的历史生效

GET /manual-chat/clear
→ "对话历史已清除"

GET /manual-chat?message=我叫什么名字
→ "抱歉,我不知道你的名字。"   ← 历史已清除

易错场景与面试考点

易错场景一:ChatModel 不会自动注入 SystemMessage

// ❌ 错误:以为 ChatModel 会像 ChatClient 一样自动注入 defaultSystem
ChatModel chatModel;
Prompt prompt = new Prompt(List.of(
    new UserMessage("今天天气怎么样")
));
// SystemMessage 缺失!模型可能回答得不够聚焦

问题分析

ChatModel 是纯粹的传输层,不做任何自动注入。SystemMessage 必须手动添加到 Prompt 中。

// ✅ 正确:手动添加 SystemMessage
Prompt prompt = new Prompt(List.of(
    new SystemMessage("你是专业助手。"),
    new UserMessage("今天天气怎么样")
));

易错场景二:流式调用 ChatModel 时错误地收集结果

// ❌ 错误:用 Flux.collectList() 阻塞流式响应
Flux<ChatResponse> flux = chatModel.stream(prompt);
List<ChatResponse> list = flux.collectList().block();
String result = list.get(list.size() - 1)   // 取最后一条
        .getResult().getOutput().getContent();
// 失去了流式的实时性优势

问题分析

流式的价值在于实时推送。如果最终还是要阻塞等待全部结果,直接使用 call() 更合适。

// ✅ 正确:流式逐 Token 处理
return chatModel.stream(prompt)
        .map(resp -> resp.getResult().getOutput().getContent())
        .filter(Objects::nonNull);

面试高频题

Q1:ChatClient 和 ChatModel 的分工边界是什么?

ChatClient 负责智能层(Fluent API、Advisor 链、Tool Calling 调度、ChatMemory 注入),ChatModel 负责传输层(把 Prompt 发给 LLM,返回 ChatResponse)。日常开发用 ChatClient,需要手动控制消息流时用 ChatModel。

Q2:什么场景下应该绕过 ChatClient 直接使用 ChatModel?

①需要手动管理 Tool Calling 的多轮循环;②需要动态构造复杂 Prompt 消息序列;③需要自定义重试/降级逻辑(如失败后切换模型);④需要精确的 Token 预算控制(手动裁剪消息历史)。

本章小结

  • ChatModel 是底层抽象,只有 call(Prompt) 和 stream(Prompt) 两个方法
  • ChatClient 内部委托给 ChatModel,并附加 Advisor、Tool Calling、ChatMemory 等智能层
  • 直接使用 ChatModel 需手动管理 SystemMessage、对话历史、工具调用循环
  • 90% 场景用 ChatClient,10% 场景(底层消息操控)用 ChatModel
上一页
ChatClient 详解
下一页
多轮对话与 ChatMemory