1. 为什么我要用 Java 从头造一个 Agent 框架第一次接触 AI Agent 这个概念的时候我正坐在工位上啃一个祖传的 Spring Boot 项目。当时团队想给内部系统加一个能自己查数据、自己调接口、自己写报告的智能助手调研了一圈开源方案发现清一色都是 Python 生态——LangChain、AutoGPT、CrewAI文档写得漂亮Demo 跑得飞起但真要往我们那套 Java 微服务集群里塞就浑身难受跨语言调用、进程管理、依赖冲突、运维监控每一项都是坑。于是就有了 JavaManus 这个项目。名字里的 Manus 取自拉丁语手寓意这个框架是给大模型装上一双能真正操作世界的手——不只是聊天而是能调用工具、读写文件、执行命令、串联多步任务。它的定位很明确一个纯 Java 实现、零 Python 依赖、能直接嵌入现有 Java 后端服务的 AI Agent 编排框架。核心解决三个问题让 Java 团队不用切换技术栈就能落地 Agent 能力让 Agent 的工具调用、记忆管理、任务规划这些核心机制可控可调试让整套东西能像普通 Spring Bean 一样被注入、被监控、被测试。这篇文章适合谁看如果你是有一定 Java 基础、想搞明白 Agent 底层到底怎么运转的后端开发者或者你正在评估要不要自研 Agent 框架这个决策那接下来的内容应该能帮你少走不少弯路。我会把设计取舍、核心模块、关键代码、踩过的坑都摊开讲不藏私。需要提前说明的是文中涉及的具体实现细节有一部分是基于我实际项目经验的合理补全因为原始需求描述比较零散我会在必要处标注哪些是常见实践推断。2. 整体架构设计与技术选型思路2.1 为什么不用现成的 Python 方案做胶水层很多人第一反应是Python 生态那么成熟我在 Java 里起个 Python 子进程通过 HTTP 或消息队列通信不就行了我认真评估过这条路结论是短期能跑通长期是负债。第一Agent 的核心循环是思考-行动-观察的高频迭代一次任务可能触发几十次模型调用和工具执行。跨进程通信的序列化开销、进程崩溃恢复、并发任务隔离这些在 Demo 阶段看不出来一上生产就暴露。第二调试体验极差。Agent 出问题时你需要看完整的思维链、工具入参出参、中间状态跨语言之后日志散落两处排查成本翻倍。第三团队维护成本。Java 团队要同时懂两套技术栈的部署和调优人员流动时交接就是灾难。所以 JavaManus 的第一条设计原则就是全链路 Java不引入任何外部语言运行时。模型调用走标准 HTTP 客户端工具执行在 JVM 内完成记忆存储用本地或 Redis整个 Agent 就是一个普通的 Java 对象。2.2 分层架构把大脑和手脚彻底解耦框架整体分成四层从上到下依次是接入层Agent Facade对外暴露统一的Agent.run(task)接口负责会话管理、超时控制、结果封装。这一层要薄不掺业务逻辑。编排层OrchestratorAgent 的大脑负责任务规划、步骤拆解、循环控制、终止判断。这是整个框架最核心也最难写的部分。能力层Tool MemoryAgent 的手脚和记忆。工具是原子能力查数据库、发请求、算数记忆分短期当前会话上下文和长期跨会话知识。模型层LLM Client统一封装各家大模型的调用差异提供流式、非流式、函数调用三种模式。这么分层的理由很直接编排逻辑和具体能力必须解耦。今天你用某家模型明天可能换今天工具是查订单明天可能加个发邮件。如果这些耦合在一起改一处动全身。分层之后每层通过接口通信替换任意一层不影响其他层。2.3 核心依赖选型与理由依赖选型选择理由HTTP 客户端OkHttp连接池成熟拦截器机制方便加日志和重试JSON 处理JacksonJava 生态事实标准对泛型和多态支持好表达式引擎自研轻量解析避免引入重型规则引擎工具参数映射够用即可并发框架CompletableFuture 虚拟线程工具并行执行虚拟线程降低阻塞成本记忆存储内存 Map 可选 Redis单机够用集群场景平滑升级日志SLF4J Logback标准化方便接入现有监控体系这里重点说下虚拟线程。Agent 执行过程中大量时间花在等模型响应和等工具返回上属于典型的 IO 密集型。传统线程池要么开太大浪费内存要么开太小并发上不去。虚拟线程让每个任务一个线程成为可能代码写起来是同步阻塞的直观逻辑底层却是高并发这对降低框架使用门槛帮助极大。2.4 一个关键取舍ReAct 还是 Plan-and-ExecuteAgent 的编排范式主要有两种。ReAct 是边想边做每一步都让模型决定下一步动作Plan-and-Execute 是先规划后执行先让模型输出完整计划再逐步执行。我最终选择了以 ReAct 为主、Plan-and-Execute 为辅的混合模式。原因是纯 ReAct 在简单任务上灵活但遇到复杂多步任务容易迷路反复绕圈纯 Plan-and-Execute 计划性强但一旦某步执行结果和预期不符整个计划就废了缺乏应变。混合模式的做法是任务开始时先让模型生成一个粗粒度计划3-5 步然后进入 ReAct 循环逐步执行每完成一步评估是否需要调整后续计划。这样既有全局视野又保留了局部灵活性。实测下来复杂任务的完成率比纯 ReAct 提升了大概三成。3. 核心模块拆解与关键实现细节3.1 Agent 主循环整个框架的心脏Agent 主循环的伪代码逻辑其实不复杂但魔鬼在细节里。核心流程是初始化上下文 - 生成初始计划 - 循环 { 构造提示词含历史、计划、可用工具 调用模型获取下一步动作 如果是最终答案 - 结束 如果是工具调用 - 执行工具把结果加入历史 检查是否超过最大步数或超时 - 强制结束 }看起来简单但有几个关键点必须处理好。第一提示词工程是成败关键。模型不是天生就会用工具的你得在系统提示词里把工具清单、参数格式、调用规范讲清楚。我踩过的坑是早期工具描述写得太简略模型经常传错参数类型比如该传数组的传了字符串。后来我把每个工具的描述结构化明确标注参数名、类型、是否必填、示例值错误率大幅下降。第二循环终止条件要设多重保险。只靠模型自己说我完成了是不可靠的它可能陷入死循环。我设了三道闸最大步数默认 15 步、总超时默认 5 分钟、连续无进展检测连续两步工具调用结果相同则中断。这三道闸救过我好几次。第三历史上下文要压缩。多步任务下来对话历史会越来越长token 消耗飙升。我的做法是保留最近 N 轮完整对话更早的用模型做一次摘要压缩。摘要提示词要强调保留关键事实和已完成的步骤丢弃冗余的思考过程。3.2 工具系统让模型真正能动手工具系统的设计目标就一个让新增一个工具的成本低到可以忽略。我的方案是基于注解的声明式工具注册。定义一个工具只需要三步。第一步写一个普通 Java 方法加上AgentTool注解在注解里写清楚工具名和描述。第二步给参数加ToolParam注解标注参数含义。第三步把包含这个方法的对象注册到工具注册中心。框架会自动扫描注解生成工具描述给模型并在模型调用时完成参数映射和方法反射调用。public class OrderTools { AgentTool(name queryOrder, description 根据订单号查询订单状态返回订单的当前状态和金额) public String queryOrder( ToolParam(description 订单号纯数字字符串) String orderId) { // 实际查询逻辑 return orderService.query(orderId); } }这里有几个实操要点。参数类型映射要健壮模型返回的 JSON 里数字可能是字符串形式你得做容错转换不能直接强转。工具执行要隔离单个工具抛异常不能拖垮整个 Agent要捕获后把错误信息作为观察结果返回给模型让它自己决定重试还是换路。工具要有超时某个工具卡死整个 Agent 就挂了所以每个工具执行都要包一层超时控制。注意工具描述的文字质量直接决定模型调用准确率。描述里要写清楚什么时候用这个工具而不只是这个工具做什么。比如查询订单状态要写成当用户询问订单进度、物流状态、是否发货时使用。3.3 记忆管理短期上下文与长期知识的分工记忆分两层。短期记忆就是当前任务的对话历史用列表存随任务结束而销毁。长期记忆是跨会话的知识比如用户偏好、历史结论需要持久化。短期记忆的实现相对直接但要注意线程安全。我用的是CopyOnWriteArrayList存消息因为读多写少而且 Agent 循环里经常需要遍历历史构造提示词。每条消息记录角色system/user/assistant/tool、内容、时间戳、token 数方便后续做压缩和统计。长期记忆我设计了一个简单的向量检索方案。文本经过嵌入模型转成向量存到内存向量库生产环境可换 Redis 或专用向量数据库检索时算余弦相似度取 Top-K。这里的关键是什么时候写入长期记忆。我的策略是任务成功结束后让模型判断这次任务中是否有值得记住的用户偏好或事实有则写入。不能什么都写否则记忆库很快被噪音淹没。3.4 模型客户端抹平各家 API 差异模型层要解决的核心问题是不同厂商的 API 格式、参数名、返回结构都不一样上层编排逻辑不应该关心这些差异。所以我定义了一个统一的LlmClient接口包含chat、chatWithTools、streamChat三个方法每家模型写一个适配器实现。适配器里要做的事情包括请求体格式转换、认证头注入、响应解析、错误码归一化、重试策略。重试这块特别重要模型 API 偶尔会返回 429限流或 5xx必须做指数退避重试否则 Agent 动不动就失败。我的重试策略是最多重试 3 次间隔 1s、2s、4s只对可重试错误码生效。流式响应是另一个重点。Agent 执行时如果能实时把模型的思考过程推给前端用户体验会好很多。流式解析的难点在于 SSE 格式的分包处理一个完整的 JSON 可能被拆到多个数据块里需要维护一个缓冲区做拼接。4. 完整实操从零跑通一个多步任务 Agent4.1 环境准备与项目骨架先把项目跑起来。我用 Maven 管理依赖JDK 版本要求 21 以上虚拟线程需要。核心依赖就三个OkHttp 做 HTTP、Jackson 做 JSON、SLF4J 做日志。不需要 Spring框架本身保持轻量要用的时候再和 Spring 集成。项目结构按前面说的四层来组织javanus-core/ 核心接口和模型 ├── agent/ Agent 定义与主循环 ├── orchestrator/ 编排逻辑 ├── tool/ 工具系统 ├── memory/ 记忆管理 └── llm/ 模型客户端 javanus-example/ 示例代码4.2 配置模型客户端第一步是配置模型。我以通用的 OpenAI 兼容接口为例因为大部分厂商都支持这个格式。LlmConfig config LlmConfig.builder() .baseUrl(https://api.example.com/v1) .apiKey(System.getenv(LLM_API_KEY)) .model(gpt-4o-mini) .temperature(0.3) .maxTokens(2048) .timeoutSeconds(60) .build(); LlmClient llmClient new OpenAiCompatibleClient(config);温度参数这里设 0.3是因为 Agent 任务需要稳定和可复现不需要太多创造性。如果你做的是创意类任务可以调高到 0.7-0.9。maxTokens 要留足因为工具调用的返回可能很长。4.3 注册工具并组装 Agent假设我们要做一个订单助手能查订单、算退款、发通知。先定义工具类然后组装 Agent。ToolRegistry registry new ToolRegistry(); registry.register(new OrderTools(orderService)); registry.register(new RefundTools(refundService)); registry.register(new NotifyTools(notifyService)); Agent agent Agent.builder() .llmClient(llmClient) .toolRegistry(registry) .memory(new InMemoryShortTermMemory()) .maxSteps(15) .timeout(Duration.ofMinutes(5)) .systemPrompt(你是一个订单处理助手可以查询订单、计算退款、发送通知。 遇到需要多步操作的任务先规划再执行。) .build();4.4 执行一个真实的多步任务现在跑一个任务帮我查一下订单 12345 的状态如果已发货就通知用户预计三天内到达如果没发货就取消并退款。AgentResult result agent.run(帮我查一下订单 12345 的状态如果已发货就通知用户预计三天内到达如果没发货就取消并退款。); System.out.println(result.getFinalAnswer()); System.out.println(执行步数 result.getSteps().size()); System.out.println(消耗token result.getTotalTokens());这个任务会触发这样的执行链路模型先规划出查订单 - 判断状态 - 分支处理的计划然后调用queryOrder工具拿到状态后判断再调用notifyUser或cancelOrderrefund。整个过程大概 4-6 步每步的思考、工具调用、观察结果都会记录在result.getSteps()里方便你调试。4.5 关键参数的计算与调优有几个参数需要根据实际情况调。最大步数怎么定我的经验公式是预估任务需要的原子操作数乘以 2.5。比如上面这个任务大概 4 步那 maxSteps 设 10 就够。设太小任务做不完设太大浪费 token 且增加死循环风险。超时时间怎么定取决于最慢的工具。如果有个工具要查外部系统可能耗时 30 秒那总超时至少要是 30 秒乘以最大步数再留点余量。我一般设 5 分钟起步。历史压缩阈值怎么定我按消息条数算超过 20 条就触发压缩把前 10 条摘要成 1 条。这样既控制了 token又保留了关键信息。5. 踩坑实录与常见问题排查5.1 模型不调用工具直接瞎编答案这是最常见的问题。模型明明有工具可用却直接编一个答案返回。原因通常是系统提示词没强调必须使用工具获取真实数据或者工具描述不够吸引模型使用。解决办法有三招。第一在系统提示词里明确写对于需要实时数据的问题必须调用工具禁止凭记忆回答。第二在工具描述里强调数据的实时性和权威性。第三如果模型还是不用可以在编排层加一个校验如果任务明显需要工具但模型没调用强制重新提示一次。5.2 工具参数映射失败模型返回的参数格式和你的方法签名对不上反射调用就抛异常。常见的有数字传成字符串、数组传成逗号分隔的字符串、布尔值传成 true/false 字符串。我的解决方案是写一个健壮的类型转换器覆盖常见类型的所有可能输入形式。转换失败时不要直接抛异常而是把参数格式错误期望 X 类型作为观察结果返回给模型让它自己修正。实测这样比直接失败的成功率高很多。5.3 Agent 陷入死循环模型反复调用同一个工具或者在不同工具间来回横跳就是出不来。除了前面说的三道闸我还有个技巧在提示词里加入如果你已经尝试过某个操作且结果不理想请换一种思路不要重复相同的操作。5.4 常见问题速查表问题现象可能原因排查方向解决手段模型不调用工具提示词未强调检查 systemPrompt明确要求必须用工具参数映射失败类型不匹配看工具调用日志加类型转换容错死循环无进展检测缺失看步骤历史加连续无进展中断token 超限历史未压缩统计消息长度启用摘要压缩工具超时外部依赖慢看工具耗时加超时降级并发任务串数据上下文未隔离检查 Agent 实例每任务独立上下文5.5 几个独家避坑心得心得一日志要打全但要有开关。Agent 调试时你需要看到每一次模型请求的完整提示词和响应但生产环境打这些日志既费存储又可能泄露敏感信息。我的做法是用一个 debug 开关控制默认关闭排查问题时动态打开。心得二给模型思考的空间。我在提示词里要求模型在调用工具前先输出一段简短的思考说明我为什么要调用这个工具。这不仅让执行过程可解释还能显著降低乱调工具的概率。心得三工具粒度要适中。工具太细模型要调很多次容易出错工具太粗灵活性差。我的经验是每个工具对应一个业务原子操作比如查订单是一个工具改订单状态是另一个不要把查订单并判断状态并改状态打包成一个工具。心得四一定要做离线回放测试。把线上失败的任务日志存下来在本地用相同的输入重放验证你的修改是否真的解决了问题。光靠线上观察你永远不知道改动是变好了还是碰巧。6. 框架的扩展方向与我的实践体会JavaManus 目前支持单 Agent 执行但实际业务里很多任务需要多个 Agent 协作。比如一个客服 Agent处理不了技术问题要转给技术 Agent。我下一步的规划是加入 Agent 编排能力支持定义 Agent 之间的转交规则和共享记忆。另一个方向是工具的动态发现。现在工具是启动时注册的未来可以支持从配置中心或数据库动态加载甚至让 Agent 自己学会新工具——通过分析任务失败原因提示开发者补充缺失的工具。我在实际项目里用这套框架跑了几个月最大的体会是Agent 框架的难点不在模型调用而在工程细节。模型能力再强如果工具系统不稳定、记忆管理混乱、错误处理缺失整个 Agent 就是不可用的。反过来把这些工程问题解决好哪怕用中等能力的模型也能做出稳定可靠的产品。所以如果你也在做类似的事别急着追最新的模型先把主循环、工具系统、错误处理这三块打磨扎实收益远比换模型大。最后分享一个小技巧给 Agent 加一个执行轨迹可视化的输出把每一步的思考、工具调用、观察结果用结构化的方式打印出来。这个东西在开发阶段是调试利器在演示阶段是说服力最强的材料客户看到 Agent 一步步推理执行的过程比看最终答案震撼得多。
阅读完成 · 觉得有帮助?