ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java 生态 AI Agent 框架自研之路:响应式编排、工具调用与生产实践

Java 生态 AI Agent 框架自研之路:响应式编排、工具调用与生产实践

先交代一下背景:这个项目不是某个大厂实验室的作品,是我们团队在业务里被逼出来的。我们一直在做 Java 后端的 SaaS 业务,这两年陆陆续续接了好几个 AI 场景,从客服工单自动处理到运营报表的自然语言查询,每个场景都绕不开“让模型自己调用工具、自己决定下一步怎么做”这个核心能力。市场上的选择不少,但真正能无缝嵌进 Java 25 + Spring Boot 4 这套业务技术栈、还支持响应式流式传输的,几乎没有。所以最后我们决定自己写一个,写完之后发现它完全能沉淀成一个独立框架,就开源了。

这篇文章不会讲太多高大上的架构理念,重点是把我们从立项、设计内核、踩坑到开源发布的真实过程记录下来。适合两类人看:一类是想在 Java 生态里搞 AI Agent 却不知道怎么下手的后端工程师,另一类是已经在做 Agent 框架、但被响应式编程和模型调用的各种边界问题折磨得够呛的人。

希望我踩过的坑能帮你少走一段弯路。

1. 为什么非要在 Java 生态里做 Agent 框架:一个缺口的观察与决策过程

1.1 起项目之前我们面对了什么

你可能也遇到过这种状况:业务方天天说“接入 AI”,但你打开需求一看,无非是几件事——让模型根据用户问题决定调用哪个接口、把多轮对话的上下文管好、最后把结果以流式的方式推给前端。这些事在 Python 生态里已经有 LangGraph 或 CrewAI 这样的成熟选择,但在 Java 生态里,尤其是我们这种以 Spring Boot 为核心、服务已经跑了两三年的团队,情况完全不同。

我们不是不能用 Python 写个独立服务,而是不想为了一个 Agent 场景引入一整套异构技术栈。这个判断基于几个非常现实的理由:

  • 运维体系不统一。公司内部的服务注册、监控告警、日志采集全都在 Java/Spring 这条链路上,Python 服务进生产意味着要把监控链路再铺一套。
  • 客户对部署形态有要求。不少客户要求私有化部署,他们已有的运维人员只熟悉 Java 应用服务器或者容器编排,多一种语言就是多一份交付成本。
  • 模型 SDK 大多有 Java 客户端,但不少供应商的 Java 客户端并不是响应式风格的。我们需要在自己的框架里统一掉这个差异,而不是让业务代码到处适配。
  • 团队里能写高质量 Java 的人远比能写 Python 的人多。框架跑在 Java 里,后续维护和扩展的阻力最小。

说白了,选择 Java 不是因为 Java 比 Python 更“适合”做 Agent,而是因为 Java 是我们生产环境里唯一合理的落盘点。框架只有落在团队真正用得上、维护得起的地方,才有生命力。

1.2 为什么没有直接选 LangChain4j 或 Spring AI

这个决策我们认真评估过,不是拍脑袋。简单说,LangChain4j 和 Spring AI 都是优秀的基础工具库,但它们解决的是“大模型怎么接进来”的问题,而不是“Agent 在生产环境里怎么跑得稳”的问题。

我们拉了一张表来对比需求:

能力维度我们需要的LangChain4j/Spring AI 现状我们的决策
流式响应从 Agent 全链路(思考、工具执行、输出)支持 SSE 流式推送更多是单次模型调用的流式返回自建事件流模型
编排控制精确控制工具调用轮数、并行步骤、条件分支提供链式或简单的循环能力,复杂分支要手写内核提供 planner 和 executor 接口
响应式融合与 Spring WebFlux 的背压、异步调度无缝衔接偏传统 Servlet 阻塞模型用 Reactor 类型做核心原语
生产可观测每个 Agent 回合都有事件日志和指标多数要靠外部集成框架里定义标准 AgentEvent
私有化部署尽量少的外部中间件依赖部分能力依赖 LangSmith 等服务内置可插拔 tracing 接口

我知道你会问:自己造轮子不累吗?其实我们的想法是,累也要控制轮子的半径。我们不做“什么都有”的全家桶,只做 Agent 运行时里最刚性、最容易被写坏的那部分:循环控制、工具协议、记忆管理、事件流。模型接入、向量数据库这些都定义成 SPI 接口,让社区和业务自己去接。所以这个框架的核心依赖非常克制,就是 Reactor 和 Spring 的自动配置那一套。

