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

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

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

自ChatGPT问世以来,大语言模型(LLM)技术飞速发展,深刻改变着软件开发的方式。然而,作为国内Java开发者,我们面临的挑战更为具体:

  • 海外模型访问受限:OpenAI等国外模型在国内访问不稳定,合规性要求高。
  • 国产模型崛起:通义千问、文心一言、智谱等国产大模型能力日益强大,急需适配Java生态的集成工具。
  • 微服务架构普及:Java后端普遍采用Spring Boot/Cloud体系,需要AI框架能与现有架构无缝融合。
  • 生产级需求:AI应用需要配置管理、服务发现、灰度发布、监控告警等企业级能力,而不仅仅是API调用。

1.2 Spring AI Alibaba是什么?

Spring AI Alibaba 是阿里巴巴开源的一款基于Spring AI的AI集成框架,专为Java开发者设计,旨在简化国产大模型(特别是通义千问系列)的接入,并与Spring Cloud Alibaba生态深度整合。它继承了Spring AI的设计哲学,同时提供了:

  • 对阿里云通义千问模型的一等支持:包括qwen-turbo、qwen-plus、qwen-max、多模态模型等。
  • 与阿里云基础设施的无缝对接:如阿里云百炼平台、MaaS(模型即服务)、OSS等。
  • 企业级微服务特性:通过Nacos配置中心实现提示词动态管理,通过服务发现实现AI服务治理,通过Higress AI网关实现统一路由。

简单来说,Spring AI Alibaba = Spring AI + 通义千问 + 阿里云生态 + Spring Cloud Alibaba,让Java开发者像写普通业务代码一样使用大模型。

1.3 核心优势

1.3.1 国产化适配,稳定可靠

  • 直连阿里云通义千问API:国内访问速度快,稳定性高。
  • 支持私有化部署:可通过阿里云百炼平台私有化模型,满足数据合规要求。

1.3.2 继承Spring生态精髓

  • 自动配置 :引入starter后,ChatClientImageModel等Bean自动创建。
  • 声明式编程 :像用RestTemplate一样用ChatClient
  • 与Spring Boot完美集成 :配置在application.yml中完成。

1.3.3 企业级微服务特性

  • 配置中心:使用Nacos管理提示词模板、模型参数,支持动态刷新。
  • 服务发现:AI服务可注册到Nacos,其他服务通过服务名调用。
  • 网关集成:Higress AI网关提供流量控制、鉴权、灰度发布。
  • 可观测性:集成阿里云ARMS,提供调用链、指标监控。

1.3.4 丰富的AI能力

  • 对话:同步/流式聊天。
  • 多模态:图像生成、图像理解、语音识别、语音合成。
  • 函数调用:让AI调用Java方法。
  • RAG:基于向量数据库的检索增强生成。
  • 工作流编排:通过Spring AI Alibaba Graph实现多Agent协作。

1.3.5 与原始调用方式对比

原始方式(手动调用阿里云API):

java 复制代码
// 手动构造HTTP请求,处理签名,解析JSON
String url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
String apiKey = "sk-xxx";
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(url))
    .header("Authorization", "Bearer " + apiKey)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"model\":\"qwen-turbo\",\"input\":{\"messages\":[{\"role\":\"user\",\"content\":\"你好\"}]}}"))
    .build();
// ... 解析响应 ...

Spring AI Alibaba方式:

java 复制代码
@RestController
public class ChatController {
    private final ChatClient chatClient;
    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }
    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt().user(message).call().content();
    }
}

代码量减少90%,且更易于维护。

1.4 本文目标

本文面向零基础Java开发者,即使你从未接触过大模型开发,也能通过一步步实践,掌握Spring AI Alibaba的所有核心组件。你将学到:

  • 基础篇:快速搭建应用,实现聊天、流式响应、提示词模板。
  • 多模态篇:图像生成与理解、语音处理。
  • 函数调用:让AI查询实时数据。
  • RAG篇:构建企业知识库问答系统。
  • 工作流编排:使用Graph实现多Agent协作。
  • 管理平台:提示词热更新、监控可观测性。
  • 微服务集成:配置中心、服务发现、灰度发布。

每一章都包含完整的代码示例和运行效果,助你真正落地AI应用。

1.5 Spring AI Alibaba组件全景图

graph TD subgraph "应用层" A[ChatClient] --> B[流式响应] A --> C[提示词模板] A --> D[结构化输出] A --> E[函数调用] end subgraph "模型层 (通义千问)" F[聊天模型] --> F1[qwen-turbo] F --> F2[qwen-plus] F --> F3[qwen-max] G[图像模型] --> G1[通义万相] H[语音模型] --> H1[语音合成] H --> H2[语音识别] end subgraph "增强层" I[VectorStore] --> I1[Milvus] I --> I2[PGVector] J[DocumentReader] K[EmbeddingClient] end subgraph "基础设施" L[Nacos配置中心] M[Nacos服务发现] N[Higress AI网关] O[ARMS监控] end A --> F A --> G A --> H A --> I A -.-> L A -.-> M N -.-> A O -.-> A style A fill:#f9f,stroke:#333,stroke-width:2px style F fill:#bbf,stroke:#333 style G fill:#bbf,stroke:#333 style H fill:#bbf,stroke:#333 style I fill:#bfb,stroke:#333 style L fill:#fbb,stroke:#333 style M fill:#fbb,stroke:#333 style N fill:#fbb,stroke:#333

图释:应用层通过ChatClient统一接口,调用底层通义模型;增强层提供RAG能力;基础设施层通过Nacos、Higress等提供微服务治理。

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

在开始动手之前,我们先确保你的开发环境就绪。本章将带领你完成从零到第一个可运行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域名:dashscope.aliyuncs.com
  • 阿里云账号:用于获取API密钥(后文详述)

2.2 在项目中引入 Spring AI Alibaba 依赖

Spring AI Alibaba 基于 Spring AI 构建,并提供了针对阿里云通义模型的自动配置。我们将创建一个标准的 Spring Boot 项目,并添加必要的依赖。

2.2.1 使用 Spring Initializr 快速创建项目(推荐)

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

2.2.2 手动添加 Spring AI Alibaba 依赖

Spring AI Alibaba 的 Starter 并未发布到 Maven Central,需要添加阿里云仓库和 Spring 里程碑仓库。在 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-alibaba-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <spring-ai-alibaba.version>0.8.0</spring-ai-alibaba.version> <!-- 使用最新稳定版 -->
    </properties>

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

        <!-- Spring AI Alibaba Starter -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter</artifactId>
            <version>${spring-ai-alibaba.version}</version>
        </dependency>
    </dependencies>

    <!-- 添加仓库:阿里云仓库和 Spring 里程碑仓库 -->
    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

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

依赖说明

  • spring-boot-starter-web:提供 REST API 能力。
  • spring-ai-alibaba-starter:Spring AI Alibaba 核心起步依赖,包含对通义千问模型的自动配置。

2.2.3 Gradle 配置(可选)

如果你使用 Gradle,在 build.gradle 中添加:

groovy 复制代码
plugins {
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'java'
}

group = 'com.example'
version = '1.0-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
    maven { url 'https://maven.aliyun.com/repository/public' }
    maven { url 'https://repo.spring.io/milestone' }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.alibaba.cloud.ai:spring-ai-alibaba-starter:0.8.0'
}

2.3 获取阿里云 API 密钥

Spring AI Alibaba 通过阿里云 DashScope 服务调用通义模型,你需要先开通服务并获取 API Key。

2.3.1 开通阿里云百炼服务

  1. 访问 阿里云百炼平台(需登录阿里云账号)。
  2. 如果首次使用,点击"立即开通",同意服务协议。
  3. 开通后,在控制台左侧导航栏选择 API-KEY 管理

2.3.2 创建 API Key

  1. 点击"创建 API-KEY",输入名称(如"我的AI应用")。
  2. 生成后,复制密钥字符串(格式如 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)。
  3. 注意:请妥善保管 API Key,不要泄露。

2.3.3 配置 API Key

src/main/resources/application.yml 中配置:

yaml 复制代码
spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}  # 从环境变量读取,或直接写字符串(不推荐)

为了安全,强烈建议使用环境变量

  • Linux/Mac :在 ~/.bashrc~/.zshrc 中添加 export DASHSCOPE_API_KEY=sk-xxx,然后执行 source ~/.bashrc
  • Windows :在系统环境变量中添加 DASHSCOPE_API_KEY=sk-xxx

如果你只是快速测试,可以临时在配置文件中写死,但切记不要提交到代码仓库。

2.3.4 可选配置:指定模型

通义千问系列有多种模型,可在配置文件中指定默认模型:

yaml 复制代码
spring:
  ai:
    dashscope:
      chat:
        options:
          model: qwen-plus  # 可选 qwen-turbo, qwen-plus, qwen-max
          temperature: 0.8

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 Alibaba 自动配置创建的构建器,用于创建 ChatClient 实例。我们通过构造器注入它,然后调用 build() 方法得到 ChatClient
  • chatClient.prompt():开始构建一个提示词(Prompt)。
  • .user(message) :设置用户消息。也可以设置系统消息(.system())。
  • .call():发起同步调用,等待模型返回完整响应。
  • .content():从响应中提取文本内容。

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 DashScope API 浏览器->>Controller: GET /chat?message=你好 Controller->>ChatClient: prompt().user(message).call() ChatClient->>DashScope API: HTTP请求(包含消息) DashScope API-->>ChatClient: 返回AI生成的回复 ChatClient-->>Controller: 返回content() Controller-->>浏览器: 返回"你好,我是通义千问AI助手..."

