Java开发者的大模型入门:Spring AI组件全攻略(一)

0 阅读26分钟

一、开篇:为什么Java开发者需要Spring AI

1.1 大模型浪潮下,Java 开发者的机遇与挑战

过去两年,大语言模型(LLM)以 ChatGPT 为代表席卷全球,从对话助手到代码生成,从内容创作到企业知识库,AI 能力正以前所未有的速度重塑软件开发。Python 凭借其丰富的 AI 库(如 LangChain、Transformers)成为这场变革的主角,而作为企业级后端的中流砥柱——Java 开发者,自然要思考:如何将大模型的能力无缝集成到现有的 Java 系统中?

直接调用大模型 API(例如 OpenAI 的接口)听起来很简单——发一个 HTTP 请求,拿回一段文本。但在实际生产环境中,我们会面临一系列棘手的问题:

  • 复杂的调用细节:需要手动构建 JSON 请求体、处理 HTTP 连接、解析流式响应、处理鉴权和错误重试。
  • 提示词管理困难:业务场景往往需要动态构造提示词,拼接用户输入、历史对话、系统指令,代码很快就变得难以维护。
  • 对话状态维护:实现一个多轮对话机器人,必须自己维护会话历史,并在每次请求时把历史消息都带上。
  • 输出不可控:大模型返回的是自然语言文本,如果想让 AI 返回结构化的数据(例如 JSON、对象),还需要自己编写解析器和异常处理。
  • 知识库集成复杂:要让模型基于企业内部知识回答问题(RAG),需要自己实现文档加载、文本分割、向量化、向量检索等一系列组件。

如果每个项目都从零开始重复造这些轮子,不仅开发效率低,而且容易出错,更难以应对模型切换、版本升级等变化。

1.2 原始调用方式:一个直观的对比

让我们先看一段最原始的 Java 代码,它使用 HttpClient 调用 OpenAI 的聊天接口:

