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

一、开篇:为什么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 的聊天接口:

java 复制代码
// 原始方式:直接调用 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 重写上面的功能:

java 复制代码
// 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 复制代码
<?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,配置如下:

yaml 复制代码
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:

yaml 复制代码
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

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 会自动生成):

java 复制代码
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 基础用法回顾

最简单的用法:

java 复制代码
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 对象:

java 复制代码
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 编程导师:

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

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

java 复制代码
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 类来加载模板文件并进行变量替换。

java 复制代码
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 还可以直接接受字符串模板,而不必从文件加载:

java 复制代码
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 注解注入文件内容,可以这样做:

java 复制代码
@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

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 支持直接添加系统消息和用户消息,我们可以更简洁:

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

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

java 复制代码
@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 基本用法

java 复制代码
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,需要补充):

xml 复制代码
<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

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 接收流:

html 复制代码
<!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

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

java 复制代码
@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 事件:

javascript 复制代码
eventSource.addEventListener('message', function(event) {
    responseDiv.innerHTML += event.data;
});

这样更清晰。

4.5 流式响应的注意事项

4.5.1 超时设置

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

yaml 复制代码
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

arduino 复制代码
// 使用 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

kotlin 复制代码
@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

typescript 复制代码
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

swift 复制代码
import org.springframework.core.ParameterizedTypeReference;

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

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

5.3.2 处理枚举

定义枚举:

java

arduino 复制代码
public enum Sentiment {
    POSITIVE, NEUTRAL, NEGATIVE
}

使用:

java

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

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

5.4 自定义输出转换器

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

5.4.1 使用 BeanOutputConverter 直接构建提示词

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

java

ini 复制代码
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

ini 复制代码
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

kotlin 复制代码
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

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

期望返回:

json

json 复制代码
{
  "name": "张三",
  "age": 28,
  "city": "上海"
}

测试提取多个:

bash

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

less 复制代码
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、集合、枚举等。
  • 自定义转换器BeanOutputConverterMapOutputConverter 的使用。
  • 实践:构建了信息提取服务,并测试了效果。

六、函数调用:赋予 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 注解需要指定工具的名称和描述,描述会被传递给模型,帮助模型理解何时调用该工具。

java 复制代码
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 带参数的工具

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

java 复制代码
@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 中,以便它知道有哪些工具可用。

java 复制代码
@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。我们需要在构建时指定工具:

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

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

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

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

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

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

6.4.1 完整代码

java 复制代码
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 中添加图像模型的配置:

yaml 复制代码
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。

java 复制代码
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。在聊天中,我们可以将图片作为用户消息的一部分发送。

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

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

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

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

java 复制代码
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。

完整示例:

java 复制代码
@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 配置语音模型

yaml 复制代码
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)

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

java 复制代码
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 中)。

java 复制代码
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 层

java 复制代码
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

java 复制代码
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。可以编写一个工具方法:

java 复制代码
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:

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

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

yaml 复制代码
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 环境,可以先用内存存储调试:

xml 复制代码
<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):

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

然后,使用 PagePdfDocumentReader 加载 PDF:

java 复制代码
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

java 复制代码
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 的示例:

java 复制代码
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 为例):

yaml 复制代码
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      embedding:
        options:
          model: text-embedding-ada-002

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

8.4 知识摄入实践

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

8.4.1 创建摄入服务

java 复制代码
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 接口:

java 复制代码
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 验证摄入结果

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

java 复制代码
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() 自动完成向量化并存入数据库。
  • 实践:编写了摄入服务,并在应用启动时自动加载知识文档。
相关推荐
布列瑟农的星空1 小时前
前端都能看懂的rust入门教程(二)——函数和闭包
前端·后端·rust
颜酱2 小时前
二叉树分解问题思路解题模式
javascript·后端·算法
晨米酱2 小时前
四、Prettier 编辑器集成指南
前端·代码规范
zone77392 小时前
001:LangChain的LCEL语法学习
人工智能·后端·面试
zone77392 小时前
001:简单 RAG 入门
后端·python·面试
文心快码BaiduComate2 小时前
Comate 4.0新年全面焕新!底层重构、七大升级、复杂任务驾驭力跃升
前端·程序员·架构
嘻哈baby2 小时前
如何理解Rust语言中Send和Sync?
后端
怪可爱的地球人2 小时前
uni-app:5 步接入 vite-plugin-uni-pages,用 <route> 自动生成 pages.json
前端
前端Hardy2 小时前
告别 !important:现代 CSS 层叠控制指南,90% 的样式冲突其实不用它也能解
前端·vue.js·面试