一、开篇:为什么Java开发者需要AgentScope Java
1.1 大模型时代,Java开发者的新机遇
过去两年,大语言模型(LLM)技术飞速发展,从ChatGPT到各种专业领域模型,AI能力正以前所未有的速度重塑软件开发。作为企业级后端的中流砥柱,Java开发者自然需要思考:如何将AI能力无缝集成到现有的Java系统中?
然而,传统的AI集成方式往往面临以下挑战:
- 调用复杂:直接调用大模型API需要手动构建HTTP请求、处理JSON、管理密钥,代码冗长且易出错。
- 缺乏智能体抽象:简单的"请求-响应"模式无法满足多步推理、工具调用、多智能体协作等复杂需求。
- 与Java生态割裂:Python生态有丰富的AI框架(如LangChain),但Java开发者需要一套能融入Spring生态的解决方案。
AgentScope Java的出现,正是为了解决这些问题。
1.2 AgentScope Java是什么?
AgentScope Java是阿里巴巴开源的智能体(Agent)开发框架,专为Java生态打造。它提供了一套完整的工具和抽象,让Java开发者能够:
- 用最熟悉的Java代码构建智能体应用
- 轻松集成通义千问、OpenAI等大模型
- 让智能体调用外部工具(如查询天气、操作数据库)
- 编排多智能体协作完成复杂任务
- 在生产环境可靠部署,具备可观测性和沙箱安全
简单来说,AgentScope Java = Java + 大模型 + 智能体 + 工具,让Java开发者像写普通业务代码一样开发AI应用。
1.3 原始调用方式 vs AgentScope Java:一个直观对比
让我们先看一段最原始的Java代码,它使用HttpClient调用大模型API:
java
// 原始方式:直接调用大模型API
HttpClient client = HttpClient.newHttpClient();
String requestBody = """
{
"model": "qwen-plus",
"messages": [{"role": "user", "content": "你好"}]
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// 手动解析JSON
JSONObject json = new JSONObject(response.body());
String answer = json.getJSONObject("output").getString("text");
System.out.println(answer);
} catch (Exception e) {
e.printStackTrace();
}
这段代码的问题显而易见:
- 硬编码API地址和密钥
- JSON构造和解析繁琐
- 没有错误处理、重试机制
- 难以扩展(如增加多轮对话、工具调用)
现在,看看AgentScope Java如何简化这一切:
java
// 1. 配置LLM(以通义千问为例)
ChatClient llm = new DashScopeChatClient.Builder()
.apiKey("your-api-key")
.model("qwen-plus")
.build();
// 2. 创建一个简单的智能体
BaseAgent agent = new BaseAgent("assistant", llm);
// 3. 运行智能体
String response = agent.run("你好");
System.out.println(response);
短短几行代码,实现了相同功能,且无需处理任何HTTP细节。这还只是开始------AgentScope Java的真正威力在于智能体的自主决策、工具调用和多智能体协作。
1.4 AgentScope Java的核心优势
| 优势 | 说明 |
|---|---|
| Java原生 | 完美融入Spring Boot、Maven/Gradle生态,用熟悉的Java语法开发AI应用。 |
| 统一模型抽象 | 支持通义千问、OpenAI、Ollama、Azure等,切换模型只需修改一行配置。 |
| 智能体即代码 | 通过@Tool注解将普通Java方法变为智能体的"手",让AI调用你的业务逻辑。 |
| 多智能体协作 | 内置消息总线(MessageHub),轻松编排多个智能体协同工作。 |
| 生产级特性 | 提供可观测性(Studio)、安全沙箱(Runtime)、记忆持久化,满足企业需求。 |
| 开放架构 | 可扩展自定义Agent、记忆存储、工具集,灵活适应各种场景。 |
下图展示了AgentScope Java的核心组件及其关系:
1.5 本文目标:零基础全组件实战
如果你是一名Java开发者,从未接触过大模型开发,不用担心。本文将带你从零开始,一步一步亲手实践AgentScope Java的所有核心组件。你将学到:
- 基础篇:搭建第一个智能体、配置模型、处理对话
- 工具篇:让智能体调用你的Java方法(函数调用)
- 多智能体篇:构建多个智能体协作完成复杂任务
- 记忆篇:让智能体记住对话历史和用户偏好
- ReAct篇:实现思考-行动-观察循环,应对复杂问题
- 工作流篇:编排结构化多Agent流程
- 部署篇:将智能体发布为服务,安全运行
- 监控篇:使用Studio可视化调试和评估
- 高级篇:多模态、长期记忆(ReMe)等前沿特性
二、环境准备:5分钟搭建第一个AgentScope Java应用
在开始动手之前,我们先确保你的开发环境就绪。本章将带领你完成从零到第一个可运行智能体应用的全过程,全程只需5分钟。
2.1 开发环境要求
- JDK 17 或更高版本(AgentScope Java 基于 Spring Boot 3.x,要求 JDK 17+)
- Maven (3.6+)或 Gradle(7.x+)------ 任选一种你熟悉的构建工具
- IDE:IntelliJ IDEA、Eclipse 或 VS Code(Java插件)
- 网络连接:能够访问阿里云DashScope API(国内网络一般没问题)
如果你还没有安装 JDK 或 Maven/Gradle,请先自行安装。这里不再赘述。
2.2 在项目中引入 AgentScope Java 依赖
AgentScope Java 提供了 Spring Boot Starter,可以轻松集成到你的 Spring Boot 应用中。我们将创建一个标准的 Spring Boot 项目,并添加必要的依赖。
2.2.1 使用 Spring Initializr 快速创建项目(推荐)
- 访问 start.spring.io/
- 选择以下选项:
- Project:Maven 或 Gradle(以 Maven 为例)
- Language:Java
- Spring Boot:选择 3.2.x 或更高版本(3.1+ 也可以,但建议 3.2)
- Group :
com.example - Artifact :
agentscope-demo - Dependencies :添加 Spring Web(因为我们后面可能需要提供 REST 接口)
- 点击 Generate 下载项目压缩包,解压后导入 IDE。
2.2.2 手动添加 AgentScope Java 依赖
由于 AgentScope Java 尚未发布到 Maven Central,需要添加阿里云仓库和官方仓库。在 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>agentscope-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<agentscope.version>0.1.0</agentscope.version> <!-- 使用最新版本,可从官网获取 -->
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AgentScope Java Starter -->
<dependency>
<groupId>com.alibaba.agentscope</groupId>
<artifactId>agentscope-spring-boot-starter</artifactId>
<version>${agentscope.version}</version>
</dependency>
</dependencies>
<!-- 添加仓库:阿里云仓库和 AgentScope 官方仓库 -->
<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>agentscope</id>
<name>AgentScope Repository</name>
<url>https://agentscope.io/maven/releases</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
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://agentscope.io/maven/releases' }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.alibaba.agentscope:agentscope-spring-boot-starter:0.1.0'
}
2.3 获取大模型 API 密钥
AgentScope Java 需要调用大模型 API 来驱动智能体。本教程以阿里云通义千问(DashScope)为例,你也可以使用 OpenAI、Ollama 等,只需更换依赖和配置。
2.3.1 开通阿里云 DashScope 服务
- 访问 阿里云百炼平台(需登录阿里云账号)。
- 如果首次使用,点击"立即开通",同意服务协议。
- 开通后,在控制台左侧导航栏选择 API-KEY 管理。
2.3.2 创建 API Key
- 点击"创建 API-KEY",输入名称(如"我的智能体应用")。
- 生成后,复制密钥字符串(格式如
sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)。 - 注意:请妥善保管 API Key,不要泄露。
2.3.3 配置 API Key
在 src/main/resources/application.yml 中配置:
yaml
agentscope:
llm:
provider: dashscope # 使用通义千问
api-key: ${DASHSCOPE_API_KEY} # 从环境变量读取,或直接写字符串(不推荐)
model: qwen-plus # 默认模型
为了安全,强烈建议使用环境变量:
- Linux/Mac :在
~/.bashrc或~/.zshrc中添加export DASHSCOPE_API_KEY=sk-xxx,然后执行source ~/.bashrc。 - Windows :在系统环境变量中添加
DASHSCOPE_API_KEY=sk-xxx。
如果你只是快速测试,可以临时在配置文件中写死,但切记不要提交到代码仓库。
2.3.4 可选:配置其他模型参数
yaml
agentscope:
llm:
provider: dashscope
api-key: ${DASHSCOPE_API_KEY}
model: qwen-plus
options:
temperature: 0.8 # 温度,控制随机性
max-tokens: 2048 # 最大生成 Token 数
2.4 编写第一个程序:Hello Agent!
现在我们来创建一个最简单的智能体,让它回答你的问题。
2.4.1 创建智能体类
在 com.example.demo 包下创建 HelloAgent.java:
java
package com.example.demo;
import com.alibaba.agentscope.agent.BaseAgent;
import com.alibaba.agentscope.llm.ChatClient;
import org.springframework.stereotype.Component;
@Component // 将智能体声明为 Spring Bean,方便后续使用
public class HelloAgent extends BaseAgent {
public HelloAgent(ChatClient chatClient) {
// 调用父类构造器,传入智能体名称和 LLM 客户端
super("hello_agent", chatClient);
}
// 可选:重写 getSystemPrompt 方法,为智能体设定角色
@Override
protected String getSystemPrompt() {
return "你是一个友好的助手,用中文回答用户的问题。";
}
}
代码解释:
BaseAgent是 AgentScope 提供的基类,我们通过继承它来创建自己的智能体。- 构造器接收
ChatClient(由自动配置创建),并传给父类。 getSystemPrompt()方法可选,用于设定系统消息,告诉智能体它的角色。
2.4.2 创建 REST 接口调用智能体
创建 AgentController.java:
java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AgentController {
private final HelloAgent helloAgent;
public AgentController(HelloAgent helloAgent) {
this.helloAgent = helloAgent;
}
@GetMapping("/chat")
public String chat(@RequestParam(defaultValue = "你好") String message) {
// 调用智能体的 run 方法,传入用户消息,获取回复
return helloAgent.run(message);
}
}
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=你好 -
你将看到类似如下的响应:
你好!我是你的智能助手,有什么可以帮你的吗?
完整流程示意图:
2.4.5 可能遇到的问题及解决
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
启动失败,ChatClient 未找到 |
依赖未正确引入,或自动配置未生效 | 检查 pom.xml 是否包含 agentscope-spring-boot-starter,并确保仓库配置正确 |
| 调用时报错 401 Unauthorized | API Key 未配置或错误 | 检查 application.yml 中的 api-key 是否正确,或环境变量是否设置 |
| 响应超时或网络错误 | 无法访问 DashScope API | 检查网络,或配置代理(如有需要) |
2.5 本章小结
通过本章的学习,你成功搭建了第一个 AgentScope Java 应用,并实现了一个简单的智能体聊天接口。你学会了:
- 使用 Spring Initializr 创建 Spring Boot 项目,并手动添加 AgentScope 依赖。
- 在阿里云百炼平台获取 API Key,并配置到项目中。
- 编写自定义智能体类,继承
BaseAgent并设定系统消息。 - 创建 REST 控制器,注入智能体并调用
run方法。 - 运行并测试第一个智能体接口。
三、核心概念:Agent、LLM与消息
在上一章中,我们创建了一个简单的智能体并让它回答了问题。但你可能会好奇:这个智能体内部是如何工作的?它如何理解我的问题?又如何与大模型交互?本章将深入AgentScope Java的三个核心抽象:Agent(智能体) 、LLM(大语言模型)和Message(消息)。掌握了这些,你就能像搭积木一样构建各种复杂的智能体应用。
3.1 AgentScope Java的核心抽象
3.1.1 Agent接口:所有智能体的根接口
在AgentScope Java中,所有的智能体都实现了 Agent 接口。这个接口定义了智能体的基本行为:
java
public interface Agent {
/**
* 运行智能体,处理输入并返回输出
* @param input 输入字符串(通常是用户消息)
* @return 智能体的回复
*/
String run(String input);
/**
* 获取智能体的名称
*/
String getName();
}
我们之前使用的 BaseAgent 就是这个接口的一个实现类,它封装了与大模型交互的通用逻辑。你可以通过继承 BaseAgent 快速创建自己的智能体,就像我们在第二章做的那样。
Agent的内部工作流程(简化版):
- 接收用户输入(
run方法被调用) - 将输入和系统提示词(如果有)组合成消息列表
- 调用大模型(LLM)获取回复
- 返回回复文本
3.1.2 LLM基座:ChatClient抽象
ChatClient 是AgentScope Java中所有大模型客户端的统一接口。它的作用是屏蔽不同模型提供商的API差异,让你可以用相同的方式调用通义千问、OpenAI、Ollama等模型。
java
public interface ChatClient {
/**
* 发送消息列表给大模型,返回生成的回复
* @param messages 消息列表(包含系统消息、用户消息、助手消息等)
* @return 模型的回复文本
*/
String chat(List<ChatMessage> messages);
/**
* 流式调用(可选)
*/
// 其他方法...
}
AgentScope Java内置了多种实现:
DashScopeChatClient:调用阿里云通义千问OpenAiChatClient:调用OpenAI(包括兼容接口的服务,如DeepSeek)OllamaChatClient:调用本地Ollama模型AzureOpenAiChatClient:调用Azure OpenAI
你可以通过配置文件或代码选择使用哪一个,而你的智能体代码完全不需要修改------这就是抽象的力量。
3.1.3 Message:智能体间通信的标准格式
在多智能体系统中,智能体之间需要交换信息。Message 类就是用来封装这些信息的标准格式。即使是单智能体,内部也是用消息列表与大模型交互的。
一个 Message 包含以下关键属性:
| 属性 | 类型 | 说明 |
|---|---|---|
role |
String |
消息角色:system(系统)、user(用户)、assistant(助手)、tool(工具) |
content |
String |
消息内容 |
name |
String |
可选,发送者的名称 |
例如,一个典型的对话历史可以表示为消息列表:
java
List<ChatMessage> messages = Arrays.asList(
new SystemMessage("你是一个友好的助手。"),
new UserMessage("你好"),
new AssistantMessage("你好,有什么可以帮你的?"),
new UserMessage("今天天气怎么样?")
);
当智能体调用大模型时,它会将这个列表发送给 ChatClient,模型根据整个对话历史生成回复。
三种核心消息的类图:
3.2 配置LLM客户端
在第二章中,我们通过 application.yml 配置了LLM客户端。AgentScope的自动配置会根据 agentscope.llm.provider 的值创建对应的 ChatClient Bean。让我们更详细地了解各种配置方式。
3.2.1 通过配置文件(推荐)
在 application.yml 中,你可以配置多种模型提供商:
通义千问(DashScope):
yaml
agentscope:
llm:
provider: dashscope
api-key: ${DASHSCOPE_API_KEY}
model: qwen-plus
options:
temperature: 0.8
max-tokens: 2048
top-p: 0.9
OpenAI:
yaml
agentscope:
llm:
provider: openai
api-key: ${OPENAI_API_KEY}
model: gpt-3.5-turbo
base-url: https://api.openai.com # 可选,默认就是官方地址
options:
temperature: 0.7
Ollama(本地模型):
yaml
agentscope:
llm:
provider: ollama
base-url: http://localhost:11434
model: llama3
options:
temperature: 0.8
3.2.2 在代码中手动创建LLM实例
如果你不想使用自动配置,也可以手动创建 ChatClient 实例。这在需要动态切换模型或进行单元测试时很有用。
java
import com.alibaba.agentscope.llm.ChatClient;
import com.alibaba.agentscope.llm.dashscope.DashScopeChatClient;
// 创建通义千问客户端
ChatClient dashScopeClient = new DashScopeChatClient.Builder()
.apiKey("your-api-key")
.model("qwen-plus")
.temperature(0.8)
.maxTokens(2048)
.build();
// 创建OpenAI客户端(需要引入对应依赖)
ChatClient openAiClient = new OpenAiChatClient.Builder()
.apiKey("your-openai-key")
.model("gpt-3.5-turbo")
.build();
3.2.3 高级参数说明
| 参数 | 类型 | 说明 | 建议值 |
|---|---|---|---|
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 |
stop |
List | 停止词,当模型生成到这些词时停止 | 可选 |
3.3 第一个自定义Agent:深入BaseAgent
在第二章中,我们简单地继承了 BaseAgent 并重写了 getSystemPrompt。现在,让我们更深入地了解 BaseAgent 的工作原理,并学习如何自定义它的行为。
3.3.1 BaseAgent的核心方法
BaseAgent 类提供了几个可以重写的方法,让你可以定制智能体的行为:
| 方法 | 作用 | 默认行为 |
|---|---|---|
getSystemPrompt() |
返回系统提示词 | 返回空字符串 |
preprocess(String input) |
预处理用户输入 | 直接返回输入 |
postprocess(String output) |
后处理模型输出 | 直接返回输出 |
buildMessages(String input) |
构建发送给模型的消息列表 | 组合系统提示和用户输入 |
最重要的方法是 run,它内部调用了上述方法:
java
public String run(String input) {
// 1. 预处理输入
String processedInput = preprocess(input);
// 2. 构建消息列表
List<ChatMessage> messages = buildMessages(processedInput);
// 3. 调用LLM
String response = chatClient.chat(messages);
// 4. 后处理输出
return postprocess(response);
}
3.3.2 实现自定义Agent:记录对话历史
默认的 BaseAgent 是无状态的------它不记得之前的对话。下面我们创建一个能记住对话历史的智能体。这需要用到 Memory 的概念,我们将在第六章深入,这里先简单演示。
java
package com.example.demo;
import com.alibaba.agentscope.agent.BaseAgent;
import com.alibaba.agentscope.llm.ChatClient;
import com.alibaba.agentscope.message.ChatMessage;
import com.alibaba.agentscope.message.UserMessage;
import com.alibaba.agentscope.message.AssistantMessage;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class MemoryAgent extends BaseAgent {
private final List<ChatMessage> history = new ArrayList<>();
public MemoryAgent(ChatClient chatClient) {
super("memory_agent", chatClient);
}
@Override
protected String getSystemPrompt() {
return "你是一个有记忆的助手,会记住用户之前说过的话。";
}
@Override
protected List<ChatMessage> buildMessages(String input) {
// 将用户输入添加到历史记录
history.add(new UserMessage(input));
// 构建消息列表:系统消息 + 历史记录
List<ChatMessage> messages = new ArrayList<>();
messages.add(new SystemMessage(getSystemPrompt()));
messages.addAll(history); // 包含之前的对话
return messages;
}
@Override
protected String postprocess(String output) {
// 将AI回复也加入历史记录
history.add(new AssistantMessage(output));
return output;
}
}
现在,当我们连续调用这个智能体时,它会记住之前的对话内容。
3.3.3 测试记忆Agent
创建一个新的控制器方法:
java
@RestController
public class AgentController {
@Autowired
private MemoryAgent memoryAgent;
@GetMapping("/memory-chat")
public String memoryChat(@RequestParam String message) {
return memoryAgent.run(message);
}
}
测试两轮对话:
- 访问
/memory-chat?message=我叫小明 - 再访问
/memory-chat?message=我叫什么名字?- 如果智能体回答"你叫小明",说明记忆功能生效了。
3.4 动手实践:构建一个简单的问答助手
现在,让我们综合运用本章所学,构建一个更实用的问答助手。这个助手将:
- 使用通义千问
qwen-plus模型 - 具备基本的角色设定(Java编程导师)
- 能够记录对话历史(可选)
3.4.1 创建智能体类
java
package com.example.demo;
import com.alibaba.agentscope.agent.BaseAgent;
import com.alibaba.agentscope.llm.ChatClient;
import org.springframework.stereotype.Component;
@Component
public class JavaTutorAgent extends BaseAgent {
public JavaTutorAgent(ChatClient chatClient) {
super("java_tutor", chatClient);
}
@Override
protected String getSystemPrompt() {
return "你是一位经验丰富的Java编程导师,用通俗易懂的语言解释Java概念,并给出代码示例。";
}
}
3.4.2 创建控制器
java
@RestController
@RequestMapping("/tutor")
public class TutorController {
private final JavaTutorAgent tutorAgent;
public TutorController(JavaTutorAgent tutorAgent) {
this.tutorAgent = tutorAgent;
}
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return tutorAgent.run(question);
}
}
3.4.3 测试
启动应用,访问以下URL测试:
http://localhost:8080/tutor/ask?question=什么是多态?http://localhost:8080/tutor/ask?question=请解释一下Java的泛型
你会看到智能体以导师的口吻回答,并附带代码示例。
3.4.4 完整调用流程图
3.5 本章小结
通过本章的学习,你掌握了AgentScope Java的三个核心抽象:
- Agent :智能体的基本单元,通过继承
BaseAgent可以快速创建自定义智能体。 - LLM :通过
ChatClient统一接口,轻松切换不同的模型提供商。 - Message:标准化的消息格式,用于与LLM交互和多智能体通信。
你还学会了:
- 通过配置文件或代码创建LLM客户端。
- 重写
BaseAgent的方法来自定义智能体行为。 - 构建一个带简单记忆的智能体。
- 实践了一个Java导师问答助手。
四、工具调用:让智能体拥有行动能力
在前面的章节中,我们的智能体只能"说话"------基于训练数据回答问题。但如果用户问"现在几点了?"、"帮我查一下订单状态"、"今天天气怎么样",纯文本模型是无法直接获取这些实时信息的------它没有时钟,也无法访问你的数据库。
工具调用(Tool Calling) 正是为了解决这个问题而生。它允许智能体在需要时调用你编写的 Java 方法,获取实时数据或执行操作,然后将结果整合到回答中。本章将带你掌握 AgentScope Java 的工具调用机制,让你的智能体真正"动手做事"。
4.1 什么是工具调用?智能体如何调用外部方法?
工具调用的核心思想是:你提供一组工具(Java 方法),并告诉智能体这些工具的存在、用途以及参数。当智能体认为需要某个工具来回答问题时,它会返回一个特殊的请求,要求执行该工具并提供参数。AgentScope 会自动执行对应方法,并将结果返回给智能体,智能体再根据结果生成最终回答。
工作流程示意图:
在整个流程中,除了定义工具方法外,你几乎不需要额外代码。AgentScope 会自动处理工具调用的握手过程。
4.2 在 AgentScope Java 中定义工具
AgentScope Java 通过 @Tool 注解来标记一个方法作为可被智能体调用的工具。你只需将工具类注册为 Spring Bean,框架会自动收集并提供给智能体。
4.2.1 引入依赖
工具调用功能已包含在 agentscope-spring-boot-starter 中,无需额外依赖。
4.2.2 定义第一个工具
创建一个 Spring 组件,其中的方法用 @Tool 注解标记。@Tool 注解需要指定工具的名称和描述,描述会被传递给模型,帮助模型理解何时调用该工具。
java
import com.alibaba.agentscope.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:工具描述,非常重要!智能体会根据描述判断何时调用该工具。描述越清晰,调用准确率越高。
4.2.3 带参数的工具
很多工具需要参数。例如,查询天气需要城市名,查询订单需要订单号。工具方法可以定义参数,智能体在调用时会自动提取参数值。
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 + "的天气数据暂未收录";
};
}
}
4.2.4 参数描述的重要性
为了让智能体更准确地填充参数,可以在 @Tool 注解中通过 params 属性提供更详细的参数描述。AgentScope Java 支持从方法参数名和 @Param 注解中提取说明。
java
import com.alibaba.agentscope.tool.annotation.Param;
@Tool(name = "getWeather", description = "查询指定城市的天气")
public String getWeather(
@Param(description = "城市名称,例如北京、上海") String city) {
// ...
}
注意 :@Param 注解的 description 会被传递给模型,帮助模型理解参数的含义。
4.3 将工具绑定到智能体
在 AgentScope 中,有两种方式将工具绑定到智能体:自动绑定 (通过 Spring 容器)和手动绑定(在创建智能体时指定)。
4.3.1 自动绑定(推荐)
只要你的工具类是一个 Spring Bean(如 @Component、@Service 等),并且你的智能体继承自 BaseAgent,AgentScope 的自动配置就会将这些工具收集起来,并在智能体需要时自动提供。你不需要在代码中显式绑定。
java
@Component
public class MyAgent extends BaseAgent {
public MyAgent(ChatClient chatClient) {
super("my_agent", chatClient);
// 不需要手动注册工具,框架会自动发现所有带 @Tool 的 Bean
}
@Override
protected String getSystemPrompt() {
return "你是一个助手,可以使用工具查询实时信息。";
}
}
4.3.2 手动绑定
如果你希望某些工具只在特定智能体中使用,可以在创建智能体时通过 ToolManager 手动注册。这需要你自定义智能体的构造逻辑。
java
@Component
public class SelectiveAgent extends BaseAgent {
public SelectiveAgent(ChatClient chatClient, TimeTools timeTools) {
super("selective_agent", chatClient);
// 手动注册工具
getToolManager().registerTool(timeTools);
}
}
但通常自动绑定更简单,我们采用自动绑定即可。
4.4 实践:让智能体查询实时信息
现在让我们构建一个完整的示例,包含两个工具:获取时间和查询天气。我们将创建一个 REST 接口,用户输入问题,智能体自动决定是否调用工具。
4.4.1 定义工具类
java
package com.example.demo.tool;
import com.alibaba.agentscope.tool.annotation.Param;
import com.alibaba.agentscope.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
@Component
public class TimeTool {
@Tool(name = "getCurrentTime", description = "获取当前时间")
public String getCurrentTime() {
return LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
}
}
@Component
public class WeatherTool {
private static final Map<String, String> WEATHER_MAP = Map.of(
"北京", "晴,25℃",
"上海", "多云,28℃",
"广州", "雷阵雨,30℃"
);
@Tool(name = "getWeather", description = "查询指定城市的天气")
public String getWeather(
@Param(description = "城市名称,如北京、上海") String city) {
return WEATHER_MAP.getOrDefault(city, city + "的天气数据暂未收录");
}
}
4.4.2 定义智能体
java
package com.example.demo.agent;
import com.alibaba.agentscope.agent.BaseAgent;
import com.alibaba.agentscope.llm.ChatClient;
import org.springframework.stereotype.Component;
@Component
public class ToolAgent extends BaseAgent {
public ToolAgent(ChatClient chatClient) {
super("tool_agent", chatClient);
}
@Override
protected String getSystemPrompt() {
return "你是一个智能助手,可以使用工具查询实时信息。" +
"当用户询问时间或天气时,请调用相应的工具。";
}
}
4.4.3 创建 Controller
java
package com.example.demo.controller;
import com.example.demo.agent.ToolAgent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ToolController {
private final ToolAgent toolAgent;
public ToolController(ToolAgent toolAgent) {
this.toolAgent = toolAgent;
}
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return toolAgent.run(question);
}
}
4.4.4 测试
启动应用,用浏览器或 curl 测试:
bash
# 时间查询
curl "http://localhost:8080/ask?question=现在几点了?"
# 预期返回类似:现在是下午2点30分45秒。
# 天气查询
curl "http://localhost:8080/ask?question=上海天气怎么样?"
# 预期返回:上海多云,28℃。
# 混合查询
curl "http://localhost:8080/ask?question=帮我查一下北京的天气,顺便告诉我现在几点了"
# 智能体会先后调用两个工具,然后综合回答。
# 无工具可用的问题
curl "http://localhost:8080/ask?question=计算123+456" # 没有计算器工具,智能体会尝试自己计算或表示无法计算
4.4.5 观察日志
在控制台,你可能会看到类似以下的日志,表示智能体调用了工具:
css
调用工具: getWeather, 参数: {"city":"上海"}
工具返回: 多云,28℃
4.5 工具调用的注意事项
4.5.1 工具方法应该是线程安全的
工具类通常是单例 Bean,因此方法需要是线程安全的。上面的例子中,getCurrentTime 是纯函数,安全;getWeather 也是只读操作,安全。如果工具修改了共享状态(如计数器),需要考虑同步。
4.5.2 工具方法的执行时间
工具方法执行时间不宜过长,因为整个调用是同步的(智能体在等待工具结果)。如果工具需要调用外部 API 或数据库,考虑设置超时,或采用异步方式(但 AgentScope 目前主要支持同步工具调用)。
4.5.3 错误处理
工具方法可能抛出异常。AgentScope 会捕获异常并将错误信息返回给智能体。智能体会根据错误信息决定如何回应(例如提示用户稍后重试)。你可以在工具方法内部处理异常,返回友好的错误消息。
java
@Tool(name = "getWeather")
public String getWeather(String city) {
try {
// 调用外部 API
return weatherApi.get(city);
} catch (Exception e) {
return "获取天气失败,请稍后重试。";
}
}
4.5.4 工具数量
不要注册过多无关的工具,因为工具描述会消耗 Token,且可能让智能体混淆。只注册必要且描述清晰的工具。
4.5.5 参数类型
工具方法的参数支持基本类型、String、复杂对象等,但智能体需要能够生成对应的 JSON。建议使用简单类型,并配合清晰描述。对于复杂对象,需要确保模型能理解其结构。
4.5.6 通义千问的特殊性
通义千问模型对工具调用的支持与 OpenAI 类似,但细节可能略有不同。AgentScope Java 已经做了适配,开发者无需关心底层差异。
4.6 工具调用流程详解
为了更深入理解,我们来看一下工具调用在 AgentScope Java 中的详细流程:
- 用户请求 :用户发送问题
/ask?question=上海天气怎么样? - 构建请求 :智能体将用户消息和可用工具列表(由
ToolManager提供)一起封装成请求,发送给大模型。 - 模型决策 :模型判断需要调用
getWeather工具,并生成参数{"city": "上海"}。 - 工具执行 :AgentScope 接收到模型返回的工具调用请求,查找对应的 Bean 方法,通过反射调用
WeatherTool.getWeather("上海")。 - 结果返回:工具执行结果("多云,28℃")被封装成新的消息(工具消息)再次发送给模型。
- 生成最终回答:模型结合工具结果和原始问题,生成最终回答:"上海今天多云,28℃。"
- 响应客户端:最终回答返回给用户。
流程图:
4.7 动手实践:扩展工具集
现在,请你自己尝试添加一个新工具,比如一个简单的计算器,能够计算两个数字的和、差、积、商。
4.7.1 定义计算器工具
java
@Component
public class CalculatorTool {
@Tool(name = "calculate", description = "计算两个数字的运算结果,支持加、减、乘、除")
public String calculate(
@Param(description = "第一个数字") double a,
@Param(description = "第二个数字") double b,
@Param(description = "运算符,可以是 +、-、*、/") String operator) {
return switch (operator) {
case "+" -> String.valueOf(a + b);
case "-" -> String.valueOf(a - b);
case "*" -> String.valueOf(a * b);
case "/" -> {
if (b == 0) yield "除数不能为0";
yield String.valueOf(a / b);
}
default -> "不支持的运算符";
};
}
}
4.7.2 测试
访问 http://localhost:8080/ask?question=计算123+456,智能体应该会调用计算器工具并返回结果。
4.8 本章小结
通过本章的学习,你掌握了:
- 工具调用的概念:让智能体调用外部方法获取实时信息或执行操作。
- 定义工具 :使用
@Tool注解标记方法,并用@Param描述参数。 - 工具注册 :AgentScope 自动收集所有带
@Tool的 Spring Bean,无需手动配置。 - 实践:构建了能查询时间和天气的智能助手,并扩展了计算器工具。
- 注意事项:线程安全、错误处理、参数描述等。
五、多智能体协作:让智能体团队合作
在前面的章节中,我们一直和单个智能体打交道。单个智能体可以完成不少任务,但当面对更复杂、需要多种专业能力的场景时,单打独斗就显得力不从心了。比如,规划一次旅行,需要查天气、查酒店、规划路线、预订门票......如果让一个智能体包揽所有,它可能会手忙脚乱,而且代码会变得异常臃肿。
多智能体协作正是为了解决这类问题:将复杂任务拆解,由多个各有所长的智能体分工合作,像人类团队一样高效协同。AgentScope Java 提供了强大的多智能体通信机制,让你能轻松构建这样的"智能体团队"。
5.1 为什么需要多个智能体?
先来看一个现实场景:用户说"我想去北京旅游3天,帮我规划一下"。这个任务其实包含多个子任务:
- 了解北京近期的天气(需要天气查询能力)
- 查找合适的酒店(需要酒店查询能力)
- 制定每日行程(需要景点推荐和路线规划能力)
如果用一个智能体完成所有事情,意味着它要同时具备天气查询、酒店查询、景点知识等多种能力,并且要自己拆解任务、按顺序执行。这不仅对模型要求高,而且代码耦合度极高,难以维护和扩展。
使用多智能体系统,我们可以这样分工:
- 主管智能体:接收用户请求,拆解任务,协调其他智能体工作,汇总结果。
- 天气智能体:专门负责查询天气。
- 酒店智能体:专门负责查询酒店信息。
- 行程智能体:专门负责规划行程。
每个智能体各司其职,代码清晰,易于扩展(比如增加机票查询智能体),而且可以复用(天气智能体也能用于其他场景)。
5.2 多智能体通信机制
要让智能体们协同工作,它们之间必须能"交流"。AgentScope Java 提供了 **MessageHub(消息总线)**作为智能体间通信的中枢。
5.2.1 MessageHub:全局消息总线
MessageHub 是一个全局的消息中心,所有智能体都可以通过它发送和接收消息。你可以把它想象成一个聊天群组:智能体可以向群组广播消息,也可以私聊某个特定智能体。
核心概念:
- 消息(Message):智能体间传递的信息单元,包含发送者、接收者、内容等。
- 发送 :智能体调用
MessageHub.send(message)将消息放入总线。 - 接收:智能体可以通过轮询或监听的方式从总线获取发给自己的消息。
为了简化,我们可以在智能体内部维护一个收件箱,每次处理完一条消息后,从 MessageHub 拉取下一条消息。
5.2.2 消息类型:广播与点对点
AgentScope 的消息支持两种路由方式:
- 广播(Broadcast) :消息的接收者为
null或特殊标识,表示所有智能体都能收到。适合发布公告、任务通知等。 - 点对点(P2P):指定接收者的名称,只有该智能体能收到。适合任务分配、结果返回。
5.2.3 在智能体中使用 MessageHub
要让智能体具备通信能力,我们只需在智能体类中注入 MessageHub 的 Bean,然后调用它的方法。下面是一个简单的例子:
java
import com.alibaba.agentscope.message.Message;
import com.alibaba.agentscope.message.MessageHub;
import org.springframework.beans.factory.annotation.Autowired;
public class MyAgent extends BaseAgent {
@Autowired
private MessageHub messageHub; // 注入消息总线
public MyAgent(ChatClient chatClient) {
super("my_agent", chatClient);
}
// 发送消息给指定智能体
protected void sendTo(String receiver, String content) {
Message msg = new Message(getName(), receiver, content);
messageHub.send(msg);
}
// 接收所有发给自己的消息
protected List<Message> receiveMessages() {
return messageHub.receive(getName()); // 获取所有发给自己的消息
}
}
实际使用时,我们通常会让智能体在一个循环中不断处理消息,类似于:
java
public void run() {
while (true) {
List<Message> messages = receiveMessages();
for (Message msg : messages) {
// 处理每条消息
String reply = processMessage(msg);
if (reply != null) {
sendTo(msg.getSender(), reply); // 回复发送者
}
}
// 短暂休眠,避免空转
Thread.sleep(100);
}
}
但为了与现有的 BaseAgent.run() 模型兼容,我们可以设计一种任务驱动 的方式:当用户请求到达时,主管智能体开始协调,而工人智能体则被动响应。这样我们不需要独立的消息循环,而是让 run 方法在需要时触发消息处理。
5.3 构建第一个多智能体系统:主管+工人模式
让我们通过一个简单的示例来感受多智能体协作:用户问"上海的天气怎么样?",主管智能体将问题转给天气智能体,获取结果后返回。
5.3.1 定义消息类
为了区分不同类型的消息,我们可以定义一个简单的 TaskMessage 类,包含任务类型和内容。但为了保持简单,我们直接使用 AgentScope 的 Message 类,并在内容中放入任务信息。
5.3.2 定义天气智能体
天气智能体专门处理天气查询。它从消息中提取城市,调用天气工具(上一章已定义),然后回复结果。
java
package com.example.demo.agent;
import com.alibaba.agentscope.agent.BaseAgent;
import com.alibaba.agentscope.llm.ChatClient;
import com.alibaba.agentscope.message.Message;
import com.alibaba.agentscope.message.MessageHub;
import com.example.demo.tool.WeatherTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class WeatherAgent extends BaseAgent {
@Autowired
private MessageHub messageHub;
@Autowired
private WeatherTool weatherTool;
public WeatherAgent(ChatClient chatClient) {
super("weather_agent", chatClient);
}
@Override
public String run(String input) {
// 此方法不会被直接调用,我们通过消息驱动
return null;
}
// 处理消息的方法,由主管智能体触发
public void handleMessage(Message msg) {
String content = msg.getContent();
// 解析任务,假设格式为 "天气:城市"
if (content.startsWith("天气:")) {
String city = content.substring(3).trim();
String weather = weatherTool.getWeather(city);
// 回复给发送者
Message reply = new Message(getName(), msg.getSender(), weather);
messageHub.send(reply);
}
}
}
5.3.3 定义主管智能体
主管智能体接收用户输入,然后根据需要向天气智能体发送消息,并等待回复。
java
package com.example.demo.agent;
import com.alibaba.agentscope.agent.BaseAgent;
import com.alibaba.agentscope.llm.ChatClient;
import com.alibaba.agentscope.message.Message;
import com.alibaba.agentscope.message.MessageHub;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@Component
public class SupervisorAgent extends BaseAgent {
@Autowired
private MessageHub messageHub;
@Autowired
private WeatherAgent weatherAgent; // 注入工人智能体
public SupervisorAgent(ChatClient chatClient) {
super("supervisor", chatClient);
}
@Override
public String run(String input) {
// 用户输入:例如 "上海的天气"
if (input.contains("天气")) {
// 1. 向天气智能体发送消息
String city = extractCity(input); // 简单提取城市,可用LLM或规则
Message taskMsg = new Message(getName(), "weather_agent", "天气:" + city);
messageHub.send(taskMsg);
// 2. 等待回复(这里用简单轮询,实际可用CompletableFuture)
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 10000) { // 最多等10秒
List<Message> replies = messageHub.receive(getName());
for (Message reply : replies) {
if (reply.getSender().equals("weather_agent")) {
return "天气查询结果:" + reply.getContent();
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return "天气查询超时";
}
// 其他情况由自己回答
return super.run(input);
}
private String extractCity(String input) {
// 简单规则:如果包含"上海"返回上海,否则默认北京
if (input.contains("上海")) return "上海";
if (input.contains("北京")) return "北京";
return "北京";
}
}
5.3.4 注册智能体为 Bean
确保所有智能体都被 Spring 管理,加上 @Component 注解。
5.3.5 创建控制器
修改之前的 ToolController,注入主管智能体:
java
@RestController
public class MultiAgentController {
@Autowired
private SupervisorAgent supervisorAgent;
@GetMapping("/multi-ask")
public String ask(@RequestParam String question) {
return supervisorAgent.run(question);
}
}
5.3.6 测试
启动应用,访问 http://localhost:8080/multi-ask?question=上海的天气怎么样?,应该能正确返回天气信息。
多智能体协作流程图:
5.4 实践:旅游规划多智能体系统
现在,我们来构建一个更实用的系统:旅游规划助手。它将包含三个专业智能体:天气智能体 、酒店智能体 和行程智能体 ,由一个主管智能体协调。
5.4.1 定义工具类
我们需要酒店查询工具和景点推荐工具(用于行程规划)。为简化,我们使用模拟数据。
java
// 酒店工具
@Component
public class HotelTool {
private static final Map<String, List<String>> HOTEL_MAP = Map.of(
"北京", List.of("北京饭店", "王府井酒店", "三里屯宾馆"),
"上海", List.of("和平饭店", "浦东香格里拉", "外滩W酒店"),
"广州", List.of("白天鹅宾馆", "花园酒店", "长隆酒店")
);
@Tool(name = "getHotels", description = "查询指定城市的酒店推荐")
public String getHotels(@Param(description = "城市名称") String city) {
List<String> hotels = HOTEL_MAP.get(city);
if (hotels == null) {
return city + "暂无酒店推荐";
}
return String.join("、", hotels);
}
}
// 景点推荐工具(用于行程)
@Component
public class AttractionTool {
private static final Map<String, List<String>> ATTRACTION_MAP = Map.of(
"北京", List.of("故宫", "长城", "天坛", "颐和园"),
"上海", List.of("外滩", "东方明珠", "迪士尼乐园", "豫园"),
"广州", List.of("广州塔", "长隆野生动物园", "沙面", "白云山")
);
@Tool(name = "getAttractions", description = "查询指定城市的景点推荐")
public String getAttractions(@Param(description = "城市名称") String city) {
List<String> attractions = ATTRACTION_MAP.get(city);
if (attractions == null) {
return city + "暂无景点推荐";
}
return String.join("、", attractions);
}
}
5.4.2 定义三个工人智能体
天气智能体 (复用之前的 WeatherAgent,但改为通过消息处理)。为了代码整洁,我们创建一个基类 WorkerAgent 封装消息处理逻辑。
java
public abstract class WorkerAgent extends BaseAgent {
@Autowired
protected MessageHub messageHub;
public WorkerAgent(String name, ChatClient chatClient) {
super(name, chatClient);
}
// 处理消息的方法,由子类实现
protected abstract String handleTask(String task);
// 启动消息处理循环(可以在单独的线程中运行,这里简化为由主管触发)
public void processMessages() {
List<Message> messages = messageHub.receive(getName());
for (Message msg : messages) {
String result = handleTask(msg.getContent());
Message reply = new Message(getName(), msg.getSender(), result);
messageHub.send(reply);
}
}
}
然后实现三个工人智能体:
java
@Component
public class WeatherWorker extends WorkerAgent {
@Autowired
private WeatherTool weatherTool;
public WeatherWorker(ChatClient chatClient) {
super("weather_worker", chatClient);
}
@Override
protected String handleTask(String task) {
if (task.startsWith("天气:")) {
String city = task.substring(3).trim();
return weatherTool.getWeather(city);
}
return "未知任务";
}
}
@Component
public class HotelWorker extends WorkerAgent {
@Autowired
private HotelTool hotelTool;
public HotelWorker(ChatClient chatClient) {
super("hotel_worker", chatClient);
}
@Override
protected String handleTask(String task) {
if (task.startsWith("酒店:")) {
String city = task.substring(3).trim();
return hotelTool.getHotels(city);
}
return "未知任务";
}
}
@Component
public class ItineraryWorker extends WorkerAgent {
@Autowired
private AttractionTool attractionTool;
public ItineraryWorker(ChatClient chatClient) {
super("itinerary_worker", chatClient);
}
@Override
protected String handleTask(String task) {
if (task.startsWith("行程:")) {
String city = task.substring(3).trim();
String attractions = attractionTool.getAttractions(city);
// 简单行程:假设三天,每天两个景点
return String.format("为您规划%s三日游行程:\n第一天:%s\n第二天:%s\n第三天:%s",
city,
attractions.split("、")[0] + "、" + attractions.split("、")[1],
attractions.split("、")[2] + "、" + attractions.split("、")[3],
"自由活动");
}
return "未知任务";
}
}
5.4.3 定义主管智能体
主管智能体接收用户请求,解析城市和任务类型,然后并发地向各工人智能体发送任务,收集结果后汇总。
java
@Component
public class TravelPlannerSupervisor extends BaseAgent {
@Autowired
private MessageHub messageHub;
public TravelPlannerSupervisor(ChatClient chatClient) {
super("travel_supervisor", chatClient);
}
@Override
public String run(String input) {
// 解析城市(简单规则:如果输入包含"北京"/"上海"/"广州",否则用默认)
String city = extractCity(input);
// 定义需要执行的任务列表
List<String> tasks = List.of(
"天气:" + city,
"酒店:" + city,
"行程:" + city
);
// 发送任务给对应工人(通过任务前缀匹配工人名称)
for (String task : tasks) {
String workerName = task.split(":")[0] + "_worker"; // 如 "weather_worker"
Message msg = new Message(getName(), workerName, task);
messageHub.send(msg);
}
// 等待所有工人回复(最多10秒)
Map<String, String> results = new HashMap<>();
long start = System.currentTimeMillis();
while (results.size() < tasks.size() && System.currentTimeMillis() - start < 10000) {
List<Message> replies = messageHub.receive(getName());
for (Message reply : replies) {
String sender = reply.getSender();
if (!results.containsKey(sender)) {
results.put(sender, reply.getContent());
}
}
try { Thread.sleep(200); } catch (InterruptedException e) {}
}
// 汇总结果
String weather = results.getOrDefault("weather_worker", "天气信息获取失败");
String hotel = results.getOrDefault("hotel_worker", "酒店信息获取失败");
String itinerary = results.getOrDefault("itinerary_worker", "行程信息获取失败");
return String.format("【%s旅游规划】\n天气:%s\n酒店推荐:%s\n%s",
city, weather, hotel, itinerary);
}
private String extractCity(String input) {
if (input.contains("北京")) return "北京";
if (input.contains("上海")) return "上海";
if (input.contains("广州")) return "广州";
return "北京"; // 默认
}
}
5.4.4 触发工人处理消息
为了让工人智能体能及时处理消息,我们需要在适当的时候调用它们的 processMessages() 方法。简单的方式是在每个工人智能体中添加一个 @Scheduled 定时任务,定期处理消息。或者,我们可以在主管发送消息后,手动调用工人的处理方法(但这样耦合度高)。这里采用定时任务方式,简单有效。
在 Spring Boot 主类上启用定时任务:
java
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
// ...
}
然后在每个工人智能体中添加定时方法:
java
@Component
public class WeatherWorker extends WorkerAgent {
// ...
@Scheduled(fixedDelay = 500) // 每500毫秒处理一次消息
public void scheduledProcess() {
processMessages();
}
}
同样为 HotelWorker 和 ItineraryWorker 添加定时方法。
5.4.5 创建控制器
java
@RestController
public class TravelController {
@Autowired
private TravelPlannerSupervisor supervisor;
@GetMapping("/plan-trip")
public String planTrip(@RequestParam String destination) {
return supervisor.run(destination);
}
}
5.4.6 测试
启动应用,访问:http://localhost:8080/plan-trip?destination=我想去上海旅游3天,会返回类似:
【上海旅游规划】
天气:多云,28℃
酒店推荐:和平饭店、浦东香格里拉、外滩W酒店
为您规划上海三日游行程:
第一天:外滩、东方明珠
第二天:迪士尼乐园、豫园
第三天:自由活动
5.4.7 多智能体协作流程图(完整)
5.5 本章小结
通过本章的学习,你掌握了多智能体协作的核心技术:
- 多智能体的价值:将复杂任务拆解,由专业智能体分工合作,提高系统可维护性和扩展性。
- MessageHub 通信机制:利用全局消息总线实现智能体间的松耦合通信。
- 广播与点对点:了解消息的两种路由方式。
- 实践:构建了旅游规划多智能体系统,包含天气、酒店、行程三个专业智能体,由主管智能体统一协调。
六、记忆管理:让智能体记住对话历史
在前面的章节中,我们构建的智能体虽然能够回答问题,但它们都是无状态 的------每次对话都是全新的开始,完全不记得之前说过什么。这就像和一个每次都忘记你是谁的朋友聊天,体验可想而知。要让智能体真正智能、个性化,它必须拥有记忆:记住用户的偏好、之前的对话内容,甚至从过往交互中学习。
本章将深入探讨 AgentScope Java 的记忆管理机制,让你能够为智能体添加短期和长期记忆,让它们成为用户的"老朋友"。
6.1 为什么需要记忆?
6.1.1 多轮对话中的上下文保持
想象一下这样的对话:
用户:我叫小明。
智能体:你好小明!
用户:我喜欢打篮球。
智能体:篮球是一项很棒的运动!
用户:你觉得我最喜欢的运动是什么?
如果没有记忆,智能体在最后一轮就无法知道用户之前提到过喜欢篮球。有了记忆,智能体可以回顾历史,给出正确答案:"你之前说过喜欢打篮球。"
6.1.2 用户偏好和个性化
记忆可以让智能体记住用户的个性化信息,如:
- 语言偏好("我喜欢用英文回答")
- 常用地点("我住在上海")
- 特殊要求("回答要简洁")
这样,智能体可以逐渐适应每个用户的独特风格,提供更贴心的服务。
6.1.3 长期学习和经验积累
更高级的记忆可以让智能体从过去的交互中学习。例如,用户经常询问 Java 编程问题,智能体可以自动调整回答风格,提供更多代码示例。这种学习通常需要结合长期记忆存储。
6.2 AgentScope Java 的 Memory 接口
AgentScope Java 提供了一个统一的 Memory 接口,用于抽象各种记忆存储实现。该接口定义了记忆的基本操作:
java
public interface Memory {
/**
* 向记忆中存入一条消息
*/
void put(ChatMessage message);
/**
* 获取所有记忆消息
*/
List<ChatMessage> get();
/**
* 清除记忆
*/
void clear();
/**
* 判断记忆是否为空
*/
boolean isEmpty();
/**
* 获取记忆中的消息数量
*/
int size();
}
任何记忆实现都需要实现这些方法。ChatMessage 是我们在第三章介绍过的消息类,包含角色和内容。
6.3 内置记忆实现详解
AgentScope Java 提供了几种内置的记忆实现,适用于不同场景。
6.3.1 ConversationMemory:简单的对话历史记忆
ConversationMemory 是最简单的实现,它将所有消息原封不动地存储在内存列表中。它无限增长,适合对话轮次不多的场景。
java
import com.alibaba.agentscope.memory.ConversationMemory;
Memory memory = new ConversationMemory();
memory.put(new UserMessage("你好"));
memory.put(new AssistantMessage("你好,有什么可以帮你的?"));
List<ChatMessage> history = memory.get(); // 返回两条消息
优点 :简单直接,信息完整。
缺点:无限增长,当对话很长时,可能超出模型上下文窗口。
6.3.2 TokenWindowMemory:基于 Token 窗口的记忆
TokenWindowMemory 解决无限增长的问题。它维护一个固定大小的 Token 窗口,当总 Token 数超过限制时,自动丢弃最早的消息,确保上下文长度可控。它需要配合一个 Tokenizer 来估算 Token 数。
java
import com.alibaba.agentscope.memory.TokenWindowMemory;
import com.alibaba.agentscope.tokenizer.SimpleTokenizer; // 假设有一个简单的 Tokenizer
Tokenizer tokenizer = new SimpleTokenizer();
Memory memory = new TokenWindowMemory(tokenizer, 2000); // 窗口大小 2000 tokens
// 不断添加消息,当总 token 超过 2000 时,最早的消息被移除
优点 :有效控制上下文长度,避免超出模型限制。
缺点:可能丢失早期重要信息。
6.3.3 SummaryMemory:定期总结压缩记忆
SummaryMemory 是一种更智能的记忆策略。它会定期(例如每 N 轮对话)调用大模型对之前的对话进行总结,将总结作为新的记忆存储,并丢弃原始对话细节。这样既保留了核心信息,又极大压缩了 Token 占用。
java
import com.alibaba.agentscope.memory.SummaryMemory;
import com.alibaba.agentscope.llm.ChatClient;
ChatClient llm = ...; // 大模型客户端
Memory memory = new SummaryMemory(llm, 5); // 每5轮总结一次
优点 :高度压缩,保留关键信息,适合长期对话。
缺点:需要额外调用模型,可能增加成本和延迟。
6.4 为智能体添加记忆
要让智能体拥有记忆,我们需要在智能体内部持有一个 Memory 实例,并在构建消息列表时加入历史消息。AgentScope 的 BaseAgent 已经预留了记忆接口,我们可以通过构造器传入或重写相关方法。
6.4.1 在创建智能体时指定 Memory
java
@Component
public class MemoryAgent extends BaseAgent {
private final Memory memory;
public MemoryAgent(ChatClient chatClient, Memory memory) {
super("memory_agent", chatClient);
this.memory = memory;
}
@Override
protected List<ChatMessage> buildMessages(String input) {
// 1. 将用户输入存入记忆
UserMessage userMsg = new UserMessage(input);
memory.put(userMsg);
// 2. 获取所有历史消息(包括刚存入的用户消息)
List<ChatMessage> history = memory.get();
// 3. 构建消息列表:系统消息 + 历史消息
List<ChatMessage> messages = new ArrayList<>();
messages.add(new SystemMessage(getSystemPrompt()));
messages.addAll(history);
return messages;
}
@Override
protected String postprocess(String output) {
// 将助手回复存入记忆
memory.put(new AssistantMessage(output));
return output;
}
}
6.4.2 在配置中定义 Memory Bean
为了灵活切换记忆策略,我们可以将 Memory 定义为 Spring Bean:
java
@Configuration
public class MemoryConfig {
@Bean
public Memory conversationMemory() {
return new ConversationMemory();
}
// 也可以选择其他实现,例如:
// @Bean
// public Memory tokenWindowMemory(Tokenizer tokenizer) {
// return new TokenWindowMemory(tokenizer, 2000);
// }
}
然后在智能体中通过 @Autowired 注入即可。
6.5 实践:构建带记忆的聊天助手
现在让我们创建一个使用 ConversationMemory 的智能体,并测试它的记忆能力。
6.5.1 创建智能体类
java
package com.example.demo.agent;
import com.alibaba.agentscope.agent.BaseAgent;
import com.alibaba.agentscope.llm.ChatClient;
import com.alibaba.agentscope.memory.Memory;
import com.alibaba.agentscope.message.AssistantMessage;
import com.alibaba.agentscope.message.ChatMessage;
import com.alibaba.agentscope.message.SystemMessage;
import com.alibaba.agentscope.message.UserMessage;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class RememberingAgent extends BaseAgent {
private final Memory memory;
public RememberingAgent(ChatClient chatClient, Memory memory) {
super("remembering_agent", chatClient);
this.memory = memory;
}
@Override
protected String getSystemPrompt() {
return "你是一个有记忆的助手,会记住用户之前说过的话。";
}
@Override
protected List<ChatMessage> buildMessages(String input) {
// 存入用户消息
memory.put(new UserMessage(input));
// 构建消息列表:系统消息 + 历史消息
List<ChatMessage> messages = new ArrayList<>();
messages.add(new SystemMessage(getSystemPrompt()));
messages.addAll(memory.get());
return messages;
}
@Override
protected String postprocess(String output) {
// 存入助手消息
memory.put(new AssistantMessage(output));
return output;
}
}
6.5.2 创建控制器
java
@RestController
public class MemoryController {
@Autowired
private RememberingAgent rememberingAgent;
@GetMapping("/memory-chat")
public String chat(@RequestParam String message) {
return rememberingAgent.run(message);
}
}
6.5.3 测试
- 启动应用。
- 第一轮:
http://localhost:8080/memory-chat?message=我叫小明 - 第二轮:
http://localhost:8080/memory-chat?message=我叫什么名字?
你会看到智能体在第二轮正确回答"你叫小明"。这说明智能体记住了之前的对话。
6.5.4 不同记忆策略的对比
你可以将配置中的 Memory Bean 替换为 TokenWindowMemory 或 SummaryMemory,观察不同策略对长对话的影响。
6.6 记忆持久化
目前的所有记忆实现都是内存存储,这意味着一旦应用重启,记忆就会丢失。在生产环境中,我们通常需要将记忆持久化到磁盘或数据库中,以便长期保存。
6.6.1 为什么需要持久化?
- 用户长期体验:用户希望智能体在多次访问后仍然记得自己的偏好。
- 跨会话记忆:比如电商客服,需要记住用户的历史订单和咨询记录。
- 系统恢复:应用重启后,记忆不丢失,无缝继续对话。
6.6.2 使用 Redis 实现持久化记忆
AgentScope Java 支持通过自定义 Memory 实现来对接 Redis。下面是一个简单的基于 Redis 的 Memory 实现(使用 Spring Data Redis):
首先,添加 Redis 依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后,实现 Memory 接口:
java
import com.alibaba.agentscope.memory.Memory;
import com.alibaba.agentscope.message.ChatMessage;
import com.alibaba.agentscope.serializer.JsonSerializer;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class RedisMemory implements Memory {
private final RedisTemplate<String, String> redisTemplate;
private final String memoryKey;
private final JsonSerializer serializer = new JsonSerializer(); // 用于序列化消息
public RedisMemory(RedisTemplate<String, String> redisTemplate, String sessionId) {
this.redisTemplate = redisTemplate;
this.memoryKey = "agent:memory:" + sessionId;
}
@Override
public void put(ChatMessage message) {
// 将消息序列化为 JSON 字符串
String json = serializer.serialize(message);
// 存入 Redis List,右推
redisTemplate.opsForList().rightPush(memoryKey, json);
// 可选:设置过期时间,比如 7 天
redisTemplate.expire(memoryKey, 7, TimeUnit.DAYS);
}
@Override
public List<ChatMessage> get() {
// 获取整个 List,从 0 到 -1 表示所有元素
List<String> jsonList = redisTemplate.opsForList().range(memoryKey, 0, -1);
if (jsonList == null) return List.of();
return jsonList.stream()
.map(json -> serializer.deserialize(json, ChatMessage.class))
.collect(Collectors.toList());
}
@Override
public void clear() {
redisTemplate.delete(memoryKey);
}
@Override
public boolean isEmpty() {
Long size = redisTemplate.opsForList().size(memoryKey);
return size == null || size == 0;
}
@Override
public int size() {
Long size = redisTemplate.opsForList().size(memoryKey);
return size == null ? 0 : size.intValue();
}
}
注意:JsonSerializer 需要自己实现,可以使用 Jackson 或 Gson 将 ChatMessage 与 JSON 互转。
6.6.3 在智能体中使用持久化记忆
由于每个用户应该有独立的记忆,我们需要在智能体中根据用户标识(如 sessionId)来创建不同的 RedisMemory 实例。这可以通过在 run 方法中接收用户 ID 来实现,但 BaseAgent.run 只接受一个字符串。一个常见的设计是:智能体内部持有一个 MemoryProvider,根据上下文动态提供记忆实例。
简化起见,我们可以在控制器层为每个用户创建独立的智能体实例,但这会消耗大量资源。更好的方式是在智能体内部维护一个 Map<userId, Memory>,并在每次调用时根据 userId 获取对应的记忆。这需要修改智能体接口,让它接收两个参数:用户 ID 和消息。
我们将在后续章节中探讨这种设计。目前,理解持久化的概念即可。
6.7 本章小结
通过本章的学习,你掌握了:
- 记忆的重要性:上下文保持、个性化、长期学习。
- Memory 接口:AgentScope Java 统一的记忆抽象。
- 内置记忆实现 :
ConversationMemory(完整历史)、TokenWindowMemory(滑动窗口)、SummaryMemory(总结压缩)。 - 为智能体添加记忆 :在
BaseAgent中集成 Memory,重写buildMessages和postprocess。 - 实践:构建了带记忆的聊天助手,测试多轮对话效果。
- 记忆持久化:了解为何需要持久化,以及如何用 Redis 实现。
七、ReAct 模式:智能体的思考与行动循环
在前面的章节中,我们已经学会了让智能体调用工具(第四章)和使用记忆(第六章)。但是,这些工具调用是单次的:智能体一次决策调用一个工具,得到结果后就直接回答。这种模式对于简单查询(如天气、时间)足够了,但对于需要多步推理、根据中间结果调整下一步行动的复杂任务,就显得力不从心。
ReAct 模式正是为了解决这类问题而生的。它让智能体进入一个"思考→行动→观察→再思考"的循环,像人类一样逐步推理,直到完成复杂任务。
7.1 什么是 ReAct 模式?
ReAct 是 Reasoning + Acting 的缩写,由研究人员提出的一种让大模型通过交替进行"推理"和"行动"来解决复杂任务的范式。其核心思想是:智能体在执行任务时,不仅要生成行动(如调用工具),还要生成推理过程(思考下一步该做什么),并根据行动的结果(观察)调整后续推理。
一个典型例子:用户要求"写一段 Python 代码计算斐波那契数列,并运行它"。
- 思考1:我需要先写代码。
- 行动1:调用代码编写工具(或直接用模型生成)写代码。
- 观察1:代码已生成。
- 思考2:现在需要运行这段代码。
- 行动2:调用代码执行工具运行代码。
- 观察2:代码运行出错,提示"变量未定义"。
- 思考3:错误原因是变量名写错了,我需要修改代码。
- 行动3:调用代码修改工具修正代码。
- 观察3:代码运行成功,输出结果。
- 最终回答:向用户报告结果。
这个循环可以持续多轮,直到任务完成或达到最大尝试次数。
ReAct 循环示意图:
与普通工具调用相比,ReAct 的优势在于:
- 多步决策:可以根据中间结果调整计划。
- 自我纠错:遇到错误可以修正,而不是直接放弃。
- 透明可解释:可以看到智能体的思考过程,便于调试。
7.2 AgentScope 中的 ReActAgent
AgentScope Java 内置了 ReActAgent 类,它继承自 BaseAgent,并封装了 ReAct 循环的逻辑。你只需要做两件事:
- 提供工具集(通过
@Tool注解,与第四章相同)。 - 设置系统提示词,告诉智能体它的任务和可用工具。
7.2.1 ReActAgent 的核心方法
ReActAgent 主要增加了以下关键配置:
- 最大迭代次数(maxIterations):防止无限循环,默认通常是 10。
- 工具管理器(ToolManager) :与第四章相同,自动收集所有
@ToolBean。
在构造 ReActAgent 时,你需要传入 ChatClient 和最大迭代次数。
7.2.2 在 Spring 中定义 ReActAgent
由于 ReActAgent 也是 Spring Bean,我们可以通过 @Component 或 @Bean 来创建它。这里我们使用 @Bean 方式,以便在创建时指定最大迭代次数。
首先,在配置类中定义 ReActAgent Bean:
java
@Configuration
public class AgentConfig {
@Bean
public ReActAgent codeAssistant(ChatClient chatClient) {
return ReActAgent.builder()
.name("code_assistant")
.chatClient(chatClient)
.maxIterations(5) // 最多循环5次
.build();
}
}
注意 :工具会自动被发现,不需要手动传入。因为 ReActAgent 内部会使用 ToolManager 从 Spring 容器中收集所有带 @Tool 的 Bean。
7.3 实践:构建一个能自我纠错的代码助手
现在,我们来实现一个能写代码、运行代码、并根据错误修正代码的智能体。为了简化,我们模拟一个代码执行工具,它可以"运行"代码并返回结果或错误信息。
7.3.1 定义代码执行工具
创建一个工具类,包含一个方法模拟运行代码。这里我们不真的执行代码,而是模拟一些常见错误。
java
package com.example.demo.tool;
import com.alibaba.agentscope.tool.annotation.Param;
import com.alibaba.agentscope.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@Component
public class CodeExecutorTool {
// 模拟代码执行结果
private final Map<String, String> codeResults = new HashMap<>();
@Tool(name = "executeCode", description = "执行一段代码并返回结果,如果代码有错误会返回错误信息")
public String executeCode(
@Param(description = "要执行的代码字符串") String code) {
// 这里我们模拟一些常见的代码错误
if (code.contains("斐波那契")) {
// 假设第一次运行总是有错误
if (!codeResults.containsKey(code)) {
codeResults.put(code, "错误:变量 'n' 未定义");
return "错误:变量 'n' 未定义";
} else {
// 第二次运行成功
return "执行成功,斐波那契数列的第10项是 55";
}
} else if (code.contains("print")) {
// 模拟成功执行
return "执行成功,输出:" + code.substring(code.indexOf("print") + 6);
} else {
return "执行成功,无输出";
}
}
}
为了更真实,我们也可以加入简单的语法检测。这里只是演示目的。
7.3.2 创建 ReAct 智能体
我们已经在 AgentConfig 中定义了 codeAssistant Bean。现在需要为它设置合适的系统提示词,指导它如何进行 ReAct 循环。系统提示词非常关键,它告诉智能体应该遵循的格式:思考、行动、观察。
通常,系统提示词会包含以下内容:
- 智能体的角色(代码助手)。
- 可用的工具列表(尽管框架会自动提供,但提示词中最好也说明)。
- ReAct 循环的格式示例,例如每次输出必须包含
Thought:和Action:或Final Answer:。
由于 ReActAgent 内部已经内置了默认的 ReAct 提示词模板,我们只需要简单设置系统消息即可。但为了定制,我们可以重写 getSystemPrompt 方法。不过 ReActAgent 本身不直接提供这个方法,我们需要通过 ReActAgent.builder() 的 systemPrompt 方法设置。
修改 AgentConfig:
java
@Bean
public ReActAgent codeAssistant(ChatClient chatClient) {
String systemPrompt = """
你是一个代码助手,可以编写代码并执行它。
你的任务是根据用户需求编写代码,然后使用 executeCode 工具运行代码。
如果代码运行出错,根据错误信息修改代码,再次运行,直到成功。
每次输出必须包含:
Thought: 你的推理过程
Action: 工具名称和参数,格式为:工具名(参数)
当任务完成时,输出:
Final Answer: 最终结果
""";
return ReActAgent.builder()
.name("code_assistant")
.chatClient(chatClient)
.systemPrompt(systemPrompt)
.maxIterations(5)
.build();
}
7.3.3 创建控制器
java
package com.example.demo.controller;
import com.alibaba.agentscope.agent.ReActAgent;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CodeController {
private final ReActAgent codeAssistant;
public CodeController(@Qualifier("codeAssistant") ReActAgent codeAssistant) {
this.codeAssistant = codeAssistant;
}
@GetMapping("/code")
public String runCode(@RequestParam String task) {
return codeAssistant.run(task);
}
}
7.3.4 测试
启动应用,访问:http://localhost:8080/code?task=请写一个Python函数计算斐波那契数列的第10项,并运行它
观察控制台日志,你会看到智能体的思考过程,以及多轮调用 executeCode 工具的尝试。最终返回的结果应该是成功后的回答。
预期输出(简化):
经过尝试,代码最终成功运行,斐波那契数列的第10项是 55。
7.3.5 ReAct 循环详细流程
让我们用序列图展示一次典型的执行过程:
7.4 ReAct 模式的进阶应用
7.4.1 多工具协作
在 ReAct 循环中,智能体可以使用多个不同的工具,根据任务需要动态选择。例如,一个数据分析智能体可能先后使用"查询数据"、"生成图表"、"解释结果"等多个工具。
7.4.2 处理复杂错误
你可以让智能体在遇到错误时,不仅修正代码,还能根据错误类型调整策略。比如,如果是语法错误,可能只需要简单修改;如果是逻辑错误,可能需要重新设计算法。
7.4.3 人机交互(Human-in-the-loop)
在某些关键步骤,可以让智能体暂停并向用户请求确认。虽然 ReActAgent 目前不直接支持,但可以通过自定义工具实现:工具可以返回一个需要用户输入的标记,智能体看到这个标记后输出问题,等待用户响应(可以通过外部机制实现)。
7.5 注意事项
-
迭代次数限制 :务必设置合理的
maxIterations,防止智能体陷入无限循环(例如工具总是返回错误,导致不断重试)。生产环境中可设置为 10 或 20。 -
提示词设计 :ReAct 模式高度依赖提示词的质量。系统提示词必须清晰地说明格式要求,并给出示例。如果智能体不按照格式输出(例如缺少
Thought:),框架可能无法解析。 -
工具返回格式:工具返回的结果应该清晰易懂,便于智能体理解。例如,返回 JSON 格式或带有明确错误码的文本。
-
性能考虑:每轮循环都会调用一次大模型,多次循环会增加延迟和 Token 消耗。对于实时性要求高的场景,应尽量减少不必要的循环。
7.6 本章小结
通过本章的学习,你掌握了 ReAct 模式的精髓:
- ReAct 模式:让智能体在思考、行动、观察之间循环,解决多步复杂任务。
- ReActAgent 使用 :通过
ReActAgent.builder()创建,设置系统提示词和最大迭代次数。 - 实践:构建了一个能自我纠错的代码助手,它可以根据运行错误修改代码,直到成功。
- 流程可视化:用序列图展示了 ReAct 循环的详细步骤。
ReAct 模式是构建强大智能体的关键。现在,你的智能体不仅会调用工具,还能自主地多步推理和纠错,应对更开放、更复杂的任务。
八、工作流编排:多智能体流程设计
在前几章中,我们学习了多智能体协作(第五章),让智能体们通过消息总线自由交流。这种模式灵活,但有时我们需要更确定的流程------比如客服系统中的"客户问题→分类→分派→处理→回复",每一步都必须按顺序执行,不允许智能体随意跳转。这种预定义、结构化 的执行路径,就是工作流编排。
AgentScope Java 提供了强大的 Pipeline 机制,让你能以声明式的方式组合多个处理单元(可以是智能体、函数,甚至是另一个工作流),实现顺序、并行、条件分支等控制流。
8.1 什么是工作流编排?
工作流编排,简单说就是定义好一个流程,告诉系统先做什么、后做什么、哪些可以同时做、根据什么条件选择做什么。它与多智能体协作的区别在于:
| 对比维度 | 多智能体协作 (MessageHub) | 工作流编排 (Pipeline) |
|---|---|---|
| 通信方式 | 通过消息总线异步通信,智能体自主决定行为 | 流程引擎控制执行顺序,节点被动调用 |
| 流程确定性 | 低,智能体可能不按预期顺序执行 | 高,执行顺序完全由流程定义 |
| 适用场景 | 开放式任务、需要动态调整 | 业务流程固定、需要审计和可靠性的场景 |
| 实现复杂度 | 需要处理消息、协调状态 | 只需定义流程,框架负责执行 |
两者并非互斥,可以结合使用:外层用工作流定义宏观流程,内层用多智能体协作处理复杂子任务。
8.2 AgentScope 中的 Pipeline 机制
AgentScope Java 的 pipeline 包提供了以下核心接口和类:
Pipeline:所有工作流的顶级接口,定义了run(input)方法。SequentialPipeline:顺序执行多个节点,前一个的输出作为下一个的输入。ParallelPipeline:并行执行多个节点,等待所有节点完成后,将所有结果合并返回。ConditionalPipeline:根据条件选择执行某个分支节点。
每个节点可以是:
Agent:智能体,执行run方法。Pipeline:嵌套子工作流,实现复用。- 普通函数 :通过
FunctionNode包装,执行任意 Java 代码。
8.3 顺序执行:SequentialPipeline
顺序执行是最简单的流程。例如:先调用天气智能体,再将结果传给穿衣建议智能体。
8.3.1 定义节点
假设我们已经有两个智能体(或函数):
weatherAgent:输入城市,返回天气。clothingAdviceAgent:输入天气描述,返回穿衣建议。
我们让它们顺序执行。
8.3.2 构建 SequentialPipeline
java
import com.alibaba.agentscope.pipeline.SequentialPipeline;
import com.alibaba.agentscope.pipeline.Node;
@Configuration
public class WorkflowConfig {
@Bean
public Pipeline weatherAdviceWorkflow(WeatherAgent weatherAgent, ClothingAdviceAgent clothingAgent) {
// 将智能体包装为 Node
Node weatherNode = Node.from(weatherAgent);
Node clothingNode = Node.from(clothingAgent);
// 构建顺序工作流
return SequentialPipeline.builder()
.name("weather_advice_workflow")
.nodes(List.of(weatherNode, clothingNode))
.build();
}
}
8.3.3 运行工作流
java
@RestController
public class WorkflowController {
@Autowired
@Qualifier("weatherAdviceWorkflow")
private Pipeline weatherAdviceWorkflow;
@GetMapping("/advice")
public String getAdvice(@RequestParam String city) {
return weatherAdviceWorkflow.run(city);
}
}
访问 /advice?city=上海,工作流会:
weatherAgent.run("上海")→ 返回 "多云,28℃"clothingAgent.run("多云,28℃")→ 返回 "建议穿短袖,带一件薄外套"
8.4 并行执行:ParallelPipeline
并行执行可以同时执行多个节点,提升效率。例如,在旅游规划中,同时查询天气、酒店、景点,然后汇总。
8.4.1 构建 ParallelPipeline
java
@Bean
public Pipeline parallelTravelPipeline(WeatherAgent weatherAgent,
HotelAgent hotelAgent,
ItineraryAgent itineraryAgent) {
List<Node> nodes = List.of(
Node.from(weatherAgent),
Node.from(hotelAgent),
Node.from(itineraryAgent)
);
return ParallelPipeline.builder()
.name("parallel_travel")
.nodes(nodes)
.build();
}
注意:并行节点的输入相同,都会收到原始输入(城市名)。它们的输出会被收集成一个列表,顺序与节点顺序一致。
8.4.2 处理并行结果
由于并行工作流的输出是一个 List<String>,我们可以再通过一个 FunctionNode 来汇总结果。
java
Node summarizerNode = Node.from((List<String> results) -> {
String weather = results.get(0);
String hotel = results.get(1);
String itinerary = results.get(2);
return String.format("天气:%s\n酒店:%s\n行程:%s", weather, hotel, itinerary);
});
Pipeline parallelPipeline = ParallelPipeline.builder()
.name("parallel_travel")
.nodes(List.of(weatherNode, hotelNode, itineraryNode))
.build();
// 将并行结果与汇总节点组合成顺序工作流
Pipeline fullWorkflow = SequentialPipeline.builder()
.name("travel_workflow")
.nodes(List.of(
Node.from(parallelPipeline), // 先并行执行
summarizerNode // 再汇总
))
.build();
8.5 条件分支:ConditionalPipeline
条件分支根据输入或中间结果,决定执行哪个节点。例如:如果用户问题包含"天气",走天气查询路径;否则走默认路径。
8.5.1 构建条件分支
需要定义一个决策函数,返回要执行的节点名称。
java
import com.alibaba.agentscope.pipeline.ConditionalPipeline;
import com.alibaba.agentscope.pipeline.DecisionFunction;
// 决策函数
DecisionFunction decision = (input) -> {
if (input.contains("天气")) {
return "weather_agent";
} else {
return "default_agent";
}
};
ConditionalPipeline conditionalPipeline = ConditionalPipeline.builder()
.name("routing_workflow")
.decisionFunction(decision)
.branch("weather_agent", Node.from(weatherAgent)) // 分支节点
.branch("default_agent", Node.from(defaultAgent))
.build();
8.6 实践:构建一个智能客服工作流
现在,我们综合运用上述知识,构建一个智能客服系统。流程如下:
- 分类节点:根据用户问题,判断是"订单查询"、"产品咨询"还是"其他"。
- 分支路由 :
- 订单查询 → 调用订单智能体
- 产品咨询 → 调用产品智能体
- 其他 → 调用通用智能体
- 汇总节点:将结果统一格式,返回给用户。
8.6.1 定义分类节点(用智能体实现分类)
创建一个 ClassifierAgent,专门用于分类。
java
@Component
public class ClassifierAgent extends BaseAgent {
public ClassifierAgent(ChatClient chatClient) {
super("classifier", chatClient);
}
@Override
protected String getSystemPrompt() {
return "你是一个问题分类器,将用户问题分类为:order(订单)、product(产品)、other(其他)。只输出分类名称,不要其他内容。";
}
@Override
protected String postprocess(String output) {
// 确保输出是纯分类名
return output.trim().toLowerCase();
}
}
8.6.2 定义专业智能体
java
@Component
public class OrderAgent extends BaseAgent {
public OrderAgent(ChatClient chatClient) {
super("order_agent", chatClient);
}
@Override
protected String getSystemPrompt() {
return "你是一个订单客服助手,回答关于订单的问题,如物流、退换货等。";
}
}
@Component
public class ProductAgent extends BaseAgent {
public ProductAgent(ChatClient chatClient) {
super("product_agent", chatClient);
}
@Override
protected String getSystemPrompt() {
return "你是一个产品专家,回答关于产品功能、规格、价格的问题。";
}
}
@Component
public class DefaultAgent extends BaseAgent {
public DefaultAgent(ChatClient chatClient) {
super("default_agent", chatClient);
}
@Override
protected String getSystemPrompt() {
return "你是一个通用客服助手,回答一般性问题。";
}
}
8.6.3 构建条件工作流
首先,分类节点的输出作为决策依据。我们创建一个决策函数,根据分类结果选择不同的专业智能体。
java
@Configuration
public class CustomerServiceWorkflowConfig {
@Bean
public Pipeline customerServiceWorkflow(ClassifierAgent classifier,
OrderAgent orderAgent,
ProductAgent productAgent,
DefaultAgent defaultAgent) {
// 分类节点
Node classifierNode = Node.from(classifier);
// 决策函数:根据分类结果选择分支
DecisionFunction routeDecision = (classification) -> {
return switch (classification.trim()) {
case "order" -> "order";
case "product" -> "product";
default -> "other";
};
};
// 条件分支
ConditionalPipeline routingPipeline = ConditionalPipeline.builder()
.name("routing")
.decisionFunction(routeDecision)
.branch("order", Node.from(orderAgent))
.branch("product", Node.from(productAgent))
.branch("other", Node.from(defaultAgent))
.build();
// 最终汇总节点(这里简单直接返回结果,可以添加包装)
// 但为了输出整洁,我们可以加一个汇总节点
// 构建顺序工作流:先分类,再路由
return SequentialPipeline.builder()
.name("customer_service_workflow")
.nodes(List.of(
classifierNode,
Node.from(routingPipeline) // 嵌套条件工作流
))
.build();
}
}
8.6.4 控制器
java
@RestController
public class CustomerServiceController {
@Autowired
@Qualifier("customerServiceWorkflow")
private Pipeline customerServiceWorkflow;
@GetMapping("/customer-service")
public String handleQuery(@RequestParam String question) {
return customerServiceWorkflow.run(question);
}
}
8.6.5 测试
http://localhost:8080/customer-service?question=我的订单什么时候发货?→ 触发订单智能体。http://localhost:8080/customer-service?question=这款手机有什么颜色?→ 触发产品智能体。http://localhost:8080/customer-service?question=你好→ 触发通用智能体。
8.6.6 工作流流程图
8.7 工作流与 Agent 的嵌套
AgentScope 的 Pipeline 支持任意层级的嵌套,这意味着你可以构建非常复杂的流程。例如,在客服工作流中,订单智能体内部又可以是一个子工作流(先查物流、再查退换货政策)。这为实现模块化、复用提供了极大便利。
8.8 本章小结
通过本章的学习,你掌握了:
- 工作流编排的概念及其与多智能体协作的区别。
- SequentialPipeline:顺序执行多个节点。
- ParallelPipeline:并行执行多个节点,提升效率。
- ConditionalPipeline:根据条件动态路由。
- 实践:构建了一个智能客服工作流,综合运用了顺序、条件和嵌套。
现在,你已经能够用工作流将多个智能体组装成可靠的业务流程,满足企业级应用的确定性要求。