Ohhnews

分类导航

$ cd ..
foojay原文

如何利用现有Java生态构建AI智能体

#java#人工智能#spring框架#ai智能体#软件架构

目录

人们总以为构建 AI 智能体必须使用 Python。但 Java 生态系统其实已经具备了所有组件:用于 LLM 集成的 Spring AI、用于解耦消息传递的 Spring Events、用于可靠后台任务的 JobRunr,以及用于保持架构整洁的 Spring Modulith。我们并没有发明什么新东西,只是将现有的组件连接在了一起。

其成果就是 ClawRunr(大家都叫它 JavaClaw,我们已经放弃纠正了)。这是一个完全用 Java 编写的开源 AI 智能体运行时。你可以通过 Telegram 或浏览器与它对话,让它每天早上总结你的电子邮件、安排提醒、浏览网站、运行 Shell 命令、通过 MCP 连接外部工具,甚至只需将 Markdown 文件放入文件夹,就能在运行时教会它新技能。

在本文中,我将带你了解 Spring 生态系统的各个部分是如何对应 AI 智能体实际需求。

[LOADING...] ClawRunr 上手向导

AI 智能体需要什么?

在看代码之前,先思考一下 AI 智能体除了聊天之外还需要做什么:

智能体需求Java 生态系统解决方案
与 LLM 对话Spring AI (ChatClient)
基于对话调用工具Spring AI @Tool 注解
处理多渠道消息Spring Events
调度和重试后台任务JobRunr
随项目增长保持模块化Spring Modulith

这些组件并非专为 AI 智能体而生,它们是成熟且经过实战检验的工具,恰好解决了智能体面临的问题。

Spring AI:LLM 层

ClawRunr 的核心是一个封装了 Spring AI ChatClientDefaultAgent。整个类仅有 20 行:

$ java
@Component
public class DefaultAgent implements Agent {

    private final ChatClient chatClient;

    public DefaultAgent(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @Override
    public String respondTo(String conversationId, String question) {
        return chatClient
                .prompt(question)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
                .call()
                .content();
    }
}

这就是你的智能体。一个类,一个依赖,与模型提供商无关。想从 OpenAI 切换到 Anthropic 或完全本地的 Ollama 实例?只需更改一个配置属性,代码无需改动。

提示词(Prompt)本身由两个工作区文件组合而成。AGENT.md 包含系统指令(用户可在上手时编辑),INFO.md 提供环境上下文:

$ java
String agentPrompt = workspace.createRelative("AGENT.md")
        .getContentAsString(StandardCharsets.UTF_8)
    + System.lineSeparator()
    + workspace.createRelative("INFO.md")
        .getContentAsString(StandardCharsets.UTF_8);

工具通过 Spring AI 的构建器进行注册。Shell 访问、文件操作、网页抓取、任务管理、MCP 支持以及运行时可发现的技能,全部在一处配置:

$ java
chatClientBuilder
    .defaultSystem(p -> p.text(agentPrompt))
    .defaultToolCallbacks(mcpToolProvider.getToolCallbacks())
    .defaultToolCallbacks(SkillsTool.builder()
        .addSkillsDirectory(skillsDir.toString()).build())
    .defaultTools(
        TaskTool.builder().taskManager(taskManager).build(),
        CheckListTool.builder().build(),
        McpTool.builder()
            .configurationManager(configurationManager).build(),
        ShellTools.builder().build(),
        FileSystemTools.builder().build(),
        SmartWebFetchTool.builder(chatClientBuilder.clone().build())
            .build())
    .defaultAdvisors(
        ToolCallAdvisor.builder().build(),
        MessageChatMemoryAdvisor.builder(chatMemory).build()
    );

LLM 会根据对话决定调用哪个工具。Spring AI 处理工具调用协议,你只需声明每个工具的功能即可。

Spring Events:即时多渠道支持

智能体应该能在 Telegram、浏览器,乃至未来的 Discord 或 Slack 上运行。ClawRunr 使用 Spring 开发者熟悉的模式解决了这个问题:事件。

Channel 接口非常简单:

$ java
public interface Channel {

