news 2026/10/14 7:13:35

Java 从零手写 AI Agent 框架:任务规划与工具调用实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 从零手写 AI Agent 框架:任务规划与工具调用实战

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 执行的完整链路

为了让你有个整体印象,我先描述一次典型的执行流程:

  1. 用户输入一个目标,比如"帮我分析这份销售数据并生成摘要"
  2. AgentExecutor接收目标,调用LlmClient让模型做任务规划
  3. 模型返回一个步骤列表,比如"读取文件 → 解析数据 → 调用分析工具 → 生成摘要"
  4. 执行器逐步执行,每一步如果需要调用工具,就从ToolRegistry找到对应工具并执行
  5. 每一步的结果写入MemoryStore,作为下一步的上下文
  6. 如果某一步失败或结果不符合预期,执行器会把错误信息反馈给模型,让模型重新规划
  7. 所有步骤完成后,汇总结果返回给用户

这个循环看起来简单,但里面有很多细节要处理,比如循环次数限制、工具调用的参数校验、异常恢复等。后面我会逐个展开。

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(); List<ToolParameter> getParameters(); ToolResult execute(Map<String, 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) { List<Message> 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 List<ToolParameter> getParameters() { return List.of( new ToolParameter("filePath", "string", "文件的绝对路径", true) ); } @Override public ToolResult execute(Map<String, Object> args) { String filePath = (String) args.get("filePath"); try { String content = Files.readString(Path.of(filePath)); Map<String, 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, Long>comparingByValue().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()); } } }

几个细节值得注意。参数定义里我标了required=true,框架会在调用前校验必填参数,避免模型漏传。异常处理上,我没有直接抛异常,而是返回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 协作和更复杂的规划策略。

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

从Excel到Python:数据分析迁移实战指南

如果你正在用 Excel 做数据分析&#xff0c;而且已经隐隐感觉到哪里不对劲——数据一多就卡、公式越写越长、同样的报表每周都要手工刷一次——那我建议你认真看完这篇内容。作为一个从 Excel 一路磕磕绊绊走到 Python 的数据分析从业者&#xff0c;我太理解这种“在表格里做分…

作者头像 李华
网站建设 2026/10/14 7:12:50

AI当设计师、AI画CAD、AI测Bug:开源工具落地专业工作流

1. 这不是“AI玩具”&#xff0c;是能进工作流的生产力切口最近在几个技术社群里&#xff0c;总有人发截图&#xff1a;一张手绘草图&#xff0c;30秒后变成带尺寸标注的CAD平面图&#xff1b;一段模糊的产品需求描述&#xff0c;直接生成可运行的Python测试脚本&#xff1b;甚…

作者头像 李华
网站建设 2026/10/14 7:12:41

C语言编译时断言实战:从宏实现到_Static_assert

写C代码的人&#xff0c;早晚会遇到“编译时断言”这个词。它的定位很朴素&#xff1a;把你在代码里那些“想当然”的假设&#xff0c;变成编译器不能无视的红线。比如你默认int是4字节、默认某个结构体没有填充字节、默认寄存器映射块正好12字节——这些假设一旦不成立&#x…

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

RocketMQ高可用架构实战:主从复制、故障切换与高级特性解析

做消息中间件这块这些年&#xff0c;我最大的一个感受是&#xff1a;RocketMQ 的高可用架构&#xff0c;绝对不等于把节点多部署几份就完了。很多人一上来就搭了个多 Broker 集群&#xff0c;以为某个节点挂了系统会自动切换、消息一条不丢&#xff0c;结果真遇到磁盘写满、主备…

作者头像 李华
网站建设 2026/10/14 7:12:18

Android TCP Socket连接失败排查:权限、明文策略与线程模型全攻略

你写了一个TCP客户端&#xff0c;在电脑模拟器里跑得飞起&#xff0c;换成真机却连不上服务器。Logcat里有时报Permission denied&#xff0c;有时报Cleartext HTTP traffic not permitted&#xff0c;还有时候什么都不报&#xff0c;就是一直卡在connect。这三个错误我全都踩过…

作者头像 李华
网站建设 2026/10/14 7:11:52

谷歌Gemini Agent通用智能体企业落地实战:技术拆解与踩坑指南

1. 从“给个目标就能干活”说起&#xff1a;通用智能体到底是个什么东西第一次看到“给个目标就能干活”这个说法&#xff0c;我脑子里蹦出来的不是兴奋&#xff0c;而是警惕。做了这么多年系统集成和自动化落地&#xff0c;我太清楚“全自动”这三个字背后藏着多少坑了。但这次…

作者头像 李华