AgentScope4J 实战:MCP、Function Calling、Hermes、Skills 全链路落地
很多团队在做 AI Agent 时,会把 MCP、Function Calling、Workflow、Skills 混在一起,最后导致系统边界不清晰:
- 模型在“决策层”做了太多“执行层”的事
- 工具能力散落在不同模块,难治理、难审计
- 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
设计原则:
Agent只做“思考和决策”,不直接控制底层执行细节。Hermes负责执行策略与安全治理。Skill对业务可见,对协议细节透明。- 统一观测链路(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 解决的核心问题
- 工具选择策略(优先本地缓存,再查远端)
- 执行保护(超时、重试、熔断、并发上限)
- 安全与权限(allowlist、审批门、敏感操作拦截)
- 失败降级(回退路径、人工兜底)
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());
}
}
八、端到端链路:一个真实请求是如何跑起来的
以“分析某公司最近财报并输出简报”为例:
AgentScope4J Planner解析用户意图,选择ResearchSkill + ReportSkillResearchSkill通过Hermes调用 MCP 搜索工具拉取资料Hermes做权限校验、超时控制、重试与审计记录ReportSkill使用 Function Calling 调本地格式化/导出工具- 返回结构化结果(正文、引用、风险提示)
链路的关键不是“模型能不能答”,而是“整个执行是否可控可回放”。
九、生产级必须补齐的 6 个治理点
9.1 安全边界
- 工具按 Skill 维度授权,而不是全局开放
- 高风险工具(写库、发邮件、转账)必须二次确认
- MCP 结果视为不可信输入,做内容清洗和输出约束
9.2 幂等与重试
- 每次工具调用带
requestId,服务端保证幂等 - 重试只对可重入操作开启
- 区分“业务失败”和“系统失败”
9.3 可观测性
- 记录
prompt_version、tool_name、latency_ms、token_cost - 同一条请求贯穿
traceId - 保留关键状态快照,支持回放
9.4 成本控制
- 按 Skill 设置预算(token 与工具次数)
- 优先缓存结果,减少重复工具调用
- 模型分层:规划用强模型,格式化用轻模型
9.5 上下文治理
- 长会话做摘要压缩,避免上下文污染
- 工具返回内容先结构化,再入记忆
- Skill 之间共享“事实层”,不共享全部原始对话
9.6 失败兜底
- 工具连续失败时自动降级到“只读回答”
- 给出“无法执行原因 + 建议下一步”
- 必要时切人工流程
十、常见反模式(你会很快踩到)
- 把所有工具直接丢给一个 Agent,让模型自由发挥
- 没有 Execution Policy,线上行为不可预期
- 只记聊天日志,不记工具调用链
- Skill 只是 Prompt 拼接,缺少输入输出契约
- MCP 调用成功就直接信任结果,不做安全检查
十一、一个可执行的落地路线(两周版本)
第 1-3 天:跑通最小闭环
- 接入
agentscope4j基础 Agent - 实现
FunctionToolRegistry和 2 个本地工具 - 打通单 Skill 流程
第 4-7 天:接入 MCP 与 Hermes
- 封装
McpToolClient - 实现
ToolGateway - 加入
ExecutionPolicy(超时、重试、allowlist)
第 8-10 天:Skill 体系化
- 定义统一
Skill接口与SkillRuntime - 抽离
ResearchSkill、ReportSkill - 完成路由与结果契约
第 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,不触发 LLMcall():读取 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 循环如何工作
大致是一个有限状态机:
- 读取当前 memory + system prompt
- 模型产出
Thought/Action(或 tool call) - 若有工具动作,执行工具并把结果写回 memory
- 继续下一轮推理,直到
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 的执行节点接到治理节点上。
十三、总结
MCP、Function Calling、Hermes、Skills 并不是互斥关系,而是四个不同层面的能力。
如果你在 agentscope4j 里把它们分层清楚,系统会从“能跑 demo”变成“可演进的工程系统”。
最终建议是:
- 用
Function Calling管本地结构化调用 - 用
MCP管跨系统工具接入 - 用
Hermes管执行策略与风险 - 用
Skills管业务能力复用与组合
这样做的收益,不是“回答更花哨”,而是“线上可控、问题可查、能力可复用”。