本篇是系列的收官加餐。上一篇《互通层》把自研装配的 web 模块收了尾,这个仓库里其实还藏着第二个入口------
dream-scope-boot:它把 AgentScope Java 2.0.3 的 11 个官方 Spring Boot starter 全部拉进依赖,验证另一条"引入即用"的路线。两条路线同一框架、同一套工具能力,装配方式完全不同,正好构成一组对照实验。环境:AgentScope Java 2.0.3 + Spring Boot 3.5.x + JDK 21。
前面四篇的 dream-scope-web 走的是自研装配:手写 PortsConfig、手动构建 HarnessAgent、自己接 Redis 和 RAG。这篇的 dream-scope-boot 反其道而行------官方 starter 自动配置,不依赖 dream-scope-domain 和 dream-scope-adapter,两个模块互不替代。README 里写得很直白:boot 是"可选入口"。
一、两条装配路线,差在哪
| 维度 | dream-scope-web(自研装配) | dream-scope-boot(官方 starter) |
|---|---|---|
| Agent 构建 | 手写 @Bean 构建 HarnessAgent |
核心 starter 自动创建 ReActAgent |
| 模型管理 | 自研 ChatHarnessOptions |
agentscope.model.provider 属性绑定 |
| RAG | 自研检索端口体系(关键词+pgvector+RRF+精排) | 官方 agentscope-extensions-rag-simple |
| HTTP 契约 | 自定义 invoke/stream | 自定义 + OpenAI 兼容 + AG-UI + A2A |
| 依赖方向 | domain / adapter 全家桶 | 不依赖 domain / adapter |
一句话概括:web 模块控制力优先,每一层都能定制,代价是装配代码量;boot 模块效率优先,starter 引入即用,代价是定制空间受限。两个模块并存,本身就是对这个问题的回答------什么时候该用官方 starter,什么时候该自研,文章最后一节展开。
二、11 个 starter 全景
boot 模块的 pom.xml 把官方 starter 拉满,按职能分四类:
| 类别 | Starter | 状态 | 入口 |
|---|---|---|---|
| 核心 | agentscope-spring-boot-starter |
开 | 装配 Memory / Toolkit / ReActAgent |
| 模型 | agentscope-dashscope-spring-boot-starter |
开 | agentscope.model.provider=dashscope |
| 模型 | openai / anthropic / gemini / ollama | 关 | 改 provider + 各自 enabled |
| 协议 | agentscope-chat-completions-web-starter |
开 | POST /v1/chat/completions |
| 协议 | agentscope-agui-spring-boot-starter |
开 | /agui 事件流 |
| 协议 | agentscope-admin-spring-boot-starter |
开 | GET /v1/admin/sessions,写操作关 |
| 协议 | agentscope-a2a-spring-boot-starter |
开 | GET /.well-known/agent-card.json |
| 基础设施 | agentscope-nacos-spring-boot-starter |
部分开 | prompt 关;A2A 注册开;技能库开 |
对应的 application.yml 核心段(dream-scope-boot/src/main/resources/application.yml):
agentscope:
agent:
enabled: true # 核心 starter 的总开关
name: chat
sys-prompt: 你是一个有帮助的助手。工具表里有 mcp__boot__echo,需要把原文回声时就调用它。回答产品或调用方式前先调用 retrieve,再按 [1][2] 引用。
max-iters: 10
model:
provider: dashscope # 切厂商只改这一处
dashscope:
enabled: true
api-key: ${DASHSCOPE_API_KEY:}
model-name: qwen-plus
stream: true
chat-completions:
enabled: true
base-path: /v1/chat/completions
agui:
path-prefix: /agui
default-agent-id: agentscopeReActAgent
admin:
enabled: true
write-enabled: false
a2a:
server:
enabled: true
card:
name: dream-scope-boot
六家模型 Provider 的 starter 全在依赖里,但同一时间只启用一家:切换厂商改 agentscope.model.provider,再把对应家的 enabled 置 true、其余置 false。密钥各走各的环境变量(OPENAI_API_KEY、ANTHROPIC_API_KEY、GEMINI_API_KEY,Ollama 用 OLLAMA_BASE_URL)。

