第九章 Spring Boot 集成:工厂方法 + WebFlux 流式端点,手动配置 HarnessAgent 为单例 Bean
"1.x 时代有
agentscope-spring-boot-starter,2 行配置就能把 agent 注入到 Spring 容器里。2.0.0-RC2 没有官方 starter——这不是退步,而是模型本身的生命周期与 Spring Bean 生命周期(懒加载、scope、AOP)不匹配。2.0 推荐 『手动配置 + 工厂方法 + WebFlux』 三步集成。"本章你将学到:如何把
HarnessAgent注册成单例 Bean、如何在 Controller / Service 里注入它、如何用 WebFlux 流式输出streamEvents()。
9.1 为什么 2.0 没有官方 starter?
1.x 的 agentscope-spring-boot-starter 把 ReActAgent 注册成 singleton Bean。2.0 不再提供这个 starter,原因是:
HarnessAgent不便宜 —— 每次.build()会创建 model client、tool registry、workspace 句柄。Spring 容器默认 eager-instantiation 可能在没人用 agent 时就建立这些连接。- 多用户隔离 —— 1.x 时代一个 Bean 服务所有用户;2.0 用
RuntimeContext区分,HarnessAgent内部需要 Session 后端支持,并发模型与 Spring MVC 的"每请求一线程"模型不匹配。 - 响应式 —— 2.0 的
streamEvents()返回Flux<AgentEvent>,天然适合 WebFlux。Starter 把所有响应式 API 包装成同步形式,反而抹掉了这个能力。
所以 2.0 推荐:
- 生产用 WebFlux(响应式)
- 小工具 / 批处理用
@Configuration+ 工厂方法(手动)
9.2 添加依赖
pom.xml(仅核心 + WebFlux):
<dependencies>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-harness</artifactId>
<version>2.0.0-RC2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
9.3 把 Model 注册成 Bean
模型对象是配置驱动的——交给 application.yml:
agentscope:
model:
provider: dashscope
api-key: ${
DASHSCOPE_API_KEY}
name: qwen-plus
workspace: ./workspace
对应的 properties 类:
package demo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "agentscope")
public class AgentScopeProperties {
private Model model = new Model();
private String workspace = "./workspace";
public static class Model {
private String provider = "dashscope";
private String apiKey;
private String name = "qwen-plus";
// getter / setter ...
}
public Model getModel() {
return model; }
public String getWorkspace() {
return workspace; }
public void setModel(Model m) {
this.model = m; }
public void setWorkspace(String w) {
this.workspace = w; }
}
Model 工厂:
package demo.config;
import io.agentscope.core.Model;
import io.agentscope.core.model.DashScopeChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ModelConfig {
@Bean
public Model chatModel(AgentScopeProperties props) {
AgentScopeProperties.Model m = props.getModel();
if (!"dashscope".equals(m.getProvider())) {
throw new IllegalArgumentException("unsupported provider: " + m.getProvider());
}
return DashScopeChatModel.builder()
.apiKey(m.getApiKey())
.modelName(m.getName())
.build();
}
}
9.4 把 HarnessAgent 做成"工厂 Bean"
我们希望"按用途"区分 agent——比如天气 agent、翻译 agent、客服 agent。推荐用 @Bean 工厂方法 而不是把 HarnessAgent 自身注册成 Bean(避免 eager build):
package demo.config;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.UserMessage;
import io.agentscope.harness.HarnessAgent;
import org.springframework.stereotype.Component;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class AgentFactory {
private final Model model;
private final Path workspace;
private final ConcurrentHashMap<String, HarnessAgent> cache = new ConcurrentHashMap<>();
public AgentFactory(Model model, AgentScopeProperties props) {
this.model = model;
this.workspace = Path.of(props.getWorkspace());
}
public HarnessAgent weatherAgent() {
return cache.computeIfAbsent("weather", id ->
HarnessAgent.builder()
.name("weather_bot")
.sysPrompt("你是一个中文天气助手,每次回答不超过 50 字。")
.model(model)
.workspace(workspace)
.build());
}
public HarnessAgent translatorAgent() {
return cache.computeIfAbsent("translator", id ->
HarnessAgent.builder()
.name("translator")
.sysPrompt("你是一个中英互译助手。")
.model(model)
.workspace(workspace)
.build());
}
}
关键:业务方调
factory.weatherAgent()才创建 agent;ConcurrentHashMap保证单例;按用途区分,内存可控。
9.5 Controller:用 WebFlux 暴露流式端点
package demo.web;
import demo.config.AgentFactory;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.event.AgentEvent;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/agent")
public class AgentController {
private final AgentFactory factory;
public AgentController(AgentFactory factory) {
this.factory = factory;
}
/**
* 流式 SSE 端点
*/
@GetMapping(value = "/weather/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<AgentEvent> stream(
@RequestParam String sessionId,
@RequestParam String userId,
@RequestParam String text) {
return Mono.fromFuture(
factory.weatherAgent()
.streamEvents(
List.of(new UserMessage("user", text)),
RuntimeContext.builder()
.sessionId(sessionId)
.userId(userId)
.traceId(UUID.randomUUID().toString())
.build())
.toFuture())
.flatMapMany(flux -> flux);
}
/**
* 一次性同步端点
*/
@PostMapping("/weather/once")
public Mono<Map<String, String>> once(
@RequestParam String sessionId,
@RequestParam String userId,
@RequestBody Map<String, String> body) {
return Mono.fromFuture(
factory.weatherAgent()
.call(
List.of(new UserMessage("user", body.get("text"))),
RuntimeContext.builder()
.sessionId(sessionId)
.userId(userId)
.build())
.toFuture())
.map(msg -> Map.of("reply", msg.getTextContent()));
}
}
前端用 EventSource 订阅:
const es = new EventSource(
`/api/agent/weather/stream?sessionId=s-1&userId=u-1&text=${
encodeURIComponent('杭州今天多少度')}`
);
es.onmessage = (e) => console.log(e.data);
9.6 与 Spring Session 协作
如果前端用 spring-session-data-redis,把 sessionId 与 Spring Session 的 Session.Id 绑定,让 2.0 的 RedisSession 与 Spring Session 共享 Redis 实例:
RuntimeContext.builder()
.sessionId(httpSession.getId()) // 复用 Spring Session ID
.userId(currentUser.getId())
.build();
这样 Spring Session 里只放 HTTP 层的 attribute,agent state 由 RedisSession 单独管理——互不污染。
9.7 完整工程结构
src/main/java/demo/
├── DemoApplication.java
├── config/
│ ├── AgentScopeProperties.java
│ ├── ModelConfig.java
│ └── AgentFactory.java
└── web/
└── AgentController.java
src/main/resources/
└── application.yml
DemoApplication.java:
package demo;
import demo.config.AgentScopeProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(AgentScopeProperties.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
9.8 本章小结
- 2.0 没有官方 starter——
HarnessAgent不适合直接做 Spring Bean,由AgentFactory工厂按需创建。 - Model 走
@ConfigurationProperties+@Bean工厂。 - Controller 用 WebFlux:
streamEvents()→ SSE、call()→ 一次性。 sessionId可以与 Spring Session ID 复用,agent state 单独走RedisSession。