跳转到主要内容

概述

Tool 是 agent 与外部世界交互的方式 —— 执行业务操作、调用 API、读写数据等。每个 tool 通过 JSON Schema 暴露给 LLM,agent 通过统一接口完成调用。 AgentScope 把 tool 相关的构件组织成三个概念:
  • Tool —— 任意实现 AgentTool 接口(通常通过继承 ToolBase)或在普通类的方法上标注 @Tool 注解的对象。Java 端把后者称为 reflective function tool,由 Toolkit#registerTool(Object) 自动反射注册。
  • Toolkit —— 容器,负责注册 tool、MCP 客户端与 skill,向模型暴露它们的 JSON schema,并把每次工具调用分发到对应的 tool 对象。
  • Tool Group —— 一组带名称的 tool / MCP / skill 集合,可以作为整体激活或停用。Agent 在运行时通过内置 meta tool 切换 group,让上下文保持聚焦。
只调用 registerTool(Object) 时,被注册对象上所有 @Tool 方法都进入特殊的 "basic" 组 —— 该组始终激活。追加 MCP 客户端、tool group 或 skill 即可拓展 agent 的能力 —— 见下文各节。

Java Tool

Java tool 是任意满足 AgentTool 契约的对象。AgentScope 同时提供了一个 ToolBase 抽象基类用于显式建模带参数 schema 的 tool,以及一个反射适配器用于把普通方法包装成 tool。

AgentTool / ToolBase 接口

ToolBase 是 AgentTool 的抽象实现,下表列出其属性与方法。 向 agent 与运行时描述 tool 的属性: 接入执行流程与权限系统的方法:

使用内置 Tool

AgentScope 当前提供以下内置 tool: 使用方式:
Toolkit 在出现额外 tool group 或 skill 时会自动注册 reset_tools meta tool 与 skill 查看器工具 load_skill_through_path,开发者无需手动实例化。详见 自我管理 Tool 与 Skill。

自定义 Tool(注解式)

最轻量的写法:在普通类的方法上标注 @Tool 与 @ToolParam,然后通过 Toolkit#registerTool(Object) 反射注册。框架自动从 Java 类型推导 JSON schema,从 description 取面向 agent 的说明。
@Tool 常用属性:

显式返回执行状态

使用默认 converter 时,注解工具的 void 或 Void 返回值,以及成功完成的 Mono<Void>、CompletableFuture<Void>,会向模型返回 (completed with no output),避免静默成功被误认为缺失的工具结果。普通 null 返回值仍为 null;错误和取消不会生成这条成功确认。 工具调用成功时返回 ToolResultBlock.success(text),失败时返回 ToolResultBlock.error(message);两者既可直接返回,也可包装在 Mono<ToolResultBlock> 中。不要通过文本前缀判断失败。ToolResultBlock.text(text) 默认是 RUNNING;如需保留既有 JSON 或多行输出,请用 withState(...) 明确终态。 对于普通客户端工具,Anthropic formatter 将 ERROR、DENIED、INTERRUPTED 映射为 is_error: true,同时保留调用 ID 和输出内容块。SUCCESS、RUNNING、SUSPENDED 不会被 formatter 判为错误。供应商执行的 server tool 仍使用独立的转换路径。 内置工具已改为结构化结果。直接调用时用 getState() 判断状态,用 getOutput() 读取文本;Toolkit 注册方式不变。

自定义 Tool(继承 ToolBase)

需要自定义权限策略、外部执行或更复杂的 schema 时,继承 ToolBase:

定义外部执行 Tool

外部执行 tool 把实际执行委派给 agent 运行时之外 —— 通常是人工操作员或外部系统。Agent 调用此类 tool 时会发出 RequireExternalExecutionEvent 并暂停。下一次调用回传匹配的 ToolResultBlock 后,agent 会发出带有相同 replyId 的 ExternalExecutionResultEvent,然后继续执行。 这种模式是 human-in-the-loop 工作流的基础 —— 某些动作需要人工确认或人工执行。 创建外部执行 tool 只需把 externalTool 设为 true,不必实现 callAsync:
完整可运行示例:agentscope-examples/documentation/.../tool/ToolBaseExample.java、tool/ToolExecutionContextExample.java。

