news 2026/10/14 6:40:53

Java 从零手写 AI Agent 框架:ReAct 循环与工具调用实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 从零手写 AI Agent 框架:ReAct 循环与工具调用实战

1. 为什么我要用 Java 从头写一个 AI Agent 框架

1.1 一个真实的需求场景

去年下半年,我所在的小团队接到了一个内部工具的需求:让系统能够自动完成一些多步骤的日常任务,比如读取一批文档、提取关键信息、调用几个内部接口做数据校验、最后生成一份汇总报告。听起来像是典型的“AI Agent”场景,但当时我们面临一个很现实的问题——团队的技术栈是清一色的 Java,Spring Boot 那套东西大家闭着眼睛都能写,但一提到 AI Agent,社区里铺天盖地的示例全是 Python。

我试过直接用 Python 写一个原型,确实快,LangChain 之类的框架把链路串起来也就百来行代码。但问题很快就暴露了:这个原型要接入我们现有的 Java 服务,得额外维护一套 Python 运行时、一套依赖管理、一套部署流程,运维同事看我的眼神都不对了。更麻烦的是,团队里其他人想改点逻辑,得先学一遍 Python 生态,成本太高。

于是我就想,能不能用 Java 从头搭一个 AI Agent 框架?不是那种把 Python 框架翻译一遍的玩具,而是真正贴合 Java 工程习惯、能直接嵌进现有 Spring Boot 项目里的东西。这个想法最终变成了 JavaManus 这个项目。这篇文章就是把我从零设计到落地的完整思路和踩过的坑,原原本本讲一遍。

1.2 JavaManus 到底是个什么东西

先把定位说清楚。JavaManus 是一个用 Java 编写的 AI Agent 编排框架,核心目标是让开发者能用纯 Java 代码定义 Agent 的思考流程、工具调用和任务执行链路。它不绑定任何特定的大模型厂商,你可以在配置里切换不同的模型服务;它也不要求你引入额外的运行时,一个普通的 Maven 依赖就能跑起来。

它解决的问题很具体:当你需要让大模型不只是“聊聊天”,而是要真正去调用你的业务接口、读写文件、执行多步推理时,JavaManus 提供了一套结构化的抽象。你可以把它理解成一个“Agent 的骨架”,思考逻辑、工具注册、上下文管理、执行循环这些脏活累活它帮你处理了,你只需要关注“这个 Agent 要干什么”和“它能用哪些工具”。

适合谁来参考呢?我觉得有三类人:一是 Java 后端开发者,想在现有项目里加一点 AI 能力但不想引入 Python;二是对 Agent 原理感兴趣、想通过读源码搞明白 ReAct、工具调用这些概念怎么落地的人;三是需要在内网环境部署、对依赖可控性要求高的团队。如果你只是想快速做个 Demo,Python 生态确实更省事,但如果你要的是可维护、可嵌入、可长期演进的方案,JavaManus 这条路值得走一遍。

2. 整体架构设计与技术选型思路

2.1 核心设计原则:少依赖、可替换、易调试

动手写第一行代码之前,我给自己定了三条规矩,后面所有的设计决策基本都围绕这三条展开。

第一条是少依赖。Java 生态里很多框架喜欢层层封装,一个功能引入十几个传递依赖,最后依赖冲突排查起来要命。JavaManus 的核心模块我只允许依赖 Jackson 做 JSON 处理、SLF4J 做日志,HTTP 客户端直接用 JDK 自带的 HttpClient。这样做的代价是有些轮子要自己造,但好处是任何一个 Java 项目引入它都不会有负担。

第二条是可替换。大模型服务、工具实现、记忆存储这些东西,今天用 A 方案,明天可能就要换 B 方案。所以框架里所有关键组件都定义成接口,通过 SPI 或者简单的工厂模式来装配。比如模型调用这块,我定义了一个ChatModel接口,OpenAI 风格的服务、本地部署的推理服务、甚至你自己写的一个 Mock 实现,只要实现这个接口就能插进来。

