大模型能力日渐成熟,智能体(Agent)正从实验室原型走向生产级应用。作为Java开发者,如何在SpringBoot生态中快速集成Agent,并实现多工具调用、多轮对话与多Agent协作?本文基于阿里巴巴开源的AgentScope Java框架,带你从零搭建一个可运行的AI智能体应用,涵盖配置、工具注册、服务封装与效果测试,全程实战,干货满满。
一、为什么Java开发者需要关注AgentScope?
在Python生态中,LangChain、AutoGPT等框架已经让AI Agent开发变得相对简单。但在Java世界,尤其是企业级SpringBoot/Spring Cloud微服务架构中,想要快速集成大模型能力,却常常遇到API调用繁琐、多Agent协同逻辑复杂、上下文管理困难、与现有Java生态(MyBatis、Spring Security等)适配不顺畅等问题。
AgentScope Java正是为解决这些痛点而生。它是一款轻量级、高可扩展的智能代理开发框架,由阿里巴巴开源,深度拥抱Java生态。它不仅支持OpenAI、阿里云通义千问、百度文心一言等多种大模型,还提供了Spring Boot Starter,让你无需改写现有业务代码,就能零障碍地将AI能力嵌入系统。无论你是做智能客服、运维诊断还是数据查询,AgentScope都能帮你大幅降低开发门槛。
二、AgentScope Java 核心能力速览
AgentScope Java 的核心目标是为企业生产环境提供一套覆盖“开发、部署、调优”全生命周期的解决方案。它的核心能力包括:
- ReAct 智能体范式:让大模型具备自主推理与动态规划能力,根据任务需求灵活调用工具,完成复杂目标。
- 实时介入控制:支持安全中断、实时打断,让开发者在Agent运行全程保持可控。
- 高效工具体系:提供标准化注册接口、结构化工具组,统一处理同步异步调用并支持并行执行。
- 结构化输出保障:内置工具确保LLM输出严格遵循预定义JSON格式,自动校正错误。
- 企业级安全沙箱:为代码执行提供高度隔离的受控环境,内置GUI、文件系统等多平台支持。
- 上下文工程优化:集成RAG检索增强生成与多租户记忆管理,支持私有化部署和语义搜索。
- 无缝协议集成:通过MCP协议零改动集成现有HTTP系统,借助A2A协议实现分布式多Agent协作。
- 高性能异步架构:基于Project Reactor实现非阻塞执行,联合GraalVM实现200毫秒冷启动。
- 全链路可观测性:深度集成OpenTelemetry,配合Studio可视化平台提供实时调试与监控。
- 数据飞轮生态:通过A/B测试、奖励模型评估和强化学习训练形成闭环,持续优化模型能力。
其中,ReAct范式是框架的灵魂。它让Agent进入“思考-行动-观察”的循环,而不是像传统工作流那样把每一步写死。比如当用户问“北京的天气如何?”,Agent会先推理出需要调用天气查询工具,然后去执行,最后根据返回结果生成答案。这种模式让Agent能处理完全未知的复杂场景。

上图清晰地展示了ReAct模式与Workflow模式的差异。Workflow模式下,开发者需要提前把每一步写死——先查数据库、再调API、最后组装返回。而ReAct模式则把控制权交给大模型,让其像人一样“思考-行动-观察”,循环推进直到完成任务。
三、与其他同类产品的对比
在Java生态中,AI Agent框架尚未出现“一家独大”的局面。以下是从多个维度进行的对比:
维度 | AgentScope Java | Spring AI Alibaba | LangChain4j |
核心理念 | Agentic(智能体优先) 最大化发挥LLM的自主决策能力 | Workflow(工作流编排) 侧重于确定性的流程控制与集成 | 工具箱(Toolbox) 提供最全面的功能组件,灵活组合 |
开发范式 | ReAct、多智能体协作、消息驱动 | Graph、有向无环图(DAG)、函数式编排 | 链式调用、AiServices、灵活自由 |
易用性 | 中等,概念新颖,但对Spring开发者友好 | 高,对Spring开发者几乎零学习成本 | 中等偏低,功能强大但API较庞大,配置稍繁琐 |
企业集成 | 强,原生支持MCP/A2A协议、Nacos、安全沙箱 | 极强,无缝集成Spring Security、Actuator、Cloud全家桶 | 较强,但多为独立组件,企业级治理需自行集成 |
模型生态 | 支持主流模型,深度集成阿里云百炼平台 | 通过Spring AI,支持OpenAI、Ollama等 | 最强,支持30+ LLM和20+向量数据库 |
独特优势 | 安全沙箱、在线训练、自进化能力 | Spring生态的完美结合,企业级治理能力即开即用 | 社区最活跃、功能最全面、集成选择最多 |
最佳场景 | 需要高安全性、持续自我进化的复杂智能应用 | 基于现有Spring Boot技术栈,需要快速、稳定集成AI能力的企业级应用 | 需要与多种模型、向量库集 |
选型建议:
- 选AgentScope Java:如果你的核心诉求是构建能处理复杂、开放性问题的智能体,且对安全性和数据隔离有极高要求,同时希望应用能从真实交互中持续学习。尤其适合金融、政务等领域的智能化转型。
- 选Spring AI Alibaba:如果你的团队深度绑定Spring技术栈,首要目标是低风险、高效率地将AI能力嵌入现有业务系统,且流程相对确定。
- 选LangChain4j:如果你的项目需要集成非常多样的模型和外部工具,或者团队更倾向于一个灵活、社区驱动的技术工具箱。
四、SpringBoot 整合 AgentScope Java 实战
4.1 前置准备
首先,你需要开通阿里云百炼账号,进入APIKEY界面提前创建一个API Key,后续在项目配置文件中使用。