重名冲突与显式替换

Tool 名字是下游一切逻辑的查找键——schema 生成、权限规则、Tool Group、确认处理都按名字解析一次调用。因此,注册一个名字已经被另一个 tool 占用的工具会立刻抛出 IllegalStateException,而不是静默覆盖原有的注册:
MCP tool 走同一道校验:远端工具与本地工具(或另一个 server 的工具)重名时,会在该 server 的工具注册当场报错。想两者共存,注册 client 时配置 toolNamePrefix,或给其中一个改名。 重复注册同一个逻辑工具依然保持幂等,刷新类场景无需特殊处理:同一个被注解对象加同一个方法、同一个 MCP client 重新注册自己的工具(重连 / tool 列表刷新)、以及用 registerSchema 重新声明一份内容完全相同的外部工具 schema,都会被识别为刷新而放行。同名但内容变了的 schema 属于冲突:先移除旧注册,或换一个名字。 如果就是要有意覆盖——在同一个名字下换一份实现——使用显式替换 API:
replaceAgentTool 是唯一的有意替换入口;其余所有注册路径(registerTool、registerAgentTool、registerSchema、sub-agent 与 MCP 注册)都经过 fail-fast 校验。 注解工具通过 getTool 取得后,再用 registerAgentTool 或 registerTool 直接注册,也按源对象实例与 Java 方法判断同源;刷新会安装传入工具的注册配置。MCP 刷新要求 wrapper 实例和远端工具名同时相同;不同前缀映射得到相同本地名称,不代表远端目标同源。 HarnessAgent.Builder.fromAgent(source) 会重建源 Harness 创建的工具,包括非活跃分组中的工具,并保留用户工具及显式替换的实现。如果保留的用户工具与 Harness 能力重名,可在新 builder 上禁用该能力,或使用不同的工具名。 构建时需要重新绑定工具时,Toolkit.copyExcludingTools(instances) 仅排除指定的实例;同名工具已被替换为新实例时,会保留新绑定。副本保留工具分组关系及 Toolkit 配置,包括 allowToolDeletion(false),源 Toolkit 不受影响。

接收 Context

每次 agent.call(msgs, runtimeContext) 传入的 RuntimeContext 会自动透传到所在 reply 内每一次工具调用。Tool 可以用两种方式拿到它:注解式 tool 走自动注入,ToolBase.callAsync 走 ToolCallParam。

自动注入(@Tool 方法)

@Tool 方法签名里,没有标注 @ToolParam 的参数会被框架视为「需要从框架注入」,并按下表的优先级解析: 注入的 ToolEmitter 会提供当前工具调用 ID,可用于把进度与前端状态或其它共享存储关联起来:
当 emitter 没有工具调用上下文时(例如 no-op 或自定义 emitter),getToolCallId() 返回 null。 「用户自定义 POJO」的判定:参数没有 @ToolParam、不是基本类型、不是 ContentBlock / Msg、不在 java.* / javax.* 包下。其余参数(带 @ToolParam 或属于上述兜底类型)从 LLM 提供的 JSON 输入按名称取值。
调用方按类型注册同款 POJO 后,每次 call 就会自动把对应实例分发到所有需要它的 tool:
模型不需要把 userCtx 写进 JSON 参数——schema 里也不会出现它。完整示例:agentscope-examples/documentation/.../tool/ToolExecutionContextExample.java。

工具进度流式输出

长时间运行的工具可以通过 ToolEmitter.emit() 发布中间进度。进度以 ToolProgressEvent 的形式出现在事件流中——仅用于展示,不会发送给 LLM。工具的返回值才是模型实际接收到的内容,它以 ToolResultTextDeltaEvent / ToolResultDataDeltaEvent 的形式单独传递。
从事件流重建工具结果的客户端必须累积 ToolResult*DeltaEvent,并忽略 ToolProgressEvent。如果混用两者,进度文本会进入工具消息并在下一轮被送回模型,违背 ToolEmitter 的契约。

