1. 为什么我要用 Java 从头造一个 AI Agent 框架1.1 一个真实的需求场景去年下半年我所在的小团队接了一个内部工具项目核心诉求是让系统能自动完成一系列操作读取本地文档、调用几个内部接口、把结果整理成结构化报告中间还要根据上一步的结果动态决定下一步做什么。说白了就是需要一个能自己思考、自己动手的智能体。一开始我们想得很简单市面上开源的 Agent 框架那么多随便挑一个用不就行了。结果真正上手才发现大部分成熟框架都是 Python 生态的而我们的后端服务、内部 SDK、权限体系全是 Java 写的。如果硬要引入 Python 那一套就得额外维护一套运行时、一套依赖、一套部署流程运维成本直接翻倍。更麻烦的是团队里没人愿意长期维护一个和主技术栈割裂的模块。于是我做了一个决定用 Java 从零搭一个轻量的 AI Agent 框架。这就是 JavaManus 的由来。它不是要做一个大而全的平台而是解决一个具体问题——让 Java 后端团队能用自己熟悉的技术栈快速构建出具备任务规划、工具调用、多轮推理能力的智能体。这篇文章我会把整个设计和实现过程拆开讲清楚包括我为什么这么设计、每一步怎么落地、踩过哪些坑。如果你也是 Java 背景想在自己的项目里加一点智能能力又不想被 Python 生态绑架那这篇内容应该对你有用。1.2 JavaManus 到底是个什么东西先把定位说清楚。JavaManus 是一个基于 Java 实现的 AI Agent 编排框架核心能力包括四块任务规划把一个模糊的用户目标拆解成可执行的步骤序列工具调用让模型能够调用外部函数、接口、数据库查询等记忆管理维护对话上下文和任务执行过程中的中间状态循环推理根据每一步的执行结果动态调整后续动作它不绑定任何特定的大模型厂商通过统一的接口适配层对接不同的模型服务。工具层也是插件化的你写一个符合规范的 Java 类注册进去就能被 Agent 调用。适合谁来参考我认为是三类人一是 Java 后端工程师想在现有系统里嵌入 Agent 能力二是技术负责人在评估自研还是引入现成方案三是对 Agent 底层原理好奇的开发者想通过一个可读的代码库理解 Agent 到底是怎么跑起来的。2. 整体架构设计与技术选型思路2.1 为什么不用现成的 Python 框架这个问题我被问过很多次。LangChain、AutoGPT 这些框架确实成熟功能也全但放到 Java 团队的实际场景里有几个绕不过去的坎。第一是部署一致性。我们的服务跑在统一的 JVM 环境里监控、日志、链路追踪都是围绕 Java 生态建的。引入 Python 运行时意味着要单独做一套可观测性出问题时排查链路会断掉。第二是类型安全。Agent 的核心是工具调用工具的参数和返回值需要严格约束。Java 的强类型系统在这里反而是优势能在编译期就发现很多问题而 Python 的动态特性容易在运行时才暴露错误。第三是团队能力匹配。让一个写了五年 Java 的工程师去维护 Python 代码短期能应付长期一定是负债。技术选型要考虑维护成本不是哪个火就用哪个。当然Python 生态在模型接入、Prompt 工程方面的积累确实更丰富。所以我的策略是核心编排逻辑用 Java 写模型调用通过 HTTP 接口对接不依赖特定语言的 SDK。这样既保住了 Java 的工程优势又能用上各家模型的能力。2.2 核心模块划分整个框架我拆成了五个核心模块每个模块职责单一通过接口解耦模块名称核心职责关键接口模型适配层统一不同模型服务的调用方式LlmClient工具注册中心管理所有可被调用的工具ToolRegistry记忆存储层维护对话历史和任务状态MemoryStore规划执行器驱动 Agent 的推理循环AgentExecutor提示词管理组装和管理各类 Prompt 模板PromptTemplate这么拆的好处是每一层都可以独立替换。比如你今天用 A 模型明天想换成 B 模型只需要实现一个新的LlmClient其他代码一行不用改。工具层同理加一个新工具就是写一个类然后注册不影响已有逻辑。2.3 一次 Agent 执行的完整链路为了让你有个整体印象我先描述一次典型的执行流程用户输入一个目标比如帮我分析这份销售数据并生成摘要AgentExecutor接收目标调用LlmClient让模型做任务规划模型返回一个步骤列表比如读取文件 → 解析数据 → 调用分析工具 → 生成摘要执行器逐步执行每一步如果需要调用工具就从ToolRegistry找到对应工具并执行每一步的结果写入MemoryStore作为下一步的上下文如果某一步失败或结果不符合预期执行器会把错误信息反馈给模型让模型重新规划所有步骤完成后汇总结果返回给用户这个循环看起来简单但里面有很多细节要处理比如循环次数限制、工具调用的参数校验、异常恢复等。后面我会逐个展开。3. 核心模块的详细实现3.1 模型适配层如何做到不绑定任何厂商模型适配层的设计目标只有一个让上层代码不感知具体用的是哪个模型。我定义了一个LlmClient接口public interface LlmClient { LlmResponse chat(LlmRequest request); String getModelName(); boolean supportsFunctionCall(); }LlmRequest里封装了消息列表、温度参数、最大 token 数等通用字段。LlmResponse则包含模型返回的文本内容、工具调用请求如果有、token 消耗统计等。针对不同厂商我写了不同的实现类。比如对接某云厂商的模型服务时需要把通用的LlmRequest转换成它特定的 JSON 格式再把返回结果映射回LlmResponse。这层转换逻辑全部封装在实现类内部上层完全无感。这里有个设计决策值得说一下我没有用任何厂商的官方 SDK而是直接发 HTTP 请求。原因是 SDK 会引入额外的依赖而且版本更新频繁容易和项目里的其他依赖冲突。直接发 HTTP 虽然要多写一点代码但可控性最强出问题也容易排查。实操心得如果你对接的模型服务支持 OpenAI 兼容格式那适配工作会简单很多因为大部分厂商都在往这个格式靠。我建议优先选支持这个格式的服务能省不少事。3.2 工具注册中心让模型长出手脚工具是 Agent 能力的延伸。没有工具模型只能聊天有了工具它才能读文件、查数据库、调接口。工具的定义我用了注解加接口的方式public interface AgentTool { String getName(); String getDescription(); ListToolParameter getParameters(); ToolResult execute(MapString, Object args); }getName是工具的唯一标识getDescription是给模型看的说明getParameters描述参数结构execute是实际执行逻辑。关键点在于getDescription和getParameters。这两个信息会被组装成模型能理解的工具描述模型根据这些描述来决定什么时候调用哪个工具、传什么参数。所以描述写得好不好直接决定工具被正确调用的概率。我踩过一个坑早期我把工具描述写得很简略比如查询用户信息结果模型经常在不该调用的时候调用它。后来我把描述改成根据用户ID查询用户的详细资料包括姓名、邮箱、注册时间。仅在需要用户具体信息时调用不要用于模糊搜索调用准确率明显提升。工具注册中心本身就是一个 Map启动时扫描所有实现了AgentTool接口的 Bean注册进去。Spring 环境下用ApplicationContext.getBeansOfType一行就能搞定。3.3 记忆存储层上下文管理的艺术Agent 和普通对话机器人的最大区别在于它需要记住任务执行过程中的中间状态。比如第一步读取了文件第二步分析时需要用到文件内容这些都得存下来。我的设计是把记忆分成两类对话记忆用户和 Agent 之间的消息往来用于维持多轮对话的连贯性任务记忆当前任务的执行计划、每一步的结果、遇到的错误用于驱动推理循环对话记忆用滑动窗口管理超过一定条数就丢弃最早的避免 token 超限。任务记忆则在整个任务生命周期内保留任务结束后清理。存储介质我做了抽象默认用内存实现适合单机场景。如果需要持久化可以实现一个基于数据库的MemoryStore。这里不建议一上来就上 Redis 或数据库因为大部分 Agent 任务的生命周期很短内存足够用过早引入外部依赖反而增加复杂度。3.4 规划执行器Agent 的大脑这是整个框架最核心的部分。AgentExecutor负责驱动整个推理循环它的逻辑大致是这样的public AgentResult execute(String goal) { ListMessage messages new ArrayList(); messages.add(systemPrompt()); messages.add(userMessage(goal)); int step 0; while (step maxSteps) { LlmResponse response llmClient.chat(buildRequest(messages)); if (response.hasToolCall()) { ToolResult result executeTool(response.getToolCall()); messages.add(assistantMessage(response)); messages.add(toolMessage(result)); step; } else { return AgentResult.success(response.getContent()); } } return AgentResult.failure(超过最大步数限制); }看起来简单但实际实现时要处理很多边界情况。比如工具执行抛异常了怎么办模型返回的工具名不存在怎么办模型陷入死循环一直调用同一个工具怎么办我的处理策略是工具异常时把异常信息作为工具结果返回给模型让模型自己决定是重试还是换方案。工具名不存在时返回一个明确的错误提示引导模型使用正确的工具名。死循环检测则通过记录最近几次的工具调用如果发现重复模式就强制中断并提示模型。注意maxSteps这个参数很关键。设太小复杂任务跑不完设太大一旦模型陷入循环会消耗大量 token。我的经验值是 10 到 15 之间具体看任务复杂度调整。3.5 提示词管理被低估的关键环节很多人觉得 Prompt 就是写一段话没什么技术含量。但在 Agent 场景里Prompt 的质量直接决定系统的稳定性。我把 Prompt 分成三类管理系统提示词定义 Agent 的角色、能力边界、行为规范工具提示词描述可用工具及其调用方式任务提示词针对具体任务的引导系统提示词我写得比较详细明确告诉模型你是一个任务执行助手你需要通过调用工具来完成任务每次只能调用一个工具调用前要说明理由任务完成后要给出总结。这些约束能显著减少模型的随意行为。工具提示词则是动态生成的根据当前注册的工具列表组装。格式上我参考了主流模型服务推荐的结构确保兼容性。4. 完整实操从零跑通一个 Agent 任务4.1 环境准备与项目搭建先说环境要求。JDK 17 以上Maven 3.8 以上这是基础。模型服务方面你需要有一个可访问的模型接口拿到 API 地址和密钥。项目结构我建议这样组织javanus/ ├── javanus-core/ # 核心接口和抽象 ├── javanus-model/ # 模型适配实现 ├── javanus-tools/ # 内置工具集 ├── javanus-example/ # 示例代码 └── pom.xml分模块的好处是依赖清晰核心模块不依赖任何具体实现方便替换和测试。Maven 依赖方面核心模块只需要几个基础库HTTP 客户端用 Java 11 自带的HttpClient就够了JSON 处理用 Jackson日志用 SLF4J。不需要引入 Spring保持轻量。示例模块可以引入 Spring Boot 方便演示。4.2 实现一个自定义工具我拿一个实际用过的例子来讲一个读取本地文本文件并统计词频的工具。Component public class WordCountTool implements AgentTool { Override public String getName() { return word_count; } Override public String getDescription() { return 统计指定文本文件中的词频。输入文件路径返回出现次数最多的前10个词及其次数。仅支持纯文本文件。; } Override public ListToolParameter getParameters() { return List.of( new ToolParameter(filePath, string, 文件的绝对路径, true) ); } Override public ToolResult execute(MapString, Object args) { String filePath (String) args.get(filePath); try { String content Files.readString(Path.of(filePath)); MapString, Long counts Arrays.stream(content.split(\\s)) .filter(s - !s.isBlank()) .collect(Collectors.groupingBy(s - s, Collectors.counting())); String result counts.entrySet().stream() .sorted(Map.Entry.String, LongcomparingByValue().reversed()) .limit(10) .map(e - e.getKey() : e.getValue()) .collect(Collectors.joining(\n)); return ToolResult.success(result); } catch (Exception e) { return ToolResult.failure(读取文件失败: e.getMessage()); } } }几个细节值得注意。参数定义里我标了requiredtrue框架会在调用前校验必填参数避免模型漏传。异常处理上我没有直接抛异常而是返回ToolResult.failure这样错误信息能作为工具结果反馈给模型让它有机会自我修正。4.3 组装并运行 Agent把模型客户端、工具、执行器组装起来public class AgentBootstrap { public static void main(String[] args) { LlmClient llmClient new OpenAiCompatibleClient( https://your-model-endpoint/v1/chat/completions, your-api-key, your-model-name ); ToolRegistry registry new ToolRegistry(); registry.register(new WordCountTool()); registry.register(new FileReadTool()); AgentExecutor executor AgentExecutor.builder() .llmClient(llmClient) .toolRegistry(registry) .memoryStore(new InMemoryStore()) .maxSteps(12) .build(); AgentResult result executor.execute( 请统计 /data/sample.txt 文件的词频并告诉我出现最多的三个词 ); System.out.println(result.getContent()); } }运行后你会看到控制台打印出模型的推理过程它先识别出需要调用word_count工具传入文件路径拿到结果后再组织语言回复用户。4.4 参数调优的实操记录跑通之后我花了不少时间调参数。这里分享几个实测有效的配置参数推荐值说明temperature0.1-0.3Agent 任务需要稳定输出温度不宜高maxSteps10-15根据任务复杂度调整工具描述长度50-150字太短模型理解不清太长浪费 token记忆窗口20条消息超过后丢弃最早的temperature 这个参数特别说一下。普通聊天场景可以设高一点让回复更有创意但 Agent 场景下模型需要严格按计划执行温度高了容易跑偏。我实测下来 0.1 到 0.3 之间最稳。5. 常见问题排查与避坑指南5.1 模型不调用工具怎么办这是最常见的问题。模型收到任务后直接用自己的知识回答而不是调用工具。原因通常有三个一是工具描述不够清晰模型没理解这个工具是干什么的。解决办法是把描述写具体包含使用场景和限制条件。二是系统提示词没有强调必须使用工具。我会在系统提示词里明确写当任务需要外部数据或操作时必须调用相应工具不要凭猜测回答。三是模型本身对工具调用的支持不好。有些模型虽然接口上支持 function call但实际效果很差。这种情况只能换模型。5.2 工具调用参数错误模型传的参数类型不对、字段名拼错、必填项缺失这些都很常见。我的做法是在框架层做严格校验参数名必须匹配类型必须正确必填项不能为空。校验失败时把具体的错误信息返回给模型比如参数 filePath 是必填项你传的是 null模型看到后通常能自我修正。5.3 死循环与 token 消耗失控模型有时候会陷入循环反复调用同一个工具。我在执行器里加了一个检测机制记录最近 5 次工具调用的名称和参数如果发现完全重复的模式就中断执行并返回提示。另外token 消耗要有监控。我在LlmResponse里记录了每次调用的 token 数执行结束后汇总输出。这样你能清楚知道每个任务花了多少成本。5.4 常见问题速查表问题现象可能原因解决方向模型不调用工具描述不清/提示词缺失优化描述强化系统提示参数传递错误模型理解偏差加强校验返回明确错误执行超时步骤过多/模型慢调整 maxSteps换更快的模型结果不稳定温度过高降低 temperature内存溢出记忆未清理检查 MemoryStore 生命周期避坑提示不要指望一次就把 Prompt 调到完美。我的做法是准备一组测试用例每次改完 Prompt 都跑一遍看通过率有没有提升。这个过程很像调参需要耐心。6. 我对这套框架的一些个人体会写到这里核心内容基本讲完了。最后分享几点我在实际使用中的真实感受。第一Agent 框架的难点不在代码在 Prompt 和工具设计。框架本身几百行就能跑起来但要让它在真实场景里稳定工作大量的时间花在调 Prompt、改工具描述、处理边界情况上。这部分没有捷径只能靠反复测试。第二不要追求大而全。我见过一些团队一上来就想做一个支持所有模型、所有工具的通用平台结果半年过去还在搭架子。我的建议是先跑通一个最小闭环解决一个具体问题然后再逐步扩展。JavaManus 到现在也只支持有限的几个工具但每个都经过实际验证。第三可观测性很重要。Agent 的执行过程是个黑盒出了问题很难排查。我在框架里加了详细的日志记录每一步的输入输出、模型返回、工具执行结果。这些日志在调试时帮了大忙。如果你也想用 Java 做类似的事情我的建议是从一个小场景开始比如自动整理文件、自动生成报告先跑通再说。框架的代码我会持续整理后续有机会再聊聊多 Agent 协作和更复杂的规划策略。