2.4.5 可能遇到的问题及解决

问题 可能原因 解决方法
启动失败,ChatClient.Builder 未找到 依赖未正确引入或自动配置未生效 检查 pom.xml 是否包含 spring-ai-alibaba-starter,并确保仓库配置正确
调用时报错 401 Unauthorized API Key 未配置或配置错误 检查 application.yml 中的 spring.ai.dashscope.api-key 是否正确,或环境变量是否设置
响应超时或网络错误 无法访问 DashScope API 检查网络,或配置代理(如有需要)
模型返回内容不符合预期 模型选择或参数问题 可在配置中调整 modeltemperature,或在调用时动态指定参数(后文介绍)

2.5 本章小结

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

  • 使用 Spring Initializr 创建 Spring Boot 项目,并手动添加 Spring AI Alibaba 依赖。
  • 在阿里云百炼平台获取 API Key,并配置到项目中。
  • 编写 REST 控制器,注入并使用 ChatClient
  • 运行并测试第一个 AI 接口,了解了可能的故障排查。

三、核心交互:ChatClient 与通义千问模型

通过上一章,你已经成功搭建了第一个 Spring AI Alibaba 应用,并实现了最简单的"一问一答"接口。但实际应用中,我们往往需要更精细的控制:给 AI 设定角色(系统消息)、动态构造提问内容、获取完整的响应元数据(如 Token 消耗)、以及实现流式输出提升用户体验等。本章将带你深入掌握 ChatClient 的核心 API,并学习如何使用提示词模板和参数配置,让你的 AI 交互更加灵活和强大。

3.1 ChatClient 核心 API 解析

ChatClient 是 Spring AI Alibaba 中用于与大模型交互的核心接口。它采用流式(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(DashScopeChatOptions.builder().temperature(0.8).build())
call() 发起同步调用,返回 ChatResponse .call()
stream() 发起流式调用,返回 Flux<ChatResponse> 下一节介绍
.content() ChatResponse 中提取文本内容(快捷方式) .call().content()

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.getMetadata().containsKey("usage")) {
    var usage = generation.getMetadata().get("usage");
    System.out.println("Token 用量:" + usage);
}

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

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();

这种方式需要你手动维护消息列表,比较繁琐。后续章节将介绍如何通过 ChatMemory 简化多轮对话管理。

3.2 模型参数配置(DashScopeChatOptions)

通义千问模型提供了丰富的参数来控制生成行为,如温度(temperature)、最大 Token 数(maxTokens)、Top P(topP)等。Spring AI Alibaba 通过 DashScopeChatOptions 类来封装这些参数。

3.2.1 全局默认配置

application.yml 中设置全局默认参数:

yaml 复制代码
spring:
  ai:
    dashscope:
      chat:
        options:
          model: qwen-plus        # 模型名称:qwen-turbo, qwen-plus, qwen-max
          temperature: 0.8         # 温度,值越高输出越随机
          max-tokens: 2048         # 最大生成 Token 数
          top-p: 0.9               # 核采样参数
          enable-search: false     # 是否启用搜索增强(通义千问特有)

这些参数将作为默认值应用于所有通过 ChatClient 的调用。

3.2.2 每次调用动态指定参数

如果你希望针对特定请求覆盖全局参数,可以在构建 Prompt 时传入 ChatOptions

java 复制代码
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;

DashScopeChatOptions options = DashScopeChatOptions.builder()
        .withModel("qwen-max")
        .withTemperature(0.5)
        .withMaxTokens(1024)
        .build();

String response = chatClient.prompt()
        .user("请用专业术语解释量子计算。")
        .options(options)
        .call()
        .content();

3.2.3 参数含义说明

参数 类型 说明 建议值
model String 通义千问模型版本 qwen-turbo(最快)、qwen-plus(平衡)、qwen-max(最强)
temperature Float 控制随机性,0-2之间,越低越确定 创意任务0.8-1.2,确定性任务0.2-0.5
maxTokens Integer 生成的最大 Token 数 根据场景调整,一般1024-2048
topP Float 核采样,0-1之间,通常与temperature协同使用 0.8-0.9
enableSearch Boolean 是否启用搜索增强(模型会联网搜索) 实时信息查询时设为 true

3.3 流式响应实现打字机效果

同步调用需要等待模型完整生成回答,对于长文本体验不佳。流式响应可以边生成边返回,形成"打字机"效果,大幅提升用户体验。

3.3.1 使用 stream() 方法

ChatClient 提供了 stream() 方法,返回 Flux<ChatResponse>(Reactor 的响应式流)。

java 复制代码
import reactor.core.publisher.Flux;

@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 提取文本内容,得到 Flux<String>

3.3.2 前端接收示例

创建一个简单的 HTML 页面(放在 src/main/resources/static/index.html):

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>流式聊天演示</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 = '';

            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();
            };
        }
    </script>
</body>
</html>

3.3.3 流式响应流程示意图

sequenceDiagram participant 浏览器 participant Controller participant ChatClient participant DashScope API 浏览器->>Controller: GET /chat/stream?message=... Controller->>ChatClient: prompt().user().stream() ChatClient->>DashScope API: 发起流式请求 loop 逐块返回 DashScope API-->>ChatClient: 返回文本块 ChatClient-->>Controller: 发射 Flux 元素 Controller-->>浏览器: SSE: data: "块内容" 浏览器->>浏览器: 追加显示 end ChatClient-->>Controller: 完成信号 Controller-->>浏览器: 关闭 SSE 连接

3.4 提示词模板(PromptTemplate)

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

3.4.1 创建模板文件

src/main/resources/prompts 目录下创建一个文本文件,例如 order-status.st

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

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

3.4.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();

3.4.3 直接使用字符串模板

如果模板比较简单,也可以直接使用字符串:

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

3.4.4 结合系统消息

如果需要同时使用系统消息和模板用户消息,可以手动构建 Prompt

java 复制代码
String systemMsg = "你是一个翻译助手,只输出翻译结果。";
String userTemplate = "将 {{text}} 翻译成 {{targetLanguage}}";
PromptTemplate userPromptTemplate = new PromptTemplate(userTemplate);
Prompt userPrompt = userPromptTemplate.create(Map.of(
    "text", "Hello",
    "targetLanguage", "法语"
));

List<Message> messages = Arrays.asList(
    new SystemMessage(systemMsg),
    new UserMessage(userPrompt.getContents())
);
Prompt finalPrompt = new Prompt(messages);
String response = chatClient.prompt(finalPrompt).call().content();

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

现在让我们综合运用本章所学,构建一个更实用的聊天助手:一个能记住用户名字并提供个性化问候的助手,同时支持流式响应。

3.5.1 创建 Controller

java 复制代码
package com.example.demo.controller;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springframework.ai.chat.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import java.util.Map;

@RestController
@RequestMapping("/assistant")
public class AssistantController {

    private final ChatClient chatClient;

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

    @GetMapping("/greet")
    public String greet(@RequestParam String name) {
        return chatClient.prompt()
                .system("你是一个热情的接待员,用感叹号结尾。")
                .user("你好,我叫 " + name + ",请向我问好。")
                .call()
                .content();
    }

    @GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chat(@RequestParam String message,
                             @RequestParam(required = false) String model) {
        var promptBuilder = chatClient.prompt().user(message);
        
        // 如果指定了模型,动态设置参数
        if (model != null) {
            DashScopeChatOptions options = DashScopeChatOptions.builder()
                    .withModel(model)
                    .build();
            promptBuilder.options(options);
        }
        
        return promptBuilder.stream()
                .map(chatResponse -> chatResponse.getResult().getOutput().getContent());
    }
}

3.5.2 测试

  • 访问 http://localhost:8080/assistant/greet?name=张三,返回热情问候。
  • 访问 http://localhost:8080/assistant/chat?message=讲个笑话,看到流式输出。
  • 访问 http://localhost:8080/assistant/chat?message=解释量子计算&model=qwen-max,使用 qwen-max 模型。

3.6 本章小结

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

  • ChatClient 核心 APIprompt()user()system()call()content() 等。
  • 获取完整响应:了解如何获取 Token 用量等元数据。
  • 参数配置:全局和动态设置通义千问模型参数。
  • 流式响应 :使用 stream() 和 SSE 实现打字机效果。
  • 提示词模板 :使用 PromptTemplate 动态构造用户消息,避免硬编码。
  • 实践:构建了带角色设定的聊天助手,支持流式和模型切换。

四、多模态能力:图像生成与理解

在前面的章节中,我们所有的交互都基于文本。但在实际应用中,图像能力同样重要------让 AI 能够"画图"或"看懂"图片,可以极大地拓展应用场景。Spring AI Alibaba 提供了对通义多模态模型的支持,包括图像生成(文生图)和图像理解(图生文)。本章将带你掌握这些能力,让你的应用不仅能说会道,还能挥毫泼墨。

4.1 Spring AI Alibaba 对多模态的支持

通义千问系列不仅包含强大的语言模型,还提供了多模态模型:

