本教程将带你从零开始构建一个基于 AgentScope Java 的结构化任务规划系统。通过这个项目,你将掌握如何使用 PlanNotebook 组件实现复杂的任务分解、进度跟踪和人机协作审查功能。
项目地址: https://gitee.com/CodeMao01/plan-notebook
一、项目介绍
1.1、项目概述
1.1.1、什么是 PlanNotebook?
PlanNotebook 是一个展示结构化任务规划模式的智能对话示例项目。在这个系统中,AI 助手能够将复杂任务分解为可管理的子任务,跟踪每个子任务的执行状态,并在关键节点暂停等待用户审查和确认。
1.1.2、应用场景
- 📋 复杂项目管理:将大型项目分解为多个阶段和子任务
- 🔍 分步任务执行:逐步完成任务并记录每个步骤的产出
- 👥 人机协作审查:在计划变更时暂停,允许用户审查和修改
- 📊 进度可视化:实时跟踪任务状态和完成情况
- 💾 历史计划管理:保存和恢复历史计划用于参考
1.2、核心特性
1.2.1、结构化任务规划
支持创建包含多个子任务的计划,每个子任务都有明确的目标和预期产出。
1.2.2、实时 SSE 流式通信
使用 Server-Sent Events 实现服务端向客户端的实时消息推送,包括:
- AI 思考过程(thinking)
- 文本回复(text)
- 工具调用结果(tool)
- 模型上下文(ctx)
- 计划状态变更(plan)
1.2.3、模型上下文检查
每次 LLM 调用都会发出 ctx SSE 负载,UI 可以显示:
- JSON 格式的系统提示和记忆内容
- 扁平化的对话转录本
- 与前一次调用的差异对比
1.2.4、人机协作审查机制
用户可以请求 Agent 在执行下一个计划工具后暂停,以便审查计划变更。
1.2.5、 完整的计划生命周期管理
- 创建计划(create_plan)
- 更新计划信息(update_plan_info)
- 添加/修改/删除子任务(revise_current_plan)
- 更新子任务状态(update_subtask_state)
- 完成子任务(finish_subtask)
- 完成计划(finish_plan)
- 查看历史计划(view_historical_plans)
1.2.6、安全的文件操作
提供沙箱化的文件读写和 Shell 命令执行功能,限制在指定的工作空间目录内。
1.3、技术栈
| 技术领域 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 4.x | 轻量级 Web 应用框架 |
| Web 引擎 | Spring WebFlux | 响应式 Web 框架,支持异步流处理 |
| AI 框架 | AgentScope Core 1.0.11-SNAPSHOT | 阿里巴巴开源的多智能体协作框架 |
| 大模型 | Qwen(通义千问) | 通过 DashScope API 调用,支持思考模式 |
| 通信协议 | SSE (Server-Sent Events) | 单向实时消息推送 |
| 计划管理 | PlanNotebook | AgentScope 内置的结构化任务规划组件 |
| 前端技术 | Vanilla JavaScript | 原生 JS,无框架依赖 |
| 构建工具 | Maven | 项目依赖管理和构建 |
二、项目搭建
- 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 https://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>4.1.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>plan-notebook</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>plan-notebook</name>
<description>plan-notebook</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-core</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>default-testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
- yaml
java
spring:
application:
name: plan-notebook
server:
port: 8804
- .env(补充自己的DASHSCOPE_API_KEY)
java
BASE_URL=http://localhost:11434
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com
DASHSCOPE_MODEL_NAME=qwen3-max-2026-01-23
- 启动类
java
package com.example.plannotebook;
import io.github.cdimascio.dotenv.Dotenv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PlanNotebookApplication {
public static void main(String[] args) {
// 方式一:
// 加载.env文件内容
// Dotenv.configure().ignoreIfMissing().systemProperties().load();
// 方式二: 这种方式会加载系统的环境变量
// 加载.env文件
Dotenv load = Dotenv.configure().ignoreIfMissing().load();
// 把.env变量设置成系统属性
load.entries().forEach(entry -> System.setProperty(entry.getKey(), entry.getValue()));
SpringApplication.run(PlanNotebookApplication.class, args);
}
}
- 测试接口
java
package com.example.plannotebook.controller;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.List;
@RestController
public class ChatController {
/**
* produces = MediaType.TEXT_EVENT_STREAM_VALUE 相当于设置成SSE返回, 等价于Flux<ServerSentEvent<String>>
*
* @param message
* @return
*/
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(String message) {
List<String> hello = List.of("hello", "world", "!");
return Flux.fromIterable(hello);
}
// @GetMapping(value = "/chat")
// public Flux<ServerSentEvent<String>> chat(String message) {
// List<String> hello = List.of("hello", "world", "!");
// return Flux.fromIterable(hello).map(v -> ServerSentEvent.<String>builder().data(v).build());
// }
}
三、Sinks
3.1、什么是 Sink?为什么要使用?
3.1.1、核心概念
Sink(接收器/汇聚点) 是 Project Reactor 提供的一种手动控制数据发射的机制。它允许你在响应式流的外部,主动地向流中推送数据。
可以把 Sink 想象成一个水龙头:
- 传统响应式流:数据源自动流动(如数据库查询结果自动返回)
- 使用 Sink:你手动拧开水龙头,决定何时放水、放多少水
3.1.2、为什么需要 Sink?
问题场景:响应式世界 vs 命令式世界
在响应式编程中,数据流通常是被动订阅的:
plain
// 传统响应式:数据源驱动
Flux.fromIterable(list) // 数据已经存在
.map(item -> process(item))
.subscribe(result -> System.out.println(result));
但现实中有大量场景需要主动推送数据:
场景 1:外部事件触发
plain
// ❌ 问题:WebSocket 收到消息时,如何推送到响应式流?
@OnMessage
public void onMessage(String message) {
// 这里不是响应式上下文,如何通知订阅者?
}
场景 2:异步回调转换
plain
// ❌ 问题:第三方 SDK 使用回调,如何转成响应式流?
thirdPartySDK.getData(new Callback() {
@Override
public void onSuccess(String data) {
// 如何将这个回调结果变成 Flux/Mono?
}
});
场景 3:多生产者单消费者
plain
// ❌ 问题:多个线程同时产生数据,如何汇聚成一个流?
thread1.produceData();
thread2.produceData();
thread3.produceData();
// 如何让一个订阅者统一接收?
Sink 就是解决这些问题的桥梁!
3.1.3、Sink 的核心价值
3.1.3.1、桥接命令式与响应式代码
plain
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Flux;
public class BridgeExample {
// 创建 Sink(命令式入口)
private final Sinks.Many<String> sink = Sinks.many()
.multicast()
.onBackpressureBuffer();
// 获取响应式流(响应式出口)
public Flux<String> getDataStream() {
return sink.asFlux();
}
// 命令式方法:随时可以调用
public void pushData(String data) {
sink.tryEmitNext(data); // 主动推送
}
}
使用方式:
plain
BridgeExample example = new BridgeExample();
// 响应式订阅
example.getDataStream()
.subscribe(data -> System.out.println("收到: " + data));
// 命令式推送(可以在任何地方调用)
example.pushData("消息1");
example.pushData("消息2");
3.1.3.2、解耦生产者和消费者
plain
import reactor.core.publisher.Sinks;
import java.util.ArrayList;
import java.util.List;
public class DecouplingExample {
public static void main(String[] args) throws InterruptedException {
Sinks.Many<String> sink = Sinks.many()
.replay()
.all();
// === 生产者(完全不知道谁在消费)===
new Thread(() -> {
for (int i = 1; i <= 3; i++) {
sink.tryEmitNext("[A] 数据-" + i);
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}).start();
new Thread(() -> {
for (int i = 1; i <= 3; i++) {
sink.tryEmitNext("[B] 数据-" + i);
try { Thread.sleep(150); } catch (InterruptedException e) {}
}
}).start();
Thread.sleep(200);
// === 消费者(完全不知道谁在生产)===
System.out.println("--- 消费者1 开始订阅 ---");
sink.asFlux().subscribe(data ->
System.out.println("[消费者1] " + data)
);
Thread.sleep(500);
System.out.println("\n--- 消费者2 开始订阅 ---");
sink.asFlux().subscribe(data ->
System.out.println("[消费者2] " + data)
);
Thread.sleep(1000);
}
}
优势:
- ✅ 生产者不需要知道有几个消费者
- ✅ 消费者可以随时加入/退出
- ✅ 支持动态扩展
3.1.3.3、实现广播和多播
plain
import reactor.core.publisher.Sinks;
public class BroadcastExample {
public static void main(String[] args) throws InterruptedException {
Sinks.Many<String> sink = Sinks.many()
.multicast()
.onBackpressureBuffer();
var flux = sink.asFlux();
// 三个订阅者同时接收相同数据
flux.subscribe(data -> System.out.println("[邮件通知] " + data));
flux.subscribe(data -> System.out.println("[短信通知] " + data));
flux.subscribe(data -> System.out.println("[APP推送] " + data));
// 一次发射,三方接收
sink.tryEmitNext("订单已发货");
sink.tryEmitNext("预计明天送达");
sink.tryEmitComplete();
Thread.sleep(500);
}
}
3.1.3.4、处理异步回调
plain
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Mono;
public class CallbackToReactiveExample {
/**
* 将回调风格的 API 转换为响应式 Mono
*/
public Mono<String> fetchDataAsync() {
Sinks.One<String> sink = Sinks.one();
// 模拟异步回调(如 HTTP 请求、数据库查询)
simulateAsyncOperation(new AsyncCallback() {
@Override
public void onSuccess(String result) {
sink.tryEmitValue(result); // 成功时发射值
}
@Override
public void onError(Exception e) {
sink.tryEmitError(e); // 失败时发射错误
}
});
return sink.asMono(); // 返回 Mono 供订阅
}
private void simulateAsyncOperation(AsyncCallback callback) {
new Thread(() -> {
try {
Thread.sleep(1000);
callback.onSuccess("数据加载完成");
} catch (Exception e) {
callback.onError(e);
}
}).start();
}
interface AsyncCallback {
void onSuccess(String result);
void onError(Exception e);
}
}
3.1.3.5、实现事件总线(Event Bus)
plain
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Flux;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class EventBus {
private final Map<String, Sinks.Many<Object>> topics = new ConcurrentHashMap<>();
/**
* 订阅某个主题
*/
public <T> Flux<T> subscribe(String topic, Class<T> type) {
Sinks.Many<Object> sink = topics.computeIfAbsent(topic,
t -> Sinks.many().replay().limit(10)
);
return sink.asFlux()
.filter(type::isInstance)
.cast(type);
}
/**
* 发布事件到某个主题
*/
public void publish(String topic, Object event) {
Sinks.Many<Object> sink = topics.get(topic);
if (sink != null) {
sink.tryEmitNext(event);
}
}
}
3.1.4、Sink vs 其他方案对比
| 方案 | 缺点 | Sink 的优势 |
|---|---|---|
BlockingQueue |
阻塞线程,不符合响应式理念 | 非阻塞,背压感知 |
CompletableFuture |
只能返回单次结果 | 支持流式多次发射 |
| 自定义观察者模式 | 需手动管理订阅、背压、线程安全 | 内置完整机制 |
Subject (RxJava) |
Reactor 不推荐使用 Subject | Reactor 官方推荐方案 |
3.1.5、实际应用场景总结
- WebSocket 实时通信:服务端收到消息 → 通过 Sink 广播给所有客户端
- SSE(Server-Sent Events):持续向浏览器推送事件
- 监控指标收集:多个服务实例上报指标 → 汇聚处理
- 日志聚合:分布式日志收集
- 进度通知:长时间任务进度推送
3.1.6、核心理解要点
Sink 的本质:
plain
┌─────────────────────────────────────┐
│ 命令式世界 │
│ (任意线程、任意时机调用) │
│ │
│ sink.tryEmitNext(data) ← 手动推送 │
│ │
└──────────────┬──────────────────────┘
│ Sink(桥梁)
▼
┌─────────────────────────────────────┐
│ 响应式世界 │
│ (背压感知、调度器管理、操作符链) │
│ │
│ sink.asFlux() / asMono() │
│ .map(...) │
│ .filter(...) │
│ .subscribe(...) │
└─────────────────────────────────────┘
为什么要用 Sink?
- 打破响应式的封闭性:允许从外部注入数据
- 统一管理数据流:多生产者 → 单 Sink → 多消费者
- 保持响应式特性:背压、线程调度、错误处理
- 简化复杂场景:广播、重放、缓冲等开箱即用
什么时候不用 Sink?
❌ 不需要 Sink 的场景:
plain
// 数据源本身就是响应式的,无需 Sink
Flux.fromDatabase(query)
.map(entity -> dto)
.subscribe();
// 简单的同步转换
Mono.just(data)
.map(this::process)
.subscribe();
✅ 需要 Sink 的场景:
- 从非响应式代码推送数据到响应式流
- 需要广播给多个订阅者
- 需要缓存历史数据供新订阅者回放
- 需要在运行时动态添加/移除数据源
3.1.6、一句话总结
Sink 是响应式世界的"手动挡" ------ 当你需要从外部主动控制数据流时,它就是连接命令式代码和响应式流的完美桥梁。
3.2、Sinks 完整分类体系
plain
Sinks
├── Sinks.One<T> // 单元素(类似 CompletableFuture)
│ └── emitOnce() // 只能发射一个值或错误
│
└── Sinks.Many<T> // 多元素(类似 Flux)
├── unicast() // 单订阅者
├── multicast() // 多订阅者(无历史回放)
└── replay() // 多订阅者(有历史回放)
3.2.1、Sinks.One
java
@Test
void sinksOneTest() {
Sinks.One<String> one = Sinks.one();
Mono<String> mono = one.asMono();
mono.subscribe(
v -> System.out.println("Subscriber 1 received: " + v),
e -> System.out.println("Subscriber 1 error: " + e),
() -> System.out.println("Subscriber 1: complete")
);
mono.subscribe(
v -> System.out.println("Subscriber 2 received: " + v),
e -> System.out.println("Subscriber 2 error: " + e),
() -> System.out.println("Subscriber 2: complete")
);
// 发送正常消息
// Sinks.EmitResult result1 = one.tryEmitValue("hello");
// System.out.println(result1);
//
// Sinks.EmitResult result2 = one.tryEmitValue("world");
// System.out.println(result2);
// 发送异常消息
Sinks.EmitResult result = one.tryEmitError(new RuntimeException("error"));
System.out.println(result);
}
3.2.2、Sinks.Many
3.2.2.1、Sinks.Many 三大策略完整对比
对比矩阵
| 特性 | unicast() |
multicast() |
replay() |
|---|---|---|---|
| 订阅者数量 | 仅 1 个 | 多个 | 多个 |
| 历史回放 | ❌ | ❌ | ✅ |
| 背压支持 | ✅ | ✅ | ✅ |
| 新订阅者接收旧数据 | N/A | ❌ | ✅ |
| 典型场景 | 任务队列 | 实时事件广播 | 聊天室/状态同步 |
| 内存占用 | 低 | 中 | 高(需缓存历史) |
3.2.2.1.1、案例 1:Unicast - 单订阅者队列
java
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Flux;
public class UnicastExample {
public static void main(String[] args) throws InterruptedException {
Sinks.Many<String> sink = Sinks.many()
.unicast()
.onBackpressureBuffer();
Flux<String> flux = sink.asFlux();
// ⚠️ 第一个订阅者
flux.subscribe(
data -> System.out.println("[订阅者A] 收到: " + data),
error -> System.err.println("错误: " + error),
() -> System.out.println("[订阅者A] 完成")
);
// 发射数据
sink.tryEmitNext("任务1");
sink.tryEmitNext("任务2");
// ⚠️ 尝试第二个订阅者(会抛出异常)
try {
flux.subscribe(data -> System.out.println("[订阅者B] 收到: " + data));
} catch (IllegalStateException e) {
System.err.println("❌ 不允许第二个订阅者: " + e.getMessage());
}
sink.tryEmitComplete();
Thread.sleep(500);
}
}
输出:
plain
[订阅者A] 收到: 任务1
[订阅者A] 收到: 任务2
❌ 不允许第二个订阅者: UnicastSink allows only one Subscriber
[订阅者A] 完成
适用场景:
- 后台任务队列
- 日志写入(单消费者)
- 数据管道(一对一传输)
3.2.2.1.2、案例 2:Multicast - 实时广播(无历史)
java
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Flux;
public class MulticastExample {
public static void main(String[] args) throws InterruptedException {
Sinks.Many<String> sink = Sinks.many()
.multicast()
.onBackpressureBuffer();
Flux<String> flux = sink.asFlux();
// 订阅者 1(立即订阅)
flux.subscribe(
data -> System.out.println("[用户A] 📩 " + data)
);
// 发射前两条消息
sink.tryEmitNext("欢迎加入聊天室");
sink.tryEmitNext("今天天气不错");
Thread.sleep(500);
// 订阅者 2(延迟订阅,收不到之前的消息)
System.out.println("\n--- 用户B 加入聊天室 ---\n");
flux.subscribe(
data -> System.out.println("[用户B] 📩 " + data)
);
// 继续发射
sink.tryEmitNext("大家下午好");
sink.tryEmitNext("有人在吗?");
sink.tryEmitComplete();
Thread.sleep(500);
}
}
输出:
plain
[用户A] 📩 欢迎加入聊天室
[用户A] 📩 今天天气不错
--- 用户B 加入聊天室 ---
[用户A] 📩 大家下午好
[用户B] 📩 大家下午好 ← B 从这里开始接收
[用户A] 📩 有人在吗?
[用户B] 📩 有人在吗?
适用场景:
- 实时股票行情推送
- 在线游戏状态同步
- 实时监控告警
3.2.2.1.1、案例 3:Replay - 带历史回放
1、Replay All(回放所有历史)
java
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Flux;
public class ReplayAllExample {
public static void main(String[] args) throws InterruptedException {
Sinks.Many<String> sink = Sinks.many()
.replay()
.all(); // 回放所有历史数据
Flux<String> flux = sink.asFlux();
// 先发射一些数据
sink.tryEmitNext("系统启动");
sink.tryEmitNext("加载配置");
sink.tryEmitNext("连接数据库");
Thread.sleep(500);
// 新订阅者能收到所有历史数据
System.out.println("--- 监控服务启动 ---");
flux.subscribe(
data -> System.out.println("[监控] 📊 " + data)
);
Thread.sleep(500);
// 继续发射新数据
sink.tryEmitNext("服务就绪");
sink.tryEmitComplete();
Thread.sleep(500);
}
}
输出:
plain
--- 监控服务启动 ---
[监控] 📊 系统启动 ← 回放历史
[监控] 📊 加载配置 ← 回放历史
[监控] 📊 连接数据库 ← 回放历史
[监控] 📊 服务就绪 ← 新数据
2、 Replay limit N(只回放最近 N 条)
java
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Flux;
public class ReplayLatestExample {
public static void main(String[] args) throws InterruptedException {
// 只保留最近 2 条数据
Sinks.Many<String> sink = Sinks.many()
.replay()
.limit(2);
Flux<String> flux = sink.asFlux();
// 发射 5 条数据
for (int i = 1; i <= 5; i++) {
sink.tryEmitNext("消息-" + i);
System.out.println("[生产者] 发射: 消息-" + i);
Thread.sleep(100);
}
Thread.sleep(500);
// 新订阅者只能收到最后 2 条
System.out.println("\n--- 新订阅者加入 ---");
flux.subscribe(
data -> System.out.println("[订阅者] 收到: " + data)
);
sink.tryEmitComplete();
Thread.sleep(500);
}
}
输出:
plain
[生产者] 发射: 消息-1
[生产者] 发射: 消息-2
[生产者] 发射: 消息-3
[生产者] 发射: 消息-4
[生产者] 发射: 消息-5
--- 新订阅者加入 ---
[订阅者] 收到: 消息-4 ← 只回放最近 2 条
[订阅者] 收到: 消息-5
适用场景:
- 聊天室(新成员查看历史消息)
- 配置中心(新服务获取最新配置)
- 状态同步(客户端重连后恢复状态)
3.2.2.2、背压策略对比
背压策略矩阵
| 策略 | 缓冲区满时行为 | 数据丢失风险 | 适用场景 |
|---|---|---|---|
onBackpressureBuffer() |
无限缓冲 | ❌ 无(但可能 OOM) | 数据不能丢 |
onBackpressureBuffer(n) |
缓冲 n | ⚠️ 中等 | 有限容错 |
3.2.2.2.1、案例 4:背压策略对比演示
java
public void testBufferStrategy() throws InterruptedException {
Sinks.Many<String> sink = Sinks.many()
.multicast()
.onBackpressureBuffer(3, true); // 最多缓冲 3 个
// 快速生产者
for (int i = 1; i <= 6; i++) {
String msg = "任务-" + i;
Sinks.EmitResult result = sink.tryEmitNext(msg);
System.out.println(" [发射] " + msg + " → " + result);
Thread.sleep(50);
}
Thread.sleep(1000);
var flux = sink.asFlux();
// 慢消费者
flux.publishOn(Schedulers.boundedElastic())
.subscribe(data -> {
try {
Thread.sleep(1000);
System.out.println(" [处理] " + data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
sink.tryEmitComplete();
Thread.sleep(7000);
}
3.3、选择决策树
plain
需要发射数据?
│
├─ 只发射一次?
│ └─ ✅ 使用 Sinks.One
│ ├─ 异步 RPC 结果
│ ├─ 数据库查询结果
│ └─ 单次计算结果
│
└─ 发射多次?
│
├─ 几个订阅者?
│ │
│ ├─ 只有 1 个
│ │ └─ ✅ Sinks.Many.unicast()
│ │ ├─ 任务队列
│ │ └─ 数据管道
│ │
│ └─ 多个订阅者
│ │
│ ├─ 需要历史数据?
│ │ │
│ │ ├─ 是 → ✅ Sinks.Many.replay()
│ │ │ ├─ 全部历史: .replay().all()
│ │ │ └─ 最近 1 条: .replay().latest()
│ │ │ └─ 最近 N 条: .replay().limit(N)
│ │ │
│ │ └─ 否 → ✅ Sinks.Many.multicast()
│ │ ├─ 实时行情
│ │ └─ 在线通知
│ │
│ └─ 背压如何处理?
│ ├─ 不能丢 → onBackpressureBuffer()
│ └─ 要最新 → onBackpressureBuffer(N)
四、左侧聊天界面
4.1、流式输出
- 配置模型、工具集
java
package com.example.plannotebook.config;
import io.agentscope.core.formatter.dashscope.DashScopeChatFormatter;
import io.agentscope.core.formatter.ollama.OllamaChatFormatter;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.OllamaChatModel;
import io.agentscope.core.model.ollama.OllamaOptions;
import io.agentscope.core.model.ollama.ThinkOption;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.file.ReadFileTool;
import io.agentscope.core.tool.file.WriteFileTool;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AiConfig {
@Bean
public OllamaChatModel ollamaChatModel() {
return OllamaChatModel.builder()
.modelName("qwen3.5:0.8b")
.defaultOptions(OllamaOptions.builder()
.thinkOption(ThinkOption.ThinkBoolean.ENABLED)
.build())
// 默认也是这样的
.formatter(new OllamaChatFormatter())
.build();
}
@Bean
public DashScopeChatModel dashScopeChatModel() {
return DashScopeChatModel.builder()
.stream(true)
.enableThinking(true)
.formatter(new DashScopeChatFormatter())
.apiKey(System.getProperty("DASHSCOPE_KEY"))
.modelName(System.getProperty("DASHSCOPE_MODEL_NAME"))
.baseUrl(System.getProperty("DASHSCOPE_BASE_URL"))
.defaultOptions(GenerateOptions.builder()
// 思考模式的token上线
.thinkingBudget(8196)
.build())
.build();
}
/**
* 通用 注册工具
*/
@Bean
public Toolkit toolkit() {
Toolkit toolkit = new Toolkit();
String workDir = "D:\\Desktop\\tmp";
toolkit.registerTool(new ReadFileTool(workDir));
toolkit.registerTool(new WriteFileTool(workDir));
return toolkit;
}
}
- 定义Agent
java
package com.example.plannotebook.service;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.EventType;
import io.agentscope.core.message.Msg;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.model.OllamaChatModel;
import io.agentscope.core.model.ollama.OllamaOptions;
import io.agentscope.core.model.ollama.ThinkOption;
import io.agentscope.core.tool.Toolkit;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class AgentService {
@Autowired
private OllamaChatModel ollamaChatModel;
@Autowired
private DashScopeChatModel dashScopeChatModel;
// 也是一种注入方式
@Setter(onMethod_ = @Autowired)
private Toolkit toolkit;
public Flux<String> chat(String message, String sessionId) {
ReActAgent agent = ReActAgent.builder()
.name("plan-notebook")
.model(dashScopeChatModel)
// 如果有自定义的工具可以在这获取到toolkit后 在补充注册工具
.toolkit(toolkit.copy())
.build();
return agent.stream(Msg.builder().textContent(message).build()).
map(event -> {
// 默认返回三词数据 第一个是流式输出、第二个是汇总的结果、第三个是结果
// 因为重复 只需要取一个就行
EventType type = event.getType();
if (type == EventType.REASONING && !event.isLast()) {
Msg msg = event.getMessage();
return msg.getTextContent();
}
return "";
});
}
}
- 测试接口返回
java
package com.example.plannotebook.controller;
import com.example.plannotebook.service.AgentService;
import com.example.plannotebook.service.PlanService;
import lombok.AllArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@AllArgsConstructor
@RequestMapping("/api")
@RestController
public class ChatController {
private final PlanService planService;
private final AgentService agentService;
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(String message,
@RequestParam(required = false, defaultValue = "default") String sessionId) {
return agentService.chat(message, sessionId);
}
}
4.2、ReactAgent基础参数
工具集 + PlanNoteBook + 最大迭代次数
- 设置通用工具集(工作目录优化)
java
package com.example.plannotebook.config;
import io.agentscope.core.formatter.dashscope.DashScopeChatFormatter;
import io.agentscope.core.formatter.ollama.OllamaChatFormatter;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.OllamaChatModel;
import io.agentscope.core.model.ollama.OllamaOptions;
import io.agentscope.core.model.ollama.ThinkOption;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.file.ReadFileTool;
import io.agentscope.core.tool.file.WriteFileTool;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@Configuration
public class AiConfig {
@Bean
public OllamaChatModel ollamaChatModel() {
return OllamaChatModel.builder()
.modelName("qwen3.5:0.8b")
.defaultOptions(OllamaOptions.builder()
.thinkOption(ThinkOption.ThinkBoolean.ENABLED)
.build())
// 默认也是这样的
.formatter(new OllamaChatFormatter())
.build();
}
@Bean
public DashScopeChatModel dashScopeChatModel() {
return DashScopeChatModel.builder()
.stream(true)
.enableThinking(true)
.formatter(new DashScopeChatFormatter())
.apiKey(System.getProperty("DASHSCOPE_KEY"))
.modelName(System.getProperty("DASHSCOPE_MODEL_NAME"))
.baseUrl(System.getProperty("DASHSCOPE_BASE_URL"))
.defaultOptions(GenerateOptions.builder()
// 思考模式的token上线
.thinkingBudget(8196)
.build())
.build();
}
/**
* 通用 注册工具
*/
@Bean
public Toolkit toolkit() {
Toolkit toolkit = new Toolkit();
String workDir = resolveWorkspaceRoot();
toolkit.registerTool(new ReadFileTool(workDir));
toolkit.registerTool(new WriteFileTool(workDir));
return toolkit;
}
private String resolveWorkspaceRoot() {
// 获取操作系统变量
String workspace = System.getenv("PLAN_NOTEBOOK_WORKSPACE");
Path root = workspace != null && !workspace.isBlank()
? Path.of(workspace).toAbsolutePath().normalize()
// "C:\Users\Administrator\.agentscope\plan-notebook\workspace"
: Path.of(System.getProperty("user.home"), ".agentscope", "plan-notebook", "workspace").toAbsolutePath().normalize();
try {
Files.createDirectories(root);
} catch (IOException e) {
throw new RuntimeException("Failed to create workspace directory " + root, e);
}
return root.toString();
}
}
- planservice补充plannotebook
java
package com.example.plannotebook.service;
import io.agentscope.core.plan.PlanNotebook;
import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Sinks;
@Service
public class PlanService {
@Getter
@Setter
private PlanNotebook planNotebook;
// 创建一个Sinks对象, many: 发送多个元素, multicast: 可以有多个订阅者. onBackpressureBuffer: 背压处理, 生产者处理不过来可以先存入缓存
@Getter
private final Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();
}
- agent参数设置
java
package com.example.plannotebook.service;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.EventType;
import io.agentscope.core.agent.StreamOptions;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.message.Msg;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.model.OllamaChatModel;
import io.agentscope.core.tool.Toolkit;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
@Service
public class AgentService {
@Autowired
private OllamaChatModel ollamaChatModel;
@Autowired
private DashScopeChatModel dashScopeChatModel;
// 也是一种注入方式
@Setter(onMethod_ = @Autowired)
private Toolkit toolkit;
@Autowired
private PlanService planService;
public Flux<String> chat(String message, String sessionId) {
ReActAgent agent = ReActAgent.builder()
.name("plan-notebook")
.model(dashScopeChatModel)
// 如果有自定义的工具可以在这获取到toolkit后 在补充注册工具
.toolkit(toolkit.copy())
.memory(new InMemoryMemory())
.planNotebook(planService.getPlanNotebook())
.maxIters(50)
.build();
return agent.stream(Msg.builder().textContent(message).build(),
getStreamOptions())
// 不阻塞主线程
.subscribeOn(Schedulers.boundedElastic())
.map(event -> {
// 默认返回三词数据 第一个是流式输出、第二个是汇总的结果、第三个是结果
// 因为重复 只需要取一个就行
EventType type = event.getType();
if (type == EventType.REASONING && !event.isLast()) {
Msg msg = event.getMessage();
return msg.getTextContent();
}
return "";
});
}
private StreamOptions getStreamOptions() {
return StreamOptions.builder()
.eventTypes(EventType.REASONING, EventType.TOOL_RESULT, EventType.AGENT_RESULT)
// 只发新增内容
.incremental(true)
// 隐藏执行过程
.includeActingChunk(false)
.build();
}
}
4.3、添加Session并返回前端规定格式
java
package com.example.plannotebook.service;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.EventType;
import io.agentscope.core.agent.StreamOptions;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.ThinkingBlock;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.model.OllamaChatModel;
import io.agentscope.core.session.InMemorySession;
import io.agentscope.core.session.Session;
import io.agentscope.core.tool.Toolkit;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import tools.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class AgentService {
@Autowired
private OllamaChatModel ollamaChatModel;
@Autowired
private DashScopeChatModel dashScopeChatModel;
// 也是一种注入方式
@Setter(onMethod_ = @Autowired)
private Toolkit toolkit;
@Autowired
private PlanService planService;
private static final Session session = new InMemorySession();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public Flux<String> chat(String message, String sessionId) {
ReActAgent agent = ReActAgent.builder()
.sysPrompt("""
你是一位专业的系统化任务规划与执行助手,擅长处理复杂的多步骤任务。
核心能力:
1. **结构化规划**: 接收任务后,先进行需求分析,制定清晰的执行路线图
2. **任务分解**: 将复杂目标拆解为逻辑清晰、可执行的子任务序列
3. **渐进式执行**: 按照优先级和依赖关系逐步推进,每步都验证结果
4. **动态调整**: 根据实际情况灵活调整计划,确保最终目标达成
5. **质量把控**: 在每个关键节点进行检查,保证输出质量
工作方式:
- 主动澄清模糊需求,确保理解准确
- 提供透明的进度反馈和决策依据
- 在必要时提出优化建议或替代方案
- 保持与用户的持续沟通和对齐
""")
.name("plan-notebook")
.model(dashScopeChatModel)
// 如果有自定义的工具可以在这获取到toolkit后 在补充注册工具
.toolkit(toolkit.copy())
.memory(new InMemoryMemory())
.planNotebook(planService.getPlanNotebook())
.maxIters(50)
.build();
agent.loadIfExists(session, sessionId);
return agent.stream(Msg.builder().textContent(message).build(),
getStreamOptions())
// 不阻塞主线程
.subscribeOn(Schedulers.boundedElastic())
.map(event -> {
// 默认返回三词数据 第一个是流式输出、第二个是汇总的结果、第三个是结果
// 因为重复 只需要取一个就行
EventType type = event.getType();
if (type == EventType.REASONING && !event.isLast()) {
Msg msg = event.getMessage();
List<ThinkingBlock> thinkingBlocks = msg.getContentBlocks(ThinkingBlock.class);
if (!thinkingBlocks.isEmpty()) {
String think = thinkingBlocks.stream()
.map(ThinkingBlock::getThinking)
.collect(Collectors.joining());
// 前端规定格式: t表示类型, d表示内容
return OBJECT_MAPPER.writeValueAsString(Map.of("t", "think", "d", think));
}
return OBJECT_MAPPER.writeValueAsString(Map.of("t", "text", "d", msg.getTextContent()));
}
return "";
})
.doFinally(signalType -> agent.saveTo(session, sessionId));
}
private StreamOptions getStreamOptions() {
return StreamOptions.builder()
.eventTypes(EventType.REASONING, EventType.TOOL_RESULT, EventType.AGENT_RESULT)
// 只发新增内容
.incremental(true)
// 隐藏执行过程
.includeActingChunk(false)
.build();
}
}
4.4、处理header及优化代码结构
- header格式优化工具
java
package com.example.plannotebook.util;
import io.agentscope.core.message.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class FormatOutputUtil {
private static final int CTX_FIELD_MAX_CHARS = 8_000;
/**
* 将消息列表转换为调试行格式用于 SSE 上下文。
*
* @param msgs 要转换的消息列表
* @return 包含角色、名称(可选)和内容块的 Map 列表
*/
public static List<Map<String, Object>> msgToDebugRows(List<Msg> msgs) {
// 如果消息列表为空,则返回一个空列表
if (msgs == null || msgs.isEmpty()) {
return List.of();
}
return msgs.stream().map(FormatOutputUtil::msgToDebugRow).toList();
}
/**
* 将消息转换为调试行格式用于 SSE 上下文。
*
* @param m 要转换的消息
* @return 包含角色、名称(可选)和内容块的 Map
*/
public static Map<String, Object> msgToDebugRow(Msg m) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("role", m.getRole().name());
if (m.getName() != null) {
row.put("name", m.getName());
}
// 将每个内容块转换为调试 Map
List<Object> parts = new ArrayList<>();
for (ContentBlock b : m.getContent()) {
parts.add(contentBlockToDebugMap(b));
}
row.put("content", parts);
return row;
}
/**
* 将内容块转换为调试 Map 格式。
* 处理 TextBlock、ThinkingBlock、ToolUseBlock 和 ToolResultBlock。
*
* @param b 要转换的内容块
* @return 包含类型特定字段的 Map 表示
*/
private static Object contentBlockToDebugMap(ContentBlock b) {
if (b instanceof TextBlock tb) {
return Map.of(
"type",
"text",
"text",
truncateIfNeeded(
tb.getText() != null ? tb.getText() : "", CTX_FIELD_MAX_CHARS));
}
if (b instanceof ThinkingBlock th) {
return Map.of(
"type",
"thinking",
"thinking",
truncateIfNeeded(
th.getThinking() != null ? th.getThinking() : "", CTX_FIELD_MAX_CHARS));
}
if (b instanceof ToolUseBlock tu) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("type", "tool_use");
m.put("id", tu.getId());
m.put("name", tu.getName());
m.put("input", tu.getInput() != null ? tu.getInput() : Map.of());
String raw = tu.getContent();
if (raw != null && !raw.isEmpty()) {
m.put("raw", truncateIfNeeded(raw, CTX_FIELD_MAX_CHARS));
}
return m;
}
if (b instanceof ToolResultBlock tr) {
return Map.of(
"type",
"tool_result",
"name",
tr.getName() != null ? tr.getName() : "",
"output",
truncateIfNeeded(flattenToolOutput(tr), CTX_FIELD_MAX_CHARS));
}
return Map.of(
"type", "other", "repr", truncateIfNeeded(String.valueOf(b), CTX_FIELD_MAX_CHARS));
}
/**
* 从消息列表构建扁平化的文本转录本。
* 用于上下文调试和可视化。
*
* @param messages 要转换的消息列表
* @return 格式化后的转录本字符串
*/
public static String buildFlatTranscript(List<Msg> messages) {
StringBuilder sb = new StringBuilder();
for (Msg m : messages) {
sb.append("--- ").append(m.getRole().name());
if (m.getName() != null) {
sb.append(" (").append(m.getName()).append(')');
}
sb.append(" ---\n");
for (ContentBlock b : m.getContent()) {
appendContentBlockForFlat(sb, b);
}
sb.append('\n');
}
return sb.toString();
}
/**
* 将内容块追加到扁平化转录本构建器。
* 适当地格式化不同类型的内容块。
*
* @param sb 要追加的 StringBuilder
* @param o 要追加的内容块
*/
private static void appendContentBlockForFlat(StringBuilder sb, ContentBlock o) {
if (o instanceof TextBlock tb) {
sb.append(tb.getText() != null ? tb.getText() : "");
} else if (o instanceof ThinkingBlock th) {
sb.append(th.getThinking() != null ? th.getThinking() : "");
} else if (o instanceof ToolUseBlock tu) {
sb.append("[tool_use ")
.append(tu.getName())
.append(" id=")
.append(tu.getId())
.append("] ");
sb.append(tu.getInput() != null ? tu.getInput().toString() : "{}");
String raw = tu.getContent();
if (raw != null && !raw.isEmpty()) {
sb.append(" raw=").append(raw);
}
sb.append('\n');
} else if (o instanceof ToolResultBlock tr) {
sb.append("[tool_result ").append(tr.getName()).append("]\n");
sb.append(flattenToolOutput(tr));
sb.append('\n');
} else {
sb.append(o);
}
}
/**
* 如果字符串超过最大长度则进行截断。
*
* @param s 要截断的字符串
* @param max 允许的最大长度
* @return 原始字符串或带有截断指示器的截断后字符串
*/
public static String truncateIfNeeded(String s, int max) {
if (s == null) {
return "";
}
if (s.length() <= max) {
return s;
}
return s.substring(0, max) + "\n...(truncated)";
}
/**
* 将工具输出块扁平化为单个字符串。
* 递归处理嵌套的内容块。
*
* @param block 要扁平化的工具结果块
* @return 扁平化的输出字符串
*/
public static String flattenToolOutput(ToolResultBlock block) {
StringBuilder sb = new StringBuilder();
for (ContentBlock o : block.getOutput()) {
appendContentBlock(sb, o);
}
return sb.toString();
}
/**
* 将内容块追加到 StringBuilder。
* 递归处理 TextBlock、ThinkingBlock 和 ToolResultBlock。
*
* @param sb 要追加的 StringBuilder
* @param o 要追加的内容块
*/
private static void appendContentBlock(StringBuilder sb, ContentBlock o) {
if (o instanceof TextBlock tb) {
sb.append(tb.getText());
} else if (o instanceof ThinkingBlock th) {
sb.append(th.getThinking());
} else if (o instanceof ToolResultBlock tr) {
sb.append(flattenToolOutput(tr));
} else {
sb.append(o);
}
}
}
- 拦截推理前和摘要前的hook
java
package com.example.plannotebook.hook;
import io.agentscope.core.hook.Hook;
import io.agentscope.core.hook.HookEvent;
import io.agentscope.core.hook.PreReasoningEvent;
import io.agentscope.core.hook.PreSummaryEvent;
import io.agentscope.core.message.Msg;
import org.apache.logging.log4j.util.TriConsumer;
import reactor.core.publisher.Mono;
import java.util.List;
public class PromptCaptureHook implements Hook {
// 阶段, 模型名称, 内容
private final TriConsumer<String, String, List<Msg>> consumer;
public PromptCaptureHook(TriConsumer<String, String, List<Msg>> consumer) {
this.consumer = consumer;
}
/**
* 把思考前、摘要前的内容 添加consumer中
* @param event
* @param <T>
* @return
*/
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (event instanceof PreReasoningEvent e) {
// 推理前拦截
consumer.accept("reasoning", e.getModelName(), e.getInputMessages());
} else if (event instanceof PreSummaryEvent e) {
// 摘要前拦截
consumer.accept("summary", e.getModelName(), e.getInputMessages());
}
return Mono.just(event);
}
@Override
public int priority() {
return 1000;
}
}
- 处理返回结果
java
package com.example.plannotebook.service;
import com.example.plannotebook.hook.PromptCaptureHook;
import com.example.plannotebook.util.FormatOutputUtil;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.Event;
import io.agentscope.core.agent.EventType;
import io.agentscope.core.agent.StreamOptions;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.ThinkingBlock;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.model.OllamaChatModel;
import io.agentscope.core.session.InMemorySession;
import io.agentscope.core.session.Session;
import io.agentscope.core.tool.Toolkit;
import lombok.Setter;
import org.jspecify.annotations.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import tools.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Service
public class AgentService {
@Autowired
private OllamaChatModel ollamaChatModel;
@Autowired
private DashScopeChatModel dashScopeChatModel;
// 也是一种注入方式
@Setter(onMethod_ = @Autowired)
private Toolkit toolkit;
@Autowired
private PlanService planService;
private static final Session session = new InMemorySession();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final AtomicInteger contextSeq = new AtomicInteger(0);
private final ConcurrentLinkedQueue<String> contextLines = new ConcurrentLinkedQueue<>();
private static final int CTX_FLAT_MAX_CHARS = 48_000;
public Flux<String> chat(String message, String sessionId) {
ReActAgent agent = getReActAgent(sessionId);
return agent.stream(Msg.builder().textContent(message).build(),
getStreamOptions())
// 不阻塞主线程
.subscribeOn(Schedulers.boundedElastic())
.concatMap(this::concatContextLines)
.doFinally(signalType -> agent.saveTo(session, sessionId));
}
private @NonNull ReActAgent getReActAgent(String sessionId) {
ReActAgent agent = ReActAgent.builder()
.sysPrompt("""
你是一位专业的系统化任务规划与执行助手,擅长处理复杂的多步骤任务。
核心能力:
1. **结构化规划**: 接收任务后,先进行需求分析,制定清晰的执行路线图
2. **任务分解**: 将复杂目标拆解为逻辑清晰、可执行的子任务序列
3. **渐进式执行**: 按照优先级和依赖关系逐步推进,每步都验证结果
4. **动态调整**: 根据实际情况灵活调整计划,确保最终目标达成
5. **质量把控**: 在每个关键节点进行检查,保证输出质量
工作方式:
- 主动澄清模糊需求,确保理解准确
- 提供透明的进度反馈和决策依据
- 在必要时提出优化建议或替代方案
- 保持与用户的持续沟通和对齐
""")
.name("plan-notebook")
.model(dashScopeChatModel)
// 如果有自定义的工具可以在这获取到toolkit后 在补充注册工具
.toolkit(toolkit.copy())
.memory(new InMemoryMemory())
.planNotebook(planService.getPlanNotebook())
.maxIters(50)
.hook(new PromptCaptureHook(this::offerContextLines))
.build();
agent.loadIfExists(session, sessionId);
return agent;
}
/**
* 拼接流式输出结果 + header头信息
* @param event
* @return
*/
private @NonNull Flux<String> concatContextLines(Event event) {
// 获取header 然后先发送header 在发送原来的数据
List<String> headers = pollContextLines();
Flux<String> headerFlux = Flux.fromIterable(headers);
// 流式输出返回结果 转换成String
String body = convertEventToString(event);
if (headers.isEmpty() && body.isEmpty()) {
return Flux.empty();
}
if (body.isEmpty()) {
return headerFlux;
}
// 先发header, 然后在返回s
return headerFlux.concatWith(Flux.just(body));
}
private @NonNull List<String> pollContextLines() {
String poll;
List<String> headers = new ArrayList<>();
while ((poll = contextLines.poll()) != null) {
headers.add(poll);
}
return headers;
}
private static String convertEventToString(Event event) {
// 默认返回三词数据 第一个是流式输出、第二个是汇总的结果、第三个是结果
// 因为重复 只需要取一个就行
EventType type = event.getType();
if (type == EventType.REASONING && !event.isLast()) {
Msg msg = event.getMessage();
List<ThinkingBlock> thinkingBlocks = msg.getContentBlocks(ThinkingBlock.class);
if (!thinkingBlocks.isEmpty()) {
String think = thinkingBlocks.stream()
.map(ThinkingBlock::getThinking)
.collect(Collectors.joining());
// 前端规定格式: t表示类型, d表示内容
return OBJECT_MAPPER.writeValueAsString(Map.of("t", "think", "d", think));
}
String textContent = msg.getTextContent();
if (textContent == null || textContent.isEmpty()) {
return "";
}
return OBJECT_MAPPER.writeValueAsString(Map.of("t", "text", "d", textContent));
}
return "";
}
private void offerContextLines(String phase, String modelName, List<Msg> messages) {
int seq = contextSeq.incrementAndGet();
Map<String, Object> root = new LinkedHashMap<>();
root.put("t", "ctx");
root.put("phase", phase);
root.put("seq", seq);
root.put("model", modelName);
root.put("messages", FormatOutputUtil.msgToDebugRows(messages));
root.put("flat", FormatOutputUtil.truncateIfNeeded(
FormatOutputUtil.buildFlatTranscript(messages), CTX_FLAT_MAX_CHARS
));
contextLines.add(OBJECT_MAPPER.writeValueAsString(root));
}
private StreamOptions getStreamOptions() {
return StreamOptions.builder()
.eventTypes(EventType.REASONING, EventType.TOOL_RESULT, EventType.AGENT_RESULT)
// 只发新增内容
.incremental(true)
// 隐藏执行过程
.includeActingChunk(false)
.build();
}
}
五、当前计划
5.1、请求响应DTO
- 计划请求
java
package com.example.plannotebook.dto;
import java.util.List;
import java.util.Map;
public record PlanRequest(
String name,
String description,
String expectedOutcome,
List<Map<String, Object>> subtasks
) {
}
- 计划响应
java
package cn.da.shuai.agentscope.learn.plannotebook.dto;
import io.agentscope.core.plan.model.Plan;
import java.util.ArrayList;
import java.util.List;
/**
* DTO for plan response.
*/
public class PlanResponse {
private String id;
private String name;
private String description;
private String expectedOutcome;
private String state;
private String createdAt;
private List<SubTaskResponse> subtasks;
public PlanResponse() {
this.subtasks = new ArrayList<>();
}
public static PlanResponse fromPlan(Plan plan) {
if (plan == null) {
return null;
}
PlanResponse response = new PlanResponse();
response.setId(plan.getId());
response.setName(plan.getName());
response.setDescription(plan.getDescription());
response.setExpectedOutcome(plan.getExpectedOutcome());
response.setState(plan.getState().getValue());
response.setCreatedAt(plan.getCreatedAt());
List<SubTaskResponse> subtaskResponses = new ArrayList<>();
if (plan.getSubtasks() != null) {
for (int i = 0; i < plan.getSubtasks().size(); i++) {
subtaskResponses.add(SubTaskResponse.fromSubTask(plan.getSubtasks().get(i), i));
}
}
response.setSubtasks(subtaskResponses);
return response;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getExpectedOutcome() {
return expectedOutcome;
}
public void setExpectedOutcome(String expectedOutcome) {
this.expectedOutcome = expectedOutcome;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public List<SubTaskResponse> getSubtasks() {
return subtasks;
}
public void setSubtasks(List<SubTaskResponse> subtasks) {
this.subtasks = subtasks;
}
}
- 子计划请求
java
package cn.da.shuai.agentscope.learn.plannotebook.dto;
/**
* DTO for subtask creation/update request.
*/
public class SubTaskRequest {
private String name;
private String description;
private String expectedOutcome;
public SubTaskRequest() {}
public SubTaskRequest(String name, String description, String expectedOutcome) {
this.name = name;
this.description = description;
this.expectedOutcome = expectedOutcome;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getExpectedOutcome() {
return expectedOutcome;
}
public void setExpectedOutcome(String expectedOutcome) {
this.expectedOutcome = expectedOutcome;
}
}
- 子计划响应
java
package cn.da.shuai.agentscope.learn.plannotebook.dto;
import io.agentscope.core.plan.model.SubTask;
/**
* 子任务响应 DTO。
* 用于 API 响应中表示子任务的状态和信息。
*/
public class SubTaskResponse {
/** 子任务在计划中的索引位置 */
private int index;
/** 子任务名称 */
private String name;
/** 子任务描述 */
private String description;
/** 期望的产出结果 */
private String expectedOutcome;
/** 子任务状态(todo, in_progress, done, abandoned) */
private String state;
/** 实际产出结果 */
private String outcome;
/** 创建时间 */
private String createdAt;
/**
* 默认构造函数,用于 JSON 反序列化。
*/
public SubTaskResponse() {}
/**
* 从 SubTask 领域对象转换为 SubTaskResponse DTO。
*
* @param subtask 子任务对象
* @param index 子任务在计划中的索引
* @return SubTaskResponse DTO 实例
*/
public static SubTaskResponse fromSubTask(SubTask subtask, int index) {
SubTaskResponse response = new SubTaskResponse();
// 设置子任务的各个属性
response.setIndex(index);
response.setName(subtask.getName());
response.setDescription(subtask.getDescription());
response.setExpectedOutcome(subtask.getExpectedOutcome());
response.setState(subtask.getState().getValue());
response.setOutcome(subtask.getOutcome());
response.setCreatedAt(subtask.getCreatedAt());
return response;
}
/**
* 获取子任务索引。
*
* @return 子任务在计划中的位置
*/
public int getIndex() {
return index;
}
/**
* 设置子任务索引。
*
* @param index 子任务在计划中的位置
*/
public void setIndex(int index) {
this.index = index;
}
/**
* 获取子任务名称。
*
* @return 子任务名称
*/
public String getName() {
return name;
}
/**
* 设置子任务名称。
*
* @param name 子任务名称
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取子任务描述。
*
* @return 子任务描述
*/
public String getDescription() {
return description;
}
/**
* 设置子任务描述。
*
* @param description 子任务描述
*/
public void setDescription(String description) {
this.description = description;
}
/**
* 获取期望产出。
*
* @return 期望的产出结果
*/
public String getExpectedOutcome() {
return expectedOutcome;
}
/**
* 设置期望产出。
*
* @param expectedOutcome 期望的产出结果
*/
public void setExpectedOutcome(String expectedOutcome) {
this.expectedOutcome = expectedOutcome;
}
/**
* 获取子任务状态。
*
* @return 子任务状态(todo, in_progress, done, abandoned)
*/
public String getState() {
return state;
}
/**
* 设置子任务状态。
*
* @param state 子任务状态
*/
public void setState(String state) {
this.state = state;
}
/**
* 获取实际产出。
*
* @return 子任务的实际产出结果
*/
public String getOutcome() {
return outcome;
}
/**
* 设置实际产出。
*
* @param outcome 子任务的实际产出结果
*/
public void setOutcome(String outcome) {
this.outcome = outcome;
}
/**
* 获取创建时间。
*
* @return ISO 格式的时间戳
*/
public String getCreatedAt() {
return createdAt;
}
/**
* 设置创建时间。
*
* @param createdAt ISO 格式的时间戳
*/
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
}
5.2、主计划
5.2.1、获取当前计划
- service
java
package com.example.plannotebook.service;
import com.example.plannotebook.dto.PlanResponse;
import io.agentscope.core.plan.PlanNotebook;
import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
@Service
public class PlanService {
@Getter
@Setter
private PlanNotebook planNotebook;
// 创建一个Sinks对象, many: 发送多个元素, multicast: 可以有多个订阅者. onBackpressureBuffer: 背压处理, 生产者处理不过来可以先存入缓存
@Getter
private final Sinks.Many<PlanResponse> sink = Sinks.many().multicast().onBackpressureBuffer();
public PlanResponse getCurrentPlan() {
if (planNotebook == null) {
return new PlanResponse();
}
return PlanResponse.fromPlan(planNotebook.getCurrentPlan());
}
public Flux<PlanResponse> getCurrentPlanStream() {
return Flux.concat(Flux.just(getCurrentPlan()), sink.asFlux());
}
}
- controller
java
package com.example.plannotebook.controller;
import com.example.plannotebook.dto.PlanResponse;
import com.example.plannotebook.service.PlanService;
import lombok.AllArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@AllArgsConstructor
@RequestMapping("/api/plan")
public class PlanController {
private final PlanService planService;
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<PlanResponse> getPlanStream() {
return planService.getCurrentPlanStream();
}
}
5.2.2、推送计划
- 当planNoteBook发生改变的时候推送消息
java
private @NonNull ReActAgent getReActAgent(String sessionId) {
PlanNotebook planNotebook = PlanNotebook.builder().build();
// 当planNotebook发生变化 则调用该方法
planNotebook.addChangeHook("planServiceBroadcastChange",
(planNotebook1, plan) -> planService.broadcastPlanChant());
planService.setPlanNotebook(planNotebook);
ReActAgent agent = ReActAgent.builder()
.sysPrompt("""
你是一位专业的系统化任务规划与执行助手,擅长处理复杂的多步骤任务。
核心能力:
1. **结构化规划**: 接收任务后,先进行需求分析,制定清晰的执行路线图
2. **任务分解**: 将复杂目标拆解为逻辑清晰、可执行的子任务序列
3. **渐进式执行**: 按照优先级和依赖关系逐步推进,每步都验证结果
4. **动态调整**: 根据实际情况灵活调整计划,确保最终目标达成
5. **质量把控**: 在每个关键节点进行检查,保证输出质量
工作方式:
- 主动澄清模糊需求,确保理解准确
- 提供透明的进度反馈和决策依据
- 在必要时提出优化建议或替代方案
- 保持与用户的持续沟通和对齐
""")
.name("plan-notebook")
.model(dashScopeChatModel)
// 如果有自定义的工具可以在这获取到toolkit后 在补充注册工具
.toolkit(toolkit.copy())
.memory(new InMemoryMemory())
.planNotebook(planNotebook)
.maxIters(50)
.hook(new PromptCaptureHook(this::offerContextLines))
.build();
agent.loadIfExists(session, sessionId);
return agent;
}
- 具体推送方法
java
package com.example.plannotebook.service;
import com.example.plannotebook.dto.PlanResponse;
import io.agentscope.core.plan.PlanNotebook;
import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
@Service
public class PlanService {
@Getter
@Setter
private PlanNotebook planNotebook;
// 创建一个Sinks对象, many: 发送多个元素, multicast: 可以有多个订阅者. onBackpressureBuffer: 背压处理, 生产者处理不过来可以先存入缓存
@Getter
private final Sinks.Many<PlanResponse> sink = Sinks.many().multicast().onBackpressureBuffer();
public PlanResponse getCurrentPlan() {
if (planNotebook == null) {
return new PlanResponse();
}
return PlanResponse.fromPlan(planNotebook.getCurrentPlan());
}
public Flux<PlanResponse> getCurrentPlanStream() {
// 返回sink.asFlux是为了可以进行sse推送
return Flux.concat(Flux.just(getCurrentPlan()), sink.asFlux());
}
public void broadcastPlanChant() {
// 广播(推送) 当前计划
sink.tryEmitNext(getCurrentPlan());
}
}
5.2.3、创建计划
- 创建计划service
java
package com.example.plannotebook.service;
import com.example.plannotebook.dto.PlanRequest;
import com.example.plannotebook.dto.PlanResponse;
import io.agentscope.core.plan.PlanNotebook;
import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
@Service
public class PlanService {
@Getter
@Setter
private PlanNotebook planNotebook;
// 创建一个Sinks对象, many: 发送多个元素, multicast: 可以有多个订阅者. onBackpressureBuffer: 背压处理, 生产者处理不过来可以先存入缓存
@Getter
private final Sinks.Many<PlanResponse> sink = Sinks.many().multicast().onBackpressureBuffer();
public PlanResponse getCurrentPlan() {
if (planNotebook == null || planNotebook.getCurrentPlan() == null) {
return new PlanResponse();
}
return PlanResponse.fromPlan(planNotebook.getCurrentPlan());
}
public Flux<PlanResponse> getCurrentPlanStream() {
// 返回sink.asFlux是为了可以进行sse推送
return Flux.concat(Flux.just(getCurrentPlan()), sink.asFlux());
}
public void broadcastPlanChant() {
// 广播(推送) 当前计划
sink.tryEmitNext(getCurrentPlan());
}
public Mono<String> createPlan(PlanRequest request) {
// 解决没有聊天直接创建子任务 报planNotebook为null 方式一:
// String sessionId = request.sessionId();
// if (sessionId == null) {
// sessionId = "default";
// }
//
// if (planNotebook == null) {
// planNotebook = PlanNotebook.builder().build();
// }
//
// Mono<String> plan = planNotebook.createPlan(
// request.name(),
// request.description(),
// request.expectedOutcome(),
// request.subtasks()
// );
// planNotebook.saveTo(AgentService.session, sessionId);
// return plan;
return planNotebook.createPlan(
request.name(),
request.description(),
request.expectedOutcome(),
request.subtasks()
);
}
}
- 创建计划接口
java
package com.example.plannotebook.controller;
import com.example.plannotebook.dto.PlanRequest;
import com.example.plannotebook.dto.PlanResponse;
import com.example.plannotebook.service.PlanService;
import lombok.AllArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@AllArgsConstructor
@RequestMapping("/api/plan")
public class PlanController {
private final PlanService planService;
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<PlanResponse> getPlanStream() {
return planService.getCurrentPlanStream();
}
@GetMapping
public PlanResponse getCurrentPlan() {
return planService.getCurrentPlan();
}
@PostMapping
public Mono<String> createPlan(@RequestBody PlanRequest request) {
if (request == null) {
return Mono.just("请求体不能为空");
}
if (request.name() == null || request.name().isBlank()) {
return Mono.just("计划名称不能为空");
}
return planService.createPlan(request);
}
}
- 解决不聊天直接创建plannotebook为空的错误
java
package com.example.plannotebook.dto;
import java.util.List;
import java.util.Map;
public record PlanRequest(
String name,
String description,
String expectedOutcome,
List<Map<String, Object>> subtasks,
String sessionId
) {
}
java
package com.example.plannotebook.service;
import com.example.plannotebook.hook.PromptCaptureHook;
import com.example.plannotebook.util.FormatOutputUtil;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.Event;
import io.agentscope.core.agent.EventType;
import io.agentscope.core.agent.StreamOptions;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.ThinkingBlock;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.model.OllamaChatModel;
import io.agentscope.core.plan.PlanNotebook;
import io.agentscope.core.session.InMemorySession;
import io.agentscope.core.session.Session;
import io.agentscope.core.tool.Toolkit;
import jakarta.annotation.PostConstruct;
import lombok.Setter;
import org.jspecify.annotations.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import tools.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Service
public class AgentService {
@Autowired
private OllamaChatModel ollamaChatModel;
@Autowired
private DashScopeChatModel dashScopeChatModel;
// 也是一种注入方式
@Setter(onMethod_ = @Autowired)
private Toolkit toolkit;
@Autowired
private PlanService planService;
public static final Session session = new InMemorySession();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final AtomicInteger contextSeq = new AtomicInteger(0);
private final ConcurrentLinkedQueue<String> contextLines = new ConcurrentLinkedQueue<>();
private static final int CTX_FLAT_MAX_CHARS = 48_000;
private ReActAgent agent;
// // 解决没有聊天直接创建子任务 报planNotebook为null 方式二:
@PostConstruct
public void init() {
agent = getReActAgent();
}
public Flux<String> chat(String message, String sessionId) {
// ReActAgent agent = getReActAgent();
agent.loadIfExists(session, sessionId);
return agent.stream(Msg.builder().textContent(message).build(),
getStreamOptions())
// 不阻塞主线程
.subscribeOn(Schedulers.boundedElastic())
.concatMap(this::concatContextLines)
.doFinally(signalType -> agent.saveTo(session, sessionId));
}
private @NonNull ReActAgent getReActAgent() {
PlanNotebook planNotebook = PlanNotebook.builder().build();
// 当planNotebook发生变化 则调用该方法
planNotebook.addChangeHook("planServiceBroadcastChange",
(planNotebook1, plan) -> planService.broadcastPlanChant());
planService.setPlanNotebook(planNotebook);
ReActAgent agent = ReActAgent.builder()
.sysPrompt("""
你是一位专业的系统化任务规划与执行助手,擅长处理复杂的多步骤任务。
核心能力:
1. **结构化规划**: 接收任务后,先进行需求分析,制定清晰的执行路线图
2. **任务分解**: 将复杂目标拆解为逻辑清晰、可执行的子任务序列
3. **渐进式执行**: 按照优先级和依赖关系逐步推进,每步都验证结果
4. **动态调整**: 根据实际情况灵活调整计划,确保最终目标达成
5. **质量把控**: 在每个关键节点进行检查,保证输出质量
工作方式:
- 主动澄清模糊需求,确保理解准确
- 提供透明的进度反馈和决策依据
- 在必要时提出优化建议或替代方案
- 保持与用户的持续沟通和对齐
""")
.name("plan-notebook")
.model(dashScopeChatModel)
// 如果有自定义的工具可以在这获取到toolkit后 在补充注册工具
.toolkit(toolkit.copy())
.memory(new InMemoryMemory())
.planNotebook(planNotebook)
.maxIters(50)
.hook(new PromptCaptureHook(this::offerContextLines))
.build();
return agent;
}
/**
* 拼接流式输出结果 + header头信息
* @param event
* @return
*/
private @NonNull Flux<String> concatContextLines(Event event) {
// 获取header 然后先发送header 在发送原来的数据
List<String> headers = pollContextLines();
Flux<String> headerFlux = Flux.fromIterable(headers);
// 流式输出返回结果 转换成String
String body = convertEventToString(event);
if (headers.isEmpty() && body.isEmpty()) {
return Flux.empty();
}
if (body.isEmpty()) {
return headerFlux;
}
// 先发header, 然后在返回s
return headerFlux.concatWith(Flux.just(body));
}
private @NonNull List<String> pollContextLines() {
String poll;
List<String> headers = new ArrayList<>();
while ((poll = contextLines.poll()) != null) {
headers.add(poll);
}
return headers;
}
private static String convertEventToString(Event event) {
// 默认返回三词数据 第一个是流式输出、第二个是汇总的结果、第三个是结果
// 因为重复 只需要取一个就行
EventType type = event.getType();
if (type == EventType.REASONING && !event.isLast()) {
Msg msg = event.getMessage();
List<ThinkingBlock> thinkingBlocks = msg.getContentBlocks(ThinkingBlock.class);
if (!thinkingBlocks.isEmpty()) {
String think = thinkingBlocks.stream()
.map(ThinkingBlock::getThinking)
.collect(Collectors.joining());
// 前端规定格式: t表示类型, d表示内容
return OBJECT_MAPPER.writeValueAsString(Map.of("t", "think", "d", think));
}
String textContent = msg.getTextContent();
if (textContent == null || textContent.isEmpty()) {
return "";
}
return OBJECT_MAPPER.writeValueAsString(Map.of("t", "text", "d", textContent));
}
return "";
}
private void offerContextLines(String phase, String modelName, List<Msg> messages) {
int seq = contextSeq.incrementAndGet();
Map<String, Object> root = new LinkedHashMap<>();
root.put("t", "ctx");
root.put("phase", phase);
root.put("seq", seq);
root.put("model", modelName);
root.put("messages", FormatOutputUtil.msgToDebugRows(messages));
root.put("flat", FormatOutputUtil.truncateIfNeeded(
FormatOutputUtil.buildFlatTranscript(messages), CTX_FLAT_MAX_CHARS
));
contextLines.add(OBJECT_MAPPER.writeValueAsString(root));
}
private StreamOptions getStreamOptions() {
return StreamOptions.builder()
.eventTypes(EventType.REASONING, EventType.TOOL_RESULT, EventType.AGENT_RESULT)
// 只发新增内容
.incremental(true)
// 隐藏执行过程
.includeActingChunk(false)
.build();
}
}
5.2.4、更新计划
- service
java
public Mono<String> updatePlan(PlanRequest request) {
return planNotebook.updatePlanInfo(
request.name(),
request.description(),
request.expectedOutcome()
);
}
- controller
java
@PutMapping
public Mono<String> updatePlan(@RequestBody PlanRequest request) {
if (request == null) {
return Mono.just("请求体不能为空");
}
if (request.name() == null || request.name().isBlank()) {
return Mono.just("计划名称不能为空");
}
return planService.updatePlan(request);
}
5.3、子计划
5.3.1、服务类
java
public Mono<String> addSubTask(Map<String, Object> request) {
return planNotebook.reviseCurrentPlan((Integer) request.get("index"), "add", request);
}
public Mono<String> reviseSubTask(int index, SubTaskRequest request) {
Map<String, Object> map = Map.of(
"name", request.getName(),
"description", request.getDescription(),
"expected_outcome", request.getExpectedOutcome()
);
return planNotebook.reviseCurrentPlan(index, "revise", map);
}
public Mono<String> updateSubTaskState(int index, String state) {
return planNotebook.updateSubtaskState(index, state);
}
public Mono<String> finishSubTask(int index, String outcome) {
return planNotebook.finishSubtask(index, outcome);
}
public Mono<String> finishPlan(String state, String outcome) {
return planNotebook.finishPlan(state, outcome);
}
public Mono<String> deleteSubTask(Integer index) {
return planNotebook.reviseCurrentPlan(index, "delete", null);
}
5.3.2、接口
java
@PostMapping("/subtasks")
public Mono<String> addSubTask(@RequestBody Map<String, Object> request) {
Object expectedOutcome = request.get("expectedOutcome");
if (expectedOutcome != null) {
request.put("expected_outcome", expectedOutcome);
}
return planService.addSubTask(request);
}
@PutMapping("/subtasks/{index}")
public Mono<String> reviseSubTask(@PathVariable int index,
@RequestBody SubTaskRequest request) {
return planService.reviseSubTask(index, request);
}
@PatchMapping("/subtasks/{index}/state")
public Mono<String> updateSubTaskState(@PathVariable int index,
@RequestBody Map<String, Object> request) {
String state = (String) request.get("state");
return planService.updateSubTaskState(index, state);
}
@PostMapping("/subtasks/{index}/finish")
public Mono<String> finishSubTask(@PathVariable int index,
@RequestBody Map<String, Object> request) {
String outcome = (String) request.get("outcome");
return planService.finishSubTask(index, outcome);
}
@PostMapping("/finish")
public Mono<String> finishPlan(@RequestBody Map<String, Object> request) {
String state = (String) request.get("state");
String outcome = (String) request.get("outcome");
return planService.finishPlan(state, outcome);
}
@DeleteMapping("/subtasks/{index}")
public Mono<String> deleteSubTask(@PathVariable Integer index) {
return planService.deleteSubTask(index);
}
5.4、暂停执行计划
- 编写agent暂停的hook
遇到哪些工具暂停、设置一些变量方便通知
java
package com.example.plannotebook.hook;
import io.agentscope.core.hook.Hook;
import io.agentscope.core.hook.HookEvent;
import io.agentscope.core.hook.PostActingEvent;
import io.agentscope.core.message.ToolUseBlock;
import lombok.AllArgsConstructor;
import reactor.core.publisher.Mono;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@AllArgsConstructor
public class PlanChangeHook implements Hook {
private final ConcurrentLinkedQueue<Map<String, Object>> pendingToolInputs;
private final AtomicBoolean requestedStopped;
private final AtomicBoolean isPaused;
private static final Set<String> PLAN_TOOL_NAMES =
Set.of(
"create_plan",
"update_plan_info",
"revise_current_plan",
"update_subtask_state",
"finish_subtask",
"get_subtask_count",
"finish_plan",
"view_subtasks",
"view_historical_plans",
"recover_historical_plan");
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (event instanceof PostActingEvent e) {
ToolUseBlock toolUse = e.getToolUse();
Map<String, Object> inputs = new LinkedHashMap<>();
if (toolUse != null && toolUse.getInput() != null) {
inputs.putAll(toolUse.getInput());
}
pendingToolInputs.offer(inputs);
if (toolUse != null
&& PLAN_TOOL_NAMES.contains(toolUse.getName())
&& requestedStopped.compareAndSet(true, false)) {
isPaused.set(true);
e.stopAgent();
}
}
return Mono.just(event);
}
}
- 服务类
java
private final ConcurrentLinkedQueue<Map<String, Object>> pendingToolInputs = new ConcurrentLinkedQueue<>();
private final AtomicBoolean requestedStopped = new AtomicBoolean(false);
private final AtomicBoolean isPaused = new AtomicBoolean(false);
.hook(new PlanChangeHook(pendingToolInputs, requestedStopped, isPaused))
public boolean requestStop() {
requestedStopped.set(true);
return requestedStopped.get();
}
public boolean isRequestStopped() {
return requestedStopped.get();
}
public boolean getPaused() {
return isPaused.get();
}
- 暂停接口
java
@PostMapping("/stop")
public Map<String, Object> requestStop(@RequestParam(defaultValue = "default") String sessionId) {
return Map.of("message", "下一个工具执行完成后暂停", "stopRequested", agentService.requestStop());
}
@GetMapping("/stop-requested")
public Map<String, Object> getRequestStop(@RequestParam(defaultValue = "default") String sessionId) {
return Map.of("stopRequested", agentService.isRequestStopped());
}
@GetMapping("/paused")
public Map<String, Object> paused(@RequestParam(defaultValue = "default") String sessionId) {
return Map.of("message", "暂停成功", "pause", agentService.getPaused());
}
@GetMapping("/health")
public String health() {
return "OK";
}
5.5、重置
- 重置服务类
java
@PostConstruct
public void init() {
contextLines.clear();
pendingToolInputs.clear();
contextSeq.set(0);
agent = getReActAgent();
}
public String reset() {
isPaused.set(false);
requestedStopped.set(false);
init();
planService.broadcastPlanChant();
return "ok";
}
- 重置接口类
java
@PostMapping("/reset")
public String reset(@RequestParam(defaultValue = "default") String sessionId) {
return agentService.reset();
}
5.6、工具结果返回
补充一个暂停结果返回
java
private String convertEventToString(Event event) {
// 默认返回三词数据 第一个是流式输出、第二个是汇总的结果、第三个是结果
// 因为重复 只需要取一个就行
EventType type = event.getType();
// 返回暂停结果
if (type == EventType.AGENT_RESULT) {
Msg msg = event.getMessage();
if (msg != null && msg.getGenerateReason() == GenerateReason.ACTING_STOP_REQUESTED) {
return OBJECT_MAPPER.writeValueAsString(Map.of("t", "paused"));
}
return "";
}
// 返回工具结果
if(type == EventType.TOOL_RESULT) {
Msg msg = event.getMessage();
if (msg == null) {
return "";
}
List<ToolResultBlock> toolResultBlocks = msg.getContentBlocks(ToolResultBlock.class);
if (toolResultBlocks.isEmpty()) {
return "";
}
ToolResultBlock toolResultBlock = toolResultBlocks.get(0);
LinkedHashMap<Object, Object> toolResultMap = new LinkedHashMap<>();
toolResultMap.put("n", msg.getName() == null ? "" : msg.getName());
toolResultMap.put("t", "tool");
toolResultMap.put("d", FormatOutputUtil.flattenToolOutput(toolResultBlock));
Map<String, Object> poll = pendingToolInputs.poll();
if (poll != null && !poll.isEmpty()) {
toolResultMap.put("i", poll);
}
return OBJECT_MAPPER.writeValueAsString(toolResultMap);
}
if (type == EventType.REASONING && !event.isLast()) {
Msg msg = event.getMessage();
List<ThinkingBlock> thinkingBlocks = msg.getContentBlocks(ThinkingBlock.class);
if (!thinkingBlocks.isEmpty()) {
String think = thinkingBlocks.stream()
.map(ThinkingBlock::getThinking)
.collect(Collectors.joining());
// 前端规定格式: t表示类型, d表示内容
return OBJECT_MAPPER.writeValueAsString(Map.of("t", "think", "d", think));
}
String textContent = msg.getTextContent();
if (textContent == null || textContent.isEmpty()) {
return "";
}
return OBJECT_MAPPER.writeValueAsString(Map.of("t", "text", "d", textContent));
}
return "";
}

5.7、恢复
- 服务类
java
public Flux<String> resume(String sessionId) {
if (isPaused.compareAndSet(true, false)) {
pendingToolInputs.clear();
contextSeq.set(0);
contextLines.clear();
return agent.stream(getStreamOptions())
.subscribeOn(Schedulers.boundedElastic())
.concatMap(this::concatContextLines)
.doFinally(signalType -> agent.saveTo(session, sessionId));
}
return Flux.just("没有暂停或者已经恢复");
}
- 接口类
java
/**
* 恢复执行
* @param sessionId
* @return
*/
@GetMapping(value = "/resume", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> resume(@RequestParam(defaultValue = "default") String sessionId) {
return agentService.resume(sessionId);
}