    default String getName() {
        return getClass().getSimpleName();
    }

    void sendMessage(String message);
}

当任何渠道收到消息时,运行时会触发 ChannelMessageReceivedEventChannelRegistry 会追踪最后一条消息来自哪个渠道,以便后台任务的结果能正确路由回原处:

$ java
@Service
public class ChannelRegistry {

    private final Map<String, Channel> channels = new HashMap<>();
    private final AtomicReference<ChannelMessageReceivedEvent> lastChannelMessage
        = new AtomicReference<>();

    public void registerChannel(Channel channel) {
        channels.put(channel.getName(), channel);
    }

    public Channel getLatestChannel() {
        if (lastChannelMessage.get() != null) {
            return channels.get(lastChannelMessage.get().getChannel());
        }
        return channels.get(defaultChannelName);
    }
}

智能体本身不需要知道或关心消息来自哪里。它处理请求,返回响应,运行时会通过同一渠道将其路由回去。想添加 Discord 支持?实现 Channel 接口即可,智能体代码保持不变。

[LOADING...]

JobRunr:常被忽视的关键组件

这里有一个问题:当你对智能体说“每天早上 8 点总结我的邮件”时,它该怎么做?

大多数智能体框架都没有好的答案。也许你会单独配置一个 Cron 任务,或者使用重启后就会丢失的内存定时器。没有重试逻辑,没有仪表板,也无法得知 8 点的总结任务是运行成功还是悄无声息地失败了。

这是我们在构建 ClawRunr 时感触最深的一点。AI 智能体最难的问题不是 LLM 部分,而是可靠的任务执行。智能体需要调度循环检查、运行延迟任务、在后台处理事务、在失败时重试,并让你全面了解发生了什么。

这不是一个 AI 问题,而是一个后台任务问题。而 JobRunr 自 2020 年以来一直在解决这个问题。

[LOADING...]

以下是 ClawRunr 中任务执行的方式。TaskHandler 使用 @Job(retries = 3) 注解:

$ java
@Component
public class TaskHandler {

    private final Agent agent;
    private final TaskRepository taskRepository;
    private final ChannelRegistry channelRegistry;

    @Job(name = "%0", retries = 3)
    public void executeTask(String taskId) {
        Task task = taskRepository.getTaskById(taskId);
        Task inProgress = taskRepository.save(
            task.withStatus(Task.Status.in_progress));
        try {
            String agentInput = formatTaskForAgent(inProgress);
            TaskResult result = agent.prompt(
                taskId, agentInput, TaskResult.class);
            taskRepository.save(inProgress
                .withFeedback(result.feedback())
                .withStatus(result.newStatus()));
            notifyUser(task.getName(), result);
        } catch (Exception e) {
            taskRepository.save(
                inProgress.withStatus(Task.Status.todo));
            throw e; // JobRunr 会自动重试
        }
    }
}

当异常抛出时,JobRunr 会捕获并重试。最多重试三次,并采用指数退避策略。如果最终失败,它会显示在 localhost:8081 的仪表板上。

TaskManager 将一切串联起来,无论是创建任务、延迟执行还是设置循环 Cron 任务:

$ java
public void create(String name, String description) {
    Task task = taskRepository.save(Task.newTask(name, description));
    jobScheduler.<TaskHandler>enqueue(x -> x.executeTask(task.getId()));
}

public void schedule(LocalDateTime executionTime, String name, String description) {
    Task task = taskRepository.save(Task.newTask(name, executionTime, description));
    jobScheduler.<TaskHandler>schedule(executionTime,
        x -> x.executeTask(task.getId()));
}

public void scheduleRecurrently(String cron, String name, String description) {
    RecurringTask rt = taskRepository.save(
        RecurringTask.newRecurringTask(name, description));
    jobScheduler.<RecurringTaskHandler>scheduleRecurrently(
        rt.getName(), cron, x -> x.executeTask(rt.getId()));
}

LLM 通过 TaskTool 调用这些方法,该工具通过 @Tool 注解暴露接口,让智能体知道何时以及如何使用它们:

$ java
@Tool(description = """
    使用 JobRunr 调度任务,根据 cron 表达式定期重复。
    用于每日报告、每周检查等循环活动。
    """)
public String scheduleRecurringTask(String cronExpression,
                                     String name,
                                     String description) {
    this.taskManager.scheduleRecurrently(cronExpression, name, description);
    return String.format(
        "任务 '%s' 已使用 cron 表达式 '%s' 调度。",
        name, cronExpression);
}

无需编写自定义调度代码,无需 Cron 解析器,无需任务持久化层,无需重试逻辑。JobRunr 开箱即用地处理了这一切,并提供了一个完整的仪表板来监控智能体运行的每一个任务。

Spring Modulith:保持可扩展性

ClawRunr 使用 Spring Modulith 来强制模块间的清晰边界:

JavaClaw/
├── base/           # 核心:智能体、任务、工具、渠道、配置
├── app/            # Spring Boot 入口、上手 UI、聊天渠道
└── plugins/
    └── telegram/   # Telegram 长轮询渠道插件

这对开源项目至关重要。当社区有人想添加 Discord 渠道时,他们只需创建一个新的插件模块。实现 Channel 接口,向 ChannelRegistry 注册,就完成了。无需修改智能体核心。

发布后的三天内,社区里就有人编写了一个将机器人消息流传输到 Web 界面的插件。他们完全没触碰智能体核心,只是通过实现正确的接口添加了一个新模块。

ClawRunr 目前的功能

有了这些组合在一起的积木,ClawRunr 已经具备了许多开箱即用的功能:

