如果你在 Java 后端团队做 AI 应用,大概率会有这样一个尴尬时刻:产品经理过来说,要在现有系统里加一个“能自己干活”的 AI 能力,比如让系统根据用户意图自动查库、调接口、做分析,最后把结果整理好回给用户。这个需求翻译过来,就是要在 Java 服务内部落地一个 AI Agent 框架。可你翻遍开源社区,发现绝大多数 Agent 框架都长在 Python 生态里,想要嵌入到现有的 Java 微服务体系,要么跨进程起一个 Python 服务,要么自己造轮子——前面那条路要处理模型部署、服务发现、链路追踪,后面那条路就得从零开始设计和实现。
JavaManus 就是这么来的。它没用任何重型的 Agent 框架依赖,核心就是一套用 Java 从零实现的轻量级 Agent 运行时。目标很简单:让 Java 开发者像写业务代码一样,把一个能“感知、推理、调用工具、自我纠错”的 Agent 嵌进自己的应用里。今天这篇文章,我把它的核心设计和实现细节完整拆开,包括循环机制、工具调用、上下文管理、并发控制以及我实测中踩过的坑。如果你正在做 Java 侧的 AI 应用,或者想看一个 Agent 框架内部到底长什么样,这篇应该对你有用。
1. 为什么要在 Java 生态里造一个 Agent 框架
1.1 Agent 框架本质上是“循环”,不是“SDK 封装”
先说一个很容易跑偏的认知。很多人第一次接触 Agent 框架时,会把它理解成“把大模型接口封装一下,方便调用”,于是做出来的东西其实是个 LLM SDK 套壳,离 Agent 差得远。
真正让一个系统被称为 Agent 的,是它具备一个持续的运行循环:系统先从用户那里拿到目标,把目标交给大模型推理,模型生成的不只是回答,还可能是一系列“要调用什么工具、传什么参数”的动作指令;系统执行这些指令,拿到工具返回的观察结果,再把这些结果送回给模型,模型基于新的信息继续推理,直到产出最终答案。
这个循环通常被称为 ReAct(Reason + Act),它意味着 Agent 不是一个一次性的“输入-输出”函数,而是一个状态不断演进的过程。这个区别决定了设计上的根本差异:SDK 只需要暴露一个 chat 方法,而 Agent 框架需要管理会话状态、维护消息历史、处理工具注册与调用、应对模型返回异常、控制循环何时终止。
很多团队直接拿 LLM SDK 去开发“Agent”,最后发现任务一复杂就崩,原因恰恰是少了这层循环治理。
1.2 Java 生态的 Agent 框架缺口
把视线放回 Java 技术栈看看现状:主流、成熟的 Agent 框架几乎都在 Python 侧实现。Python 的优势在于 AI 生态深厚,从模型 SDK、向量库到各种工具插件应有尽有,开发调试链路短。
但现实中的企业级系统,尤其是金融、电商、制造等行业的业务中台,技术栈主力仍然是 Java。这些系统沉淀了完整的用户体系、权限模型、事务机制、消息队列和运维监控设施。如果要把 Agent 能力嵌进这些体系,让 AI 直接调用内部的订单接口、库存服务、客户数据查询,那么用 Java 原生实现显然在集成成本、部署运维、性能调优上有不可替代的优势——不用起旁路服务,不用走跨进程 RPC,直接在业务进程里就是一个组件。
JavaManus 的定位,就是在这样的背景下确定的:不做大而全的低代码平台,不搞复杂的工作流编排画布,而是提供一个可嵌入 Java 服务的 Agent 运行时核心。它要解决的是四个问题:模型接入的抽象、Agent 循环的执行、工具的统一注册与调用、上下文的管理与裁剪。
1.3 设计边界:先做对这四件事,再谈其他
任何框架一上来就想什么都支持,最后往往什么都做不扎实。在设计 JavaManus 时,我给自己划了一条边界:第一版只做单 Agent 的完整闭环。
具体说,这版要覆盖的非功能需求包括:
- 支持对接主流大模型服务的 HTTP 接口,不绑定具体厂商
- 提供流式和非流式两种模型调用模式
- 内置可扩展的工具注册中心,新工具接入成本控制在“写一个类加一个注解”
- 对多轮会话里的上下文自动做 token 统计与裁剪
- 工具执行异常能够反馈回模型,让 Agent 有自我纠错的机会
至于多 Agent 协作、复杂工作流、知识库 RAG 这类进阶能力,放进后续迭代。单 Agent 循环如果跑不稳,之上的所有复杂功能都是空中楼阁。这个取舍我认为是 JavaManus 到目前为止最正确的一个决定。
2. 核心循环设计:Agent 不等于“调大模型”
2.1 循环的状态机:从 Idle 到 Completed
Agent 循环的本质是一个状态机。我在 JavaManus 里为它定义了一组清晰的状态,避免在循环代码里到处写 if-else 导致状态混乱。
public enum AgentState { IDLE, // 会话创建,等待用户输入 THINKING, // 已提交模型请求,等待模型响应 ACTING, // 模型返回工具调用指令,正在执行工具 OBSERVING, // 工具执行完成,将观察结果追加到上下文 COMPLETED, // 模型返回最终答案,循环结束 FAILED // 超过最大轮数或出现不可恢复错误 }状态之间的流转是:IDLE 收到用户消息后进入 THINKING;模型响应里如果带工具调用参数,进入 ACTING,工具执行完转 OBSERVING,把结果拼回消息列表再回到 THINKING;如果模型响应里只有文本且判定为最终答案,则转 COMPLETED。每一轮循环都记录轮次计数,超过预设上限(一般 10~15 轮)直接转 FAILED,防止模型陷入死循环。
这个状态机的价值在于,它让 Agent 执行过程变得可观测、可中断、可恢复。后续如果要加断点续跑、人工审批,只需要在状态节点上挂扩展点。
2.2 Session:一切状态的容器
循环不是孤立的函数调用,它需要携带跨轮次的上下文。我在框架里引入了一个 AgentSession 对象,作为一次任务执行期间所有状态的容器。
public class AgentSession { private final String sessionId; private final Deque<Message> messages; private final Map<String, ToolHandler> toolRegistry; private final AgentState state; private final int maxRounds; public void appendMessage(Message message) { messages.addLast(message); // 触发上下文裁剪策略 ContextPolicyExecutor.apply(messages); } }之所以用 Deque 而不是 List,是因为上下文裁剪时经常需要从头部移除过期消息,双端队列在内存和性能上更合适。Session 还持有工具注册表的引用,因为 ACTING 状态的执行逻辑需要根据模型给出的工具名找到对应的处理器。
设计 Session 而非把状态散落在执行器里,是参考了 HTTP Session 的思路:一次完整的 Agent 任务,无论中间有多少轮推理和工具调用,都属于同一个逻辑单元。这个单元需要被唯一标识、被追踪、被清理,Session 就是它的载体。
2.3 循环骨架:30 行代码说清执行逻辑
整个 Agent 循环的执行逻辑,去掉细节后其实很紧凑。我把它抽成了下面这个骨架,是 JavaManus 里 AgentExecutor 的核心:
public AgentResult execute(AgentRequest request) { AgentSession session = sessionFactory.create(request); session.appendMessage(Message.user(request.getUserInput())); while (session.getState() != COMPLETED && session.getState() != FAILED) { if (session.getRounds() >= session.getMaxRounds()) { session.fail("Exceeded max rounds"); break; } // 1. 调用模型 ModelResponse response = modelProvider.chat( session.getMessages(), session.getToolSchemas() ); // 2. 判断模型意图 if (response.hasToolCalls()) { for (ToolCall call : response.getToolCalls()) { Observation observation = toolExecutor.execute(call); session.appendMessage(Message.observation(observation)); } } else if (response.hasFinalAnswer()) { session.appendMessage(Message.assistant(response.getContent())); session.complete(); } else { session.fail("Model returned empty response"); } session.incrementRounds(); } return AgentResult.of(session); }这个骨架里最关键的设计决策是:模型调用和工具执行是串行交替的,而不是先批量生成再批量执行。每一轮工具执行的结果必须立刻追加进消息上下文,这样才能在下一次模型调用时被模型感知到。如果一次性把所有工具调用都执行完再整体返回,模型就失去了“根据前一个工具的结果调整下一个动作”的机会,这在多步骤任务里是致命的。
2.4 为什么模型返回的“意图”需要协议化
注意细节的话会发现,循环里判断模型意图用的是response.hasToolCalls()和response.hasFinalAnswer()。这里有个很隐蔽但重要的设计:模型如何告诉框架“我要调用工具”而不是“我给出答案了”?
业界主流的做法是让模型返回结构化的工具调用指令,也就是在请求里声明有哪些工具可用,模型在需要时会返回一个特殊的工具调用结构,包含工具名和 JSON 格式的参数。JavaManus 的 ModelProvider 抽象层会把不同厂商的响应格式统一成内部的 ModelResponse 对象,屏蔽掉协议差异。
我有意识地在设计时避免依赖“模型在文本里输出特定标记”这种脆弱方式。早年很多 Agent 实现靠正则解析模型输出里的 Action/Action Input 标记,一旦模型格式稍有偏差就全部崩溃。现在我用结构化工具调用协议加上解析兜底,稳定性提升了不止一个量级。
3. 模块拆解:JavaManus 的五层架构
3.1 分层原则:依赖方向单向,扩展点外置
JavaManus 没有采用过于复杂的微内核架构,而是沿用了我在后端系统里一贯的风格:分层清晰、依赖单向、扩展点通过接口暴露。
五个核心层次从内到外分别是:
- 模型接入层(Model Provider):封装不同厂商的模型服务调用,统一输入输出协议
- 会话管理层(Session Manager):负责消息历史、Token 统计、上下文裁剪
- 推理循环层(Agent Executor):实现 ReAct 循环,协调模型调用与工具执行
- 工具层(Tool Layer):定义工具接口,维护工具注册中心
- 扩展层(Extension):通过事件监听器暴露循环节点,用于日志、监控和自定义逻辑
依赖方向是外层依赖内层,内层不感知外层的存在。举例来说,模型接入层不知道工具层怎么工作,它只负责把消息列表变成模型响应。这样的好处是每个层可以独立测试,替换实现时不会引发连锁修改。
3.2 模型接入层:面向协议编程,而非面向厂商
模型接入层是 JavaManus 里最“政治正确”的一层。现在各家大模型服务接口虽然大体相似,但细节上都有差异:有的支持流式,有的参数名不同,有的返回结构不同。如果让上层逻辑直接依赖某个厂商的 SDK,后续切换模型几乎等于重写一遍。
我的做法是定义一个极简的 ModelProvider 接口:
public interface ModelProvider { ModelResponse chat(List<Message> messages, List<ToolSchema> toolSchemas); Stream<ModelResponse> chatStream(List<Message> messages, List<ToolSchema> toolSchemas); }然后针对不同厂商实现适配器。由于大多数模型服务都支持业界通用的 Chat Completions 风格的 HTTP 协议,适配器内部核心逻辑是:把 Message 列表序列化成 JSON 请求体,用 Java 内置的 java.net.http.HttpClient 发出去,再把响应 JSON 反序列化成统一的 ModelResponse 对象。
Java 标准库的 HttpClient 在这个场景下完全够用。它支持 HTTP/2、连接复用、异步发送,不需要额外引入重量级 HTTP 客户端框架。我在适配器里配置了连接超时 10 秒、读取超时 60 秒,因为模型推理耗时通常较长,读取超时设太短容易误杀正常请求。
3.3 工具层:让“给 Agent 加能力”变成写一个类的事
工具层是 Agent 能力的来源,也是 JavaManus 里开发体验最直观的部分。我在设计工具注册机制时,参考了 Spring 的组件扫描思路:一个工具就是一个标注了注解的类,框架启动时自动收集并注册到工具注册中心。
@AgentTool(name = "order_query", description = "查询订单状态,参数:orderId(字符串,必填)") public class OrderQueryTool { public String execute(String orderId) { // 实际业务逻辑:查数据库、调订单服务等 return "订单 " + orderId + " 当前状态:已发货"; } }框架在扫描到这个类后,会通过反射解析方法签名,自动生成工具描述所需的 JSON Schema。这里有个细节:如果工具方法参数只有一个基础类型,直接映射;如果参数复杂,建议用一个 POJO 接收,框架会把这个 POJO 的字段结构转换成 JSON Schema,这样模型就能知道该传哪些字段。
工具执行结果统一转成字符串返回给模型。有些工具返回结构化数据,我会先序列化成 JSON 字符串;如果是我们内部的服务调用,返回的自然语言描述也完全可以。模型并不在乎观察结果的格式,它只需要从中提取有用的信息做下一步决策。
3.4 扩展层:事件监听器让框架不再是黑盒
Agent 循环运行过程中,开发者往往需要知道“现在到哪一步了”“模型这次返回了什么”“工具调用花了多久”。为了满足这些需求,JavaManus 在 Executor 的关键节点上埋了事件。
事件通过一个简单的监听器接口暴露:
public interface AgentEventListener { default void onThinking(AgentSession session, ModelRequest request) {} default void onToolCall(AgentSession session, ToolCall call, long durationMs) {} default void onToolResult(AgentSession session, Observation observation) {} default void onCompleted(AgentSession session, AgentResult result) {} default void onFailed(AgentSession session, Throwable error) {} }业务方可以自由实现这些回调方法,比如在 onToolCall 里记录审计日志、在 onThinking 里推送实时进度到前端。这个设计让框架在具备闭环能力的同时保持透明,属于投入很小但收益极大的架构决策。
4. 工具调用的“最后一公里”:从 JSON Schema 到方法执行
4.1 让模型“看懂”工具:Schema 生成与参数校验
工具调用链路里最先出问题的地方,恰恰在最前面:模型根本不知道有哪些工具可用,或者知道有但是传参总是不对。要解决这个问题,必须把工具的能力边界明确地告诉模型。
JavaManus 在每次模型调用时,都会把当前会话注册的所有工具转换成工具 Schema 列表,随请求一起发给模型。每个 Schema 至少包含三部分:名称、描述、参数结构。描述写的越清楚,模型选错工具的概率越低。比如订单查询工具的 description,我建议写成“根据订单号查询订单的实时状态,用于物流跟踪和售后处理场景”,而不是简单的“查询订单”。
参数结构用 JSON Schema 格式描述,框架在扫描工具类时自动生成。模型在决定调用工具时,会按照这个 Schema 中的字段名和类型生成参数 JSON,这样在后面反序列化时才有据可依。
4.2 参数反序列化的容错处理:模型不是严谨的机器
如果说生成 Schema 是第一步,那么解析模型返回的参数就是最容易翻车的环节。模型生成的 JSON 常常会有“不老实”的地方:数字可能带小数点,字符串可能多余空格,字段名偶尔和 Schema 不一致,更有甚者输出被截断导致 JSON 不完整。
在 JavaManus 里,我封装了一个参数解析器,专门处理这些异常场景。核心逻辑是先尝试标准反序列化,失败后使用宽松模式:手工提取参数段,按逗号切分键值对,再做类型转换。
public Map<String, Object> parseArguments(String toolCallId, String rawArguments) { try { return objectMapper.readValue(rawArguments, new TypeReference<Map<String, Object>>() {}); } catch (JsonProcessingException e) { // 宽松模式:截取最外层花括号之间的内容,丢弃被截断的尾部 String cleaned = extractPartialJson(rawArguments); return objectMapper.readValue(cleaned, new TypeReference<Map<String, Object>>() {}); } }这个“先严格后宽松”的策略在实际使用中挽救了大量调用。特别是当模型生成流式输出时,如果遇到网络波动导致响应体不完整,宽松解析能大概率提取到有效参数,避免整个任务失败。
4.3 工具执行异常:把错误变成模型的“养料”
JavaManus 在工具执行上的一个重要理念是:工具异常不应该直接导致 Agent 任务失败,而应该作为观察结果反馈给模型。
比如某个工具要求“参数是数字”,但模型传了个字符串“abc”,这时候如果直接抛异常退出循环,用户就只会看到一个干巴巴的报错。我改成捕获异常,把异常信息包装成一个 Observation 消息追加到上下文,模型在下一轮推理时看到“工具执行失败:参数格式错误,需要数字类型”,它通常会修正参数重新尝试。
试想人类的做事方式:一个人调用接口失败了,他大概率会看一眼报错信息然后调整策略再来一次。Agent 最接近人类智能的地方恰恰在这个反馈闭环里。JavaManus 的 ToolExecutor 里预留了失败重试次数设置,默认 2 次,超过后把异常作为最终观察结果返回模型,由模型决定是换一种工具还是直接告知用户失败原因。
5. 上下文管理与记忆:让 Agent 不“失忆”
5.1 Token 是 Agent 的“有限工作记忆”
大模型有上下文窗口限制,这意味着 Agent 在执行多轮任务时,消息列表不可能无限增长。这个问题在真实项目里几乎一定会遇到:一个复杂任务跑上五六轮,每轮还要塞入大段的工具返回结果,很快 token 就逼近上限了。
我把上下文管理看作 Agent 框架的“记忆系统”,类比人类的工作记忆。工作记忆容量有限,人会把重要信息转移成长时记忆,需要时再调取。Agent 的上下文裁剪也遵循同样的逻辑。
5.2 Token 估算:不调 API 也能知道大概花了多少
在裁剪上下文之前,需要先知道当前消息列表大概占了多少 token。最精确的做法是调用模型自带的 token 计数器接口,但每轮都调一次既不经济也增加延迟。实际工程里更务实的方式是本地估算。
我用的估算规则是:中文字符按 1.7 token 每个估算,英文字符按 0.3 token 每个(大约 3.3 个字符一个 token),代码块和特殊字符适当加权。虽然这个估算和厂商的精确 tokenizer 有一定误差,但对裁剪决策来说足够了。
估算的目的不是精确计费,而是判断当前上下文离模型窗口上限还有多远。预留 20% 的余量作为模型输出的空间,一旦估算值超过窗口上限的 80%,就触发裁剪。
5.3 裁剪策略:先摘要后截断,滚动窗口兜底
JavaManus 实现了三种裁剪策略,可以按场景配置:
| 策略 | 做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 直接截断 | 保留系统消息+最近N轮,丢弃最老消息 | 简单快速 | 丢掉早期关键信息 | 短会话、经验证无回头需求 |
| 滚动窗口 | 固定保留最近K轮完整消息 | 实现简单,行为可预期 | 上下文割裂,模型可能不知道前因后果 | 一般业务操作类任务 |
| 摘要压缩 | 调用模型把早期历史生成摘要,替换原文 | 最大化保留信息 | 额外消耗一次模型调用 | 长对话、复杂多步推理 |
我实际用得最多的是“摘要压缩+滚动窗口”的组合:先把早期历史压缩成一段摘要,作为 system 消息挂在最前面,然后保留最近的 6 轮完整消息。这样模型既有全局脉络,又看到最近的细节。
动态调整的滚动窗口还有一层设计:为了支持“用户中途修正指令”的场景,最近几轮消息始终完整保留,这样即使早期信息被摘要了,用户最新的意图仍然能完整地被模型感知。
5.4 结构化记忆:让跨会话信息也能被感知
光有消息裁剪还不够,部分场景下 Agent 需要跨会话记住一些结构化信息。比如用户身份、偏好设置、任务编号。这些信息如果写进自然语言历史里,会被摘要压缩弄丢。
我在 Session 里单独维护了一个结构化记忆区,用 Map 存储键值对业务信息。在每次组装模型请求时,把结构化记忆序列化成一个独立的 JSON 块,注入到系统提示里。这样即使用户消息列表被裁剪得再厉害,关键业务参数也不会丢。
这个设计的灵感来自 RAG 的思路:不是让模型记住一切,而是在需要时把最相关的信息检索出来投喂给它。结构化记忆就是最简单的“确定性检索”。
6. Java 实现里的坑:泛型擦除、并发与超时控制
6.1 泛型擦除:写 Tool 时拿不到类型怎么办
在 Java 里写通用框架,泛型擦除是绕不开的坎。设计 Tool 接口时,T 是工具执行结果类型,本想着在运行时反射拿到 T 去序列化观察结果,结果发现因为擦除,T 完全不可见。
网上很多框架面对这个问题的做法是让使用者额外传入一个 TypeReference,但这样 API 就变丑了。JavaManus 的妥协方案是:框架里工具执行结果统一用 Object 传递,序列化时通过 FastType 判断——对常见的 String、Integer、Long、Map、List 做显式类型分支,自定义 POJO 则由工具实现类自己负责返回 JSON 字符串。这个取舍牺牲了一定的类型安全,但换来了工具开发的简单直接。
6.2 并发调用:虚拟线程让多工具并行零成本
一个 Agent 任务里经常出现“模型一次返回多个工具调用”的情况。比如要让 Agent 同时查天气和查机票,这两个工具互不依赖,完全应该并行执行。
Java 21+ 的虚拟线程在这里派上了大用场。我用 ExecutorService 创建虚拟线程池,把每个工具调用丢进池子里并发执行,然后等待所有结果返回后统一追加到消息列表。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { List<Future<Observation>> futures = toolCalls.stream() .map(call -> executor.submit(() -> executeTool(call))) .toList(); for (Future<Observation> future : futures) { observations.add(future.get(10, TimeUnit.SECONDS)); } }虚拟线程的极大优势在于,即使同时跑几十个工具调用,也不需要维护庞大的线程池配置。对比传统平台线程,每个线程占 1MB 栈空间,虚拟线程的开销可以忽略不计。执行工具时如果内部有阻塞 IO,虚拟线程会自动让出载体线程,并发能力提升非常明显。
6.3 超时控制:给大模型和工具分别设好“闹钟”
Agent 循环中最痛苦的故障是“既不成功也不失败,就是一直挂着”。模型服务偶发响应慢,工具调用的下游接口也可能阻塞。没有超时控制的 Agent 在线上就是定时炸弹。
我把超时分成两级:模型调用超时和工具执行超时。
模型调用超时在 ModelProvider 适配器层设置,非流式模式下读取超时建议 60 秒,流式模式下首包超时 30 秒、整体无数据超时 60 秒。工具执行超时在 ToolExecutor 层通过 Future.get 的 timeout 参数控制,每个工具默认 10 秒,超时后把 TimeoutException 包装成观察结果返回模型,让模型知道“这个工具挂了,换个思路”。
这种超时策略用一句话概括:超时不是终止,而是反馈。只要循环还没结束,超时的工具调用就是一条新的观察信息,Agent 可以据此调整策略。
6.4 JSON 容错:截断、转义、非法反斜杠的处理
大模型生成 JSON 时翻车的姿势多种多样。最常见的是响应被截断,整个 JSON 只有前半截;其次是在字符串字面量里出现未转义的反斜杠;还有就是字段类型和预期不一致。
JavaManus 的 JSON 容错层做了三件事。第一,解析前先做基础清洗:把模型常见的“包裹代码块”痕迹去掉。第二,解析失败时尝试提取最外层花括号区块再做二次解析。第三,如果目标字段需要 int 但收到的是 12.0 这样的 Double,做一次数值语义的宽松转换。
这些容错逻辑加起来不到两百行代码,却撑起了整套框架在真实数据下的可用性。在模型输出结构化数据这件事上,不要相信“大模型已经很准了”这种话,永远做一层兜底。
7. 与 Python 生态的对比:Java Agent 的价值在哪里
7.1 一张表看清差异
做 JavaManus 的过程中,我不止一次被问:为什么不用现成的 Python Agent 框架?我把实际对比的结论整理成了一张表。
| 维度 | Python 生态 Agent 框架 | JavaManus(Java 原生) |
|---|---|---|
| 组件丰富度 | 高,各类 SDK 和工具插件多 | 中等,常用能力需要自行封装 |
| 开发速度 | 快,上手容易,原型阶段优势明显 | 略慢,框架层面的代码要自己写 |
| 性能与并发 | 依赖进程内协程,高并发需额外部署 | 虚拟线程加持,并发能力出色 |
| 与 Java 业务系统集成 | 需要跨进程 RPC,链路长 | 零成本嵌入,共享进程内上下文 |
| 运维成本 | 额外维护 Python 服务、环境、监控 | 与应用一体部署,无额外组件 |
| 统一类型体系 | 动态类型,灵活但运行时风险高 | 静态类型,编译期发现大部分问题 |
7.2 什么场景下 Java 原生 Agent 更合适
从我的实践来看,如果你的 Agent 主要任务是“调用现有 Java 服务的能力”,比如查业务库、调内部接口、生成报表,那 Java 原生框架的嵌入价值极其明显。想象一个场景:Agent 要查询某订单的物流状态并给用户做出解释,Python 方案需要 Agent 通过 HTTP 调用 Java 服务暴露的 OpenAPI,来回多走两跳,还要处理认证、限流、监控。而 Java 坐拥框架原型,可以直接实例化一个业务服务类,在进程内完成调用,时延少了十几毫秒,且事务、权限、日志链路完全复用现有设施的语义。
再者,Java 项目对类型安全和编译期检查有天然依赖。大型团队协作时,没有人希望 Agent 的工具参数在写出错后才在运行时爆炸。Java 的强类型在 Agent 工具注册这一层带来了巨大的安心感。
7.3 承认不足:Java 生态的 Agent 组件仍需“补课”
说完了优势,也必须承认 Java 生态在 Agent 这件事上确实有短板。Python 侧很多现成的组件在 Java 侧找不到高质量替代,比如某些特定领域的数据处理库、可视化工具、机器学习工具链。如果 Agent 的核心任务严重依赖这类组件,Java 原生实现会显得力不从心。
所以我的建议一直是:不要把 Java 和 Python 当成对立面。实际项目中可能是 Java 主管业务闭环、Python 主管重计算和模型侧能力,两边通过异步消息或 API 衔接。JavaManus 的定位是“业务系统里的 Agent 大脑”,而不是取代 Python AI 生态的工具。
8. 从 JavaManus 到生产可用:还有哪些事必须做
8.1 可观测性:Agent 的每一步都要留下足迹
Agent 在生产环境里最让人头疼的问题是不确定性。传统接口要么返回成功要么失败,Agent 的流程是动态变化的,一个任务可能经历多种工具组合。如果没有可观测性,出了问题根本无从排查。
JavaManus 从最开始就把事件监听器设计成可观测性的基础。我在项目里接入了一套日志方案:每次模型请求记录消息数和估算 token 数,每次工具调用记录工具名、参数摘要、耗时,每次循环结束记录轮次和状态。这些日志以 JSON 格式输出,可供日志平台检索和告警。
建议任何上生产的 Agent 应用,至少要追踪四个指标:任务成功率、平均轮次、工具调用耗时分布、上下文裁剪频率。裁剪频率过高通常意味着任务设计不合理或者需要引入外部记忆组件。
8.2 安全与权限:让 Agent 只能在“笼子”里干活
Agent 能调用工具,意味着它拥有执行能力,这对安全体系是严峻考验。JavaManus 的工具注册中心支持在工具执行前插入权限检查回调,确保只有被授权范围内的工具才允许执行。
更细致的设计是参数级别的白名单校验。比如一个“发送邮件”工具,可以在回调里规定:允许发送到哪些域名、邮件内容最大长度、每分钟调用次数上限。这些策略虽然要额外写代码,但在金融、医疗、政务等场景里是不可妥协的底线。
我强烈建议不要让 Agent 直接操作生产数据库或执行任意命令行。即使在内部环境做实验,也一定要先通过只读账户、沙箱容器等手段限制其影响半径。这是我在多个项目上看到血泪教训后总结出来的。
8.3 后续演进:多 Agent 协作与记忆外部化
如果单 Agent 循环已经足够稳定,后续可以沿着两个方向演进。
第一个方向是多 Agent 协作。不再是一个 Agent 包揽所有工具,而是设一个主控 Agent 负责任务拆解,把子任务分发给多个专职 Agent,各 Agent 再汇聚结果。这个方向需要基础设施支持:子 Agent 的会话隔离、结果汇总格式、任务排布与并发控制,复杂度会上一个新台阶。
第二个方向是记忆外部化。把 Session 里的结构化记忆和摘要同步到外部存储,比如本地 KV、向量数据库,让 Agent 在大规模历史和跨会话场景下依然能快速定位关键信息。这个方向适合做知识密集型的 Agent,比如企业知识库问答助手。
就目前而言,JavaManus 先把单 Agent 的可靠性打磨到位,再逐步往这两个方向扩展。
写在最后
从零写一个 Agent 框架,和用现成框架最大的区别在于:你对每一层行为都有完全的控制力。模型返回的任何意外、工具的任何一个异常、上下文的每一次裁剪,都在你的掌控之中,不存在“框架为什么这么设计”的黑盒困惑。当然代价是,这部分代码从一开始就需要自己维护、自己测试,工作量是实打实的。
我做完 JavaManus 之后的体会是,Agent 框架的复杂度不在“调用模型”这一下,而在循环的治理、上下文的保鲜、工具的规范化,以及大量边界情况的容错。如果你也是 Java 后端出身,想做 Agent 又没有合适的现成方案,我的建议是从一个最小的循环开始:先接模型、再挂两个工具、跑通“推理-执行-观察”的基础闭环,然后逐步加厚。不要一上来就追求多 Agent 和花哨的编排能力——把单 Agent 的底座打扎实,后面所有上层建筑都有的放矢。