ChatClient 是 Spring AI 中用于与大语言模型(LLM)交互的核心 API。它采用了流式(Fluent)API 设计,非常类似于 Spring 生态中的 WebClientRestClient

以下是一个基于 Spring AI 最新特性的使用 Demo,涵盖了依赖配置、基础调用以及带参数的复杂调用。


1. 依赖配置 (Maven)

确保你的 pom.xml 中包含了 Spring AI 的相关依赖。

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

2. 基础使用 Demo

在 Spring 中,推荐通过 ChatClient.Builder 来创建实例。你可以直接在 Service 或 Controller 中注入并使用。

代码实现

@RestController
public class ChatController {

    private final ChatClient chatClient;

    // 通过构造函数注入 Builder 并创建 ChatClient
    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return this.chatClient.prompt()
                .user(message)
                .call()
                .content(); // 直接获取字符串内容
    }

    @GetMapping("/chat-complex")
    public Map<String, Object> complexChat(@RequestParam String topic) {
        return this.chatClient.prompt()
                .system("你是一个专业的科普作家,请用幽默的语气回答。")
                .user(u -> u.text("请解释什么是 {topic}").param("topic", topic))
                .call()
                .entity(new ParameterizedTypeReference<Map<String, Object>>() {}); 
                // 自动将输出解析为 Map 或 POJO
    }
}

3. 运行输出示例

场景 A:简单对话

请求: GET /chat?message=你是谁?

输出:

"你好!我是由 Google 训练的大语言模型。我可以帮你回答问题、编写代码、翻译文本等。有什么我可以帮你的吗?"


场景 B:结构化与系统角色 (Complex Chat)

请求: GET /chat-complex?topic=量子纠缠

输出 (JSON格式):

{
  "topic": "量子纠缠",
  "explanation": "想象一下你有两只神奇的袜子,当你给左脚穿上红色袜子时,远在宇宙另一端的右脚袜子瞬间也变成了红色!这就是量子纠缠,爱因斯坦曾称之为‘幽灵般的超距作用’。",
  "funny_note": "如果快递也能有这种速度,我就再也不给差评了。"
}

4. 核心 API 关键点

为了让你更好地理解 ChatClient 的工作流,可以参考下面的逻辑结构:

  • prompt(): 开始构建一个请求。
  • system(String): 设置系统提示词(设定 AI 的“人设”)。
  • user(String): 设置用户输入。
  • call(): 阻塞式调用,等待完整响应。
  • stream(): 流式调用,返回 Flux<String>(适用于打字机效果)。
  • entity(): 强大的解析器,可以直接把 AI 的文本回复转成 Java 对象(POJO)。

在 Spring AI 的 ChatClient 体系中,prompt() 之后的链式调用非常灵活。/chat-complex 这个示例方法展示了 Fluent API 的核心能力。

为了更深入理解,我们可以将 ChatClient 的参数配置分为四个维度:消息角色 (Roles)模板替换 (Templating)配置参数 (Options)输出处理 (Output)


1. 消息角色参数 (Messaging Roles)

这是构建对话的基础,决定了 AI 的行为逻辑。

  • .system(String text): 定义 AI 的“人格”。例如:"你是一个只用 SQL 回答问题的数据库专家"
  • .user(String text): 用户的具体指令或问题。
  • .messages(List<Message> messages): 如果你需要传入一段完整的历史对话上下文(Context),可以使用此方法。

2. 动态模板与参数 (Prompt Templates)

ChatClient 支持类似 SQL 占位符的操作,避免了手动拼接字符串的繁琐和风险。

  • .user(u -> u.text(template).param("key", value)):
    • template: 包含占位符的字符串,如 "请帮我翻译这段话到 {language}: {text}"
    • .param(): 自动将 {language}{text} 替换为实际变量。

3. 模型运行配置 (Chat Options)

你可以针对单次请求覆盖全局配置。

  • .options(ChatOptions options):
    • Temperature: 控制随机性(0.0 趋向稳定,1.0 趋向发散)。
    • TopP / TopK: 采样策略参数。
    • Model: 临时切换模型(例如从 gpt-3.5 切换到 gpt-4o)。

4. 响应转换参数 (Response Transformation)

这是 ChatClient 最强大的地方,它能将 AI 的模糊文本直接“序列化”为 Java 对象。

  • .call(): 触发阻塞式同步调用。
  • .stream(): 触发流式响应(返回 Flux<String>)。
  • .entity(Class<T> type): 将结果自动转为 POJO 类。
  • .entity(new ParameterizedTypeReference<List<MyBean>>() {}): 处理复杂的泛型集合(如返回一个对象列表)。

完整参数示例代码

@GetMapping("/chat-ultra")
public ProjectInfo ultraChat(@RequestParam String projectName) {
    return this.chatClient.prompt()
            // 1. 系统约束
            .system("你是一个高级架构师,请输出 JSON 格式的架构方案")
            // 2. 动态模板
            .user(u -> u.text("为项目 {name} 生成一份技术栈建议")
                        .param("name", projectName))
            // 3. 运行参数定制
            .options(OpenAiChatOptions.builder()
                        .withTemperature(0.7)
                        .withModel("gpt-4-turbo")
                        .build())
            // 4. 自动类型转换
            .call()
            .entity(ProjectInfo.class); 
}

输出结果预测 (ProjectInfo 对象)

如果 ProjectInfo 类包含 name, language, database 等字段,输出将是:

ProjectInfo[name="电商中台", language="Java/Spring Boot", database="PostgreSQL/Redis"]


在 Spring AI 中,要实现对话记忆(Memory),我们通常使用 Advisor(顾问/建议者)。这是 ChatClient 最优雅的设计之一,它能自动处理对话历史的存储和读取,不需要你手动去维护一个 List<Message>