// 原始方式:直接调用 OpenAI API
HttpClient client = HttpClient.newHttpClient();
String requestBody = """
    {
        "model": "gpt-3.5-turbo",
        "messages": [
            {"role": "user", "content": "你好,请介绍一下自己"}
        ]
    }
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.openai.com/v1/chat/completions"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer YOUR_API_KEY")
    .POST(HttpRequest.BodyPublishers.ofString(requestBody))
    .build();

try {
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    // 手动解析 JSON 响应
    JSONObject json = new JSONObject(response.body());
    String answer = json.getJSONArray("choices")
                        .getJSONObject(0)
                        .getJSONObject("message")
                        .getString("content");
    System.out.println("AI 回答:" + answer);
} catch (Exception e) {
    e.printStackTrace();
}

这段代码虽然能工作,但存在明显的问题:

  • 硬编码 API 地址和密钥,难以管理。
  • JSON 构造和解析冗长且脆弱(字段名变化、空值处理)。
  • 没有错误重试、超时控制等生产级必备机制。
  • 如果要支持多轮对话,需要自己拼接历史消息数组,代码复杂度会急剧上升。

1.3 Spring AI 登场:Spring 官方出品的 AI 集成框架

Spring AI 正是为了解决上述痛点而诞生的。它是 Spring 官方团队(VMware 旗下)推出的全新项目,旨在将 Spring 生态的核心理念——依赖注入、自动配置、POJO 编程——带入 AI 开发领域。它的目标很简单:让 Java 开发者用最熟悉的方式,像调用普通方法一样使用大模型。

让我们用 Spring AI 重写上面的功能:

// Spring AI 方式:简洁、直观
@RestController
public class ChatController {
    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

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

短短几行代码,没有 JSON 处理,没有 HTTP 细节,甚至不需要自己解析响应。这就是 Spring AI 的魅力——它将所有底层复杂性封装起来,让你专注于业务逻辑。而且,它与 Spring Boot 无缝集成,配置简单,开箱即用。

1.4 Spring AI 的核心优势

优势说明
遵循 Spring 生态设计哲学依赖注入、自动配置、面向接口编程,让你用最熟悉的 Spring 方式开发 AI 应用。ChatClient 就像 RestTemplate 一样简单。
统一 API 抽象支持 OpenAI、Azure、Ollama、通义千问、DeepSeek 等主流模型提供商,切换模型只需修改配置文件,业务代码无需改动。
与 Spring Boot 无缝集成通过 Starter 自动配置,只需引入依赖并配置 API 密钥,即可注入 ChatClientEmbeddingClient 等核心 Bean。
企业级特性内置流式响应(SSE)、函数调用、向量存储抽象、缓存集成、监控指标(Actuator),满足生产环境需求。
可扩展性基于 Spring 的扩展机制,可以轻松定制 ChatClient 的行为,添加拦截器、自定义输出转换器等。
面向 RAG 的完整工具链提供 VectorStore 抽象、文档加载器、分割器、嵌入客户端,让构建知识库问答系统变得轻而易举。

下图展示了 Spring AI 的核心组件及其关系:

graph TD
    subgraph 应用层
        A[ChatClient] --> B[提示词模板]
        A --> C[函数调用]
        A --> D[输出解析]
    end
    
    subgraph 模型层
        E[ChatModel] --> F[OpenAI]
        E --> G[Azure]
        E --> H[Ollama]
        E --> I[通义千问]
    end
    
    subgraph 知识库层
        J[VectorStore] --> K[PGvector]
        J --> L[Milvus]
        J --> M[Redis]
        N[DocumentLoader] --> O[分割器]
        O --> P[嵌入客户端]
        P --> J
    end
    
    A --> E
    A -.-> J
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style E fill:#bbf,stroke:#333
    style J fill:#bfb,stroke:#333

1.5 本文目标:零基础全组件实战

如果你是一名 Java 开发者,但从未接触过大模型开发,不用担心。本文将带你从零开始,一步一步亲手实践 Spring AI 的所有核心组件。你将学到:

  • 基础篇ChatClient 的基本用法、提示词模板、流式响应
  • 结构化输出:让 AI 直接返回 Java 对象,告别手动解析
  • 函数调用:让 AI 调用你的 Java 方法(工具),获取实时信息
  • 多模态:处理图片、语音(如适用)
  • RAG 篇:构建基于私有知识库的问答系统(向量存储、文档处理)
  • 注解式开发:用自定义注解简化 AI 服务调用
  • 生产级特性:缓存、监控与 Spring Boot Actuator 集成
  • 微服务集成:与 Spring Cloud 生态结合,实现配置中心、灰度发布

二、环境准备:5分钟搭建第一个Spring AI应用

在开始动手之前,我们先确保你的开发环境就绪。本章将带领你完成从零到第一个可运行REST API的全过程,全程只需5分钟。

2.1 开发环境要求

  • JDK 17 或更高版本(Spring Boot 3.x 要求 JDK 17+)
  • Maven(3.6+)或 Gradle(7.x+)—— 任选一种你熟悉的构建工具
  • IDE:IntelliJ IDEA、Eclipse 或 VS Code(Java插件)
  • 网络连接:能够访问公网(因为需要调用大模型API)

如果你还没有安装 JDK 或 Maven/Gradle,请先自行安装。这里不再赘述。

2.2 在项目中引入 Spring AI 依赖

我们将创建一个最简单的 Spring Boot Web 项目,并添加 Spring AI 的依赖。

方式一:使用 Spring Initializr 快速创建(推荐)

  1. 访问 start.spring.io/
  2. 选择以下选项:
    • Project:Maven 或 Gradle(这里以 Maven 为例)
    • Language:Java
    • Spring Boot:选择 3.2.x 或更高版本(3.1+ 也可以,但建议 3.2)
    • Groupcom.example
    • Artifactspring-ai-demo
    • Dependencies:添加 Spring WebOpenAI(Spring AI 的 OpenAI Starter)
  3. 点击 Generate 下载项目压缩包,解压后导入 IDE。

方式二:手动配置 Maven

如果不想通过 Initializr,可以手动创建 pom.xml 文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>spring-ai-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.0-M2</spring-ai.version> <!-- 使用最新里程碑版本 -->
    </properties>

    <dependencies>
        <!-- Spring Boot Web Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring AI OpenAI Starter -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <!-- Spring AI 里程碑仓库(如果使用里程碑版本需要添加) -->
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
</project>

注意:Spring AI 目前(2025年)仍处于快速迭代阶段,最新版本可能为 1.0.0-M2 或更高。请访问 Spring AI 官方文档 查看最新版本号。里程碑版本需要添加 Spring Milestones 仓库。

2.3 配置大模型 API 凭证

src/main/resources/application.yml 中配置 OpenAI 相关参数。如果你使用 OpenAI 官方 API,配置如下:

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}  # 从环境变量读取,避免硬编码
      base-url: https://api.openai.com  # 可选,默认就是官方地址
      chat:
        options:
          model: gpt-4o-mini  # 或 gpt-3.5-turbo
          temperature: 0.7

如果你使用的是兼容 OpenAI API 的其他服务商(如 DeepSeek、通义千问等),只需修改 base-urlapi-key。例如 DeepSeek:

spring:
  ai:
    openai:
      api-key: ${DEEPSEEK_API_KEY}
      base-url: https://api.deepseek.com/v1
      chat:
        options:
          model: deepseek-chat

为了安全,不要将 API 密钥直接写在配置文件中。推荐通过环境变量设置:

  • Linux/Macexport OPENAI_API_KEY=sk-xxxx
  • Windowsset OPENAI_API_KEY=sk-xxxx

或者在 IDE 的运行配置中设置环境变量。如果你只是快速测试,也可以临时在配置文件中写死,但切记不要提交到代码仓库。

2.4 编写第一个程序:ChatClient 基础用法

现在我们来创建一个 REST 控制器,注入 ChatClient,实现一个简单的聊天接口。

2.4.1 创建 Controller

com.example.demo 包下创建 ChatController.java

package com.example.demo;

import org.springframework.ai.chat.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {

    private final ChatClient chatClient;

    // 通过构造器注入 ChatClient.Builder,然后构建 ChatClient
    public ChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/chat")
    public String chat(@RequestParam(defaultValue = "你好,请介绍一下自己") String message) {
        // 使用 prompt().user() 设置用户消息,call() 调用模型,content() 获取文本回复
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

2.4.2 代码逐行解释

  • ChatClient.Builder:由 Spring AI 自动配置创建的构建器,用于创建 ChatClient 实例。我们通过构造器注入它,然后调用 build() 方法得到 ChatClient
  • chatClient.prompt():开始构建一个提示词(Prompt)。
  • .user(message):设置用户消息。也可以设置系统消息(.system())。
  • .call():发起同步调用,等待模型返回完整响应。
  • .content():从响应中提取文本内容。如果需要完整响应对象(包含 Token 用量等),可以调用 call() 返回 ChatResponse

2.4.3 启动类

确保项目有标准的 Spring Boot 启动类(Initializr 会自动生成):

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

2.4.4 运行测试

  1. 启动应用(运行 DemoApplicationmain 方法)。
  2. 打开浏览器或使用 curl 访问:http://localhost:8080/chat?message=你好
  3. 你将看到类似如下的 JSON 格式响应(纯文本):
你好!我是AI助手,有什么可以帮你的吗?

完整流程示意图:

sequenceDiagram
    participant 浏览器
    participant Controller
    participant ChatClient
    participant OpenAI API

    浏览器->>Controller: GET /chat?message=你好
    Controller->>ChatClient: prompt().user(message).call()
    ChatClient->>OpenAI API: HTTP请求(包含消息)
    OpenAI API-->>ChatClient: 返回AI生成的回复
    ChatClient-->>Controller: 返回content()
    Controller-->>浏览器: 返回"你好,我是AI助手..."

2.4.5 可能遇到的问题及解决

  • 启动失败,提示找不到 ChatClient.Builder:检查依赖是否正确引入,以及 Spring AI 的自动配置是否生效。可以尝试在配置类上添加 @EnableAutoConfiguration 或检查 @SpringBootApplication 是否正常扫描。
  • 调用时报错 401:API 密钥配置错误或未设置环境变量。检查 application.yml 中的 api-key 是否正确。
  • 响应慢:网络问题,可尝试更换网络或使用代理。

2.5 本章小结

通过本章的学习,你成功搭建了第一个 Spring AI 应用,并实现了最基础的聊天接口。你学会了:

  • 使用 Spring Initializr 快速创建包含 Spring AI 的项目
  • 配置 OpenAI(或其他兼容 API)的凭证
  • 编写 REST 控制器,注入并使用 ChatClient
  • 运行并测试第一个 AI 接口

三、核心交互:ChatClient 与提示词管理

通过上一章,你已经成功搭建了第一个 Spring AI 应用,并实现了最简单的“一问一答”接口。但实际应用中,我们往往需要更精细的控制:给 AI 设定角色(系统消息)、动态构造提问内容、获取完整的响应元数据(如 Token 消耗)等。本章将带你深入掌握 ChatClient 的核心 API,并学习如何使用提示词模板来管理复杂的提示词。

3.1 ChatClient 的核心 API 解析

ChatClient 是 Spring AI 中用于与大模型交互的核心接口。它采用流式(fluent)API 设计,让你可以像搭积木一样构建请求。我们先用一个更完整的例子来展示它的主要方法。

3.1.1 基础用法回顾

最简单的用法:

String response = chatClient.prompt()
        .user("你好")
        .call()
        .content();

prompt() 返回的 Prompt 构建器远不止这些功能。

3.1.2 主要方法详解

方法描述示例
prompt()开始构建一个新的提示词(Prompt)chatClient.prompt()
.user(String text)添加用户消息(纯文本).user("你好")
.system(String text)添加系统消息.system("你是一个友好的助手")
.messages(List<Message> messages)直接添加多条消息(用于多轮对话)见下文示例
.options(ChatOptions options)设置模型参数(如温度、最大 Token 等).options(OpenAiChatOptions.builder().temperature(0.8).build())
.advisors(Advisor... advisors)添加顾问(用于增强功能,如记忆、RAG 等)后续章节介绍
call()发起同步调用,返回 ChatResponse.call()
stream()发起流式调用,返回 Flux<ChatResponse>下一章介绍
.content()ChatResponse 中提取文本内容(快捷方式).call().content()
.entity(Class<T> type)将响应解析为指定类型的 Java 对象第五章介绍

3.1.3 获取完整响应对象

除了直接获取文本内容,有时我们需要获取 Token 消耗、响应元数据等信息。此时可以调用 call() 得到 ChatResponse 对象:

ChatResponse response = chatClient.prompt()
        .user("你好")
        .call();

// 获取生成的文本
String content = response.getResult().getOutput().getContent();

// 获取 Token 用量
Generation generation = response.getResult();
if (generation.getOutput() != null) {
    // 有些模型会返回 Token 使用情况
    var usage = response.getMetadata(); 
    // 具体获取方式视版本而定,可能通过 response.getMetadata() 或 generation.getMetadata()
}

// 打印完整响应以便调试
System.out.println(response);

注意:Token 用量的获取方式在不同版本中可能有所变化,建议参考当前版本的 Javadoc 或文档。

3.1.4 系统消息与多轮对话

系统消息用于设定 AI 的角色和行为。例如,让 AI 扮演一个 Java 编程导师:

String response = chatClient.prompt()
        .system("你是一个 Java 编程导师,回答要简洁并给出代码示例。")
        .user("请解释一下什么是多态?")
        .call()
        .content();

对于多轮对话,你需要维护一个消息列表,包含之前的用户消息和 AI 回复。Spring AI 的 Prompt 对象可以接受一个消息列表:

import org.springframework.ai.chat.messages.*;

List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个乐于助人的助手。"));
messages.add(new UserMessage("我叫小明。"));
messages.add(new AssistantMessage("你好小明,我是你的助手。"));
messages.add(new UserMessage("我叫什么名字?"));

String response = chatClient.prompt(new Prompt(messages))
        .call()
        .content();

这种方式需要你手动维护消息列表,比较繁琐。Spring AI 提供了 ChatMemory 抽象来简化多轮对话管理,我们将在后续章节介绍。

3.2 提示词模板(PromptTemplate)的使用

在实际业务中,用户消息往往需要动态插入变量,例如“我的订单号是 {orderId},请查询状态”。如果每次都用字符串拼接,不仅繁琐,而且容易出错(如注入风险)。Spring AI 提供了 提示词模板 功能,让你可以定义带占位符的模板文件,然后通过参数填充。

3.2.1 创建模板文件

src/main/resources 目录下创建 prompts 文件夹(可选),然后创建一个文本文件,例如 order-status.st

我的订单号是 {{orderId}},请帮我查询当前状态。如果订单存在,请告诉我物流信息;如果不存在,请提示我检查订单号。

占位符使用双大括号 {{变量名}} 表示。

3.2.2 加载模板并填充

Spring AI 提供了 PromptTemplate 类来加载模板文件并进行变量替换。

import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.core.io.ClassPathResource;

// 加载模板文件
PromptTemplate promptTemplate = new PromptTemplate(new ClassPathResource("prompts/order-status.st"));

// 准备变量
Map<String, Object> variables = Map.of("orderId", "NO123456789");

// 渲染模板,生成 Prompt 对象
Prompt prompt = promptTemplate.create(variables);

// 发送请求
String response = chatClient.prompt(prompt)
        .call()
        .content();

PromptTemplate 还可以直接接受字符串模板,而不必从文件加载:

String template = "请将以下文本翻译成 {{targetLanguage}}:{{text}}";
PromptTemplate promptTemplate = new PromptTemplate(template);
Prompt prompt = promptTemplate.create(Map.of(
    "targetLanguage", "中文",
    "text", "Hello, world"
));

3.2.3 与 @Value 注解结合使用

如果你更习惯使用 Spring 的 @Value 注解注入文件内容,可以这样做:

@Component
public class OrderService {
    @Value("classpath:prompts/order-status.st")
    private Resource orderStatusTemplate;

    public String queryOrderStatus(String orderId) {
        PromptTemplate promptTemplate = new PromptTemplate(orderStatusTemplate);
        Prompt prompt = promptTemplate.create(Map.of("orderId", orderId));
        return chatClient.prompt(prompt).call().content();
    }
}

3.2.4 模板的优势

  • 关注点分离:提示词与 Java 代码分离,便于维护和修改。
  • 复用性:同一模板可用于不同场景,只需传入不同变量。
  • 安全性:避免字符串拼接导致的注入风险(不过大模型提示词注入是另一回事,但至少代码更清晰)。

3.3 实践:构建一个带角色设定的聊天助手

现在让我们综合运用本章所学,构建一个更实用的聊天助手:一个能记住用户名字并提供个性化问候的助手。我们将使用系统消息设定角色,使用提示词模板动态构造用户消息。

3.3.1 定义接口

创建 GreetingController.java

package com.example.demo;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/greet")
public class GreetingController {

    private final ChatClient chatClient;

    @Autowired
    public GreetingController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping
    public String greet(@RequestParam String name) {
        // 系统消息:设定角色
        String systemMessage = "你是一个友好的接待员,用热情的语气问候客人。";

        // 用户消息模板
        String userTemplate = "你好,我叫 {{name}},请向我问好。";

        // 创建 PromptTemplate
        PromptTemplate promptTemplate = new PromptTemplate(userTemplate);
        Prompt prompt = promptTemplate.create(Map.of("name", name));

        // 发送请求(注意:这里需要将系统消息和用户消息合并)
        // 目前 PromptTemplate.create() 只返回用户消息,我们需要手动组合系统消息
        // 方案:使用 Prompt 的构造器接受消息列表
        List<Message> messages = Arrays.asList(
                new SystemMessage(systemMessage),
                new UserMessage(prompt.getContents()) // prompt.getContents() 返回渲染后的文本
        );
        Prompt fullPrompt = new Prompt(messages);

        return chatClient.prompt(fullPrompt)
                .call()
                .content();
    }
}

3.3.2 简化版本:使用 builder 方法

上述代码略显冗长。实际上 ChatClient 的 fluent API 支持直接添加系统消息和用户消息,我们可以更简洁:

@GetMapping("/simple")
public String greetSimple(@RequestParam String name) {
    return chatClient.prompt()
            .system("你是一个友好的接待员,用热情的语气问候客人。")
            .user("你好,我叫 " + name + ",请向我问好。") // 简单拼接
            .call()
            .content();
}

但如果想使用模板,可以这样:

@GetMapping("/template")
public String greetTemplate(@RequestParam String name) {
    PromptTemplate userPromptTemplate = new PromptTemplate("你好,我叫 {{name}},请向我问好。");
    Prompt userPrompt = userPromptTemplate.create(Map.of("name", name));

    return chatClient.prompt()
            .system("你是一个友好的接待员,用热情的语气问候客人。")
            .user(userPrompt.getContents()) // 直接使用渲染后的文本
            .call()
            .content();
}

3.3.3 测试

启动应用,访问:

  • http://localhost:8080/greet?name=张三
  • http://localhost:8080/greet/simple?name=李四
  • http://localhost:8080/greet/template?name=王五

观察返回的问候语是否符合预期(应该包含名字,且语气热情)。

3.3.4 流程图

graph TD
    A[用户请求 /greet?name=张三] --> B[Controller接收参数]
    B --> C[构造系统消息]
    B --> D[加载用户模板并填充name]
    C --> E[合并消息列表]
    D --> E
    E --> F[调用ChatClient]
    F --> G[返回AI回复]
    G --> H[响应给用户]

3.4 本章小结

通过本章的学习,你掌握了:

  • ChatClient 核心 APIprompt()user()system()call()content() 等。
  • 获取完整响应对象:了解如何获取 Token 用量等元数据。
  • 系统消息的作用:设定 AI 角色和行为。
  • 提示词模板:使用 PromptTemplate 动态构造用户消息,避免硬编码和字符串拼接。
  • 实践:构建了一个带角色设定的个性化问候助手。

四、流式响应:实现打字机效果

在前面的章节中,我们使用 call() 方法同步调用大模型,等待模型生成完整回答后才一次性返回结果。这种方式简单直接,但用户体验上存在一个明显的问题:用户需要等待模型完全生成后才能看到内容,对于长回答,等待时间可能长达数秒甚至十几秒,体验不够流畅。

流式响应(Streaming Response)可以很好地解决这个问题。它允许模型一边生成内容,一边将生成的文本块(Token)逐块推送给客户端,客户端可以实时展示,形成“打字机”效果,大大提升了交互的实时感和用户体验。

4.1 为什么需要流式响应?

对比维度同步调用(call)流式调用(stream)
响应时间等待全部生成,延迟较高首字延迟低,边生边推
用户体验长时间空白等待,用户可能焦虑实时看到内容,体验流畅
技术实现简单,返回完整字符串较复杂,需处理流式数据
适用场景后台处理、非实时交互聊天机器人、实时生成

在现代 AI 应用中,流式响应已成为标配。Spring AI 内置了对流式响应的支持,通过 ChatClientstream() 方法即可轻松实现。

4.2 Spring AI 流式 API 的使用

ChatClient 提供了 stream() 方法,它返回一个 Reactor Flux 对象(Flux<ChatResponse>)。Flux 是响应式编程中的一种发布者,可以发射 0 到 N 个元素,这里每个元素代表模型生成的一个响应块(可能是一个 Token 或一段文本)。

4.2.1 基本用法

Flux<ChatResponse> flux = chatClient.prompt()
        .user("讲一个简短的笑话")
        .stream();

// 订阅并处理每个响应块
flux.subscribe(chatResponse -> {
    String chunk = chatResponse.getResult().getOutput().getContent();
    System.out.print(chunk); // 逐块打印,形成打字机效果
});

但在 Web 应用中,我们通常希望将流式数据直接推送给前端,而不是打印到控制台。这就需要结合 Spring WebFlux 的 Server-Sent Events (SSE)

4.3 结合 Spring WebFlux 实现 SSE

Server-Sent Events 是一种基于 HTTP 的轻量级推送技术,允许服务器向客户端推送文本数据。前端可以使用 JavaScript 的 EventSource API 轻松接收。

4.3.1 引入 WebFlux 依赖

首先,在项目中添加 Spring WebFlux 依赖(如果之前只加了 Spring Web,需要补充):

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

注意:Spring WebFlux 与 Spring Web MVC 可以共存,但流式响应通常使用 WebFlux 的响应式支持。

4.3.2 Controller 返回 Flux

创建 StreamController.java

package com.example.demo;

import org.springframework.ai.chat.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
public class StreamController {

    private final ChatClient chatClient;

    public StreamController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chatStream(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .stream()
                .map(chatResponse -> chatResponse.getResult().getOutput().getContent());
    }
}

这里的关键点:

  • produces = MediaType.TEXT_EVENT_STREAM_VALUE 告诉 Spring 返回的是 SSE 流。
  • stream() 返回 Flux<ChatResponse>,我们用 map 将每个 ChatResponse 转换为文本块(String)。
  • 最终返回 Flux<String>,Spring 会自动将每个元素包装为 SSE 事件发送给客户端。

4.3.3 前端接收示例

创建一个简单的 HTML 页面(放在 src/main/resources/static/index.html),使用 JavaScript 的 EventSource 接收流:

<!DOCTYPE html>
<html>
<head>
    <title>AI 流式聊天</title>
</head>
<body>
    <h2>流式聊天演示</h2>
    <input type="text" id="message" placeholder="输入消息" value="讲一个简短的笑话">
    <button onclick="send()">发送</button>
    <div id="response" style="margin-top:20px; border:1px solid #ccc; padding:10px; min-height:100px;"></div>

    <script>
        function send() {
            const msg = document.getElementById('message').value;
            const responseDiv = document.getElementById('response');
            responseDiv.innerHTML = ''; // 清空

            // 创建 EventSource 连接到流接口
            const eventSource = new EventSource(`/chat/stream?message=${encodeURIComponent(msg)}`);

            // 收到消息时追加内容
            eventSource.onmessage = function(event) {
                responseDiv.innerHTML += event.data;
            };

            // 错误处理
            eventSource.onerror = function() {
                responseDiv.innerHTML += '<br>连接关闭或出错';
                eventSource.close();
            };

            // 流结束时(服务端关闭连接),EventSource 会自动触发 onerror 并重连,这里我们主动关闭
            // 更好的做法是服务端发送一个特定事件标识结束,但简单起见我们等几秒后关闭
            setTimeout(() => {
                eventSource.close();
            }, 10000);
        }
    </script>
</body>
</html>

运行应用,访问 http://localhost:8080,点击发送,即可看到文字逐字出现的效果。

4.4 实践:构建流式聊天接口

为了更贴近实际应用,我们来构建一个完整的流式聊天 REST API,并处理一些细节。

4.4.1 增强的 Controller

增加系统消息设定,并处理可能出现的错误:

@RestController
@RequestMapping("/api/chat")
public class StreamingChatController {

    private final ChatClient chatClient;

    public StreamingChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> streamChat(@RequestParam String message) {
        return chatClient.prompt()
                .system("你是一个友好的聊天助手,用中文回答。")
                .user(message)
                .stream()
                .map(chatResponse -> {
                    String content = chatResponse.getResult().getOutput().getContent();
                    // 将每个文本块包装为 SSE 事件
                    return ServerSentEvent.<String>builder()
                            .data(content)
                            .event("message")  // 事件类型,前端可监听特定事件
                            .build();
                })
                .doOnError(e -> {
                    // 错误处理,可以记录日志
                    System.err.println("流式响应出错: " + e.getMessage());
                });
    }
}

这里使用了 ServerSentEvent 包装,可以指定事件类型、ID 等元数据。前端可以监听特定事件(如 message)来处理数据。

4.4.2 前端监听特定事件

修改前端,监听 message 事件:

eventSource.addEventListener('message', function(event) {
    responseDiv.innerHTML += event.data;
});

这样更清晰。

4.5 流式响应的注意事项

4.5.1 超时设置

SSE 连接可能会因为网络问题或模型响应过慢而超时。可以在 Spring Boot 配置中调整 WebFlux 的超时时间:

spring:
  webflux:
    base-path:
  mvc:
    async:
      request-timeout: 60000  # 60秒

或者在代码中设置请求超时(需要更底层的配置)。

4.5.2 背压处理

Flux 支持背压(backpressure),但 SSE 本身是基于 HTTP 的,客户端接收速度可能影响服务端发送。如果客户端处理慢,服务端可以限制发送速率,但通常模型生成速度不会太快,不需要特别处理。

4.5.3 错误处理与重连

EventSource 在连接断开时会自动重连,可能导致重复接收数据。服务端可以在流结束时发送一个特殊标记,前端检测到后主动关闭连接。另一种方式是服务端在流结束后关闭连接(默认行为),前端在 onerror 中判断是否正常结束。

4.5.4 内存泄漏

使用 Flux 时,确保订阅被正确取消,避免内存泄漏。在 Spring WebFlux 中,当客户端断开连接时,框架会自动取消订阅,开发者一般无需操心。

4.6 流式响应流程示意图

sequenceDiagram
    participant 客户端
    participant Controller
    participant ChatClient
    participant 大模型

    客户端->>Controller: GET /chat/stream?message=你好
    Controller->>ChatClient: prompt().user(message).stream()
    ChatClient->>大模型: 发起流式请求
    大模型-->>ChatClient: 返回第一个文本块
    ChatClient-->>Controller: 发射 Flux 元素
    Controller-->>客户端: SSE: data: "你"
    客户端->>客户端: 显示 "你"
    大模型-->>ChatClient: 返回第二个文本块
    ChatClient-->>Controller: 发射 Flux 元素
    Controller-->>客户端: SSE: data: "好"
    客户端->>客户端: 显示 "好" 继续直到结束
    ChatClient-->>Controller: 完成信号
    Controller-->>客户端: 关闭 SSE 连接

4.7 本章小结

通过本章的学习,你掌握了:

  • 流式响应的价值:提升用户体验,降低首字延迟。
  • Spring AI 流式 APIstream() 方法返回 Flux<ChatResponse>
  • 与 WebFlux 集成:返回 Flux<String>Flux<ServerSentEvent>,实现 SSE。
  • 前端接收:使用 JavaScript 的 EventSource 逐字展示内容。
  • 注意事项:超时、错误处理、连接管理。

第五章:结构化输出——让 AI 返回 Java 对象

在前几章中,我们一直让 AI 返回自然语言文本。但在企业级应用中,我们往往需要从 AI 的回答中提取结构化数据,例如从用户描述中提取姓名、年龄、地址,或者让 AI 返回一个包含多个字段的 JSON 对象。手动解析自然语言不仅繁琐,而且容易出错。Spring AI 提供了强大的 结构化输出 功能,可以自动将 AI 的响应转换为 Java 对象。

5.1 场景:从自然语言提取结构化数据

假设我们有一个需求:用户输入一段描述,系统从中提取出人物信息,包括姓名、年龄、城市。我们希望 AI 直接返回一个 Java 对象,而不是让开发者自己写正则表达式或调用其他 NLP 库。

5.2 使用 entity() 方法映射 POJO

Spring AI 的 ChatClient 提供了 .entity(Class<T> type) 方法,它可以:

  1. 在发送给模型的提示词中,自动要求模型以 JSON 格式返回。
  2. 将模型返回的 JSON 字符串反序列化为指定类型的 Java 对象。

5.2.1 定义 POJO

首先,我们定义一个简单的 Java 类来接收数据。可以使用普通的 class 或 Java 14+ 的 record。

java

// 使用 record(推荐,简洁)
public record Person(String name, int age, String city) {}

// 或者使用传统 class
public class Person {
    private String name;
    private int age;
    private String city;

    // 必须有无参构造函数和 getter/setter
    public Person() {}

    // getter/setter...
}

注意:如果使用 class,必须提供无参构造函数和 getter/setter,因为反序列化需要。

5.2.2 使用 entity() 方法

在 Controller 中注入 ChatClient,然后调用 entity()

java

@RestController
public class StructuredOutputController {

    private final ChatClient chatClient;

    public StructuredOutputController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @PostMapping("/extract/person")
    public Person extractPerson(@RequestBody String description) {
        return chatClient.prompt()
                .user(description)
                .call()
                .entity(Person.class);  // 指定目标类型
    }
}

当调用此接口时,Spring AI 会自动构建一个内部提示词,要求模型以 JSON 格式返回,然后解析为 Person 对象。

5.2.3 背后原理

Spring AI 内部使用 BeanOutputConverter 来生成格式指令和解析响应。它会在用户消息后附加类似这样的指令:

text

Your response should be in JSON format.
The response should contain only the JSON object.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Use this schema:
{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer" },
    "city": { "type": "string" }
  },
  "required": ["name", "age", "city"]
}

模型返回的 JSON 会被 Jackson 反序列化为 Person 对象。

5.3 支持的类型

entity() 方法支持多种返回类型:

  • 简单 POJO:如上例所示。
  • 集合类型:例如 List<Person>,需要特殊处理,因为泛型擦除。可以使用 entity(new ParameterizedTypeReference<List<Person>>() {})
  • 枚举:如果 AI 需要返回固定的枚举值,可以直接指定枚举类。
  • 基本类型:如 StringInteger,但通常直接使用 content() 即可。

5.3.1 处理集合类型

假设我们需要 AI 返回一个包含多个人的列表:

java

import org.springframework.core.ParameterizedTypeReference;

List<Person> persons = chatClient.prompt()
        .user("提取以下文本中的所有人物信息:张三25岁北京,李四30岁上海")
        .call()
        .entity(new ParameterizedTypeReference<List<Person>>() {});

注意:ParameterizedTypeReference 用于保留泛型信息,避免类型擦除。

5.3.2 处理枚举

定义枚举:

java

public enum Sentiment {
    POSITIVE, NEUTRAL, NEGATIVE
}

使用:

java

Sentiment sentiment = chatClient.prompt()
        .user("分析评论的情感:这个产品太棒了!")
        .call()
        .entity(Sentiment.class);

模型会被要求返回枚举值的字符串表示,如 "POSITIVE",然后自动转换。

5.4 自定义输出转换器

如果默认的 JSON 格式不满足需求,或者需要更精细的控制,可以使用 OutputConverter 接口。Spring AI 提供了 BeanOutputConverter 和 MapOutputConverter 等实现。

5.4.1 使用 BeanOutputConverter 直接构建提示词

你可以手动创建 BeanOutputConverter,获取格式指令,然后添加到提示词中:

java

BeanOutputConverter<Person> converter = new BeanOutputConverter<>(Person.class);
String formatInstructions = converter.getFormatInstructions();

Prompt prompt = new Prompt(new UserMessage("提取人物信息:" + description + "\n" + formatInstructions));

ChatResponse response = chatClient.prompt(prompt).call();
Person person = converter.convert(response.getResult().getOutput().getContent());

这种方式给了你更大的控制权,比如可以自定义格式指令的位置或内容。

5.4.2 使用 MapOutputConverter 获取动态结构

如果返回的 JSON 结构不确定,可以使用 MapOutputConverter 将其转换为 Map

java

MapOutputConverter converter = new MapOutputConverter();
String formatInstructions = converter.getFormatInstructions();

Prompt prompt = new Prompt(new UserMessage("提取信息:" + description + "\n" + formatInstructions));

ChatResponse response = chatClient.prompt(prompt).call();
Map<String, Object> result = converter.convert(response.getResult().getOutput().getContent());

5.5 实践:从用户描述中提取人员信息

让我们构建一个完整的 REST 服务,接收用户描述,返回结构化的 Person 对象。

5.5.1 创建 Controller

java

package com.example.demo;

import org.springframework.ai.chat.ChatClient;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/extract")
public class ExtractionController {

    private final ChatClient chatClient;

    public ExtractionController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @PostMapping("/person")
    public Person extractPerson(@RequestBody String description) {
        return chatClient.prompt()
                .system("你是一个信息提取助手,从用户描述中提取人物信息,以JSON格式返回。")
                .user(description)
                .call()
                .entity(Person.class);
    }

    @PostMapping("/persons")
    public List<Person> extractPersons(@RequestBody String description) {
        return chatClient.prompt()
                .system("你是一个信息提取助手,从文本中提取所有人物的姓名、年龄和城市,以JSON数组返回。")
                .user(description)
                .call()
                .entity(new ParameterizedTypeReference<List<Person>>() {});
    }
}

5.5.2 测试

使用 curl 测试:

bash

curl -X POST http://localhost:8080/api/extract/person \
  -H "Content-Type: text/plain" \
  -d "我叫张三,今年28岁,住在上海。"

期望返回:

json

{
  "name": "张三",
  "age": 28,
  "city": "上海"
}

测试提取多个:

bash

curl -X POST http://localhost:8080/api/extract/persons \
  -H "Content-Type: text/plain" \
  -d "张三25岁北京,李四30岁上海,王五35岁广州"

期望返回 JSON 数组。

5.5.3 可能遇到的问题及解决

  • 模型返回的 JSON 格式错误:如果模型偶尔返回非 JSON 内容,entity() 会抛出异常。可以添加错误处理,如使用 try-catch,或要求模型重新生成。
  • 字段缺失:如果描述中缺少某些字段,模型可能不返回该字段。在 POJO 中可以将字段设为可选(如使用 Optional 或默认值)。
  • 枚举值不匹配:确保枚举的字符串值与模型返回的一致,可以通过 @JsonProperty 或自定义反序列化解决。

5.6 高级:使用 Record 与 Jackson 注解

Spring AI 使用 Jackson 进行 JSON 解析,因此你可以使用 Jackson 注解来控制序列化/反序列化行为。

java

import com.fasterxml.jackson.annotation.JsonProperty;

public record Person(
    @JsonProperty("full_name") String name,
    int age,
    String city
) {}

这样模型返回的 JSON 中如果包含 full_name 字段,会自动映射到 name 属性。

5.7 本章小结

通过本章的学习,你掌握了:

  • 结构化输出的价值:从自然语言到 Java 对象的自动转换。
  • entity() 方法:直接指定目标类型,简化代码。
  • 支持的类型:POJO、集合、枚举等。
  • 自定义转换器BeanOutputConverter 和 MapOutputConverter 的使用。
  • 实践:构建了信息提取服务,并测试了效果。

六、函数调用:赋予 AI 行动能力

在前面的章节中,我们的 AI 应用只能基于模型训练时学到的知识回答问题。如果用户问“现在几点了?”、“帮我查一下订单状态”、“今天天气怎么样”,纯文本模型是无法直接获取这些实时信息的——它没有时钟,也无法访问你的数据库。

函数调用(Function Calling)正是为了解决这个问题而生。它允许大模型在需要时“调用”你编写的 Java 方法,获取实时数据或执行操作,然后将结果整合到回答中。

6.1 什么是函数调用?AI 如何调用外部方法?

函数调用的核心思想是:你提供一组工具(Java 方法),并告诉 AI 这些工具的存在、用途以及参数。当 AI 认为需要某个工具来回答问题时,它会返回一个特殊的请求,要求执行该工具并提供参数。你的应用负责执行对应方法,并将结果返回给 AI,AI 再根据结果生成最终回答。

工作流程示意图:

sequenceDiagram
    participant 用户
    participant ChatClient
    participant 工具方法

    用户->>ChatClient: 提问(例如“现在几点了?”)
    ChatClient->>ChatClient: 分析问题,决定需要调用工具
    ChatClient->>工具方法: 执行 getCurrentTime()
    工具方法-->>ChatClient: 返回 "14:30"
    ChatClient->>ChatClient: 将结果整合到回答中
    ChatClient-->>用户: 返回 "现在是下午2点30分。"

6.2 在 Spring AI 中定义工具

Spring AI 通过 @Tool 注解来标记一个 Bean 的方法作为可调用的工具。

6.2.1 引入依赖

函数调用功能需要额外的依赖(Spring AI 自动包含,但确保版本支持)。如果使用 OpenAI,它原生支持函数调用。无需额外依赖。

6.2.2 定义工具类

创建一个 Spring 组件,其中的方法用 @Tool 注解标记。@Tool 注解需要指定工具的名称和描述,描述会被传递给模型,帮助模型理解何时调用该工具。

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

@Component
public class TimeTools {

    @Tool(name = "getCurrentTime", description = "获取当前时间")
    public String getCurrentTime() {
        return LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
    }
}

6.2.3 工具描述的重要性

@Tool 注解的 description 属性非常重要,它告诉 AI 这个工具是做什么的,在什么情况下应该调用。描述越清晰,AI 调用工具的准确率越高。

对于带参数的工具,参数描述也需要提供。Spring AI 会从方法参数名和 Javadoc 中提取描述,也可以使用 @ToolParam 注解(如果有)来显式说明。

6.2.4 带参数的工具

假设我们需要一个查询天气的工具,它需要城市名作为参数:

@Component
public class WeatherTools {

    @Tool(name = "getWeather", description = "查询指定城市的天气")
    public String getWeather(String city) {
        // 这里应该是真实的天气 API 调用,为了演示,我们返回模拟数据
        return switch (city) {
            case "北京" -> "晴,25℃";
            case "上海" -> "多云,28℃";
            default -> city + "的天气数据暂未收录";
        };
    }
}

6.3 将工具注册到 ChatClient

我们需要将工具类实例注册到 ChatClient 中,以便它知道有哪些工具可用。

@RestController
public class FunctionCallingController {

    private final ChatClient chatClient;

    // 注入工具类
    public FunctionCallingController(ChatClient.Builder chatClientBuilder,
                                      TimeTools timeTools,
                                      WeatherTools weatherTools) {
        this.chatClient = chatClientBuilder
                .build();
    }

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

但是上面的代码并没有将工具传递给 ChatClient。我们需要在构建时指定工具:

this.chatClient = chatClientBuilder
        .defaultTools(timeTools, weatherTools)  // 注册默认工具
        .build();

或者在每次调用时动态指定工具:

return chatClient.prompt()
        .user(question)
        .tools(timeTools, weatherTools)  // 临时指定工具
        .call()
        .content();

建议使用 defaultTools,这样工具对所有请求都可用。

6.4 实践:让 AI 查询实时信息

让我们构建一个完整的示例,包含两个工具:获取时间和查询天气。我们将创建一个 REST 接口,用户输入问题,AI 自动决定是否调用工具。

6.4.1 完整代码

package com.example.demo;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;

@RestController
public class AssistantController {

    private final ChatClient chatClient;

    public AssistantController(ChatClient.Builder chatClientBuilder,
                               TimeTools timeTools,
                               WeatherTools weatherTools) {
        this.chatClient = chatClientBuilder
                .defaultTools(timeTools, weatherTools)
                .build();
    }

    @GetMapping("/assistant")
    public String assistant(@RequestParam String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

@Component
class TimeTools {
    @Tool(name = "getCurrentTime", description = "获取当前时间")
    public String getCurrentTime() {
        return LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
    }
}

@Component
class WeatherTools {
    @Tool(name = "getWeather", description = "查询指定城市的天气,需要传入城市名称")
    public String getWeather(String city) {
        Map<String, String> mockWeather = Map.of(
                "北京", "晴,25℃",
                "上海", "多云,28℃",
                "广州", "雷阵雨,30℃"
        );
        return mockWeather.getOrDefault(city, city + "的天气数据暂未收录");
    }
}

6.4.2 测试

启动应用,用浏览器或 curl 测试:

  • http://localhost:8080/assistant?question=现在几点了?
  • http://localhost:8080/assistant?question=上海天气怎么样?
  • http://localhost:8080/assistant?question=帮我查一下北京的天气
  • http://localhost:8080/assistant?question=计算 123+456(没有对应工具,AI 会尝试自己计算或表示无法计算)

6.4.3 观察输出

对于第一个问题,你应该会看到类似“现在是下午2点30分45秒”的回答,说明工具被成功调用。对于第二个问题,应该返回模拟的天气信息。

6.4.4 工作原理

当用户提问时,Spring AI 会:

  1. 将问题发送给模型,同时附上可用的工具列表(名称、描述、参数)。
  2. 模型判断是否需要调用工具。如果需要,它会返回一个工具调用请求,包含工具名称和参数。
  3. Spring AI 接收到请求后,根据工具名称找到对应的 Bean 方法,执行该方法。
  4. 将工具执行结果返回给模型。
  5. 模型根据工具结果生成最终回答。

6.5 函数调用的注意事项

6.5.1 工具方法应该是线程安全的

工具类通常是单例 Bean,因此方法需要是线程安全的。上面的例子中,getCurrentTime 是纯函数,安全;getWeather 也是只读操作,安全。如果工具修改了共享状态,需要考虑同步。

6.5.2 工具方法的执行时间

工具方法执行时间不宜过长,因为整个调用是同步的(模型在等待工具结果)。如果工具需要调用外部 API 或数据库,考虑设置超时,或采用异步方式(但 Spring AI 目前主要支持同步工具调用)。

6.5.3 错误处理

工具方法可能抛出异常,Spring AI 会捕获异常并将错误信息返回给模型。模型会根据错误信息决定如何回应(例如提示用户稍后重试)。你可以在工具方法内部处理异常,返回友好的错误消息。

6.5.4 工具数量

不要注册过多无关的工具,因为工具描述会消耗 Token,且可能让模型混淆。只注册必要且描述清晰的工具。

6.5.5 参数类型

工具方法的参数支持基本类型、String、复杂对象等,但模型需要能够生成对应的 JSON。建议使用简单类型,并配合清晰描述。

6.6 本章小结

通过本章的学习,你掌握了:

  • 函数调用的概念:让 AI 调用外部方法获取实时信息或执行操作。
  • 定义工具:使用 @Tool 注解标记方法,并描述其用途。
  • 注册工具:通过 defaultToolstools() 将工具注入 ChatClient
  • 实践:构建了能查时间和天气的智能助手。
  • 注意事项:线程安全、超时、错误处理。

七、多模态探索:图像与语音模型

在前面的章节中,我们所有的交互都基于文本。但现实世界的信息是多样的——图片、语音、视频……大语言模型正在向多模态进化,能够理解和生成非文本内容。Spring AI 也紧跟潮流,提供了对图像和语音模型的支持,让你可以轻松构建能够“看懂”图片、“听懂”语音的智能应用。

本章将带你探索 Spring AI 的多模态能力,包括让 AI 描述图片内容、将文本转为语音、以及将语音转为文本。虽然多模态功能还在快速发展中,但掌握基础用法可以为你的应用打开更广阔的场景。

7.1 Spring AI 对多模态的支持现状

Spring AI 目前主要支持以下多模态能力:

  • 图像生成:通过 ImageModel 接口,支持 DALL-E、Stable Diffusion 等图像生成模型。
  • 图像理解:通过支持多模态的 ChatModel(如 OpenAI 的 GPT-4 Vision),可以在聊天中发送图片,让 AI 描述或分析图片。
  • 语音:通过 AudioModel 接口,支持文本转语音(TTS)和语音识别(ASR),例如 OpenAI 的 TTS 模型和 Whisper 模型。

需要注意的是,多模态功能需要模型本身支持。本章示例将使用 OpenAI 的相关模型(如 gpt-4-vision-previewtts-1whisper-1),你需要拥有 OpenAI API 密钥并确保账户有权限访问这些模型。如果使用其他服务商,原理类似,只需调整配置。

7.2 图像模型(ImageModel)的使用

ImageModel 是 Spring AI 中用于图像生成的接口。目前主要支持文本生成图像(text-to-image)。如果你需要让 AI 描述图片(图像理解),则需要使用支持视觉的 ChatModel

我们先介绍图像生成,再介绍图像理解。

7.2.1 配置图像模型

application.yml 中添加图像模型的配置:

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      image:
        options:
          model: dall-e-3          # 或 dall-e-2
          quality: standard         # 仅 dall-e-3 支持
          size: 1024x1024           # 图片尺寸
          n: 1                      # 生成图片数量

Spring AI 会根据配置自动创建 ImageModel 的 Bean。

7.2.2 图像生成示例

创建一个 ImageController,接收文本提示词,返回生成的图片 URL。

package com.example.demo;

import org.springframework.ai.image.ImageClient;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ImageController {

    private final ImageClient imageClient;

    public ImageController(ImageClient imageClient) {
        this.imageClient = imageClient;
    }

    @GetMapping("/generate-image")
    public String generateImage(@RequestParam String prompt) {
        // 创建图像生成请求,可以覆盖默认选项
        ImagePrompt imagePrompt = new ImagePrompt(prompt,
                OpenAiImageOptions.builder()
                        .withModel("dall-e-3")
                        .withQuality("hd")      // 高清
                        .withN(1)
                        .withHeight(1024)
                        .withWidth(1024)
                        .build());

        ImageResponse response = imageClient.call(imagePrompt);
        // 返回生成的图片 URL
        return response.getResult().getOutput().getUrl();
    }
}

测试:访问 http://localhost:8080/generate-image?prompt=a cute cat,会返回一个图片 URL,打开即可看到生成的图片。

7.2.3 图像理解(让 AI 描述图片)

图像理解需要使用支持视觉的聊天模型,如 OpenAI 的 gpt-4-vision-preview。在聊天中,我们可以将图片作为用户消息的一部分发送。

配置支持视觉的聊天模型:

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4-turbo       # 或 gpt-4-vision-preview

构造包含图片的用户消息:

Spring AI 的 Message 接口支持多种内容类型,包括文本和图片。我们可以创建一个 UserMessage,包含文本和图片 URL。

import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Media;
import org.springframework.util.MimeTypeUtils;

// 构造包含图片的用户消息
UserMessage userMessage = new UserMessage("请描述这张图片的内容",
        new Media(MimeTypeUtils.IMAGE_JPEG, "https://example.com/cat.jpg"));

然后调用 ChatClient 处理这个 Prompt。

完整示例:

@RestController
public class VisionController {

    private final ChatClient chatClient;

    public VisionController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/describe-image")
    public String describeImage(@RequestParam String imageUrl) {
        UserMessage userMessage = new UserMessage("请用中文详细描述这张图片的内容",
                new Media(MimeTypeUtils.IMAGE_JPEG, imageUrl));
        Prompt prompt = new Prompt(userMessage);
        return chatClient.prompt(prompt).call().content();
    }
}

测试时传入一张图片的 URL,例如: http://localhost:8080/describe-image?imageUrl=https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg

AI 会返回对图片的描述。

注意Media 的 MIME 类型需要根据实际图片类型设置,可以是 IMAGE_JPEGIMAGE_PNGIMAGE_GIF 等。对于本地图片,你需要先将图片上传到可访问的 URL,或者将图片转为 Base64 编码后作为 data URL 传入(OpenAI 支持 data URL 格式)。

7.3 语音模型(AudioModel)的使用

Spring AI 通过 AudioModel 接口支持文本转语音(TTS)和语音识别(ASR)。目前主要实现是 OpenAI 的 TTS 和 Whisper 模型。

7.3.1 配置语音模型

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      audio:
        options:
          model: tts-1              # 或 tts-1-hd
          voice: alloy               # 语音风格 (alloy, echo, fable, onyx, nova, shimmer)
          response-format: mp3       # 输出格式

Spring AI 会自动创建 AudioModel 的 Bean。

7.3.2 文本转语音(TTS)

创建一个接口,将文本转为语音文件并返回给客户端。

import org.springframework.ai.audio.AudioModel;
import org.springframework.ai.audio.AudioPrompt;
import org.springframework.ai.audio.AudioResponse;
import org.springframework.ai.openai.OpenAiAudioOptions;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TtsController {

    private final AudioModel audioModel;

    public TtsController(AudioModel audioModel) {
        this.audioModel = audioModel;
    }

    @GetMapping(value = "/tts", produces = "audio/mpeg")
    public byte[] textToSpeech(@RequestParam String text) {
        // 创建语音选项
        OpenAiAudioOptions options = OpenAiAudioOptions.builder()
                .withModel("tts-1")
                .withVoice("alloy")
                .withResponseFormat("mp3")
                .build();

        AudioPrompt prompt = new AudioPrompt(text, options);
        AudioResponse response = audioModel.call(prompt);
        // 获取音频资源的字节数组
        Resource audioResource = response.getResult().getOutput();
        try {
            return audioResource.getContentAsByteArray();
        } catch (Exception e) {
            throw new RuntimeException("读取音频失败", e);
        }
    }
}

返回类型设置为 audio/mpeg,浏览器会直接播放音频。你可以用前端 <audio> 标签播放,或者直接访问接口测试。

7.3.3 语音识别(ASR)

使用 OpenAI Whisper 模型将语音文件转为文本。首先需要能够接收文件上传。

添加 Spring Boot 的文件上传支持(已在 web starter 中)。

import org.springframework.ai.audio.AudioModel;
import org.springframework.ai.audio.AudioPrompt;
import org.springframework.ai.audio.AudioResponse;
import org.springframework.ai.openai.OpenAiAudioOptions;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@RestController
public class AsrController {

    private final AudioModel audioModel;

    public AsrController(AudioModel audioModel) {
        this.audioModel = audioModel;
    }

    @PostMapping(value = "/asr", produces = "text/plain")
    public String speechToText(@RequestParam("file") MultipartFile file) throws IOException {
        // 将上传的文件转换为 Resource
        org.springframework.core.io.ByteArrayResource audioResource =
                new org.springframework.core.io.ByteArrayResource(file.getBytes());

        // 创建 ASR 选项
        OpenAiAudioOptions options = OpenAiAudioOptions.builder()
                .withModel("whisper-1")
                .withResponseFormat("text")
                .build();

        AudioPrompt prompt = new AudioPrompt(audioResource, options);
        AudioResponse response = audioModel.call(prompt);
        return response.getResult().getOutput().toString(); // 返回识别出的文本
    }
}

使用 Postman 或前端表单上传音频文件(如 mp3、wav)进行测试。

7.4 实践:构建图片描述服务

将图像理解功能封装成一个完整的 REST 服务,并添加一些错误处理和日志。

7.4.1 创建 Service 层

package com.example.demo.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Media;
import org.springframework.stereotype.Service;
import org.springframework.util.MimeTypeUtils;

@Service
public class ImageDescriptionService {

    private final ChatClient chatClient;

    public ImageDescriptionService(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    public String describeImage(String imageUrl, String question) {
        UserMessage userMessage = new UserMessage(question,
                new Media(MimeTypeUtils.IMAGE_JPEG, imageUrl));
        Prompt prompt = new Prompt(userMessage);
        return chatClient.prompt(prompt).call().content();
    }

    // 重载方法,使用默认问题
    public String describeImage(String imageUrl) {
        return describeImage(imageUrl, "请详细描述这张图片的内容,包括主体、颜色、动作、背景等。");
    }
}

7.4.2 创建 Controller

package com.example.demo.controller;

import com.example.demo.service.ImageDescriptionService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ImageDescriptionController {

    private final ImageDescriptionService descriptionService;

    public ImageDescriptionController(ImageDescriptionService descriptionService) {
        this.descriptionService = descriptionService;
    }

    @GetMapping("/api/describe-image")
    public String describeImage(@RequestParam String url,
                                @RequestParam(required = false) String question) {
        if (question == null || question.isBlank()) {
            return descriptionService.describeImage(url);
        } else {
            return descriptionService.describeImage(url, question);
        }
    }
}

7.4.3 测试

启动应用,访问: http://localhost:8080/api/describe-image?url=https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg

你应该会得到一段详细的图片描述。

7.4.4 处理本地图片

如果图片在本地,你需要将其转为 Base64 的 data URL。可以编写一个工具方法:

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

public static String imageFileToDataUrl(String filePath) throws IOException {
    Path path = Path.of(filePath);
    String mimeType = Files.probeContentType(path);
    byte[] bytes = Files.readAllBytes(path);
    String base64 = Base64.getEncoder().encodeToString(bytes);
    return "data:" + mimeType + ";base64," + base64;
}

然后将生成的 data URL 传入描述接口。

7.5 本章小结

通过本章的学习,你探索了 Spring AI 的多模态能力:

  • 图像生成:使用 ImageModel 从文本生成图片。
  • 图像理解:通过支持视觉的 ChatModel,将图片作为消息的一部分发送,让 AI 描述图片。
  • 语音:使用 AudioModel 实现文本转语音(TTS)和语音识别(ASR)。
  • 实践:构建了图片描述服务,可以分析任意公开图片。

八、构建企业级知识库:RAG 实现(上)

在前面的章节中,我们已经能够与 AI 进行流畅的对话,甚至让 AI 调用外部工具获取实时信息。但是,当用户问到公司内部的规章制度、产品文档、技术规范等私有知识时,AI 就无能为力了——因为它从未见过这些资料。

RAG(Retrieval-Augmented Generation,检索增强生成) 正是为了解决这个问题而生。它允许 AI 在回答问题时,先从你的私有知识库中检索相关文档片段,然后基于这些片段生成答案,从而将模型的知识边界扩展到你的企业内部资料。

本章将带你一步步构建一个基于私有知识库的问答系统,涵盖从文档加载、分割、向量化到存储的完整知识摄入流程。下一章我们将实现检索与生成部分。

8.1 RAG 核心概念回顾

RAG 的标准流程分为两大阶段:

  1. 知识摄入(Ingestion):将原始文档(PDF、TXT、Word 等)处理成可供检索的格式,并存入向量数据库。
  2. 问答检索(Retrieval & Generation):用户提问时,先从向量数据库中检索相关文档片段,然后将这些片段作为上下文与问题一起发送给大模型,生成最终答案。

为什么需要 RAG?

  • 知识时效性:大模型的知识截止日期之后的信息,它不知道。
  • 私有知识:公司内部文档、产品手册等,模型从未见过。
  • 减少幻觉:基于检索到的真实文档生成答案,大大降低编造的可能。
  • 可解释性:可以引用来源,增强用户信任。

RAG 整体流程示意图:

graph TD
    subgraph 知识摄入
        A[原始文档] --> B[文档加载器]
        B --> C[文档分割器]
        C --> D[嵌入模型]
        D --> E[向量数据库]
    end
    
    subgraph 问答
        F[用户问题] --> G[嵌入模型]
        G --> H[向量检索]
        H --> I[检索到的文档片段]
        I --> J[增强提示词]
        J --> K[大语言模型]
        K --> L[最终答案]
    end

8.2 Spring AI 的向量存储抽象(VectorStore)

Spring AI 提供了一个统一的 VectorStore 接口,用于抽象各种向量数据库的操作。目前支持的实现包括:

  • PGvector:PostgreSQL 的向量插件
  • Milvus / Zilliz Cloud:专业的向量数据库
  • Redis:通过 Redis Stack 的向量搜索能力
  • Elasticsearch:通过 Elasticsearch 的向量检索功能
  • Neo4j:图数据库的向量支持
  • Azure Cosmos DBPineconeQdrant

在本教程中,我们将使用 PGvector,因为它可以与关系数据共存,且对 Java 开发者非常友好。如果你没有 PostgreSQL 环境,也可以使用简单的内存实现(如 SimpleVectorStore)进行测试,但生产环境建议使用持久化存储。

8.2.1 配置向量数据库(以 PGvector 为例)

首先,在你的项目中引入 PGvector 的 Spring Boot Starter:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

然后,在 application.yml 中配置数据库连接:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: postgres
  ai:
    vectorstore:
      pgvector:
        index-type: HNSW        # 索引类型
        distance-type: COSINE   # 距离度量方式
        dimensions: 1536        # 向量维度(取决于嵌入模型,OpenAI ada-002 是 1536)

Spring AI 会自动创建一个 VectorStore 的 Bean,可以直接注入使用。

如果你没有 PostgreSQL 环境,可以先用内存存储调试:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-simple-vector-store</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

然后无需配置,Spring Boot 会自动创建一个 SimpleVectorStore 的 Bean。

8.3 文档处理与嵌入

在将文档存入向量数据库之前,需要进行一系列处理:加载文档、分割成块、转换为向量。

8.3.1 文档加载器(DocumentReader)

Spring AI 提供了多种文档读取器,用于从不同格式的文件中提取文本。目前支持:

  • TextReader:读取纯文本文件(.txt)
  • JsonReader:读取 JSON 文件
  • PagePdfDocumentReader:读取 PDF 文件(基于 Apache PDFBox)
  • MarkdownDocumentReader:读取 Markdown 文件

首先引入 PDF 解析器的依赖(如果需要处理 PDF):

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

然后,使用 PagePdfDocumentReader 加载 PDF:

import org.springframework.ai.document.Document;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;

// 加载 PDF 文件
Resource pdfResource = new UrlResource("file:///path/to/document.pdf");
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(pdfResource);
List<Document> documents = pdfReader.get();

对于纯文本文件,可以使用 TextReader

import org.springframework.ai.reader.TextReader;

Resource textResource = new UrlResource("file:///path/to/knowledge.txt");
TextReader textReader = new TextReader(textResource);
// 可以设置元数据,如文件名
textReader.setCustomMetadata("source", "knowledge.txt");
List<Document> documents = textReader.get();

Document 对象包含文本内容和元数据(如文件名、页码等),后续会用于分割和嵌入。

8.3.2 文档分割器(DocumentSplitter)

大模型对输入长度有限制,且检索时需要小块才能精确匹配。因此需要将文档切分成多个段落。Spring AI 提供了 DocumentSplitter 接口,常用实现有:

  • TokenTextSplitter:基于 Token 数分割(推荐,因为模型按 Token 计费)
  • SentenceSplitter:按句子分割
  • RecursiveTextSplitter:递归分割,尝试保持段落完整

使用 TokenTextSplitter 的示例:

import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;

// 创建分割器,设置每块最大 Token 数和重叠 Token 数
TokenTextSplitter splitter = new TokenTextSplitter(500, 100, 5, 5000, true);
List<Document> splitDocuments = splitter.apply(documents);

参数说明:

  • chunkSize:每块的最大 Token 数(通常 500-1000 比较合适)
  • chunkOverlap:相邻块之间的重叠 Token 数,避免切在关键位置丢失上下文
  • 其他参数可以保持默认

8.3.3 嵌入客户端(EmbeddingClient)

嵌入模型将文本转换为向量。Spring AI 提供了 EmbeddingClient 接口,支持多种实现:

  • OpenAiEmbeddingClient:使用 OpenAI 的 text-embedding-ada-002 模型
  • OllamaEmbeddingClient:使用本地 Ollama 嵌入模型
  • AzureOpenAiEmbeddingClient:Azure 版本

在配置文件中配置嵌入客户端(以 OpenAI 为例):

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      embedding:
        options:
          model: text-embedding-ada-002

Spring AI 会自动创建 EmbeddingClient 的 Bean,可以直接注入使用。

8.4 知识摄入实践

现在我们将以上组件组合起来,完成一个完整的知识摄入流程。

8.4.1 创建摄入服务

package com.example.demo.service;

import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class IngestionService {

    private final EmbeddingClient embeddingClient;
    private final VectorStore vectorStore;

    public IngestionService(EmbeddingClient embeddingClient, VectorStore vectorStore) {
        this.embeddingClient = embeddingClient;
        this.vectorStore = vectorStore;
    }

    /**
     * 摄入文本文件
     * @param fileResource 文件资源
     * @param sourceName   来源名称(用于元数据)
     */
    public void ingestTextFile(Resource fileResource, String sourceName) {
        // 1. 读取文档
        TextReader textReader = new TextReader(fileResource);
        textReader.setCustomMetadata("source", sourceName);
        List<Document> documents = textReader.get();

        // 2. 分割文档
        TokenTextSplitter splitter = new TokenTextSplitter(500, 100, 5, 5000, true);
        List<Document> splitDocuments = splitter.apply(documents);

        // 3. 计算向量并存储
        // 注意:VectorStore 的 add 方法内部会调用 embeddingClient 生成向量
        vectorStore.add(splitDocuments);

        System.out.println("成功摄入 " + splitDocuments.size() + " 个文档片段");
    }
}

这里的关键是 vectorStore.add(documents) 方法。它会自动调用 embeddingClient 为每个 Document 生成向量,并将文档与向量一起存储到底层数据库。

8.4.2 在应用启动时摄入知识

我们可以在 Spring Boot 启动后自动加载一些预定义的知识文档。创建一个 IngestionRunner 实现 ApplicationRunner 接口:

package com.example.demo;

import com.example.demo.service.IngestionService;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

@Component
public class IngestionRunner implements ApplicationRunner {

    private final IngestionService ingestionService;

    public IngestionRunner(IngestionService ingestionService) {
        this.ingestionService = ingestionService;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 从 classpath 加载 knowledge.txt 文件
        ClassPathResource resource = new ClassPathResource("knowledge.txt");
        ingestionService.ingestTextFile(resource, "knowledge.txt");
    }
}

src/main/resources 目录下创建 knowledge.txt 文件,内容可以是你的私有知识,例如:

Spring AI 是一个为 Java 开发者设计的 AI 集成框架,由 Spring 官方团队开发。
它提供了统一的 API 来对接大语言模型,如 OpenAI、Azure、Ollama 等。
RAG(检索增强生成)是 Spring AI 的核心功能之一,可以帮助企业构建基于私有知识库的问答系统。
Spring AI 的 VectorStore 抽象支持多种向量数据库,包括 PGvector、Milvus、Redis 等。
使用 Spring AI,开发者可以像调用普通方法一样使用 AI 能力,极大地简化了开发。

启动应用,你会看到控制台输出摄入成功的日志。

8.4.3 验证摄入结果

为了确保文档已经成功存入向量数据库,我们可以编写一个简单的查询测试:

package com.example.demo.controller;

import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.stream.Collectors;

@RestController
public class VectorStoreTestController {

    private final VectorStore vectorStore;
    private final EmbeddingClient embeddingClient;

    public VectorStoreTestController(VectorStore vectorStore, EmbeddingClient embeddingClient) {
        this.vectorStore = vectorStore;
        this.embeddingClient = embeddingClient;
    }

    @GetMapping("/test-search")
    public List<String> testSearch(@RequestParam String query) {
        // 创建搜索请求,返回最相似的 3 个文档
        SearchRequest request = SearchRequest.query(query).withTopK(3);
        List<Document> results = vectorStore.similaritySearch(request);
        return results.stream()
                .map(doc -> "【相似度: " + doc.getScore() + "】\n" + doc.getContent())
                .collect(Collectors.toList());
    }
}

访问 /test-search?query=什么是RAG,你应该能看到检索到的文档片段及其相似度得分。

8.5 本章小结

通过本章的学习,你已经掌握了 RAG 的核心第一步——知识摄入

  • 向量存储抽象:Spring AI 的 VectorStore 统一了多种向量数据库的操作。
  • 文档加载:使用 DocumentReader 从不同格式文件中提取文本。
  • 文档分割:用 TokenTextSplitter 将长文档切成小块。
  • 嵌入与存储:通过 VectorStore.add() 自动完成向量化并存入数据库。
  • 实践:编写了摄入服务,并在应用启动时自动加载知识文档。