ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

openJiuwen agent-core-java 工具抽象基类 Tool 深度解析:ToolCard、统一调用入口与回调生命周期

openJiuwen agent-core-java 工具抽象基类 Tool 深度解析:ToolCard、统一调用入口与回调生命周期
  • 人工智能
  • AI Agent
  • Agent 框架
  • 工具调用
  • Agent 工作流
  • RAG
  • MCP Clients
  • 强化学习

【免费下载链接】agent-core-java

openJiuwen agent core Java是 openJiuwen Core的 Java版本,提供AI Agent开发、运行、调优与演进相关的全套SDK能力。

项目地址:https://gitcode.com/openJiuwen/agent-core-java
点击查看免费下载

本文基于openJiuwen/agent-core-java开源仓库中 com.openjiuwen.core.foundation.tool.Tool 的 API 文档,并结合仓库源码(Tool.java、LocalFunction.java、McpTool.java、RestfulApi.java 等)进行纵深展开。全文围绕该抽象基类的设计意图、字段与构造约束、统一调用入口、子类扩展机制及配套回调事件展开,帮助读者理解 openJiuwen Core Java 版工具体系的调用形态,并掌握自定义工具、接入 MCP/HTTP 工具与流式输出的完整实现路径。

Tool是 openJiuwen agent-core-java 工具体系的抽象基类。它本身不承载任何具体业务逻辑,而是通过持有一张ToolCard元数据卡片,为所有工具(本地函数、MCP 工具、RESTful API 工具等)定义统一的「同步执行」与「流式执行」两个入口,并围绕这两个入口挂接完整的回调事件生命周期。理解Tool,就掌握了 openJiuwen 中所有工具被 LLM 调用时的统一形态:入参解析 → 调用开始 → 结果产出/异常上报 → 输出变换。

一、Tool 在工具体系中的定位

从源码结构看,Tool位于core/src/main/java/com/openjiuwen/core/foundation/tool/Tool.java,是全工具族系的抽象基类,仓库中的具体工具实现均直接或间接继承自它:

实现类源码路径用途
LocalFunctionfunction/LocalFunction.java包装本地 Java 函数/方法,将可调用对象封装为工具
McpToolmcp/McpTool.java包装 MCP(Model Context Protocol)客户端,透传远端工具调用
RestfulApiservice_api/RestfulApi.java将 RESTful HTTP 接口封装为工具,执行请求并解析响应

这些子类共享同一套「卡片 + 调用入口」抽象:构造时注入一张描述工具元数据的ToolCard,对外通过invoke/stream两个统一入口被上层调用。源码注释明确指出该类镜像了 Python 侧openjiuwen/core/foundation/tool/base.py中的Tool,因此本仓库的 Java 实现与 openJiuwen Python 版保持一致的调用契约。

设计要点:Tool自身不实现具体业务逻辑,只负责约束所有工具共有的调用形态。具体行为全部由子类通过两个受保护的模板方法(invokeInternal/streamInternal)提供,父类负责输入预处理、回调触发与输出变换的公共编排。

二、唯一字段 card 与构造约束

Tool只有一个实例字段:card(类型ToolCard),即工具卡片,保存工具的名称、描述、输入参数 Schema 等元数据。字段说明如下:

字段类型默认值说明
cardToolCard-工具卡片。构造时要求非空且id非空字符串。

源码 Tool.java 中的受保护构造器Tool(ToolCard card)在初始化时执行两条硬性校验:

public Tool(ToolCard card) { if (card == null) { throw toolCardInvalid(null, "card is None"); } if (card.getId() == null || card.getId().isEmpty()) { throw toolCardInvalid(card, "card id is None or empty"); } this.card = card; }
  • card为null,直接抛出异常(错误码TOOL_CARD_INVALID,由 ErrorHelper 构造);
  • card.getId()为空或 null,同样拒绝构造。

因此「每个工具必须携带一张有合法 id 的卡片」是工具体系不可绕过的约束。值得注意:该构造器为受保护(protected),仅供子类初始化时调用,不作为公开构造方法暴露,这与文档「源码中的Tool(ToolCard card)为受保护构造器,供子类初始化时使用」的说明一致。card声明为final,一旦构造完成不可替换,保证工具生命周期内元数据稳定。

