从”能聊天”到”能干活”——Java AI Agent 的进化之路
在AI应用开发领域,2024-2025年见证了从简单对话到智能代理(Agent)的重大范式转变。当OpenClaw在GitHub上掀起热潮,Python、Rust、Go、C#等语言纷纷涌现出相关实现时,Java生态却长期处于空白状态。
直到最近,两款重量级Java AI开源项目横空出世:
- RuoYi-AI v3.0:历时半年打磨,实现了从”对话”到”行动”的关键跨越
- JavaClaw:JobRunr作者新作,填补了Java版OpenClaw的生态空白
本文将深入剖析这两个项目的设计理念、核心架构和实战案例,帮助Java开发者快速掌握AI Agent开发的最新实践。
1.0时代:套壳时代 —— 能说不能做
特征:简单套壳,单纯调用LLM做返回
用户输入 → LLM API → 文本返回 → 用户
这是大多数AI应用的起点。一个聊天框,一个API调用,就能提供”智能对话”能力。
局限性:
- ❌ 无知识:无法获取企业数据
- ❌ 无行动:只能”说”不能”做”
那时的开发者还在思考:AI到底能为企业做什么?
2.0时代:知识增强时代 —— 能看能连
特征:引入RAG + MCP,具备外部数据交互能力
用户输入 → 向量检索(RAG) → LLM + 知识库 → 返回增强结果
↓ MCP工具调用 → 外部数据源
核心能力:
- RAG(检索增强生成):让AI"看见"企业知识库
- MCP(模型上下文协议):让AI"连接"外部系统
新的困境:复杂工作流只能依靠流程编排,按固定节点执行:
用户请求 → 节点A → 节点B → 节点C → 输出 (固定) (固定) (固定)
这种方式的问题:
- ✅ 可控、可预测
- ❌ 僵化、无法适应变化
- ❌ 每个场景需要重新编排
- ❌ 无法处理意料之外的问题
关键认知:真正的智能不应该是"预设的路径",而应该是"自主的决策"。
3.0时代:智能体时代 —— ReAct范式完整实现
特征:思考→行动→观察循环,具备自主决策能力
这是ReAct(Reasoning + Acting)范式的完整实现:
┌─────────────────────────────────────────┐
│ ReAct 循环 │ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ Thought │───→│ Action │ │ │ │ (思考) │ │ (行动) │ │ │ └──────────┘ └────┬─────┘ │ │ ↑ │ │ │ │ ↓ │ │ ┌────┴────┐ ┌──────────┐ │ │ │ Final │ │Observation│ │ │ │ Answer │ │ (观察) │ │ │ └─────────┘ └──────────┘ │ └─────────────────────────────────────────┘
ReAct四步循环详解:
整体架构图
┌─────────────────────────────────────────────────────────┐ │ SupervisorAgent │ │ (任务协调器) │ ├─────────────────────────────────────────────────────────┤ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │ │SqlAgent │ │ChartAgent│ │SkillsAgent│ │WebAgent │ │ │ │(数据库) │ │(图表) │ │(技能) │ │(浏览器) │ │ │ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │ │ │ │ │ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌───▼────┐ │ │ │DB Tools │ │ECharts │ │MCP Tools│ │Playwright│ │ │ └─────────┘ └─────────┘ └─────────┘ └────────┘ │ └─────────────────────────────────────────────────────────┘
核心代码实现
public interface SqlAgent {
@SystemMessage(""" You are an intelligent database query assistant. CRITICAL REQUIREMENT: 1. Think about what information you need 2. Take action by calling appropriate tools 3. Observe the results 4. Repeat until you have enough information 5. Provide final answer """) @UserMessage("Answer: {{query}}") @Agent("Intelligent database query assistant...") String getData(@V("query") String query);
}
// 构建监督者 Agent - 管理多个子 Agent SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
.chatModel(plannerModel) .listener(new SupervisorStreamListener(null)) .subAgents(skillsAgent, searchAgent, sqlAgent, chartGenerationAgent, echartsAgent) // 加入历史上下文 - 使用 ChatMemoryProvider 提供持久化的聊天内存 .chatMemoryProvider(memoryId -> createChatMemory(chatRequest.getSessionId())) .responseStrategy(SupervisorResponseStrategy.LAST) .build();
支持的编排模式:
- 顺序工作流(Sequential workflow)
- 循环工作流(Loop workflow)
- 并行工作流(Parallel workflow)
- 条件工作流(Conditional workflow)
- 纯代理(Pure agents)
支持的输出模式:
public enum SupervisorResponseStrategy { / * 使用内部LLM对最后响应和交互摘要进行评分, * 返回得分较高的那个 */ SCORED, / * 返回监督者与子代理交互的摘要 */ SUMMARY, / * 仅返回最后调用的子代理的最终响应(默认) */ LAST
}
案例一:智能图表生成
用户请求:”查询chat_model表格返回厂商模型统计图表”
ReAct执行过程:
[Thought 1] 用户需要数据统计和图表,我先查询数据库 [Action 1] 调用 queryAllTables → 获取表列表 [Observation 1] 发现 chat_model 表存在
↓
[Thought 2] 需要了解表结构才能写SQL [Action 2] 调用 queryTableSchema("chat_model") [Observation 2] 获取到字段:vendor, model_type, model_name…
↓
[Thought 3] 现在可以查询统计数据了 [Action 3] 调用 executeSql("SELECT vendor, COUNT(*)…") [Observation 3] 查询结果:ppio厂商2个模型,对话/向量各1个
↓
[Thought 4] 数据已获取,生成图表配置 [Action 4] 调用 ChartGenerationAgent [Observation 4] 生成ECharts饼图JSON配置
↓
[Final Answer] 返回图表给用户
关键价值:Agent自主完成6步推理决策,无需人工编排流程,自动完成从数据查询到图表生成的全链路。
案例二:Skills创建Excel文件
用户请求:”帮我创建一个员工工资excel表格并且计算总额”
ReAct执行过程:
[Thought 1] 用户需要创建Excel,这是文档处理任务 [Action 1] 调用 SkillsAgent → activate_skill("xlsx") [Observation 1] xlsx技能已激活,可用openpyxl处理
↓
[Thought 2] 需要生成带公式的工资报表 [Action 2] 编写Python代码创建Excel [Observation 2] 文件已生成:sales_report.xlsx
↓
[Final Answer] 返回文件下载链接
核心代码实现:
public interface SkillsAgent {
@SystemMessage(""" 你是一个文档处理技能助手。 可用技能: docx: Word文档处理 pdf: PDF文档处理 xlsx: Excel电子表格处理 """) @UserMessage("{{query}}") @Agent("文档处理技能助手...") String process(@V("query") String query);
}
// ========== LangChain4j Skills 基本用法 ========== // 通过 SKILL.md 文件定义,LLM 按需通过 activate_skill 工具加载 Path skillsPath = Path.of(userDir, "ruoyi-admin/src/main/resources/skills"); List
ShellSkills skills = ShellSkills.from(skillsList);
// 构建子 Agent: SkillsAgent - 负责文档处理技能(docx、pdf、xlsx) SkillsAgent skillsAgent = AgenticServices.agentBuilder(SkillsAgent.class)
.chatModel(plannerModel) .systemMessage("You have access to the following skills:
"
+ skills.formatAvailableSkills() + "
When the user’s request relates to one of these skills, "
+ "activate it first using the `activate_skill` tool before proceeding.") .toolProvider(skills.toolProvider()) .build();
Skills系统特点:
- 通过SKILL.md文件自然语言定义技能
- 延迟加载优化Token消耗
- 约定优于配置,零代码扩展
⚠️ 安全警告:Shell执行本质上是不安全的。命令直接在主机进程环境中运行,无需沙箱化、容器化或权限限制。建议仅在完全信任输入的受控环境下使用,生产环境应考虑沙箱隔离。
案例三:浏览器自动化
用户请求:"访问 http://localhost:5666/ 登录系统 用户名:admin/admin123 打开模型管理 搜索baai/bge-m3 然后点击编辑按钮查看详情 并且输出"
ReAct执行过程:
[Thought 1] 需要访问指定网页并进行登录操作
[Action 1] 调用 browser_navigate("http://localhost:5666/") [Observation 1] 页面已加载,等待登录表单
↓
[Thought 2] 需要获取当前页面快照,识别登录元素 [Action 2] 调用 browser_snapshot [Observation 2] 页面结构已获取,发现用户名/密码输入框
↓
[Thought 3] 执行登录操作 [Action 3] 调用 browser_run_code 填写表单并提交 [Observation 3] 登录成功,进入系统主页
↓
[Thought 4] 导航到模型管理页面 [Action 4] 调用 browser_navigate 或点击菜单 [Observation 4] 已进入模型管理列表页
↓
[Thought 5] 搜索目标模型 [Action 5] 在搜索框输入 "baai/bge-m3" 并搜索 [Observation 5] 找到目标模型记录
↓
[Thought 6] 点击编辑按钮查看详情 [Action 6] 调用 browser_click 定位编辑按钮 [Observation 6] 详情页面已打开,获取并输出内容
↓
[Final Answer] 返回模型详情信息
技术实现:Playwright浏览器自动化插件提供了8个工具方法:
navigateTo:打开网页clickElement:点击元素fillInput:填写输入框getText:提取文本takeScreenshot:截图evaluateJavaScript:执行JSwaitForSelector:等待元素出现closeBrowser:关闭浏览器
实现细节:
- 返回的网页文本被截断到1万字符以内,防止撑爆上下文
- Playwright实例是懒初始化的,首次使用时才启动浏览器并自动安装Chromium
ReAct循环面临的挑战
单纯的”思考→行动→观察”循环在短任务中表现良好,但长任务面临挑战:
框架演进的长期目标
为了解决上述挑战,我们定义了框架长期发展的核心能力方向:
能力一:上下文管理
能力二:工具/技能调度体系
能力三:流程与约束管理
能力四:外部化状态管理
短期计划(近期待实现):
- 🎯 Agent市场:动态注册和发现新Agent
- 🧠 记忆增强:长期记忆 + 短期记忆分层
- 🖼️ 多模态Agent:图像理解、语音交互
长期愿景(未来探索):
- 🔄 Agent进化:基于反馈自动优化Prompt
- 🔍 自我反思:执行结果自评与纠错
- 📚 自主学习:从用户行为中学习新模式
总结:RuoYi-AI v3.0 实现了从”对话”到”行动”的关键跨越
核心原则:
- 渐进式演进:从简单场景开始迭代
- 单一职责:每个Agent专注一件事
- 工具驱动:通过Tools扩展能力边界
- 可观测:全链路追踪确保可控
OpenClaw刷屏之后,GitHub上冒出了接近6000个相关仓库,Python、Rust、Go、C#都有实现——唯独Java一直是空白。
直到JobRunr作者Ronald Dehuysser的新项目JavaClaw横空出世:基于Spring Boot + Spring AI + JobRunr的开源AI Agent框架,纯Java技术栈。
先说清楚:JavaClaw不是要取代OpenClaw。OpenClaw有25个传输通道、5400+技能、移动端应用,生态成熟度完全不在一个量级。JavaClaw更适合作为一个学习项目——代码量可控、架构清晰、用到的都是Java开发者熟悉的技术栈(Spring Boot、Advisor链、@Tool注解),非常适合用来理解一个AI Agent底层到底是怎么运转的。
在读源码之前,先交代一个有用的概念框架。Mitchell Hashimoto(HashiCorp联合创始人)提过一个说法叫Harness Engineering,核心公式是:
$\( ext{Agent} = ext{Model} + ext{Harness}\)$
你不是模型,那你就是Harness。
- Model是能力的来源
- Harness是模型之外的一切——系统提示词、工具调用、文件系统、编排逻辑、反馈回路、约束机制
模型本身只是”大脑”,只有Harness把状态、工具、反馈串起来,它才真正变成一个Agent。可以这么理解:模型是CPU,Harness是操作系统。CPU再强,OS拉胯也白搭。
用这个视角看JavaClaw,你会发现它本质上就是一套用Java/Spring Boot实现的Harness。下面我们逐层拆解。
JavaClaw的项目结构很清晰:
JavaClaw/ ├── base/ # 核心:Agent、任务、工具、频道、配置、记忆 ├── app/ # Spring Boot 入口、Onboarding 引导、Web 聊天频道 ├── plugins/ # 扩展插件:Telegram、Discord、Brave 搜索、Playwright 浏览器自动化 └── providers/ # LLM 提供商适配:OpenAI、Anthropic、Ollama、Google
四个模块各有明确的边界:
- base是核心逻辑
- app负责把base的能力暴露给用户
- plugins和providers则以可插拔模块的形式扩展通道、工具和模型能力
技术栈:Java 25 + Spring Boot 4.0.3 + Spring AI 2.0.0-SNAPSHOT + JobRunr 8.5.1
值得注意的技术选型:项目确实引入了Spring Modulith。不过从当前仓库能直接看到的内容看,它更像是在为模块化治理打基础,而不是已经把所有模块边界都用显式声明和验证测试铺满了。
这是全文最重要的部分。理解了Agent Loop,JavaClaw的其他模块就都通了。
Agent接口为什么只有两个方法
先看Agent接口的定义:
public interface Agent {
String respondTo(String conversationId, String question);
T prompt(String conversationId, String input, Class
result);
}
就这么点东西。
- 第一个方法处理对话——用户发消息,Agent回复文本
- 第二个方法处理结构化输出——给Agent一个输入,让它返回指定类型的Java对象
接口越薄,实现越灵活。DefaultAgent的实现本质上是Spring AI ChatClient的薄封装。
看起来简单,但重点在chatClient的构建配置里——那里藏着一整条Advisor链,正是这条链实现了Agent的核心能力。
ChatClient管道拆解
JavaClaw在构建ChatClient时注册了一条Advisor链:
chatClientBuilder
.defaultAdvisors(new SimpleLoggerAdvisor()) .defaultSystem(p -> p.text(agentPrompt) .param(ENVIRONMENT_INFO_KEY, AgentEnvironment.info())) .defaultToolCallbacks(mcpToolProvider.getToolCallbacks()) .defaultToolCallbacks(SkillsTool.builder() .addSkillsDirectory(skillsDir(workspace).toString()).build()) .defaultTools(taskTool, checkListTool, mcpTool, shellTools, fileSystemTools, smartWebFetchTool) .defaultAdvisors( ToolCallAdvisor.builder().build(), MessageChatMemoryAdvisor.builder(chatMemory).build());
这条链的执行顺序是关键:
SimpleLoggerAdvisor(最外层) ↓ 记录请求和响应日志,纯观察者角色
ToolCallAdvisor(中间层)
↓ 这就是 ReAct 循环的实现者
MessageChatMemoryAdvisor(最内层)
↓ 调用前加载历史消息,调用后保存本轮对话
一句话概括:
- MessageChatMemoryAdvisor管"记得住"
- ToolCallAdvisor管"干得了"
- SimpleLoggerAdvisor管"看得见"
ToolCallAdvisor如何驱动ReAct循环
ReAct(Reasoning and Acting)由Shunyu Yao等人于2022年在论文《ReAct: Synergizing Reasoning and Acting in Language Models》中提出,是当前AI Agent最主流的执行范式。思路很简单:
- 观察(Observe):把用户消息 + 历史对话 + 系统提示词 + 可用工具列表喂给LLM
- 思考(Think):LLM决定是直接回答,还是调用某个工具
- 行动(Act):如果LLM返回了工具调用请求,执行对应工具,把结果喂回LLM
- 循环:重复2-3,直到LLM认为不需要再调用工具,返回最终文本
在JavaClaw中,这个过程完全由Spring AI的ToolCallAdvisor驱动。你不需要自己写循环判断逻辑——ToolCallAdvisor拦截LLM的响应,如果包含tool call,就自动执行对应的@Tool方法,把结果拼回上下文,再调一次LLM。如此往复,直到LLM返回纯文本。
换句话说,JavaClaw的Agent循环没有手写while(true),靠的是Advisor链的拦截-转发机制。这是一个很"Spring"的做法——用声明式的管道代替命令式的循环。
ToolCallAdvisor内部做了几个关键操作:
- 通过
setInternalToolExecutionEnabled(false)禁用模型内置的工具执行,由Advisor自己接管整个工具调用流程 - 递归调用一条复制的子链(
callAdvisorChain.copy(this))来实现循环,直到响应中不再包含tool call - 支持
returnDirect机制——如果某个工具标记了returnDirect=true,Advisor直接把工具的返回值透传给调用方,跳过后续的模型处理
还有一个容易忽略的细节:Spring AI的ToolCallAdvisor支持两种对话历史模式。默认的conversationHistoryEnabled模式会在每次循环迭代中维护完整历史;如果配合了MessageChatMemoryAdvisor(JavaClaw就是这种情况),则会切换到disableMemory()模式,每次迭代只传递上一步的工具返回值,由Memory Advisor统一管理历史。避免同一条消息在Advisor链和Memory Advisor中被重复注入。
系统提示词的动态构建
系统提示词不是写死的。每次调用LLM时,系统提示由几部分动态拼接:
- AGENT.private.md:如果存在,优先作为私有系统提示词使用
- AGENT.md:当AGENT.private.md不存在时的回退提示词
- INFO.md:环境与工作区说明,拼接在Agent提示词后面
- AgentEnvironment.info():作为模板参数注入INFO.md里的{ENVIRONMENT_INFO}占位符
这意味着运行时环境信息不是简单粗暴地硬编码进系统提示,而是通过模板参数注入。这样Agent能知道"我现在在哪台机器上、几点了、工作目录和git状态如何",同时又保留了提示词文件本身的可编辑性。
Agent再强,也得有个入口接收用户消息、有个出口返回执行结果。这就是Channel系统的职责。
接口设计:两个方法解决所有问题
public interface Channel void sendMessage(String message);
}
- getName()用于标识频道
- sendMessage()用于向用户推送消息
就这么简单。一共有三个实现类:WebSocket Chat、Telegram、Discord。
你可能会问:接收消息的方法呢?
答案是:每个Channel的接收逻辑由各自的传输协议决定(WebSocket handler、Telegram long-polling、Discord listener),不需要统一接口。Channel接口只管”出站”——把Agent的回复推给用户。
事件驱动的消息分发
每个Channel在收到用户消息后,做的事情是一样的:
- 把消息包装成ChannelMessageReceivedEvent发布出去
- 调用agent.respondTo(conversationId, message)
- 把Agent的回复通过sendMessage()推回用户
ChannelRegistry是所有频道的注册中心。它做了两件事:管理所有Channel实例,以及追踪最近一次消息事件(这样后台任务完成后知道往哪个频道发通知)。
Chat频道的双模式设计
内置的Chat频道(Web UI)用了WebSocket + REST双模式:
- 优先走WebSocket实时推送,用户体验最好
- 如果WebSocket会话断了,自动降级到内存队列,前端通过REST轮询拉取
这个降级策略用ConcurrentLinkedQueue实现,简单但有效。不需要引入消息队列中间件,对个人助手场景来说足够了。
插件式扩展
Telegram、Discord这些频道都是插件,通过Spring Boot的@AutoConfiguration + @ConditionalOnProperty实现条件激活:
@AutoConfiguration @ConditionalOnProperty(prefix = "agent.channels.telegram",
name = {"token", "username"})
public class TelegramChannelAutoConfiguration { … }
配置了Telegram的token和username才会创建Telegram频道Bean,否则完全不加载。这种”配置即插拔”的方式,是Spring生态的标准玩法,也是JavaClaw扩展性好的原因之一。
Agent能对话是基本功,能管理任务才是从聊天机器人到Agent的质变。
为什么用文件而不是数据库
JavaClaw的任务持久化方案很”朴素”——直接写Markdown文件。每个任务是一个.md文件,带YAML frontmatter:
— task: 调研 Spring AI 最新版本特性 createdAt: 2026-04-10T10:00:00Z status: todo description: 阅读 Spring AI 2.0 release notes,总结关键变化 —
文件命名按日期分桶,格式大致是:workspace/tasks/yyyy-MM-dd/HHmmss-
这里要注意两点:
- 状态不在文件名里,而是放在YAML frontmatter中
- 任务名会经过sanitize,中文和空格等字符不会原样出现在最终文件名里
这个选择背后有明确的权衡:
对于个人AI助手来说,文件方案的优势很明显:你可以直接用文本编辑器查看、修改、甚至手动创建任务,Agent也能读写同样的文件。人和Agent共享同一份任务视图,这比任何数据库管理界面都直观。
任务状态机
任务有四个状态:
todo → in_progress → completed
→ awaiting_human_input
awaiting_human_input是个值得单独说的状态。Agent在执行任务时,可能遇到需要用户确认或补充信息的情况。这时候它不会猜,而是把状态设为awaiting_human_input,并通过当前活跃的Channel通知用户。
不过按当前代码实现,这条链路只做到"通知用户",还没有完整打通"用户后续回复自动重新挂回这条任务继续执行"的恢复机制;源码里甚至留着关于conversationId丢失的TODO。也正因为如此,这个状态更像是一个明确的暂停点,而不是已经闭环的人机协作工作流。
TaskManager与TaskHandler的分工
TaskManager是任务管理的编排器。它负责:
- 创建任务文件
- 把任务交给JobRunr调度(立即、延迟、或Cron定时)
- 管理定时任务的增删
TaskHandler是实际干活的JobRunr作业处理器。它的executeTask()方法做了这几件事:
@Job(name = "%0", retries = 3)
public void executeTask(String taskId) {
// 1. 加载任务,校验状态必须是 todo // 2. 更新状态为 in_progress // 3. 构造提示词,调用Agent(结构化输出) TaskResult result = agent.prompt(taskId, "Handle the following task...", TaskResult.class); // 4. 根据返回结果更新任务状态 // 5. 通过活跃 Channel 通知用户 // 异常时状态回退到 todo,JobRunr 自动重试
}
注意第3步——这里用的不是respondTo()而是prompt(),要求LLM返回结构化的TaskResult对象,包含新状态和反馈文本。让LLM自己决定任务执行到什么程度,用结构化输出确保结果可解析。这比用正则表达式从自由文本中提取状态靠谱得多。
JobRunr的三种调度模式
JobRunr还自带一个Dashboard(端口8081),可以实时查看任务执行状态。对调试Agent行为来说,这个Dashboard比翻日志方便太多。
Agent光有脑子不够,还得有手有脚——能执行Shell命令、读写文件、搜索网页。这就是工具系统。
@Tool注解驱动的工具注册
JavaClaw的大部分自研工具都通过Spring AI的@Tool注解声明。比如TaskTool的方法:
@Tool(description = "Create a new task…") public String createTask(String name, String description) { … }
在ChatClient构建时统一注册。LLM在对话过程中决定调用哪个工具时,Spring AI负责匹配方法名、传递参数、执行方法、返回结果。开发者只需要写业务逻辑和加注解。
不过也不是所有能力都统一走@Tool这条路径。像SkillsTool和MCP ToolCallbacks,就是通过defaultToolCallbacks(…)注入到ChatClient里的。
内置工具全景
其中几个值得展开说说:
SmartWebFetchTool比普通的HTTP抓取工具多了一步——它内部会克隆一个新的ChatClient实例,把抓到的网页原文喂给LLM做摘要,只把摘要返回给Agent。这样做的好处是避免把几十KB的原始HTML直接灌进Agent的上下文窗口,节省Token且信息密度更高。
Playwright浏览器自动化(插件)提供了8个工具方法:navigateTo、clickElement、fillInput、getText、takeScreenshot、evaluateJavaScript、waitForSelector、closeBrowser。有两个实现细节值得注意:
- 返回的网页文本被截断到1万字符以内,防止撑爆上下文
- Playwright实例是懒初始化的,首次使用时才启动浏览器并自动安装Chromium
插件工具的自动发现
插件(Brave搜索、Playwright浏览器自动化)的工具注册用了AutoDiscoveredTool包装器:
record AutoDiscoveredTool
(T tool) { }
插件把工具包装成这个record暴露为Spring Bean,ChatClient构建时自动收集所有AutoDiscoveredTool Bean并注册。新增插件工具不需要改核心代码,只需要加一个Bean。
Skills:零代码扩展
Skills系统可能是JavaClaw里最”魔法”的部分。你只需要在workspace/skills/下创建一个目录,放进一个SKILL.md文件,Agent就会自动发现并使用这个技能。
一句话定义:Skill是一个用自然语言定义的、具有特定领域上下文的逻辑指令集,本质上是通过延迟加载(Lazy Loading)优化Token消耗的子Agent。
SKILL.md通常包含两部分:
- YAML frontmatter(元数据,描述技能名称、触发条件等,始终加载)
- 正文(详细的自然语言指令,仅在触发时按需加载)
这个设计很关键——如果把所有Skill的完整内容都塞进系统提示词,几百个Skill会瞬间撑爆上下文窗口。延迟加载保证了Agent知道”有哪些技能可以用”(元数据常驻),但只在需要时才读取具体指令(正文按需注入)。
底层实现依赖spring-ai-community-agent-utils的SkillsTool。它在构建时扫描skills目录,把每个SKILL.md的元数据注册为工具回调。Agent在对话中判断需要某个技能时,调用SkillsTool读取对应的SKILL.md完整内容,将指令注入推理上下文,然后按照里面的规则行事。
这种”约定优于配置”的思路很像Spring Boot本身——不需要写Java代码,不需要编译部署,丢个文件进去就能扩展Agent能力。
Skills和MCP解决的是不同层面的问题:
- MCP负责把外部系统接入进来(连通性)
- Skills负责决定什么时候用、怎么组合这些能力(编排逻辑)
一个高级Skill的底层完全可以编排多个MCP工具的调用。
MCP(Model Context Protocol)是Anthropic于2024年提出的开放协议,被形象地称为“AI领域的USB-C接口”——通过JSON-RPC 2.0统一了LLM与外部数据源/工具的通信规范。一次开发的MCP Server,所有支持MCP的AI应用都能直接复用。
MCP的架构分四层
一个Host可以管理多个Client,每个Client对应一个Server,互不影响。
JavaClaw通过Spring AI MCP Client接入了这个生态,支持两种传输模式:
- stdio:本地进程间通信,适合Agent调用本机工具
- Streamable HTTP:远程HTTP连接,适合接入云端MCP服务
集成分两层:
配置层:默认情况下,这些连接信息会落到app/src/main/resources/application.private.yaml;主application.yaml只是通过spring.config.import去导入它。Spring AI的自动配置会为每个声明的Server创建MCP客户端,获取其暴露的工具列表,转换为Spring AI的ToolCallback。
运行时层:Agent可以通过McpTool动态注册新的MCP Server。注册信息通过ConfigurationManager持久化到配置文件。配置变更后,JavaClawApplicationMonitor会监听事件,关闭并重新启动Spring应用上下文,让新配置生效。
这个”配置变更→自动重启”的方案看着粗暴,但对个人助手来说合理——你不会频繁改配置,改一次重启几秒钟完全可以接受。比设计一套热加载机制简单太多。
回顾一下JavaClaw的几个代表性设计决策:
1. ReAct循环:Advisor链 vs 手写循环
JavaClaw选择完全依赖Spring AI的ToolCallAdvisor,而不是自己实现循环。
好处:代码量极少,升级Spring AI版本就能获得循环逻辑的改进
代价:对Spring AI的依赖很深,如果Advisor的行为不符合预期,调试起来不如自己写的循环直观
2. 文件持久化 vs 数据库
前面已经分析过。一句话总结:对单用户场景,文件系统的可读性优势远大于数据库的并发优势。
3. Spring Modulith vs 单体
在个人助手这种规模的项目里引入模块化框架,初看有点”过度设计”。但考虑到项目定位是”面向Java社区的开源框架”,模块化边界对贡献者来说就是一份隐形的架构文档——哪些可以动,哪些不能动,编译器帮你守着。
4. Chat Memory的处理
JavaClaw没有直接照搬Spring AI默认实现,而是自己复制并微调了两块Chat Memory组件。
第一块是MessageChatMemoryAdvisor:核心改动不是一句”把HashSet换成LinkedHashSet”就能概括的,而是用LinkedHashSet做去重并保持顺序,同时确保SystemMessage仍然排在最前面。这样既避免重复注入历史消息,也尽量保持对话上下文的稳定顺序。
第二块是MessageWindowChatMemory:它把策略调整成”仓库存全量、读取时做窗口裁剪”,而不是简单把仓库里的旧消息截掉。这个改法对排查问题很友好,因为磁盘上能保留完整历史,推理时再取最近窗口。
项目还自定义了AppendableChatMemoryRepository接口,给仓储层补了appendAll()这个扩展点。但要实话实说:当前文件系统实现的appendAll()仍然是先读已有消息、拼接新消息、再全量写回YAML文件,所以这里更准确地说是”为增量追加留出了接口”,而不是已经彻底消除了全量重写。
对话历史本身持久化为YAML文件(workspace/conversations/chat-{conversationId}.yaml),每条消息带有时间戳和角色标记。这也是文件优于数据库的一个体现——你可以直接打开YAML文件查看对话记录,排查Agent为什么做了某个决策时非常方便。
这些”小补丁”说明一个事实:Spring AI虽然发展很快,但在Agent的生产细节上还有不少坑需要踩。
既然JavaClaw号称”Java版OpenClaw”,两者的差距在哪?直接看对比:
OpenClaw在生态成熟度上领先很多——25个传输通道、移动端应用、技能商店。JavaClaw目前更像是概念验证+社区孵化器,项目README里也明确说了:”这是一份面向Java社区的公开邀请,让我们一起构建Java AI Agent的未来。”
但JavaClaw有一个OpenClaw没有的优势:对Java/Spring开发者的技术亲和度。如果你想基于AI Agent做二次开发,用JavaClaw你面对的是熟悉的Spring Boot自动配置、Bean注册、Advisor模式,学习曲线比从零学TypeScript要平缓得多。
JavaClaw的Anthropic提供商有个”彩蛋”功能:如果你在Onboarding时把API Key设为
,它会自动读取你系统里已有的Claude Code OAuth凭证,不需要手动填Key。
具体实现是AnthropicClaudeCodeOAuthTokenExtractor——在macOS上从Keychain读取Claude Code-credentials,在Linux上读取~/.claude/.credentials.json。拿到token后,它把标准的x-api-key头替换成Authorization: Bearer {token},并注入Claude Code专属的beta头和系统提示前缀。
这个设计让已经装了Claude Code的开发者可以零配置上手JavaClaw,很贴心。
文章开头提到OpenClaw生态已经蔓延到多种编程语言,这里展开列一下各语言的代表项目,方便感兴趣的读者按语言去研究:
可以看到,TypeScript原版一家独大,Rust生态最为活跃(IronClaw、OpenCrust、MicroClaw三足鼎立),Go在IM和Agent框架两个方向都有布局,C#有官方的Windows原生支持,Nix适合追求声明式部署的运维同学,Elixir则在探索OTP并发模型与Agent的结合。Java在这个版图里长期缺席,JavaClaw是目前第一个填补这个空白的项目。
从源码里能看到几个有意思的结论:
✅ Java生态完全有能力构建Agent
→ Spring AI的ChatClient + Advisor链 + @Tool注解,这套组合拳让Java开发者用熟悉的编程模型就能接入LLM能力
✅ 文件可以比数据库更好用
→ 在个人助手场景下,人和AI共享同一份文件视图,比任何管理界面都直观
✅ Spring Boot的插件化机制天然适合Agent扩展
→ @AutoConfiguration + @ConditionalOnProperty,新增一个传输通道或工具,只需要加一个模块,核心代码零改动
- RuoYi-AI:面向企业级应用,强调多Agent协作和工作流编排
- JavaClaw:面向个人助手和学习研究,强调架构清晰度和学习价值
如果你想在Java技术栈上构建自己的AI Agent:
- 关注RuoYi-AI的企业级落地实践,特别是SupervisorAgent的多Agent协作模式
- 研究JavaClaw的源码实现,理解Spring AI Advisor链的工作原理
- 项目地址:https://github.com/jobrunr/JavaClaw
让我们一起构建Java AI Agent的未来!🚀
参考资源:
- RuoYi-AI官方文档
- JavaClaw GitHub仓库:https://github.com/jobrunr/JavaClaw
- Spring AI官方文档
- ReAct论文:https://arxiv.org/abs/2210.03629
- AI应用开发实战和面试指南:javaguide.cn
本文基于RuoYi-AI v3.0正式版和JavaClaw最新源码编写,旨在帮助Java开发者快速入门AI Agent开发。如有问题欢迎讨论交流!
本文由mdnice多平台发布
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/262644.html