一、开篇:为什么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后,
ChatClient、ImageModel等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组件全景图
图释:应用层通过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 快速创建项目(推荐)
- 访问 start.spring.io/
- 选择以下选项:
- Project:Maven 或 Gradle(以 Maven 为例)
- Language:Java
- Spring Boot:选择 3.2.x 或更高版本(建议 3.2.0+)
- Group :
com.example - Artifact :
spring-ai-alibaba-demo - Dependencies :添加 Spring Web
- 点击 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 开通阿里云百炼服务
- 访问 阿里云百炼平台(需登录阿里云账号)。
- 如果首次使用,点击"立即开通",同意服务协议。
- 开通后,在控制台左侧导航栏选择 API-KEY 管理。
2.3.2 创建 API Key
- 点击"创建 API-KEY",输入名称(如"我的AI应用")。
- 生成后,复制密钥字符串(格式如
sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)。 - 注意:请妥善保管 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 运行测试
-
启动应用(运行
DemoApplication的main方法)。 -
打开浏览器或使用 curl 访问:
http://localhost:8080/chat?message=你好 -
你将看到类似如下的 JSON 格式响应(纯文本):
你好!我是通义千问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 | 检查网络,或配置代理(如有需要) |
| 模型返回内容不符合预期 | 模型选择或参数问题 | 可在配置中调整 model 和 temperature,或在调用时动态指定参数(后文介绍) |
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 流式响应流程示意图
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 核心 API :
prompt()、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 文生图流程示意图
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 图像理解流程示意图
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 对一段文本进行情感分析,返回
POSITIVE、NEUTRAL、NEGATIVE这样的枚举值。
如果 AI 只返回文本,你就需要编写复杂的正则表达式或依赖大模型生成 JSON 然后手动解析。这不仅增加了代码复杂度,而且容易因模型输出格式变化而失效。
结构化输出正是为了解决这些问题。Spring AI 通过在请求中隐式地要求模型以 JSON 格式返回数据,并自动将 JSON 反序列化为你指定的 Java 类型,让你可以像调用普通方法一样获得类型安全的对象。
5.2 使用 entity() 方法映射 POJO
ChatClient 提供了 entity(Class<T> type) 方法,它接受一个 Java 类型,并返回该类型的实例。框架会在背后完成以下工作:
- 在发送给模型的提示词中,添加指令要求模型以 JSON 格式返回,并符合指定的 Java 类型结构。
- 调用模型获取响应。
- 将响应的 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 基础类型和包装类
可以直接返回 Integer、Boolean、String 等。例如,让 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 再根据结果生成最终回答。
工作流程示意图:
在整个流程中,除了定义工具方法外,你几乎不需要额外代码。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 注解中通过 value 和 array 提供更详细的参数描述。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.Builder 的 tools() 方法。
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 中的详细流程:
- 用户请求 :用户发送问题
/assistant?question=上海天气怎么样? - 构建请求 :ChatClient 将用户消息和可用工具列表(由
ToolProvider提供)一起封装成请求,发送给通义千问模型。 - 模型决策 :模型判断需要调用
getWeather工具,并生成参数{"city": "上海"}。 - 工具执行 :Spring AI Alibaba 接收到模型返回的工具调用请求,查找对应的 Bean 方法,通过反射调用
WeatherTools.getWeather("上海")。 - 结果返回:工具执行结果("多云,28℃")被封装成新的消息(工具消息)再次发送给模型。
- 生成最终回答:模型结合工具结果和原始问题,生成最终回答:"上海今天多云,28℃。"
- 响应客户端:最终回答返回给用户。
流程图:
6.7 本章小结
通过本章的学习,你掌握了:
- 函数调用的概念:让 AI 调用外部方法获取实时信息或执行操作。
- 定义工具 :使用
@Tool注解标记方法,并描述其用途。 - 注册工具 :Spring AI Alibaba 自动收集所有带
@Tool的 Spring Bean,无需手动配置。 - 实践:构建了能查询时间和天气的智能助手。
- 注意事项:线程安全、超时、错误处理、工具数量等。
七、向量存储与 RAG:构建企业知识库
在前面的章节中,我们已经能够与 AI 进行流畅的对话,甚至让 AI 调用外部工具获取实时信息。但是,当用户问到公司内部的规章制度、产品文档、技术规范等私有知识时,AI 就无能为力了------因为它从未见过这些资料。
RAG(Retrieval-Augmented Generation,检索增强生成) 正是为了解决这个问题而生。它允许 AI 在回答问题时,先从你的私有知识库中检索相关文档片段,然后基于这些片段生成答案,从而将模型的知识边界扩展到你的企业内部资料。
本章将带你一步步构建一个基于私有知识库的问答系统,涵盖从文档加载、分割、向量化到存储的完整知识摄入流程,以及检索增强的问答实现。
7.1 RAG 核心概念
RAG 的标准流程分为两大阶段:
- 知识摄入(Ingestion):将原始文档(PDF、TXT、Word 等)处理成可供检索的格式,并存入向量数据库。
- 问答检索(Retrieval & Generation):用户提问时,先从向量数据库中检索相关文档片段,然后将这些片段作为上下文与问题一起发送给大模型,生成最终答案。
为什么需要 RAG?
- 知识时效性:大模型的知识截止日期之后的信息,它不知道。
- 私有知识:公司内部文档、产品手册等,模型从未见过。
- 减少幻觉:基于检索到的真实文档生成答案,大大降低编造的可能。
- 可解释性:可以引用来源,增强用户信任。
RAG 整体流程示意图:
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 检索增强流程示意图
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(追加策略):将新值追加到旧值后面(通常用于列表)。适用于需要累积信息的字段,如对话历史。
核心概念关系图:
8.3 快速体验:商品评价分类系统
让我们通过一个完整的示例来感受 Spring AI Alibaba Graph 的魅力。这是一个商品评价分类系统,功能如下:
- 第一级分类:将用户评论分为 positive(正面)和 negative(负面)
- 第二级分类:如果是负面评论,进一步细分为售后服务、产品质量、物流运输等具体问题
- 处理与记录:根据不同分类结果执行相应处理,最后记录结论
8.3.1 系统流程图
正面/负面] 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 架构
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 多智能体架构
监督者] 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 完成复杂任务。
- 高级特性:了解了状态持久化、人工介入、可视化导出等企业级功能。