能力 模型名称 适用场景 Spring AI Alibaba 客户端
文本对话 qwen-turbo / plus / max 通用聊天、问答 ChatClient
图像生成 通义万相(wanx) 根据文本描述生成图片 TongYiImagesModel
图像理解 qwen-vl系列(如qwen-vl-plus) 识别图片内容、视觉问答 ChatClient(需传入图片)
语音合成 sambert系列 文本转语音 TongYiAudioModel(待支持)
语音识别 whisper系列 语音转文字 TongYiAudioModel(待支持)

目前,Spring AI Alibaba 对图像生成(通义万相)有完善的支持,对图像理解(qwen-vl)可以通过 ChatClient 传入图片消息实现。本章将重点介绍这两项能力。

4.2 文生图实践:TongYiImagesModel 使用详解

通义万相(Wanx)是阿里云提供的图像生成模型,支持根据文本描述生成高质量图片。Spring AI Alibaba 通过 TongYiImagesModel 客户端封装了相关 API。

4.2.1 引入依赖

spring-ai-alibaba-starter 已经包含了图像模型的自动配置,无需额外依赖。

4.2.2 配置参数

application.yml 中,可以配置图像生成的默认参数:

yaml 复制代码
spring:
  ai:
    dashscope:
      image:
        options:
          model: wanx-v1                # 通义万相模型
          n: 1                           # 生成图片数量
          size: 1024x1024                 # 图片尺寸
          style: <auto>                   # 风格(可选)

也可以不配置,使用默认值。

4.2.3 注入并使用 TongYiImagesModel

java 复制代码
import com.alibaba.cloud.ai.dashscope.image.TongYiImagesModel;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
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 ImageController {

    private final TongYiImagesModel imageModel;

    @Autowired
    public ImageController(TongYiImagesModel imageModel) {
        this.imageModel = imageModel;
    }

    @GetMapping("/generate-image")
    public String generateImage(@RequestParam String prompt) {
        ImagePrompt imagePrompt = new ImagePrompt(prompt);
        ImageResponse response = imageModel.call(imagePrompt);
        // 返回第一张图片的 Base64 编码或 URL(取决于模型)
        String b64Image = response.getResult().getOutput().getB64Json();
        // 前端可以用 <img src="data:image/png;base64,{{b64Image}}"> 显示
        return b64Image;
    }
}

说明

  • ImagePrompt 封装了提示词,也可以设置参数覆盖默认值。
  • ImageResponse 包含生成的图片,可能是 Base64 字符串或图片 URL(取决于模型版本)。
  • 通义万相返回的是 Base64 编码的图片,可以直接在 HTML 的 img 标签的 src 属性中使用 data:image/png;base64, 前缀显示。

4.2.4 动态设置图像参数

可以在调用时动态指定参数,覆盖全局配置:

java 复制代码
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;

DashScopeImageOptions options = DashScopeImageOptions.builder()
        .withModel("wanx-v1")
        .withN(2)               // 生成2张图
        .withSize("512x512")
        .withStyle("<auto>")
        .build();

ImagePrompt prompt = new ImagePrompt("一只可爱的猫", options);
ImageResponse response = imageModel.call(prompt);

4.2.5 前端显示示例

创建一个简单的 HTML 页面来测试图像生成:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>通义万相图像生成</title>
</head>
<body>
    <h2>文生图演示</h2>
    <input type="text" id="prompt" placeholder="输入图片描述" value="一只可爱的猫">
    <button onclick="generate()">生成</button>
    <div id="result" style="margin-top:20px;"></div>

    <script>
        function generate() {
            const prompt = document.getElementById('prompt').value;
            fetch(`/generate-image?prompt=${encodeURIComponent(prompt)}`)
                .then(res => res.text())
                .then(base64 => {
                    document.getElementById('result').innerHTML = 
                        `<img src="data:image/png;base64,${base64}" style="max-width:100%;">`;
                });
        }
    </script>
</body>
</html>

4.2.6 文生图流程示意图

sequenceDiagram participant 前端 participant Controller participant ImagesModel participant 通义万相API 前端->>Controller: GET /generate-image?prompt=... Controller->>ImagesModel: call(ImagePrompt) ImagesModel->>通义万相API: 发起图像生成请求 通义万相API-->>ImagesModel: 返回Base64图片 ImagesModel-->>Controller: ImageResponse Controller-->>前端: Base64字符串 前端->>前端: 显示图片

4.3 图像理解:让 AI 描述图片

通义千问的 qwen-vl 系列模型支持视觉输入,可以接收图片并理解其内容。在 Spring AI Alibaba 中,我们可以通过 ChatClient 发送包含图片的用户消息来实现图像理解。

4.3.1 配置支持视觉的模型

application.yml 中指定使用 qwen-vl 模型:

yaml 复制代码
spring:
  ai:
    dashscope:
      chat:
        options:
          model: qwen-vl-plus   # 或 qwen-vl-max

4.3.2 构造包含图片的消息

Spring AI 的 Message 接口支持多种内容类型,包括文本和图片。我们需要创建一个 UserMessage,包含文本和图片的 Media 对象。

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;

// 从 URL 加载图片
UserMessage userMessage = new UserMessage("请描述这张图片",
        new Media(MimeTypeUtils.IMAGE_JPEG, "https://example.com/cat.jpg"));

// 从本地文件加载图片(需转为 Base64 data URL)
String base64Image = imageFileToDataUrl("/path/to/cat.jpg");
UserMessage userMessage = new UserMessage("请描述这张图片",
        new Media(MimeTypeUtils.IMAGE_JPEG, base64Image));

4.3.3 工具方法:图片文件转 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;
}

4.3.4 完整示例:图片描述接口

创建 VisionController.java

java 复制代码
package com.example.demo.controller;

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.util.MimeTypeUtils;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

@RestController
@RequestMapping("/vision")
public class VisionController {

    private final ChatClient chatClient;

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

    @PostMapping("/describe")
    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();
    }

    @PostMapping("/describe-local")
    public String describeLocalImage(@RequestParam String filePath) throws IOException {
        String dataUrl = imageFileToDataUrl(filePath);
        UserMessage userMessage = new UserMessage("请用中文详细描述这张图片的内容",
                new Media(MimeTypeUtils.IMAGE_JPEG, dataUrl));
        Prompt prompt = new Prompt(userMessage);
        return chatClient.prompt(prompt).call().content();
    }

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

4.3.5 测试图像理解

启动应用,使用 curl 测试:

bash 复制代码
# 测试网络图片
curl "http://localhost:8080/vision/describe?imageUrl=https://example.com/cat.jpg"

# 测试本地图片(需提供绝对路径)
curl -X POST "http://localhost:8080/vision/describe-local?filePath=/Users/xxx/cat.jpg"

AI 会返回对图片的描述。

4.3.6 图像理解流程示意图

sequenceDiagram participant 用户 participant Controller participant ChatClient participant qwen-vl API 用户->>Controller: POST /describe?imageUrl=... Controller->>ChatClient: 构建包含图片的 UserMessage ChatClient->>qwen-vl API: 发送图片+文本提示 qwen-vl API-->>ChatClient: 返回图片描述 ChatClient-->>Controller: 返回文本 Controller-->>用户: 返回描述

4.4 实践:构建图文问答助手

结合图像理解和对话能力,我们可以构建一个能够回答图片相关问题的助手。用户上传一张图片,并提出问题(例如"这张图里有什么动物?"),AI 基于图片内容回答。

4.4.1 创建图文问答接口

java 复制代码
@PostMapping("/ask-about-image")
public String askAboutImage(@RequestParam String imageUrl,
                            @RequestParam String question) {
    UserMessage userMessage = new UserMessage(question,
            new Media(MimeTypeUtils.IMAGE_JPEG, imageUrl));
    Prompt prompt = new Prompt(userMessage);
    return chatClient.prompt(prompt).call().content();
}

4.4.2 前端上传示例

创建一个简单的 HTML 表单,允许用户输入图片 URL 和问题。

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>图文问答</title>
</head>
<body>
    <h2>图文问答助手</h2>
    <input type="text" id="imageUrl" placeholder="图片URL" size="50"><br>
    <input type="text" id="question" placeholder="问题" size="50"><br>
    <button onclick="ask()">提问</button>
    <div id="answer" style="margin-top:20px;"></div>

    <script>
        async function ask() {
            const imageUrl = document.getElementById('imageUrl').value;
            const question = document.getElementById('question').value;
            const response = await fetch(`/vision/ask-about-image?imageUrl=${encodeURIComponent(imageUrl)}&question=${encodeURIComponent(question)}`);
            const text = await response.text();
            document.getElementById('answer').innerText = text;
        }
    </script>
</body>
</html>

4.5 本章小结

通过本章的学习,你掌握了 Spring AI Alibaba 的多模态能力:

  • 图像生成 :使用 TongYiImagesModel 从文本生成图片,并获取 Base64 格式的结果。
  • 图像理解:通过支持视觉的聊天模型(qwen-vl),将图片作为消息的一部分发送,让 AI 描述或问答。
  • 实践:构建了文生图接口和图文问答助手。

五、结构化输出:让 AI 返回 Java 对象

在前面的章节中,AI 返回的都是自然语言文本。但在实际应用中,我们经常需要从自然语言中提取结构化数据,例如从一段用户描述中提取姓名、年龄、城市,或者将 AI 生成的内容直接作为 Java 对象供程序使用。如果每次都手动解析文本,不仅繁琐,而且容易出错。