1.3 目标定位:只解决 Agent 运行时的三个核心问题

项目启动前我们开过几次会,最后把范围收敛到三件事:

  1. Agent 循环控制。模型输出结果后,框架要决定是继续调用工具还是结束任务,每一轮的上下文都要正确传递。
  2. 工具调用的协议与执行。让模型能够按照我们暴露的 JSON Schema 去调用 Java 方法,同时让 Java 开发者可以用注解轻松暴露工具。
  3. 记忆与上下文管理。自动把多轮对话里的消息压缩、裁剪,避免上下文膨胀导致模型行为漂移。

这三件事解决好了,业务方接入时只需要写自己的工具类和提示词,循环和记忆这些脏活全部交给框架。

事实证明,这三个点也正好是后来社区贡献者最感兴趣的部分。很多人不是不想用 Agent,而是不敢写循环控制和记忆压缩,怕写错。框架把这两块做成默认能力,恰好切中了他们的实际痛点。

2. 框架的内核结构:响应式执行链、编排器与工具协议

2.1 顶层执行模型:WebFlux 的入口一路通到 Agent 循环

我们的框架对外暴露的第一个核心类叫AgentRuntime,它的入口方法是这样的:

public interface AgentRuntime { Flux<AgentEvent> stream(AgentRequest request); Mono<AgentResult> execute(AgentRequest request); }

stream方法面向 WebFlux Controller,可以直接把事件流桥接成 SSE 响应;execute则是把事件流累积成单个结果对象,方便同步调用场景。两个入口底层共享同一套执行管道。

整个执行链路的核心思想是“一次 Agent 回合就是一个 Reactor 数据流”。我用简化的方式描述一下:

