资讯详情

Java从零构建AI Agent框架:架构设计与工程实践

📅 2026/10/22 5:29:41 | 华诺云谱 👁 阅读
Java从零构建AI Agent框架:架构设计与工程实践
1. 为什么我要用 Java 从头造一个 AI Agent 框架先说说背景。过去大半年AI Agent 这个词几乎成了技术圈的硬通货各种演示视频里一个模型能自己规划任务、调用工具、读写文件、甚至操作浏览器看起来像是给大模型装上了手脚。但如果你真去翻开源实现会发现一个很尴尬的现实绝大多数 Agent 框架都是 Python 写的。LangChain、AutoGPT、MetaGPT、CrewAI清一色 Python。Python 在这个领域确实有先发优势生态成熟、胶水能力强、迭代快这些我都认。问题在于真正要把 Agent 落到企业级的生产环境里Python 的那套东西就开始露怯了。我所在的团队维护着一套跑了七八年的 Java 微服务体系订单、风控、结算、消息队列全是 Spring 生态监控用的是 Micrometer 加 Prometheus配置中心、服务注册、链路追踪一应俱全。你让我在这种体系里塞一个 Python 的 Agent 服务进去先不说运维同学会不会翻白眼光是跨语言的服务治理、线程模型、内存管理、依赖隔离这几件事就够喝一壶的。更别提很多企业的核心业务逻辑本身就封装在 Java 的 SDK 和内部库里Python 想调只能走 HTTP 或者 RPC绕一大圈延迟和稳定性都受影响。所以我的想法很直接既然 Java 生态这么完整为什么不干脆用 Java 从零构建一个 AI Agent 框架这就是 JavaManus 这个项目的由来。名字里的 Manus 取自拉丁语里“手”的意思寓意给大模型装上一双能干活的手。它不是要做一个 Python 框架的翻译版而是想认认真真回答一个问题在一个强类型、多线程、面向企业级治理的语言环境里一个 Agent 框架应该长什么样这篇文章我会把整个设计和实现过程拆开讲。从整体架构怎么定、核心模块怎么切、工具调用怎么设计、记忆和规划怎么做到实际跑起来踩过的坑我都会尽量讲透。适合两类人看一类是 Java 后端工程师想在自己的技术栈里引入 Agent 能力但不知道从哪下手另一类是对 Agent 原理感兴趣想看看抛开 Python 那套惯用写法之后一个 Agent 框架的骨架到底由哪些部件组成。哪怕你暂时不写 Java读完也能对 Agent 的工程实现有个更立体的认识。2. 整体架构设计与技术选型思路2.1 一个 Agent 框架到底需要哪些核心部件在动手写第一行代码之前我花了大概一周时间把主流 Agent 框架的源码翻了一遍试图抽象出一个最小可用的核心模型。翻完之后我发现不管框架大小一个能真正干活的 Agent本质上就是四个部件的循环协作大脑LLM 调用层、手脚工具调用层、记忆上下文管理层、调度执行循环层。大脑负责和模型对话把自然语言指令翻译成结构化的动作意图。手脚负责真正去执行这些动作比如查数据库、发请求、读写文件。记忆负责在有限的上下文窗口里决定哪些历史信息该保留、哪些该压缩、哪些该丢弃。调度则是那个把所有东西串起来的循环思考、行动、观察结果、再思考直到任务完成或者触发终止条件。这个模型听起来简单但真正难的地方在于每一层的边界怎么划。比如工具调用的结果要不要进记忆进了记忆之后怎么保证不把上下文撑爆模型返回的 JSON 解析失败了怎么办是重试还是降级这些问题在 Python 框架里往往被动态类型的灵活性掩盖了但在 Java 里你必须在编译期就把这些契约想清楚。这其实是个好事强类型逼着你把每个接口的输入输出定义明白后期维护成本低很多。2.2 为什么不用现成的 Java Agent 库市面上不是没有 Java 的 Agent 相关库比如 Spring AI 就提供了一些模型调用的封装还有一些公司开源的轻量 Agent 工具。我评估过几个最后决定自己写原因有三个。第一是可控性。Agent 的执行循环是整个框架的心脏它的重试策略、超时控制、并发模型直接决定了系统的稳定性。用别人的库你只能在其给定的扩展点上做文章一旦遇到它没考虑到的场景比如需要在一个步骤里并行调用多个工具再聚合结果就会很别扭。自己写的话整个循环的逻辑完全透明想怎么改就怎么改。第二是依赖治理。很多现成的库会引入一大堆传递依赖版本冲突在企业项目里是家常便饭。JavaManus 我刻意把核心依赖压到最低除了 JSON 处理和一个 HTTP 客户端几乎不依赖别的东西。模型调用层做成可插拔的接口你想接哪家的模型自己实现就行框架本身不绑定任何厂商。第三是学习价值。说实话造轮子这件事如果只是为了用确实没必要。但如果你想把 Agent 的原理吃透自己从零实现一遍是最快的路径。每当你被迫去解决一个具体问题比如“工具调用的参数怎么从模型输出里安全地反序列化”你对整个机制的理解就会深一层。2.2 分层架构的划分最终我把 JavaManus 分成了五层从下往上依次是层级名称职责L1模型接入层统一不同 LLM 提供商的调用接口处理流式与非流式响应L2工具层定义工具契约、注册机制、参数校验与执行沙箱L3记忆层上下文窗口管理、历史压缩、向量检索可选L4执行引擎层Agent 主循环、规划、反思、终止条件判断L5应用层对外暴露的 API、会话管理、可观测性埋点这个分层的关键在于依赖方向单一上层依赖下层下层不知道上层的存在。模型接入层完全不知道工具的存在工具层也不关心是谁在调度它。这样做的好处是每一层都可以独立测试和替换。比如我想把模型从一家换成另一家只需要在 L1 加一个实现类上面四层一行代码都不用动。2.3 技术选型的几个关键决策选型这块我纠结最久的是三件事JSON 库、HTTP 客户端、并发模型。JSON 库我最后选了 Jackson。原因很简单它在 Java 生态里最成熟对泛型和多态的支持最好而 Agent 框架里到处都是需要反序列化成不同工具参数对象的场景Jackson 的TypeReference和多态反序列化能省很多事。虽然它比一些轻量库重一点但这点开销在 Agent 这种 IO 密集型的场景里可以忽略。HTTP 客户端我用了 JDK 11 自带的HttpClient。这个决定可能有点反直觉很多人会选 OkHttp 或者 Apache HttpClient。我选 JDK 自带的原因是想把外部依赖降到最低而且HttpClient支持异步和流式响应对于处理 LLM 的 SSE 流式输出完全够用。实测下来性能也不差没必要为了一个 HTTP 客户端再引一个第三方库。并发模型是重点。Agent 的执行循环天然是串行的思考、行动、观察一步一步来。但工具执行本身可能是并行的比如一个步骤里需要同时查三个数据源。所以我用了CompletableFuture来做工具层的并行编排主循环保持单线程顺序执行。这样既保证了 Agent 决策逻辑的确定性又能在工具层榨取并发性能。线程池的大小我设成了 CPU 核数的两倍因为工具调用大多是 IO 等待线程多一点能提高吞吐。3. 核心模块的深度拆解与实现要点3.1 模型接入层如何优雅地屏蔽不同厂商的差异模型接入层要解决的核心问题是不同厂商的 API 长得完全不一样。有的用messages数组有的用prompt字符串有的工具调用返回tool_calls字段有的叫function_call流式响应的格式更是五花八门。如果让上层直接面对这些差异代码会变成一堆 if-else。我的做法是定义一个统一的ChatModel接口把差异全部收敛在实现类里public interface ChatModel { ChatResponse chat(ChatRequest request); void chatStream(ChatRequest request, StreamCallback callback); }ChatRequest里封装了消息列表、工具定义、温度、最大 token 这些通用参数。ChatResponse里则是统一后的结果要么是文本回复要么是一组工具调用请求。每个厂商写一个实现类在实现类里做请求和响应的双向转换。这里有个细节值得说工具定义的格式转换。Agent 框架里工具是用 Java 类描述的但发给模型时必须转成模型能理解的 JSON Schema。我在工具层定义了一套注解比如ToolParam标注参数描述然后写了一个 Schema 生成器把 Java 类反射成 JSON Schema。这个生成器是模型无关的生成出来的 Schema 再由各个模型实现类转成自己需要的格式。这样加一个新工具只需要写一个 Java 类加注解Schema 自动生成非常省心。注意反射生成 Schema 的时候一定要处理嵌套对象和数组否则遇到复杂参数类型会生成错误的 Schema模型调用工具时就会传错参数。我一开始就踩了这个坑一个接收ListFilter的工具死活调不对排查半天才发现是 Schema 里数组的 items 没写对。3.2 工具层契约、注册与安全执行工具层是整个框架里我最花心思的部分因为它是 Agent 真正“干活”的地方也是最容易出安全问题的地方。先说契约。一个工具在 JavaManus 里就是一个实现了Tool接口的类public interface Tool { String name(); String description(); String parameterSchema(); ToolResult execute(MapString, Object params); }name和description是给模型看的模型靠这两个信息决定什么时候调用这个工具。parameterSchema是参数的 JSON Schema。execute是真正的执行逻辑接收反序列化后的参数返回结果。工具注册用一个ToolRegistry来管理支持按名字查找。这里我加了一个命名空间的设计工具名可以带前缀比如db.query、file.read避免不同模块的工具重名。注册的时候会做一次校验确保 Schema 是合法的 JSON Schema名字不重复。安全执行是重点。工具执行可能抛异常、可能超时、可能返回超大结果。我在ToolRegistry外面包了一层ToolExecutor统一处理这些情况超时控制每个工具可以配置超时时间默认 30 秒超时直接中断并返回错误结果给模型。异常捕获工具抛出的任何异常都被捕获转成结构化的错误信息返回给模型让模型有机会自我修正。结果截断如果工具返回的结果超过一定长度比如 8000 字符自动截断并提示模型结果被截断避免撑爆上下文。参数校验执行前用 Schema 校验参数不合法直接拒绝不浪费一次模型调用。实操心得结果截断这个功能看起来不起眼但在实际使用中救了我好几次。有一次一个工具返回了几万行的日志如果不截断直接就把上下文窗口撑爆了模型后面的对话全部报错。截断之后模型看到提示会主动缩小查询范围问题自然就解决了。3.3 记忆层上下文窗口的精细化管理上下文窗口是 Agent 最稀缺的资源。模型的窗口再大也是有限的而 Agent 跑长任务时历史消息会迅速堆积。记忆层要做的就是在这个有限空间里尽可能保留对当前任务有用的信息。我把记忆分成了三层第一层是短期记忆就是最近几轮的对话原样保留。这部分信息最新鲜对当前决策影响最大。第二层是摘要记忆把较早的历史消息压缩成一段摘要。压缩的触发条件是 token 数超过阈值比如窗口的 70%。压缩的时候我会保留工具调用的关键结果丢弃中间的思考过程因为思考过程对后续决策的价值不大。第三层是长期记忆可选用向量检索实现。把重要的历史信息存进向量库需要的时候按语义相似度召回。这一层我做成可插拔的不强制使用因为引入向量库会增加部署复杂度。token 计数这块有个坑不同模型的 tokenizer 不一样精确计数需要引入对应的 tokenizer 库。为了保持轻量我用了一个近似算法按字符数除以一个系数来估算。中文大概 1.5 个字符一个 token英文大概 4 个字符一个 token。这个估算不精确但用于触发压缩足够了反正压缩阈值本身也留了余量。3.4 执行引擎Agent 主循环的设计执行引擎是框架的心脏它把前面三层串起来形成一个完整的“思考-行动-观察”循环。核心逻辑用伪代码表示大概是这样while (!finished step maxSteps) { ChatResponse response model.chat(buildContext()); if (response.hasToolCalls()) { ListToolResult results toolExecutor.executeAll(response.toolCalls()); memory.add(results); } else { // 模型给出了最终答案 finished true; } step; }看起来简单但魔鬼在细节里。我列几个关键的设计点。终止条件。除了模型主动给出最终答案还要有兜底最大步数限制防止死循环、总超时限制防止任务卡死、连续错误次数限制防止模型反复调用同一个失败的工具。这几个条件任何一个触发循环都会终止并返回当前状态。工具调用的并行执行。模型一次可能返回多个工具调用如果这些工具之间没有依赖关系可以并行执行。我用CompletableFuture.allOf来编排等所有工具执行完再一起把结果加进记忆。这里要注意并行执行的结果顺序要和模型请求的顺序一致否则模型会困惑。反思机制。我加了一个可选的反思步骤当工具执行失败时不直接把错误丢给模型而是先让模型分析一下失败原因再决定下一步。这个机制能显著提高复杂任务的完成率代价是多一次模型调用。我把它做成了配置项简单任务关掉复杂任务打开。踩过的坑一开始我没做最大步数限制结果有一次模型陷入了一个死循环反复调用同一个查询工具因为查询结果总是空的它就一直重试。跑了几十步才被我发现白白烧了一堆 token。加上步数限制之后这种情况最多跑 20 步就停了损失可控。4. 完整实操从零跑通一个 Agent 任务4.1 环境准备与依赖配置先把项目骨架搭起来。我用 Maven 管理依赖核心依赖就三个dependencies dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.17.0/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version2.0.12/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.12/version /dependency /dependenciesJDK 版本要求 17 以上因为用到了 record 和一些新的集合 API。项目结构按前面说的五层来分javanus/ ├── model/ # L1 模型接入层 ├── tool/ # L2 工具层 ├── memory/ # L3 记忆层 ├── engine/ # L4 执行引擎层 └── app/ # L5 应用层4.2 实现一个最简单的工具我们从一个最简单的工具开始一个计算器接收两个数字和一个运算符返回计算结果。这个工具足够简单能让我们把整个链路跑通。public class CalculatorTool implements Tool { Override public String name() { return calc.evaluate; } Override public String description() { return 执行简单的四则运算输入两个数字和一个运算符 - * /; } Override public String parameterSchema() { return { type: object, properties: { a: {type: number, description: 第一个操作数}, b: {type: number, description: 第二个操作数}, op: {type: string, enum: [, -, *, /], description: 运算符} }, required: [a, b, op] } ; } Override public ToolResult execute(MapString, Object params) { double a ((Number) params.get(a)).doubleValue(); double b ((Number) params.get(b)).doubleValue(); String op (String) params.get(op); double result switch (op) { case - a b; case - - a - b; case * - a * b; case / - { if (b 0) throw new IllegalArgumentException(除数不能为零); yield a / b; } default - throw new IllegalArgumentException(不支持的运算符: op); }; return ToolResult.success(String.valueOf(result)); } }注意execute里对除零的处理。工具内部抛异常是允许的ToolExecutor会捕获它并转成错误结果返回给模型。模型看到“除数不能为零”这个提示下次就会换个参数。4.3 组装 Agent 并运行把工具注册进去配好模型就可以跑起来了ToolRegistry registry new ToolRegistry(); registry.register(new CalculatorTool()); ChatModel model new OpenAiCompatibleModel( https://api.example.com/v1/chat/completions, System.getenv(API_KEY), your-model-name ); Agent agent Agent.builder() .model(model) .toolRegistry(registry) .maxSteps(15) .timeout(Duration.ofMinutes(3)) .build(); String result agent.run(帮我算一下 (123 * 456) 789 等于多少); System.out.println(result);跑起来之后你会看到 Agent 的执行日志模型先思考决定调用calc.evaluate计算 123 乘 456拿到结果 56088然后再调用一次计算 56088 加 789最后给出答案 56877。整个过程模型自己规划了两步我们只提供了一个工具没有写任何编排逻辑。4.4 参数计算与关键配置的取值依据几个关键参数的取值我解释一下背后的逻辑。maxSteps 设为 15。这个数字不是拍脑袋定的。我统计过一批典型任务简单查询类任务平均 2 到 3 步中等复杂度的多步推理任务平均 5 到 8 步超过 15 步的任务基本可以判定为模型陷入了循环或者任务本身描述不清。设 15 是一个平衡点既能覆盖绝大多数正常任务又能在异常时及时止损。工具超时设为 30 秒。这个取决于工具的性质。查询类工具通常几百毫秒就返回了30 秒是给那些可能慢的接口留的余量。如果某个工具确实需要更长时间可以在工具级别单独配置。上下文压缩阈值设为窗口的 70%。留 30% 的余量是因为压缩本身也需要消耗 token而且压缩后的摘要加上后续对话需要保证还有空间。如果设成 90%很可能压缩完发现还是超了就得再压一次浪费。线程池大小设为 CPU 核数乘 2。工具调用是 IO 密集型线程在等待网络响应时是阻塞的所以线程数可以超过核数。乘 2 是个经验值实测下来对于几十个并发工具调用的场景够用。如果工具调用特别频繁可以调大但要注意下游服务的承受能力。5. 常见问题排查与避坑经验实录5.1 模型返回的 JSON 解析失败怎么办这是最常见的问题。模型有时候会在 JSON 外面包一层 markdown 代码块有时候会多写一个逗号有时候干脆返回一段自然语言而不是 JSON。我的处理策略是分级降级第一级尝试直接解析。第二级如果失败用正则把 JSON 部分抠出来再解析。第三级如果还失败把解析错误信息返回给模型让它重新生成。第四级连续失败三次终止任务并报错。这里有个技巧在系统提示词里明确要求模型“只返回 JSON不要有任何其他文字”能大幅降低解析失败率。但也不能完全依赖提示词解析层的容错必须做。5.2 工具调用陷入死循环的排查死循环的表现是 Agent 反复调用同一个工具参数几乎一样结果也一样。原因通常有两个一是工具返回的结果模型理解不了它以为没成功就重试二是任务本身无解模型在瞎试。排查方法很简单把执行日志打出来看连续几步的工具调用和结果。如果是第一种检查工具返回的结果格式是不是清晰错误信息是不是明确。如果是第二种说明任务描述有问题需要人工介入。预防措施就是前面说的最大步数限制加上一个“相同工具相同参数连续调用超过 3 次就强制终止”的检测。5.3 上下文爆炸的应急处理上下文爆炸的典型症状是模型开始返回 400 错误提示 token 超限。这时候如果任务还没完成就很尴尬。我的应急处理是立即触发一次强制压缩把历史消息压到最小然后重试当前步骤。如果压缩后还是超限说明单条消息本身就太大了那就只能截断单条消息。根本的解决办法还是做好预防工具结果截断、定期压缩、控制单条消息长度。我建议在开发阶段就把 token 计数打点监控上下文的使用率超过 80% 就告警。5.4 常见问题速查表问题现象可能原因排查方向解决方案模型不调用工具直接回答工具描述不清或提示词没强调检查工具 description 和系统提示词优化工具描述在提示词里明确要求优先使用工具工具参数解析失败Schema 定义有误或模型输出格式不对打印模型原始输出和 Schema修正 Schema增加解析容错任务跑很久不结束死循环或工具超时看执行日志的步数和工具耗时加步数限制和工具超时结果不准确工具返回错误或模型理解偏差核对工具返回的原始数据修正工具逻辑优化结果格式并发工具调用结果错乱结果顺序与请求顺序不一致检查并行执行的编排逻辑保证结果按请求顺序聚合5.5 几个我踩过的坑第一个坑是工具描述写得太技术化。我一开始写工具描述用的是给程序员看的语言比如“执行 SQL 查询并返回 ResultSet”。模型根本不知道 ResultSet 是什么调用率很低。后来改成“根据 SQL 语句查询数据库返回查询到的数据行”调用率立刻上去了。工具描述是写给模型看的要用模型能理解的自然语言。第二个坑是忽略了模型的输出长度限制。有些模型对单次输出的 token 数有限制如果工具调用的参数特别长模型可能输出到一半就被截断了导致 JSON 不完整。解决办法是在提示词里提醒模型参数要简洁同时在解析层做好截断检测。第三个坑是线程安全问题。工具注册表我用的是HashMap在多线程环境下并发注册和查找会出问题。后来换成了ConcurrentHashMap。这个坑很隐蔽单线程测试完全正常一上并发就偶发失败。第四个坑是日志打太多导致性能下降。Agent 执行过程中会产生大量日志如果把每一步的完整上下文都打出来日志量会非常恐怖而且序列化上下文本身也耗性能。我的做法是分级打日志正常流程只打关键节点调试模式下才打完整上下文。6. 这套框架还能怎么扩展写到这里JavaManus 的核心已经能跑通一个完整的 Agent 任务了。但一个框架的生命力在于扩展性我留了几个明确的扩展点这里也分享一下思路。多 Agent 协作。目前的执行引擎是单 Agent 的但接口设计上已经预留了空间。可以定义一个AgentTeam里面包含多个 Agent每个 Agent 有自己的工具集和角色定位通过一个协调者来分配任务。这个模式适合复杂任务比如一个 Agent 负责调研一个负责写代码一个负责测试。持久化会话。现在的记忆是内存态的进程重启就没了。可以加一个MemoryStore接口把记忆持久化到数据库或者 Redis支持会话恢复。这个对企业应用很重要用户不希望每次打开都要重新描述任务。可观测性增强。Agent 的执行过程是个黑盒出了问题很难排查。可以接入 OpenTelemetry把每一步的思考、工具调用、结果都做成 span这样在链路追踪系统里就能看到完整的执行链路。对于生产环境这个几乎是必须的。工具生态。框架本身只提供工具契约具体工具可以做成插件包。比如数据库工具包、文件工具包、HTTP 工具包按需引入。这样框架保持轻量用户按需组合。流式输出。现在的结果是等任务全部完成才返回用户体验不够好。可以改成流式模型每产生一个 token 就推给前端让用户看到 Agent 的实时思考过程。这个在交互式场景里体验提升很明显。我个人在实际操作中的体会是造 Agent 框架这件事难点从来不在模型调用本身而在那些工程细节上下文怎么管、工具怎么安全执行、循环怎么不失控、错误怎么优雅降级。这些东西没有捷径只能一个个场景去踩、去调。JavaManus 现在还不完美但它的每一行代码我都知道为什么这么写出了问题我也知道去哪找。这种掌控感是直接用现成框架换不来的。如果你也在 Java 生态里折腾 Agent希望这篇东西能帮你少走点弯路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