Spring AI 提供了强大的结构化输出能力,允许你直接让 AI 返回一个 Java 对象,框架会自动处理 JSON 生成与解析。本章将带你掌握这一技能,让你的 AI 交互更加智能和便捷。

5.1 为什么需要结构化输出?

考虑以下场景:

  • 你有一个客服系统,用户输入"我叫张三,今年28岁,住在上海",你需要提取出用户的姓名、年龄和城市,存入数据库。
  • 你需要 AI 生成一份格式化的报告,包含标题、作者、发布日期等字段,后续需要将这些字段映射到 Java 对象中。
  • 你希望 AI 对一段文本进行情感分析,返回 POSITIVENEUTRALNEGATIVE 这样的枚举值。

如果 AI 只返回文本,你就需要编写复杂的正则表达式或依赖大模型生成 JSON 然后手动解析。这不仅增加了代码复杂度,而且容易因模型输出格式变化而失效。

结构化输出正是为了解决这些问题。Spring AI 通过在请求中隐式地要求模型以 JSON 格式返回数据,并自动将 JSON 反序列化为你指定的 Java 类型,让你可以像调用普通方法一样获得类型安全的对象。

5.2 使用 entity() 方法映射 POJO

ChatClient 提供了 entity(Class<T> type) 方法,它接受一个 Java 类型,并返回该类型的实例。框架会在背后完成以下工作:

  1. 在发送给模型的提示词中,添加指令要求模型以 JSON 格式返回,并符合指定的 Java 类型结构。
  2. 调用模型获取响应。
  3. 将响应的 JSON 内容解析为指定类型的对象。

5.2.1 定义 POJO 类

首先,定义一个简单的 Java 类(或 Record)用于接收数据。我们以 Person 为例:

java 复制代码
public class Person {
    private String name;
    private int age;
    private String city;

    // 必须提供无参构造器(或全参构造器+默认构造器)
    public Person() {}

    // getter 和 setter 必须提供,因为框架通过反射设置属性
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", city='" + city + "'}";
    }
}

或者使用更简洁的 Java Record(Java 14+):

java 复制代码
public record Person(String name, int age, String city) {}

使用 Record 更简洁,但需要确保你的 Java 版本支持(JDK 17 完美支持)。Spring AI 对 Record 的支持同样良好。

5.2.2 调用 entity() 方法

现在,我们可以在 Controller 或 Service 中使用 entity() 方法:

java 复制代码
@RestController
public class StructuredOutputController {

    private final ChatClient chatClient;

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

    @GetMapping("/extract-person")
    public Person extractPerson(@RequestParam String text) {
        return chatClient.prompt()
                .user(text)
                .call()
                .entity(Person.class);
    }
}

5.2.3 测试

启动应用,访问: http://localhost:8080/extract-person?text=我叫张三,今年28岁,住在上海

你会看到类似以下的 JSON 响应(Spring 会自动将返回的 Person 对象序列化为 JSON):

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

背后的原理:Spring AI 实际上在发送给模型的提示词中附加了类似以下的指令:

php 复制代码
请根据用户输入,以 JSON 格式输出,包含 name、age、city 字段,类型分别为 string、integer、string。

通义千问模型能够理解这些指令并生成符合要求的 JSON。

5.2.4 如果 AI 返回的 JSON 无法解析怎么办?

如果模型偶尔返回的 JSON 格式不正确,entity() 方法会抛出异常。你可以捕获异常并处理,或者要求模型重试。在后续章节中,我们会介绍如何通过重试机制提高成功率。

5.3 支持的类型

entity() 方法支持多种 Java 类型,不仅限于简单的 POJO。

5.3.1 基础类型和包装类

可以直接返回 IntegerBooleanString 等。例如,让 AI 判断一句话的情感极性(返回布尔值):

java 复制代码
@GetMapping("/is-positive")
public Boolean isPositive(@RequestParam String text) {
    return chatClient.prompt()
            .user("判断以下文本的情感极性,积极返回 true,消极返回 false:" + text)
            .call()
            .entity(Boolean.class);
}

5.3.2 集合类型

可以返回 List<T>Set<T> 等。例如,让 AI 从一段文本中提取多个人的信息:

java 复制代码
public record Person(String name, int age) {}

@GetMapping("/extract-persons")
public List<Person> extractPersons(@RequestParam String text) {
    return chatClient.prompt()
            .user("从以下文本中提取所有人名和年龄,以 JSON 数组返回:" + text)
            .call()
            .entity(new ParameterizedTypeReference<List<Person>>() {});
}

注意,对于泛型集合,需要使用 ParameterizedTypeReference 来传递类型信息,因为 Java 会在运行时擦除泛型。

5.3.3 枚举类型

假设我们有一个情感枚举:

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

可以这样使用:

java 复制代码
@GetMapping("/analyze-sentiment")
public Sentiment analyzeSentiment(@RequestParam String text) {
    return chatClient.prompt()
            .user("分析以下文本的情感,返回 POSITIVE、NEUTRAL 或 NEGATIVE:" + text)
            .call()
            .entity(Sentiment.class);
}

框架会将模型返回的字符串(如 "POSITIVE")自动转换为枚举常量。

5.3.4 嵌套对象

如果数据结构复杂,可以定义嵌套的 Java 类。例如,包含地址信息的用户:

java 复制代码
public class Address {
    private String street;
    private String city;
    // getters/setters
}

public class User {
    private String name;
    private int age;
    private Address address;
    // getters/setters
}

调用方式与简单对象相同,框架会递归处理嵌套 JSON。

5.4 实践:从用户输入提取人员信息

现在,我们构建一个更完整的示例,演示如何从一段自然语言中提取多个人员信息,并返回列表。

5.4.1 定义数据类

java 复制代码
public record Person(String name, Integer age, String city) {}

5.4.2 创建 Service 层

java 复制代码
package com.example.demo.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ExtractionService {

    private final ChatClient chatClient;

    public ExtractionService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public List<Person> extractPersons(String text) {
        String prompt = "从以下文本中提取所有人物的姓名、年龄和城市,以 JSON 数组格式返回,数组每个元素包含 name, age, city 字段。文本:" + text;
        return chatClient.prompt()
                .user(prompt)
                .call()
                .entity(new ParameterizedTypeReference<List<Person>>() {});
    }
}

5.4.3 创建 Controller

java 复制代码
package com.example.demo.controller;

import com.example.demo.model.Person;
import com.example.demo.service.ExtractionService;
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;

@RestController
public class ExtractionController {

    private final ExtractionService extractionService;

    public ExtractionController(ExtractionService extractionService) {
        this.extractionService = extractionService;
    }

    @GetMapping("/extract")
    public List<Person> extract(@RequestParam String text) {
        return extractionService.extractPersons(text);
    }
}

5.4.4 测试

启动应用,使用 curl 测试:

bash 复制代码
curl "http://localhost:8080/extract?text=张三28岁住北京,李四35岁在上海,王五42岁在广州"

期望返回 JSON 数组:

json 复制代码
[
  {"name":"张三","age":28,"city":"北京"},
  {"name":"李四","age":35,"city":"上海"},
  {"name":"王五","age":42,"city":"广州"}
]

5.4.5 可能遇到的问题及解决

  • 模型返回格式不正确 :可以在提示词中加强约束,例如"必须返回有效的 JSON 数组,不要包含任何额外文字"。Spring AI 的 entity() 方法已经内置了默认的格式指令,但有时模型可能忽略,你可以自定义提示词模板。
  • 字段名不匹配 :确保 Java 类的字段名与模型返回的 JSON 键名一致(大小写敏感)。如果不同,可以在类上使用 @JsonProperty 注解(Jackson)来映射。
  • 类型转换错误:如果模型返回的 age 是字符串 "28",而 Java 字段是 int,会转换失败。可以在提示词中明确指定类型。

5.5 进阶:自定义输出转换器

在某些复杂场景下,你可能需要完全控制输出解析逻辑。Spring AI 允许你自定义 OutputParser。例如,实现一个解析器将模型输出转换为 Map 或其他格式。

entity() 方法已经足够覆盖绝大多数场景,本教程不再深入。

5.6 本章小结

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

  • 结构化输出的价值:从自然语言到 Java 对象的自动化映射。
  • entity() 方法的使用:指定目标类型,获取类型安全的对象。
  • 支持的类型:基础类型、POJO、集合、枚举、嵌套对象。
  • 实践:构建了人员信息提取服务,并测试了效果。

六、函数调用:让 AI 执行你的 Java 方法

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

函数调用(Function Calling)正是为了解决这个问题而生。它允许大模型在需要时"调用"你编写的 Java 方法,获取实时数据或执行操作,然后将结果整合到回答中。Spring AI Alibaba 完美支持这一能力,并与通义千问模型深度集成。

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分。"

在整个流程中,除了定义工具方法外,你几乎不需要额外代码。Spring AI Alibaba 会自动处理工具调用的握手过程。

6.2 在 Spring AI Alibaba 中定义工具

Spring AI Alibaba 通过 @Tool 注解来标记一个方法作为可被 AI 调用的工具。你只需将工具类注册为 Spring Bean,框架会自动收集并提供给模型。

6.2.1 引入依赖

函数调用功能已包含在 spring-ai-alibaba-starter 中,无需额外依赖。

6.2.2 定义工具类

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

java 复制代码
import com.alibaba.cloud.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"));
    }
}

