Java + Spring 实现 Hermes Agent 之龙虾、Skills、MCP 和沙箱代码执行环境思路

发布时间:2026/10/8 12:20:16

Java + Spring 实现 Hermes Agent 之龙虾、Skills、MCP 和沙箱代码执行环境思路 1. 为什么要在 Spring 里手搓一个 Hermes AgentHermes Agent 是一套把大模型能力工程化的思路它不满足于“问一句答一句”而是让模型能记住上下文、能定时干活、能按需加载技能包、能调用外部工具、还能把生成的代码关进沙箱里跑。用 Java Spring 实现它核心价值在于把 Agent 的每个能力都做成可插拔的 Spring 组件而不是写成一坨脚本。我这次要拆的是四块绕不开的骨架龙虾Lobster调度、Skills 注册、MCP 工具接入、沙箱代码执行环境。所谓“龙虾”是社区里对 Agent 调度中枢的一个戏称你可以理解成一只负责“派活”的手——它决定哪条请求该走哪个模型、该挂哪些工具、该不该起沙箱。Skills 是能力包跟着请求热插拔不重启进程就能切换。MCP 是 Model Context Protocol让 Agent 直接复用外部生态里现成的工具服务。沙箱则是把模型生成的 shell 或 Python 代码关进隔离环境避免一条rm -rf把宿主机搞掉。适合谁看如果你已经用 Spring Boot 写过接口想往 Agent 方向走但被“记忆、调度、工具、隔离”这几块卡住这篇就是给你准备的。下面所有配置和代码都可以直接抄进项目里跑我会给出完整的 Spring Boot 配置片段、MCP 客户端注册示例、沙箱隔离参数并演示一次 Skills 调用和沙箱执行的验证动作。整篇按“先搭骨架、再填肉、最后验证”的顺序走你跟着做就能得到一个能跑的最小 Agent。2. TaoToken 前置把模型入口和 Key 准备好在写任何 Agent 代码之前得先有一个能稳定调用的模型入口。Hermes Agent 的调度层需要根据请求切换模型所以入口最好支持多模型路由。我这边用的是 TaoToken 作为统一入口它的 API 地址是https://taotoken.net/api兼容 OpenAI 的请求格式Spring AI 的 OpenAI starter 改一下 base-url 就能接上。先拿 Key。打开https://taotoken.net/api-keys登录后创建一个 API Key复制出来形如sk-xxxxxxxx。这个 Key 后面要写进application.yml不要硬编码在 Java 里。拿到 Key 之后去https://taotoken.net/doc看一眼接口文档确认 chat completions 的路径和参数Spring AI 默认走/v1/chat/completionsTaoToken 这边是兼容的。模型 ID 这块要注意不同模型的名字不一样比如deepseek-chat、qwen-plus、glm-4这些。你在https://taotoken.net/models或者模型对话页https://taotoken.net/chat里能看到当前可用的模型列表。我实测下来deepseek-chat对消息顺序比较敏感后面配记忆的时候要留意。如果你打算长期跑编码类 Agent可以顺手看一下 Coding Plan 页面https://taotoken.net/coding-plan它针对代码场景做了优化适合挂到沙箱执行那条链路上。把 Key 和 base-url 写进 Spring Boot 配置这是整个 Agent 的模型入口# application.yml spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: deepseek-chat temperature: 0.7环境变量TAOTOKEN_API_KEY在启动时注入别写死在文件里。到这里前置就齐了一个 Key、一个 base-url、一个模型 ID。接下来所有 Agent 组件都复用这个ChatModelBean调度层再按请求覆盖模型名。3. 可复制配置Lobster 调度、Skills 注册与 MCP 客户端这一节是骨架的核心三块配置我都给全。先说 Lobster 调度。它的职责是收到请求后解析出userId / assistantId / sessionId / modelName / skills / mcpConfig然后组装出一个当次请求专属的ChatClient。我把它写成一个Component注入ChatModel和几个工厂。Component public class LobsterDispatcher { private final ChatModel chatModel; private final SkillCache skillCache; private final SandboxFactory sandboxFactory; private final DynamicMcpClientFactory mcpFactory; public LobsterDispatcher(ChatModel chatModel, SkillCache skillCache, SandboxFactory sandboxFactory, DynamicMcpClientFactory mcpFactory) { this.chatModel chatModel; this.skillCache skillCache; this.sandboxFactory sandboxFactory; this.mcpFactory mcpFactory; } public FluxChatEvent dispatch(ChatRequest req) { ListResource skillDirs skillCache.resolve( req.userId(), req.assistantId(), req.sessionId(), req.skills()); Sandbox sandbox skillDirs.isEmpty() ? null : sandboxFactory.create(skillDirs); ToolCallback[] skillTools skillDirs.isEmpty() ? new ToolCallback[0] : new ToolCallback[]{ SkillsTool.builder().addSkillsResources(skillDirs).build() }; var spec ChatClient.create(chatModel).prompt().user(req.query()); if (sandbox ! null) { spec.tools(new SandboxBashTool(sandbox, Duration.ofSeconds(30), Map.of()), new SandboxFileSystemTools(sandbox)); } spec.toolCallbacks(skillTools); return spec.stream().chatResponse() .map(this::toEvent) .doFinally(s - { if (sandbox ! null) sandbox.close(); }); } }Skills 注册的关键是SkillCache它按userId/assistantId/sessionId三段分桶缓存下载解压后的目录。请求里带一组name url服务端下载 zip、解压、喂给SkillsTool。单个 skill 下载失败只记 warn 跳过不整次请求挂掉。Component public class SkillCache { private final Path root Path.of(System.getProperty(java.io.tmpdir), skill-cache); public ListResource resolve(Long userId, Long assistantId, String sessionId, ListSkillRef skills) { if (skills null || skills.isEmpty()) return List.of(); Path bucket root.resolve(userId / assistantId / sessionId); ListResource dirs new ArrayList(); for (SkillRef ref : skills) { try { Path dir bucket.resolve(ref.name()); if (!Files.exists(dir)) { downloadAndUnzip(ref.url(), dir); } dirs.add(new FileSystemResource(dir)); } catch (Exception e) { log.warn(skill {} load failed: {}, ref.name(), e.getMessage()); } } return dirs; } }MCP 客户端注册用短连接按请求开一组结束就关。DynamicMcpClientFactory每请求出一个McpSession实现AutoCloseablepublic McpSession build(MapString, McpServerConfig mcpConfig) { ListMcpSyncClient clients new ArrayList(); for (var entry : mcpConfig.entrySet()) { var cfg entry.getValue(); var transport HttpClientStreamableHttpTransport .builder(originOf(cfg.url())) .endpoint(pathOf(cfg.url())) .httpRequestCustomizer((req, m, ep, body, ctx) - cfg.headers().forEach(req::header)) .build(); McpSyncClient client McpClient.sync(transport) .requestTimeout(Duration.ofSeconds(30)) .build(); client.initialize(); clients.add(client); } return new McpSession(clients); }沙箱隔离参数写在application.yml里切后端只改一行chat: sandbox: mode: DOCKER image: ghcr.io/spring-ai-community/agents-runtime:latest timeout-seconds: 30 max-output-bytes: 30720SandboxFactory按 mode 返回不同实现Sandbox sandbox switch (props.getMode()) { case DOCKER - DockerSandbox.builder().image(props.getImage()).build(); case LOCAL - LocalSandbox.builder().tempDirectory(chat-sandbox-).build(); };LOCAL 模式启动时要打 warn它没有任何隔离只给开发用。Docker 模式才是生产选择。三块配置拼起来Lobster 调度就能按请求组装出带 Skills、MCP、沙箱的完整工具集。4. 验证请求跑一次 Skills 调用与沙箱执行配置写完得验证它真的能跑。我准备了一个最小请求带一个 skill 和一个 MCP server让模型先读 skill 目录再在沙箱里执行一条命令。请求体长这样POST /chat/stream { userId: 1001, assistantId: 7, sessionId: s-001, modelName: deepseek-chat, query: 列出 skills 目录下的文件然后跑一下 chart-maker 的入口脚本, skills: [ { name: chart-maker, url: https://cdn.example.com/skills/chart-maker-0.3.zip } ], mcpConfig: { github: { url: https://mcp.example.com/github/mcp, headers: { Authorization: Bearer ghp_xxx } } } }服务端收到后Lobster 先解析 skills下载解压到缓存桶起一个 Docker 沙箱把 skill 目录 seed 进去。然后组装工具集SandboxBashTool、SandboxFileSystemTools、SkillsTool再加上 MCP 的ToolCallback[]。模型这一侧看到的工具列表里Bash 的描述会告诉它 skill 文件在./skills/name/下。验证第一步看 Skills 是否被模型正确读取。模型会先调Bash执行ls skills/沙箱返回chart-maker这一步说明 skill 目录已经 seed 进沙箱模型能看见。验证第二步看沙箱执行是否隔离。模型接着调Bash跑python skills/chart-maker/run.py --help沙箱返回脚本的 usage 信息。整个过程宿主机上没有任何文件被创建所有读写都在容器里。SSE 流里能看到完整的事件序列event: tool_call [{id: c1, name: Bash, args: ls skills/}] event: tool_result [{id: c1, name: Bash, result: chart-maker}] event: tool_call [{id: c2, name: Bash, args: python skills/chart-maker/run.py --help}] event: tool_result [{id: c2, name: Bash, result: usage: run.py [-h] --input ...}] event: token {text: chart-maker 脚本可用参数是 --input。}如果你只想先验证模型入口通不通可以打开模型对话页https://taotoken.net/chat用同一个 Key 发一条消息确认 base-url 和模型 ID 没问题。这一步过了再回来跑上面的 Agent 请求。验证通过的标准是tool_call和tool_result成对出现沙箱里能读到 skill 文件宿主机上不留痕迹。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth跑这套骨架报错基本集中在四个地方。我按真实遇到的顺序列一下。第一个是401 Unauthorized。这个最常见原因是api-key没注入或者写错了。检查application.yml里的${TAOTOKEN_API_KEY}环境变量是否真的存在可以在启动日志里打一行System.getenv(TAOTOKEN_API_KEY)的前几位确认。如果是 MCP server 返回 401那是headers里的Authorization没带上检查httpRequestCustomizer有没有把 header 注入到每次 POST。第二个是local proxy failed。这个报错通常出现在沙箱 LOCAL 模式下命令里带了网络请求但本地进程没有网络出口。解决办法是切到 DOCKER 模式容器有自己的网络栈。如果必须用 LOCAL检查命令是不是依赖了外部服务把网络调用挪到 MCP 工具里走。第三个是reading choices相关的解析错误。这个多半是模型返回的 JSON 结构跟 Spring AI 预期的不一致常见于自定义ChatModel实现里stream方法没把choices字段映射对。检查你的MyModelChatModel.stream返回的ChatResponse里generations是否为空。如果是用 OpenAI starter确认 base-url 末尾没有多余的斜杠https://taotoken.net/api后面不要再加/v1starter 会自己拼。第四个是OAuth相关。MCP server 如果走 OAuth 鉴权initialize阶段会失败。这时候要检查McpServerConfig里的 headers 是不是带了正确的 token以及 token 有没有过期。如果是 GitHub MCP用 personal access token 走Authorization: Bearer头别用 OAuth 流程短连接场景下 token 更省事。排查顺序建议先确认模型入口通用模型对话页测再确认 MCP 握手通单独跑client.initialize()最后确认沙箱能起docker ps看容器。三件套Base URL Key Model ID任何一件不对都会在前面几步就报错别急着往沙箱里查。6. 继续往下走把 Agent 挂到长期任务上骨架跑通之后下一步是让它能“在你不在的时候干活”。这块我用 JobRunr 接长期任务一次性、定时、cron 周期都能跑。任务用 lambda 调度JobRunr 帮你做序列化、持久化、重启恢复自带 dashboard 能看到队列里有什么、跑过什么、失败了几次。TaskManager这层很薄三个方法对应三种调度Component public class TaskManager { private final JobScheduler jobScheduler; private final TaskRepository taskRepository; public void create(String name, String desc) { Task task taskRepository.save(Task.newTask(name, desc)); jobScheduler.TaskHandlerenqueue(x - x.executeTask(task.getId())); } public void schedule(LocalDateTime when, String name, String desc) { Task task taskRepository.save(Task.newTask(name, desc)); jobScheduler.TaskHandlerschedule(when, x - x.executeTask(task.getId())); } public void scheduleRecurrently(String cron, String name, String desc) { RecurringTask task taskRepository.save(RecurringTask.newRecurringTask(name, desc)); jobScheduler.RecurringTaskHandlerscheduleRecurrently( task.getName(), cron, x - x.executeTask(task.getId())); } }到点真正干活的是TaskHandler.executeTask拿到 taskId 从仓库捞出任务描述喂给 Agent 自己处理写回状态。Job(retries 3)一行就能让 JobRunr 失败时自动重试三次。注意参数用 taskId 而不是整个 Task 对象JobRunr 要序列化 lambda简单值更稳。把任务能力暴露给模型就是一个TaskTool三个Tool方法对应 create / schedule / scheduleRecurrently。用户说“每天早上九点帮我同步一下昨天的 commits”模型自己拼出 cron 表达式调scheduleRecurrently。到点 JobRunr 触发TaskHandler让 Agent 真正执行结果通过通道推回给用户。几个坑记一下任务的conversationId和聊天会话的不是一回事我这边直接拿 taskId 当 Agent prompt 的 conversationId意思是这条任务有自己独立的对话上下文不会跟实时聊天混在一起。JobRunr 默认用 H2 存储就能跑生产换 Postgres 或 MySQL 都行存储层是插拔的。dashboard 默认在/dashboard部署到生产记得加鉴权或者关掉里面能看到所有任务的执行历史和栈。如果你打算把这条链路长期跑起来模型入口建议用 Coding Plan 那套配置针对代码和任务场景做了优化挂到 JobRunr 触发的 Agent 上更稳。整套骨架到这里就闭环了Lobster 调度负责派活Skills 负责热插拔能力MCP 负责接外部工具沙箱负责隔离执行JobRunr 负责长期任务。你可以先跑通单次请求再把 TaskTool 挂上去让 Agent 自己决定什么时候该定时干活。
延伸阅读