第三条是易调试。Agent 执行过程是个黑盒,出了问题很难定位。所以我在框架里内置了完整的执行轨迹记录,每一步的输入、输出、耗时、调用的工具名都会结构化地记下来。调试的时候把轨迹打出来,一眼就能看出是哪一步的提示词写歪了,还是工具返回的数据格式不对。

2.2 分层结构:从模型接入到 Agent 编排

整个框架我分成了四层,从上到下依次是:

  • Agent 编排层:负责定义 Agent 的行为逻辑,包括系统提示词、可用工具列表、最大迭代次数、终止条件等。这一层是开发者直接打交道的部分。
  • 执行引擎层:这是框架的心脏,实现了 ReAct 风格的思考-行动-观察循环。它负责解析模型的输出、决定下一步是调用工具还是给出最终答案、管理上下文窗口。
  • 能力抽象层:包括ChatModel、Tool、Memory这几个核心接口,以及它们的默认实现。这一层决定了框架的扩展能力。
  • 基础设施层:HTTP 通信、JSON 序列化、日志、异常体系这些底层支撑。

这么分层的好处是职责清晰。比如你想换一个模型服务,只需要在能力抽象层加一个ChatModel实现,上面的执行引擎和 Agent 编排完全不用动。反过来,如果你想改执行循环的逻辑,比如从 ReAct 换成 Plan-and-Execute,也只需要动执行引擎层。

2.3 为什么选择 ReAct 作为默认执行模式

Agent 的执行模式有好几种,常见的有 ReAct、Plan-and-Execute、Reflexion 等。我最终选择 ReAct 作为默认模式,理由有几个。

ReAct 的核心思想是“边想边做”,模型每一步先输出一段思考,然后决定是调用某个工具还是直接回答。这种模式的好处是灵活,不需要预先规划完整的步骤,遇到意外情况可以随时调整。对于大多数业务场景来说,任务的不确定性比较高,预先规划往往不靠谱,ReAct 的适应性更强。

另一个原因是 ReAct 的实现相对简单,对模型的要求也没那么高。Plan-and-Execute 需要模型有很强的规划能力,一旦规划错了后面全错;Reflexion 需要额外的反思步骤,token 消耗大。ReAct 在效果和成本之间取得了比较好的平衡。

当然,ReAct 也有它的局限,比如容易陷入循环、对提示词质量依赖高。这些在后面的章节里我会具体讲怎么缓解。

3. 核心模块的详细拆解与实现要点

3.1 ChatModel 接口:把模型调用抽象干净

模型调用是整个框架的入口,也是最容易写乱的地方。我见过很多项目直接把 HTTP 请求散落在业务代码里,换个模型就要改一堆地方。JavaManus 的做法是定义一个极简的ChatModel接口:

public interface ChatModel { ChatResponse chat(ChatRequest request); }

ChatRequest里封装了消息列表、温度、最大 token 数这些参数,ChatResponse里则是模型返回的消息内容和元信息。就这么简单,没有多余的方法。

默认实现我提供了两个:一个是基于 OpenAI 兼容接口的OpenAiChatModel,另一个是用于测试的MockChatModel。前者用 JDK 的 HttpClient 发请求,JSON 序列化用 Jackson,整个实现不到两百行。这里有个细节值得说:我在ChatRequest里加了一个stopSequences字段,因为 ReAct 模式下需要让模型在输出特定标记时停下来,这个字段很关键。

注意:不同模型服务对参数的支持程度不一样,比如有些服务不支持stopSequences,有些对温度的取值范围有要求。我在ChatModel实现里做了参数过滤,不支持的参数直接忽略而不是报错,这样切换服务时不会因为参数问题挂掉。

3.2 Tool 接口:让模型能真正干活

工具是 Agent 的手和脚。没有工具,Agent 就只是个聊天机器人。JavaManus 的Tool接口设计如下:

public interface Tool { String name(); String description(); String inputSchema(); String execute(String input); }