三、统一调用入口:invoke 与 stream

Tool为所有工具定义了两种执行模式:同步执行invoke与流式执行stream,各自提供带/不带kwargs两个重载版本。公开方法签名如下:

签名说明
public ToolCard getCard()返回当前工具持有的卡片对象。
public abstract Object invoke(Map<String, Object> inputs, Map<String, Object> kwargs) throws Exception执行完整工具调用,具体行为由子类实现。
public Object invoke(Map<String, Object> inputs) throws Exception以空kwargs调用重载版本。
public abstract Iterator<Object> stream(Map<String, Object> inputs, Map<String, Object> kwargs) throws Exception以流式方式执行工具,返回增量结果迭代器。
public Iterator<Object> stream(Map<String, Object> inputs) throws Exception以空kwargs调用流式重载版本。

3.1 参数语义

两个入口的第一个参数inputs是结构化输入,需要符合工具卡片ToolCard.inputParams声明的 JSON Schema;第二个参数kwargs是附加执行参数,用于向执行过程传递额外控制信息。以 LocalFunction.java 的实现为例,kwargs中以下两个键会被识别并影响入参解析:

  • skip_none_value(布尔):为true时跳过值为None/null的输入字段;
  • skip_inputs_validate(布尔):为true时跳过输入 Schema 校验。

单参数重载版本内部直接以Map.of()构造空kwargs调用双参数版本(见 Tool.java),因此无论调用方传不传kwargs,行为路径完全一致。

3.2 invoke 的内部编排

从源码 Tool.java 看,invoke并非简单转发到子类,而是父类编排的一整条事件链:

  1. 入参准备(prepareInput):复制inputs/kwargs,若注册了回调框架(DecoratorFramework),先触发tool_invoke_input变换事件,允许回调修改入参;
  2. 调用开始:触发tool_call_started事件(携带tool_name、tool_id、inputs等生命周期参数);
  3. 执行与结果/异常上报:调用子类实现的invokeInternal;成功触发tool_call_finished,失败捕获异常并触发tool_call_error后重新抛出;
  4. 输出变换:触发tool_invoke_output事件,回调可对结果做二次加工(transformOutput)。

3.3 stream 的流式生命周期

stream的编排见 Tool.java:父类先触发tool_stream_input与tool_call_started,然后调用子类的streamInternal获得底层迭代器;若返回null则替换为空迭代器(Collections.emptyIterator()),最后包一层内部的CallbackIterator返回。该迭代器在遍历过程中按需触发:

  • 每取出一块增量结果:触发tool_result_received与tool_stream_output;
  • 迭代耗尽:触发tool_call_finished;
  • 遍历抛异常:触发tool_call_error(同一迭代器只上报一次,通过finished标志保证幂等)。

所有事件名常量集中在 ToolCallEvents.java 中,完整清单包括:tool_call_started、tool_call_finished、tool_call_error、tool_result_received、tool_parse_started、tool_parse_finished、tool_invoke_input、tool_invoke_output、tool_stream_input、tool_stream_output、tool_auth。这意味着Tool 层的可观测能力是全体系统一的:无论底层是本地函数、MCP 远端还是 HTTP 接口,回调消费者拿到的生命周期事件形态一致。

四、子类如何实现具体工具:invokeInternal / streamInternal

Tool将「父类编排 + 子类实现」分离。公开的invoke/stream是final级编排入口(非 abstract),而真正的业务逻辑由两个受保护模板方法承载:

protected Object invokeInternal(Map<String, Object> inputs, Map<String, Object> kwargs) throws Exception; protected Iterator<Object> streamInternal(Map<String, Object> inputs, Map<String, Object> kwargs) throws Exception;

若子类未覆盖某一方法,基类默认抛出明确错误:

  • 未实现invokeInternal→TOOL_INVOKE_NOT_SUPPORTED(工具不支持同步调用);
  • 未实现streamInternal→TOOL_STREAM_NOT_SUPPORTED(工具不支持流式调用)。

