AgentScope4J 实战:MCP、Function Calling、Hermes、Skills 全链路落地

很多团队在做 AI Agent 时,会把 MCPFunction CallingWorkflowSkills 混在一起,最后导致系统边界不清晰:

  • 模型在“决策层”做了太多“执行层”的事
  • 工具能力散落在不同模块,难治理、难审计
  • Demo 阶段能跑,线上阶段一旦扩展就失控

这篇文章给出一个工程化拆解:基于 agentscope4j 搭建一套清晰的分层,把这四个关键能力拼成一条可扩展、可观测、可治理的链路。


一、先把概念边界讲清楚

很多讨论无效,根源是“词对了,语义不对”。下面先做边界划分。

概念核心职责解决的问题不负责的事情
Function Calling让模型按 JSON Schema 选择并调用本地函数结构化参数、减少自由文本歧义跨进程发现工具、权限治理
MCP标准化“模型如何发现并调用远端工具”工具注册、工具描述、跨系统接入业务编排策略
Hermes(编排层)统一调度策略与执行控制路由、重试、超时、回退、审批工具协议本身
Skills可复用能力单元(提示词+工具+策略)把“能力”沉淀为可组合模块具体传输协议

一句话总结:
Function Calling 是“调用形态”,MCP 是“工具接入协议”,Hermes 是“执行与治理中枢”,Skills 是“业务能力封装”。


二、目标架构:四层分离

flowchart TD
    U["User Request"] --> A["AgentScope4J Agent (Planner)"]
    A --> H["Hermes Orchestrator"]
    H --> S1["Skill: Research"]
    H --> S2["Skill: Report"]
    S1 --> FC["Function Calling Adapter"]
    S1 --> MCP["MCP Client Adapter"]
    FC --> T1["Local Tool: SqlTool/FileTool"]
    MCP --> T2["Remote MCP Tools"]
    S2 --> T3["Formatter/Exporter"]
    T1 --> O["Observability: Trace/Log/Metrics"]
    T2 --> O
    T3 --> O

设计原则:

  1. Agent 只做“思考和决策”,不直接控制底层执行细节。
  2. Hermes 负责执行策略与安全治理。
  3. Skill 对业务可见,对协议细节透明。
  4. 统一观测链路(Prompt、Tool Call、Error、Cost)。

三、基于 AgentScope4J 的最小工程骨架

下面示例展示一个典型目录,便于落地:

src/main/java/com/example/agent/
├── app/
│   └── AgentApplication.java
├── hermes/
│   ├── HermesOrchestrator.java
│   ├── ExecutionPolicy.java
│   └── ToolGateway.java
├── skills/
│   ├── Skill.java
│   ├── ResearchSkill.java
│   └── ReportSkill.java
├── tools/
│   ├── fc/FunctionToolRegistry.java
│   ├── mcp/McpToolRegistry.java
│   └── model/ToolResult.java
└── obs/
    ├── TraceContext.java
    └── AgentMetrics.java

四、Function Calling:本地工具结构化调用

agentscope4j 中,Function Calling 通常通过“工具定义 + schema + 执行器”构建。关键是让模型只输出结构化参数,避免自由文本直接执行。

4.1 工具定义

public record ToolSpec(
        String name,
        String description,
        String jsonSchema
) {}

public interface FunctionTool {
    String name();
    ToolResult execute(Map<String, Object> args);
}

4.2 注册与执行

public class FunctionToolRegistry {
    private final Map<String, FunctionTool> tools = new HashMap<>();

    public void register(FunctionTool tool) {
        tools.put(tool.name(), tool);
    }

    public ToolResult invoke(String name, Map<String, Object> args) {
        FunctionTool tool = tools.get(name);
        if (tool == null) {
            return ToolResult.error("TOOL_NOT_FOUND", "Unknown tool: " + name);
        }
        return tool.execute(args);
    }
}

4.3 与 Agent 对接(示意)

// 伪代码:不同版本 agentscope4j API 名称可能略有差异
assistantAgent = ReActAgent.builder()
        .name("assistant")
        .sysPrompt("你是数据分析助手,必须优先通过工具获取事实。")
        .tools(functionCallingToolSpecs)
        .build();