四个方法各有讲究。name()是工具的唯一标识,模型在调用时会用到;description()是给模型看的说明,写得清不清楚直接决定模型会不会正确使用这个工具;inputSchema()返回一个 JSON Schema 字符串,描述输入参数的格式;execute()是实际执行逻辑,输入输出都是字符串。

为什么输入输出都用字符串?因为模型生成的是文本,返回的也是文本。如果强行用泛型或者对象,序列化和反序列化的边界情况会非常多,反而容易出错。用字符串,把解析的责任交给工具实现者,框架保持简单。

工具注册我用了一个ToolRegistry来管理,支持按名称查找。这里有个实用技巧:在把工具列表传给模型时,我会把每个工具的 name、description 和 inputSchema 拼成一段结构化的文本,而不是直接塞 JSON。实测下来,这种格式模型理解得更准,调用成功率更高。

3.3 执行引擎:ReAct 循环的落地细节

执行引擎是框架里最复杂的部分,也是我花时间最多的地方。ReAct 循环的基本流程是:把系统提示词、历史消息、工具列表拼成请求发给模型,模型返回一段文本,解析这段文本判断是要调用工具还是给出最终答案,如果是调用工具就执行工具、把结果加入历史消息,然后进入下一轮。

听起来简单,但细节全是坑。第一个坑是输出解析。模型返回的文本格式不稳定,有时候用 JSON,有时候用特定的标记语言。我最终定了一套基于标记的协议,要求模型按固定格式输出:

Thought: 我需要先查询天气 Action: get_weather Action Input: {"city": "北京"}

然后在代码里用正则去解析。这里的关键是提示词里要把格式要求写得非常明确,并且给几个例子。我试过只写一句“请按格式输出”,模型经常自由发挥,加上两三个完整的示例后,格式遵循率明显提升。

第二个坑是循环控制。模型有时候会反复调用同一个工具,或者陷入无意义的思考。我加了三重保护:最大迭代次数限制、相同工具调用去重、以及一个“无进展检测”——如果连续两轮的工具调用和返回结果高度相似,就强制终止并返回当前最好的答案。

第三个坑是上下文窗口管理。多轮循环下来,历史消息会越来越长,最终超出模型的上下文限制。我的做法是保留系统提示词和最近 N 轮的消息,更早的消息做摘要压缩。摘要用一个单独的模型调用来生成,把之前的思考过程和工具结果浓缩成一段简短描述。

3.4 Memory 接口:上下文管理的抽象

Memory 这块我设计得比较轻量,因为大多数场景下用简单的滑动窗口就够了。接口定义如下:

public interface Memory { void add(Message message); List<Message> getMessages(); void clear(); }

默认实现是WindowMemory,保留最近 N 条消息。另外还有一个SummaryMemory,在窗口基础上加了摘要功能。这里我想强调一点:Memory 和上下文窗口管理是两回事。Memory 负责存什么,上下文管理负责取什么发给模型。把这两个职责分开,代码会清晰很多。

4. 完整实操:从零跑通一个 Agent

4.1 环境准备与依赖引入

先说一下环境要求。JDK 17 及以上,Maven 3.8 以上,这是基础。JavaManus 本身没有其他硬性依赖,但如果你要用默认的OpenAiChatModel,需要能访问对应的模型服务。

在pom.xml里引入核心依赖:

<dependency> <groupId>com.javamanus</groupId> <artifactId>javamanus-core</artifactId> <version>0.1.0</version> </dependency>

如果你是从源码构建,直接mvn clean install就行。整个核心模块编译出来大概 300KB 左右,非常轻量。

4.2 定义你的第一个工具

假设我们要做一个查询订单状态的 Agent,先定义一个工具:

