
去年底我们团队把内部跑了大半年的一个东西整理干净开源出去了。它是一个用 Java 25 和 Spring Boot 4 搭的响应式 AI Agent 框架源码和示例工程都放出来了文档也补了不少。说实话,开源之前我们纠结了很久,毕竟这类东西市面上已经有不少选择,但真正上手之后你会发现,大部分框架要么太重量级、要么绑定太死、要么对响应式支持流于表面。我们想做的是一款真正贴合 Java 生态习惯、能怼进现有 Spring 服务里的 Agent 框架。今天把这大半年踩坑、调优、翻车的经验一次说清楚。1. 为什么我们要自己写一个 Agent 框架1.1 市面上的方案看着多能落地的没几个做 AI Agent 相关开发的人应该都有这个感受网上框架一大堆但每个都有自己的脾气。有的追求大而全从 RAG、记忆、多智能体协作到可视化编排全给你包圆了结果依赖几百个启动慢不说版本冲突就能整死人。有的则相反只给你最基础的“模型调用工具执行”两件套剩下全让你自己写写着写着发现和直接调 API 没区别。我们自己真实遇到的问题是服务本身是用 Spring Boot 建的有大量现成的业务方法需要暴露给 Agent 使用。市面框架无法干净利落地把 Spring 容器里的 Bean 变成 Agent 能调用的工具。它们要么要求你把方法重写成特定注解或接口要么干脆就是反射里扒方法、参数名极其不可控很容易在真实场景中意外暴露内部接口。我们是做某电商平台的客服与运营自动化系统Agent 要调用的工具动辄几十个有的还是需要流式返回长文本的查询接口。这种场景下响应式能力就不是可选项而是必选项。传统框架用同步 HTTP 或线程池阻塞调用在高并发 SSE 流式输出的组合下很容易把线程池打满、整个服务拖垮。1.2 自己写之后我们想明白了几个核心原则折腾了这么久我们最终提炼出三条设计原则也成了这个开源框架的“魂”第一Agent 不是玩具是服务。所以框架必须生产级得有良好的可观测性、优雅的生命周期管理、可控的并发模型而不是又一个只会在本地 shell 里跑个 demo 的脚手架。第二工具调用应该是“方法即工具”。它不应该强迫你重新搭一套 DSL 去描述工具。最自然的做法是把普通方法与入参类型作为工具签名和参数 Schema由框架自动完成参数绑定和过滤。第三AI 交互天然是事件流不是一次性请求响应。用户提问、中间思考、调用多个工具、工具返回、最终汇总每一步都应该是可观测、可截断、可取消的事件。同步阻塞模型很难优雅处理这种“链路中随时可能因为工具返回慢而挂起”的复杂度响应式流的背压和取消机制天然适合这种场景。2. 技术选型背后的思考Java 25 和 Spring Boot 4 凭什么2.1 Java 25 带来的是“响应式虚拟线程”的双模并发先说 Java 25。可能有人觉得用这么新的版本是追新求快但对我们而言这是响应式框架逃不掉的必经之路。Java 25 里最有价值的三个特性我们的框架全踩在了上面虚拟线程、结构化并发、模式匹配与 switch 表达式的最终形态。虚拟线程解决的是“同步式工具调用”的并发问题。响应式 API 虽然优雅但让业务团队把几十个工具方法全改成 Mono/Flux 并不现实——他们是写传统 Java 的老手不是响应式专家。所以框架在设计上做一个中间层框架内部用 Reactor 作为事件流主干但暴露给开发者的工具注册 API 同时支持同步方法和响应式方法。同步方法在执行时自动包装到虚拟线程池里执行响应式方法直接订阅执行。你可能觉得这是偷懒但实际生产里这是最好的折中方案。我们做过压测单机跑 20 路并发 Agent 对话每个 Agent 链路里调用 8~12 个工具其中一半是同步方法。用 Java 25 虚拟线程包装后整体吞吐和纯响应式几乎没区别但代码可读性和维护成本完全不在一个量级。Spring Boot 4 完全拥抱了虚拟线程默认内嵌容器的线程模型已经基于虚拟线程做了适配。2.2 Spring Boot 4 的模块化让框架不再“重”Spring Boot 4 最打动我们的是模块化和自动配置的整理。它把很多以前“自动配置一把梭”的东西拆成了更清晰的边界底层基于 Spring Framework 7函数式 Bean 注册成为一等公民应用启动占用和内存占用都比 Boot 3 时代下降不少。对我们的框架来说这相当于开了一扇“干净接入”的窗口。用户可以只引入agent-spring-boot-starter框架自动注册 Agent 管理器、工具注册中心、会话记忆存储但不会碰你已有的任何业务配置。好插件框架就该安安静静的不抢戏不搞事。Spring Boot 4 在可观测性上的强化也很重要。Agent 链路里要埋点除了基础的 traceId我们还给每个工具调用生成子 span记录调用入参摘要、耗时和返回类型。Micrometer 与 OpenTelemetry 的集成在 Boot 4 里更加顺畅我们几乎没写多少胶水代码就把 Agent 调用链完整接进了监控大盘。3. 框架核心架构从事件流到 Agent 编排的完整链路3.1 三大核心模块与一条事件总线整个框架分三块协议接入层、Agent 编排内核、工具与记忆扩展层。协议接入层处理的是“用户怎么和 Agent 说话”。目前支持 REST 接口接入也做了 WebFlux 的 WebSocket 端点接入后续社区在PR里加了 Telegram 风格的消息适配也很容易扩展。Agent 编排内核是核心它负责的事很像一个线程模型下的状态机接收用户输入、构造上下文、触发模型推理、审阅模型输出可能需要调用工具、解析工具结果并决定是继续推理还是返回最终答案。这整个流程我们抽象成AgentChain响应式管线内部用 Flux 串起各个阶段。工具与记忆扩展层比较简单Spring Bean 容器里加了AgentTool注解的方法会被自动扫描注册记忆存储抽象成MemoryStore接口内存、Redis 两种实现开箱即用其他存储可以自己接。整个框架内部跑在一条主干 Flux 上。所有组件之间的通信通过自定义的AgentEvent对象完成事件类型包括用户输入、模型响应块、工具开始、工具完成、工具异常、最终答案、链路取消。每一个事件都带链路 ID 和时间戳。这意味着什么只要愿意你可以在中间任意一个环节插入订阅者去记录日志、打指标、甚至做限流熔断。3.2 生命周期管理从“问一句答一句”到“跑完一整段任务”Agent 的生命周期这个词很多人是做了之后才真正理解的。刚开始我们做的版本比较天真输入一句话、调模型、拿结果、结束。但真实业务里用户一句“帮我查一下昨天售前客服的响应时长再把超时最严重的三个客服列出来”在这个看起来很简单的请求背后Agent 需要自己拆解任务、查指标库、做因果分析、按照模板生成报告。这一连串动作里还带着上下文依赖——第一次查询结果可能决定第二次查询怎么筛选。所以我们给 Agent 设计了完善的状态机待命、思考中、工具执行中、等待模型内部输出、挂起等待用户补充信息、完成、失败、已取消。这个状态机不是摆设每个状态转移都会发出事件外部系统可以据此做实时状态展示也可以实现人机协作审批流。多 Agent 协作我们目前没有做到框架内核层面而是通过工具调用来间接实现——你可以注册一个工具它的作用是调用另一个 Agent 的完整对话链路并拿到结果。实践下来这种“Agent 即工具”的模式比引入复杂多智能体通信协议要稳得多也好调试得多。4. 核心实现细节工具调用、记忆与会话管理的硬骨头4.1 注解驱动的工具注册方法即工具这是框架最有特色的地方也是我们花时间最多的地方。AgentTool(name order_detail, description 根据订单ID查询订单详情返回金额、状态、商品信息, paramsSchema ToolParamsSchema( properties Property(name orderId, type string, description 订单ID例如 ORD2025010001))) public OrderDetail getOrderDetail(String orderId) { return orderService.getDetailById(orderId); }原理其实不复杂Bean 容器启动后框架扫描到AgentTool注解方法通过 Spring 的MethodIntrospector拿到方法签名和注解元数据自动生成 JSON Schema。方法参数基础类型String、数字、布尔自动转换复杂对象支持从 JSON 反序列化。有一点非常关键也是很多框架做不好的工具方法的入参不应该被强制要求是字符串。Agent 模型返回的工具调用参数是一段 JSON 片段框架会根据方法参数的复杂程度做层级解析。一开始我们是把整段 JSON 直接塞给方法直到有同事把订单状态枚举写成参数才发现需要更细粒度的绑定。后来我们写了ParameterResolver链基础类型走直转LocalDateTime 支持日期格式自动匹配自定义对象走 Jackson 反序列化List/Map 支持泛型还原。这个下沉到框架底层的决定让后面接入业务方法时几乎没有重写成本。4.2 流式输出下的响应式背压处理如果你做过 AI 流式输出一定会对“模型输出速度远跟不上消费速度”这件事有共鸣。在大模型返回场景下尤为明显——模型生成 text-delta 的速率可能只有每秒几 KB 到几十 KB但消费者比如前端 SSE 管道处理速度快得惊人。不处理好这个速度差异就会出问题。早期版本我们用的是简单的 Reactor DirectProcessor模型一边推、前端一边收结果偶尔会出现“内存里积压几万条消息”的诡异问题。后来痛定思痛整个事件流全部改成Sinks.Many 自定义背压策略模型响应块按时间窗口分组成微批默认 200ms 或 512KB取先到者。每个工具调用返回的流式数据流单独建模外部消费速度过慢时走丢弃 重取策略而不是无限堆积。Agent 的最终答案流实现为FluxString让 WebFlux 层直接把它作为 SSE 返回体。var answerStream agentChain.run(AgentRequest.builder() .sessionId(session-123) .userId(user-456) .input(帮我把这周订单量做个趋势分析) .build()); return ServerSentEvent.builder(answerStream) .mediaType(MediaType.TEXT_EVENT_STREAM) .build();表面看这段代码只是把 Flux 作为返回值暴露出去但它背后是完整的链路模型流输出 - 工具结果解析 - 上下文拼装 - 最终答案组装。每一环都在同一条响应式流上没有线程阻塞也没有中间缓冲区的无限增长。4.3 会话记忆的存储策略与上下文窗口管理这一节是最容易被低估的。很多初学者以为记忆就是把历史对话直接拼进提示词里真做了才发现两件事很棘手一是上下文长度永远不够用二是不同业务场景对“记忆”粒度要求完全不同。我们抽象了两级记忆短期对话记忆负责当前会话的对话轮次内容长期事实记忆负责从历史会话里提炼出的关键信息条目比如用户的偏好、重点项目、常用仓库。存储层分两种实现内存版用 ConcurrentHashMap 过期策略做演示和学习生产推荐 Redis 版支持按会话维度存取Hash 结构存每一轮的摘要和原始内容。上下文窗口管理是个老问题业内一致做法是“裁剪总结”。我们默认提供最近 N 轮完整对话 之前会话的滚动摘要。N 可以配置默认 12 轮。一旦历史轮次超过这个数就调用摘要模型对更早的内容做压缩压缩结果写入长期记忆。实测下来这种方式既保证了模型能拿到及时信息又不会让提示词爆掉。记忆这块我们踩过最大的坑是“摘要模型被自己以前的摘要污染”。你以为压缩之后省略细节结果模型顺着错误的旧摘要继续推理越跑越偏。后来我们给摘要加了个置信度字段低于阈值的摘要只做展示、不做推理依据问题才缓解。4.4 编排内核模型推理与工具调用的循环博弈Agent 现场工作中模型的输出不只是“一段话”还可能是“一段带工具调用意图的结构化指令”。这个被称为 function calling 的机制是所有 Agent 框架的心脏。我们的实现方式是模型输出解析成三条分支——最终回答、工具调用数组、需要澄清的追问。如果是工具调用框架会先校验工具是否存在、参数是否完整然后异步执行得到结果后把工具输出拼回上下文再让模型继续推理。这个循环次数默认上限是 15 次防止模型在工具调用里原地转圈出不来。工具执行结果也可能是一个异常。这个 We 处理得很严格。工具抛出的异常会被包装成ToolExecutionException事件总线广播该工具的失败事件并把错误摘要传回给模型让它决定是换参数重试、跳到备用方案还是直接如实回答用户“这个查不到”。在这套机制之前我们还有过一次惨痛教训模型在连续 6 次工具调用全部失败后吐出一句“查询成功”——它认为工具返回错误对象只是数据格式这家伙是个乐观派直接忽略。后来我们强制在工具结果前缀标注执行状态成功、失败、超时并设计成错误结果不进入模型上下文。从那以后模型再也没犯过这种“睁眼说瞎话”的毛病。5. 5分钟快速跑通一个带工具调用的 Agent 场景5.1 工程目录与最小配置先说怎么把它跑起来。假设你的项目是 Maven 结构引入依赖之后加一个启动配置即可。dependency groupIdcom.agentframework/groupId artifactIdagent-spring-boot-starter/artifactId version1.0.0/version /dependency配置文件里指定大模型连接方式和 Agent 的默认参数。支持 OpenAI 兼容接口、Ollama 本地模型以及 Spring AI 里抽象好的ChatClient。agent: default-model: openai default-temperature: 0.3 tools-base-package: com.example.demo.agent memory: type: redis ttl: 24h chain: max-iterations: 15tools-base-package是框架扫描AgentToolBean 的所在包要确保业务工具类都注册进去了。5.2 写一个业务工具类并用 SSE 输出结果假设我们要给 Agent 加一个查天气的工具。啥也不用多学就是一个带注解的普通 Spring 组件。Component public class WeatherTools { AgentTool(name weather_by_city, description 输入城市名获取实时天气与未来三天预报, paramsSchema ToolParamsSchema( properties Property(name city, type string, description 城市中文名如北京))) public WeatherInfo getWeather(String city) { return weatherApiClient.fetch(city); } }启动服务打开一个接受 SSE 的客户端连接发送一段自然语言请求比如{ sessionId: demo-session, userId: demo-user, input: 北京这两天适合出门跑步吗 }框架会启动一条 AgentChain 链路。模型判断需要查询天气于是链路上依次触发tool_start、工具执行、tool_complete最终模型根据天气数据给出“建议傍晚出门白天有轻度霾”之类的回答。整个过程你在日志里可以看到逐步展开的事件轨迹这在调试时非常爽。5.3 从事件总线到监控大盘把链路观测做实这个框架我们最满意的一点是它把“可观测性”做成了开箱即用。每一条 Agent 链路会自动生成 traceId并且通过 Micrometer 暴露一批指标链路总数、工具调用数、工具失败率、平均端到端耗时、模型推理耗时 P99。类似这样的结构agentChain.events() .filter(e - e.type() AgentEventType.TOOL_START) .subscribe(e - meterRegistry.counter(agent.tool.calls, tool, e.toolName()).increment());当时在生产排障中就是靠这套指标发现某个工具调用失败率异常升高。后来揪出原因是下游系统的 API 限流策略调紧了返回的 TOO_MANY_REQUESTS 没被工具层识别成可重试错误而是一路抛给了模型。我们的模型 AI 有时候会一本正经地把限流错误解释成“服务资源紧张”然后建议用户换个时间段再查——这结论不能说错但实在不够精确。后来我们集中把限流类错误统一重试两次、第三方返回上游 5xx 时直接走降级文案指标才恢复平稳。6. 开源之前踩过的坑、填过的漏6.1 响应式与事务的边界问题先说一个很多响应式新手根本不会注意到但生产里要命的问题在响应式链路上开数据库事务。我们最早支持的工具方法里有业务团队直接标了Transactional的老代码运行时就出问题了Transactional在 Spring 中默认依赖线程绑定数据库连接而 WebFlux 响应式调用链始终运行在事件循环线程上事务同步器根本拿不到连接。前两周我们几乎是靠数据库连接池的报错和队友的疯狂告警才逐步定位到问题。后来我们提供了一套规范的并发边界策略同步事务方法由虚拟线程执行器包装确保Transactional不会阻挠。这种方式对传统企业存量代码最友好不需要改业务实现。响应式事务方法配合 R2DBC 和TransactionalOperator使用响应式链路上可以正常提交回滚。我们还在文档里加了一个醒目的注意事项工具方法如果没有特殊的强一致要求建议不要直接开事务框架提供了自定义的幂等键机制失败重试时先校验幂等键很多场景根本不需要用到事务。6.2 上下文窗口膨胀与 Token 成本失控做 Agent 最烧钱的路子是“无节制地把历史记录全塞给模型”。我们有一个客户跑了一周后账单上涨了近 5 倍原因是他们追求“完美体验”把每个会话的所有上下文全量保留再加上工具返回的长文本Token 量爆炸也是意料之中。框架现在默认支持三种上下文压缩策略原始保留适合简单问答上下文短。滚动裁剪最近 N 轮完整 之前滚动摘要适合大多数客服机器人。分层整合长会话下才启用将历史摘要分主题存放再按需拼装适合复杂任务分析类 Agent。这个选配置的过程我们做了很谨慎的收益模型验证——在一个续费用户提醒场景里使用滚动裁剪策略后Token 成本单机下降 70%而意图识别准确率只降了不到 1.5 个百分点这对成本控制来说完全值得。6.3 工具参数的模型乱入如何防住大模型的幻觉参数Machines 幻觉不止体现在嘴上还会体现在工具调用参数上。模型可能在工具参数里塞进一些它自己发明的枚举值、字段名甚至是当前上下文里根本没出现过的 ID。这是所有 Agent 框架都绕不开的硬问题。我们的解决思路是防爆在结构化校验层框架内部集成了轻量 JSON Schema 校验器工具注册时定义的 Schema 就是校验标准。比如它定义了status只能是created | paid | shipped | completed模型传了done框架直接拦截工具调用返回一个“参数不合法”的错误并要求模型基于可取值列表重新生成。还有一种常见情况是模型把多个参数混合进一个字符串里例如它会把orderIdORD123typepaid直接塞给一个String orderId参数。我们通过预设的参数格式提示、和校验失败后的自动纠偏把这类问题从最初的 17% 压到了 3% 以下。老实说这种问题没法完全清零但 3% 已经可以接受。6.4 排错速查表这些坑照着查就行症状排查思路解决方案工具方法没被注册检查扫描包配置、确认AgentTool注解没被继承覆盖tools-base-package换成具体业务包路径SSE 流突然断开检查背压策略是否触发了丢弃查看前端消费速率调大 micro-batch 窗口或改批量推送模式模型陷入工具调用死循环检查链路最大迭代次数查看日志里 tool_complete 是否齐全调低chain.max-iterations或者给工具调用加去重唤醒机制虚拟线程池耗尽观察指标里 thread_pool_queue 是否持续打高分别调整独立线程池的大小不该互相挤占模型经常虚构参数工具入参 JSON Schema 校验是否开启确保ToolParamsSchema定义完整开启严格校验模式7. 从开源到后续一点真实体会写框架这件事最大的乐趣不只是“高效地实现了某个功能”更是把一个团队过去踩坑的经验沉淀成产品化的能力。开源之后我们收到了不少 issue 和 PR有些提得比我们内部代码评审还认真这本身就是价值。如果你也在做 Java 生态下的 AI Agent我的建议是先想清楚你是需要一个纯粹的命令行编排工具还是需要一个能和现有 Spring 服务无缝衔接的生产级组件。如果是后者响应式能力、工具注册的注解化、可观测性这三点缺一不可。我们的框架当然不完美配置项还不少、文档也还在补、有些边界场景用例还不够丰富。但它的核心思路——以事件流贯穿 Agent 全链路、工具即方法、虚拟线程与响应式兼顾——我们坚信是这个方向的正解。后面我们计划往里面加 Agent 嵌套编排、更强的流式工具调用中途改参能力以及更完善的 OpenTelemetry 自动埋点。如果这个框架刚好能帮到你或者你对我们的一些设计决策有不同意见欢迎来开源仓库聊。技术这东西关起门来容易变执念多交流才能少走弯路。