工程重点:

  • 入参必须做 JSON Schema 校验(字段类型、范围、必填约束)
  • 工具执行必须有超时、重试、幂等键
  • 工具输出统一结构化(code, message, data

五、MCP:远端工具接入标准化

Function Calling 适合本地函数。
当工具在“别的服务/进程/团队”维护时,MCP 更合适。

5.1 MCP 接入层抽象

public interface McpToolClient {
    List<ToolSpec> listTools();
    ToolResult callTool(String toolName, Map<String, Object> args, Duration timeout);
}
public class McpToolRegistry {
    private final Map<String, McpToolClient> clients = new HashMap<>();

    public void register(String serverName, McpToolClient client) {
        clients.put(serverName, client);
    }

    public ToolResult call(String serverName, String toolName, Map<String, Object> args) {
        McpToolClient client = clients.get(serverName);
        if (client == null) {
            return ToolResult.error("MCP_SERVER_NOT_FOUND", serverName);
        }
        return client.callTool(toolName, args, Duration.ofSeconds(15));
    }
}

5.2 统一成“工具网关”

不建议让 Agent 直接知道“这次是 FC 还是 MCP”。
应该在 Hermes 内统一网关:

public class ToolGateway {
    private final FunctionToolRegistry fcRegistry;
    private final McpToolRegistry mcpRegistry;

    public ToolResult invoke(ToolCall call) {
        if (call.channel() == ToolChannel.FUNCTION) {
            return fcRegistry.invoke(call.toolName(), call.args());
        }
        return mcpRegistry.call(call.server(), call.toolName(), call.args());
    }
}

这样后续替换协议(例如 gRPC、HTTP Internal Tool API)不会影响上层 Skill。


六、Hermes:把“能调用”升级为“可治理地调用”

只靠模型“想明白”不够,线上系统必须有执行控制层。
这里把它命名为 Hermes Orchestrator

6.1 Hermes 解决的核心问题

  1. 工具选择策略(优先本地缓存,再查远端)
  2. 执行保护(超时、重试、熔断、并发上限)
  3. 安全与权限(allowlist、审批门、敏感操作拦截)
  4. 失败降级(回退路径、人工兜底)

6.2 执行策略模型

public record ExecutionPolicy(
        Duration timeout,
        int maxRetry,
        boolean requireApproval,
        Set<String> allowedTools
) {
    public boolean allowed(String toolName) {
        return allowedTools.contains(toolName);
    }
}

6.3 编排主流程

public class HermesOrchestrator {
    private final ToolGateway toolGateway;
    private final AgentMetrics metrics;

    public ToolResult executeToolCall(ToolCall call, ExecutionPolicy policy) {
        long start = System.currentTimeMillis();
        try {
            if (!policy.allowed(call.toolName())) {
                return ToolResult.error("TOOL_DENIED", call.toolName());
            }

            ToolResult result = Retry.withMax(policy.maxRetry(), () -> toolGateway.invoke(call));
            metrics.recordToolLatency(call.toolName(), System.currentTimeMillis() - start);
            return result;
        } catch (Exception e) {
            metrics.recordToolError(call.toolName(), e.getClass().getSimpleName());
            return ToolResult.error("TOOL_EXEC_ERROR", e.getMessage());
        }
    }
}

七、Skills:把“流程”沉淀成“能力模块”

Skill 不是单纯 Prompt 模板。
在工程上,Skill 应该包含:

  • 场景目标(这个能力要完成什么)
  • 触发条件(什么请求走这个 Skill)
  • 可用工具集(最小权限)
  • 输出契约(返回结构)
  • 失败策略(失败怎么退)

7.1 Skill 抽象

public interface Skill<I, O> {
    String id();
    boolean supports(TaskContext ctx);
    O run(I input, SkillRuntime runtime);
}
public record SkillRuntime(
        HermesOrchestrator hermes,
        AgentBase planner,
        TraceContext trace
) {}

7.2 示例:ResearchSkill

public class ResearchSkill implements Skill<ResearchRequest, ResearchResult> {
    @Override
    public String id() {
        return "research";
    }

    @Override
    public boolean supports(TaskContext ctx) {
        return ctx.intent().equals("research");
    }

    @Override
    public ResearchResult run(ResearchRequest input, SkillRuntime runtime) {
        ToolCall searchCall = ToolCall.mcp("search-mcp", "web_search", Map.of("q", input.topic()));
        ToolResult searchResult = runtime.hermes().executeToolCall(
                searchCall,
                Policies.RESEARCH_POLICY
        );
        return ResearchResult.from(searchResult.data());
    }
}

八、端到端链路:一个真实请求是如何跑起来的

以“分析某公司最近财报并输出简报”为例:

  1. AgentScope4J Planner 解析用户意图,选择 ResearchSkill + ReportSkill
  2. ResearchSkill 通过 Hermes 调用 MCP 搜索工具拉取资料
  3. Hermes 做权限校验、超时控制、重试与审计记录
  4. ReportSkill 使用 Function Calling 调本地格式化/导出工具
  5. 返回结构化结果(正文、引用、风险提示)

链路的关键不是“模型能不能答”,而是“整个执行是否可控可回放”。


九、生产级必须补齐的 6 个治理点

9.1 安全边界

  • 工具按 Skill 维度授权,而不是全局开放
  • 高风险工具(写库、发邮件、转账)必须二次确认
  • MCP 结果视为不可信输入,做内容清洗和输出约束

9.2 幂等与重试

  • 每次工具调用带 requestId,服务端保证幂等
  • 重试只对可重入操作开启
  • 区分“业务失败”和“系统失败”

9.3 可观测性

  • 记录 prompt_versiontool_namelatency_mstoken_cost
  • 同一条请求贯穿 traceId
  • 保留关键状态快照,支持回放

9.4 成本控制

  • 按 Skill 设置预算(token 与工具次数)
  • 优先缓存结果,减少重复工具调用
  • 模型分层:规划用强模型,格式化用轻模型

9.5 上下文治理

  • 长会话做摘要压缩,避免上下文污染
  • 工具返回内容先结构化,再入记忆
  • Skill 之间共享“事实层”,不共享全部原始对话

9.6 失败兜底

  • 工具连续失败时自动降级到“只读回答”
  • 给出“无法执行原因 + 建议下一步”
  • 必要时切人工流程

十、常见反模式(你会很快踩到)

  1. 把所有工具直接丢给一个 Agent,让模型自由发挥
  2. 没有 Execution Policy,线上行为不可预期
  3. 只记聊天日志,不记工具调用链
  4. Skill 只是 Prompt 拼接,缺少输入输出契约
  5. MCP 调用成功就直接信任结果,不做安全检查

十一、一个可执行的落地路线(两周版本)

第 1-3 天:跑通最小闭环

  • 接入 agentscope4j 基础 Agent
  • 实现 FunctionToolRegistry 和 2 个本地工具
  • 打通单 Skill 流程

第 4-7 天:接入 MCP 与 Hermes

  • 封装 McpToolClient
  • 实现 ToolGateway
  • 加入 ExecutionPolicy(超时、重试、allowlist)

第 8-10 天:Skill 体系化

  • 定义统一 Skill 接口与 SkillRuntime
  • 抽离 ResearchSkillReportSkill
  • 完成路由与结果契约

第 11-14 天:治理上线

  • 接入 trace/metrics/log
  • 增加审批门和风险工具隔离
  • 做回放测试与失败注入测试

十二、AgentScope4J API 与底层实现补充

前面主要讲的是工程分层。
这一节补你最关心的两件事:AgentScope4J 常见 API 怎么用,以及这些 API 背后大致怎么跑。

说明:不同版本 agentscope4j 的类名和 builder 细节可能有差异,下面以常见用法和稳定抽象为主(AgentBase/ReActAgent/MsgHub/Pipeline)。

12.1 Agent 初始化与基础调用

典型创建方式:

ReActAgent analyst = ReActAgent.builder()
        .name("analyst")
        .sysPrompt("""
                你是研究员,先澄清问题,再调用工具,最后给出结构化结论。
                """)
        .tools(toolkit)                 // Function Calling 工具集合
        .memory(windowMemory)           // 短期记忆
        .maxTurns(8)                    // ReAct 循环上限
        .build();

Msg userMsg = Msg.user("请分析 A 公司本季度利润变化原因");
analyst.observe(userMsg).block();       // 只写入记忆,不触发模型
Msg answer = analyst.call().block();    // 执行一次完整推理/工具循环

关键点:

  • observe() 负责“摄入上下文”
  • call() 负责“触发生成”
  • 把两者拆开,是 AgentScope 在工程上非常实用的设计

12.2 多 Agent 协作:MsgHub 用法

你之前写过 MsgHub 文章,这里给一个和本篇主题对齐的简版:

try (MsgHub hub = MsgHub.builder()
        .name("analysis_hub")
        .participants(researcher, riskReviewer, writer)
        .announcement(Msg.system("请围绕同一主题协作,先事实后结论"))
        .enableAutoBroadcast(true)
        .build()) {

    hub.enter().block(); // 建立订阅关系并广播公告

    researcher.call().block();
    riskReviewer.call().block();
    writer.call().block();
}

enableAutoBroadcast(true) 的含义是:某个 Agent call() 完成后,其消息会自动广播给其他参与者并触发其 observe(),从而形成共享上下文。

12.3 并发执行:Pipeline/Fanout

当你希望多个 Agent 独立执行同一任务(如投票、打分)时,可以用 fanout 模式:

FanoutPipeline pipeline = FanoutPipeline.builder()
        .addAgents(reviewerA, reviewerB, reviewerC)
        .concurrent()
        .build();

List<Msg> reviews = pipeline
        .execute(Msg.user("请分别给出该方案风险评分"), ScoreCard.class)
        .block();

适用场景:

  • 并行评审
  • 多专家投票
  • 候选答案重排

12.4 结构化输出与函数参数约束

推荐让 Agent 的关键输出走结构化对象,避免后处理正则地狱:

public record InvestmentReport(
        String summary,
        List<String> riskPoints,
        List<String> evidence
) {}

InvestmentReport report = reportAgent.call(
        Msg.user("输出投资分析报告"),
        InvestmentReport.class
).block();

同理,Function Calling 入参最好做 Schema 约束:

public record QueryArgs(
        @NotBlank String symbol,
        @Min(1) @Max(8) int quarters
) {}

12.5 一个完整的 AgentScope + Hermes 调用片段

Msg req = Msg.user("总结公司 2025Q4 到 2026Q1 的经营变化");
planner.observe(req).block();

Msg planMsg = planner.call().block();                      // 产出执行计划
ToolCall toolCall = ToolCallParser.from(planMsg);          // 从计划提取工具动作
ToolResult data = hermes.executeToolCall(toolCall, policy); // Hermes 控制执行

writer.observe(Msg.system("工具结果如下:" + data.data())).block();
Msg finalMsg = writer.call().block();                      // 生成最终报告

这段代码体现了“Agent 做计划,Hermes 做执行”的职责分离。


12.6 底层原理一:为什么 observe()call() 必须分离

在 AgentScope 的常见实现中:

  • observe(msg):写 memory,不触发 LLM
  • call():读取 memory,执行 ReAct 循环,产生回复

简化后的思路:

protected Mono<Void> doObserve(Msg msg) {
    memory.addMessage(msg);
    return Mono.empty();
}

public Mono<Msg> call() {
    return plannerLoop(memory.snapshot());
}

好处是:

  • 防止“收到消息就自动回复”的无限连锁
  • 让调度器(MsgHub/Pipeline/Hermes)精准控制何时生成
  • 更容易做回放和调试(输入与触发时机可复现)

12.7 底层原理二:ReAct 循环如何工作

大致是一个有限状态机:

  1. 读取当前 memory + system prompt
  2. 模型产出 Thought/Action(或 tool call)
  3. 若有工具动作,执行工具并把结果写回 memory
  4. 继续下一轮推理,直到 Final Answer 或达最大轮次

简化伪代码:

for (int i = 0; i < maxTurns; i++) {
    ModelOutput out = llm.generate(context);
    if (out.hasToolCall()) {
        ToolResult tr = toolGateway.invoke(out.toolCall());
        memory.addMessage(Msg.tool(tr));
        continue;
    }
    return Msg.assistant(out.finalAnswer());
}
return Msg.assistant("达到最大推理轮次,返回降级结果");

这也是为什么 maxTurns、工具超时、重试策略必须配置,不然很容易出现高成本循环。

12.8 底层原理三:MsgHub 自动广播机制

你可以把 MsgHub 看成“临时会话总线”:

  • enter() 时建立参与者之间的订阅关系
  • 某 Agent call() 成功后,框架把结果 broadcast 给其他 Agent
  • 其他 Agent 仅 observe() 消息,不自动 call()

这让多智能体协作既有信息流,又不会雪崩式互相触发。

12.9 底层原理四:Reactor 模型与非阻塞链路

agentscope4j 常见实现基于 Mono/Flux,意味着:

  • Agent 调用是可组合的异步流
  • 工具调用可并发但要限流
  • 错误处理可统一挂在 reactive pipeline 上

比如统一超时与回退:

agent.call()
    .timeout(Duration.ofSeconds(20))
    .onErrorResume(ex -> Mono.just(Msg.assistant("暂时无法完成,已返回降级结果")));

12.10 把底层机制映射回工程治理

有了上面这套机制,Hermes 层就有抓手了:

  • call() 前后注入 trace/span
  • 在工具执行点统一做审批/审计
  • 在 memory 写入前做内容清洗(防 prompt injection 污染)
  • 在 ReAct 每轮做 budget 检查(token、工具次数、时延)

所以真正可上线的关键,不是“再加一个模型”,而是把 AgentScope 的执行节点接到治理节点上。


十三、总结

MCPFunction CallingHermesSkills 并不是互斥关系,而是四个不同层面的能力。
如果你在 agentscope4j 里把它们分层清楚,系统会从“能跑 demo”变成“可演进的工程系统”。

最终建议是:

  • Function Calling 管本地结构化调用
  • MCP 管跨系统工具接入
  • Hermes 管执行策略与风险
  • Skills 管业务能力复用与组合

这样做的收益,不是“回答更花哨”,而是“线上可控、问题可查、能力可复用”。