ToolBase.callAsync 中访问

继承 ToolBase 的 tool 通过 ToolCallParam 取 context:
ToolCallParam 同时暴露 getAgent()、getInput()、getEmitter()、getToolUseBlock() 以及(已 deprecated 的)getContext()。新代码使用 getRuntimeContext()。

协调 hook 与 tool

RuntimeContext 的 string 层(put(String, Object) / get(String))是同一次 call 内 middleware 与 tool 之间的临时通信通道——middleware 在 onActing/onReasoning 等位置写入,tool 通过注入 RuntimeContext 参数读取;调用结束后该实例与 hook 一并解绑。

MCP

AgentScope 集成 Model Context Protocol (MCP),让 agent 可以接入任意 MCP 兼容的工具提供方。框架自动处理协议协商、工具发现与结果转换。 支持三种连接方式:
  • STDIO —— 本地进程 stdin/stdout 通信
  • SSE / Streamable HTTP —— 远程 HTTP 长连接
MCP tool 在 toolkit 中以 mcp__{server_name}__{tool_name} 命名,避免冲突;标注了 readOnlyHint 的 tool 会被权限系统自动放行。

注册 MCP Tool

通过 McpClientBuilder 构建 McpClientWrapper,再注册到 Toolkit:
完整运行示例:agentscope-examples/documentation/.../mcp/McpStdioExample.java、mcp/McpSseExample.java、mcp/McpStreamableHttpExample.java。

Skill

Skill 是基于 markdown 的指令集,无需写新工具代码即可拓展 agent 能力。每个 skill 是一个目录,包含一个带 frontmatter 元数据与详细指令的 SKILL.md 文件。 与 tool 不同,skill 不能被直接调用。Agent 通过自动注册的查看器工具 load_skill_through_path 读取 skill 指令,再用现有的 tool 按指令执行。

注册 Skill

通过 ReActAgent.builder().skillRepository(...) 直接挂载一个或多个 AgentSkillRepository。Builder 在 build() 时自动装配 DynamicSkillMiddleware,每次 call() 都会按 skill 来源刷新 skill prompt 与 tool group:
多次调用 skillRepository(...) 按调用顺序追加(低 → 高优先级),同名 skill 后者覆盖前者;如需替换整批,调用 skillRepositories(List<AgentSkillRepository>)。 参考实现:agentscope-examples/documentation/.../skill/AgentSkillExample.java、skill/SkillWithToolGroupExample.java。

Skill 的工作方式

Toolkit 在含 skill 时,注册与查看分两阶段进行。 初始化阶段:
  • Toolkit 扫描所有注册的 skill 来源,收集每个 skill 的名称、描述与目录。
  • 自动把内置查看器工具 load_skill_through_path(实现位于 io.agentscope.core.skill.SkillToolFactory)作为无分组、始终可见的工具注册。
  • 组装一段 system prompt 片段,列出可用 skill(仅名称与描述),并指示 agent 通过 load_skill_through_path 读取完整内容。
运行时阶段,agent 用两个必填参数调用查看器: 调用示例:
每次成功调用产生两件事:
  1. 返回请求的内容(SKILL.md markdown,或指定的资源文件)。
  2. 激活该 skill —— Toolkit 中与之绑定的 tool group 被启用,本轮对话余下时段都可调用 skill 自带的工具。如果 path 不存在,查看器会返回错误并列出可用资源路径(SKILL.md 始终排在第一位),便于 agent 重试。
Skill 不是 tool —— agent 不能直接调用 skill。它必须先用 load_skill_through_path 读取指令,再用其他 tool 按描述的步骤执行。

Skill 执行脚本:配置 Shell 工具

Skill 只提供指令,真正的执行依赖 agent 已有的 tool。如果 skill 指令涉及脚本执行(例如 scripts/run.py),agent 需要拥有 shell 执行能力:
  • ReActAgent —— 注册 ShellCommandTool 到 toolkit:
  • HarnessAgent —— harness 模块自带 workspace 感知的 shell 与文件工具(execute、read_file、write_file 等),无需额外注册。