public Flux<AgentEvent> stream(AgentRequest request) { return Flux.defer(() -> runLoop(request, 0)) .contextWrite(ctx -> ctx.put(AGENT_CONTEXT, new ConversationContext())); } private Flux<AgentEvent> runLoop(AgentRequest request, int round) { return Mono.just(round) .filter(r -> r < maxRounds.get()) .flatMapMany(r -> encodeContext(request)) // 1. 把上下文编码为模型消息 .flatMap(messages -> planner.plan(messages)) // 2. 调用模型,得到决策 .flatMapMany(plan -> dispatchSteps(request, plan)) // 3. 按决策执行工具或结束 .concatWith(Mono.defer(() -> runLoop(request, round + 1))); }

上面这段不是完整代码,但你可以看到循环的本质:每执行完一轮,框架就把新产生的工具结果追加回上下文,然后递归进入下一轮。递归在这里是有业务含义的,Agent 本来就是多轮决策的过程,每一轮依赖上一轮的结果。

为了防止递归失控,我们有一个LoopGuard组件,它同时检查三件事:

  • 总轮数是否超过maxRounds(默认 4 轮,可配置)
  • 最近两轮的工具调用是否完全重复(相同工具、相同参数)
  • 整个上下文是否触达 token 预算红线

这三道闸门我们在后面踩坑部分会展开讲,现在先记在心理上。

2.2 编排器如何把“并行工具调用”和“顺序依赖”统一掉

模型在一次决策里可能想同时调用多个工具,例如同时查订单状态和查物流轨迹;也可能要求“先查库存,如果不足再查补货计划”。这两种语义差别很大,不能用同一个执行模板硬套。

所以我们定义了三种步骤原语:

  • SequentialStep:按顺序执行,前一个工具的结果可以作为后一个工具的参数。
  • ParallelStep:并行执行多个工具,所有结果同时回收。
  • ConditionalStep:根据前置条件表达式,决定走哪个分支。

在响应式世界里,并行天然对应Flux.merge,顺序天然对应concatMap。我给大家看一眼编排器的核心调度逻辑:

Flux<AgentEvent> dispatchSteps(AgentTask task, Plan plan) { if (plan.steps().isEmpty()) { return Flux.just(AgentEvent.finished(task)); } Flux<ToolResult> results = Flux.fromIterable(plan.steps()) .flatMap(step -> { if (step instanceof ParallelStep p) { return Flux.fromIterable(p.steps()) .flatMap(s -> executor.execute(task.context(), s)) .collectList() .map(list -> mergeResults(list)); } return executor.execute(task.context(), step) .concatMap(result -> result.isFailure() ? handleFailure(result) : Flux.just(result)); }); return results.map(result -> AgentEvent.toolResult(task, result)) .doOnNext(event -> task.context().append(event)); }

这里有一个关键设计:工具结果的回写不是零散完成的,而是在一个步骤完成后再统一append到对话上下文。为什么要这样做?

因为模型对上下文的解读是有顺序依赖的。如果你把并行工具的结果按完成时间先后无序写进上下文,模型可能会误判“先返回的就是先发生的”,这会让多工具并行场景下的推理质量大幅下降。统一回收再统一写入,虽然有一些延迟,但保证了上下文里的执行时序与真实世界一致。这一点我们在踩坑部分还会再提。

2.3 虚拟线程与响应式并不是二选一

聊聊 Java 25 和 Spring Boot 4 在这个项目里的价值。有人说响应式编程和虚拟线程是互斥的,我们实践下来并不完全是这样。它们解决的其实是不同层面的问题:虚拟线程让阻塞代码变得廉价,响应式让 IO 密集管道具备背压控制。两者完全可以分工合作。

我们在项目里的具体分工是这样的:

  • HTTP 层用 WebFlux,面向大量客户端 SSE 连接,背压语义天然匹配。
  • Agent 编排层用 Reactor 的Mono/Flux,保证工具的并发、顺序、超时控制都符合声明式模型。
  • 模型 SDK 调用和一些遗留的 JDBC 组件仍然使用虚拟线程。Java 25 对虚拟线程的支持已经非常成熟,我们用它包了一层VirtualThreadExecutor,专门处理那些没法改造成响应式的阻塞调用。

这里我要特意说明一个细节。很多人以为把Schedulers.parallel()扔给阻塞代码就能完美适配,其实不是。响应式管道的线程亲和性非常重要,尤其是当你在 WebFlux 的 Netty 线程上执行了阻塞调用,整个 EventLoop 线程组会被拖垮。我们的经验是:给阻塞模型调用单独配置一个基于虚拟线程的调度器,在Schedulers.fromExecutorService包一层,然后通过统一工厂获取,避免散落在业务代码里的自定义线程池。

这一套组合下来,既保留了响应式流式返回的优势,又大大降低了全部响应式改造的工程量。如果你们团队对响应式没那么熟,我建议也可以采用同样的混合策略,而不是一上来就要求所有代码都是非阻塞的。

3. 工具调用与记忆设计的取舍:两种避免“自嗨”的设计约束

3.1 工具协议为什么固定在 JSON Schema 而不是走简单反射

Agent 框架里最难做的不是循环,而是工具协议。模型不了解你的 Java 方法,它只知道一段 JSON 文本。如何让模型准确知道你有哪些工具、每个工具参数长什么样,以及什么时候不该调用某个工具,这是决定框架好不好用的分水岭。

我们的做法很简单:用注解声明工具,运行时自动推导 JSON Schema。

@AgentTool(name = "create_ticket", description = "创建一条用户工单,需要提供用户ID和问题描述") public ToolResult createTicket( @ToolParam(description = "用户唯一标识") String userId, @ToolParam(description = "问题描述,不超过500字") String message) { return ticketService.create(userId, message); }

框架在扫描到这个注解后,会生成类似这样的 JSON Schema 给模型:

{ "type": "function", "function": { "name": "create_ticket", "description": "创建一条用户工单,需要提供用户ID和问题描述", "parameters": { "type": "object", "properties": { "userId": {"type": "string"}, "message": {"type": "string"} }, "required": ["userId", "message"] } } }

看到这里你可能会问,为什么不直接用反射去枚举方法参数类型就行?原因是模型的工具调用准确率很大程度依赖描述文本的质量,不只是类型。一个参数叫message,问题是它到底填什么?如果不给描述,模型经常会把用户原始问题原封不动塞进去,或者凭空捏造。所以我们强制要求每个@AgentTool和@ToolParam都必须写描述,框架在启动时校验,缺失就报错启动失败。

这个设计一开始被团队同事吐槽“太啰嗦”,但实际跑起来效果立竿见影:工具选择的准确率从 70% 左右稳定到 90% 以上。模型真的非常依赖文字描述来理解调用边界,这点怎么强调都不过分。

还有一个我们后期补上的细节,是工具描述里要写清楚“什么时候不调用”。比如某个工具是删除数据的,如果你只写“删除订单”,模型可能在用户说“取消订单”时就调用它,而取消和删除在业务语义里是完全不同的操作。我们现在的做法是在描述里补充条件性说明,例如“仅当用户明确要求删除或作废订单时调用,如果是取消订单请调用 cancel_order”。别小看这句话,它能把误调用率降一个量级。

3.2 响应式工具执行与副作用补偿

工具方法本身的返回值可能是同步的,也可能是异步的。我们设计了一个统一的ToolExecutor适配层:

public interface ToolExecutor { Mono<ToolResult> execute(ToolInvocation invocation); default Flux<String> stream(ToolInvocation invocation) { return execute(invocation).map(ToolResult::content); } }

同步方法内部会被包装成Mono.fromCallable,再投到我们前面说的虚拟线程调度器上执行;如果工具返回了Publisher类型,框架会直接桥接成响应式流。这样业务开发者根本不必关心自己写的是同步还是异步,框架统一处理调用耗时、超时和取消。

这里必须提一下“副作用补偿”这个坑。Agent 调用工具不等于普通 API 请求,它可能在对话进行到一半时已经执行了一次写操作,但用户随后中断了对话。比如:模型先调用了“创建订单草稿”,然后又调用了“发送提醒短信”,结果用户中途关了页面,SSE 连接断了。这时候短信已经发出了,但订单草稿可能根本没保存,两边不一致。

我们的处理方式是约定一套补偿接口:每个工具都可以实现AgentToolCompensator,在 Agent 会话异常终止时,框架会主动调用已执行工具的补偿逻辑。但老实说,这个能力到现在也只是做得“可用”,并没有覆盖所有业务场景。我的建议是,如果你们的工具里有明显的副作用操作,不要在 Agent 里让它自动执行,而是让模型生成“待确认动作列表”,由业务系统在执行前做一个人工确认,这种模式稳妥得多。

3.3 记忆层:窗口与摘要两种实现并存的理由

记忆是 Agent 框架里最容易做“重”也最容易做“空”的部分。很多框架一上来就给你接向量数据库、搞语义检索,但我们认为大部分业务场景的第一需求根本不是“长期记忆”,而是“别让上下文爆掉”。

所以我们做了两个实现,共用一个MemoryStrategy接口:

  • WindowMemory:固定保留最近 N 轮对话。优点是简单、成本低、行为可预期;缺点是会把早期关键信息扔掉。适用于客服工单这类对话轮次短、关键信息集中在最近的场景。
  • SummarizeMemory:当窗口超过预算时,把前面的对话摘要成一段自然语言,再和最近对话一起送入模型。优点是信息浓缩、可控上下文窗口;缺点是摘要本身有压缩损耗,且摘要过程会额外产生一次模型调用,增加延迟和费用。

我自己在项目里的建议是,默认先用WindowMemory,只有当对话长度明显超过模型上下文一半时,再去考虑SummarizeMemory。不要一上来就上向量库,那个是把问题复杂化了。向量库适合的是“在几十万条历史记录里找相似案例”的场景,而绝大多数 Agent 只需要“记得刚才发生了什么”。

顺带说一个取舍细节:记忆层不是把消息简单拼一起就完事。我们在 context 里维护了三个分区:系统指令区、对话历史区、工具回写区。工具回写区可以在每轮结束时做裁剪,因为有些工具返回的 JSON 特别长,但真正有用的可能只有其中两个字段。如果任它堆积,很快就把模型上下文撑爆。我们提供ToolResultTrimmer接口,允许每个工具自定义“保存到上下文的净值”,这个功能是后期社区反馈最多、加得也最值的一个扩展点。

4. 高频踩坑记录:背压、阻塞调用与 Agent 死循环的实战修复

4.1 坑一:并行工具结果回写顺序错乱,模型误判业务时序

这个 Bug 是我们的测试环境先炸出来的。当时场景是“查询订单并发查询物流”,两个工具都很快,但网络抖动导致物流查询先返回,订单查询后返回。结果框架把先返回的结果先写进上下文,模型读到的是“物流轨迹在前、订单状态在后”,它推理时居然认为订单是在物流之后创建的,给出了一堆莫名其妙的回答。

这个问题的根源不是模型蠢,而是我们违反了时序一致性原则。修复方式我们在 2.2 节已经提到了,就是把同一个步骤里的多个并行工具结果collectList收集齐了再统一写回。这样虽然牺牲了一点等待时间,但换来的是上下文里的逻辑时序永远正确。

我额外说一个容易被人忽略的细节:统一写回时,工具结果在上下文里的顺序也要固定。不能每次执行完按返回顺序排,要按工具在步骤里的声明顺序排。否则即使你是统一写回的,两次运行之间顺序可能不一样,模型还是会困惑。我们在ParallelStep里为每个工具挂了index字段,写回前按index排序,这个逻辑写进单元测试里,之后再也没有复发过。

4.2 坑二:Java HTTP Client 的同步阻塞混进 Netty 线程

这是我们踩过最隐蔽的一个性能坑,跟大家详细复盘一下。

我们一开始接入大模型供应商 SDK 时,用的是 JDK 自带的HttpClient。它在单次调用上千毫秒的场景下性能没什么问题,问题出在放进了flatMap之后。

Agent 编排天然是并发很高的,一个 WebFlux 应用可能同时跑几十个 Agent 会话,每个会话每轮要调多次模型。如果你在flatMap里直接放一个同步阻塞调用,它会在 Reactor 的 Netty 线程上占用时间。刚开始流量不大看不出来,等压测一上来,你会发现整个应用的吞吐断崖式下跌,因为 Netty 的 EventLoop 线程被阻塞调用占满了。

我们用BlockHound这个库做了检测,第一时间就把问题定位了。修复方案就是前面说的,把同步阻塞调用包装到专门的虚拟线程调度器里。

private final Scheduler virtualScheduler = Schedulers.fromExecutorService( Executors.newVirtualThreadPerTaskExecutor(), "llm-blocking"); public Mono<String> callModel(String payload) { return Mono.fromCallable(() -> syncClient.call(payload)) .subscribeOn(virtualScheduler); }

这里还有第二个坑:虚拟线程调度器不能一开到底。因为Schedulers.fromExecutorService创建的调度器默认会缓存线程,如果你给每个请求都新建一个执行器,线程数会不受控制地膨胀。我们用了一个静态共享的ExecutorService,配合固定上限的线程池,保证高并发下虚拟线程资源可控。

4.3 坑三:Agent 死循环的暴力防止,我们的三层闸门是怎么设计的

模型在工具调用场景里偶尔会“魔怔”,比如查询物流失败后,它反复调用同一个查询接口,每次换一点参数,但换汤不换药。如果没有循环上限,这个请求会一直打到你后端,把下游接口打爆。

我们的第一层闸门是轮数上限,这个不用多说;第二层闸门是累计 token 上限,上下文总量超过预算就直接终止;第三层闸门是最有特色的——重复行为检测。

我们在LoopGuard里维护一个最近三轮的工具调用指纹列表,指纹由“工具名 + 参数 HashMap 去掉时间戳后的序列化结果”生成。如果连续三轮指纹完全一致,我们直接中断 Agent,返回一个特殊的事件给调用方:“检测到模型重复调用相同工具,已终止本次任务”。

说一个小教训:指纹计算不能直接拿原始参数序列化,因为大多数业务参数里都带timestamp、requestId这类会变化的字段。你要是连这些都算进去,重复行为永远检测不到。我们把这种情况做成注解属性,比如@ToolParam(ignoreInFingerprint = true),框架计算指纹时会跳过标注字段。这个细节也是被生产环境教育出来的。

4.4 坑四:SSE 流式输出与事务一致性不可兼得时怎么选

还有一类坑,不是代码层面的,而是产品层面。当我们把 Agent 结果以 SSE 流式推给前端时,对话是逐字显示的。如果 Agent 中间执行了一个“扣减库存”的操作,用户看到一半觉得不对,关掉了页面。此时库存已经扣了,但没有生成订单。

我们一开始的方案是记录补偿事件,在连接断开时自动回滚。但在一次真实客户对接中我们发现,自动补偿比不补偿更危险——因为补偿可能发生在扣减成功之后、用户又点了确认的那几毫秒窗口里,结果把有效扣减回滚了。后来我们改成“写操作一律等 Agent 全部执行完成,由前端点击确认按钮触发提交”。即 Agent 只生成操作方案,业务系统按方案执行,不在 Agent 内部做最终写操作。

这个经验可能只适合部分业务,但值得所有做 Agent 工具调用的人思考:Agent 的职责是决策和推理,不是执行不可逆操作。把不可逆操作留给业务流程确认,才能避免很多在分布式环境里解释不清的问题。

5. 开源发布复盘:模块布局、文档策略与社区冷启动

5.1 仓库结构与 Maven 模块怎么切才合理

开源最怕的是把内部代码原封不动扔出来。我们花了几周时间,把内部项目拆成适合开源的结构,最后定了五个 Maven 模块:

├── agent-api // 公共接口和注解,依赖最少 ├── agent-core // 内核实现,包含 AgentRuntime、编排器、LoopGuard ├── agent-spring-boot-starter// Spring Boot 自动配置,让用户一个依赖引入全部功能 ├── agent-adapters // 模型接入适配层,OpenAI、通义等不同实现 └── agent-samples // 示例工程,每个场景一个子目录

agent-api是我们认为最关键的模块。它只包含接口、注解和事件模型,不依赖任何框架和网络库。这样第三方用户可以只依赖agent-api就能实现自己的运行时,或者在自己的框架里嵌这套协议。

发布到 Maven Central 的过程这里也顺带说一句:你需要先配好 GPG 签名、Sonatype 的账号、POM 里的 SCM 坐标。我们第一次发布因为忘记在 POM 里声明<licenses>和<scm>,被中央仓库拒了好几次。后来把发布流程写成了 GitHub Actions workflow,每次提交 tag 自动触发,省心很多。

5.2 文档策略:示例比 API 文档重要十倍

开源项目冷启动阶段,最大的敌人不是代码质量,而是“用户看不懂怎么从零跑起来”。我们犯过的错误是前期把大量时间花在 Javadoc 上,结果 README 里的一步到位示例写得不够细,导致第一波用户进来以后不知道怎么和 Spring Boot 4 应用集成。

后来我们把agent-samples里的第一个示例精简成 60 行以内能跑通的完整工程,并且把运行步骤写在 README 最前面:引入依赖、写一个@Configuration注册工具、在 Controller 里调用runtime.stream()、启动应用。配一张完整的 SSE 返回截图。

第二个被验证有效的事情是“错误信息友好化”。框架解析模型返回的非法 JSON 时,我们不只抛异常,而是把原始模型输出片段、当前上下文轮次、工具列表一并打包进异常信息。这样用户看到报错至少知道往哪儿查,不会一头雾水地在 issue 里打转。开源项目的内容里,这一点投入产出比极高,强烈建议所有做框架的人参考。

5.3 社区冷启动的一些个人体感

我不打算晒 star 数或下载量,那种数据意义不大,而且每个项目情况不一样。我更想说的是:开源一个 Java 框架,你收到的第一批反馈往往不是赞美,而是功能需求。你就当这是社区给你开的产品评审会,认真对待比申诉和辩论更有价值。

我们在发布后的第一周收到的最有价值反馈是“希望能支持自定义的模型响应解析器”。因为我们内置的解析只针对 OpenAI 风格的 function call 格式,而社区用户用的模型五花八门,有的一开始就把工具调用直接以 JSON 写在 assistant 消息里,格式完全不一样。这个反馈让我们意识到,框架的模型适配层必须留出足够宽的扩展点,而不是绑死在一种协议上。

另一个体感是,文档里要明确“什么情况不推荐使用这个框架”。我们写了一段:如果你的场景只是单轮问答、不需要工具调用、不需要流式输出,那么直接用 HTTP 客户端调模型的官方 API 就行,别引入 Agent 框架增加复杂度。这段话表面上是劝退用户,实际上让真正需要框架的用户对你建立了信任。

最后说一下我们接下来想做的事。Roadmap 上排了三件事:一是支持更多模型厂商的 function calling 协议,二是把记忆层扩展成可以接向量数据库的 SPI,三是一些更细致的可观测性指标,比如每个工具调用耗时、失败率、平均轮数。这些功能能不能落地,很大程度上取决于社区反馈的优先级排序,我们欢迎感兴趣的人参与讨论或提 PR。

回看整个过程,我最大的感受是:在一个成熟的技术栈里做“新东西”,最难的不是把新东西写出来,而是让新东西和老生态不打架。Java 25 的虚拟线程、Spring Boot 4 的响应式底座、大模型 SDK 的阻塞调用,这三者原本并不天然兼容,但通过合理的分层设计,它们完全可以成为一套顺手的生产工具。开源只是开始,后续的路还长,设计和维护中的坑还得继续填。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进