三、自动配置的三个条件注解
核心 starter 的自动配置类逻辑不复杂,但三个注解的组合值得细看(AgentScope 官方 starter 源码):
@AutoConfiguration
@EnableConfigurationProperties(AgentscopeProperties.class)
@ConditionalOnClass(ReActAgent.class)
public class AgentscopeAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = "agentscope.agent", name = "enabled", havingValue = "true")
@ConditionalOnMissingBean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Memory agentscopeMemory() { return new InMemoryMemory(); }
@Bean
@ConditionalOnProperty(prefix = "agentscope.agent", name = "enabled", havingValue = "true")
@ConditionalOnMissingBean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Toolkit agentscopeToolkit() { return new Toolkit(); }
}
三个注解各管一件事:
| 注解 | 作用 |
|---|---|
@ConditionalOnProperty |
agentscope.agent.enabled=true 才装配,一行配置整体关停 |
@ConditionalOnMissingBean |
用户自定义 Bean 优先,starter 不抢 |
@Scope(SCOPE_PROTOTYPE) |
Memory / Toolkit 每次注入都创建新实例 |
第三个最容易被忽视,也最关键。Memory 持对话历史,Toolkit 持工具注册,都是有状态且非线程安全的对象------在 Web 环境里如果做成单例,并发请求会互相踩状态。所以官方把它声明成原型作用域,每次注入拿到独立副本。代价是:想让两个 Agent 用同一份工具,直接 @Autowired 注入是行不通的(它们会拿到两个不同的 Toolkit 实例),必须在装配层显式传递。第五节会看到 boot 模块怎么处理这件事。
自动配置的触发链是:agentscope.agent.enabled=true 且容器里已有 Model Bean → starter 创建 ReActAgent。而 Model 由厂商 starter 提供------这就是为什么没有配 DashScope 密钥时进程直接起不来:Model Bean 创建失败,整条链断在源头。
四、两个工程细节
排除 spring-boot-autoconfigure。 boot 模块的 pom.xml 里,每个 AgentScope starter 都带一组排除(dream-scope-boot/pom.xml):
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclusion>
</exclusions>
原因:AgentScope starter 自身依赖 spring-boot-autoconfigure,而 boot 模块已经通过 spring-boot-starter-web 引入了它。11 个 starter 各带一份传递依赖,轻则版本漂移重则重复类。排除后由父 POM 的 spring-boot-dependencies BOM 统一管理。这个做法有个隐含前提:AgentScope 2.0.3 的自动配置与 Spring Boot 3.5.x 兼容,否则会报 NoClassDefFoundError。细节是核心 starter 与 Provider starter 的排除列表不一致(后者多排了 autoconfigure-processor),如果照抄这套排除方案,建议统一。
六家 Provider 共存的代价。 依赖全拉、只启用一家,带来三个隐形成本:所有 Provider 的自动配置类都会被 Spring Boot 处理(启动开销);每个 starter 传递引入对应 SDK(JAR 膨胀);理论上多 Provider 都创建 Model Bean 会有冲突风险,靠各自的 @ConditionalOnMissingBean 和 enabled 开关兜底。生产项目更稳妥的做法是用 Maven profile 控制 Provider 依赖,只装实际使用的那家。
五、两条对话路径怎么装配
starter 搞定了 ReActAgent 这条"官方链",boot 模块自己的活是把 HarnessAgent 这条"自研链"也装配进来,两链并存、互不干扰。装配核心在 StarterPortsConfig(com.zhu.scope.boot.config):
@Bean(destroyMethod = "close")
@ConditionalOnProperty(prefix = "agentscope.agent", name = "enabled", havingValue = "true")
BootHarness bootHarness(Model model, Toolkit toolkit, BootRetrieveTool retrieve,
BootScopeProperties props, Environment env,
ObjectProvider<NacosSkillRepository> nacosSkills) {
JedisPooled jedis = BootRedis.open(props.getRedis().getUri());
DistributedStore store = BootRedis.store(jedis, props.getRedis().getKeyPrefix());
HarnessAgent agent = BootPlanAgent.create(
model, toolkit, workspace(props),
props.getPlan().getDirectory(), store,
props.getCompaction().getTriggerMessages(),
props.getCompaction().getKeepMessages(),
props.getSkills().getDirectory(),
BootFallback.open(env, props, model),
props.getEviction().getMaxResultChars(),
props.getEviction().getPreviewChars(),
props.getEviction().getPath(),
/* 采样参数与 plan 迭代上限 */ ...,
nacosSkills.getIfAvailable());
return new BootHarness(agent, jedis);
}
几个值得注意的点:
- Model 来自 starter :bootHarness 的入参直接注入
Model,这就是第三节说的"厂商 starter 提供 Model Bean,自研装配复用它"。HarnessAgent 和 ReActAgent 底层用的是同一个模型实例,切厂商时两边一起切。 - Redis 是硬依赖 :
BootRedis.open连不上就抛异常,带对话能力的 Agent 起不来。会话存储用官方DistributedStore,键前缀dream-scope-boot:,userId与sessionId成对出现才写入。 - Nacos 技能库是显式失败 :
agentscope.nacos.skill.enabled=true时创建NacosSkillRepository,连不上直接抛IllegalStateException------宁可起不来也不静默降级。Nacos 没打算启时,把 A2A 注册和技能库两个开关关掉,进程才能启动。 - 知识索引无条件创建 :
knowledgeIndex不挂条件注解,配了DREAM_SCOPE_RAG_PG_JDBC_URL走 PostgreSQL(表dream_scope_boot_rag),否则退回内存索引;配了DASHSCOPE_API_KEY才有向量补召回(text-embedding-v4),关键词命中不受 0.3 分数阈值限制。
chat 路径的调用入口 StarterChatAgent 把 HarnessAgent 包成同步/流式两种调用(com.zhu.scope.boot.agent.impl):
public StarterResult handle(StarterRequest request) {
Msg inbound = StarterMessages.toUserMessage(request.input(), request.imageUrls());
RuntimeContext context = contextOf(request);
Msg outbound = agent.call(List.of(inbound), context).block(callTimeout);
return new StarterResult(id(), StarterMessages.textOf(outbound), ...);
}
public void streamHandle(StarterRequest request, Sink sink) {
Disposable disposable = agent.streamEvents(List.of(inbound), context)
.timeout(callTimeout)
.subscribe(event -> codec.toEvent(event).ifPresent(sink::onEvent),
error -> sink.onError(mapError(error)), sink::onComplete);
sink.bindCancel(() -> { disposable.dispose(); agent.interrupt(context); });
}
同步路径 block(120s) 兜底超时;流式路径超时用 Reactor 的 timeout,客户端断开时 dispose 订阅再 interrupt Agent,取消信号传导到模型调用层。错误统一映射成 StarterTimeoutException / StarterProviderException 两类,Controller 层不用感知 Reactor 的异常细节。
请求怎么找到 Agent?一个 25 行的极简注册表(StarterHandlerRegistry):
public final class StarterHandlerRegistry {
private final Map<String, StarterHandler> handlers = new ConcurrentHashMap<>();
public StarterHandlerRegistry(List<StarterHandler> agents) {
if (agents == null) return;
for (StarterHandler handler : agents) {
if (handler == null || handler.id() == null || handler.id().isBlank())
throw new IllegalArgumentException("agent handler id required");
handlers.put(handler.id(), handler);
}
}
public Optional<StarterHandler> find(String agentId) {
if (agentId == null || agentId.isBlank()) return Optional.empty();
return Optional.ofNullable(handlers.get(agentId));
}
}
构造时从 Spring 收集所有 StarterHandler Bean(chat 和 knowledge 两个实现),id 空白直接 fail-fast,查找返回 Optional,未知 agentId 由 Controller 层翻译成 404。没有注册中心的花哨设计------两个 Handler 的规模用不上。