Skill + ToolGroup:按需披露工具

SkillToolGroup 把一组 tool 绑定到某个 skill name —— agent 加载该 skill 时 tool group 自动激活,未加载时 tool 不出现在模型 schema 中,减少上下文噪音。
当 agent 通过 load_skill_through_path 加载名为 data-analysis 的 skill 时,analysis-tools group 自动激活,其中的 tool 立即可用。配合 enableMetaTool(true),模型还可以通过 reset_tools 主动管理 tool group 的激活状态。 参考实现:agentscope-examples/documentation/.../skill/SkillWithToolGroupExample.java。

自我管理 Tool

内置 meta tool(reset_tools)让 agent 在运行时自我管理哪些 tool group 处于激活状态,从而保持上下文聚焦 —— 只有与当前任务相关的 tool 暴露给模型。

定义 Tool Group

ToolGroup 是带名称的 tool / MCP / skill 集合。把 group 注册到 Toolkit 后再用 builder 启用 meta tool:
ToolGroup 接收名称、描述、作用域(ToolGroupScope)以及初始激活态。保留名 "basic" 由 Toolkit#registerTool(Object) 自动构成,且始终激活。

使用 Meta Tool

只要存在至少一个非 basic 的 tool group,并通过 enableMetaTool(true) 打开开关,Toolkit 就会自动注册 reset_tools 并把其 schema 暴露给 agent。每个非 basic group 在 schema 中表示为一个布尔字段,agent 调用 meta tool 时声明期望的最终状态。 运行时行为:
  • "basic" 组中的 tool 始终暴露,meta tool 不会影响它们。
  • 每次调用 reset_tools 都会整体覆盖激活集合 —— 任何未显式置为 true 的非 basic group 都会被停用,无论之前的状态。
  • 对每个本次切换为激活的 group,其 description 与(若提供的)使用说明会被拼接进 meta tool 的返回值,告诉 agent 如何正确使用该组。
  • 未激活 group 中的 tool 不会出现在 agent 的工具 schema 中,从而把上下文留给当前激活的工具集。
Meta tool 的输入表示所有 group 的最终状态而非增量。任何未显式置为 true 的 group 都会被停用,无论之前的状态如何。

按需搜索工具

工具目录较大时,可以启用 ToolSearchMiddleware。它会注册只读工具 tool_search(query, limit),limit 默认值为 5。普通模型最初只看到搜索工具和配置为 始终可见的工具。搜索后,命中的工具会在后续模型调用中可用,搜索本身不会执行它们。
默认的 KeywordToolSearchStrategy 不引入额外依赖,按关键词匹配工具名称和描述, 名称匹配的排序权重更高。可以实现 ToolSearchStrategy.search(ToolSearchRequest), 接入语义检索或远程索引,并通过 ToolSearchResult 按相关性返回可用工具名称。 结果在写入会话状态前会去重、限制数量;不属于候选集合的名称会被拒绝。 候选集合遵守当前激活的工具组和请求级工具覆盖规则,未激活的组仍需通过现有工具组 管理功能启用。 需要保留控制工具,或强制使用普通函数调用时,可以使用完整构造方法:
最后一个参数为 true 时启用原生加载(默认值)。模型通过 getToolSearchMode() 选择加载协议;自定义模型默认为 FUNCTION。OpenAIResponsesChatModel 使用原生 client-executed 搜索,通过 tool_search_output 重放命中的工具定义; AnthropicChatModel 发送带有延迟加载标记的候选工具目录,并返回 tool_reference。 这两种方式需要 provider 和模型支持对应的原生协议。不支持时,将参数设为 false, 改用普通函数调用。不要将此 middleware 与 OpenAI hosted tool_search server tool 同时配置。 发现结果保存在会话的 ToolContextState 中,支持通过状态存储恢复。每次请求都会按 当前候选目录重新检查工具;搜索不会激活工具组或改变执行权限。显式指定调用的工具和 结构化输出控制工具会保持可用。调用期间不会修改共享 Toolkit。原生加载保持初始工具 声明稳定,通过会话历史保留已发现的工具。如果历史压缩删除了搜索结果,或模型切换了 加载协议,则根据持久化的发现记录立即暴露对应定义,保证工具仍然可用。普通函数调用 则在 tools 中累积已发现的定义。 无需凭证即可运行的示例: ToolSearchExample。