更多相关文章

2026/10/8 12:15:16

SigmaStar与海思IPC芯片选型对照:从入门到AI高端的平替指南

1. 从一次选型纠结说起:为什么要做这份SigmaStar与海思IPC芯片的对照梳理前阵子帮一个做安防整机的朋友选主控,需求很明确:200万像素、H.265编码、带轻量AI人形检测、单板成本要压到某个数以内。他第一反应是翻海思的型号表,结果发…

2026/10/8 12:15:16

Surface Studio 一代升级 SSD 全攻略:拆机、迁移与性能验证

Surface Studio 一代这台机器,放到今天看依然是个很有意思的存在。28 英寸 3:2 的触控屏、零重力铰链、一体化的主机底座,当年是不少设计师和内容创作者的梦中情机。但它出厂标配的那块混合硬盘,放在今天确实有点拖后腿了——系统盘读写慢、开…

2026/10/8 12:15:15

Java车辆管理系统课程设计:从数据库设计到Spring Boot全栈实现

简介:本资源是面向计算机相关专业学生与Java Web学习者的课程设计项目包,以车辆管理系统为主题,采用JSPServletMySQLMaven技术栈,在IDEA环境下开发,适合正在做毕设或需要项目实战练手的读者参考。压缩包共70个文件&…

2026/10/8 13:20:50