  • 跨渠道聊天:在移动时通过 Telegram 与智能体对话,或在桌面端通过内置 Web UI 对话。智能体可在两者之间保持上下文。
  • 通过对话调度任何任务:“提醒我明天上午 10 点查看那个 PR”或“每天早上 8 点总结我的邮件”。智能体会在 JobRunr 中创建任务,并提供重试和 localhost:8081 的仪表板可见性。
  • 浏览网页:集成 Playwright 后,智能体可以导航网站、点击 Cookie 弹窗并反馈发现的内容。
  • 通过 MCP 连接外部工具:在上手期间添加 Gmail、日历或任何 MCP 兼容的工具服务器。智能体能自动发现并使用它们。
  • 在运行时学习新技能:只需将 SKILL.md 文件放入 workspace/skills/,智能体即可识别。无需重启,无需重新编译。想让它管理购物清单?写好说明,它就能做到。
  • 运行 Shell 命令和管理文件:智能体拥有本地机器的完全访问权限(它在你的硬件上运行,隐私优先)。

这一切都由我们刚才介绍的生态组件驱动。JobRunr 处理调度和重试,Spring AI 处理 LLM 和工具调用,Spring Events 处理跨渠道消息路由,Spring Modulith 保持模块化,让社区可以在不破坏现有功能的前提下进行扩展。

立即尝试

$ bash
git clone https://github.com/jobrunr/javaclaw.git
cd javaclaw
./gradlew :app:bootRun
# 打开 http://localhost:8080/onboarding

你将通过 7 个步骤完成上手(选择 LLM 提供商、配置 Telegram、设置 MCP 服务器),大约两分钟内即可开始与智能体对话。

我们五天前发布了 ClawRunr,目前已有 200 多个 GitHub Star,32 个 Fork,Oracle GraalVM 团队的 Alina Yurenko 甚至提交了 GraalVM 原生镜像支持,我们还收到了第一个外部 Pull Request。这些反馈告诉我们,这已不仅仅是一个演示项目。

来自 README 的一段话:

本项目最初是作为展示 JobRunr 的演示而创建的。JavaClaw 现在是对 Java 社区的一个公开邀请。让我们共同构建基于 Java 的 AI 智能体的未来。

网站: clawrunr.io

GitHub: github.com/jobrunr/javaclaw

演示视频: youtu.be/_n9PcR9SceQ

祝编程愉快!