六、协议出口与 Nacos 的三组开关
starter 自动装配的协议能力,boot 一个没落下:
| 入口 | 提供方 | 说明 |
|---|---|---|
POST /v1/chat/completions |
chat-completions-web-starter | OpenAI 兼容,ChatUI 等前端直连 |
POST /agui |
agui-starter | AG-UI 协议事件流,default-agent-id 指向 starter 的 ReActAgent |
GET /.well-known/agent-card.json |
a2a-starter | Agent Card 自描述 |
GET /v1/admin/sessions |
admin-starter | 会话管理,write-enabled: false 只读 |
AG-UI starter 做的事是把 AgentScope 事件流翻译成 AG-UI Protocol 事件,映射关系由 starter 内置:
| AgentScope 事件 | AG-UI 事件 |
|---|---|
| AgentStartEvent / AgentEndEvent | RUN_STARTED / RUN_FINISHED |
| 文本增量 | TEXT_MESSAGE_START / CONTENT / END |
| 推理内容(enableReasoning) | REASONING_MESSAGE_* |
| 工具调用 / 结果 | TOOL_CALL_START / ARGS / END / RESULT |
| 未映射事件 | RAW(原样透传) |
boot 模块用的是 MVC(spring-boot-starter-web),AG-UI starter 走 MVC 适配路径;官方示例更推荐搭配 WebFlux。这条路径不经过计划模式、Redis 会话和工作区------它和 invoke 端点共用工具表,但会话能力是 HarnessAgent 那条链的专属。A2A 客户端扩展(agentscope-extensions-a2a-client,非 starter)配的 url 指向自己的 8092,创建时不访问远端,调用时才去拉对方的 agent-card------一个自调用演示位。
Nacos 这块在 boot 模块里出现了三组开关,配置前缀互不相干,实测最容易混淆:
| 开关组 | 前缀 | boot 的状态 |
|---|---|---|
| AgentScope Prompt 热加载 | agentscope.nacos.prompt.enabled |
关 |
| AgentScope A2A 注册 | agentscope.a2a.nacos.enabled |
开 |
| AgentScope 技能库 | agentscope.nacos.skill.enabled |
开(技能 boot-echo) |
| Spring Cloud 配置中心 | spring.cloud.nacos.config.enabled |
默认关(环境变量控制) |
| Spring Cloud 服务发现 | spring.cloud.nacos.discovery.enabled |
默认关 |
AgentScope 自己的 Nacos 集成(prompt / A2A / skill 三子模块)和 Spring Cloud 的 Nacos 集成(config / discovery)是两套完全独立的体系。boot 还加了 spring.config.import: optional:nacos:dream-scope-boot.yaml,配 optional: 前缀,Nacos 不在也不阻断启动------和技能库的 fail-fast 形成对比,一个可选一个必需,按业务影响分级。
fig2 已在第五节画出两条路径的全景。到这里,boot 模块的完整画面是:11 个 starter 负责官方链和协议出口,StarterPortsConfig 一个配置类负责自研链,两条链共享同一个 Model 和知识索引。
七、官方 starter 与自研装配的选型判断
跑完两个模块,取舍其实很清晰:
- 官方 starter 赢在起步速度。配好 provider 和密钥,ReActAgent + OpenAI 兼容接口 + AG-UI 就位,半天能跑通一条完整链路。快速原型、内部 demo、给前端团队提供标准协议出口,starter 是正解。
- 自研装配赢在控制力。web 模块里检索的降级链、事件编解码、SSE 取消传导、大结果截断回读,这些深度定制点 starter 都没留口子。生产级 Agent 一旦涉及复杂会话策略和可观测性,自研是绕不开的。
- 两条路线可以渐进过渡 。boot 模块验证了一个重要事实:starter 的
ModelBean 和原型Toolkit可以被自研装配复用(@ConditionalOnMissingBean保证了用户 Bean 优先)。先 starter 跑通,再逐个替换成自研 Bean,迁移路径是平滑的,不需要一步到位。
官方 starter 把"开箱一条链"做得很扎实,深度定制上留白较多------这些留白正是 web 模块那种自研装配的存在价值。两个模块并存不是冗余,是同一框架下两种工程策略的完整对照。
本篇学到什么
- 条件装配三件套 :
@ConditionalOnProperty控制总开关、@ConditionalOnMissingBean让用户 Bean 优先、SCOPE_PROTOTYPE处理有状态对象的线程安全------starter 设计的标准范式。 - 原型作用域的坑 :Memory / Toolkit 每次注入都是新实例,跨 Agent 共享工具必须在装配层显式传递,直接
@Autowired共享是常见误区。 - starter 传递依赖治理 :多 starter 场景统一排除
spring-boot-autoconfigure,交给 BOM 管,前提是版本兼容性已验证。 - 多 Provider 共存的代价:依赖全拉只开一家,换来切换便利,付出启动开销与 JAR 膨胀,生产建议 Maven profile 裁剪。
- 依赖分级策略:Redis、技能库连不上就 fail-fast,知识索引退内存、配置中心 optional------按业务影响决定硬失败还是降级。
系列五篇到此完结:01 起步与 Agent 核心、02 会话与工具、03 RAG、04 互通层、05 Spring Boot 整合。dream-scope 一个仓库、两条路线,从自研装配到官方 starter 全部踩了一遍。
有问题评论区见,欢迎交流~