4.2 搭建SpringBoot工程
本次实战的技术栈为:SpringBoot 3.2.5、Java 17、AgentScope 1.0.12。
4.3 添加配置文件
在工程中添加 application.yml 配置文件:
server:
port: 8088
spring:
application:
name: agentscope-java
# Force servlet mode when agentscope pulls in reactor/webflux transitively
main:
web-application-type: reactive
agentscope:
# -----------------------------------------------------------------------
# DashScope (Alibaba Qwen) — fill in your API key
# Get it from: https://dashscope.aliyun.com/
# -----------------------------------------------------------------------
dashscope:
api-key: 你的百炼apikey
# -----------------------------------------------------------------------
# OpenAI-compatible providers — fill in the key you actually use.
# DeepSeek example: baseUrl=https://api.deepseek.com, model=deepseek-chat
# OpenAI example: baseUrl=https://api.openai.com, model=gpt-4o
# -----------------------------------------------------------------------
openai:
api-key: ${OPENAI_API_KEY:your-openai-api-key-here}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
model-name: ${OPENAI_MODEL:gpt-4o-mini}
# Agent session persistence directory (JsonSession)
session:
storage-path: ./sessions
# AG-UI endpoint (optional, for front-end streaming integration)
agui:
path-prefix: /agui
cors-enabled: true
server-side-memory: true
logging:
level:
io.agentscope: DEBUG
com.example.agentscope: DEBUG
⚠️ 建议使用环境变量注入敏感信息,避免硬编码:
export DASHSCOPE_API_KEY="your-dashscope-api-key"
4.4 添加核心配置类
接下来,添加一个核心配置类 AgentConfig.java,它的主要作用是使用Spring Boot的依赖注入机制来创建和管理AI模型、工具包和智能体(Agent)的Bean。
package com.example.agentscope.config; import com.example.agentscope.tools.CalculatorTools; import com.example.agentscope.tools.WeatherTools; import io.agentscope.core.ReActAgent; import io.agentscope.core.memory.InMemoryMemory; import io.agentscope.core.model.DashScopeChatModel; import io.agentscope.core.model.Model; import io.agentscope.core.model.OpenAIChatModel; import io.agentscope.core.tool.Toolkit; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; /** * Spring bean configuration for AgentScope models and agents. * *Supports two model providers out of the box: *
*
* *- DashScope (Alibaba Qwen) — active by default when DASHSCOPE_API_KEY is set
*- OpenAI-compatible (OpenAI, DeepSeek, …) — activated via agentscope.openai.* properties
*
Fill in the API key(s) in application.yml or via environment variables. */ @Slf4j @Configuration public class AgentConfig { // ----------------------------------------------------------------------- // Model beans // ----------------------------------------------------------------------- /** * DashScope chat model — primary model when DASHSCOPE_API_KEY is provided. */ @Bean @Primary @ConditionalOnProperty(name = "agentscope.dashscope.api-key", matchIfMissing = false) public Model dashScopeChatModel( @Value("${agentscope.dashscope.api-key}") String apiKey) { log.info("Initializing DashScope chat model"); return DashScopeChatModel.builder() .apiKey(apiKey) .modelName("qwen-plus") // change to qwen-max / qwen3-max as needed .build(); } /** * OpenAI-compatible chat model — activated when agentscope.openai.api-key is set. * Works with OpenAI, DeepSeek, Moonshot, SiliconFlow, etc. */ @Bean("openAIChatModel") @ConditionalOnProperty(name = "agentscope.openai.api-key", matchIfMissing = false) public Model openAIChatModel( @Value("${agentscope.openai.api-key}") String apiKey, @Value("${agentscope.openai.base-url}") String baseUrl, @Value("${agentscope.openai.model-name}") String modelName) { log.info("Initializing OpenAI-compatible chat model: baseUrl={}, model={}", baseUrl, modelName); return OpenAIChatModel.builder() .apiKey(apiKey) .baseUrl(baseUrl) .modelName(modelName) .build(); } // ----------------------------------------------------------------------- // Toolkit beans // ----------------------------------------------------------------------- /** Toolkit containing only weather tools. */ @Bean("weatherToolkit") public Toolkit weatherToolkit(WeatherTools weatherTools) { Toolkit toolkit = new Toolkit(); toolkit.registerTool(weatherTools); return toolkit; } /** Toolkit containing both weather and calculator tools. */ @Bean("fullToolkit") public Toolkit fullToolkit(WeatherTools weatherTools, CalculatorTools calculatorTools) { Toolkit toolkit = new Toolkit(); toolkit.registerTool(weatherTools); toolkit.registerTool(calculatorTools); return toolkit; } // ----------------------------------------------------------------------- // Agent beans // ----------------------------------------------------------------------- /** * General-purpose assistant — no tools, stateless (no memory). * Suitable for single-turn Q&A. */ @Bean("generalAgent") public ReActAgent generalAgent(Model dashScopeChatModel) { return ReActAgent.builder() .name("GeneralAssistant") .model(dashScopeChatModel) .sysPrompt("You are a helpful, concise and friendly AI assistant. " + "Answer questions clearly and accurately.") .build(); } /** * Weather-aware agent — has access to weather tools. * Stateless; each call is independent. */ @Bean("weatherAgent") public ReActAgent weatherAgent( Model dashScopeChatModel, @org.springframework.beans.factory.annotation.Qualifier("weatherToolkit") Toolkit weatherToolkit) { return ReActAgent.builder() .name("WeatherAssistant") .model(dashScopeChatModel) .sysPrompt("You are a professional weather assistant. " + "Always use the weather tools to fetch real data before answering.") .toolkit(weatherToolkit) .build(); } /** * Full-capability agent — weather + calculator tools, with in-memory conversation history. * The InMemoryMemory persists across multiple .call() invocations on the same bean instance. */ @Bean("fullAgent") public ReActAgent fullAgent( Model dashScopeChatModel, @org.springframework.beans.factory.annotation.Qualifier("fullToolkit") Toolkit fullToolkit) { return ReActAgent.builder() .name("FullAssistant") .model(dashScopeChatModel) .sysPrompt("You are a capable AI assistant with access to weather and calculation tools. " + "Use tools whenever precise data is needed.") .toolkit(fullToolkit) .memory(new InMemoryMemory()) .build(); } /** * Pipeline agent A — data collection / research role. * Used in multi-agent sequential/parallel pipelines. */ @Bean("collectorAgent") public ReActAgent collectorAgent(Model dashScopeChatModel) { return ReActAgent.builder() .name("DataCollector") .model(dashScopeChatModel) .sysPrompt("You are a research assistant. Collect and summarize key facts about the given topic. " + "Be thorough but concise.") .memory(new InMemoryMemory()) .build(); } /** * Pipeline agent B — analysis role. * Used in multi-agent sequential pipelines after the collector. */ @Bean("analyzerAgent") public ReActAgent analyzerAgent(Model dashScopeChatModel) { return ReActAgent.builder() .name("DataAnalyzer") .model(dashScopeChatModel) .sysPrompt("You are an expert analyst. Given the collected information, " + "perform in-depth analysis, identify patterns and provide insights.") .memory(new InMemoryMemory()) .build(); } /** * Pipeline agent C — report generation role. * Used as the final stage in sequential pipelines. */ @Bean("reporterAgent") public ReActAgent reporterAgent(Model dashScopeChatModel) { return ReActAgent.builder() .name("ReportGenerator") .model(dashScopeChatModel) .sysPrompt("You are a professional report writer. " + "Given the analysis, produce a structured, well-written report.") .memory(new InMemoryMemory()) .build(); } }
这个配置类负责以下三件事:
- 配置大语言模型:支持DashScope(阿里云通义千问)和OpenAI-compatible两种模式,通过配置自动切换。
- 配置工具包:将Java编写的工具类注册为AgentScope可调用的工具集,例如天气查询、计算器等。
- 定义智能体角色:创建不同角色的ReActAgent实例,每个Agent都有特定的系统提示词、模型和工具权限。
Agent 名称 | Bean 名称 | 职责描述 | 是否拥有工具 | 是否有记忆 |
通用助手 | generalAgent | 简单的问答助手,无特殊能力。 | ❌ | ❌ |
天气助手 | weatherAgent | 专门处理天气相关的问题。 | ✅ (天气工具) | ❌ |
全能助手 | fullAgent | 具备天气和计算能力,支持多轮对话。 | ✅ (全套工具) | ✅ (内存记忆) |
数据收集员 | collectorAgent | 在多 Agent 协作中负责搜集信息。 | ❌ | ✅ |
数据分析员 | analyzerAgent | 负责对收集到的信息进行深度分析。 | ❌ | ✅ |
报告生成员 | reporterAgent | 负责将分析结果整理成结构化报告。 | ❌ | ✅ |
简单来说,AgentConfig.java 就像是一个“工厂”,决定了你的AI应用:大脑是谁(用哪个模型),手里有什么工具(能查天气还是能算数),有多少个员工(不同的Agent扮演不同角色)。
4.5 添加业务服务层
接下来,我们定义接口层调用的常用方法 AgentService.java,它充当控制器与底层AI智能体之间的桥梁。
package com.example.agentscope.service; import io.agentscope.core.ReActAgent; import io.agentscope.core.message.Msg; import io.agentscope.core.pipeline.FanoutPipeline; import io.agentscope.core.pipeline.SequentialPipeline; import io.agentscope.core.session.JsonSession; import io.agentscope.core.session.Session; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import java.nio.file.Path; /** * Service layer that wraps AgentScope agent interactions. * *All public methods return reactive types (Mono / Flux) so callers can choose * to block or subscribe as needed. * *
Constructor injection is written manually to ensure {@link Qualifier} * annotations survive the Spring dependency-resolution process correctly. */ @Slf4j @Service public class AgentService { private final ReActAgent generalAgent; private final ReActAgent weatherAgent; private final ReActAgent fullAgent; private final ReActAgent collectorAgent; private final ReActAgent analyzerAgent; private final ReActAgent reporterAgent; @Value("${agentscope.session.storage-path:./sessions}") private String sessionStoragePath; public AgentService( @Qualifier("generalAgent") ReActAgent generalAgent, @Qualifier("weatherAgent") ReActAgent weatherAgent, @Qualifier("fullAgent") ReActAgent fullAgent, @Qualifier("collectorAgent") ReActAgent collectorAgent, @Qualifier("analyzerAgent") ReActAgent analyzerAgent, @Qualifier("reporterAgent") ReActAgent reporterAgent) { this.generalAgent = generalAgent; this.weatherAgent = weatherAgent; this.fullAgent = fullAgent; this.collectorAgent = collectorAgent; this.analyzerAgent = analyzerAgent; this.reporterAgent = reporterAgent; } // ----------------------------------------------------------------------- // Single-agent chat // ----------------------------------------------------------------------- /** * Simple stateless chat with the general assistant. */ public Mono
chat(String userMessage) { log.debug("AgentService.chat: {}", userMessage); Msg input = Msg.builder() .name("user") .textContent(userMessage) .build(); return generalAgent.call(input) .map(Msg::getTextContent) .doOnError(e -> log.error("Chat error", e)); } /** * Weather-aware chat — the agent will invoke weather tools automatically. */ public Mono weatherChat(String userMessage) { log.debug("AgentService.weatherChat: {}", userMessage); Msg input = Msg.builder() .name("user") .textContent(userMessage) .build(); return weatherAgent.call(input) .map(Msg::getTextContent) .doOnError(e -> log.error("WeatherChat error", e)); } /** * Full-capability chat — weather + calculator tools, with in-memory conversation history. */ public Mono fullChat(String userMessage) { log.debug("AgentService.fullChat: {}", userMessage); Msg input = Msg.builder() .name("user") .textContent(userMessage) .build(); return fullAgent.call(input) .map(Msg::getTextContent) .doOnError(e -> log.error("FullChat error", e)); } // ----------------------------------------------------------------------- // Session-aware chat (persistent conversation history via JsonSession) // ----------------------------------------------------------------------- /** * Multi-turn chat that persists the conversation to disk under {@code sessionId}. * *
Flow: load previous session → call agent → save updated session. */ public MonosessionChat(String sessionId, String userMessage) { log.debug("AgentService.sessionChat sessionId={}: {}", sessionId, userMessage); Session session = new JsonSession(Path.of(sessionStoragePath)); try { fullAgent.loadIfExists(session, sessionId); } catch (Exception e) { log.warn("Could not load session {}: {}", sessionId, e.getMessage()); } Msg input = Msg.builder() .name("user") .textContent(userMessage) .build(); return fullAgent.call(input) .map(response -> { try { fullAgent.saveTo(session, sessionId); } catch (Exception e) { log.warn("Could not save session {}: {}", sessionId, e.getMessage()); } return response.getTextContent(); }) .doOnError(e -> log.error("SessionChat error", e)); } // ----------------------------------------------------------------------- // Multi-agent pipelines // ----------------------------------------------------------------------- /** * Sequential pipeline: Collector → Analyzer → Reporter. * Each agent receives the previous agent's output as its input. * Uses {@link SequentialPipeline} whose {@code execute()} returns {@code Mono }. */ public Mono runSequentialPipeline(String topic) { log.debug("AgentService.runSequentialPipeline topic={}", topic); Msg input = Msg.builder() .name("user") .textContent(topic) .build(); return SequentialPipeline.builder() .addAgent(collectorAgent) .addAgent(analyzerAgent) .addAgent(reporterAgent) .build() .execute(input) .map(Msg::getTextContent) .doOnError(e -> log.error("Sequential pipeline error", e)); } /** * Parallel (fanout) pipeline: Collector, Analyzer, and Reporter all receive the same input * concurrently. {@link FanoutPipeline#execute} returns {@code Mono >} — one element * per agent — which we merge into a single labelled string. */ public Mono
runParallelPipeline(String topic) { log.debug("AgentService.runParallelPipeline topic={}", topic); Msg input = Msg.builder() .name("user") .textContent(topic) .build(); return FanoutPipeline.builder() .addAgent(collectorAgent) .addAgent(analyzerAgent) .addAgent(reporterAgent) .build() .execute(input) .map(msgs -> { String[] labels = {"[DataCollector]", "[DataAnalyzer]", "[ReportGenerator]"}; StringBuilder sb = new StringBuilder(); for (int i = 0; i < msgs.size(); i++) { sb.append(i < labels.length ? labels[i] : "[Agent-" + i + "]") .append("\n") .append(msgs.get(i).getTextContent()) .append("\n\n"); } return sb.toString().trim(); }) .doOnError(e -> log.error("Parallel pipeline error", e)); } }
AgentService.java 的核心作用:
- 封装Agent交互逻辑:将复杂的AgentScope API调用封装成简单的Java方法。
- 提供多种对话模式:基础聊天、工具增强聊天、会话状态管理(支持多轮对话)。
- 实现多智能体协作:定义了串行流水线和并行流水线两种工作流,前一个Agent的输出作为后一个Agent的输入,或同时向多个Agent发送请求并汇总结果。
- 响应式编程支持:所有方法返回Project Reactor的类型(
Mono<String>),非阻塞,提高高并发下的性能。
如果把 AgentConfig 比作**“招聘中心和装备库”(负责创建 Agent 并分发工具),那么 AgentService 就是“项目经理”**。它负责根据用户的需求(是简单聊天、查天气、还是写深度报告),调度合适的 Agent 员工去完成任务,并把最终结果整理好返回给用户。
4.6 添加自定义工具
接下来,我们增加两个自定义Tool:一个计算器工具和一个天气查询工具。实际业务中可以根据需求进行扩展。
CalculatorTools 完整代码:
package com.example.agentscope.tools;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* Basic arithmetic tools exposed to the agent for precise calculations.
*/
@Slf4j
@Component
public class CalculatorTools {
@Tool(name = "calculate",
description = "Perform basic arithmetic: add, subtract, multiply, divide. "
+ "Use this tool for any numerical calculation instead of estimating.")
public String calculate(
@ToolParam(name = "expression",
description = "A simple arithmetic expression, e.g. '(3 + 4) * 2' or '100 / 5'")
String expression) {
log.debug("CalculatorTools.calculate called with expression={}", expression);
try {
double result = evaluateSimple(expression);
return String.format("Result of [%s] = %s", expression, formatResult(result));
} catch (Exception e) {
return "Error evaluating expression: " + e.getMessage();
}
}
@Tool(name = "power",
description = "Compute base raised to the exponent (base^exponent).")
public String power(
@ToolParam(name = "base", description = "The base number") double base,
@ToolParam(name = "exponent", description = "The exponent") double exponent) {
double result = Math.pow(base, exponent);
return String.format("%s ^ %s = %s", base, exponent, formatResult(result));
}
@Tool(name = "sqrt",
description = "Compute the square root of a non-negative number.")
public String sqrt(
@ToolParam(name = "number", description = "Non-negative number") double number) {
if (number < 0) {
return "Error: cannot compute square root of a negative number.";
}
return String.format("sqrt(%s) = %s", number, formatResult(Math.sqrt(number)));
}
// -----------------------------------------------------------------------
// Minimal recursive-descent parser — supports +, -, *, /, parentheses
// -----------------------------------------------------------------------
private double evaluateSimple(String expr) {
return new ExprParser(expr.replaceAll("\\s+", "")).parse();
}
private String formatResult(double value) {
return (value == Math.floor(value) && !Double.isInfinite(value))
? String.valueOf((long) value)
: String.valueOf(value);
}
private static class ExprParser {
private final String input;
private int pos = 0;
ExprParser(String input) { this.input = input; }
double parse() { double v = addSub(); if (pos < input.length()) throw new IllegalArgumentException("Unexpected: " + input.charAt(pos)); return v; }
double addSub() {
double v = mulDiv();
while (pos < input.length() && (input.charAt(pos) == '+' || input.charAt(pos) == '-')) {
char op = input.charAt(pos++);
v = op == '+' ? v + mulDiv() : v - mulDiv();
}
return v;
}
double mulDiv() {
double v = unary();
while (pos < input.length() && (input.charAt(pos) == '*' || input.charAt(pos) == '/')) {
char op = input.charAt(pos++);
double rhs = unary();
if (op == '/' && rhs == 0) throw new ArithmeticException("Division by zero");
v = op == '*' ? v * rhs : v / rhs;
}
return v;
}
double unary() {
if (pos < input.length() && input.charAt(pos) == '-') { pos++; return -atom(); }
if (pos < input.length() && input.charAt(pos) == '+') { pos++; }
return atom();
}
double atom() {
if (pos < input.length() && input.charAt(pos) == '(') {
pos++;
double v = addSub();
if (pos >= input.length() || input.charAt(pos) != ')') throw new IllegalArgumentException("Missing ')'");
pos++;
return v;
}
int start = pos;
if (pos < input.length() && (Character.isDigit(input.charAt(pos)) || input.charAt(pos) == '.')) {
while (pos < input.length() && (Character.isDigit(input.charAt(pos)) || input.charAt(pos) == '.')) pos++;
return Double.parseDouble(input.substring(start, pos));
}
throw new IllegalArgumentException("Unexpected character: " + (pos < input.length() ? input.charAt(pos) : "EOF"));
}
}
}
WeatherTools 完整代码:
package com.example.agentscope.tools;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Random;
/**
* Mock weather tool — replace getWeatherFromApi() with a real HTTP call in production.
*/
@Slf4j
@Component
public class WeatherTools {
private static final Map MOCK_WEATHER = Map.of(
"beijing", "Beijing: Sunny, 28°C, Humidity 45%",
"shanghai", "Shanghai: Cloudy, 24°C, Humidity 70%",
"guangzhou", "Guangzhou: Rainy, 26°C, Humidity 85%",
"shenzhen", "Shenzhen: Partly cloudy, 27°C, Humidity 75%",
"hangzhou", "Hangzhou: Sunny, 25°C, Humidity 55%"
);
@Tool(name = "get_current_weather",
description = "Get the current weather for a specified city. Returns temperature, conditions and humidity.")
public String getCurrentWeather(
@ToolParam(name = "city", description = "City name (e.g. Beijing, Shanghai)") String city) {
log.debug("WeatherTools.getCurrentWeather called with city={}", city);
String result = MOCK_WEATHER.get(city.toLowerCase());
if (result == null) {
// Generic fallback for unknown cities
int temp = 15 + new Random().nextInt(20);
result = String.format("%s: Partly cloudy, %d°C, Humidity 60%%", city, temp);
}
return result;
}
@Tool(name = "get_weather_forecast",
description = "Get a 3-day weather forecast for a specified city.")
public String getWeatherForecast(
@ToolParam(name = "city", description = "City name") String city,
@ToolParam(name = "days", description = "Number of forecast days (1-7), default 3") int days) {
log.debug("WeatherTools.getWeatherForecast called with city={}, days={}", city, days);
int safeDays = Math.max(1, Math.min(days, 7));
StringBuilder sb = new StringBuilder(city).append(" ").append(safeDays).append("-day forecast:\n");
String[] conditions = {"Sunny", "Cloudy", "Rainy", "Partly cloudy", "Overcast"};
Random rnd = new Random();
for (int i = 1; i <= safeDays; i++) {
int high = 18 + rnd.nextInt(15);
int low = high - 5 - rnd.nextInt(5);
sb.append(String.format("Day %d: %s, High %d°C / Low %d°C%n",
i, conditions[rnd.nextInt(conditions.length)], high, low));
}
return sb.toString().trim();
}
}
4.7 添加测试接口
为了方便测试效果,我们添加一个接口类,并定义几个常用的接口:
package com.example.agentscope.controller;
import com.example.agentscope.dto.*;
import com.example.agentscope.service.AgentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
/**
* REST API for interacting with AgentScope agents.
*
* All endpoints accept JSON and return JSON.
* Streaming endpoints produce text/event-stream (SSE).
*
*
* Quick test with curl (after starting the server on port 8080):
*
* # 1. Simple chat
* curl -s -X POST http://localhost:8080/api/agent/chat \
* -H "Content-Type: application/json" \
* -d '{"message":"Hello, who are you?"}'
*
* # 2. Weather query
* curl -s -X POST http://localhost:8080/api/agent/weather \
* -H "Content-Type: application/json" \
* -d '{"message":"What is the weather like in Beijing today?"}'
*
* # 3. Session chat (multi-turn)
* curl -s -X POST http://localhost:8080/api/agent/session/chat \
* -H "Content-Type: application/json" \
* -d '{"message":"My name is Alice.","sessionId":"session-001"}'
*
* curl -s -X POST http://localhost:8080/api/agent/session/chat \
* -H "Content-Type: application/json" \
* -d '{"message":"What is my name?","sessionId":"session-001"}'
*
* # 4. Sequential multi-agent pipeline
* curl -s -X POST http://localhost:8080/api/agent/pipeline/sequential \
* -H "Content-Type: application/json" \
* -d '{"topic":"Artificial Intelligence in healthcare"}'
*
* # 5. Parallel multi-agent pipeline
* curl -s -X POST http://localhost:8080/api/agent/pipeline/parallel \
* -H "Content-Type: application/json" \
* -d '{"topic":"Electric vehicles market trends"}'
*
*/
@Slf4j
@RestController
@RequestMapping("/api/agent")
@RequiredArgsConstructor
public class AgentController {
private final AgentService agentService;
// -----------------------------------------------------------------------
// Single-agent endpoints
// -----------------------------------------------------------------------
/**
* POST /api/agent/chat
* General-purpose stateless chat.
*/
@PostMapping("/chat")
public Mono chat(@RequestBody ChatRequest request) {
log.info("POST /api/agent/chat message={}", request.getMessage());
return agentService.chat(request.getMessage())
.map(reply -> ChatResponse.ok("GeneralAssistant", reply))
.onErrorResume(e -> Mono.just(ChatResponse.error(e.getMessage())));
}
/**
* POST /api/agent/weather
* Chat with an agent that has weather tool access.
*/
@PostMapping("/weather")
public Mono weatherChat(@RequestBody ChatRequest request) {
log.info("POST /api/agent/weather message={}", request.getMessage());
return agentService.weatherChat(request.getMessage())
.map(reply -> ChatResponse.ok("WeatherAssistant", reply))
.onErrorResume(e -> Mono.just(ChatResponse.error(e.getMessage())));
}
/**
* POST /api/agent/full
* Chat with the full-capability agent (weather + calculator, with memory).
*/
@PostMapping("/full")
public Mono fullChat(@RequestBody ChatRequest request) {
log.info("POST /api/agent/full message={}", request.getMessage());
return agentService.fullChat(request.getMessage())
.map(reply -> ChatResponse.ok("FullAssistant", reply))
.onErrorResume(e -> Mono.just(ChatResponse.error(e.getMessage())));
}
// -----------------------------------------------------------------------
// Session-aware chat
// -----------------------------------------------------------------------
/**
* POST /api/agent/session/chat
* Multi-turn conversation — conversation history is persisted per sessionId.
* Send the same sessionId in subsequent requests to continue the conversation.
*/
@PostMapping("/session/chat")
public Mono sessionChat(@RequestBody ChatRequest request) {
String sessionId = request.getSessionId() != null
? request.getSessionId()
: "default-session";
log.info("POST /api/agent/session/chat sessionId={}, message={}", sessionId, request.getMessage());
return agentService.sessionChat(sessionId, request.getMessage())
.map(reply -> ChatResponse.ok("FullAssistant", reply, sessionId))
.onErrorResume(e -> Mono.just(ChatResponse.error(e.getMessage())));
}
// -----------------------------------------------------------------------
// Multi-agent pipeline endpoints
// -----------------------------------------------------------------------
/**
* POST /api/agent/pipeline/sequential
* Runs a 3-agent sequential pipeline: DataCollector → DataAnalyzer → ReportGenerator.
* Each agent enriches the output of the previous one.
*/
@PostMapping("/pipeline/sequential")
public Mono sequentialPipeline(@RequestBody PipelineRequest request) {
log.info("POST /api/agent/pipeline/sequential topic={}", request.getTopic());
return agentService.runSequentialPipeline(request.getTopic())
.map(result -> PipelineResponse.ok("sequential", request.getTopic(), result))
.onErrorResume(e -> Mono.just(PipelineResponse.error("sequential", e.getMessage())));
}
/**
* POST /api/agent/pipeline/parallel
* Runs DataCollector, DataAnalyzer, and ReportGenerator in parallel on the same input.
* Returns all three agents' outputs merged together.
*/
@PostMapping("/pipeline/parallel")
public Mono parallelPipeline(@RequestBody PipelineRequest request) {
log.info("POST /api/agent/pipeline/parallel topic={}", request.getTopic());
return agentService.runParallelPipeline(request.getTopic())
.map(result -> PipelineResponse.ok("parallel", request.getTopic(), result))
.onErrorResume(e -> Mono.just(PipelineResponse.error("parallel", e.getMessage())));
}
// -----------------------------------------------------------------------
// Health check
// -----------------------------------------------------------------------
/**
* GET /api/agent/health
* Simple health probe — verifies that the Spring context started correctly.
*/
@GetMapping(value = "/health", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono health() {
return Mono.just("{\"status\":\"UP\",\"framework\":\"AgentScope-Java\",\"version\":\"1.0.12\"}");
}
}
也可以使用下面的单元测试代码:
package com.example.agentscope; import com.example.agentscope.dto.ChatRequest; import com.example.agentscope.dto.ChatResponse; import com.example.agentscope.dto.PipelineRequest; import com.example.agentscope.dto.PipelineResponse; import com.example.agentscope.service.AgentService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; /** * Integration tests for AgentController using mocked AgentService. * *
These tests verify HTTP routing, request/response mapping and error handling * without making real LLM API calls. */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureWebTestClient class AgentControllerTest { @Autowired private WebTestClient webTestClient; @MockBean private AgentService agentService; // ----------------------------------------------------------------------- // Health check // ----------------------------------------------------------------------- @Test @DisplayName("GET /api/agent/health should return UP status") void healthCheck() { webTestClient.get() .uri("/api/agent/health") .exchange() .expectStatus().isOk() .expectBody(String.class) .value(body -> assertThat(body).contains("UP")); } // ----------------------------------------------------------------------- // Simple chat // ----------------------------------------------------------------------- @Test @DisplayName("POST /api/agent/chat should return agent reply") void chatSuccess() { when(agentService.chat(anyString())) .thenReturn(Mono.just("Hello! I am GeneralAssistant, your AI helper.")); ChatRequest request = new ChatRequest(); request.setMessage("Hello, who are you?"); webTestClient.post() .uri("/api/agent/chat") .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) .exchange() .expectStatus().isOk() .expectBody(ChatResponse.class) .value(response -> { assertThat(response.isSuccess()).isTrue(); assertThat(response.getReply()).isNotBlank(); assertThat(response.getAgentName()).isEqualTo("GeneralAssistant"); }); } @Test @DisplayName("POST /api/agent/chat should return error response when service fails") void chatServiceError() { when(agentService.chat(anyString())) .thenReturn(Mono.error(new RuntimeException("LLM API unavailable"))); ChatRequest request = new ChatRequest(); request.setMessage("Hello"); webTestClient.post() .uri("/api/agent/chat") .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) .exchange() .expectStatus().isOk() .expectBody(ChatResponse.class) .value(response -> { assertThat(response.isSuccess()).isFalse(); assertThat(response.getErrorMessage()).contains("LLM API unavailable"); }); } // ----------------------------------------------------------------------- // Weather chat // ----------------------------------------------------------------------- @Test @DisplayName("POST /api/agent/weather should return weather information") void weatherChatSuccess() { when(agentService.weatherChat(anyString())) .thenReturn(Mono.just("Beijing: Sunny, 28°C, Humidity 45%")); ChatRequest request = new ChatRequest(); request.setMessage("What is the weather in Beijing?"); webTestClient.post() .uri("/api/agent/weather") .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) .exchange() .expectStatus().isOk() .expectBody(ChatResponse.class) .value(response -> { assertThat(response.isSuccess()).isTrue(); assertThat(response.getAgentName()).isEqualTo("WeatherAssistant"); assertThat(response.getReply()).contains("Beijing"); }); } // ----------------------------------------------------------------------- // Session chat // ----------------------------------------------------------------------- @Test @DisplayName("POST /api/agent/session/chat should echo back session ID") void sessionChatEchoesSessionId() { when(agentService.sessionChat(anyString(), anyString())) .thenReturn(Mono.just("Nice to meet you, Alice!")); ChatRequest request = new ChatRequest(); request.setMessage("My name is Alice."); request.setSessionId("test-session-001"); webTestClient.post() .uri("/api/agent/session/chat") .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) .exchange() .expectStatus().isOk() .expectBody(ChatResponse.class) .value(response -> { assertThat(response.isSuccess()).isTrue(); assertThat(response.getSessionId()).isEqualTo("test-session-001"); }); } @Test @DisplayName("POST /api/agent/session/chat should use 'default-session' when sessionId is null") void sessionChatDefaultSession() { when(agentService.sessionChat(anyString(), anyString())) .thenReturn(Mono.just("Hello!")); ChatRequest request = new ChatRequest(); request.setMessage("Hello"); // sessionId intentionally null webTestClient.post() .uri("/api/agent/session/chat") .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) .exchange() .expectStatus().isOk() .expectBody(ChatResponse.class) .value(response -> { assertThat(response.isSuccess()).isTrue(); assertThat(response.getSessionId()).isEqualTo("default-session"); }); } // ----------------------------------------------------------------------- // Sequential pipeline // ----------------------------------------------------------------------- @Test @DisplayName("POST /api/agent/pipeline/sequential should return pipeline result") void sequentialPipelineSuccess() { when(agentService.runSequentialPipeline(anyString())) .thenReturn(Mono.just("Comprehensive AI in healthcare report generated.")); PipelineRequest request = new PipelineRequest(); request.setTopic("AI in healthcare"); webTestClient.post() .uri("/api/agent/pipeline/sequential") .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) .exchange() .expectStatus().isOk() .expectBody(PipelineResponse.class) .value(response -> { assertThat(response.isSuccess()).isTrue(); assertThat(response.getPipelineType()).isEqualTo("sequential"); assertThat(response.getTopic()).isEqualTo("AI in healthcare"); assertThat(response.getResult()).isNotBlank(); }); } @Test @DisplayName("POST /api/agent/pipeline/sequential should return error on failure") void sequentialPipelineError() { when(agentService.runSequentialPipeline(anyString())) .thenReturn(Mono.error(new RuntimeException("Pipeline timeout"))); PipelineRequest request = new PipelineRequest(); request.setTopic("Irrelevant"); webTestClient.post() .uri("/api/agent/pipeline/sequential") .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) .exchange() .expectStatus().isOk() .expectBody(PipelineResponse.class) .value(response -> { assertThat(response.isSuccess()).isFalse(); assertThat(response.getErrorMessage()).contains("Pipeline timeout"); }); } // ----------------------------------------------------------------------- // Parallel pipeline // ----------------------------------------------------------------------- @Test @DisplayName("POST /api/agent/pipeline/parallel should return merged result") void parallelPipelineSuccess() { when(agentService.runParallelPipeline(anyString())) .thenReturn(Mono.just("[DataCollector]\nEV facts...\n\n[DataAnalyzer]\nTrend analysis...\n\n[ReportGenerator]\nSummary...")); PipelineRequest request = new PipelineRequest(); request.setTopic("Electric vehicles"); webTestClient.post() .uri("/api/agent/pipeline/parallel") .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) .exchange() .expectStatus().isOk() .expectBody(PipelineResponse.class) .value(response -> { assertThat(response.isSuccess()).isTrue(); assertThat(response.getPipelineType()).isEqualTo("parallel"); assertThat(response.getResult()).isNotBlank(); }); } }
4.8 效果测试
基本对话测试

天气查询测试

其他测试用例(如计算器、多Agent协作等)有兴趣的同学可以继续验证。
五、总结与展望
本文从Java开发者的实际痛点出发,详细介绍了AgentScope Java框架的核心能力、ReAct范式、与同类产品的对比,并通过一个完整的SpringBoot实战案例,演示了从配置、工具注册、服务封装到效果测试的全过程。AgentScope Java真正做到了“Java原生、企业级安全、持续进化”,是Java生态中构建生产级AI智能体的最佳选择之一。希望本文能帮助你快速上手,在实际项目中落地AI智能体能力,提升开发效率与业务价值。
然而,模型一旦部署上线,其能力便趋于静态。若无法持续从真实用户交互中学习,Agent 很难适应业务变化、工具演进或用户行为漂移,长期效果将逐渐退化。
如果你对AgentScope Java有更多疑问,欢迎在评论区留言交流。后续我会继续分享关于多Agent协作、RAG集成、性能优化等进阶内容,敬请期待!
---技能提升
如果你觉得本文有帮助,以下资源可以帮你深入学习:
- 玩转Spring全家桶 — 全面掌握Spring生态体系
- Android开发高手课 — 突破Android开发进阶瓶颈
- Java核心技术面试精讲 — 以面试题为切入点,有效提升Java内功
开发资源
浙公网安备 33010602011771号