说明

  • name:工具名称,建议使用驼峰命名,如 getCurrentTime
  • description:工具描述,非常重要!AI 会根据描述判断何时调用该工具。描述越清晰,调用准确率越高。

6.2.3 带参数的工具

很多工具需要参数。例如,查询天气需要城市名,查询订单需要订单号。工具方法可以定义参数,AI 在调用时会自动提取参数值。

java 复制代码
@Component
public class WeatherTools {

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

6.2.4 参数描述的重要性

为了让 AI 更准确地填充参数,可以在 @Tool 注解中通过 valuearray 提供更详细的参数描述。Spring AI Alibaba 支持从 Javadoc 或 @ToolParam 注解中提取参数说明(如果有)。

java 复制代码
@Tool(name = "getWeather", description = "查询指定城市的天气")
public String getWeather(
        @ToolParam(description = "城市名称,如北京、上海") String city) {
    // ...
}

注意 :目前 Spring AI Alibaba 对 @ToolParam 的支持可能需要特定版本,建议查阅最新文档。如果暂时不支持,可以在工具描述中一并说明参数含义。

6.3 将工具注册到 ChatClient

Spring AI Alibaba 会自动检测所有带有 @Tool 注解的 Spring Bean,并将它们注册到 ToolProvider 中。你只需要在构建 ChatClient 时调用 build() 即可,无需额外配置。

但如果你需要手动指定工具,可以使用 ChatClient.Buildertools() 方法。

6.3.1 默认自动注册

默认情况下,只要你的工具类是一个 Spring Bean(如 @Component@Service 等),Spring AI Alibaba 的自动配置就会将它们收集起来,并在创建 ChatClient 时自动包含。因此,你不需要在代码中显式传递工具。

java 复制代码
@RestController
public class FunctionCallingController {

    private final ChatClient chatClient;

    public FunctionCallingController(ChatClient.Builder builder) {
        this.chatClient = builder.build(); // 自动包含所有 @Tool Bean
    }

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

6.3.2 手动指定工具(可选)

如果你希望某些工具只在特定情况下使用,可以在构建时通过 tools() 指定:

java 复制代码
this.chatClient = builder
        .tools(timeTools, weatherTools)  // 只使用这两个工具
        .build();

或者在每次调用时临时指定:

java 复制代码
chatClient.prompt()
        .user(question)
        .tools(timeTools, weatherTools)  // 本次调用使用的工具
        .call()
        .content();

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

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

6.4.1 完整代码

java 复制代码
package com.example.demo;

import com.alibaba.cloud.ai.tool.annotation.Tool;
import org.springframework.ai.chat.ChatClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
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;

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

// ---------- 工具类 ----------
@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 + "的天气数据暂未收录");
    }
}

// ---------- Controller ----------
@RestController
class AssistantController {

    private final ChatClient chatClient;

    public AssistantController(ChatClient.Builder builder) {
        this.chatClient = builder.build(); // 自动包含 TimeTools 和 WeatherTools
    }

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

6.4.2 测试

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

  • 时间查询http://localhost:8080/assistant?question=现在几点了? 预期回答包含当前时间,如"现在是下午2点30分45秒。"

  • 天气查询http://localhost:8080/assistant?question=上海天气怎么样? 预期回答"上海多云,28℃。"

  • 混合查询http://localhost:8080/assistant?question=帮我查一下北京的天气,顺便告诉我现在几点了 AI 可能会先后调用两个工具,然后综合回答。

  • 无工具可用的问题http://localhost:8080/assistant?question=计算123+456(没有计算器工具),AI 会尝试自己计算或表示无法计算。

6.4.3 观察日志

在控制台,你可能会看到类似以下的日志,表示 AI 调用了工具:

css 复制代码
调用工具: getWeather, 参数: {"city":"上海"}
工具返回: 多云,28℃

6.5 函数调用的注意事项

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

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

6.5.2 工具方法的执行时间

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

6.5.3 错误处理

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

java 复制代码
@Tool(name = "getWeather")
public String getWeather(String city) {
    try {
        // 调用外部 API
        return weatherApi.get(city);
    } catch (Exception e) {
        return "获取天气失败,请稍后重试。";
    }
}

6.5.4 工具数量

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

6.5.5 参数类型

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

6.5.6 通义千问的特殊性

通义千问模型对函数调用的支持与 OpenAI 类似,但细节可能略有不同。Spring AI Alibaba 已经做了适配,开发者无需关心底层差异。

6.6 函数调用流程详解

为了更深入理解,我们来看一下函数调用在 Spring AI Alibaba 中的详细流程:

  1. 用户请求 :用户发送问题 /assistant?question=上海天气怎么样?
  2. 构建请求 :ChatClient 将用户消息和可用工具列表(由 ToolProvider 提供)一起封装成请求,发送给通义千问模型。
  3. 模型决策 :模型判断需要调用 getWeather 工具,并生成参数 {"city": "上海"}
  4. 工具执行 :Spring AI Alibaba 接收到模型返回的工具调用请求,查找对应的 Bean 方法,通过反射调用 WeatherTools.getWeather("上海")
  5. 结果返回:工具执行结果("多云,28℃")被封装成新的消息(工具消息)再次发送给模型。
  6. 生成最终回答:模型结合工具结果和原始问题,生成最终回答:"上海今天多云,28℃。"
  7. 响应客户端:最终回答返回给用户。

流程图:

sequenceDiagram participant 用户 participant ChatClient participant 工具 participant 通义千问模型 用户->>ChatClient: 提问(上海天气) ChatClient->>通义千问模型: 用户消息 + 工具列表 通义千问模型-->>ChatClient: 工具调用请求(getWeather, 上海) ChatClient->>工具: 执行 getWeather(上海) 工具-->>ChatClient: 返回"多云,28℃" ChatClient->>通义千问模型: 工具结果 通义千问模型-->>ChatClient: 最终回答 ChatClient-->>用户: 上海多云,28℃

6.7 本章小结

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

  • 函数调用的概念:让 AI 调用外部方法获取实时信息或执行操作。
  • 定义工具 :使用 @Tool 注解标记方法,并描述其用途。
  • 注册工具 :Spring AI Alibaba 自动收集所有带 @Tool 的 Spring Bean,无需手动配置。
  • 实践:构建了能查询时间和天气的智能助手。
  • 注意事项:线程安全、超时、错误处理、工具数量等。

七、向量存储与 RAG:构建企业知识库

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

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

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

7.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

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

Spring AI 提供了一个统一的 VectorStore 接口,用于抽象各种向量数据库的操作。Spring AI Alibaba 在此基础上提供了更多支持,但底层接口一致。目前支持的实现包括:

  • Milvus:专业的开源向量数据库
  • PGvector:PostgreSQL 的向量插件
  • Redis:通过 Redis Stack 的向量搜索能力
  • Elasticsearch:通过 Elasticsearch 的向量检索功能
  • SimpleVectorStore:内存存储,仅用于测试

在本教程中,我们将使用 SimpleVectorStore 进行演示,因为它无需额外基础设施,方便学习。生产环境建议使用持久化的向量数据库,如 Milvus 或 PGvector。

7.2.1 配置向量存储(以 SimpleVectorStore 为例)

Spring AI Alibaba 的自动配置会根据依赖自动创建 VectorStore 的 Bean。如果只想使用内存存储,无需额外配置。我们只需在 pom.xml 中添加 Spring AI 的向量存储模块(SimpleVectorStore 已包含在核心中,但需要显式引入才能启用?实际检查:spring-ai-alibaba-starter 可能已传递依赖,但为了保险,可以添加):

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

注意:Spring AI Alibaba 的版本可能基于特定 Spring AI 版本,需要保持一致。

application.yml 中,可以配置嵌入模型相关参数(后文详述),向量存储本身无需配置。

7.3 文档处理与嵌入

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

7.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.getCustomMetadata().put("source", "knowledge.txt");
List<Document> documents = textReader.get();

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

7.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 数,避免切在关键位置丢失上下文
  • 其他参数可以保持默认

7.3.3 嵌入客户端(EmbeddingClient)

嵌入模型将文本转换为向量。Spring AI 提供了 EmbeddingClient 接口,通义千问也提供了嵌入模型(如 text-embedding-v1)。Spring AI Alibaba 通过 DashScopeEmbeddingClient 封装了对通义嵌入模型的支持。

application.yml 中配置嵌入客户端:

yaml 复制代码
spring:
  ai:
    dashscope:
      embedding:
        options:
          model: text-embedding-v1   # 通义文本嵌入模型

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

7.4 知识摄入实践

现在我们将以上组件组合起来,完成一个完整的知识摄入流程。我们将创建一个服务,从 knowledge.txt 文件中读取内容,分割后生成向量并存入向量存储。

7.4.1 准备测试文档

src/main/resources 目录下创建 knowledge.txt 文件,内容例如:

复制代码
Spring AI Alibaba 是一个为 Java 开发者设计的 AI 集成框架,由阿里巴巴开源。
它基于 Spring AI 构建,提供了对通义千问系列模型的一等支持。
RAG(检索增强生成)是 Spring AI 的核心功能之一,可以帮助企业构建基于私有知识库的问答系统。
Spring AI Alibaba 的 VectorStore 抽象支持多种向量数据库,包括 Milvus、PGvector、Redis 等。
使用 Spring AI Alibaba,开发者可以像调用普通方法一样使用 AI 能力,极大地简化了开发。

7.4.2 创建摄入服务

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.getCustomMetadata().put("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() + " 个文档片段");
    }
}

