AgentScope Java 是如何调用 Tool 的?原理拆解 + UML 图解
AgentScope Java 是如何调用 Tool 的?原理拆解 + UML 图解
AgentScope Java 是阿里通义实验室开源的 Java Agent 框架。本文不贴一行运行代码,只讲透一件事:当模型说"我要调用工具"时,背后到底发生了什么。全程以 2.0.x 源码为蓝本,并附 3 张 UML 图。
目录
1. 一句话讲清原理
AgentScope Java 的工具调用,是 ReAct 循环里的一个"执行阶段":LLM 在推理时看到工具的 JSON Schema,决定"该调哪个工具、填什么参数",然后以一段结构化 JSON(ToolUseBlock)声明这个决定;框架拿到声明后,先过权限系统,再交给 Toolkit 路由到具体工具执行,把结果(ToolResultBlock)回填进对话上下文,喂回模型继续推理,直到模型不再调用工具。
拆开看就是三个动作的循环:推理 → 行动 → 观察(Reason → Act → Observe),这正是 ReAct(Reasoning + Acting)范式的名字由来。
2. 前置:LLM 到底是怎么"调用"工具的
很多人第一次接触 Agent 时会卡在这里:LLM 不是只会生成文本吗?它凭什么会"调用工具"?
答案是三个字:声明(Declaration)。LLM 永远不会真正执行任何东西,它只负责在回复里"声明"一个调用意图。真正动手执行的是框架。
2.1 模型必须具备的 7 项能力
| # | 能力 | 说明 |
|---|---|---|
| 1 | Function Calling / Tool Use | 模型要经过专门训练(SFT + RL),才能输出"结构化工具调用声明",而不是把意图写成普通文本。OpenAI 2023 年 6 月首发,之后主流模型(含国产 Qwen / DeepSeek / GLM / Kimi 等)普遍支持 |
| 2 | 结构化 JSON 输出 | 工具参数必须是合法 JSON,且和给定的 JSON Schema 匹配。解码层就要保证 |
| 3 | 工具选择(Tool Selection) | 从几十个工具的 name / description 里挑出对的那一个,并判断"此刻该不该调" |
| 4 | 参数映射(Parameter Filling) | 把用户的自然语言意图翻译成参数值:类型转换、枚举取值、必填项填充 |
| 5 | 多步规划 & 状态跟踪 | 在循环里记住"已调过什么、结果是什么",据此规划下一步,靠上下文维持 |
| 6 | 停止判断(Stop) | 知道什么时候不用再调了,直接给最终答案,避免死循环空转 |
| 7 | 指令遵循(Instruction Following) | 严格按 Schema 填参、不乱填、不编造不存在的工具名 |
2.2 底层原理:从训练到推理
① 工具调用是"教"出来的,不是天生就会
普通 LLM 只会续写文本。工具调用能力来自两个阶段的专门训练:
- 指令微调(SFT):喂入"工具定义 + 对话 + 工具调用序列 + 工具结果 + 最终回复"的完整轨迹数据,让模型学会在恰当的时机输出调用声明;
- 强化学习(RL):进一步教会模型"该调用时就调用、不该调时不调"的边界,避免滥用或不敢用。
新一代 reasoning 模型还会先输出"思考"再决定调用,工具调用成了推理链上自然的一环。
② 消息协议:模型只输出"声明",执行归框架
调用方把工具定义以 tools=[{type:"function", function:{name, description, parameters}}] 传入请求;模型返回的 assistant 消息里带 tool_calls 数组(每个含 id、function.name、function.arguments——注意 arguments 是一个 JSON 字符串)。
这是"模型无关"的关键设计:模型只负责产出结构化的声明文本,执行、权限、审计、沙箱全部由框架接管。
③ 流式累积:工具参数可能"分批到达"
流式推理时,一个工具调用的 arguments 可能跨多个 chunk 返回。框架必须按 tool call id 累积合并参数,否则就会出现经典的"流式 tool call 参数不完整"bug(AgentScope 官方仓库就有过类似 issue)。AgentScope Java 里的 ToolCallsAccumulator 就是干这个的。
④ 兜底保障
即使模型偶尔输出非法 JSON,框架也可以:
- 在推理端用约束解码保证合法(如 vLLM guided decoding、llama.cpp GBNF);
- 在拿到输出后做 schema 校验兜底(
ToolValidator); - 模型调用失败时自动重试 / 切换 fallback 模型(2.0 特性),避免整个长任务中断。
给选型者的提醒:模型不支持 function calling,框架做得再好也白搭;模型上下文长度决定 ReAct 能稳定跑多少轮。
3. 框架侧三个核心概念
| 概念 | 职责 | 关键类/机制 |
|---|---|---|
| Tool(工具) | 一个可被 LLM 调用的能力单元,对 LLM 暴露为 JSON Schema | AgentTool 接口;ToolBase(抽象基类);@Tool 注解方法会被包装成 ReflectiveFunctionTool;MCP 工具包装为 McpTool |
| Toolkit(工具包) | 注册、管理、暴露工具,并负责分发每次调用 | Toolkit 是门面,内部分工给 ToolRegistry(注册/查找)、ToolGroupManager(工具组)、ToolSchemaProvider(生成暴露给模型的 schema)、McpClientManager、ToolExecutor(执行器) |
| Tool Group(工具组) | 一组工具的命名开关,可整组激活/停用,控制暴露给模型的工具面 | ToolGroup + 内置 meta tool(源码里叫 reset_equipped_tools)让模型在运行期动态切换 |
一句话记住:Tool 定义"能做什么",Toolkit 管"怎么调",Tool Group 管"给模型看哪些"。
4. 完整调用链路:一次工具调用的旅程
4.1 注册:工具怎么"变成"可调用的
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new SimpleTools()); // @Tool 注解方法 → 反射扫描
toolkit.registerAgentTool(new WebSearchTool()); // AgentTool / ToolBase 实例
toolkit.registerMcpClient(mcpWrapper); // MCP 客户端registerTool(Object)用反射扫描@Tool注解方法,每个方法被包装成ReflectiveFunctionTool(ToolBase子类),并由ToolSchemaGenerator从 Java 类型自动推导出 JSON Schema;AgentTool实例直接注册;ToolBase由开发者手动声明inputSchema;- 注册后进入
ToolRegistry,按工具名索引,同时挂到ToolGroupManager的组里(默认"basic"组,常驻激活)。
4.2 推理:模型怎么"知道"有哪些工具
每次调用 LLM 前,ReActAgent 通过 Toolkit.getToolSchemas(activeGroups) 拿到当前激活工具组的工具列表,把每个工具的 name / description / parameters(JSON Schema)拼进模型请求。
模型看到的不是 Java 方法,而是一份 JSON Schema——它"会调用工具"全靠这份 schema。
4.3 行动:模型声明调用 → 框架执行
模型返回的 assistant Msg 里若包含 ToolUseBlock(字段 id / name / input / state),就进入行动阶段。核心代码路径:actingStream() → evaluatePermissions() → runToolBatch() → executeToolCalls() → Toolkit.callTools()。
① 权限门(PermissionGate)
evaluatePermissions() 把每个 ToolUseBlock 送进 PermissionEngine(权限上下文为空时走轻量路径:只认工具自身 checkPermissions 返回的 ASK),产出三态:
ALLOW→ 继续执行DENY→ 不调用工具,直接合成ToolResultBlock("Permission denied by rules", state=DENIED)回填上下文,让模型看到"被拒了"ASK→ 发射RequireUserConfirmEvent、返回GenerateReason.PERMISSION_ASKING并暂停,等外部喂回ConfirmResult恢复(Human-in-the-Loop)
② 路由与执行(ToolExecutor.executeCore)
- 按
toolCall.getName()从ToolRegistry查AgentTool; - 外部工具(
isExternalTool()==true,如SchemaOnlyTool)短路为ToolResultBlock.suspended→TOOL_SUSPENDED,把调用抛给外部(人/系统)执行; - 校验工具组是否激活、用
ToolValidator按 schema 校验参数; - 合并 preset 参数 + RuntimeContext;
- 调用
AgentTool.callAsync(ToolCallParam),返回Mono<ToolResultBlock>。
③ 基础设施(ToolExecutor.executeWithInfrastructure)
- 调度:默认
Schedulers.boundedElastic()(或自定义ExecutorService); - 超时:
ExecutionConfig.timeout; - 重试:
Retry.backoff(maxAttempts-1, initialBackoff)带 jitter; - 优雅停机保护;
- 并发策略:
executeAll按isConcurrencySafe()分组——安全工具用Flux.mergeSequential并发跑(保持输出顺序),不安全的工具串行,避免共享状态被并发踩踏。
④ 反射调用与结果转换
- 参数转换:带
@ToolParam的参数按名字从 LLM 给的 JSON 里取值;不带注解的参数按类型注入(ToolEmitter/Agent/AgentState/RuntimeContext/ 业务 POJO); - 结果转换:返回值由
ToolResultConverter(默认DefaultToolResultConverter)转成ToolResultBlock(可含 TextBlock / DataBlock 等)。
⑤ 回填上下文,继续循环
工具结果作为 ToolResultMessage 追加进 AgentState.getContext() → 回到"决定下一步"→ 再次调 LLM(此时模型能看到工具结果)→ 若无新工具调用则返回最终回复。整个循环受 maxIters(默认 10)约束。
5. UML 图解
图 1:Tool 子系统类图
图 2:一次完整 Tool 调用时序图
图 3:行动阶段决策活动图
6. 原理要点总结
- 声明—执行分离:LLM 只输出工具调用声明(JSON),框架负责执行——这是所有 Agent 框架的底层范式。
- Schema 驱动:工具对 LLM 只是 JSON Schema;模型"会调用"靠的是 schema + 模型自身的 function calling 能力,而不是 Java 反射。
- 统一执行抽象:本地
@Tool方法、ToolBase工具、MCP 工具、外部工具,全部收敛成AgentTool接口,由Toolkit/ToolExecutor统一分发——"智能工具总线"思想。 - 异步响应式:整条链路基于 Project Reactor(
Mono/Flux),天然支持流式工具结果、并发、超时重试。 - 权限是"执行前哨兵":每次调用先过
PermissionEngine,DENY / ASK / ALLOW三态;ASK对应 Human-in-the-Loop 暂停恢复。 - 循环闭环:工具结果作为消息回填上下文,驱动下一次推理,直到模型不再调用工具。
参考
- AgentScope Java 官方文档(v2):Tool / Permission System / Agent / Message & Event / Harness Architecture
- AgentScope Java 2.0 发布说明
- AgentScope Java 2.0.x 源码(
agentscope-core模块)