public class OrderQueryTool implements Tool { @Override public String name() { return "query_order"; } @Override public String description() { return "根据订单号查询订单状态,输入为订单号字符串"; } @Override public String inputSchema() { return """ { "type": "object", "properties": { "orderId": {"type": "string", "description": "订单号"} }, "required": ["orderId"] } """; } @Override public String execute(String input) { // 解析 input,调用实际业务逻辑 // 返回结果字符串 return "订单状态:已发货"; } }

这里description()的写法很关键。我踩过的坑是描述写得太笼统,比如只写“查询订单”,模型不知道输入该给什么。后来改成明确说明输入格式和用途,调用准确率提升很多。

4.3 组装并运行 Agent

接下来把模型、工具、Agent 组装起来:

ChatModel model = new OpenAiChatModel(apiKey, baseUrl, "gpt-4"); ToolRegistry registry = new ToolRegistry(); registry.register(new OrderQueryTool()); Agent agent = Agent.builder() .chatModel(model) .toolRegistry(registry) .systemPrompt("你是一个订单助手,帮助用户查询订单状态。") .maxIterations(5) .build(); String result = agent.run("帮我查一下订单 12345 的状态"); System.out.println(result);

运行起来后,框架内部会经历这样的过程:模型先输出思考“用户想查订单,我需要调用 query_order 工具”,然后输出 Action 和 Action Input,框架解析后执行工具,把结果返回给模型,模型再生成最终回答。整个过程你可以在日志里看到完整的轨迹。

4.4 参数选择与调优建议

几个关键参数我给出实测后的建议值。maxIterations默认设 5 到 8 比较合适,太小复杂任务跑不完,太大容易浪费 token。温度建议设 0 到 0.3,Agent 场景需要稳定输出,太高的温度会让格式遵循率下降。stopSequences一定要设,通常设成"\nObservation:",这样模型输出到观察标记就会停,避免它自己编造工具返回结果。

提示:如果你发现 Agent 经常不调用工具而是直接瞎编答案,先检查系统提示词里有没有明确要求“必须使用工具获取信息,不要凭记忆回答”。这句话看起来废话,但实测对行为影响很大。

5. 常见问题与排查技巧实录

5.1 模型不按格式输出怎么办

这是最高频的问题。表现是模型返回的文本里找不到 Action 标记,或者格式对不上。排查思路分三步:先看提示词里的格式说明和示例够不够清楚,通常加一两个完整示例就能解决;再看温度是不是太高,调低试试;最后看模型本身的能力,有些小模型对复杂格式的遵循确实差,这种情况要么换模型,要么把格式简化。

我整理了一个速查表:

问题表现可能原因解决方向
找不到 Action 标记提示词格式说明不清增加完整示例
格式时对时错温度过高温度降到 0.1
输出 JSON 但字段名不对Schema 描述不明确在提示词里明确字段名
直接给出答案不调工具系统提示词约束不足加“必须使用工具”指令

5.2 工具调用陷入死循环

模型反复调用同一个工具,每次输入还差不多。我的处理是在执行引擎里加了一个调用指纹机制,把工具名和输入参数拼起来做哈希,如果同一个指纹连续出现两次,就在下一轮的提示词里插入一句提醒:“你已经调用过这个工具并得到了结果,请基于已有信息给出答案。”大多数情况下这一句就能把模型拉回来。如果还不行,就触发强制终止。

5.3 上下文超限导致报错

长任务跑到后面,历史消息堆积导致请求超出模型上下文限制。除了前面说的摘要压缩,还有一个应急方案:在拼接请求前做一个 token 估算,如果超过阈值就自动丢弃最早的非系统消息。token 估算不用太精确,按字符数除以 3 粗略估计就够了,留出足够余量。

5.4 工具执行异常的处理

工具执行过程中抛异常是常有的事,比如网络超时、参数解析失败。我的做法是捕获所有异常,把异常信息作为工具返回结果传给模型,而不是让整个 Agent 挂掉。模型看到错误信息后,往往会尝试换一种方式调用或者给出解释。这比直接抛异常中断流程要健壮得多。

6. 一些设计上的取舍与个人体会

写这个框架的过程中,我做了不少取舍,有些当时觉得纠结,事后看是对的。比如坚持用字符串作为工具输入输出的类型,一开始觉得不够优雅,但后来发现它避免了大量边界情况,反而让代码更稳。再比如没有引入响应式编程,虽然 Java 生态里 WebFlux 很流行,但 Agent 执行本质上是串行的思考循环,用同步阻塞的写法更直观,调试也更容易。

还有一个体会是关于提示词的。框架能帮你管理流程,但 Agent 的实际效果很大程度上取决于提示词的质量。我花在调提示词上的时间不比写代码少。好的提示词要满足几点:角色定义清晰、任务边界明确、输出格式有示例、异常情况有指引。这几点做到了,同样的框架效果能差出一大截。

后续这个框架还可以往几个方向扩展,比如支持多 Agent 协作、加入更复杂的规划模式、提供可视化的执行轨迹查看界面。但核心的这套抽象我觉得已经够用了,先把简单的事情做扎实,比堆功能更重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:40:48

多智能体协作系统Agency Agents:架构设计与落地实操指南

1. 从单兵作战到团队协同&#xff1a;为什么我们需要重新思考智能体架构做过智能体应用开发的人大概都有过这种体验&#xff1a;一开始用单个大模型驱动一个智能体&#xff0c;处理简单任务时效果惊艳&#xff0c;可一旦任务链条变长、涉及的角色变多&#xff0c;整个系统就开始…

作者头像 李华
网站建设 2026/10/14 6:40:04

用Hugo搭建超快静态博客:从选型到部署全攻略

上个月有个同事跟我说&#xff0c;他花了两天时间搭了个 WordPress 博客&#xff0c;结果第三天就在折腾插件兼容性和服务器内存&#xff0c;第四天直接把博客删了。我给他推荐了 Hugo&#xff0c;他半天时间把博客重新搭好&#xff0c;还写完了第一篇文章。这件事让我特别想聊…

作者头像 李华
网站建设 2026/10/14 6:40:04

用shared_ptr实现list:打破循环引用与内存泄漏的现代C++实践

用shared_ptr写一个能用的list&#xff0c;听起来是件挺酷的事&#xff1a;再也不用在析构函数里写while循环手动delete节点了。但真正动手之后你会发现&#xff0c;它带来的麻烦远比想象中多&#xff0c;最典型的就是循环引用。两个节点互相用shared_ptr指来指去&#xff0c;引…

作者头像 李华
网站建设 2026/10/14 6:39:48

Python门诊挂号系统:号源动态管理与高并发实战

简介&#xff1a;本资源是一套完整的医院门诊挂号与预约系统毕业设计级源码&#xff0c;面向计算机专业本科生、医疗信息化初学者及全栈开发学习者&#xff0c;旨在解决传统挂号流程效率低、信息不透明、系统耦合度高等现实问题。压缩包共220个文件&#xff0c;总大小8.39MB&am…

作者头像 李华
网站建设 2026/10/14 6:39:28

Vibe Coding工具选型与四步实施框架:效率提升27%与成本差24倍背后的工程实践

1. 效率数字背后的真实账本先把标题里两个数字拆开看。效率提升27%这个量级&#xff0c;放在任何研发团队里都算得上可观&#xff0c;但它不是凭空冒出来的——它通常来自编码环节中重复劳动被压缩、上下文切换次数减少、以及试错周期缩短这三块的叠加。而成本差24倍这个说法&a…

作者头像 李华
网站建设 2026/10/14 6:39:21

基于YOLOv8的外墙裂缝检测系统:从数据标注到界面部署全解析

外墙裂缝检测这几年在建筑安全评估、老旧小区改造、物业管理里需求增长很快&#xff0c;很多读者私信问有没有现成的方案能直接改一改用于课程设计或者小规模工程试点。我最近把一套基于YOLOv8的外墙裂缝检测识别系统完整落地了&#xff0c;做了中英文双版界面&#xff0c;包含…

作者头像 李华