7.4.3 在应用启动时摄入知识

我们可以在 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");
    }
}

启动应用,你会看到控制台输出"成功摄入 X 个文档片段",表示知识已存入向量存储。

7.4.4 验证摄入结果

为了确保文档已经成功存入,我们可以编写一个简单的查询测试。在后面的问答接口中,我们会实际使用这些知识。现在,可以临时编写一个测试端点来查看存储中的内容(SimpleVectorStore 支持查看所有文档)。

java 复制代码
@RestController
public class TestController {
    private final VectorStore vectorStore;

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

    @GetMapping("/test-docs")
    public List<Document> testDocs() {
        // 返回所有文档(仅用于测试,SimpleVectorStore 有此方法)
        return ((org.springframework.ai.vectorstore.SimpleVectorStore) vectorStore).getAllDocuments();
    }
}

访问 /test-docs 可以看到存储的文档片段列表。

7.5 检索增强问答实现

知识摄入完成后,我们需要在问答时利用这些知识。Spring AI 提供了 QuestionAnswerAdvisor,它可以自动在每次请求时检索相关文档,并将检索结果注入到提示词中。

7.5.1 创建带有 RAG 的 ChatClient

我们需要构建一个 ChatClient Bean,并为其添加 QuestionAnswerAdvisor。可以在配置类中完成:

java 复制代码
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatClientBuilder;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.rag.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.rag.retrieval.search.DocumentRetriever;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RagConfig {

    @Bean
    public ChatClient ragChatClient(ChatClient.Builder builder, VectorStore vectorStore) {
        // 创建文档检索器
        DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)
                .similarityThreshold(0.5)   // 相似度阈值
                .topK(3)                     // 返回最相似的 3 个文档
                .build();

        // 创建检索顾问
        QuestionAnswerAdvisor advisor = new QuestionAnswerAdvisor(retriever);

        // 构建 ChatClient,并添加顾问
        return builder
                .defaultAdvisors(advisor)
                .build();
    }
}

7.5.2 创建 RAG 控制器

java 复制代码
package com.example.demo.controller;

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

@RestController
public class RagController {

    private final ChatClient ragChatClient;

    public RagController(ChatClient ragChatClient) {
        this.ragChatClient = ragChatClient;
    }

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

7.5.3 测试 RAG 问答

启动应用,访问 /ask?question=什么是RAG?。你会看到 AI 基于 knowledge.txt 中的内容进行回答,例如:

复制代码
RAG(检索增强生成)是 Spring AI 的核心功能之一,可以帮助企业构建基于私有知识库的问答系统。

如果问题超出知识库范围(例如"明天天气怎么样"),AI 可能会说不知道,或者根据自身知识回答(取决于你的配置)。

7.5.4 检索增强流程示意图

sequenceDiagram participant 用户 participant Controller participant ChatClient participant 检索顾问 participant VectorStore participant 大模型 用户->>Controller: GET /ask?question=什么是RAG? Controller->>ChatClient: prompt().user(question).call() ChatClient->>检索顾问: 执行检索增强 检索顾问->>VectorStore: 相似度搜索(question) VectorStore-->>检索顾问: 返回最相关文档片段 检索顾问->>检索顾问: 构建增强提示词(上下文+问题) 检索顾问-->>ChatClient: 返回增强后的Prompt ChatClient->>大模型: 发送增强后的Prompt 大模型-->>ChatClient: 返回AI回答 ChatClient-->>Controller: 返回content() Controller-->>用户: 返回最终答案

7.6 高级 RAG 策略简介

基本的 QuestionAnswerAdvisor 已经能满足许多场景,但在生产环境中,你可能需要更精细的控制来提升检索质量和回答准确性。Spring AI 提供了一系列可插拔的组件,允许你定制 RAG 流程的每个环节。

7.6.1 查询转换(Query Transformation)

用户的问题可能不够精确,或者需要结合对话历史才能理解。查询转换可以在检索前对问题进行改写,以提高检索效果。

Spring AI 提供了 QueryTransformer 接口,常用实现有:

  • CompressingQueryTransformer:结合对话历史压缩查询(例如将"它是什么意思"扩展为完整问题)。
  • ExpandingQueryTransformer:生成多个查询变体,检索后合并结果。
  • TranslationQueryTransformer:将查询翻译成其他语言后再检索(如果文档是多语言的)。

使用示例(需引入相关依赖):

java 复制代码
QueryTransformer transformer = new CompressingQueryTransformer(chatModel);
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
        .vectorStore(vectorStore)
        .queryTransformer(transformer)
        .build();

7.6.2 混合检索(Hybrid Search)

向量检索擅长语义匹配,但关键词检索在某些场景下更精确(如精确匹配产品型号)。混合检索结合了两者,通常能取得更好的效果。

Spring AI 目前没有内置的混合检索器,但你可以通过组合多个检索器来实现。例如,同时使用向量检索和 Elasticsearch 关键词检索,然后合并结果。

7.6.3 重排序(Reranking)

检索出的文档片段按相似度得分排序,但有时最相似的未必最有用。重排序可以使用专门的模型(如 Cross-encoder)对检索结果重新打分,提高相关性。

Spring AI 提供了 DocumentRanker 接口,你可以实现自己的重排序逻辑,或调用第三方服务。

7.6.4 自定义提示词模板

QuestionAnswerAdvisor 使用默认的提示词模板,但你可以通过覆盖其行为来自定义。例如,你希望 AI 在无法回答时明确说"根据现有知识库无法回答"。

可以创建自定义的 Advisor:

java 复制代码
public class CustomQuestionAnswerAdvisor implements ChatMemoryAdvisor {
    // 自定义实现,参考上一章类似代码
}

7.7 本章小结

通过本章的学习,你完成了 RAG 的完整实现:

  • 知识摄入:使用文档加载器、分割器、嵌入客户端,将私有文档存入向量存储。
  • 检索增强 :通过 QuestionAnswerAdvisor 自动检索并注入上下文。
  • 实践:构建了基于私有知识库的问答系统,并测试了效果。
  • 高级策略:了解了查询转换、混合检索、重排序等优化方向。

八、工作流编排:Spring AI Alibaba Graph

在前面的章节中,我们已经能够实现单轮对话、多轮记忆、函数调用、RAG知识库等丰富功能。但是,当业务场景变得复杂------比如需要多步决策、条件分支、循环执行、多Agent协作时,传统的线性调用方式就显得力不从心了。

**工作流编排(Workflow Orchestration)**正是为了解决这类复杂场景而生。Spring AI Alibaba Graph 模块提供了一套基于状态图(StateGraph)的编程模型,让你可以像搭积木一样构建复杂的 AI 工作流和多智能体系统。

8.1 为什么需要工作流编排?

先来看几个典型场景:

  • 场景一:客户评价处理系统 - 需要先判断评价是正面还是负面,如果是负面还要进一步细分问题类型(售后、物流、质量),然后根据不同问题走不同的处理流程。
  • 场景二:代码审计助手 - 需要经历"写代码 → 运行测试 → 读取错误 → 修改代码 → 再运行"的循环,直到代码通过测试。
  • 场景三:多智能体协作 - 需要规划Agent、监督Agent、执行Agent共同完成一个复杂任务。

如果用传统的 if-else 硬编码,很快就会遇到这些问题:

  • 路由逻辑越来越乱,难以维护
  • Agent 越加越多,耦合度越来越高
  • 人工介入(Human-in-the-loop)难以实现
  • 状态管理混乱,调试困难

Spring AI Alibaba Graph 通过引入**状态图(StateGraph)**的概念,将复杂业务拆解为多个清晰节点,用有向图描述执行流程,完美解决了上述问题。

8.2 Graph 核心概念

在深入代码之前,我们需要理解几个核心概念:

8.2.1 概念模型

概念 说明 类比
State(状态) 贯穿整个工作流的共享数据容器,所有节点都从这里读取数据,并向这里写入数据 工作流的"短期记忆"
Node(节点) 工作流中的一个步骤,执行具体的业务逻辑(调用大模型、执行工具、处理数据等) 流程图中的一个方框
Edge(边) 连接节点的线,决定下一步执行哪个节点 流程图中的箭头
StateGraph(状态图) 由节点和边构成的有向图,是工作流的定义 整个流程图

8.2.2 边的类型

  • 普通边(Direct Edge):无条件的跳转,A 执行完后直接到 B。
  • 条件边(Conditional Edge) :根据当前状态的值,动态决定下一步去哪个节点。这是实现循环分支的关键。

8.2.3 状态合并策略(KeyStrategy)

当多个节点更新同一个状态字段时,需要定义合并策略:

  • ReplaceStrategy(覆盖策略):后执行的节点覆盖先执行节点的值。适用于单一值的字段。
  • AppendStrategy(追加策略):将新值追加到旧值后面(通常用于列表)。适用于需要累积信息的字段,如对话历史。

核心概念关系图

graph TD subgraph "StateGraph" direction LR N1[Node A] -- "普通边" --> N2[Node B] N2 -- "条件边" --> N3[Node C] N2 -- "条件边" --> N4[Node D] end State[OverAllState] -.-> N1 State -.-> N2 State -.-> N3 State -.-> N4 N1 -.-> State N2 -.-> State N3 -.-> State N4 -.-> State style State fill:#f9f,stroke:#333,stroke-width:2px

8.3 快速体验:商品评价分类系统

让我们通过一个完整的示例来感受 Spring AI Alibaba Graph 的魅力。这是一个商品评价分类系统,功能如下:

