1. 为什么我要用 Java 从头造一个 AI Agent 框架1.1 一个被忽视的现实Agent 生态几乎被 Python 垄断过去一年多我一直在做企业级后端系统的开发主要技术栈就是 Java。大模型火起来之后我陆陆续续试过不少 Agent 相关的开源项目结果发现一个很尴尬的现象几乎所有拿得出手的 Agent 框架清一色都是 Python 写的。想在自己的 Java 服务里嵌一个能自主规划、能调用工具、能多轮反思的智能体要么得单独起一个 Python 服务做进程间通信要么就得忍受各种不成熟的 Java 移植版。单独起 Python 服务这件事听起来简单实际落地的时候问题一大堆。运维要维护两套运行时环境团队里写 Java 的同事看不懂 Python 那套异步逻辑出了问题排查链路特别长。更麻烦的是企业里很多核心业务逻辑、权限体系、数据访问层全都是 Java 写的Agent 想调用这些能力就得跨语言来回序列化性能和可维护性都很难看。所以我就动了念头能不能用纯 Java 从零搭一个 Agent 框架把规划、工具调用、记忆、反思这些核心能力都实现出来让它能直接嵌进现有的 Java 服务体系里这个念头最后变成了一个我自己维护的项目我给它起名叫 JavaManus。这篇文章就是把这个框架的设计思路、核心实现和踩过的坑完整地摊开讲一遍。1.2 JavaManus 到底能做什么适合谁看先把定位说清楚。JavaManus 是一个用纯 Java 实现的 AI Agent 运行时框架它的核心能力包括四块任务规划、工具调用、上下文记忆、结果反思。你可以把它理解成一个“智能体的操作系统内核”——它不负责训练模型也不绑定某一家的大模型服务而是负责把大模型的推理能力和外部工具、业务系统连接起来让模型能真正“动手做事”而不只是聊天。它解决的问题很具体让 Java 团队不用引入 Python 依赖就能在自己的服务里跑起一个能自主完成多步任务的智能体。比如你给它一个目标“帮我查一下这个季度销售额下滑的原因”它能自己拆解成“查数据库”“对比历史数据”“生成分析结论”几个步骤然后一步步调用你注册好的工具去执行。适合看这篇文章的人我大致分三类。第一类是有 Java 基础、想了解 Agent 底层原理的后端开发者你不需要有 AI 背景我会把大模型交互的部分讲透。第二类是想把大模型能力接入现有 Java 系统的架构师文章里的工具注册、上下文管理这些设计你可以直接参考。第三类是对 Agent 框架本身好奇的技术爱好者哪怕你平时不写 Java看看一个 Agent 框架需要哪些模块、每个模块怎么协作也是有价值的。2. 整体架构设计一个 Agent 框架到底需要哪些模块2.1 从“聊天机器人”到“智能体”的关键跨越很多人对 Agent 的理解还停留在“能对话的机器人”这个层面这其实是个误区。普通的对话系统是“你问一句它答一句”整个过程是无状态的、单轮的。而 Agent 的核心区别在于它有“目标感”和“行动力”——你给它一个目标它会自己决定要分几步走、每步用什么工具、拿到结果之后要不要调整策略。这个跨越带来的直接后果就是框架的设计复杂度上了一个台阶。对话系统只需要管好“输入拼接”和“输出解析”两件事而 Agent 框架至少要管好四件事怎么让模型规划任务、怎么让模型调用工具、怎么在多轮交互中维护上下文、怎么判断任务是否完成并反思。这四件事对应到代码里就是四个核心模块。我在设计 JavaManus 的时候第一件事就是把这四个模块的边界划清楚。划不清楚的后果我见过太多——很多玩具级 Agent 项目把所有逻辑塞在一个大方法里模型输出解析、工具调用、状态更新全混在一起跑简单任务还行一旦任务步数多了状态就乱套调试起来基本靠打印日志大海捞针。2.2 四大核心模块的职责划分我把 JavaManus 拆成了四个核心模块每个模块只干一件事模块之间通过明确定义的数据结构通信。第一个是规划器Planner。它的职责是接收用户目标结合当前可用的工具列表让大模型生成一个可执行的任务计划。这个计划不是死的而是一个可以动态调整的步骤序列。规划器的输出是一个结构化的计划对象里面包含每一步的描述、预期使用的工具、以及步骤之间的依赖关系。第二个是执行器Executor。它负责按计划逐步执行每一步要么是调用某个工具要么是让模型做一次推理。执行器最关键的设计是“单步可中断”——每一步执行完都要把结果回传给规划器让规划器判断是否需要调整后续计划。这个设计让 Agent 具备了应对意外的能力而不是一条道走到黑。第三个是记忆模块Memory。它维护整个任务执行过程中的上下文包括用户原始目标、每一步的执行结果、模型的中间推理。记忆模块要解决的核心问题是“上下文窗口有限”所以它不能简单地把所有历史都塞给模型而要做摘要和裁剪。第四个是工具注册中心Tool Registry。所有 Agent 能调用的能力都要在这里注册包括工具的名称、描述、参数 schema 和实际执行逻辑。工具描述的质量直接决定了模型能不能正确选择工具这块后面我会专门讲。2.3 为什么选择“规划-执行-反思”这个循环Agent 的执行模式有好几种流派。最简单的是 ReAct 模式就是“推理一步、行动一步”交替进行不预先做完整规划。复杂一点的是先做完整规划再执行。我最后选的是“规划-执行-反思”的混合循环理由是纯 ReAct 模式在长任务上容易迷失方向而纯规划模式又不够灵活。具体来说JavaManus 的执行流程是这样的先让规划器生成一个初步计划然后执行器执行第一步执行完把结果交给反思模块反思模块判断“这一步是否达成了预期”“是否需要修改后续计划”。如果需要修改就回到规划器重新规划剩余步骤。这个循环一直持续到任务完成或者达到最大步数限制。这个设计的好处是兼顾了方向感和灵活性。初步计划给了 Agent 一个大方向不至于在长任务里跑偏而每步之后的反思又让它能根据实际情况调整不会因为初始计划不合理就彻底卡死。实测下来这种模式在需要多步推理的任务上成功率比纯 ReAct 模式高不少。3. 核心模块的深度实现细节3.1 规划器怎么让模型输出可解析的计划规划器是整个框架里最考验 prompt 工程的地方。核心难点在于大模型天然输出的是自然语言而程序需要的是结构化数据。你直接让模型“输出一个计划”它可能给你一段散文也可能给你一个格式五花八门的列表解析起来非常痛苦。我的做法是强制模型输出 JSON 格式的计划并且在 prompt 里给出严格的 schema 定义。计划对象的 schema 大致是这样一个 steps 数组每个 step 包含 id、description、toolName、toolInput、dependsOn 几个字段。为了让模型稳定输出这个格式我在 prompt 里做了三件事给出完整的字段说明、给出一个填充好的示例、明确要求“只输出 JSON不要有任何额外文字”。但即便这样模型偶尔还是会输出带 markdown 代码块包裹的 JSON或者在某些字段里塞进多余的解释。所以解析层必须做容错。我写了一个健壮的 JSON 提取器它会先尝试直接解析失败的话就用正则把第一个完整的 JSON 对象抠出来再解析再失败就触发一次“修复重试”——把模型的错误输出和解析报错一起发回去让它重新生成。这里有个经验值得分享schema 里的字段名不要太抽象。我一开始用action和params这种字段名模型经常搞混。后来改成toolName和toolInput语义更明确模型的输出准确率明显提升。字段命名这件事看着小实际影响很大。3.2 工具注册中心描述质量决定调用准确率工具注册中心的设计看起来简单其实藏着很多门道。每个工具需要注册的信息包括唯一名称、自然语言描述、参数 schema、执行函数。其中自然语言描述的质量直接决定了模型能不能在正确的时机选中这个工具。我踩过一个典型的坑。早期我注册了一个叫queryData的工具描述写的是“查询数据”。结果模型经常在该用别的工具的时候误选它因为它描述太模糊了模型根本不知道它查的是什么数据、什么场景下该用。后来我把描述改成“根据指定的时间范围和指标名称从销售数据库中查询对应的数值适用于需要获取具体业务数据的场景”误选率立刻降下来了。参数 schema 我用的是 JSON Schema 标准每个参数都要写清楚类型、描述、是否必填。这里有个细节枚举类型的参数一定要把可选值列全。比如一个“时间粒度”参数可选值是 day、week、month如果你不列全模型可能给你编一个“quarterly”出来然后执行的时候直接报错。工具执行函数我设计成同步和异步两种都支持。同步的适合快速返回的本地计算异步的适合调用外部接口。执行结果统一包装成一个 ToolResult 对象包含成功标志、结果内容、错误信息三个字段。这样执行器拿到结果之后不用关心工具内部是怎么实现的只看这个统一结构就行。3.3 记忆模块上下文窗口的取舍艺术记忆模块要解决的核心矛盾是Agent 执行长任务时产生的上下文会越来越长但大模型的上下文窗口是有限的。你不能把所有历史都塞进去那样要么超限报错要么成本高得离谱。我的策略是分层记忆。第一层是原始目标这个永远保留因为它定义了任务的方向。第二层是最近 N 步的详细记录N 我一般设成 5这几步的完整输入输出都保留因为模型做下一步决策时最依赖近期上下文。第三层是更早步骤的摘要超过 N 步的历史会被压缩成简短摘要只保留关键结论。摘要的生成也是让模型来做的。我会把要压缩的几步历史发给模型让它用一两句话总结“这几步做了什么、得到了什么关键结果”。这里要注意摘要 prompt 里要强调“只保留对后续决策有用的信息”否则模型会把一些无关紧要的细节也塞进摘要里压缩效果大打折扣。还有一个容易被忽视的点记忆模块要区分“事实”和“推理”。工具返回的结果是事实模型的中间思考是推理。事实必须准确保留推理可以适当压缩。我在数据结构上把这两类内容分开存储摘要的时候只压缩推理部分事实部分原样保留。这个区分让 Agent 在长任务里的表现稳定了很多。3.4 反思模块让 Agent 知道自己做错了反思模块是我认为整个框架里最有价值、也最容易被做砸的部分。它的作用是每执行完一步判断这一步的结果是否符合预期以及是否需要调整后续计划。反思的 prompt 设计很关键。我一开始让模型“判断这一步是否成功”结果模型几乎永远说“成功”哪怕工具明明返回了错误。后来我改了策略不问“是否成功”而是问三个具体问题这一步的预期结果是什么实际结果是什么两者是否一致如果不一致可能的原因是什么把一个大判断拆成几个小问题模型的判断质量明显提升。反思的结论会反馈给规划器。如果反思认为“这一步失败了且原因明确”规划器会重新规划剩余步骤可能换一个工具也可能调整参数。如果反思认为“这一步成功了但发现了新信息”规划器也会相应调整后续计划。这个反馈闭环是 Agent 能应对复杂任务的关键。这里有个坑要提醒反思不能太频繁也不能太稀疏。每一步都反思成本高且容易过度调整几步才反思一次又可能错过及时纠错的机会。我的经验是对于工具调用步骤每步都反思对于纯推理步骤可以两三步合并反思一次。4. 完整实操从零跑通一个多步任务4.1 环境准备与依赖选型先把环境说清楚。JavaManus 我用的 JDK 17主要是看中了 record 类型和 sealed class写数据模型特别清爽。构建工具用的 Maven依赖管理直观。核心依赖其实很少一个 HTTP 客户端用来调大模型接口一个 JSON 库用来做序列化就这两样是必须的。HTTP 客户端我选的是 JDK 自带的 HttpClient没引入第三方库。理由是它支持异步、支持连接池功能够用而且少一个依赖就少一份维护成本。JSON 库我用的是 Jackson主要是生态成熟、注解支持好序列化和反序列化都很方便。大模型接口这块我设计了一个抽象的 ModelClient 接口把“发消息、收回复”这个动作抽象出来。这样具体接哪家模型只需要实现这个接口就行框架核心代码完全不用改。这个抽象层是必须的因为模型服务更新换代太快绑定死某一家后面会很被动。4.2 定义第一个工具让 Agent 能查数据光说不练没意思我带你走一遍完整流程。假设我们要做一个“销售数据分析 Agent”第一步得给它注册一个查数据的工具。工具类的结构是这样的实现一个 Tool 接口接口里定义 getName、getDescription、getParameterSchema、execute 四个方法。getName 返回工具唯一标识getDescription 返回自然语言描述getParameterSchema 返回 JSON Schema 字符串execute 接收参数 Map 返回 ToolResult。查数据工具的描述我这么写“根据指标名称和时间范围查询销售数据返回对应数值。适用于需要获取具体销售数字的场景。”参数 schema 里定义两个参数metric字符串必填描述为“指标名称如销售额、订单量”、timeRange字符串必填描述为“时间范围格式为 YYYY-MM 到 YYYY-MM”。execute 方法里就是实际的查询逻辑我这里用模拟数据代替真实数据库查询。返回的 ToolResult 里成功的话把数值放进 result 字段失败的话把错误原因放进 error 字段。这个工具注册到注册中心之后Agent 就能在规划时“看到”它了。4.3 跑通一个完整任务分析销售额下滑原因现在给 Agent 一个真实目标“分析 2024 年 3 月销售额相比 2 月下滑的原因”。我们看看它是怎么一步步执行的。第一步规划器拿到目标和工具列表生成初步计划。计划大概是先查 2 月销售额再查 3 月销售额然后计算环比变化最后生成分析结论。注意这里规划器会自动把“对比”拆成“分别查询再计算”因为工具列表里只有查询工具没有现成的对比工具。第二步执行器执行“查 2 月销售额”调用查数据工具参数是 metric销售额、timeRange2024-02。工具返回一个数值比如 120 万。执行结果进入记忆模块。第三步反思模块判断预期是拿到 2 月销售额实际拿到了 120 万一致这一步成功。继续执行下一步。第四步执行“查 3 月销售额”返回 95 万。反思判断成功。第五步执行“计算环比变化”这一步没有对应工具所以是纯推理步骤模型自己算出下滑了约 20.8%。第六步执行“生成分析结论”模型结合前面的数据输出一段分析。任务完成。整个过程里如果某一步工具返回了错误比如时间范围格式不对反思模块会识别出来规划器会调整参数重试。这就是完整闭环的价值。4.4 关键参数配置与调优记录跑通之后有几个参数值得单独说。第一个是最大步数限制我默认设成 15。设太小复杂任务跑不完设太大万一 Agent 陷入死循环会一直烧 token。15 这个值是我在几十个测试任务上试出来的平衡点。第二个是单步超时时间默认 30 秒。工具调用如果超过这个时间还没返回就判定为超时失败触发反思和重规划。这个参数要根据你实际工具的执行时间来调如果工具有慢查询就得适当放宽。第三个是反思触发阈值。我前面说工具调用每步都反思但实际实现里我加了一个判断如果工具返回成功且结果格式符合预期可以跳过反思直接进入下一步节省一次模型调用。只有结果异常或者格式不对时才触发完整反思。这个优化让整体 token 消耗降了大概三成。5. 常见问题排查与避坑经验实录5.1 模型输出格式不稳定怎么办这是最高频的问题。表现是模型有时候输出纯 JSON有时候包一层 markdown 代码块有时候在 JSON 前后加一句“好的这是计划”。我的应对策略是三层防护。第一层是 prompt 约束明确要求“只输出 JSON第一个字符必须是左花括号最后一个字符必须是右花括号”。第二层是解析容错用正则提取第一个完整的 JSON 对象忽略前后杂质。第三层是修复重试如果前两层都失败把错误输出和解析报错一起发回模型让它重新生成最多重试两次。实测下来加了这三层之后格式问题的最终失败率降到了 1% 以下。这里的关键是修复重试的 prompt 要包含具体的报错信息只告诉模型“你格式错了”它不知道该改哪里告诉它“第 15 个字符处缺少逗号”它就能精准修复。5.2 工具选错或者参数填错怎么排查工具选错通常有两个原因工具描述太模糊或者工具之间功能重叠。排查的时候我会把模型选择工具时的完整 prompt 打出来看重点看工具描述部分。如果两个工具描述语义太接近模型就会犹豫甚至选错。解决办法是把描述写得更具体突出各自的适用场景差异。参数填错更常见尤其是格式类参数。比如时间范围模型可能填“2024年3月”而不是“2024-03”。解决办法是在参数描述里给出明确的格式示例并且执行前做一次参数校验格式不对就直接返回错误让模型重填而不是硬着头皮执行。我整理了一个常见问题速查表放在下面供参考。问题现象可能原因排查方向解决手段模型输出无法解析格式约束不够强检查 prompt 约束加三层防护工具选错描述模糊或重叠打印工具描述细化描述差异参数格式错误缺少格式示例检查参数 schema加示例和校验任务跑不完步数限制太小看执行日志调大最大步数陷入死循环反思未生效看反思结论加强反思 prompttoken 消耗过高记忆未压缩看上下文长度启用分层记忆5.3 长任务上下文爆炸的处理技巧长任务跑到后面上下文越来越长token 消耗飙升甚至超限。我的处理技巧有三个。第一个是及时摘要前面讲记忆模块时说过超过 N 步就压缩。第二个是结果截断工具返回的结果如果特别长比如返回了一大段文本我会只保留前若干字符加省略号把完整结果存到外部存储需要时再取。第三个是无关信息剔除反思模块判断为“无关”的中间推理直接从上下文里删掉。这三个技巧组合使用我实测一个原本要 20 万 token 的长任务压缩到了 6 万 token 左右效果很明显。要注意的是摘要和截断都可能丢失关键信息所以我在实现里加了一个“关键信息标记”机制模型可以主动标记某条信息为“必须保留”这样压缩时会跳过它。5.4 我踩过的三个印象最深的坑第一个坑是异步工具的线程安全问题。早期我让多个工具并发执行结果共享的记忆模块被多个线程同时写数据错乱。后来给记忆模块加了读写锁并且规定同一时刻只有一个工具在执行虽然损失了一点并发性能但稳定性上来了。Agent 场景下稳定性比并发性能重要得多。第二个坑是模型对“失败”的过度乐观。工具明明返回了错误码模型在反思时还是说“这一步基本成功”。后来我在反思 prompt 里明确要求“如果工具返回中包含 error 字段或错误码必须判定为失败”并且把工具返回的原始结构完整展示给模型不让它自己脑补。这个改动之后错误识别率大幅提升。第三个坑是计划过于理想化。模型生成的计划经常假设每一步都成功没有考虑失败分支。我的解决办法是在规划 prompt 里要求“为每个关键步骤考虑失败时的备选方案”虽然增加了规划阶段的 token 消耗但让 Agent 在遇到意外时有了预案整体任务成功率反而更高。6. 框架的扩展方向与我的个人体会6.1 可以继续深挖的几个方向JavaManus 目前是个能跑通核心流程的基础版本后面还有不少可以扩展的地方。我列几个我觉得最有价值的方向。第一个是多 Agent 协作。现在的框架是单 Agent 串行执行如果能让多个 Agent 分工协作比如一个负责规划、一个负责执行、一个负责审核处理复杂任务的能力会更强。这个方向的关键是设计好 Agent 之间的通信协议。第二个是工具的动态发现。现在工具是启动时静态注册的如果能让 Agent 在运行时根据任务需要动态发现和加载工具灵活性会大大提升。这个可以结合一些服务注册发现的思路来做。第三个是执行过程的可视化。现在调试主要靠看日志如果能把 Agent 的规划、执行、反思过程可视化出来排查问题会直观很多。这个对团队协作特别有价值产品经理也能看懂 Agent 在干什么。6.2 用 Java 做 Agent 框架这件事本身的价值最后说点个人体会。用 Java 做 Agent 框架一开始我也担心是不是逆潮流而行毕竟 Python 生态那么成熟。但做下来发现Java 在这个领域有它独特的优势。最大的优势是工程化能力。Java 的强类型、成熟的并发模型、完善的依赖管理让框架在长期维护和团队协作上更稳。Python 写 Agent 原型很快但一旦要上生产、要多人协作、要长期迭代Java 的工程优势就体现出来了。另一个优势是和企业现有系统的融合。大部分企业的核心业务系统都是 Java 写的Agent 框架用 Java 实现接入这些系统几乎零成本。这个优势在真实落地场景里比技术上的先进性重要得多。当然Java 做 Agent 也有短板主要是生态和迭代速度。Python 那边新出的 Agent 技巧Java 这边往往要等一段时间才有对应实现。但我觉得这不是大问题Agent 的核心原理是相通的把原理吃透用什么语言实现只是工程选择。如果你也在做类似的事情我的建议是先把核心循环跑通别一上来就追求功能大而全。规划、执行、反思这个闭环能稳定跑起来比堆一堆花哨功能有价值得多。工具描述的质量、反思 prompt 的设计、记忆压缩的策略这三个地方值得反复打磨它们对 Agent 实际表现的影响远比框架架构本身大。