1. 为什么我要用 Java 从头造一个 AI Agent 框架1.1 一个真实的需求场景去年下半年我所在的技术团队接到一个内部需求把日常运维中大量重复的“查日志、比对配置、生成巡检报告”流程自动化。市面上的 AI Agent 框架不少但绝大多数是 Python 生态的而我们的核心业务系统、内部工具链、权限体系全部是 Java 写的。如果为了跑一个 Agent 单独维护一套 Python 服务运维成本、人员技能栈、发布流程都要翻倍。于是我做了一个决定用 Java 从零构建一个轻量级的 AI Agent 框架内部代号 JavaManus。它的目标很明确——让 Java 开发者能用自己熟悉的语言和工程习惯快速搭建出具备“思考-行动-观察-再思考”循环能力的智能体而不是被 Python 生态绑定。这篇文章不是一篇“框架使用手册”而是我在设计和实现过程中对架构选型、核心模块、踩坑经验的完整复盘。如果你也是 Java 后端想在自己的系统里嵌入 Agent 能力或者单纯好奇“Java 到底能不能做好这件事”那这篇内容应该能给你一些直接可用的参考。1.2 JavaManus 到底解决了什么问题先说清楚定位。JavaManus 不是一个“大而全”的平台它更像是一个可嵌入的 Agent 运行时内核。你可以把它理解成一个“决策循环引擎”给它一个目标它会自动拆解任务、调用你注册的工具、根据工具返回结果决定下一步直到任务完成或达到终止条件。它解决的核心痛点有三个语言鸿沟Java 团队不需要引入 Python 运行时直接用 Maven 依赖就能接入。工具集成成本企业内部大量 Java 方法、HTTP 接口、数据库查询可以通过注解或配置快速注册为 Agent 可调用的工具。可控性每一步决策、每一次工具调用都有完整的日志和状态快照方便排查“它为什么这么做”。适合谁来参考我认为是三类人一是 Java 后端工程师想在现有系统里加一点“智能决策”能力二是架构师在评估自研 Agent 框架的可行性三是对 Agent 原理感兴趣、但不想被 Python 框架细节淹没的学习者。2. 整体架构设计与核心思路拆解2.1 为什么选择“循环工具”而不是“大模型直出”很多人第一次接触 Agent会觉得“不就是让大模型多轮对话吗”。但真正落地时会发现纯对话式的大模型有两个致命问题一是它无法主动获取实时信息二是它无法执行有副作用的操作比如重启服务、写数据库。JavaManus 的核心设计思想是ReAct 模式的 Java 化实现Reason推理 Act行动。每一轮循环里Agent 先根据当前上下文和可用工具列表生成一个“思考”和一个“行动指令”行动指令被解析后由框架调用对应的 Java 工具工具返回的“观察结果”再被拼回上下文进入下一轮。这个循环看起来简单但工程上有大量细节如何解析大模型输出的行动指令如何保证工具调用的参数类型安全如何防止无限循环这些才是框架真正的价值所在。2.2 分层架构把“智能”和“工程”分开我把 JavaManus 分成四层每层职责单一方便替换和测试层级职责关键类/接口接入层接收用户目标管理会话生命周期AgentSession、AgentRequest决策层调用大模型解析思考与行动Planner、ActionParser执行层工具注册、参数绑定、调用与结果封装ToolRegistry、ToolInvoker观测层日志、状态快照、循环控制AgentContext、LoopGuard这样分层的最大好处是决策层可以换不同的大模型供应商执行层可以换不同的工具类型互不影响。我在实际项目中就遇到过“上午用 A 模型下午因为限流换成 B 模型”的情况只改了一个配置类其他代码零改动。2.3 核心数据结构AgentContext 的设计取舍AgentContext 是整个框架的“记忆体”它需要保存用户原始目标、每一轮的思考、行动、观察结果、当前轮次、已调用工具历史。我最初的设计是用一个 List 存所有消息简单直接。但很快发现两个问题一是上下文会无限增长最终超出模型窗口二是排查问题时很难快速定位“第几轮发生了什么”。后来改成分轮次的结构化存储每一轮是一个 RoundRecord包含 thought、action、observation 三个字段。这样既方便做窗口截断只保留最近 N 轮也方便在日志里按轮次打印。这个改动看起来小但对调试体验的提升是巨大的。提示上下文管理是 Agent 框架最容易忽视的部分。不要等到 token 超限才想起来做截断最好在设计初期就把“轮次”作为一等公民。3. 核心模块的详细实现与实操要点3.1 工具注册机制注解驱动的设计工具是 Agent 的“手脚”。在 Java 里最自然的注册方式就是注解。我定义了一个AgentTool注解可以标注在方法上AgentTool(name queryLog, description 根据服务名和时间范围查询日志) public String queryLog( ToolParam(serviceName) String serviceName, ToolParam(startTime) String startTime) { // 实际查询逻辑 return logService.query(serviceName, startTime); }框架启动时扫描所有带AgentTool的方法把方法名、描述、参数信息注册到 ToolRegistry。描述信息非常关键——它会被拼进给大模型的提示词里模型靠这个描述来决定“什么时候该调用哪个工具”。这里有个实操心得工具描述要写得像给新人看的 API 文档。我试过把描述写成“查询日志”模型经常在不需要的时候乱调改成“根据服务名和起止时间查询指定服务的运行日志返回最近 100 条”调用准确率明显提升。3.2 行动指令解析如何让模型输出可执行的 Java 调用这是整个框架最“脆”的环节。大模型输出的是自然语言我需要从中提取出结构化的工具名和参数。我的做法是在提示词里强制约定输出格式用类似 JSON 的结构{ thought: 我需要先查询订单服务的日志, action: queryLog, actionInput: { serviceName: order-service, startTime: 2024-01-01 00:00:00 } }然后在 Java 侧用 Jackson 解析。但模型并不总是听话有时候会多输出一段解释有时候 JSON 格式有细微错误。我的应对策略是三层防护提示词约束明确说“只输出 JSON不要有其他内容”。容错解析先用正则提取第一个{到最后一个}之间的内容再尝试解析。失败重试如果解析失败把错误信息作为“观察结果”喂回模型让它重新生成。实测下来加了第三层之后解析失败导致的循环中断减少了 90% 以上。3.3 参数类型绑定从 JSON 到 Java 方法参数解析出 actionInput 后需要把它绑定到具体 Java 方法的参数上。这里我用的是反射 类型转换。难点在于 JSON 里的数字可能是 Integer但方法参数可能是 Long 或 Double。我的处理方式是读取方法参数的ToolParam注解拿到参数名从 JSON 里取值然后根据参数声明的类型做转换。对于复杂对象直接用 Jackson 的convertValue方法。这个过程必须做严格的异常捕获因为一旦类型不匹配整个循环就会崩。注意不要让工具方法抛出未捕获的异常。框架会把异常信息作为观察结果返回给模型这其实是好事——模型看到“参数类型错误”后下一轮往往会自己修正。3.4 循环控制防止 Agent “鬼打墙”Agent 最常见的失控场景是反复调用同一个工具得到同样的结果却一直不结束。我在 LoopGuard 里设了三道闸最大轮次限制默认 10 轮超过就强制终止并返回当前结果。重复行动检测如果连续两轮的 action 和 actionInput 完全一致直接判定为死循环终止。无进展检测如果连续三轮的 observation 内容高度相似用简单的编辑距离判断也终止。这三道闸不是拍脑袋定的是我在实际跑任务时一次次“救火”总结出来的。尤其是第二道能拦住大部分因为提示词歧义导致的重复调用。4. 完整实操流程从零跑通一个巡检 Agent4.1 环境准备与依赖引入假设你有一个标准的 Maven 项目引入 JavaManus 的核心依赖这里用虚构的 groupId 示意dependency groupIdcom.example.agent/groupId artifactIdjavamanus-core/artifactId version1.0.0/version /dependency然后需要一个能调用大模型的客户端。JavaManus 把模型调用抽象成LlmClient接口你只需要实现一个chat(ListMessage)方法。我用的是 HTTP 方式调用兼容 OpenAI 格式的接口核心代码大概三十行用 Java 11 的 HttpClient 就够了。4.2 定义你的第一个工具集假设我们要做一个“服务巡检 Agent”先定义两个工具Component public class InspectionTools { AgentTool(name getServiceStatus, description 获取指定服务的当前运行状态返回 UP 或 DOWN) public String getServiceStatus(ToolParam(serviceName) String serviceName) { return monitorClient.getStatus(serviceName); } AgentTool(name getErrorCount, description 获取指定服务最近一小时的错误日志数量) public int getErrorCount(ToolParam(serviceName) String serviceName) { return logClient.countErrors(serviceName, 1); } }这两个工具都很简单但组合起来就能让 Agent 完成“检查服务是否正常如果不正常再看错误数”这样的逻辑。4.3 启动 Agent 并观察执行过程启动代码非常简洁AgentSession session AgentSession.builder() .llmClient(new HttpLlmClient(apiKey, endpoint)) .toolRegistry(toolRegistry) .maxRounds(8) .build(); AgentResult result session.run(请检查 order-service 和 user-service 的运行状态如果有服务异常报告其错误日志数量); System.out.println(result.getFinalAnswer());跑起来之后控制台会按轮次打印日志。我截取一段真实的执行记录服务名已脱敏[Round 1] Thought: 需要先检查两个服务的状态 Action: getServiceStatus(order-service) Observation: UP [Round 2] Thought: order-service 正常继续检查 user-service Action: getServiceStatus(user-service) Observation: DOWN [Round 3] Thought: user-service 异常需要查询错误数 Action: getErrorCount(user-service) Observation: 47 [Round 4] Thought: 已获得所有信息可以总结 Final Answer: order-service 运行正常user-service 状态为 DOWN最近一小时错误日志 47 条建议立即排查。整个过程 4 轮结束没有多余调用。这个例子虽然简单但完整展示了 Agent 的核心价值根据中间结果动态决定下一步而不是执行固定的脚本。4.4 参数计算与阈值设定在更复杂的场景里工具可能需要数值参数。比如“查询最近 N 分钟的错误日志”N 应该由模型决定还是框架固定我的经验是能固定就固定必须动态才交给模型。因为模型对数值的推理并不稳定让它算“最近两小时等于多少分钟”经常出错。如果确实需要动态计算我会在工具内部做转换比如参数传“2h”“30m”这样的字符串由 Java 代码解析成分钟数。这样模型只需要做它擅长的语义理解数值计算交给确定性代码。5. 常见问题与排查技巧实录5.1 模型不按格式输出怎么办这是最高频的问题。我的排查顺序是先看提示词里有没有明确说“只输出 JSON”。再看模型返回的原始内容是不是被 Markdown 代码块包裹了比如json ...。如果是解析前先剥掉代码块标记。如果还是不行降低 temperature 参数通常 0.1 以下格式稳定性会好很多。我整理了一个速查表现象可能原因解决方式输出多余解释文字提示词约束不够强在提示词末尾加“不要输出任何解释”JSON 被代码块包裹模型习惯性行为解析前用正则剥离 标记参数名拼写错误模型幻觉在提示词里列出所有工具的参数名反复调用同一工具观察结果没被正确拼回上下文检查上下文拼接逻辑确保 observation 非空5.2 工具调用超时或异常如何处理工具方法可能因为网络、数据库等原因超时。我的做法是给每个工具调用包一层超时控制默认 10 秒。超时后不抛异常而是返回一个字符串“工具调用超时请稍后重试或换一种方式”。这样模型有机会调整策略而不是整个 Agent 崩溃。注意不要让工具异常直接冒泡到框架顶层。Agent 的健壮性很大程度上取决于“它能不能从失败中恢复”。5.3 上下文太长导致模型“失忆”当轮次多了之后早期的重要信息可能被挤出窗口。我的解决方案是关键信息摘要在每一轮结束时如果 observation 超过一定长度就截断并保留前 200 字加“...”。同时在上下文最前面始终保留用户原始目标确保模型不跑偏。另一个技巧是把工具列表放在系统提示词里而不是每轮都拼进去。这样能省下不少 token。5.4 独家避坑不要在工具里做“智能”判断我踩过最大的坑是在工具方法里写了一些“如果参数为空就默认查所有服务”的逻辑。结果模型发现传空参数也能工作就再也不传具体服务名了导致每次都是全量查询性能极差。教训是工具应该严格、笨拙、确定性。智能判断留给模型工具只做精确执行。参数缺失就报错让模型自己学会传对参数。6. 框架扩展与后续演进方向6.1 支持多 Agent 协作单 Agent 适合线性任务但复杂场景需要多个 Agent 分工。我在 JavaManus 上做了一个简单扩展允许把一个 Agent 注册为另一个 Agent 的工具。这样“巡检 Agent”可以调用“日志分析 Agent”形成层级结构。实现上并不复杂只需要把 AgentSession 包装成一个带AgentTool的方法。但要注意控制递归深度否则可能出现 A 调用 B、B 又调用 A 的死循环。6.2 加入短期记忆与长期记忆目前 AgentContext 只保存单次会话的记忆。如果要跨会话记住“上次巡检发现 user-service 有问题”就需要引入持久化。我的做法是用一个简单的 KV 存储把每次任务的最终结论按服务名索引存下来下次任务开始时作为背景信息注入。这个改动让 Agent 从“一次性工具”变成了“有经验的助手”。实测在重复巡检场景下模型能更快定位到历史问题服务。6.3 可观测性把黑盒变成白盒Agent 最大的信任障碍是“不知道它为什么这么做”。我在框架里加了一个简单的 Trace 机制每一轮的 thought、action、observation、耗时都记录到结构化日志里可以导出成 JSON 供前端展示。这个功能在内部推广时帮了大忙。当运维同学看到 Agent 的完整思考链后对它的信任度明显提升也更愿意反馈“哪一步判断错了”。7. 一些个人体会用 Java 做 AI Agent 框架最大的挑战不是语言本身而是工程思维和模型思维的切换。Java 开发者习惯确定性、强类型、可预测而大模型是概率性的、模糊的、需要容错的。JavaManus 的设计过程本质上是在这两者之间找平衡点。我个人的经验是把模型当成一个“聪明但不可靠的实习生”。给它清晰的指令、明确的工具、严格的边界同时准备好它犯错时的兜底方案。框架的价值不在于让模型变得完美而在于让模型的不完美变得可管理。最后分享一个小技巧在调试阶段把每一轮的完整提示词和模型原始返回都打印出来。虽然日志会很长但当你看到模型“为什么突然跑偏”时答案往往就藏在某一句提示词的措辞里。这个习惯帮我省下了大量猜测时间。