Agent-Reach 实战:用 Python CLI 快速构建可调试的 AI Agent

1. 从零认识 Agent-Reach:它到底解决什么问题Agent-Reach 这个名字,第一次看到的时候我以为是某个网络探测工具,后来翻了一圈资料才搞明白,它本质上是一个面向 AI Agent 的 CLI 工具层,用 Python 写的,核心…

2026/10/8 13:20:50

2026深圳罗湖大创客节:校园跳绳挑战赛解析

引言 健康生活与信息科技正在校园里越走越近。在 2026 深圳市罗湖区中小学第九届大创客节人工智能编程设计赛 的图形化赛项中,评委非常看重「用程序解决真实场景问题」的能力——把体育锻炼变成一款可玩、可计数的小游戏,正是这类赛事喜欢的方向。 今天…

2026/10/8 13:20:50

PA Agent 演示模式使用教程:零API成本回放历史K线分析记录

PA Agent 演示模式使用教程:零API成本回放历史K线分析记录 【免费下载链接】PA_Agent 项目地址: https://gitcode.com/gh_mirrors/pa/PA_Agent PA Agent 是一款基于价格行为学(Price Action)的 AI K 线分析工具,而它的演示…

2026/10/8 13:20:50

PS5串流全攻略:从局域网到远程,打造AnyPS5方案