  • 第一级分类:将用户评论分为 positive(正面)和 negative(负面)
  • 第二级分类:如果是负面评论,进一步细分为售后服务、产品质量、物流运输等具体问题
  • 处理与记录:根据不同分类结果执行相应处理,最后记录结论

8.3.1 系统流程图

graph TD START((开始)) --> feedback_classifier[第一级分类
正面/负面] feedback_classifier -- "正面" --> recorder[记录节点] feedback_classifier -- "负面" --> specific_classifier[第二级分类
售后/质量/物流] specific_classifier --> recorder recorder --> END((结束))

8.3.2 添加依赖

首先,在 pom.xml 中添加 Graph 相关依赖:

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-graph-starter</artifactId>
    <version>${spring-ai-alibaba.version}</version>
</dependency>

8.3.3 定义全局状态(OverAllState)

java 复制代码
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.state.AgentStateFactory;
import com.alibaba.cloud.ai.graph.state.AppendStrategy;
import com.alibaba.cloud.ai.graph.state.ReplaceStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

/**
 * 评价处理系统的全局状态
 * 包含三个字段:
 * - input: 用户输入的评价内容
 * - classifier_output: 分类结果
 * - solution: 最终处理方案
 */
public class ReviewState extends OverAllState {
    
    public ReviewState(Map<String, Object> inputs) {
        super(inputs);
        // 注册状态字段及其合并策略
        registerKeyAndStrategy("input", new ReplaceStrategy());
        registerKeyAndStrategy("classifier_output", new ReplaceStrategy());
        registerKeyAndStrategy("solution", new ReplaceStrategy());
    }
    
    // 便捷方法
    public String getInput() {
        return (String) value("input").orElse("");
    }
    
    public String getClassifierOutput() {
        return (String) value("classifier_output").orElse("");
    }
    
    public void setClassifierOutput(String output) {
        update("classifier_output", output);
    }
    
    public String getSolution() {
        return (String) value("solution").orElse("");
    }
    
    public void setSolution(String solution) {
        update("solution", solution);
    }
}

// 状态工厂,用于创建工作流时初始化状态
@Configuration
public class ReviewStateConfig {
    
    @Bean
    public AgentStateFactory<ReviewState> reviewStateFactory() {
        return inputs -> new ReviewState(inputs);
    }
}

8.3.4 创建分类节点(使用预置节点)

Spring AI Alibaba Graph 提供了丰富的预置节点,QuestionClassifierNode 就是专门用于文本分类的节点。

java 复制代码
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.action.AsyncNodeAction;
import com.alibaba.cloud.ai.graph.node.QuestionClassifierNode;
import org.springframework.ai.chat.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ReviewNodesConfig {
    
    /**
     * 第一级分类节点:判断评价是正面还是负面
     */
    @Bean
    public AsyncNodeAction feedbackClassifier(ChatClient chatClient) {
        return QuestionClassifierNode.builder()
                .chatClient(chatClient)
                .name("feedback_classifier")
                .inputKey("input")           // 从状态中读取用户输入
                .outputKey("classifier_output") // 将分类结果写入状态
                .categories(new String[]{"positive", "negative"}) // 分类类别
                .prompt("请判断以下用户评价的情感倾向是正面还是负面:\n{input}") // 提示词模板
                .build();
    }
    
    /**
     * 第二级分类节点:将负面评价细分为具体问题
     */
    @Bean
    public AsyncNodeAction specificQuestionClassifier(ChatClient chatClient) {
        return QuestionClassifierNode.builder()
                .chatClient(chatClient)
                .name("specific_question_classifier")
                .inputKey("input")
                .outputKey("classifier_output")
                .categories(new String[]{"after-sale", "quality", "transportation", "others"})
                .prompt("这是一条负面评价,请判断用户具体投诉的是哪类问题:售后、质量、物流还是其他?\n评价内容:{input}")
                .build();
    }
}

8.3.5 创建自定义记录节点

对于需要自定义逻辑的节点,可以实现 AsyncNodeAction 接口或继承 NodeAction 类。

java 复制代码
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.action.AsyncNodeAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.CompletableFuture;

@Component
public class RecordingNode implements AsyncNodeAction {
    
    private static final Logger log = LoggerFactory.getLogger(RecordingNode.class);
    
    @Override
    public CompletableFuture<OverAllState> apply(OverAllState state) {
        return CompletableFuture.supplyAsync(() -> {
            String input = (String) state.value("input").orElse("");
            String classifierOutput = (String) state.value("classifier_output").orElse("");
            
            log.info("收到评价:{}", input);
            log.info("分类结果:{}", classifierOutput);
            
            // 根据分类结果生成解决方案
            String solution;
            if (classifierOutput.contains("positive")) {
                solution = "感谢您的肯定,我们会继续努力!";
            } else {
                solution = switch (classifierOutput) {
                    case "after-sale" -> "已转售后客服处理,将在24小时内联系您。";
                    case "quality" -> "已反馈质量部门,将为您安排退换货。";
                    case "transportation" -> "已联系物流公司,将尽快更新配送信息。";
                    default -> "已记录您的问题,客服会尽快处理。";
                };
            }
            
            // 将解决方案写入状态
            state.update("solution", solution);
            log.info("处理方案:{}", solution);
            
            return state;
        });
    }
}

8.3.6 实现条件边分发器

条件边需要实现 EdgeAction 接口,根据当前状态决定下一步去哪个节点。

java 复制代码
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.action.AsyncEdgeAction;
import org.springframework.stereotype.Component;

import java.util.concurrent.CompletableFuture;

/**
 * 第一级分类后的路由分发器
 * 根据分类结果决定:正面评价直接到记录节点,负面评价到二级分类节点
 */
@Component
public class FeedbackQuestionDispatcher implements AsyncEdgeAction {
    
    @Override
    public CompletableFuture<String> apply(OverAllState state) {
        return CompletableFuture.supplyAsync(() -> {
            String classifierOutput = (String) state.value("classifier_output").orElse("");
            
            if (classifierOutput.contains("positive")) {
                return "positive";
            } else {
                return "negative";
            }
        });
    }
}

/**
 * 第二级分类后的路由分发器
 * 无论细分类别是什么,都汇聚到记录节点
 */
@Component
public class SpecificQuestionDispatcher implements AsyncEdgeAction {
    
    @Override
    public CompletableFuture<String> apply(OverAllState state) {
        return CompletableFuture.supplyAsync(() -> {
            String classifierOutput = (String) state.value("classifier_output").orElse("");
            
            // 根据分类结果映射到不同的路由值
            if (classifierOutput.contains("after-sale")) {
                return "after-sale";
            } else if (classifierOutput.contains("transportation")) {
                return "transportation";
            } else if (classifierOutput.contains("quality")) {
                return "quality";
            } else {
                return "others";
            }
        });
    }
}

8.3.7 构建 StateGraph

现在,我们把所有组件组装起来,构建完整的 StateGraph

java 复制代码
import com.alibaba.cloud.ai.graph.StateGraph;
import com.alibaba.cloud.ai.graph.action.AsyncNodeAction;
import com.alibaba.cloud.ai.graph.action.AsyncEdgeAction;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

import static com.alibaba.cloud.ai.graph.StateGraph.END;
import static com.alibaba.cloud.ai.graph.StateGraph.START;
import static com.alibaba.cloud.ai.graph.node.NodeAsyncWrapper.node_async;
import static com.alibaba.cloud.ai.graph.edge.EdgeWrapper.edge_async;

@Configuration
public class ReviewWorkflowConfig {
    
    @Bean
    public StateGraph reviewStateGraph(
            AgentStateFactory<ReviewState> stateFactory,
            AsyncNodeAction feedbackClassifier,
            AsyncNodeAction specificQuestionClassifier,
            RecordingNode recorder,
            FeedbackQuestionDispatcher feedbackDispatcher,
            SpecificQuestionDispatcher specificDispatcher) {
        
        // 创建状态图,指定名称和状态工厂
        StateGraph graph = new StateGraph("评价处理工作流", stateFactory);
        
        // 1. 注册节点(使用 node_async 包装为异步节点)
        graph.addNode("feedback_classifier", node_async(feedbackClassifier));
        graph.addNode("specific_question_classifier", node_async(specificQuestionClassifier));
        graph.addNode("recorder", node_async(recorder));
        
        // 2. 设置起始节点
        graph.addEdge(START, "feedback_classifier");
        
        // 3. 添加条件边:feedback_classifier 后的路由
        graph.addConditionalEdges(
                "feedback_classifier",
                edge_async(feedbackDispatcher),
                Map.of(
                        "positive", "recorder",
                        "negative", "specific_question_classifier"
                )
        );
        
        // 4. 添加条件边:specific_question_classifier 后的路由(所有分支都汇聚到 recorder)
        graph.addConditionalEdges(
                "specific_question_classifier",
                edge_async(specificDispatcher),
                Map.of(
                        "after-sale", "recorder",
                        "transportation", "recorder",
                        "quality", "recorder",
                        "others", "recorder"
                )
        );
        
        // 5. 添加结束边
        graph.addEdge("recorder", END);
        
        return graph;
    }
}

8.3.8 创建 Controller 触发工作流

最后,创建一个 REST 接口来触发工作流执行。

java 复制代码
import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.StateGraph;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@RestController
public class ReviewController {
    
