SpringBoot 集成 AgentScope Java

SpringBoot 集成 AgentScope Java

  • [1 依赖](#1 依赖)
  • [2 配置](#2 配置)
  • [3 代码](#3 代码)
    • [1 配置](#1 配置)
    • [2 工具](#2 工具)
    • [3 请求](#3 请求)
    • [4 记忆](#4 记忆)
  • [4 测试](#4 测试)
框架 维护方/背景 大致发布时间 核心定位 LLM 支持 向量库 编排范式 状态管理 循环/分支 多Agent协作 适用场景
Spring AI Spring 官方(VMware) 2024.02 Spring 生态 AI 能力基础集成框架 20+ 主流模型(OpenAI、Anthropic、通义、文心等) 15+(Milvus、PGVector、Redis 等) 链式(Chain)编排 弱,需开发者自行维护会话状态 弱,原生不支持复杂流程控制 弱,需自定义扩展实现 Spring Boot 项目快速接入 AI 基础能力
LangChain4j 开源社区主导 2023.01 Java 生态通用 LLM/Agent 工具集,对标 Python LangChain 20+ 主流及国产模型(通义、文心、智谱等原生支持) 30+,Java 生态覆盖最广 链式编排 + 基础 Agent 范式 基础 Memory 模块,支持会话记忆 有限支持,需通过代码逻辑实现 中等,支持简单多角色 Agent 交互 非 Spring 栈项目、RAG 应用开发、多模型兼容需求
Spring AI Alibaba 阿里云官方 2024.09 基于 Spring AI 的企业级 AI 工作流与 Agent 编排框架 继承 Spring AI + 深度集成通义、DeepSeek 等国产模型 继承 Spring AI + 阿里云原生向量服务 Graph 图式 + Workflow 工作流编排 有状态,支持工作流上下文持久化 强,原生支持条件分支、循环执行 强,支持可视化多 Agent 角色编排 企业级 AI 工作流、国产模型落地、阿里云生态项目
AgentScope Java 阿里通义实验室 2025.12 生产级自治智能体运行框架,主打 ReAct 范式 原生支持通义、OpenAI、Anthropic 等主流模型 内置向量能力 + 适配主流向量数据库 ReAct 自治范式(LLM 主导决策) 强,内置会话、记忆、工具调用全链路状态 强,由 LLM 自主控制执行循环与分支 强,支持动态协作、子 Agent 委派 复杂任务自主规划、生产级自治 Agent、多 Agent 协作系统
AgentHarness Java 开源社区 / 阿里联合 2026.04 Agent 工程化运行外壳,解决 Agent 从 Demo 到生产的稳定性问题 依赖底层 AgentScope / Spring AI,继承其模型能力 依赖底层框架,继承其向量库支持 Harness 封装式可控自治范式 强,支持分布式会话持久化、多租户工作区隔离 强,支持可控循环、异常分支与熔断 强,支持会话隔离下的子 Agent 委派与协作 分布式多用户 Agent 系统、长期运行的企业级 Agent、高可用生产环境
LangGraph4j 开源社区 2025 年初 Java 版有状态图式工作流编排框架,对标 Python LangGraph 不直接对接模型,为编排层,依赖 LangChain4j / Spring AI 等底层框架 不直接对接,依赖底层框架的向量库能力 StateGraph 状态图式编排 内置全局 AgentState,原生支持状态读写、持久化与回溯 原生强支持,内置条件边、循环、回退、分支能力 强,支持多角色、多节点 Agent 的图式协作编排 复杂有状态 AI 工作流、需要循环/回退的多 Agent 系统、RAG + 工作流结合场景

1 依赖

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.xu</groupId>
    <artifactId>agentscope</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>25</java.version>
    </properties>

    <dependencies>

        <!-- webmvc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>

        <!-- spring-boot-agentscope -->
        <dependency>
            <groupId>io.agentscope</groupId>
            <artifactId>agentscope-spring-boot-starter</artifactId>
            <version>2.0.0-RC2</version>
        </dependency>

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!-- webmvc-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                    <execution>
                        <id>default-testCompile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

2 配置

yml 复制代码
spring:
  application:
    name: agentscope
  data:
    redis:
      host: ${REDIS_HOST:127.0.0.1}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:123456}
      timeout: 5s
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 2

3 代码

1 配置

java 复制代码
package com.xu.agentscope.conf;

import io.agentscope.core.ReActAgent;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.OpenAIChatModel;
import io.agentscope.core.state.AgentStateStore;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.coding.ShellCommandTool;
import com.xu.agentscope.state.RedisAgentStateStore;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * AgentScope 核心配置 ------ 定义三个 {@link ReActAgent} Bean 及共享组件。
 *
 * <p>每个 Agent 已预配置系统提示词、生成参数(温度、最大 Token)、最大推理步数、
 * 工具集({@link ShellCommandTool})和 Redis 会话持久化,
 * Controller 注入后可直接调用,无需每次请求重新构建。</p>
 */
@Configuration
public class AgentScopeConfig {

    @Value("${DASHSCOPE_API_KEY:你的API_KEY}")
    private String apiKey;

    @Value("${DASHSCOPE_API_BASE_URL:你的BASE_URL}")
    private String baseUrl;

    /**
     * 共享工具集 ------ 注册 {@link ShellCommandTool},允许 Agent 执行 shell 命令。
     */
    @Bean
    public Toolkit toolkit() {
        Toolkit toolkit = new Toolkit();
        toolkit.registerAgentTool(new ShellCommandTool());
        return toolkit;
    }

    /**
     * Redis 持久化存储 ------ 用于保存和恢复会话聊天历史。
     */
    @Bean
    public AgentStateStore agentStateStore(RedisTemplate<String, String> redisTemplate) {
        return new RedisAgentStateStore(redisTemplate);
    }

    /**
     * 对话 Agent ------ 用于普通文本对话、问答等场景。
     */
    @Bean
    public ReActAgent llmAgent(AgentStateStore vector, Toolkit toolkit) {
        return ReActAgent.builder()
                .name("assistant")
                .sysPrompt("You are a helpful AI assistant.")
                .model(OpenAIChatModel.builder()
                        .apiKey(apiKey)
                        .baseUrl(baseUrl)
                        .modelName("deepseek-v4-flash")
                        .stream(true)
                        .build())
                .toolkit(toolkit.copy())
                .stateStore(vector)
                .maxIters(3)
                .generateOptions(GenerateOptions.builder()
                        .temperature(0.7)
                        .maxTokens(4096)
                        .build())
                .build();
    }

    /**
     * 视觉 Agent ------ 用于图像理解、图片描述等视觉任务。
     */
    @Bean
    public ReActAgent visionAgent(AgentStateStore vector, Toolkit toolkit) {
        return ReActAgent.builder()
                .name("vision")
                .sysPrompt("You are a visual analysis assistant. Describe images accurately and help users understand visual content.")
                .model(OpenAIChatModel.builder()
                        .apiKey(apiKey)
                        .baseUrl(baseUrl)
                        .modelName("deepseek-v4-flash")
                        .stream(true)
                        .build())
                .toolkit(toolkit.copy())
                .stateStore(vector)
                .maxIters(3)
                .generateOptions(GenerateOptions.builder()
                        .temperature(0.5)
                        .maxTokens(4096)
                        .build())
                .build();
    }

    /**
     * 全模态 Agent ------ 支持文本、图像、音频等混合输入的综合推理。
     */
    @Bean
    public ReActAgent multimodalAgent(AgentStateStore vector, Toolkit toolkit) {
        return ReActAgent.builder()
                .name("multimodal")
                .sysPrompt("You are a multimodal assistant capable of understanding text, images, audio, and video. Analyze all provided content comprehensively.")
                .model(OpenAIChatModel.builder()
                        .apiKey(apiKey)
                        .baseUrl(baseUrl)
                        .modelName("deepseek-v4-flash")
                        .stream(true)
                        .build())
                .toolkit(toolkit.copy())
                .stateStore(vector)
                .maxIters(3)
                .generateOptions(GenerateOptions.builder()
                        .temperature(0.6)
                        .maxTokens(4096)
                        .build())
                .build();
    }

}

2 工具

java 复制代码
package com.xu.agentscope.utils;

import io.agentscope.core.event.AgentEvent;
import io.agentscope.core.event.TextBlockDeltaEvent;
import io.agentscope.core.event.ThinkingBlockDeltaEvent;
import io.agentscope.core.event.ToolCallDeltaEvent;
import io.agentscope.core.event.ToolCallStartEvent;

import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;

/**
 * SSE (Server-Sent Events) 事件发射工具类。
 *
 * <p>将 AgentScope 的事件流转换为 SSE 事件,推送给前端。
 * 所有控制器共享此类中的事件处理逻辑。</p>
 */
public final class EmitterHelper {

    private EmitterHelper() {}

    /**
     * 发送空的"完成"信号后关闭发射器。
     *
     * @param emitter SSE 发射器
     */
    public static void completeEmpty(SseEmitter emitter) {
        try {
            emitter.send(SseEmitter.event().name("done").data(""));
            emitter.complete();
        } catch (IOException ex) {
            emitter.completeWithError(ex);
        }
    }

    /**
     * 处理 AgentScope 事件并转发为 SSE 事件。
     *
     * <p>事件类型映射:</p>
     * <ul>
     *   <li>{@link TextBlockDeltaEvent} → {@code text_delta}</li>
     *   <li>{@link ThinkingBlockDeltaEvent} → {@code thinking_delta}</li>
     *   <li>{@link ToolCallStartEvent} → {@code tool_start}</li>
     *   <li>{@link ToolCallDeltaEvent} → {@code tool_delta}</li>
     * </ul>
     *
     * @param emitter SSE 发射器
     * @param event   AgentScope 事件
     * @param buffer  用于累积文本片段的缓冲区(在流结束时输出完整结果)
     */
    public static void handleEvent(SseEmitter emitter, AgentEvent event,
                                   AtomicReference<StringBuilder> buffer) {
        try {
            switch (event) {
                case TextBlockDeltaEvent e -> {
                    buffer.get().append(e.getDelta());
                    emitter.send(SseEmitter.event()
                            .name("text_delta").data(e.getDelta()));
                }
                case ThinkingBlockDeltaEvent e ->
                        emitter.send(SseEmitter.event()
                                .name("thinking_delta").data(e.getDelta()));
                case ToolCallStartEvent e ->
                        emitter.send(SseEmitter.event()
                                .name("tool_start").data(e.getToolCallName()));
                case ToolCallDeltaEvent e ->
                        emitter.send(SseEmitter.event()
                                .name("tool_delta").data(e.getDelta()));
                default -> { }
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}
java 复制代码
package com.xu.agentscope.utils;

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.Base64Source;
import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.DataBlock;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.message.UserMessage;

import lombok.SneakyThrows;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

/**
 * 消息构建工具类 ------ 将请求参数转换为 AgentScope 的 {@link UserMessage} 和 {@link RuntimeContext}。
 */
public final class MessageHelper {

    private MessageHelper() {}

    /**
     * 构建包含文本和可选文件的多模态用户消息。
     *
     * <p>上传文件通过 Base64 编码后以 {@link DataBlock} 形式附加到消息中。</p>
     *
     * @param message 用户输入的文本(必填)
     * @param files   上传的文件列表(可为 null 或空)
     * @return 构建好的 {@link UserMessage}
     */
    public static UserMessage buildUserMessage(String message, List<MultipartFile> files) {
        List<ContentBlock> blocks = new ArrayList<>();
        blocks.add(TextBlock.builder().text(message).build());

        if (files != null) {
            for (MultipartFile file : files) {
                if (file != null && !file.isEmpty()) {
                    blocks.add(toDataBlock(file));
                }
            }
        }
        return new UserMessage(blocks);
    }

    /**
     * 将 {@link MultipartFile} 转为 Base64 编码的 {@link DataBlock}。
     */
    @SneakyThrows
    private static DataBlock toDataBlock(MultipartFile file) {
        String base64 = Base64.getEncoder().encodeToString(file.getBytes());
        return DataBlock.builder()
                .name(file.getOriginalFilename())
                .source(Base64Source.builder()
                        .mediaType(file.getContentType())
                        .data(base64)
                        .build())
                .build();
    }

    /**
     * 构建带 sessionId 的 {@link RuntimeContext},用于多轮对话上下文恢复。
     *
     * @param sessionId 会话标识,为 null 或空时使用 Agent 默认会话
     * @return 配置好的 RuntimeContext
     */
    public static RuntimeContext buildContext(String sessionId) {
        RuntimeContext.Builder builder = RuntimeContext.builder();
        if (sessionId != null && !sessionId.isBlank()) {
            builder.sessionId(sessionId);
        }
        return builder.build();
    }
}

3 请求

java 复制代码
package com.xu.agentscope.controller;

import com.xu.agentscope.utils.EmitterHelper;
import com.xu.agentscope.utils.MessageHelper;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.UserMessage;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
 * 对话 Agent 控制器。
 *
 * <p>提供普通响应和 SSE 流式响应两个端点,支持可选的附件上传和会话 ID。
 * 使用预构建的 {@link ReActAgent}(llmAgent),系统提示词、温度等参数已在配置类中固化。</p>
 *
 * <p>请求示例(multipart/form-data):</p>
 * <pre>{@code
 * curl -X POST http://localhost:8080/api/llm/chat -F "message=你好" -F "sessionId=abc123"
 * curl -X POST http://localhost:8080/api/llm/chat -F "message=分析这张图" -F "files=@photo.jpg"
 * }</pre>
 */
@RestController
@RequestMapping("/api/llm")
public class LlmController {

    private final ReActAgent agent;

    public LlmController(@Qualifier("llmAgent") ReActAgent agent) {
        this.agent = agent;
    }

    /**
     * 普通响应 ------ 非流式调用 Agent,等待完整回复后返回。
     *
     * @param message   用户输入的消息文本(必填)
     * @param files     上传的文件列表(可选)
     * @param sessionId 会话 ID,用于多轮对话上下文保持(可选)
     * @return 包含 {@code response} 字段的 JSON 对象
     */
    @PostMapping(value = "/chat", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Object chat(@RequestPart String message,
                       @RequestPart(required = false) List<MultipartFile> files,
                       @RequestPart(required = false) String sessionId) {
        UserMessage msg = MessageHelper.buildUserMessage(message, files);
        return agent.call(List.of(msg), MessageHelper.buildContext(sessionId)).block();
    }

    /**
     * 流式响应 ------ 通过 SSE (Server-Sent Events) 实时推送文本增量。
     *
     * <p>事件类型:</p>
     * <ul>
     *   <li>{@code text_delta} ------ 文本片段增量</li>
     *   <li>{@code thinking_delta} ------ 思考过程片段增量</li>
     *   <li>{@code tool_start} ------ 工具调用开始</li>
     *   <li>{@code tool_delta} ------ 工具调用参数增量</li>
     *   <li>{@code result} ------ 最终完整文本</li>
     *   <li>{@code done} ------ 流结束信号</li>
     *   <li>{@code error} ------ 错误信息</li>
     * </ul>
     *
     * @param message   用户输入的消息文本(必填)
     * @param files     上传的文件列表(可选)
     * @param sessionId 会话 ID,用于多轮对话上下文保持(可选)
     * @return SSE 事件流
     */
    @PostMapping(value = "/chat/stream",
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
            produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter chatStream(@RequestPart String message,
                                 @RequestPart(required = false) List<MultipartFile> files,
                                 @RequestPart(required = false) String sessionId) {
        SseEmitter emitter = new SseEmitter(0L);
        UserMessage msg = MessageHelper.buildUserMessage(message, files);
        AtomicReference<StringBuilder> buffer = new AtomicReference<>(new StringBuilder());

        agent.streamEvents(List.of(msg), MessageHelper.buildContext(sessionId))
                .subscribe(
                        event -> EmitterHelper.handleEvent(emitter, event, buffer),
                        error -> {
                            try {
                                emitter.send(SseEmitter.event()
                                        .name("error").data(error.getMessage()));
                            } catch (IOException ignored) {
                            }
                            emitter.complete();
                        },
                        () -> {
                            try {
                                emitter.send(SseEmitter.event()
                                        .name("result").data(buffer.get().toString()));
                                emitter.send(SseEmitter.event().name("done").data(""));
                            } catch (IOException ignored) {
                            }
                            emitter.complete();
                        });

        return emitter;
    }
}
java 复制代码
package com.xu.agentscope.controller;

import com.xu.agentscope.utils.EmitterHelper;
import com.xu.agentscope.utils.MessageHelper;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.UserMessage;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
 * 全模态 Agent 控制器。
 *
 * <p>使用预构建的 {@link ReActAgent}(multimodalAgent),支持文本、图像、音频等混合输入。
 * 请求参数仅需文本和可选文件、会话 ID,其他配置已在 Agent 构建时固化。</p>
 *
 * <p>请求示例(multipart/form-data):</p>
 * <pre>{@code
 * curl -X POST http://localhost:8080/api/multimodal/chat -F "message=分析这个文件" -F "files=@audio.mp3" -F "sessionId=abc123"
 * }</pre>
 */
@RestController
@RequestMapping("/api/multimodal")
public class MultimodalController {

    private final ReActAgent agent;

    public MultimodalController(@Qualifier("multimodalAgent") ReActAgent agent) {
        this.agent = agent;
    }

    /**
     * 普通响应 ------ 非流式调用全模态 Agent。
     *
     * @param message   用户输入的消息文本(必填)
     * @param files     上传的文件列表(可选,支持图片、音频等)
     * @param sessionId 会话 ID,用于多轮对话上下文保持(可选)
     * @return 包含 {@code response} 字段的 JSON 对象
     */
    @PostMapping(value = "/chat", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Object chat(@RequestPart String message,
                       @RequestPart(required = false) List<MultipartFile> files,
                       @RequestPart(required = false) String sessionId) {
        UserMessage msg = MessageHelper.buildUserMessage(message, files);
        return agent.call(List.of(msg), MessageHelper.buildContext(sessionId)).block();
    }

    /**
     * 流式响应 ------ 通过 SSE 实时推送全模态 Agent 回复。
     *
     * @param message   用户输入的消息文本(必填)
     * @param files     上传的文件列表(可选)
     * @param sessionId 会话 ID,用于多轮对话上下文保持(可选)
     * @return SSE 事件流
     */
    @PostMapping(value = "/chat/stream",
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
            produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter chatStream(@RequestPart String message,
                                 @RequestPart(required = false) List<MultipartFile> files,
                                 @RequestPart(required = false) String sessionId) {
        SseEmitter emitter = new SseEmitter(0L);
        UserMessage msg = MessageHelper.buildUserMessage(message, files);
        AtomicReference<StringBuilder> buffer = new AtomicReference<>(new StringBuilder());

        agent.streamEvents(List.of(msg), MessageHelper.buildContext(sessionId))
                .subscribe(
                        event -> EmitterHelper.handleEvent(emitter, event, buffer),
                        error -> {
                            try {
                                emitter.send(SseEmitter.event()
                                        .name("error").data(error.getMessage()));
                            } catch (IOException ignored) {
                            }
                            emitter.complete();
                        },
                        () -> {
                            try {
                                emitter.send(SseEmitter.event()
                                        .name("result").data(buffer.get().toString()));
                                emitter.send(SseEmitter.event().name("done").data(""));
                            } catch (IOException ignored) {
                            }
                            emitter.complete();
                        });

        return emitter;
    }
}
java 复制代码
package com.xu.agentscope.controller;

import com.xu.agentscope.utils.EmitterHelper;
import com.xu.agentscope.utils.MessageHelper;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.UserMessage;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
 * 视觉 Agent 控制器。
 *
 * <p>使用预构建的 {@link ReActAgent}(visionAgent),用于图像理解、图片描述等视觉任务。
 * 请求参数仅需文本和可选图片文件、会话 ID,其他配置已在 Agent 构建时固化。</p>
 *
 * <p>请求示例(multipart/form-data):</p>
 * <pre>{@code
 * curl -X POST http://localhost:8080/api/vision/chat -F "message=描述这张图片" -F "files=@photo.jpg" -F "sessionId=abc123"
 * }</pre>
 */
@RestController
@RequestMapping("/api/vision")
public class VisionController {

    private final ReActAgent agent;

    public VisionController(@Qualifier("visionAgent") ReActAgent agent) {
        this.agent = agent;
    }

    /**
     * 普通响应 ------ 非流式调用视觉 Agent。
     *
     * @param message   用户输入的消息文本(必填)
     * @param files     上传的图片文件列表(可选)
     * @param sessionId 会话 ID,用于多轮对话上下文保持(可选)
     * @return 包含 {@code response} 字段的 JSON 对象
     */
    @PostMapping(value = "/chat", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Object chat(@RequestPart String message,
                       @RequestPart(required = false) List<MultipartFile> files,
                       @RequestPart(required = false) String sessionId) {
        UserMessage msg = MessageHelper.buildUserMessage(message, files);
        return agent.call(List.of(msg), MessageHelper.buildContext(sessionId)).block();
    }

    /**
     * 流式响应 ------ 通过 SSE 实时推送视觉 Agent 回复。
     *
     * @param message   用户输入的消息文本(必填)
     * @param files     上传的图片文件列表(可选)
     * @param sessionId 会话 ID,用于多轮对话上下文保持(可选)
     * @return SSE 事件流
     */
    @PostMapping(value = "/chat/stream",
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
            produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter chatStream(@RequestPart String message,
                                 @RequestPart(required = false) List<MultipartFile> files,
                                 @RequestPart(required = false) String sessionId) {
        SseEmitter emitter = new SseEmitter(0L);
        UserMessage msg = MessageHelper.buildUserMessage(message, files);
        AtomicReference<StringBuilder> buffer = new AtomicReference<>(new StringBuilder());

        agent.streamEvents(List.of(msg), MessageHelper.buildContext(sessionId))
                .subscribe(
                        event -> EmitterHelper.handleEvent(emitter, event, buffer),
                        error -> {
                            try {
                                emitter.send(SseEmitter.event()
                                        .name("error").data(error.getMessage()));
                            } catch (IOException ignored) {
                            }
                            emitter.complete();
                        },
                        () -> {
                            try {
                                emitter.send(SseEmitter.event()
                                        .name("result").data(buffer.get().toString()));
                                emitter.send(SseEmitter.event().name("done").data(""));
                            } catch (IOException ignored) {
                            }
                            emitter.complete();
                        });

        return emitter;
    }
}

4 记忆

java 复制代码
package com.xu.agentscope.state;

import io.agentscope.core.state.AgentState;
import io.agentscope.core.state.AgentStateStore;
import io.agentscope.core.state.State;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.springframework.data.redis.core.RedisTemplate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 基于 Redis 的 {@link AgentStateStore} 实现。
 *
 * <p>将 Agent 的会话状态(聊天历史、工具上下文等)持久化到 Redis,
 * 支持多轮对话的上下文恢复。所有状态数据以 JSON 格式存储。</p>
 *
 * <p><b>Redis Key 结构:</b></p>
 * <pre>
 * # 单值状态(如 AgentState)
 * agentscope:session:{userId}:{sessionId}:single:{key}
 *
 * # 列表状态(如消息历史)
 * agentscope:session:{userId}:{sessionId}:list:{key}
 *
 * # 会话索引(记录某个用户下的所有 sessionId)
 * agentscope:session:{userId}:index
 * </pre>
 *
 * <p>匿名用户(userId 为空)统一映射为 {@code __anon__}。所有 Key 在 7 天无访问后自动过期。</p>
 */
public class RedisAgentStateStore implements AgentStateStore {

    private static final Logger log = LoggerFactory.getLogger(RedisAgentStateStore.class);
    private static final long TTL_DAYS = 7;
    private static final String ANON = "__anon__";
    private static final String SCOPE_KEY = "agentscope:session";

    private final RedisTemplate<String, String> redisTemplate;

    /**
     * 构造 Redis 状态存储。
     *
     * @param redisTemplate Spring Data Redis 模板(由 {@code spring-boot-starter-data-redis} 自动配置)
     */
    public RedisAgentStateStore(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    private static String slot(String userId, String sessionId) {
        String uid = (userId == null || userId.isBlank()) ? ANON : userId;
        return uid + ":" + sessionId;
    }

    private static String singleKey(String userId, String sessionId, String key) {
        return SCOPE_KEY + ":" + slot(userId, sessionId) + ":single:" + key;
    }

    private static String listKey(String userId, String sessionId, String key) {
        return SCOPE_KEY + ":" + slot(userId, sessionId) + ":list:" + key;
    }

    private static String indexKey(String userId) {
        String uid = (userId == null || userId.isBlank()) ? ANON : userId;
        return SCOPE_KEY + ":" + uid + ":index";
    }

    /**
     * 更新 Key 的过期时间(每次读写时续期)。
     */
    private void setTtl(String redisKey) {
        redisTemplate.expire(redisKey, TTL_DAYS, TimeUnit.DAYS);
    }

    /**
     * 保存单个状态对象到 Redis。
     *
     * @param userId    用户标识(可为 null,表示匿名用户)
     * @param sessionId 会话标识
     * @param key       状态键名(如 "agent_state")
     * @param value     状态对象
     */
    @Override
    public void save(String userId, String sessionId, String key, State value) {
        if (value == null) {
            return;
        }
        String redisKey = singleKey(userId, sessionId, key);
        String json = serializeSingle(value);
        redisTemplate.opsForValue().set(redisKey, json);
        setTtl(redisKey);

        redisTemplate.opsForSet().add(indexKey(userId), sessionId);
        setTtl(indexKey(userId));

        log.debug("已保存单值状态: key={}", redisKey);
    }

    /**
     * 保存列表状态到 Redis(使用 Redis List 结构)。
     *
     * <p>每次保存会先清空旧数据,再写入新列表。</p>
     *
     * @param userId    用户标识
     * @param sessionId 会话标识
     * @param key       状态键名(如 "memory_messages")
     * @param values    状态列表
     */
    @Override
    public void save(String userId, String sessionId, String key, List<? extends State> values) {
        if (values == null || values.isEmpty()) {
            return;
        }
        String redisKey = listKey(userId, sessionId, key);
        redisTemplate.delete(redisKey);

        List<String> jsons = values.stream()
                .map(this::serializeSingle)
                .collect(Collectors.toList());
        redisTemplate.opsForList().rightPushAll(redisKey, jsons);
        setTtl(redisKey);

        redisTemplate.opsForSet().add(indexKey(userId), sessionId);
        setTtl(indexKey(userId));

        log.debug("已保存列表状态: key={}, size={}", redisKey, values.size());
    }

    /**
     * 从 Redis 读取单个状态对象。
     *
     * @param userId    用户标识
     * @param sessionId 会话标识
     * @param key       状态键名
     * @param type      状态类型
     * @return 状态对象,不存在时返回 {@link Optional#empty()}
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T extends State> Optional<T> get(String userId, String sessionId, String key, Class<T> type) {
        String redisKey = singleKey(userId, sessionId, key);
        String json = redisTemplate.opsForValue().get(redisKey);
        if (json == null || json.isBlank()) {
            return Optional.empty();
        }
        try {
            T value = deserializeSingle(json, type);
            setTtl(redisKey);
            return Optional.ofNullable(value);
        } catch (Exception e) {
            log.warn("反序列化单值状态失败: key={}", redisKey, e);
            return Optional.empty();
        }
    }

    /**
     * 从 Redis 读取列表状态。
     *
     * @param userId    用户标识
     * @param sessionId 会话标识
     * @param key       状态键名
     * @param itemType  列表元素类型
     * @return 状态列表,不存在时返回空列表
     */
    @Override
    public <T extends State> List<T> getList(String userId, String sessionId, String key, Class<T> itemType) {
        String redisKey = listKey(userId, sessionId, key);
        Long size = redisTemplate.opsForList().size(redisKey);
        if (size == null || size == 0) {
            return Collections.emptyList();
        }
        List<String> jsons = redisTemplate.opsForList().range(redisKey, 0, size - 1);
        if (jsons == null || jsons.isEmpty()) {
            return Collections.emptyList();
        }
        setTtl(redisKey);

        List<T> results = new ArrayList<>();
        for (String json : jsons) {
            try {
                T value = deserializeSingle(json, itemType);
                if (value != null) {
                    results.add(value);
                }
            } catch (Exception e) {
                log.warn("反序列化列表项失败: key={}", redisKey, e);
            }
        }
        return results;
    }

    /**
     * 检查指定会话是否存在。
     *
     * @param userId    用户标识
     * @param sessionId 会话标识
     * @return 存在返回 {@code true}
     */
    @Override
    public boolean exists(String userId, String sessionId) {
        String pattern = SCOPE_KEY + ":" + slot(userId, sessionId) + ":*";
        Set<String> keys = redisTemplate.keys(pattern);
        return keys != null && !keys.isEmpty();
    }

    /**
     * 删除指定会话的全部状态数据。
     *
     * @param userId    用户标识
     * @param sessionId 会话标识
     */
    @Override
    public void delete(String userId, String sessionId) {
        String pattern = SCOPE_KEY + ":" + slot(userId, sessionId) + ":*";
        Set<String> keys = redisTemplate.keys(pattern);
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
            log.debug("已删除会话: userId={}, sessionId={}, keys={}", userId, sessionId, keys.size());
        }
        redisTemplate.opsForSet().remove(indexKey(userId), sessionId);
    }

    /**
     * 删除指定会话下某个状态键的数据。
     *
     * @param userId    用户标识
     * @param sessionId 会话标识
     * @param key       状态键名
     */
    @Override
    public void delete(String userId, String sessionId, String key) {
        redisTemplate.delete(singleKey(userId, sessionId, key));
        redisTemplate.delete(listKey(userId, sessionId, key));
    }

    /**
     * 列出指定用户的所有会话 ID。
     *
     * @param userId 用户标识
     * @return 会话 ID 集合
     */
    @Override
    public Set<String> listSessionIds(String userId) {
        Set<String> ids = redisTemplate.opsForSet().members(indexKey(userId));
        return ids != null ? ids : Collections.emptySet();
    }

    /**
     * 关闭存储(连接由 Spring 管理,无需手动释放)。
     */
    @Override
    public void close() {
    }

    /**
     * 将状态对象序列化为 JSON 字符串。
     *
     * <p>对 {@link AgentState} 使用其内置的 {@link AgentState#toJson()} 方法,
     * 其他类型使用 AgentScope 的 {@code JsonUtils}。</p>
     */
    private String serializeSingle(State value) {
        if (value instanceof AgentState as) {
            return as.toJson();
        }
        return io.agentscope.core.util.JsonUtils.getJsonCodec().toJson(value);
    }

    /**
     * 将 JSON 字符串反序列化为指定类型的状态对象。
     */
    @SuppressWarnings("unchecked")
    private <T extends State> T deserializeSingle(String json, Class<T> type) {
        if (AgentState.class.isAssignableFrom(type)) {
            return (T) AgentState.fromJsonString(json);
        }
        return io.agentscope.core.util.JsonUtils.getJsonCodec().fromJson(json, type);
    }
}

4 测试

java 复制代码
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v4.1.0)

2026-06-13T18:29:26.950+08:00  INFO 34124 --- [agentscope] [  restartedMain] com.xu.agentscope.App                    : Starting App using Java 25.0.2 with PID 34124 (D:\Code\Learn\agentscope\target\classes started by xuhya in D:\Code\Learn\agentscope)
2026-06-13T18:29:26.953+08:00  INFO 34124 --- [agentscope] [  restartedMain] com.xu.agentscope.App                    : No active profile set, falling back to 1 default profile: "default"
2026-06-13T18:29:26.984+08:00  INFO 34124 --- [agentscope] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2026-06-13T18:29:26.984+08:00  INFO 34124 --- [agentscope] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2026-06-13T18:29:27.338+08:00  INFO 34124 --- [agentscope] [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2026-06-13T18:29:27.340+08:00  INFO 34124 --- [agentscope] [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2026-06-13T18:29:27.357+08:00  INFO 34124 --- [agentscope] [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 8 ms. Found 0 Redis repository interfaces.
2026-06-13T18:29:27.634+08:00  INFO 34124 --- [agentscope] [  restartedMain] o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8080 (http)
2026-06-13T18:29:27.644+08:00  INFO 34124 --- [agentscope] [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-06-13T18:29:27.644+08:00  INFO 34124 --- [agentscope] [  restartedMain] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/11.0.22]
2026-06-13T18:29:27.664+08:00  INFO 34124 --- [agentscope] [  restartedMain] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 680 ms
2026-06-13T18:29:27.934+08:00  INFO 34124 --- [agentscope] [  restartedMain] io.agentscope.core.tool.Toolkit          : Registered tool 'execute_shell_command' in group 'ungrouped'
2026-06-13T18:29:28.266+08:00  INFO 34124 --- [agentscope] [  restartedMain] o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8080 (http) with context path '/'
2026-06-13T18:29:28.270+08:00  INFO 34124 --- [agentscope] [  restartedMain] com.xu.agentscope.App                    : Started App in 1.602 seconds (process running for 2.011)
相关推荐
郭东东2 小时前
用数据工程与策略,推动模型持续进化|字节跳动招聘全栈研发工程师 - AI 数据与安全
llm·ai编程·招聘
道一云黑板报2 小时前
告别提示词工程:为什么“循环工程”才是 AI 编程的未来?
人工智能·驱动开发·软件工程·ai编程
沉默王二2 小时前
面试官坏笑:“你用 AI 编程一年了,怎么保证 Claude Code 写出来的代码是对的?”我:“直接上 Claude Fable 5 啊!”
agent·ai编程·claude
米小虾2 小时前
AI Agent从Demo到生产:2026年主流Agent开发框架全景对比与实战选型指南
人工智能·agent
冬奇Lab2 小时前
Agent 系列(20):Harness 实战——从单文件到生产级模块包
人工智能·agent
雨辰AI2 小时前
从零搭建大模型本地运行环境|Python+CUDA 基础配置避坑大全
大数据·开发语言·人工智能·python·ai·ai编程·ai写作
玉鸯2 小时前
我认为的2026 年,Agent开发最佳的学习教程
agent
云烟成雨TD3 小时前
Agent Scope Java 2.x 系列【8】工具调用
java·人工智能·agent