
1. 为什么我们决定用 Java 25 Spring Boot 4 造一个响应式 AI Agent 框架1.1 一个让 Java 工程师坐不住的现实过去一年我身边做 Java 后端的同行几乎都在讨论同一件事AI Agent 的开发工具链怎么全是 Python 的天下。LangChain、AutoGPT、CrewAI清一色 Python 生态。你打开任何一个 AI Agent 的开源项目README 第一行大概率是pip install。作为一个写了十多年 Java 的人这种感觉就像你精通一门手艺结果整个市场突然换了语言。但冷静下来想想企业级 AI Agent 真正落地的时候拼的根本不是“能不能跑通一个 Demo”而是并发能力、可观测性、事务一致性、服务治理、长期可维护性。这些恰恰是 Java 生态积累了二十年的护城河。Python 写 Agent 原型快但你要把它塞进一个日均千万级调用的微服务体系里线程模型、内存管理、链路追踪、灰度发布每一项都是硬仗。所以我们团队做了一个决定用 Java 25 Spring Boot 4 写一个原生响应式的 AI Agent 框架并且开源出来。这不是为了标新立异而是我们自己在生产环境里踩过坑之后觉得这条路必须有人走。1.2 这个框架到底解决什么问题先说清楚它是什么。简单讲这是一个让你用 Java 写 AI Agent 的运行时框架。你定义 Agent 的决策逻辑、工具调用、记忆存储、多轮对话编排框架负责调度、并发、容错和可观测性。它不是一个“帮你调 OpenAI 接口的 SDK”而是一套完整的 Agent 生命周期管理方案。它解决的核心痛点有三个。第一响应式背压。AI Agent 调用大模型接口的延迟波动极大从几百毫秒到几十秒都有。传统阻塞式线程模型下一个慢请求就占住一个线程高并发时线程池瞬间打满。响应式模型下线程不会被阻塞背压机制能自然处理流量洪峰。第二工具调用的编排复杂度。一个 Agent 可能要调用搜索、数据库、外部 API、代码执行器等十几种工具还要处理超时、重试、降级。框架把这些抽象成统一的 Tool 接口和编排引擎。第三状态与记忆的一致性。多轮对话的上下文管理、长期记忆的读写、会话隔离这些在单机 Demo 里不是问题但在分布式环境里就是灾难。适合谁来参考如果你是有 Java 基础、想切入 AI Agent 开发的后端工程师这个框架能让你用熟悉的技术栈上手。如果你是架构师正在评估企业级 AI Agent 的技术选型这里的响应式设计和编排思路值得一看。如果你只是好奇 Java 能不能做 AI Agent那正好往下看。1.3 技术选型背后的真实考量为什么是 Java 25因为虚拟线程Virtual Threads在 Java 21 正式落地后到 Java 25 已经相当成熟。虚拟线程让我们在保持响应式非阻塞优势的同时能用同步的代码风格写异步逻辑大幅降低心智负担。为什么是 Spring Boot 4因为 Spring Boot 4 基于 Spring Framework 7对响应式栈的支持更加彻底WebFlux、Reactive Data、Reactive Streams 的整合度到了一个新高度。而且 Spring Boot 4 对 Java 25 的虚拟线程做了深度适配spring.threads.virtual.enabledtrue一行配置就能开启。这里有个关键决策点我们没有选择纯 Reactor 的响应式编程模型作为唯一路径。纯 Reactor 写起来爽但调试痛苦栈追踪难读团队里新手容易写出“回调地狱”的变体。我们的方案是双模并行底层用 Reactor 做非阻塞调度上层暴露虚拟线程的同步 API。你可以用Mono和Flux写响应式链路也可以用AgentTool注解写普通的同步方法框架自动把它调度到虚拟线程上执行。这个设计让不同水平的团队成员都能参与开发。2. 框架核心架构拆解响应式 AI Agent 的骨架怎么搭2.1 四层架构与数据流向整个框架分成四层从下往上依次是传输层、调度层、编排层、定义层。传输层负责和大模型 API 通信支持 HTTP/SSE/WebSocket 多种协议底层用 WebClient 做非阻塞请求。调度层是核心基于 Reactor 的 Scheduler 和虚拟线程池管理 Agent 任务的并发执行和背压。编排层处理 Agent 的决策循环包括思考、工具选择、执行、观察、再思考这个 ReAct 循环。定义层是开发者直接接触的 API用注解和接口定义 Agent 的行为。数据流向是这样的用户请求进入传输层被封装成AgentRequest对象。调度层根据 Agent 的类型和优先级把请求分配到对应的 Scheduler。编排层启动决策循环每一轮调用大模型得到下一步动作如果是工具调用就通过 ToolRegistry 找到对应工具执行结果回填到上下文。循环直到 Agent 决定输出最终答案或者达到最大轮次限制。整个过程是响应式的每一步都返回Mono或Flux背压信号沿着链路向上传递。2.2 响应式核心背压与虚拟线程的配合背压这个词听起来玄乎其实用生活类比很好理解。你家的水管如果水龙头开太大下水道来不及排水就会漫出来。背压就是下水道告诉水龙头“你慢点开我处理不过来”。在响应式编程里消费者通过request(n)告诉生产者“我还能处理 n 个”生产者就只发 n 个。AI Agent 场景下大模型 API 的响应速度就是那个下水道你的请求量就是水龙头。框架里背压的实现靠 Reactor 的Flux和Mono操作符。比如工具调用的并发控制我们用flatMap的concurrency参数限制同时进行的工具调用数量。代码大概长这样public MonoAgentResponse executeTools(ListToolCall toolCalls) { return Flux.fromIterable(toolCalls) .flatMap(toolCall - toolExecutor.execute(toolCall) .timeout(Duration.ofSeconds(30)) .onErrorResume(e - Mono.just(ToolResult.failure(toolCall, e))), maxConcurrency) // 背压核心限制并发数 .collectList() .map(this::aggregateResults); }虚拟线程在这里的角色是降低阻塞操作的代价。有些工具调用本质上是阻塞的比如 JDBC 查询、文件读写。在传统响应式模型里你得把这些包装成Mono.fromCallable()并指定subscribeOn(Schedulers.boundedElastic())但 boundedElastic 线程池大小有限高并发时还是会排队。虚拟线程没有这个问题每个阻塞操作可以分配一个虚拟线程JVM 自动管理创建成本极低。我们的做法是响应式链路负责编排和背压虚拟线程负责执行阻塞型工具。两者配合既保证了非阻塞的吞吐量又简化了阻塞代码的编写。2.3 Agent 定义层的 API 设计开发者定义 Agent 的方式我们反复打磨了好几版。第一版是纯接口实现太啰嗦。第二版是纯注解灵活性不够。最终版是注解 接口的混合模式。看个例子AiAgent(name research-agent, description 一个能搜索和总结的研究助手, maxIterations 10) public interface ResearchAgent { SystemPrompt default String systemPrompt() { return 你是一个研究助手善于使用搜索工具查找信息并总结。; } AgentTool(name web_search, description 搜索互联网获取最新信息) MonoSearchResult search(ToolParam(query) String query); AgentTool(name summarize, description 总结一段文本) default String summarize(ToolParam(text) String text) { // 同步方法框架自动调度到虚拟线程 return text.length() 500 ? text.substring(0, 500) ... : text; } }这个接口不需要你写实现类。框架在启动时扫描AiAgent注解用动态代理生成实现。AgentTool标注的方法会被注册到 ToolRegistryAgent 决策时大模型返回的工具调用请求框架自动路由到对应方法。同步方法自动跑在虚拟线程上返回Mono的方法直接接入响应式链路。SystemPrompt定义系统提示词maxIterations控制最大决策轮次防止死循环。这里有个设计细节值得说工具方法的参数绑定。大模型返回的工具调用参数是 JSON框架需要把它映射到方法参数上。我们用ToolParam注解指定参数名然后通过 Jackson 做反序列化。如果参数类型不匹配框架会返回一个结构化的错误信息给大模型让它重新生成参数。这个错误反馈机制很关键实测能大幅提升工具调用的成功率。2.4 记忆系统的分层设计Agent 的记忆分三层短期记忆、长期记忆、会话记忆。短期记忆是当前决策循环的上下文存在内存里循环结束就释放。长期记忆是跨会话的知识存在向量数据库里通过语义检索召回。会话记忆是单个用户会话的历史存在 Redis 里带 TTL。为什么这么分因为三者的访问频率、生命周期、存储要求完全不同。短期记忆要求极低延迟放内存最合适。长期记忆要求语义检索能力向量数据库是标配。会话记忆要求跨实例共享和过期清理Redis 天然适合。框架把这三种记忆抽象成统一的Memory接口开发者可以替换实现。默认提供基于 Caffeine 的短期记忆、基于 Redis 的会话记忆、基于 Milvus 的长期记忆。记忆的读写时机也有讲究。短期记忆在每轮决策前写入决策后读取。会话记忆在会话开始时加载每轮对话后追加。长期记忆在 Agent 显式调用recall工具时检索或者配置成自动召回模式每轮决策前根据当前上下文做语义搜索。自动召回模式方便但会增加延迟我们建议只在知识密集型 Agent 上开启。3. 从零搭建一个响应式 AI Agent完整实操流程3.1 环境准备与项目初始化先把环境搞定。你需要 JDK 25推荐用 SDKMAN 管理版本sdk install java 25.0.1-tem sdk use java 25.0.1-tem java -versionMaven 用 3.9 以上或者 Gradle 8.10 以上。IDE 推荐 IntelliJ IDEA 2025.1对 Java 25 的虚拟线程调试支持最好。项目初始化用 Spring Initializr选 Spring Boot 4.0.x依赖勾选Spring Reactive Web、Spring Data Reactive Redis、Spring Boot Actuator。如果你要用向量数据库再加对应的 starter。pom.xml 里关键依赖长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version4.0.0/version /parent properties java.version25/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency dependency groupIdio.projectreactor/groupId artifactIdreactor-core/artifactId /dependency !-- AI Agent 框架核心 -- dependency groupIdio.github.reactive-agent/groupId artifactIdreactive-agent-core/artifactId version0.9.0/version /dependency /dependenciesapplication.yml 里开启虚拟线程和响应式配置spring: threads: virtual: enabled: true data: redis: host: localhost port: 6379 reactive: enabled: true agent: default-model: gpt-4o max-iterations: 10 tool-timeout: 30s memory: short-term: type: caffeine max-size: 1000 ttl: 10m session: type: redis ttl: 2h注意spring.threads.virtual.enabledtrue开启后Spring Boot 会把 Tomcat 的请求处理线程换成虚拟线程。但 WebFlux 默认用 NettyNetty 的 EventLoop 不要换成虚拟线程否则会破坏非阻塞模型。这个配置只影响Async和TaskExecutor相关的线程池。3.2 定义你的第一个 Agent环境好了写一个最简单的 Agent。假设我们要做一个“技术文档问答助手”能搜索文档库并回答用户问题。第一步定义工具接口Component public class DocSearchTool { private final ReactiveVectorStore vectorStore; public DocSearchTool(ReactiveVectorStore vectorStore) { this.vectorStore vectorStore; } AgentTool(name search_docs, description 根据关键词搜索技术文档返回最相关的5个片段) public MonoListDocFragment search( ToolParam(query) String query, ToolParam(value topK, required false) Integer topK) { int k topK ! null ? topK : 5; return vectorStore.similaritySearch(query, k) .collectList(); } }第二步定义 Agent 接口AiAgent(name doc-qa-agent, description 技术文档问答助手, maxIterations 5) public interface DocQaAgent { SystemPrompt default String prompt() { return 你是一个技术文档问答助手。用户提问后你需要 1. 先用 search_docs 工具搜索相关文档片段 2. 根据搜索结果组织答案 3. 如果搜索结果不相关如实告知用户 回答要简洁准确引用文档来源。 ; } AgentTool(name search_docs) MonoListDocFragment searchDocs(ToolParam(query) String query); }第三步写一个 Controller 暴露接口RestController RequestMapping(/api/agent) public class AgentController { private final AgentRuntime agentRuntime; public AgentController(AgentRuntime agentRuntime) { this.agentRuntime agentRuntime; } PostMapping(value /chat, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxAgentEvent chat(RequestBody ChatRequest request) { return agentRuntime.stream(DocQaAgent.class) .sessionId(request.sessionId()) .userMessage(request.message()) .execute(); } }启动应用用 curl 测试curl -N -X POST http://localhost:8080/api/agent/chat \ -H Content-Type: application/json \ -d {sessionId:test-001,message:响应式编程的背压是什么}你会看到 SSE 流式返回先是thinking事件然后是tool_call事件接着是tool_result事件最后是answer事件。整个过程是响应式的客户端可以随时断开服务端会自动取消后续操作。3.3 响应式链路的关键配置与调优框架默认配置能跑但生产环境需要调优。第一个关键参数是工具调用的并发数。在AgentRuntime的配置里Configuration public class AgentConfig { Bean public AgentRuntime agentRuntime(ToolRegistry toolRegistry, MemoryManager memoryManager) { return AgentRuntime.builder() .toolRegistry(toolRegistry) .memoryManager(memoryManager) .toolConcurrency(8) // 同时最多8个工具调用 .llmTimeout(Duration.ofSeconds(60)) .toolTimeout(Duration.ofSeconds(30)) .retryPolicy(Retry.backoff(3, Duration.ofMillis(500))) .build(); } }toolConcurrency设多少合适这取决于你的工具类型。如果是 IO 密集型HTTP 调用、数据库查询可以设大一点16 到 32 都行因为虚拟线程会处理阻塞。如果是 CPU 密集型本地计算、图像处理设成 CPU 核心数左右避免上下文切换开销。我们实测下来混合型工具场景下 8 到 12 是比较稳的区间。第二个关键参数是大模型调用的超时和重试。大模型 API 的 P99 延迟可能到 30 秒以上超时设太短会导致大量重试设太长会拖垮整体响应时间。我们的经验值是超时设 60 秒重试 3 次退避策略用指数退避初始 500 毫秒。重试时要区分错误类型429限流和 503服务不可用值得重试400参数错误重试没意义。第三个是背压策略。当上游请求速率超过下游处理能力时框架默认用onBackpressureBuffer缓冲缓冲大小 256。如果缓冲满了会触发onBackpressureDrop或者onBackpressureError。生产环境建议用onBackpressureBuffer加上溢出回调记录日志并返回 429 给客户端。不要用无界缓冲内存会爆。3.4 流式输出的实现细节AI Agent 的流式输出不是简单的 SSE 推送。大模型返回的是 token 流工具调用返回的是结构化结果Agent 的思考过程是中间状态。这三种数据要合并成一个统一的事件流推给客户端。框架定义了AgentEvent类型public sealed interface AgentEvent { record Thinking(String content) implements AgentEvent {} record ToolCall(String toolName, String args) implements AgentEvent {} record ToolResult(String toolName, String result) implements AgentEvent {} record Answer(String content, boolean finished) implements AgentEvent {} record Error(String message) implements AgentEvent {} }流式输出的实现用Flux.create()手动控制事件发射public FluxAgentEvent execute() { return Flux.create(sink - { // 启动决策循环 decisionLoop(sink); }, FluxSink.OverflowStrategy.BUFFER); }这里有个坑Flux.create()的sink不是线程安全的多个虚拟线程同时发射事件会乱序。我们的做法是用sink.next()之前加锁或者用Sinks.many().unicast()做中转。实测下来Sinks.many().unicast().onBackpressureBuffer()性能最好而且天然支持背压。实操心得流式输出时客户端的断开检测很重要。如果客户端断开了服务端还在跑决策循环就是浪费资源。框架通过sink.onCancel()注册取消回调收到取消信号后中断决策循环释放资源。这个机制在移动端弱网环境下特别有用。4. 踩坑实录响应式 AI Agent 开发中的典型问题与排查4.1 虚拟线程与 Reactor 的冲突排查我们遇到的最诡异的一个 bug某个工具调用偶尔会卡住 30 秒然后超时但同样的代码在单元测试里跑一百次都没问题。排查了两天才发现问题出在虚拟线程里调用了Mono.block()。Mono.block()在响应式链路里是禁忌它会阻塞当前线程等待结果。在传统线程模型下阻塞就阻塞了最多浪费一个线程。但在虚拟线程 Reactor 的组合下block()会触发 Reactor 的BlockingSingleSubscriber它内部用CountDownLatch等待。虚拟线程的调度器ForkJoinPool检测到虚拟线程阻塞在CountDownLatch上会把它挂起让出载体线程。这本身没问题但如果这个block()调用发生在 Reactor 的 EventLoop 线程上就会导致 EventLoop 被占用整个响应式链路卡死。排查方法在application.yml里开启 Reactor 的调试模式logging: level: reactor.core: DEBUG然后搜索日志里的block()调用栈。更彻底的方法是加一个自定义的BlockHound集成在测试环境检测阻塞调用BeforeAll static void setupBlockHound() { BlockHound.install(builder - builder .allowBlockingCallsInside(java.io, FileInputStream) .allowBlockingCallsInside(java.lang, Thread)); }BlockHound 会在检测到响应式线程上的阻塞调用时抛出异常直接定位问题代码。这个工具我们强烈建议在开发阶段就加上能省掉大量排查时间。4.2 工具调用参数映射失败的常见原因大模型返回的工具调用参数是 JSON映射到 Java 方法参数时经常出问题。我们统计了一下最常见的失败原因有三类。第一类是类型不匹配。大模型可能把数字返回成字符串比如{topK: 5}而不是{topK: 5}。Jackson 默认会尝试转换但有些场景下会失败。解决方案是在ToolParam上指定converter或者用自定义的ObjectMapper配置宽松模式Bean public ObjectMapper toolObjectMapper() { return JsonMapper.builder() .configure(DeserializationFeature.ACCEPT_STRING_AS_NUMBER, true) .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) .build(); }第二类是必填参数缺失。大模型有时候会漏掉某个参数。框架的做法是返回一个结构化的错误信息给大模型让它重新生成。错误信息格式{ error: MISSING_PARAMETER, parameter: query, message: 缺少必填参数 query请重新生成工具调用 }实测下来大模型收到这个错误后90% 的情况下能在下一轮修正。第三类是参数名不匹配。大模型可能用searchQuery而不是query。解决方案是在ToolParam里配置别名AgentTool(name search_docs) MonoListDocFragment search( ToolParam(value query, aliases {searchQuery, q, keyword}) String query);4.3 记忆系统的数据一致性陷阱会话记忆存在 Redis 里多个实例共享。问题来了同一个会话的请求可能被负载均衡到不同实例如果两个请求同时读写会话记忆就会产生竞态条件。我们踩过的坑是用户快速连续发两条消息第一条的上下文还没写入 Redis第二条就开始读了导致第二条丢失了第一条的上下文。解决方案是用 Redis 的乐观锁或者分布式锁。框架默认用 Redis 的WATCH/MULTI/EXEC做乐观锁冲突时重试。配置如下agent: memory: session: type: redis concurrency: optimistic-lock max-retries: 3如果并发冲突频繁可以换成 Redisson 的分布式锁但会引入额外依赖。我们的建议是会话记忆的写入用追加模式不要用覆盖模式。每条消息作为一个独立的 Redis List 元素追加读取时按时间排序。这样即使有并发也不会丢失数据最多是顺序略有偏差。长期记忆的坑在向量检索的召回质量。默认的相似度阈值设 0.7但不同 embedding 模型的分数分布不一样。OpenAI 的 text-embedding-3-small 分数普遍偏高0.7 可能召回一堆不相关的内容。我们的做法是先用一个较大的 topK比如 20召回然后用重排序模型Reranker精排取前 5 个。重排序模型可以用 Cohere Rerank 或者本地的 BGE-Reranker。这个两步走策略比单纯调阈值效果好得多。4.4 常见问题速查表问题现象可能原因排查方法解决方案Agent 卡住不返回响应式链路中有block()调用开启 Reactor DEBUG 日志用 BlockHound 检测移除block()改用flatMap或then工具调用超时工具执行时间超过toolTimeout查看工具执行日志确认耗时调大超时或优化工具实现大模型返回格式错误提示词不够明确打印大模型原始返回在系统提示词中明确输出格式要求会话上下文丢失Redis 并发写入冲突检查 Redis 慢查询日志改用追加模式或加分布式锁内存持续增长短期记忆未释放用 JProfiler 看堆内存检查maxIterations是否过大记忆 TTL 是否生效流式输出乱序Flux.create()多线程发射检查sink.next()调用线程用Sinks.many().unicast()替代虚拟线程固定Pinning虚拟线程内用了synchronized加-Djdk.tracePinnedThreadsfull启动参数用ReentrantLock替代synchronized关于虚拟线程固定Java 25 对synchronized的固定问题做了大幅优化但某些 native 调用仍然会导致固定。启动时加-Djdk.tracePinnedThreadsfull可以打印固定事件帮助定位。5. 生产环境部署与性能调优的实战经验5.1 容器化部署的关键配置Docker 部署时虚拟线程的载体线程池大小需要关注。默认情况下ForkJoinPool 的并行度等于 CPU 核心数。在容器里如果没设置-XX:ActiveProcessorCountJVM 可能读到宿主机的核心数导致载体线程过多上下文切换开销增大。建议在 Dockerfile 里显式设置FROM eclipse-temurin:25-jre ENV JAVA_OPTS-XX:ActiveProcessorCount4 \ -XX:UseZGC \ -Xmx2g \ -Djdk.virtualThreadScheduler.parallelism4 \ -Djdk.virtualThreadScheduler.maxPoolSize256 COPY target/agent-app.jar /app/app.jar ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar /app/app.jar]jdk.virtualThreadScheduler.parallelism控制载体线程数一般设成 CPU 核心数。maxPoolSize控制虚拟线程调度器的最大线程数设 256 足够应对大多数场景。GC 用 ZGC因为 AI Agent 场景下对象创建频繁ZGC 的亚毫秒级停顿对响应式链路更友好。Kubernetes 的 readiness 和 liveness 探针要指向 Actuator 的/actuator/health。但注意如果 Agent 正在执行长任务liveness 探针可能超时导致 Pod 重启。解决方案是用单独的端口暴露健康检查或者把 liveness 的initialDelaySeconds设大一点。5.2 可观测性指标、追踪、日志响应式链路的可观测性比传统 MVC 复杂因为一个请求可能跨越多个线程。框架集成了 Micrometer 和 OpenTelemetry关键指标包括agent.execution.durationAgent 执行耗时分布agent.tool.calls工具调用次数和成功率agent.llm.tokens大模型 token 消耗量agent.memory.hits记忆命中率链路追踪用 OpenTelemetry 的 Reactor 集成Hooks.onEachOperator()自动为每个操作符创建 Span。配置Configuration public class TracingConfig { PostConstruct public void setupReactorTracing() { ReactorHook.enableTracing(); } }日志方面响应式链路的 MDC 传递是个坑。传统 ThreadLocal 在切换线程时会丢失 MDC。解决方案是用 Reactor 的contextWrite传递上下文return agentRuntime.stream(DocQaAgent.class) .sessionId(sessionId) .userMessage(message) .contextWrite(ctx - ctx.put(traceId, traceId)) .execute() .doOnEach(signal - { String tid signal.getContextView().get(traceId); MDC.put(traceId, tid); });5.3 性能压测数据与调优建议我们用 JMeter 做了一轮压测场景是 100 并发用户每个用户发 10 条消息Agent 平均执行 3 轮决策每轮调用 1 次大模型和 1 次工具。硬件是 4 核 8G 的容器。配置项默认值调优值吞吐量提升toolConcurrency41235%虚拟线程载体线程数44持平LLM 超时30s60s18%短期记忆 TTL5m10m8%背压缓冲大小25651212%调优后P99 延迟从 8.2 秒降到 4.7 秒吞吐量从 45 req/s 提升到 78 req/s。最大的瓶颈其实是大模型 API 的响应速度框架层面的优化空间有限。我们的建议是如果大模型 API 支持批量请求尽量合并调用。比如多个工具调用的结果可以一次性发给大模型而不是逐个发送。另一个调优技巧对于高频的、结果确定性强的工具调用加本地缓存。比如“查询当前时间”这种工具没必要每次都执行。框架支持在AgentTool上加Cacheable注解用 Caffeine 做本地缓存TTL 设短一点比如 10 秒。5.4 安全与限流AI Agent 的安全问题比传统 API 更复杂因为大模型可能被诱导执行危险操作。框架层面做了三层防护。第一层是工具白名单只有注册到 ToolRegistry 的工具才能被调用大模型无法调用未注册的方法。第二层是参数校验每个工具方法可以用 Jakarta Validation 注解做参数校验AgentTool(name execute_sql) public MonoQueryResult executeSql( ToolParam(sql) Pattern(regexp ^SELECT.*) String sql) { // 只允许 SELECT 语句 }第三层是速率限制用 Resilience4j 的 RateLimiter 限制每个会话的工具调用频率resilience4j: ratelimiter: instances: agent-tools: limitForPeriod: 20 limitRefreshPeriod: 1m timeoutDuration: 0s限流触发时框架返回一个结构化的错误给大模型让它告知用户“操作过于频繁请稍后再试”。这样比直接抛异常给客户端更友好。6. 这个框架后续还能怎么扩展6.1 多 Agent 协作的支持思路单 Agent 的能力有上限复杂任务需要多个 Agent 协作。框架的扩展方向是引入AgentGroup概念多个 Agent 通过消息总线通信。比如一个“研究报告生成”场景可以有搜索 Agent、分析 Agent、写作 Agent、审核 Agent它们通过AgentMessage交换数据。实现上可以用 Reactor 的Sinks.many().multicast()做消息广播每个 Agent 订阅自己关心的消息类型。6.2 与工作流引擎的整合有些场景下Agent 的决策流程是固定的不需要大模型动态决策。比如“先查数据库再调 API最后发邮件”这种确定性流程。框架可以暴露一个AgentWorkflowAPI让开发者用 DSL 定义流程底层复用响应式调度和工具执行能力。这样既能享受框架的并发和容错能力又避免了不必要的 LLM 调用开销。6.3 本地模型的支持目前框架默认对接云端大模型 API但很多企业有本地部署模型的需求。扩展点是LlmClient接口只要实现这个接口就能接入任何模型服务。我们已经在做 Ollama 和 vLLM 的适配预计下个版本发布。本地模型的优势是数据不出域、延迟可控劣势是效果可能不如云端大模型。混合模式是个好选择简单任务走本地模型复杂任务走云端。我在实际使用这个框架的过程中最大的体会是响应式编程的学习曲线确实陡但一旦跨过去处理 AI Agent 这种高延迟、高并发的场景优势非常明显。虚拟线程的加入让阻塞代码的编写变得自然两者结合是目前 Java 生态里做 AI Agent 比较舒服的方案。如果你也在用 Java 做 AI 相关的东西欢迎一起交流踩坑经验。