从零构建Java AI智能体框架:演进式模块设计与核心原理剖析

张开发
2026/4/26 3:52:45 15 分钟阅读

分享文章

从零构建Java AI智能体框架:演进式模块设计与核心原理剖析
1. 项目概述从零构建一个Java版AI智能体框架最近在折腾AI应用开发发现市面上的Agent框架要么是Python的天下要么就是封装得太“黑盒”想深入理解其内部运作机制得扒好几层皮。作为一个有十多年经验的Java开发者我决定自己动手用Java从零开始完整实现一个基于Claude风格API的AI智能体框架——这就是HoppinZQ Agent项目的由来。这个项目不是一个简单的API封装而是一个完整的、演进式的教学项目。它通过11个递进式的模块带你从最基础的Bash命令执行器开始一步步构建出具备任务管理、上下文压缩、多工具协同、甚至Web服务能力的完整Agent系统。每个模块都是独立的Maven子项目你可以像搭积木一样从s01开始逐个模块学习和实践亲眼见证一个AI智能体是如何“长大”的。如果你是一名Java开发者对AI应用开发感兴趣但又觉得现有的框架过于复杂或不够透明那么这个项目就是为你准备的。它不依赖任何“魔法”所有代码都是清晰可读的Java核心逻辑不过几百行。通过它你不仅能学会如何使用大模型API更能深刻理解Agent背后的设计哲学工具调用、上下文管理、任务编排这些核心概念到底是怎么落地的。2. 核心架构与设计理念拆解2.1 为什么选择Java和Claude风格API在开始动手之前得先想清楚技术选型。为什么用Java又为什么瞄准Claude的API风格首先Java的生态和工程化能力是毋庸置疑的。我们构建的是一个可能用于生产环境的Agent框架需要健壮的错误处理、清晰的模块划分、以及易于集成的能力。Spring Boot、MyBatis-Plus这些成熟的Java生态组件能让我们快速搭建起Web服务和数据持久层而不用从零发明轮子。更重要的是这个项目的目标是“教学”Java严谨的面向对象特性和丰富的调试工具能让学习路径上的每一步都清晰可见。其次选择Claude风格的API背后有很实际的考量。目前在代码生成和推理能力上Claude系列模型尤其是Claude 3.5 Sonnet是公认的佼佼者。它的API设计非常优雅与OpenAI的接口高度相似这意味着我们的框架在底层上具备很好的兼容性。一个更关键的点是国产的顶尖模型如智谱GLM-5和DeepSeek都已经提供了对AnthropicClaude出品方API协议的兼容。这意味着你写好一套基于Claude API的调用逻辑只需要更换BASE_URL和API_KEY就能无缝切换到这些国产大模型上彻底解决了“卡脖子”的访问问题。实操心得在项目初期我就把API调用的抽象层做得足够薄。核心的ZQAgent基类只依赖一个非常简单的HTTP客户端和JSON解析器。这样设计的好处是未来如果出现更优秀的API协议或模型我们可以用最小的代价进行适配而不是被某一家供应商绑定。2.2 演进式模块设计像拼图一样学习整个项目最精妙的设计莫过于它的模块化演进路线。它不是一上来就给你一个庞然大物而是设计了11个独立的模块s01到s08外加s13、s14和web每个模块在前一个的基础上只增加一个核心功能。你可以把s01模块看作是这个智能体的“胚胎期”。它只有一个能力执行Bash命令。对应的代码里也只有一个核心工具类BashTool。这时Agent的循环逻辑ZQAgent.runLoop已经成型接收用户输入构造包含工具描述的System Prompt发给大模型解析模型的返回可能是纯文本也可能是工具调用请求执行工具将结果追加到对话历史然后继续循环。到了s02模块这个“胚胎”长出了“手”和“眼睛”——我们增加了文件读写、编辑和内容搜索基于ripgrep等5个新工具。这时Agent已经能帮你操作本地文件了。s03模块增加了“记事本”功能即Todo待办事项管理。你会发现每个新功能的加入并不是对原有代码的颠覆性修改而是在ZQAgent这个稳固的基类之上通过注册新的ToolDefinition来扩展能力。这种设计的教学意义巨大。你不需要一次性理解Agent所有的复杂概念。你可以先运行s01看看最简单的工具调用是怎么工作的理解透彻后再进入s02学习如何设计和集成更复杂的工具。这种循序渐进的方式极大地降低了学习曲线也让调试和问题定位变得异常简单——如果新加的功能出了问题你几乎可以肯定问题就出在新写的那个工具类里。2.3 统一的核心循环与工具系统无论模块如何演进所有Agent都共享同一个心脏——ZQAgent基类中定义的核心运行循环。这个循环的逻辑非常经典也是理解任何Agent框架的钥匙初始化根据当前注册的工具列表动态生成System Prompt。这个Prompt会告诉大模型“你现在是一个助手可以调用以下工具A、B、C...调用时请遵循JSON格式。”对话循环将用户输入和累积的对话历史Context一起发送给大模型。解析与分发解析大模型的返回。如果是普通文本直接输出给用户本轮结束。如果是一个工具调用请求一个结构化的JSON则进入下一步。工具执行根据工具名找到对应的Java方法传入参数执行真正的业务逻辑比如执行一条Shell命令、读取一个文件。结果反馈将工具执行的结果成功或失败格式化成一段自然语言描述追加到对话历史中。继续或终止带着包含了工具执行结果的、更丰富的上下文再次跳回第2步请求大模型进行下一步推理。循环会一直持续直到大模型返回纯文本结论或者达到预设的最大交互轮次。工具系统是另一个设计亮点。每个工具都是一个普通的Java方法但需要用Tool注解进行标记并定义一个描述其输入参数的Schema类。框架在启动时会扫描这些注解自动将它们包装成LLM能理解的ToolDefinition对象。这种基于注解和反射的机制让增加一个新工具变得和写一个Service方法一样简单开发者只需要关注业务逻辑本身。3. 核心模块深度解析与实操要点3.1 基石模块s01 Bash执行器与Agent循环实现s01模块是整个项目的起点代码量最少但包含了Agent最核心的骨架。我们来看关键部分。ZQAgent基类的核心循环伪代码public void runLoop() { ListMessage conversationHistory new ArrayList(); // 1. 构建系统提示包含所有工具的描述 String systemPrompt buildSystemPromptWithTools(); conversationHistory.add(Message.system(systemPrompt)); while (true) { // 2. 获取用户输入 String userInput getUserInput(); conversationHistory.add(Message.user(userInput)); // 3. 调用LLM获取响应 LLMResponse response callLLM(conversationHistory); if (response.isText()) { // 4. 如果是文本输出并结束本轮 printToUser(response.getText()); break; } else if (response.isToolCall()) { // 5. 如果是工具调用执行工具 ToolCall toolCall response.getToolCall(); String toolResult executeTool(toolCall); // 6. 将工具执行结果作为“系统”或“助手”消息追加到历史 conversationHistory.add(Message.assistant(工具执行结果: toolResult)); // 循环继续让LLM基于结果进行下一步思考 } } }BashTool工具的实现Slf4j public class BashTool { Tool(name execute_bash, description 在本地执行一条bash命令并返回结果) public String executeBash(ToolParam(description 要执行的bash命令) String command) { try { Process process Runtime.getRuntime().exec(new String[]{bash, -c, command}); String stdout IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8); String stderr IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8); process.waitFor(); int exitCode process.exitValue(); return String.format(Exit Code: %d\nStdout:\n%s\nStderr:\n%s, exitCode, stdout, stderr); } catch (Exception e) { log.error(执行命令失败: {}, command, e); return 命令执行失败: e.getMessage(); } } }注意事项在s01中一个容易被忽略但至关重要的细节是工具描述的编写质量。Tool注解里的description和ToolParam里的description会直接作为Schema的一部分发送给大模型。这些描述必须清晰、无歧义并且说明白工具的“边界”。例如execute_bash的描述明确了是“在本地执行”这能防止模型产生不切实际的期望比如让它去操作远程服务器。模糊的描述会导致模型错误地调用工具或传递错误的参数。3.2 能力扩展s02文件操作与s03 Todo管理s02模块引入了文件操作工具集这是Agent从“对话机”迈向“执行者”的关键一步。我们新增了read_file,write_file,edit_file,list_files,search_content等工具。其中search_content工具基于ripgreprg命令提供了强大的代码库内容检索能力这对于编程助手类的Agent至关重要。这里以edit_file工具为例展示如何设计一个“有状态”的复杂工具。它不仅要修改文件还要能向LLM清晰地展示修改前后的差异diff以便LLM能判断修改是否正确或进行下一步调整。EditFileTool的核心逻辑public class EditFileTool { Tool(name edit_file, description 编辑文件中的特定内容。需要提供文件路径、要替换的旧文本和新文本。) public String editFile( ToolParam(description 文件的绝对路径) String filePath, ToolParam(description 文件中需要被替换的旧文本) String oldText, ToolParam(description 替换旧文本的新文本) String newText) { try { String originalContent Files.readString(Paths.get(filePath)); if (!originalContent.contains(oldText)) { return 错误在文件中未找到指定的旧文本。; } String newContent originalContent.replace(oldText, newText); Files.write(Paths.get(filePath), newContent.getBytes()); // 构造一个清晰的diff结果帮助LLM理解 return String.format(文件编辑成功。\n原内容片段[...]%s[...]\n新内容片段[...]%s[...], abbreviate(oldText), abbreviate(newText)); } catch (Exception e) { return 编辑文件时出错: e.getMessage(); } } }s03模块引入了TodoManager这是一个更高级别的抽象。它不再是简单的原子操作而是维护了一个在内存中的待办事项列表ListTodoItem。工具包括add_todo,list_todos,mark_todo_done,delete_todo等。这个模块演示了如何让Agent操作和管理一个内部状态。这个状态在Agent的单次会话生命周期内是持久的使得Agent可以像一个真正的个人助理一样记住你交代的事情。实操心得在实现s02和s03时错误处理的友好性变得特别重要。文件可能不存在文本可能找不到参数可能格式错误。工具方法必须捕获所有异常并返回一段LLM能理解的、自然语言描述的错误信息。例如返回“错误文件/tmp/test.txt不存在”而不是一个FileNotFoundException的堆栈信息。这能帮助LLM进行后续的推理和决策比如提示用户先创建文件。3.3 架构进阶s04子Agent与s05技能系统从s04开始项目进入了架构设计的深水区。s04引入了SubAgent的概念。有时候一个复杂的任务需要专注的、上下文隔离的“子对话”来完成。比如用户要求“写一个Java单例模式并解释其线程安全性”主Agent可以创建一个专注于“代码生成与解释”的子Agent去处理这个子任务。子Agent拥有独立的对话历史不会污染主对话的上下文。任务完成后子Agent将最终结果返回给主Agent。SubAgent的关键设计public class SubAgent { private ZQAgent innerAgent; // 内部运行一个独立的Agent实例 private String objective; // 子任务目标 public String run(String objective, String initialInput) { this.objective objective; // 为子Agent构建一个专属的系统提示强调其专注范围 String subSystemPrompt String.format(你是一个专注于解决以下任务的专家%s。请专注于此任务完成后给出最终答案。, objective); // 内部Agent运行拥有独立的历史记录 return innerAgent.runWithPrompt(subSystemPrompt, initialInput); } }s05模块的技能Skill系统是另一个精妙设计。它的核心问题是如何让Agent掌握大量、复杂的知识比如一个项目的API文档、一套复杂的操作流程而又不一次性耗尽宝贵的上下文窗口项目采用了两层注入的策略第一层轻量索引在System Prompt中只注入所有技能的名称和简短描述约100 tokens/技能。例如“可用技能spring_boot_startup_guide- Spring Boot项目启动指南”。第二层按需加载当LLM认为需要某个技能时它会调用load_skill工具。此时框架才会从磁盘或数据库加载该技能的完整内容可能是几千字的Markdown文档并将其作为工具执行结果返回给LLM。这样上下文窗口只在实际需要时才被大量占用。3.4 性能与工程化s06上下文压缩与s07/s08任务管理随着对话轮次增加上下文会越来越长最终会触及模型的最大Token限制。s06模块的上下文压缩Context Compaction就是为了解决这个问题。它实现了三层策略微压缩每次工具调用后自动将冗长的“工具调用请求”和“原始结果”替换为一句简短的总结如“已使用bash工具查看了当前目录”。自动压缩当上下文长度接近阈值时自动触发。调用LLM对除最近几轮外的历史对话进行总结用一段摘要替换掉大量旧消息。手动压缩提供compact工具LLM可以在认为对话历史冗长时主动调用手动触发压缩。s07和s08模块则展现了Agent的工程化与自动化能力。s07引入了基于有向无环图DAG的任务管理系统。你可以描述一个包含多个步骤且步骤间有依赖关系的复杂任务比如“1. 解析需求2. 根据需求生成代码3. 运行单元测试”。TaskManager会解析依赖并按正确顺序调度任务节点执行。s08的后台任务更进一步。某些任务如“监控日志文件中的错误”是长期运行的。BackgroundManager允许Agent启动一个守护线程在后台执行任务并通过一个通知队列将后台产生的重要事件如“发现错误日志”注入回主Agent的上下文中从而实现了异步的事件驱动交互。避坑指南实现上下文压缩时最大的坑在于信息丢失。过于激进的压缩会导致LLM失忆。我们的策略是“保新舍旧”和“保留关键指令”。永远保留最新的用户指令和最近几轮交互的完整内容。压缩的目标是那些遥远的、细节性的中间过程。在自动压缩的Prompt中要明确指示LLM“请总结对话的目标和目前已完成的进展”而不是总结所有细节。4. 高级特性与集成方案实战4.1 s13 MCP协议集成连接外部工具宇宙MCPModel Context Protocol是一个新兴的开放协议旨在标准化LLM与外部工具、数据源之间的连接。你可以把它想象成LLM世界的“USB标准”。s13模块实现了MCP客户端让我们的Agent能动态发现并调用任何符合MCP标准的服务器提供的工具。例如你可以运行一个MCP服务器它提供了“查询数据库”、“发送邮件”、“控制智能家居”等工具。我们的Agent在启动时会通过MCP协议连接到这个服务器自动获取这些工具的Schema并注册到自己的工具列表中。此后Agent就能像调用内置的bash工具一样调用“发送邮件”工具。MCP集成的核心流程连接Agent启动时根据配置STDIO/SSE/HTTP连接到MCP服务器。发现通过MCP定义的tools/list等接口获取服务器暴露的所有工具列表及其详细Schema。注册将这些远程工具动态地包装成本地ToolDefinition注册到ZQAgent的工具系统中。调用当LLM请求调用某个MCP工具时框架将参数通过MCP协议转发给服务器执行并将结果返回。这使得Agent的能力边界得到了无限扩展而且不需要修改Agent本身的代码。这是构建通用型、可插拔Agent系统的关键。4.2 s14 ReAct推理模式让思考过程可见ReActReasoning Acting是一种让LLM将思考过程显式化的提示模式。标准的工具调用是“黑盒”的用户输入 - LLM直接返回工具调用。而在ReAct模式下LLM会先输出一个Thought:思考阐述它接下来要做什么以及为什么然后才是Action:工具调用。执行工具得到Observation:观察结果后再进入下一轮Thought。s14模块实现了这个模式。这不仅让Agent的决策过程更透明、更易于调试而且对于复杂任务显式的推理链能显著提高任务完成的准确率。框架通过一个特殊的ReActInput处理器在System Prompt中植入ReAct的格式要求并在解析LLM输出时区分Thought和Action部分。4.3 agent-webSpring Boot一体化Web服务agent-web模块是前面所有能力的集大成者。它基于s08的全部功能并集成了s14的ReAct模式然后用Spring Boot包装成一个完整的Web服务。这提供了一个生产级Agent应用的原型包含以下关键部分RESTful API提供/chat接口处理用户对话支持多轮会话。会话管理利用Spring Session或数据库为每个用户或对话线程维护独立的对话上下文。持久化层使用MyBatis-Plus将对话历史、任务状态、技能库等数据存入MySQL。可配置的技能与工具库通过数据库管理技能和工具配置实现动态加载。一个典型的Web请求处理流程用户通过HTTP POST发送消息到/api/chat。AgentChatController根据会话ID从数据库加载历史上下文。创建或获取一个WebZQAgent实例它继承了所有核心能力。将用户消息和历史上下文传入Agent循环。Agent与LLM交互可能调用工具、访问数据库。将最终回复和更新的上下文保存回数据库并通过HTTP响应返回给用户。这个模块展示了如何将一个实验性的、命令行驱动的Agent工程化为一个可扩展、可维护、支持多用户并发访问的在线服务。5. 实战部署与常见问题排查5.1 环境配置与快速启动要让项目跑起来你需要准备好三样东西Java 17、Maven 3.8、以及一个可用的AI模型API密钥。第一步克隆与编译git clone https://github.com/HOPPINZQ/hoppinai-agent.git cd hoppinai-agent # 编译所有模块 mvn clean compile第二步关键配置所有模块的API配置都集中在hoppinzq-core模块下的AIConstants.java文件中。你需要根据自己使用的模型服务商来修改它。// 示例使用DeepSeek的Anthropic兼容端点 public class AIConstants { // 必填API的基础地址 public static final String BASE_URL https://api.deepseek.com/anthropic; // 必填你的API Key public static final String API_KEY sk-your-deepseek-api-key-here; // 必填模型名称 public static final String MODEL deepseek-chat; // 可选设置超时、代理等 public static final Duration TIMEOUT Duration.ofSeconds(60); }如果你使用智谱GLM只需将BASE_URL和MODEL替换为智谱对应的值即可代码无需任何改动。第三步运行体验# 运行最基础的s01模块体验Bash工具调用 mvn exec:java -pl hoppinzq-module-agent-01 -Dexec.mainClasscom.hoppinzq.agent.Agent01 # 运行完整的Web Demo mvn spring-boot:run -pl hoppinzq-module-agent-web运行后访问http://localhost:8080即可与Web版的Agent进行交互。5.2 常见问题与解决方案速查表在实际搭建和运行过程中你可能会遇到以下典型问题。这里我把自己踩过的坑和解决方案整理出来。问题现象可能原因排查步骤与解决方案运行后无反应或立即退出1. API Key或BASE_URL配置错误。2. 网络问题导致无法连接API端点。1.检查配置确认AIConstants.java中的API_KEY和BASE_URL无误。对于国内用户确保BASE_URL是可访问的国内代理或兼容端点。2.测试连接使用curl命令或Postman手动调用一次配置的API地址和Key看是否能返回正确响应或认证错误。控制台报错ToolExecutionException工具方法执行时抛出未处理的异常。1.查看详细日志日志会打印出具体是哪个工具调用失败以及异常堆栈。2.检查工具逻辑重点检查文件操作工具的路径权限、Bash命令的环境依赖等。确保所有工具方法都有完善的try-catch并返回友好错误信息。Agent陷入无限循环不断调用同一个工具1. 工具执行结果未能让LLM理解任务已完成。2. 工具描述不清导致LLM误解。1.优化工具输出工具执行成功后返回的信息应具有“终结性”。例如搜索文件后返回“未找到匹配内容”比返回空字符串更好这能明确告知LLM搜索动作已结束且无结果。2.审查工具描述检查Tool注解中的description确保它清晰指明了工具的用途和边界。上下文很快耗尽提示Token超限对话历史过长未有效压缩。1.启用压缩确保在s06及之后的模块中上下文压缩功能是开启的。2.调整压缩策略在ContextCompactor中调整autoCompactionThreshold自动压缩阈值使其在上下文达到模型限制的70%-80%时触发。3.简化System Prompt检查是否在System Prompt中注入了过多不必要的指令或技能描述。Web模块启动失败数据库连接错误application.yml中数据库配置不正确或MySQL服务未启动。1.检查配置打开agent-web模块的src/main/resources/application.yml检查datasource.url,username,password是否正确。2.初始化数据库项目可能需要执行特定的SQL脚本来建表。查看模块的README或resources/db目录下的SQL文件。3.简化启动初次体验可先注释掉数据库相关配置将对话历史存储在内存中快速验证Web服务是否正常。MCP模块连接失败MCP服务器未启动或传输方式STDIO/SSE/HTTP配置错误。1.先启动服务器确保你已经按照MCP服务器如sqlite-mcp-server的文档正确启动它。2.核对配置检查McpSetting类中的连接配置如进程命令、SSE URL等是否与服务器匹配。3.查看日志MCP客户端在初始化时会尝试连接并列出工具查看此阶段的日志是定位连接问题的关键。5.3 性能调优与扩展建议当项目跑通后你可能会考虑如何让它更快、更稳定、功能更强。1. 异步化工具调用目前工具调用是同步的如果某个工具如一个慢速的网络请求执行时间很长会阻塞整个Agent线程。可以考虑将工具执行器改为异步模式使用CompletableFuture。当LLM请求调用工具时立即返回一个“任务已接收”的中间状态然后通过WebSocket或轮询的方式将最终结果推送给用户。2. 上下文管理的持久化与向量化对于agent-web目前的对话历史是存在数据库里的纯文本。当历史很长时每次检索和加载效率低。可以考虑向量化存储将每一轮对话的文本通过Embedding模型转换为向量存入如Milvus、Chroma等向量数据库。当需要压缩或总结时可以进行语义相似度检索只加载最相关的历史片段而不是全部。分级存储将最近的对话放在内存或Redis中将较久的历史归档到对象存储如S3或冷数据库中。3. 工具的动态热加载目前工具是在Agent启动时通过扫描注解静态注册的。在生产环境中你可能希望在不重启服务的情况下增加新工具。可以实现一个ToolRegistry中心支持通过HTTP API或配置文件动态注册/注销工具定义并由ZQAgent在运行时刷新其工具列表。4. 多模型路由与降级不要只依赖一个模型服务。可以封装一个ModelRouter根据请求的类型代码生成、文案创作、逻辑推理、预算、当前各服务的延迟和可用性智能地将请求路由到最合适的模型如Claude、GLM、DeepSeek。当主服务不可用时自动降级到备用服务。这个项目提供了一个坚实而透明的起点。它的价值不在于提供了多少开箱即用的功能而在于清晰地揭示了一个现代AI Agent框架的每一块骨骼和肌肉是如何生长和协同工作的。你可以把它当作一个超级模板在此基础上根据你自己的业务需求去构建那个独一无二的、智能的“数字员工”。

更多文章