尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Java + Spring实现Hermes Agent:龙虾、Skills、Mcp与沙箱代码执行环境配置思路

发布时间:2026/9/26 12:26:19

资讯中心
01
ARTICLE

Java + Spring实现Hermes Agent:龙虾、Skills、Mcp与沙箱代码执行环境配置思路

Java + Spring实现Hermes Agent:龙虾、Skills、Mcp与沙箱代码执行环境配置思路
1. 为什么要在 Java Spring 里搭 Hermes AgentHermes Agent 是一套把「记忆、任务调度、技能热插拔、MCP 工具、沙箱执行」拼在一起的 Agent 运行骨架。放到 Java Spring 技术栈里它要解决的核心问题是让一个 Spring Boot 服务既能跟大模型对话又能在对话之外记住事实、定时干活、按需加载技能包、复用外部工具生态并且把模型生成的代码关进隔离环境里跑。适合谁适合已经有一套 Spring Boot 后端、想在上面长出 Agent 能力、又不想把整套东西重写成 Python 的团队。我把它拆成四个模块来落地龙虾记忆管理短期会话历史 长期事实清单、Skills按请求热插拔的技能包、Mcp复用外部工具协议、沙箱代码执行环境把 Bash / Read / Write / Edit 关进容器。这四个模块共享一个 workspace 目录短期记忆由框架兜底写长期记忆由模型自己用文件工具维护Skills 和沙箱通过 seeding 把脚本喂进隔离环境Mcp 则按请求开短连接拿工具回调。整条链路里模型看到的工具签名始终是 Spring AI 的Tool隔离边界藏在方法实现里业务代码不用改。下面按「前置准备 → 可复制配置 → 验证请求 → 排错」的顺序走一遍配置骨架可以直接抄。2. TaoToken 前置统一 Key 与 API 通道在接 Mcp 和模型之前先把模型通道统一掉。TaoToken 提供 OpenAI 兼容的 API 通道一个 Key 可以走多个模型省得每个 provider 配一套 base-url 和鉴权。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 这个不加 UTM。你需要先拿到 API Key再去控制台确认通道可用。拿 Key 的入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你后面要长期跑编码类 Agent可以看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。注意Key 只放在服务端环境变量或配置中心别写进前端请求体也别提交到仓库。Mcp 的鉴权头同理走服务端注入。拿到 Key 之后Spring AI 这边直接复用 OpenAI starter把 base-url 指到 TaoToken 的 API 地址即可。这样ChatModel、ChatClient、advisor、tool call、memory 这一整套都能直接用不用自己实现协议。3. 可复制配置Spring Boot 骨架 Mcp settings.json3.1 application.yml 骨架spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: deepseek-chat temperature: 0.3 chat: workspace: ${user.home}/hermes-workspace sandbox: mode: LOCAL # LOCAL / DOCKER image: ghcr.io/spring-ai-community/agents-runtime:latest mcp: request-timeout-seconds: 30workspace是四个模块共享的根目录短期会话历史落在{workspace}/conversations/长期事实清单落在{workspace}/AGENT.md和{workspace}/memories/*.md。迁机器时把整个 workspace 拷过去就行。3.2 短期记忆文件版 ChatMemoryRepositorySpring AI 自带的InMemoryChatMemoryRepository进程一重启就清空做 Agent 不够用。写一个落到 YAML 的实现每个会话一个文件Component public class FileSystemChatMemoryRepository implements ChatMemoryRepository { private final Path conversationsDir; public FileSystemChatMemoryRepository(Value(${chat.workspace}) String workspace) { this.conversationsDir Path.of(workspace, conversations); } Override public ListMessage findByConversationId(String id) { Path f conversationsDir.resolve(chat- id .yaml); return Files.exists(f) ? ChatYamlSerializer.deserialize(YamlParser.parse(Files.readString(f)).body()) : List.of(); } Override public void saveAll(String id, ListMessage msgs) { // 写 frontmatter body只追加增量 } Override public void deleteByConversationId(String id) { // 删文件 } }conversationId直接用通道名web、telegram-123、discord-456多通道天然隔离。这里有个坑Spring 原生的MessageWindowChatMemory内部用HashSet会把消息顺序打乱DeepSeek 这类对顺序敏感的模型会直接报错。换成LinkedHashSet保留顺序并且把窗口化从写入侧挪到读取侧——磁盘留全量给模型时再截最近 N 条。3.3 长期记忆让模型自己维护 AGENT.md长期记忆不专门搞 MemoryTool复用 Read / Write / Edit 三个通用文件工具让模型自己在 workspace 里维护事实清单。装配时把FileSystemTools给模型MessageChatMemoryAdvisor给框架ChatClient.builder(chatModel) .defaultSystem(p - p.text(agentPrompt).param(WORKSPACE, workspace)) .defaultTools(FileSystemTools.builder().build()) .defaultAdvisors( ToolCallAdvisor.builder().build(), MessageChatMemoryAdvisor.builder(chatMemory).build() ) .build();Tool的 description 就是给模型看的说明书Spring AI 会把它拼进 JSON Schema 发给模型写不写得清楚直接决定模型用不用得对。Write 的描述里要把「先 Read 再 Write」「不要主动创建文档文件」这些约束讲明白。3.4 Mcp 接入settings.json 示例Mcp 按请求开短连接请求里带mcpConfig服务端 connect → initialize → 拿 callbacks → 喂给模型 → 请求结束 close。settings.json 示例{ mcpServers: { github: { url: https://mcp.example.com/github/mcp, headers: { Authorization: Bearer ${GITHUB_TOKEN} } }, brave-search: { url: https://mcp.example.com/brave?key${BRAVE_KEY} } } }构建逻辑放在DynamicMcpClientFactory里每请求出一个McpSessionpublic 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); }McpSession实现AutoCloseable同步接口用 try-with-resources流式接口在doFinally里关。多个 server 中某一个 initialize 失败时要把已经打开的 client 都closeGracefully再抛异常不能留半开状态。3.5 沙箱执行环境模型生成的 shell 或 Python 代码绝对不能直接在宿主机上跑。做法是本地Tool方法加沙箱执行环境工具签名还是用 Spring AI 的Tool暴露给模型方法实现里不直接Runtime.exec而是把命令通过统一的Sandbox接口转出去Tool(name Bash, description Execute a bash command inside an isolated sandbox container. Use for terminal ops like npm/pip/python/mvn; NOT for file IO. Skill files live under ./skills/name/. ) public String bash(ToolParam String command, ToolParam(required false) Long timeout) { ExecSpec spec ExecSpec.builder() .command(bash, -lc, command) .timeout(timeoutOf(timeout)) .env(envOverrides) .build(); ExecResult r sandbox.exec(spec); return formatForLlm(r); }切后端就是配置一行的事Sandbox sandbox switch (props.getMode()) { case DOCKER - DockerSandbox.builder().image(props.getImage()).build(); case LOCAL - LocalSandbox.builder().tempDirectory(chat-sandbox-).build(); };平时开发用 LOCAL 起得快生产或跑不可信 skill 就切 DOCKER。沙箱按请求级 try-with-resources 管每次/chat或/chat/stream开一个、结束关一个别在进程里复用。4. 验证请求从对话到工具调用配置搭好后先验证模型通道通不通。用模型对话入口 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一条消息确认 Key 和 base-url 没问题。然后在 Spring Boot 里发一个带 Skills 和 Mcp 的请求curl -N -X POST http://localhost:8080/chat/stream \ -H Content-Type: application/json \ -d { userId: 1001, assistantId: 7, sessionId: s-xxx, query: 把附件 csv 画成折线图, 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} } } }预期看到的事件序列类似event: reasoning {text: 让我先查一下...} event: tool_call [{id: c1, name: Bash, args: ls skills/}] event: tool_result [{id: c1, name: Bash, result: chart-maker}] event: token {text: 找到了 chart-maker我用它来画图...}tool_call和tool_result这两个事件需要额外处理。Spring AI 的ToolCallAdvisor打开streamToolCallResponses(true)后含 toolCalls 的中间响应会透传tool_call好转但工具执行结果默认只进下一轮 conversationHistory不会作为独立 chunk 发出来。解法是装饰一层ToolCallingManager在executeToolCalls后把本轮工具响应旁路到一个 sinkclass ObservableToolCallingManager implements ToolCallingManager { private final ToolCallingManager delegate; private final Sinks.ManyChatEvent sink; Override public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse resp) { ToolExecutionResult result delegate.executeToolCalls(prompt, resp); try { ListChatEvent.ToolResultRef refs extractToolResponses(result); if (!refs.isEmpty()) sink.tryEmitNext(ChatEvent.toolResult(refs)); } catch (RuntimeException e) { log.warn(emit tool_result failed: {}, e.getMessage()); } return result; } }装配时把它喂给ToolCallAdvisor再把 sink 跟主流 Flux 合一下ChatClient.create(chatModel).prompt().user(req.query()) .advisors(ToolCallAdvisor.builder() .toolCallingManager(observable) .streamToolCallResponses(true) .build()) .stream().chatResponse() .mergeWith(toolEventSink.asFlux()) .map(this::toEvent);5. 本篇常见错排查Mcp URL 带 query string 被吞掉。MCP SDK 内部走URI.resolve(base, endpoint)如果 endpoint 以/开头会把 base 上的?keyxxx直接吞掉。把 URL 拆成 origin 和相对 endpoint query 再喂给 builder 才能解决。Mcp 鉴权 401。鉴权要走 headers 字段配合httpRequestCustomizer注入每次 POST 都带上否则 server 端直接 401。Mcp 连接泄漏。MCP 走 HTTP 长流或 stdio 子进程忘了 close 会泄漏连接和进程。McpSession实现AutoCloseable同步接口用 try-with-resources流式接口在doFinally里关。沙箱环境变量丢失。Docker 走ExecSpec.env对应docker exec -eLocal 模式还得在命令前加export ...绕开bash -lc的 login profile 和 WSLENV 白名单导致的变量丢失。skill 文件 seed 进沙箱时二进制损坏。skill 默认是脚本加 Markdown 这类文本遇到超过阈值或读不出 UTF-8 的直接 skip 加 warn免得SandboxFiles.create把二进制损坏。seeding 过程中抛异常要立刻把已创建的 sandbox close 掉不要留孤儿容器或临时目录。消息顺序错乱。前面提过MessageWindowChatMemory内部HashSet会打乱顺序换成LinkedHashSet并把窗口化挪到读取侧。模型路由不生效。写一个ModelRouter按 modelName 前缀路由到不同 Bean/chat/stream入口拿请求里的 modelName 解析一下就行。每个 provider 自己的Bean配置照常写路由这层只是个 switch。6. 继续往下走四个模块跑通之后下一步是把任务调度接进来。用 JobRunr 做长期任务模型用工具调用TaskTool创建/调度任务TaskManager落库并往JobScheduler塞一条到点了 JobRunr 反序列化 lambda、回调TaskHandler.executeTask(taskId)执行。Job(retries 3)一行就能让失败自动重试三次。注意taskId而不是整个 Task 对象作为参数JobRunr 要把 lambda 序列化进存储参数得是简单可序列化的值。如果你要长期跑编码类 AgentCoding Plan 那条通道更适合 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接入过程中遇到 Mcp 或沙箱的问题先翻接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 再对着 API Keys 页面确认通道状态 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。控制台在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 可以看调用记录和用量。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。