这正是McpTool的做法:MCP 远端调用本身不具备原生增量流式契约,因此 McpTool.java 的streamInternal直接抛出TOOL_STREAM_NOT_SUPPORTED,而invokeInternal则完成参数按 Schema 清洗、调用 MCP 客户端的callTool/call_tool方法、并支持operation_timeout/operationTimeout/timeout键动态覆盖超时(默认McpBase.NO_TIMEOUT)。

LocalFunction则两个方法都实现(LocalFunction.java),其流式策略很有代表性:

  • 返回结果是Iterator:直接透传;
  • 返回结果是Iterable(如List):取其iterator();
  • 返回普通对象:包装成单元素迭代器(singletonList(result).iterator())。

同时invokeInternal中若发现函数返回的是Iterator,会抛出func can not be generator错误——即同步调用不允许返回生成器语义的迭代器,必须走stream入口,从源码与测试双重印证了这一约束。

五、ToolCard:工具元数据卡片

ToolCard(ToolCard.java)继承自BaseCard,除基础的身份信息(id、name、description)外,还维护工具声明的输入 Schema 与执行特性:

成员默认值含义
inputParams空LinkedHashMap输入参数的 JSON Schema(type、properties、required等)
properties空LinkedHashMap附加属性(如 MCP 场景的传输细节)
isParallelSafetrue是否可并行安全调用
isStatelessfalse是否无状态
isIdempotentfalse是否幂等

ToolCard提供两种构建方式:

  1. 多参构造器:ToolCard(id, name, description[, inputParams[, properties]]);
  2. Builder 模式:ToolCard.builder().id(...).name(...).description(...).inputParams(...).parallelSafe(...).stateless(...).idempotent(...).build(),其中id缺省时会自动生成,name/description缺省为空字符串。

卡片的核心产出方法是toolInfo(),返回 ToolInfo.java 描述符——遵循OpenAI function calling 格式(type="function"、name、description、parameters),可直接用于 LLM 的 function/tool 参数拼接。equals/hashCode覆盖了完整声明面(身份 + 参数/属性 + 三个特性标志),用于工具重复注册时的等价性判定(ability-declaration reuse check)。

六、工具的三种典型创建方式

围绕Tool基类,仓库提供了三种把「普通代码」变成「工具」的路径:

6.1 手动构造 LocalFunction

最直接的方式:先构建ToolCard,再new LocalFunction(card, func),其中func是Function<Map<String, Object>, Object>。LocalFunction还支持ContextFunction变体((inputs, kwargs) -> Object),可在函数体内访问session等执行期上下文;且函数返回CompletionStage(如CompletableFuture)时会被自动等待并解包(见 LocalFunction.java)。

6.2 ToolDecorator 工厂

ToolDecorator.java 是 Python 版@tool(...)装饰器的 Java 等价物,提供tool(Function)、tool(name, Function)、toolFromMethod(target, method)等静态工厂方法,以及携带完整选项的Options记录(name、description、inputParams、card、autoExtract、sourceMethod):

  • 未显式指定inputParams且autoExtract=true时,通过 CallableSchemaExtractor.java 基于反射自动生成参数 Schema;
  • 方法参数为Optional类型时会自动包裹空值(argumentValue);
  • 显式传入card时,若name/description/inputParams与卡片不一致,会生成一张覆盖后的新卡片而非修改原卡片。

6.3 @ToolDefinition 注解扫描

annotation/ToolDefinition.java 提供name、description、autoExtract三个注解属性;配合 AnnotatedToolFactory.java 的scan(target),可一键扫描目标对象上所有@ToolDefinition标注的方法并批量转换为LocalFunction,适用于工具注册表批量装配场景。

七、异步执行与虚拟线程:Java 版的设计取舍