工具调用拦截器

ToolCallInterceptor 是 Toolkit 层、单次调用级的参数拦截扩展点。它运行在 JSON Schema 校验与预置参数合并之后、会话分发记录与真实执行之前 —— 因此它能看到工具将收到的确切参数与调用者身份,并决定实际执行的内容。 用它表达权限系统无法覆盖的参数级业务规则:工具本身被允许调用,但本次调用的 file_path 是否在当前用户的沙箱内?这条 SQL 是否超出调用者的数据范围?工具级策略(ALLOW/ASK/DENY、人审)仍归权限引擎;批次级、事件流层面的关注点仍归中间件的 onActing。 拦截器在 ToolkitConfig 构建期注册:
interceptor(...) 为追加语义,可重复调用;interceptors(List) 为覆盖语义,替换此前注册的全部拦截器 —— 适合已在别处组装好拦截器列表的场景。拦截器按 order() 降序稳定排序(数值越大越靠外层:先拦截、后收到结果;同序保持注册顺序),排序后运行期不可变。Toolkit 被并发会话共享,拦截器实现必须无状态或线程安全;运行期可变的策略(熔断开关、租户规则)以拦截器内部的可变状态表达。 钩子形状与中间件同构(洋葱模式):
拦截器可执行的动作: 需要注意的语义:
  • 按 agent 身份分发。快照携带 agentId(审计关联)与 agentName(写规则的依据,如”planner 禁止写操作”);直连无 agent 时两者为 null。刻意只透出标识而非 Agent 句柄 —— 其操作属于中间件层且带重入风险;结构性的按 agent 差异宜在构建期表达于各 agent 自己的 ToolkitConfig。
  • 改写发生在权限批准之后。权限引擎判定的是模型原始输入;若安全敏感参数(路径、SQL、URL)改写后的最终值需要用户确认,应挂起人审而非静默改写。拦截器拒绝一个已获批准的调用属预期行为 —— 拒绝结果直接回馈模型。
  • 拒绝消息应面向模型可行动(“请使用工作区内的相对路径”),不泄露服务端信息,并带拦截器身份前缀(如 PathSandbox:)。在示例之外加固路径守卫:调用者可控的 id 先校验格式再用作路径组件;已存在路径(及新建文件的最近已存在祖先)用 toRealPath() 复检 —— normalize() 不消解符号链接、Windows 8.3 短名与 ADS,且 Path#startsWith 区分大小写;改写时避免把服务端绝对根路径回显给模型。
  • 会话记录保持真实。tool/dispatch 记录位于链终点,记录准备派发的参数;被拦截器拒绝的调用不会留下 allowed/dispatch 记录。
  • 超时与重试在批处理 callTools 路径包裹整条链:拦截器耗时计入工具超时,重试时每次尝试都会重新走链。直连 Toolkit#callTool 路径保持现状 —— 无超时包装 —— 因此拦截器绝不能阻塞;阻塞 IO 用 Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic()) 卸载。
  • 外部工具不会被拦截。externalTool=true / SchemaOnlyTool 在链之前短路返回 suspended,校验由外部执行方负责。挂起后回填的结果属数据注入而非执行,同样不经过链。框架内部合成工具 generate_response 亦不经过链。
  • 传播。Toolkit#copy()(Harness 子代理即经此构建)会携带拦截器;以全新 Toolkit 构建的子代理则没有,除非显式注册。工具体内再委托其他工具(工具在 callAsync 内部调用别的工具)不会被二次拦截 —— 按外层工具名分发守卫。

工具实际派发前检查

