大模型能力日渐成熟,智能体(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.5Java 17AgentScope 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 Mono sessionChat(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集成、性能优化等进阶内容,敬请期待!

---

技能提升

如果你觉得本文有帮助,以下资源可以帮你深入学习:


开发资源