如果你家里有一台PS5,大概率经历过这样的场景:客厅电视被家人占着,你想推两把游戏,却只能对着手机发呆。我试过把主机搬到卧室,结果第二天又得搬回去,HDMI线在背包里绕成一团麻花。后来我把目光转向了串流&…

2026/10/8 13:15:50

Spring Boot零基础入门:从环境搭建到MyBatis数据库实战

我最近在带几个完全零基础的同事转Java方向,发现一个很普遍的现象:大家一说学Spring Boot,第一反应就是去搜“SSM框架教程”,然后从Spring的IOC容器、Bean生命周期开始啃,啃了两个星期连一个能跑的HelloWorld都没写出来…

2026/10/8 10:03:18

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/8 10:03:20

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 0:02:17

自然数立方等于连续奇数之和:从证明到编程验证

十几年来我一直游走在数学科普和编程教学这两块内容之间,对“看起来像魔法、拆开全是数学”的结论总是格外敏感。最近翻资料时又撞见一句话:任何一个自然数 m 的立方,都可以写成 m 个连续奇数之和。2 的立方等于 3 加 5,3 的立方等…

2026/10/8 0:02:17

C#上位机SSH连接实战:用SSH.NET补齐超时、批量与密钥认证

简介:这是一份基于 C# 开发的 SSH 连接功能半成品工程,原本作为另一个主项目的子功能模块,现独立打包分享。工程采用 WinForms 界面,包含源码、解决方案、安装部署工程、NuGet 依赖包及说明文档,适合正在做远程连接、网…

2026/10/8 0:02:17

Java SpringBoot一体化智能售后系统设计与实现全解析

毕业设计年年做,Java Web 方向的题目翻来覆去就那么几个,但“一体化智能售后系统”这个题,每次看到我都觉得值得认真聊一聊。它不是一个简单 curd 堆出来的管理系统,而是把客户、工单、派单、处理、回访、统计整条链路串起来的一套…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