应用需要在排队工具开始前再次核验本轮状态时,可配置 ToolkitConfig.builder().beforeToolCall(...),例如其他工具已报告基础设施故障:
检查必须同步且不阻塞,在每次订阅和重试时,前序调度、拦截器及派发日志持久化等待结束后,紧邻本地 callAsync 执行。检查接收最终执行参数,包含拦截器改写后的参数。外部工具虽不经过拦截器,仍会在返回挂起结果前执行此检查。拒绝派发时抛普通 RuntimeException;检查回调不得抛出 ToolSuspendException,它表示请求挂起,不能用于拒绝派发。拒绝沿既有工具错误结果路径返回,批次结果保留调用 ID 和名称。 默认不配置检查。Toolkit.copy() 保留配置,覆盖复制后及后注册的工具;独立新建的工具集需要单独配置。跨运行共享工具集时,应从调用的 RuntimeContext 获取状态,避免捕获共享停止标记。已开始的调用不会被取消,Agent 循环停止与恢复决策仍由应用负责。框架内部合成工具路径及外部结果回填不经过此检查。 本地检查刻意放在派发日志持久化之后,因此即使检查随后拒绝执行,已持久化的 tool/dispatch 仍表示派发意图,动作的最终结果会记录失败。持久化之前的参数策略、改写及审计继续使用拦截器。

延伸阅读

Agent

Agent 如何在 ReAct 循环中编排 tool 调用

Permission System

精细控制哪个 tool 可以执行、何时执行

Middleware

用洋葱式 middleware 拦截并改写 tool 调用

Human-in-the-Loop

外部执行 tool 与人工审批工作流

请求级工具视图与会话隔离

Agent 构建时复制工具注册表和注册元数据;已有工具实例仍按引用共享,因此自定义工具需要自行保证并发安全。构建某个 Agent 时添加的 Hook、知识库等工具不会覆盖其他 Agent 的注册。 调用期间不要通过修改共享 Toolkit 注入外部工具或切换会话的工具组。使用 io.agentscope.core.tool.ToolRequestConfig 描述本次请求的工具视图:
外部工具只包含 schema,执行时返回 suspended,由调用方执行后提交结果。同名外部工具覆盖后端工具;EXTERNAL_ONLY 隐藏全部后端工具,即使外部列表为空或 Toolkit 禁止删除工具也一样,因为请求视图不会删除注册。AGENT_ONLY 不允许同时配置外部工具。 Toolkit.callTool 和 callTools 同样读取显式传入的 RuntimeContext。无配置的 getTool(name) 查看原始注册表;需要请求视图时使用 getTool(name, config)。工具组激活状态保存在会话的 ToolContextState,流式工具回调绑定本次调用。 需要按用户或会话加载技能的仓库实现 Core 中的 io.agentscope.core.skill.repository.RuntimeContextSkillRepository。Core 和 Harness 都将当前上下文传给仓库。动态 Skill 视图按内容签名缓存,并发缓存未命中共享一次完整的资源物化;淘汰后重建视图使用新目录,保留运行中执行所引用的文件。缓存淘汰只限制保留的 SkillBox 数量,不限制磁盘占用:自动生成的目录保留到 JVM 退出时清理。调用方指定的工作目录由调用方负责清理;不要在仍有执行引用资源时删除它。

工具执行生命周期回调

ToolExecutionCallback 提供 onStart(call)、onProgress(call, chunk) 和 onResult(call, result)。执行器在工具真正开始执行时(串行批次中为轮到该工具时)发送 onStart;onResult 每次调用至多触发一次,先于有序汇总。Toolkit.callToolsWithCallback(...) 转发全部生命周期通知,原 callTools(...) 与 chunk callback 签名保持兼容。 最终结果在有序汇总前通知,快工具无需等待慢工具。重试、超时和工具错误转换沿用原逻辑;progress callback 异常沿用原有日志隔离;最终结果 callback 异常通过返回的 Publisher 传播。实现必须线程安全且不能阻塞。取消或基础设施异常可能没有最终结果。 Agent 默认渐进发布结果,无需配置开关。普通工具和合成 generate_response 使用该回调,服务端结果与权限拒绝保留直接事件转换。callback 只发布事件,不创建 Msg 或修改上下文;消息结算与中断恢复沿用原流程,提前收到事件不代表结果已保存。