    private final StateGraph reviewStateGraph;
    private final ChatClient chatClient;
    
    public ReviewController(StateGraph reviewStateGraph, ChatClient.Builder builder) {
        this.reviewStateGraph = reviewStateGraph;
        this.chatClient = builder.build();
    }
    
    @GetMapping("/review")
    public String processReview(@RequestParam String query) {
        try {
            // 1. 编译状态图(每次调用可以编译,但更高效的方式是缓存 CompiledGraph)
            CompiledGraph compiledGraph = reviewStateGraph.compile();
            
            // 2. 准备初始状态
            Map<String, Object> initialState = new HashMap<>();
            initialState.put("input", query);
            
            // 3. 执行工作流
            Optional<OverAllState> finalState = compiledGraph.invoke(initialState);
            
            // 4. 获取结果
            return finalState
                    .map(state -> (String) state.value("solution").orElse("处理完成"))
                    .orElse("处理失败");
            
        } catch (Exception e) {
            e.printStackTrace();
            return "系统错误:" + e.getMessage();
        }
    }
}

8.3.9 测试运行

启动应用,用浏览器或 curl 测试几个例子:

bash 复制代码
# 测试正面评价
curl "http://localhost:8080/review?query=收到的产品非常棒,质量很好,下次还会购买!"
# 预期返回:感谢您的肯定,我们会继续努力!

# 测试负面评价 - 物流问题
curl "http://localhost:8080/review?query=快递太慢了,等了一周才收到"
# 预期返回:已联系物流公司,将尽快更新配送信息。

# 测试负面评价 - 质量问题
curl "http://localhost:8080/review?query=用了两天就坏了,质量太差了"
# 预期返回:已反馈质量部门,将为您安排退换货。

# 测试负面评价 - 售后问题
curl "http://localhost:8080/review?query=客服态度很差,根本不解决问题"
# 预期返回:已转售后客服处理,将在24小时内联系您。

8.4 ReAct Agent 模式:天气查询系统

除了工作流编排,Spring AI Alibaba Graph 还内置了 ReAct(Reasoning + Acting)Agent 模式,让 Agent 可以在"思考"和"行动"之间循环,直到完成任务。

8.4.1 ReAct Agent 架构

graph LR START((开始)) --> AgentNode[Agent节点
LLM思考] AgentNode -- "需要调用工具" --> ToolNode[工具节点
执行工具] ToolNode --> AgentNode AgentNode -- "直接回答" --> END((结束))

8.4.2 实现天气查询 Agent

java 复制代码
import com.alibaba.cloud.ai.graph.agent.ReActAgent;
import com.alibaba.cloud.ai.graph.tool.ToolCallback;
import org.springframework.ai.chat.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

/**
 * 天气查询工具
 */
@Component
public class WeatherTool {
    
    @Tool(name = "get_weather", description = "查询指定城市的天气")
    public String getWeather(String city) {
        // 模拟天气查询
        Map<String, String> weatherMap = Map.of(
                "北京", "晴,25℃",
                "上海", "多云,28℃",
                "广州", "雷阵雨,30℃"
        );
        return weatherMap.getOrDefault(city, city + "的天气数据暂未收录");
    }
}

@Configuration
public class ReactAgentConfig {
    
    @Bean
    public ReActAgent weatherAgent(ChatClient chatClient, WeatherTool weatherTool) {
        // 将工具转换为 ToolCallback
        List<ToolCallback> tools = List.of(
                ToolCallback.from(weatherTool, "get_weather")
        );
        
        return ReActAgent.builder()
                .name("weather_agent")
                .chatClient(chatClient)
                .tools(tools)                     // 注册工具
                .maxIterations(10)                 // 最大循环次数
                .build();
    }
}

8.4.3 调用 ReAct Agent

java 复制代码
@RestController
public class ReactController {
    
    private final ReActAgent weatherAgent;
    
    public ReactController(ReActAgent weatherAgent) {
        this.weatherAgent = weatherAgent;
    }
    
    @GetMapping("/weather")
    public String queryWeather(@RequestParam String query) {
        Map<String, Object> result = weatherAgent.invoke(Map.of("input", query));
        return (String) result.get("output");
    }
}

测试访问:

  • http://localhost:8080/weather?query=上海今天天气怎么样?
  • http://localhost:8080/weather?query=帮我查一下北京和广州的天气

8.5 Supervisor 多智能体模式:OpenManus 示例

Spring AI Alibaba Graph 还支持更复杂的多智能体协作模式。官方提供了 OpenManus 的 Java 实现,通过 Supervisor Agent 协调 Planning Agent 和 Executor Agent 共同完成任务。

8.5.1 多智能体架构

graph TD User[用户请求] --> Supervisor[Supervisor Agent
监督者] Supervisor --> Planning[Planning Agent
任务规划] Planning --> Supervisor Supervisor --> Executor[Executor Agent
执行者] Executor --> Supervisor Supervisor --> Result[返回结果] Executor --> Tool1[Browser_use
浏览器工具] Executor --> Tool2[FileSaver
文件保存] Executor --> Tool3[PythonExecuter
Python执行]

8.5.2 核心代码示意

java 复制代码
// 创建三个 Agent
ReactAgent planningAgent = ReactAgent.builder()
        .name("planning_agent")
        .prompt("你是一个任务规划专家,将用户需求拆解为详细步骤。")
        .chatClient(chatClient)
        .build();

ReactAgent executorAgent = ReactAgent.builder()
        .name("executor_agent")
        .prompt("你负责执行具体任务,可使用各种工具。")
        .chatClient(chatClient)
        .tools(tools)  // 包含 Browser_use、FileSaver 等工具
        .build();

// 创建 Supervisor Agent,协调规划者和执行者
SupervisorAgent supervisor = SupervisorAgent.builder()
        .name("supervisor")
        .chatClient(chatClient)
        .agents(List.of(planningAgent, executorAgent))
        .build();

// 执行任务
Map<String, Object> result = supervisor.invoke(Map.of("input", "帮我查一下阿里巴巴过去一周的股票信息"));

8.6 Graph 的高级特性

8.6.1 状态持久化与 Checkpoint

Spring AI Alibaba Graph 支持将工作流状态持久化,以便从中断点恢复执行。

java 复制代码
import com.alibaba.cloud.ai.graph.checkpoint.CheckpointSaver;
import com.alibaba.cloud.ai.graph.checkpoint.MemorySaver;

// 创建内存检查点保存器
CheckpointSaver saver = new MemorySaver();

// 编译图时传入 saver
CompiledGraph graph = stateGraph.compile(saver);

// 执行时指定 threadId,用于区分不同会话
RunnableConfig config = RunnableConfig.builder()
        .threadId("user-123-session")
        .build();

Optional<OverAllState> result = graph.invoke(initialState, config);

8.6.2 Human-in-the-Loop(人工介入)

工作流可以在特定节点暂停,等待人工确认或输入,然后恢复执行。

java 复制代码
// 在编译图时设置中断点
CompiledGraph graph = stateGraph.compile(saver, List.of("review_node"));

// 执行到 review_node 后会暂停
Optional<OverAllState> state = graph.invoke(initialState, config);

// 人工审核后,可以继续执行
graph.resume(config, Map.of("approved", true));

8.6.3 可视化导出

Graph 支持导出 PlantUML 或 Mermaid 格式的流程图,方便文档和沟通。

java 复制代码
// 导出 PlantUML
String plantUml = stateGraph.getGraph(PlantUMLGenerator.INSTANCE);
System.out.println(plantUml);

// 导出 Mermaid
String mermaid = stateGraph.getGraph(MermaidGenerator.INSTANCE);

8.7 本章小结

通过本章的学习,你掌握了 Spring AI Alibaba Graph 的核心能力:

  • Graph 核心概念:理解了 State、Node、Edge、StateGraph 等基础组件。
  • 工作流编排:通过商品评价分类示例,学会了构建多步骤、带条件分支的工作流。
  • 预置节点:学会了使用 QuestionClassifierNode 等预置节点快速开发。
  • ReAct Agent:掌握了让 Agent 在思考与行动之间循环的模式。
  • 多智能体协作:了解了 Supervisor 模式如何协调多个 Agent 完成复杂任务。
  • 高级特性:了解了状态持久化、人工介入、可视化导出等企业级功能。
相关推荐
Java编程爱好者1 小时前
小米二面:std::map和std::unordered_map谁更快?别只知道哈希表
后端
ssshooter2 小时前
Tauri 踩坑 appLink 修改后闪退
前端·ios·rust
刮涂层_赢大奖2 小时前
我把 AI 编程 Agent 变成了宝可梦,让它们在像素风办公室里跑来跑去
前端·typescript·claude
重庆穿山甲3 小时前
Java开发者的大模型入门:Spring AI组件全攻略(二)
前端·后端
重庆穿山甲3 小时前
Java开发者的大模型入门:Spring AI组件全攻略(一)
前端·后端
布列瑟农的星空3 小时前
前端都能看懂的rust入门教程(二)——函数和闭包
前端·后端·rust
颜酱4 小时前
二叉树分解问题思路解题模式
javascript·后端·算法
晨米酱4 小时前
四、Prettier 编辑器集成指南
前端·代码规范
zone77394 小时前
001:LangChain的LCEL语法学习
人工智能·后端·面试