从零用 Java 构建 AI Agent 框架:JavaManus 设计与实现深度解析
当大模型从“对话工具”进化为“能调用工具的智能体”,Agent 框架的工程实现就成了关键。本文以一个从零构建的 Java Agent 项目 JavaManus 为线索,深入拆解 ReAct 循环、工具调用编排、记忆管理、火山引擎 Ark 适配层的设计思路,并分享开发过程中踩到的几个真实坑——包括一个让超时机制完全失效的隐蔽死锁。
提起 AI Agent,大家首先想到的是 Python 生态:LangChain、LlamaIndex、AutoGPT……确实,Python 在 LLM 领域有压倒性的生态优势。但在实际工程落地中,我们常常面临一个现实问题:企业的核心业务系统是 Java 的。
把 Python Agent 嵌入 Java 系统,意味着跨进程调用、序列化开销、运维复杂度翻倍。如果能有一个轻量、可控、能直接复用 Spring 生态的 Java Agent 框架,工程价值是巨大的。
JavaManus 的目标就是:用纯 Java 实现一个可扩展的 ReAct Agent,无缝接入 Spring Boot 应用,支持自定义工具和任意 LLM 提供商。
项目基于 Spring AI 1.1.0 + Spring Boot 3.4,使用火山引擎 Ark(豆包)作为默认 LLM,整体不到 30 个 Java 文件,结构清晰,适合学习和二次开发。
这种分层让每一层的职责单一,替换 LLM 或工具集都不需要改 Agent 核心逻辑。
ReAct(Reasoning + Acting)是 Agent 最经典的模式。它的核心思想是:让模型在"思考"和"行动"之间交替,每一步的行动结果都喂回模型作为下一步思考的依据。
在 JavaManus 中,这个循环被抽象为 BaseAgent.run():
step() 由 ReActAgent 实现,是一个经典的模板方法:
ToolCallAgent.think() 的职责是:把 memory 中的消息 + system prompt + 工具定义发给 LLM,解析返回的文本和工具调用。
这里有一个关键设计点:internalToolExecutionEnabled(false)。
Spring AI 默认会在 ChatModel.call() 内部自动执行工具调用并递归请求 LLM。但在 Agent 框架中,我们需要手动控制工具执行的时机和方式(比如截断工具输出、触发事件、检测特殊工具),所以必须禁用自动执行,自己编排循环。
注意 maxObserve 的设计:工具执行结果可能非常大(比如 cat 了一个几万行的文件),如果全部喂回 LLM,会迅速耗尽 context window。所以必须截断,只保留前 N 个字符。
Memory 本质上是一个带上限的消息列表,每次 addMessage 后会检查是否超过 maxMessages,超过就丢弃最早的消息:
这个"滑动窗口"策略很朴素,但在长任务中能保证 context 不会无限膨胀。更好的做法是用摘要或向量检索做长期记忆,但对于大部分工具调用场景,滑动窗口已经够用。
工具是 Agent 与外部世界交互的桥梁。JavaManus 的工具系统设计很简洁。
这里复用了 Spring AI 的 ToolCallback 接口,让所有工具天然能被 Spring AI 的工具调用机制识别。execute 返回纯文本,简化了工具与 LLM 的交互协议。
StrReplaceEditor:文件查看/创建/编辑工具,支持 view、create、str_replace、insert、undo_edit 五种命令。这是 Agent 修改代码的主要手段。
PythonExecute:通过 ProcessBuilder 启动独立 Python 进程执行代码,带超时保护。让 Agent 能做数学计算、数据处理等 Python 擅长的事。
Spring AI 官方没有火山引擎的 starter,所以需要自己实现 ChatModel 和 EmbeddingModel。
核心是把 Spring AI 的 Prompt 转成 Ark SDK 的 ChatCompletionRequest,再把响应转回 Spring AI 的 ChatResponse:
消息转换中最复杂的是 tool call 的双向映射:
这里要注意:Ark 的 ChatFunctionCall 中 arguments 是 JSON 字符串,Spring AI 也是字符串,所以可以直接透传。
Ark 适配层有一个 systemPrompt 配置项,但 Agent 层也有自己的 systemPrompt。这两个的职责是:
来源:稀土掘金 原文