用Java构建AI Agent框架:从Manus到JavaManus核心设计 2025 年 3 月Manus 在技术圈刷屏的时候我作为常年搞 Java 后端的人第一反应不是去看它的 Demo 视频而是去翻它的开源仓库。看完之后我意识到一个尴尬的事实整个 AI Agent 的主流生态几乎被 Python 垄断LangChain、CrewAI、Dify 全是 Python 系的。可我所在团队的后端基础设施全是 Java / Spring Boot如果要在现有业务系统里嵌入 Agent 能力要么引入一套异构技术栈要么自己动手用 Java 造轮子。我选了后者于是就有了 JavaManus 这个项目——一个借鉴 Manus 设计思想、完全用 Java 实现的 AI Agent 框架。JavaManus 能做什么核心就三件事把自然语言任务拆解成可执行的子任务按顺序调用外部工具完成实际操作再把结果整理成最终回复。它不依赖任何 Python 生态的东西用 Java 17 自带的 HttpClient 加 Jackson 就能跑起来。这篇我会把框架的整体设计、核心引擎、工具抽象层、记忆管理、并行执行全部拆开讲最后用一个端到端的例子带着走一遍完整流程。如果你是想把 Agent 能力嵌入 Java 企业系统的工程师或者对 Agent 底层原理感兴趣的人这篇文章应该能帮你省不少弯路。1. 为什么是 Java在 Python 垄断的 Agent 生态里另起炉灶1.1 现实情况Agent 框架的 Python 一边倒AI Agent 这个词这两年被炒得很热但真正动手做过的人都知道它不是什么神秘技术本质就是大模型 任务规划 工具调用 记忆管理的编排器。目前主流的开源实现LangChain 用 PythonCrewAI 用 PythonDify 后端虽然也有别的语言但生态和插件体系基本围绕 Python 转。Python 在这个领域的优势毋庸置疑AI 相关的 SDK 最全模型厂商的第一方库基本都是 Python 优先学术圈的代码也全是 Python 写的。但问题在于企业级 Java 系统要接这些东西成本是非常高的。你需要在现有服务旁边再维护一套 Python 微服务两边用 HTTP 或消息队列通信不仅要处理两套部署链路还要面对 Python 侧类型不够严谨、并发模型和企业基础设施不匹配等一系列问题。以我实际经历来说团队里为了接一个 Agent 能力光环境统一就折腾了两周最后代码没写几行运维倒是先崩溃了。1.2 Java 做编排层的核心优势那 Java 适合做 Agent 框架吗我的判断是不仅适合而且天然匹配。Agent 框架本质上是一个胶水层它要做的事情是接收用户请求、调用大模型、根据模型返回的指令调用工具、管理多轮上下文、把最终结果拼装返回。这套流程里有大量系统级的工作——并发调度、超时控制、重试机制、日志链路、状态管理——这些恰恰是 Java 生态积累最深的地方。具体来说并发能力Agent 执行过程中经常需要并行跑多个子任务Java 的 CompletableFuture、虚拟线程Java 21能非常优雅地处理这种编排Python 的 asyncio 虽然也能做但心智负担明显更高。类型安全工具调用的参数解析、消息结构的序列化反序列化这种涉及大量数据契约转换的地方Java 的强类型体系能在编译期就拦截掉大量低级错误。企业集成Java 后端要调用内部 RPC、访问数据库、对接 MQ这些都是 Java 工程师的日常把 Agent 做成 Java 库可以直接嵌入现有 Spring Boot 服务不需要额外部署。1.3 明确边界JavaManus 不做什么很多 Agent 框架做着做着就膨胀了什么都要管。我在设计 JavaManus 的时候给自己划了几条硬边界不做模型训练和微调Agent 框架的职责是编排不是学习。模型能力靠外部 API框架只负责把模型能力组织起来。不做向量数据库底层实现RAG 可能会用到向量检索但 JavaManus 只定义检索接口具体实现交给外部存储比如 Elasticsearch、Milvus 的 Java 客户端。不做复杂的可视化编排界面框架层面只提供可编程的 API前端工作不是现在要考虑的。守住这些边界框架才能保持轻量聚焦在如何组织大模型与工具这个核心问题上。2. 总体架构从 Manus 的 Ori/Worker 协作模型说起2.1 Ori 与 Worker一个管规划一个管执行JavaManus 的架构灵感直接来自 Manus 的 Multiple Agent Architecture。Manus 的设计里系统不是一个全能 Agent 包打天下而是拆成两类角色OriPlanner负责接收用户目标、理解意图、拆解执行计划WorkerExecutor负责执行具体的工具调用把每一步的结果反馈回来。这个设计背后的逻辑很朴素规划需要的大模型能力和执行需要的大模型能力其实是两种不同的负载。规划要求模型有全局视野、能把模糊的指令结构化成清晰的步骤执行则要求模型能准确理解当前子任务、选择正确工具、解析工具返回的数据。把这两种职责混在一个模型上下文里容易出现上下文污染——规划时的长文本分析会挤占执行时的有效 token 空间。拆成两个角色之后每个模型的上下文窗口压力都显著降低出错的概率也会小很多。在 JavaManus 里我保留了 Ori / Worker 的双角色设计但做了一层简化两者共用一个底层引擎 AgentLoop只是在提示词System Prompt和可用的工具集合上做了区分。也就是说Ori 和 Worker 不是两套完全不同的代码而是同一个引擎配了不同的人格配置。这样实现成本低后面扩展第三种角色比如专门做反思的 Reviewer也只需要再加一套提示词和工具白名单。2.2 核心抽象消息、工具、上下文、循环JavaManus 的代码里最核心的抽象只有四个其他所有模块都围绕它们展开抽象职责关键属性/方法AgentMessage统一的对话消息格式role、content、toolCallId、toolCallsAgentTool工具的统一接口name、description、run(JsonNode args)AgentContext一次会话的共享状态消息列表、变量槽、工具注册表、配置AgentLoop循环引擎run()、step()、isFinished()AgentMessage 是整个框架的数据基石。大模型 API 需要的是 role/content 结构的消息工具调用需要额外携带 tool_call_id 和参数AgentMessage 把这些全部统一成一个对象。你可能觉得这不就是一个 DTO 吗但实际做下来你会发现消息模型设计的好坏直接决定框架的扩展性——后面要支持多模态、要支持流式输出、要支持工具结果回填全都靠这个类的字段能不能撑得住。AgentContext 则是每个会话的沙盒。它保存了当前会话的完整消息历史、已经注册的工具、以及跨步骤传递的变量。比如 Worker 在执行子任务时需要知道 Ori 规划的步骤文本这个值就放在 Context 的变量槽里而不是硬编码在代码中。这样设计的好处是同一个会话内的多个子任务可以共享信息而不同的会话之间又完全隔离。2.3 一次完整调用的数据流把架构串起来看一次完整请求的流转长这样用户传入自然语言任务OriAgent 启动把任务文本放入 AgentContext。AgentLoop 把系统提示词 用户任务 工具定义列表组装成 ChatCompletion 请求发送给大模型。大模型返回结果两种可能普通文本表示规划完成或最终答案或 tool_calls表示需要调用工具。如果是 tool_callsOri 解析出工具名和参数通过 ToolRegistry 找到对应方法执行调用。工具执行结果作为 tool 角色的消息回填到消息列表继续下一次循环。如果 Ori 判断规划完成会把子任务列表写入 Context交给 WorkerAgent 逐个执行。Worker 执行每个子任务时同样走 AgentLoop直到产出最终结果回传给 Ori 汇总。Ori 汇总所有 Worker 的结果生成最终回复返回给用户。这个流程看着有点绕但跑起来之后你会发现它其实很有弹性任务简单的时候Ori 可能一轮循环就输出 FINAL_ANSWER任务复杂的时候Ori 会反复调用工具、反复调整计划直到结果收敛或达到迭代上限。整个编排能力就藏在这个循环—判断—执行—再循环的过程里。3. 核心引擎 AgentLoop让大模型从聊天走向动手3.1 循环的基本形态AgentLoop 是整个框架的发动机也是我和大模型交互的主循环。它的核心逻辑用伪代码描述极其简单反复调用大模型如果模型说我要调用工具就执行工具并把结果喂回去如果模型说我完成了就跳出循环。但把这段逻辑写进 Java 工程里需要考虑的细节就多了。先看一段核心代码public AgentMessage run(String userInput) { AgentContext ctx this.context; ctx.appendMessage(AgentMessage.user(userInput)); for (int step 0; step ctx.getMaxIterations(); step) { ChatCompletionRequest request buildRequest(ctx); ChatCompletionResponse response llmClient.chat(request); AgentMessage message response.toAgentMessage(); ctx.appendMessage(message); // 模型要求调用工具 ListToolCall toolCalls message.getToolCalls(); if (toolCalls ! null !toolCalls.isEmpty()) { for (ToolCall call : toolCalls) { JsonNode result toolRegistry.invoke(call.getFunctionName(), call.getArguments()); ctx.appendMessage(AgentMessage.toolResult(call.getId(), result.toString())); } continue; } // 模型输出最终答复 if (message.isFinalAnswer()) { return message; } // 不是最终答复且无工具调用视为普通中间输出继续循环 log.debug(Step {} intermediate output: {}, step, message.getContent()); } throw new AgentLoopExceededException(Reached max iterations: ctx.getMaxIterations()); }你可能会问这一步一循环地调大模型成本会不会太高确实会所以 maxIterations 这个参数很关键。我默认设置是 30 次实际跑下来大多数任务在 5 到 15 次之间就能收敛。超过 30 次的任务十有八九是计划出了问题或者工具返回的数据不合预期让它继续跑纯属浪费 token。3.2 与 OpenAI 兼容接口的对接细节JavaManus 不绑定某个特定厂商的模型 SDK而是直接对接 OpenAI 兼容的 ChatCompletion 接口。原因很简单现在国内外的模型厂商DeepSeek、通义千问、智谱 GLM、Moonshot基本都提供了 OpenAI 兼容的 HTTP 接口只要换个 baseUrl 和 apiKey 就能用。这让我不需要为每个模型写一套适配代码。请求体的组装有一个关键点工具定义列表。大模型并不知道你有哪些工具你需要把每个工具的 JSON Schema 描述发给它它才能在你需要的时候生成正确的 tool_call。这个我会在第四章详细展开。请求体大致结构如下MapString, Object toolDef new HashMap(); toolDef.put(type, function); toolDef.put(function, functionSchema); // name description parameters响应解析时重点是从 choices[0].message 里区分 content 和 tool_calls 两个字段。content 可能是 null当模型选择调用工具时tool_calls 可能是 null当模型直接回复文本时两个字段都有可能同时为空或同时非空解析时一定要做独立判断不能假设互斥。3.3 终止条件与控制参数AgentLoop 里最需要设计清楚的是什么时候算完。我设置了三个终止条件任何一个命中都会结束循环模型返回 FINAL_ANSWER这是最理想的结束方式。我在系统提示词里明确要求模型如果你认为任务已经完成在回复末尾加上FINAL_ANSWER:前缀框架检测到这个前缀就返回。达到 maxIterations防止死循环的兜底方案。注意这里的迭代次数是模型调用次数不是工具调用次数因为一次模型调用可能附带多个工具调用。工具级异常如果某个工具调用抛出致命异常比如传入了非法参数我会根据异常类型决定是记录后重试还是直接终止。一般参数解析错误我会让模型重试一次如果连续三次同一个工具失败就强制终止并返回中间结果。另外关于AI agent token 是什么意思这个问题我在实际做的时候才真正理解它的分量。Token 是模型对文本切分的基本单位一个 token 可能是半个词也可能是四分之一个中文汉字。每次请求的上下文窗口、计费大小、模型能力上限全部以 token 为单位。JavaManus 的上下文管理本质就是 token 预算管理这个会在第五章展开讲。4. 工具抽象层用注解和反射把 Java 方法变成 Agent 的双手4.1 设计一套工具协议Agent 能不能真正干活取决于它手里的工具够不够多、够不够好用。JavaManus 里把一个工具抽象成三个信息名字、描述、输入参数 Schema。大模型通过这三个信息来决定什么时候调、传什么参数。所以工具注册的代码设计非常关键既要有类型安全性又不能让使用者写太多重复代码。我采用了注解 反射的方案。定义一个注解AgentTool任何类的方法只要标注了这个注解就会被框架自动发现并注册为一个工具。看代码Retention(RetentionPolicy.RUNTIME) Target(ElementType.METHOD) public interface AgentTool { String name(); String description(); }然后在工具方法上这么写public class FileTools { AgentTool(name read_file, description 读取指定路径的文本文件内容返回原始字符串) public String readFile(Parameter(文件路径绝对路径) String path) { return Files.readString(Path.of(path), StandardCharsets.UTF_8); } AgentTool(name write_file, description 将内容写入指定路径文件parent目录不存在会自动创建) public String writeFile(Parameter(目标文件路径) String path, Parameter(文件内容) String content) { Files.writeString(Path.of(path), content, StandardCharsets.UTF_8); return 文件写入成功; } }4.2 反射生成函数 Schema注册工具之后框架会通过反射读取方法名、参数名、注解里的描述自动生成 OpenAI 协议里要求的 function schema JSON。这一步是整个工具层设计中最核心的为什么你写的 Java 方法签名是给 JVM 看的而大模型只能看懂 JSON Schema中间的翻译工作必须自动化。public MapString, Object buildSchema(Method method, AgentTool annotation) { MapString, Object schema new HashMap(); schema.put(name, annotation.name()); schema.put(description, annotation.description()); MapString, Object parameters new HashMap(); parameters.put(type, object); MapString, Object properties new HashMap(); ListString required new ArrayList(); for (Parameter parameter : method.getParameters()) { ParameterDesc desc parameter.getAnnotation(ParameterDesc.class); MapString, Object paramSchema new LinkedHashMap(); paramSchema.put(type, mapJavaType(parameter.getType())); paramSchema.put(description, desc ! null ? desc.value() : ); properties.put(parameter.getName(), paramSchema); required.add(parameter.getName()); } parameters.put(properties, properties); parameters.put(required, required); schema.put(parameters, parameters); return schema; }有个细节需要注意mapJavaType方法需要把 Java 类型映射成 JSON Schema 类型。String 映射为 stringint/Integer 映射为 integerdouble/Double 映射为 numberboolean/Boolean 映射为 boolean。List 要映射成 array items 结构Map 要映射成 object。这些映射规则虽然简单但是漏掉一个基本类型就可能导致模型传参时出现格式错误。4.3 工具调用链路的完整闭环工具调用的完整链路是模型返回 tool_call → 框架解析出函数名和参数 JSON → 反射定位方法 → 参数绑定 → 执行 → 结果转成字符串回填消息列表。这里最容易踩坑的是参数绑定环节大模型生成的 JSON 参数可能和你的 Java 方法参数不完全匹配比如传了个字符串 123 给 int 类型的参数。我的处理方式是用 Jackson 的 ObjectMapper 做参数绑定加上一个宽松策略。如果绑定失败不是直接抛异常而是把错误信息作为 observation 返回给模型让模型自己修正。你可以把这段逻辑看作让 AI 自己 debug——大多数情况下模型看到参数 x 类型错误期望 integer实际是 string之后下一次调用就会自己修正。这个机制比在框架层强制校验要灵活得多。4.4 内置工具集与安全边界JavaManus 内置了一组基础工具覆盖最常见的操作场景工具名功能安全注意事项read_file读取本地文件做路径白名单校验防止读任意系统文件write_file写入本地文件限制写入目录范围execute_shell执行系统命令默认禁用需显式开启限制命令白名单web_request发起 HTTP 请求只允许 GET限制目标域名协议search_memory检索会话历史无外部副作用执行工具的权限是我踩过坑的地方。早期版本 execute_shell 是默认开启的直到有一次 Agent 在调试时执行了一个 rm -rf 命令幸好目标目录是沙箱目录我才意识到工具默认开启是个灾难。现在的策略是安全优先默认全部禁用需要时由开发者在配置里手动开启并且建议在容器里运行 Worker 进程给 Agent 一套受限的操作系统环境。5. 多任务协作与记忆管理让框架能并行、记得住5.1 任务拆分与并行执行Ori 规划出任务列表之后如果所有子任务都串行执行效率会非常低。举个例子一个分析三份报表并汇总结果的任务三个报表的分析过程互相独立完全可以并行。JavaManus 利用 CompletableFuture 实现了 Worker 级别的并行调度。public ListTaskResult executeTasks(ListTask tasks) { ListCompletableFutureTaskResult futures tasks.stream() .map(task - CompletableFuture .supplyAsync(() - worker.execute(task), agentExecutor)) .toList(); return futures.stream() .map(CompletableFuture::join) .toList(); }这段代码看着简单但真正实施时有两个问题要解决。第一个是线程池的大小每个 Worker 线程同时占用一个大模型 HTTP 连接线程池开得太大容易把下游 API 限流打满我一般把 corePoolSize 设置为 2 到 4并根据任务类型调整。第二个问题是异常传播某个子任务失败不应该影响其他子任务我用 exceptionally 把异常包装成 TaskResult由 Ori 在汇总阶段处理然后把失败原因加进最终输出里而不是让整个框架崩掉。5.2 上下文管理与 Token 预算前面提到 token 是上下文的基本单位我把上下文管理这件事彻底量化之后才发现它其实是 Agent 框架最需要精细控制的地方。一次模型调用的 token 消耗由三部分组成系统提示词每条请求都要带上token 固定比如 Ori 的提示词大约 800 到 1200 token。历史消息当前会话所有消息拼接后的 token 数这个会随对话轮数线性增长。工具定义注册的所有工具 Schema 序列化后的 token 数。这个容易被忽略当工具数量到了 20 个以上时光工具定义就可能吃掉几千 token。这意味着JavaManus 每次请求前都要做一次预算计算如果历史消息 工具定义超过了模型 context window 的一定比例就必须触发压缩策略。我常用的阈值是历史消息超过 context window 的一半时开始压缩。5.3 长对话记忆压缩策略压缩策略有两套方案我分别实测过。第一种是截断简单粗暴地只保留最近 N 轮消息但问题在于早期规划信息可能丢失导致 Worker 在执行后半段任务时忘了最初的目标。第二种是摘要式压缩用一个特殊的摘要消息代替一部分旧消息——让大模型把之前的内容总结成几个要点作为一条历史消息保留。实际使用下来摘要式压缩的稳定性明显更好但成本也更高触发压缩本身需要额外的模型调用且摘要过程本身要消耗 token。我的经验是在一次任务循环内尽量使用截断策略——因为同一任务的上下文关联性强截断掉太旧的信息反而能让模型更聚焦跨会话的记忆才值得用摘要。这个判断需要在实测中调整不同模型的摘要能力和上下文敏感度差异很大。6. 端到端示例让 JavaManus 自动完成一份数据分析报告6.1 场景与预期效果理论讲了这么多还是得跑一个真实例子才能验证。我设计了一个场景把一个销售数据 CSV 文件给 JavaManus让它统计各区域销售总额并生成一份 Markdown 格式的分析报告。用户只输入一句话剩下的全靠 Agent 自己规划、执行、汇总。这个任务的难点在于Agent 需要自己决定用哪个工具读取文件、如何解析数据、用什么方式汇总分析、最后生成什么样的报告形式。大模型有可能在第一步就卡住——有的模型会直接说我没有数据分析能力而不会想到去调用 read_file 工具。所以提示词里对工具使用意图的引导非常关键。6.2 实际运行过程拆解实际运行时Ori 的第一轮输出会先给出一个计划大概长这样1. 使用 read_file 工具读取 sales.csv 的内容 2. 分析文件结构识别区域和时间字段 3. 按区域对销售额求和 4. 使用 write_file 工具生成报告文件 5. 输出最终汇总结果计划本身其实不太智能但它已经完成了最关键的事情把模糊的用户意图拆成了明确的工具调用序列。接下来 Worker 接管每一步read_file 把文件内容读出来作为 observation 给回模型模型继续生成下一步行动。我监控了运行日志整个任务耗时约 40 秒模型调用次数是 9 次其中 5 次是带工具调用的4 次是中间文本输出。这个数字在不同模型上差异很大效果好的模型可能 5 次就收敛差一点的模型可能要 15 次以上。6.3 遇到的一个坑CSV 解析丢失列名这个例子运行到第二步时出了一个很有意思的问题模型调用 read_file 成功后返回的内容是 CSV 原文。模型在下一步分析时把区域列名识别成了省份导致后续分组结果完全错误。我修复的方式不是改模型提示词而是改工具给 read_file 增加一个可选参数preview当预览模式下只返回前 20 行数据让模型先看清结构再决定怎么处理。同时我在工具描述里加了一句话如果文件是 CSV/TSV 格式请特别注意第一行是列名。这类贴近真实场景的细节调整远比改提示词有效——因为大模型读取工具描述时的注意力是全量的描述里包含正确的语义提示它更可能做出正确的选择。7. 工程化落地把 Agent 从 Demo 变成可用服务的几个关键问题7.1 重试与超时是第一条性命线Agent 框架的稳定性很大程度上取决于你对待 HTTP 调用的态度。我在早期版本里犯过一个错误直接用同步阻塞式调用大模型接口一旦网络抖动整个 Worker 线程就卡住不动后续任务全部排队。后来我把所有外呼操作改为超时控制连接超时 5 秒读取超时 60 秒大模型接口的响应时间通常很长60 秒是我的实测均值 三倍方差。重试策略第一次失败后等待 1 秒重试第二次失败等待 3 秒重试最多重试 3 次。超过 3 次直接把这个结果标记为失败让上层决策。熔断如果连续 5 次请求都失败暂停该模型通道 30 秒避免雪崩。这个机制在一次真实线上事故里帮我兜住了底当时某个模型服务出问题熔断后框架自动切到备用模型用户无感知。7.2 工具执行的超时与资源限制大模型的工具调用可能产生意想不到的副作用。比如一个 WebSearch 工具底层是个外部 HTTP 请求如果目标网站响应极慢整个 Agent 流程都会卡住。我的处理是给每个工具的执行包一层 CompletableFuture 超时控制CompletableFutureJsonNode future CompletableFuture .supplyAsync(() - tool.run(args), toolExecutor); try { return future.get(30, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); return errorResult(工具执行超时请检查外部服务状态或改用其他方式); }注意超时后要把 future 取消否则工具内部的阻塞操作可能继续占着线程。这种问题排查起来非常隐蔽看似是 Thread.sleep 卡住实际上是取消操作没有传递到子线程。我的经验是工具执行统一使用独立的线程池并且每个工具都要保证响应中断不能写那种无视 interrupted 标志的循环逻辑。7.3 成本控制与可观测性Agent 跑起来是每一句话都在花钱的。我上线早期最焦虑的就是成本失控一个复杂任务可能消耗几十万 token费用几块钱到几十块钱不等。为此我在框架里加了三层控制Token 预算单个任务设置最大 token 消耗超过阈值强制终止。用量日志每次模型调用都记录 usage 里的 prompt_tokens / completion_tokens / total_tokens按会话维度汇总方便审计。并发配额用 Semaphore 控制同时进行的 Agent 任务数量避免多个用户的大任务同时触发把日成本推到危险水位。可观测性方面我强烈建议从第一天就加上 verbose 模式日志把每一步的模型输入输出完整打出来。Agent 的调试难度远超普通程序——你无法单步执行大模型只能靠日志还原它每一步的思考过程。没有好的日志Agent 一旦行为异常你连问题在哪一环都找不到。最后分享一个我个人的心得做 JavaManus 这个框架最大的收获不是我会写 Agent 了而是重新理解了软件工程里的编排思想。Agent 本质上就是一个业务编排系统只不过编排的对象从确定性的 API 变成了概率性的大模型。所以不要急着写代码先把消息模型和工具协议设计清楚这两块地基稳了后面加什么能力都顺。如果你也在用 Java 做 Agent 方向的东西建议从复制一个最小 AgentLoop 开始跑通一次工具调用再逐步加规划、并行、记忆这些能力。那样踩的坑会少很多。