以下是如何在 ChatClient 中加入 ChatMemoryAdvisor 的完整 Demo。


1. 基础配置:引入内存存储

首先,你需要一个存储对话的地方。最简单的做法是使用内存级别的 InMemoryChatMemory

@Configuration
public class AiConfig {
    @Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }
}

2. 使用 Advisor 的 Controller 示例

我们在创建 ChatClient 时,通过 .defaultAdvisors() 加入记忆功能。这样,AI 就能“记住”上一次你说了什么。

@RestController
public class MemoryChatController {

    private final ChatClient chatClient;

    public MemoryChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
        this.chatClient = builder
                .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) // 核心:加入记忆顾问
                .build();
    }

    @GetMapping("/memo")
    public String memoChat(@RequestParam String chatId, @RequestParam String message) {
        return this.chatClient.prompt()
                .user(message)
                // 通过 ADVISOR_CHAT_ID_VALUE 指定这是哪一个人的对话 ID
                .advisors(a -> a.param(AbstractChatMemoryAdvisor.CHAT_ID_SIZE, chatId)) 
                .call()
                .content();
    }
}

3. 对话输出演练

记忆功能的效果通常体现在“多轮对话”中:

第一轮对话

请求: GET /memo?chatId=user123&message=你好,我是张三
输出: > "你好,张三!很高兴认识你。有什么我可以帮你的吗?"

第二轮对话(AI 记住了名字)

请求: GET /memo?chatId=user123&message=我还记得我叫什么吗?
输出: > "当然记得,你刚才告诉我你叫张三。"


4. Advisor 的工作原理

Advisor 就像是请求发送前后的一个“拦截器”:

  1. 发送前:它从 ChatMemory 中根据 chatId 捞出最近的几条历史记录,塞进 Prompt 里。
  2. 回复后:它自动把 AI 的回复和你的新问题存回 ChatMemory

进阶提示:持久化存储

如果你的应用重启了,InMemoryChatMemory 的数据会丢失。在生产环境下,Spring AI 支持将记忆存储在 Redis、Cassandra 或 JDBC 数据库中。你只需要更换 ChatMemory 的 Bean 实现即可:

// 伪代码示例:使用 Redis 存储
@Bean
public ChatMemory chatMemory(RedisTemplate redisTemplate) {
    return new RedisChatMemory(redisTemplate);
}

如何利用 Advisor 实现“检索增强生成(RAG)”,让 AI 读取你私有的 PDF 或文档库

太棒了,这正是 Spring AI 最具威力的部分。RAG (Retrieval Augmented Generation) 让 AI 不再只是“纸上谈兵”,而是能根据你提供的私有文档(如 PDF、Markdown、Word)来回答问题。

在 Spring AI 中,实现 RAG 同样是通过 QuestionAnswerAdvisor 来完成的。


1. RAG 的核心组件

要让 AI 读懂你的文档,需要以下三个角色协同工作:

  1. VectorStore:向量数据库(如 PGVector, Redis, Pinecone),用于存储文档的“特征”。
  2. Resource:你的原始文档(PDF 或文本)。
  3. QuestionAnswerAdvisor:拦截用户提问,先去数据库搜资料,再把资料喂给 AI。

2. 简易 RAG 代码实现

第一步:初始化向量数据库并导入文档

在实际应用中,你通常会有一个专门的 Service 来解析并上传文档。

@Service
public class DocumentService {
    private final VectorStore vectorStore;

    public DocumentService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public void loadDocs(Resource pdfResource) {
        // 1. 加载并拆分文档(防止单次输入太长)
        TikaDocumentReader reader = new TikaDocumentReader(pdfResource);
        TokenTextSplitter splitter = new TokenTextSplitter();
        
        // 2. 写入向量数据库(此过程会自动调用 Embedding 模型将文字转为向量)
        vectorStore.accept(splitter.apply(reader.get()));
    }
}

第二步:在 ChatClient 中配置 Advisor

现在,你的 AI 就拥有了“查资料”的能力。

@RestController
public class RagController {

    private final ChatClient chatClient;

    public RagController(ChatClient.Builder builder, VectorStore vectorStore) {
        this.chatClient = builder
                // 核心:注入 QA 顾问,它会自动根据用户问题去 vectorStore 检索
                .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))
                .build();
    }

    @GetMapping("/ask-docs")
    public String ask(@RequestParam String message) {
        return this.chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

3. RAG 运行原理与输出

当用户发送请求时,底层发生的事情如下:

运行输出示例

假设你上传了一份关于“公司 2026 年报”的 PDF。

请求: GET /ask-docs?message=公司去年的利润是多少?

AI 的思考过程(隐式):

  1. 检索:从向量数据库中找到相关段落:“...2025年度净利润为 1.2 亿人民币...”
  2. 增强:将该段落作为上下文,重写 Prompt 给 LLM。
  3. 生成:输出最终答案。

最终输出:

"根据您提供的 2026 年报资料,公司在 2025 年度的净利润为 1.2 亿人民币,较前一年增长了 15%。"


4. 为什么选择这种方式?

  • 解耦:你不需要在业务逻辑里写“搜索”代码。
  • 低成本:只在需要时检索相关片段,而不是把整个 PDF 发给 AI,节省 Token。
  • 动态更新:只要往 VectorStore 里新增文档,AI 的知识库就会实时更新。

总结:你的 ChatClient 技能树

目前我们已经解锁了:

  1. Fluent API:优雅的链式调用。
  2. Structured Output:自动转 POJO。
  3. Memory Advisor:多轮对话记忆。
  4. QA Advisor (RAG):基于私有知识库回答。