文档明确说明:Java 版本没有内建异步接口,Tool的invoke/stream均为同步阻塞形态(throws Exception)。如果需要异步执行,由调用方自行结合虚拟线程或CompletableFuture处理。这一点与 Python 版Tool的协程语义不同——Java 侧刻意不把异步写进基类契约,原因可以从两个层面印证:

  1. 函数层支持异步返回值:LocalFunction允许包装的函数返回CompletionStage,父类在调用线程上统一await等待完成(awaitIfNeeded),因此「异步函数 + 同步调用入口」天然兼容;
  2. 调用层由使用者编排异步:需要真正并行发起多个工具调用时,可在外层用虚拟线程(本项目文档 Java版本与虚拟线程兼容 有专项说明)包裹每个invoke,或将调用提交给CompletableFuture组合,工具自身无需感知线程模型。

另外,LocalFunction.invokeFunction在调用前后会保存/恢复SessionContextHolder中的当前 Session(见 LocalFunction.java),其注释明确指出这是为「并行工具调用不互相污染调用线程绑定的 Session」而设计——从源码结构看,这是对虚拟线程/并行场景的显式防护。

八、测试验证:四个相关测试覆盖的行为

文档列出四个相关测试,均可在仓库core/src/test下找到,覆盖了Tool体系的核心行为契约:

测试类路径验证重点
LocalFunctionTestcore/src/test/java/com/openjiuwen/core/foundation/tool/function/LocalFunctionTest.java构造时拒绝null函数;输入按 Schema 填充默认值(如mode缺省补"text");tool_parse_started/tool_parse_finished回调携带tool_name/tool_id/raw_inputs/schema/formatted_inputs;CompletableFuture返回值被自动等待;同步invoke拒绝迭代器返回(func can not be generator);stream正确产出Iterator元素并触发解析回调
McpToolTestcore/src/test/java/com/openjiuwen/core/foundation/tool/mcp/McpToolTest.javaMCP 客户端调用透传、参数清洗与超时控制
RestfulApiTestcore/src/test/java/com/openjiuwen/core/foundation/tool/service_api/RestfulApiTest.javaRESTful 工具的 URL 校验、HTTP 执行与响应解析
ToolCardTestcore/src/test/java/com/openjiuwen/core/foundation/tool/ToolCardTest.javaBuilder 构建卡片属性、id自动生成、默认inputParams为空 Map、默认特性标志(isParallelSafe=true、isStateless=false、isIdempotent=false)、toolInfo()输出

其中LocalFunctionTest的invokeFormatsInputsWithSchemaDefaults用例直观展示了 Schema 默认值填充:卡片声明mode字段默认"text"且path必填,仅传path调用即可得到"readme.md:text"的结果——这正是「工具卡片声明驱动入参解析」的具体体现。

九、小结:Tool 基类的设计价值

从整体设计看,openJiuwen agent-core-java 的Tool抽象基类承担了三层职责:

  1. 统一契约:以ToolCard承载元数据、以invoke/stream承载调用入口,所有工具(本地、MCP、HTTP)对上层与 LLM 暴露一致的形态;
  2. 集中编排:输入变换、生命周期回调、输出变换全部收敛在父类,子类只需实现invokeInternal/streamInternal两个模板方法,且通过默认抛错让「不支持的能力」显式化;
  3. 可观测基础:tool_call_started/finished/error、tool_parse_started/finished、tool_stream_output等标准事件构成统一的工具调用追踪与装饰器挂接点。

对于希望在本仓库中自定义工具的开发者,标准路径是:构建一张ToolCard(或直接用ToolDecorator/@ToolDefinition自动生成)→ 继承Tool实现invokeInternal(必要时实现streamInternal)→ 通过setCallbackFramework注册回调框架即可获得全套调用事件。若仅需快速接入,优先使用LocalFunction包装函数,或通过McpTool/RestfulApi对接远端能力。

  • 人工智能
  • AI Agent
  • Agent 框架
  • 工具调用
  • Agent 工作流
  • RAG
  • MCP Clients
  • 强化学习

【免费下载链接】agent-core-java

openJiuwen agent core Java是 openJiuwen Core的 Java版本,提供AI Agent开发、运行、调优与演进相关的全套SDK能力。

项目地址:https://gitcode.com/openJiuwen/agent-core-java
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进