跳转至

Tools cn

本文聚焦 Jianmu 工具系统的三层抽象架构:底层 Tool 基类 定义可执行能力契约,中层 @tool 装饰器 将任意 Python 函数零摩擦地提升为一等工具对象,顶层 ToolSet 提供跨来源(内置、自定义、MCP 动态提供者)的统一工具集装配与执行入口。这三层共同构成 Agent 与工作流节点消费工具能力的唯一通道,总计约 1,879 行 覆盖核心抽象、类型定义、提供者协议与行为树集成。

工具系统全景:从函数到 Agent 可调用能力的完整链路

在深入各个组件之前,先以一张核心类关系图建立全局认知。Tool 是抽象根类,FunctionTool(通过 @tool 产生)和 CalculatorTool 等内建工具是具体实现;ToolSet 聚合多个 Tool 实例并提供统一查询与执行接口;ToolProvider(Protocol)定义动态工具源的生命周期;ToolNode 与 ToolExecutor 分别对应"显式工作流节点"与"Agent 循环批量执行"两种消费模式。

classDiagram
    class Tool {
        <<abstract>>
        +name: str
        +description: str
        +input_schema: dict
        +output_schema: dict
        +parallel_safe: bool
        +effect_tags: tuple
        +run(input) Any*
        +execute(args, injected, runner) Any
        +as_node() ToolNode
        +to_schema() dict
        +spec() dict
    }

    class FunctionTool {
        +_fn: Callable
        +run(*args, **kwargs) Any
    }

    class ToolSet {
        +tools: list~Tool~
        +from_tools(iter) ToolSet$
        +resolve(names, custom) ToolSet$
        +from_providers(providers) ToolSet$
        +merge(*groups) ToolSet
        +get(name) Tool|None
        +execute(name, args) Any
        +schemas() list~dict~
    }

    class ToolProvider {
        <<Protocol>>
        +initialize() None
        +get_tools(**kwargs) list~Tool~
        +close() None
    }

    class BuiltinToolProvider {
        +resolve_named_tools(names) list~Tool~$
        +list_builtin_names() list~str~$
    }

    class ToolNode {
        +tool: Tool
        +input_key: str
        +output_key: str
        +update_async() Status
    }

    class ToolExecutor {
        +toolset: ToolSet
        +register_tool(tool) None
        +update_async() Status
    }

    Tool <|-- FunctionTool : @tool 装饰
    Tool <|-- CalculatorTool : 内建工具
    ToolSet o-- Tool : 聚合 0..*
    ToolProvider <|.. BuiltinToolProvider : 实现
    ToolNode o-- Tool : 包装 1
    ToolExecutor o-- ToolSet : 包含 1

Tool 基类:可执行能力的抽象契约

Tool 是所有工具实现的抽象根类,它定义了 Jianmu 中"工具"这一概念的完整契约。理解这个基类是理解整个工具系统的第一原理——无论是内置工具、装饰器包装的函数工具,还是通过 MCP 协议接入的外部工具,都统一收敛到这一套接口上。

类属性:声明式元数据

Tool 通过六个类属性完成自描述,这些属性直接驱动 LLM function-calling schema 的生成、并行执行策略的决策以及 Guard 防护系统的副作用追踪:

属性 类型 默认值 用途
name str "unnamed_tool" 工具的唯一标识名,Agent 通过此名调用工具
description str "No description provided" 人类可读描述,注入 Prompt 引导 LLM 选择工具
input_schema dict {"type": "string", ...} JSON Schema 格式的输入规范,驱动 function-calling parameters
output_schema dict {"type": "string", ...} JSON Schema 格式的输出规范,用于 UI 展示和文档生成
parallel_safe bool True 标记工具是否可以与其他工具并行执行
effect_tags tuple () 副作用标签(如 "file_write", "http"),Guard 系统据此聚合统计

子类只需覆写这些属性即可完成声明。以 CalculatorTool 为例,一个完整的工具定义仅需 10 行左右:

class CalculatorTool(Tool):
    name = "calculator"
    description = "Performs basic math calculations..."
    input_schema = {"type": "string", "description": "Python math expression, e.g. '2+2'"}
    output_schema = {"type": "string", "description": "Result of the expression"}

    def run(self, input: str) -> str:
        # 实现逻辑...

核心方法:run 与 execute 的双层执行模型

Tool 区分两个执行层级。run() 是抽象方法,由子类实现具体的业务逻辑——这是工具开发者唯一需要关心的方法。execute() 则是框架层入口,它在 run() 之上叠加了三项横切关注点:参数合并(将显式参数与运行时注入的上下文合并)、Runner 委托(当配置了沙箱执行环境时,将调用转发给 Docker/Local Runner)以及同步/异步适配(自动检测 run() 是否为协程,同步函数在独立线程中执行以不阻塞事件循环)。

sequenceDiagram
    participant Caller
    participant Tool.execute
    participant _merge_call_args
    participant Runner
    participant Tool.run

    Caller->>Tool.execute: execute(args, injected, runner)
    Tool.execute->>_merge_call_args: 合并 args + injected
    _merge_call_args-->>Tool.execute: merged_args

    alt runner 存在 (沙箱模式)
        Tool.execute->>Runner: runner.run(self, merged_args)
        Runner->>Tool.run: 在沙箱中执行
    else 本地模式
        Tool.execute->>Tool.execute: _execute_local(merged_args)
        Tool.execute->>Tool.run: 直接调用 (sync → to_thread, async → await)
    end

    Tool.run-->>Tool.execute: result
    Tool.execute-->>Caller: result

参数合并遵循直观的优先级规则:当 prefer_injected=True(默认)时,运行时注入的上下文值覆盖显式参数;当 prefer_injected=False 时则相反。注入参数仅在 args 为字典时生效——这为 Guard 系统的参数注入(如注入当前用户身份、会话 ID)提供了标准通道。

Schema 规范化:从工具定义到 LLM Function Calling

_normalize_parameters() 和 _normalize_output_schema() 两个内部方法将工具声明转换为 OpenAI function-calling 兼容的 JSON Schema。核心逻辑在于:当 input_schema 类型为 "object" 时直接透传;否则自动包装为 {"type": "object", "properties": {"input": <原始schema>}, "required": ["input"]}。这确保了无论工具定义的输入是简单字符串还是复杂对象,LLM 始终看到的是统一的 "object" 类型 parameters。

to_schema() 是面向 LLM Provider 的公共出口,返回 {"name": ..., "description": ..., "parameters": ...} 三元组。spec() 则面向 Prompt 构建和 UI 渲染,额外包含 input_schema, output_schema, returns 等更丰富的元数据。

描述解析优先级:逐级回退的智能策略

get_description() 实现了一个三级回退策略:优先使用显式设置的 description 类属性(当它不等于默认值 "No description provided" 时),其次提取类的 __doc__ 文档字符串,最后返回空字符串。这意味着工具开发者只需写好 Python docstring,无需手动维护 description 属性即可获得合格的 Prompt 描述。

as_node():工具到行为树节点的流畅转换

as_node() 是 Tool 基类提供的最优雅的桥接方法——它将任意工具实例包装为 ToolNode(继承自 AsyncNode),使其能直接嵌入行为树。只需一行 tool.as_node(name="MyStep"),工具就从"LLM 可调用的能力"转变为"工作流图中显式编排的节点"。这统一了两种使用模式:Agent 自主选择工具 vs. 开发者显式编排工具。

@tool 装饰器与 FunctionTool:函数的零摩擦工具化

@tool 装饰器是 Jianmu 工具系统中最常用的入口——它将任意 Python 函数(同步或异步)原地提升为 FunctionTool 实例,继承 Tool 基类的全部能力。装饰器同时支持无参数形式 @tool 和有参数形式 @tool(name="...", description="..."),前者从函数名和 docstring 自动推导元数据。

FunctionTool 的实现机制

FunctionTool 是 @tool 装饰器的返回类型,它在 __init__ 中完成两项关键工作:(1) 保存对原始函数的引用 self._fn,(2) 如果检测到原始函数是协程函数(inspect.iscoroutinefunction),则动态创建一个异步 run 方法覆写默认的同步版本。这种设计使得 FunctionTool 对外表现一致——使用者始终调用 tool.run(...) 或 tool.execute(...),而无需关心底层是同步还是异步实现。

装饰器参数 input_schema 和 output_schema 是可选的 JSON Schema 字典,用于覆盖默认的字符串类型假设。当工具需要结构化输入(如多个字段)时,建议显式提供:

@tool(
    name="currency_convert",
    description="Convert amount between USD and CNY using a fixed demo rate.",
    input_schema={
        "type": "object",
        "properties": {
            "amount": {"type": "number"},
            "from_currency": {"type": "string"},
            "to_currency": {"type": "string"},
        },
        "required": ["amount", "from_currency", "to_currency"],
    },
)
def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
    # ...

两种创建风格的对比

Jianmu 提供 Tool 子类继承和 @tool 装饰器两种工具创建方式,各有适用场景:

维度 Tool 子类继承 @tool 装饰器
代码量 需要完整的类定义 最少一行装饰器
状态管理 可在实例属性中持有状态 无状态(纯函数)
复杂逻辑 适合多方法、状态ful 工具 适合单一职责的纯函数
Schema 控制 类属性声明,清晰直观 通过装饰器参数传入
测试便利 可实例化后直接调用 run() 装饰后即为 Tool 实例,直接调用
典型场景 HTTPTool, FileReadTool 等内置工具 用户自定义的轻量工具

ToolCall 与 ToolResult:类型化的调用/响应协议

在 Agent 循环中,LLM 生成工具调用请求,ToolExecutor 执行后产生观察结果。这一来一回由两个冻结数据类(frozen dataclass)精确建模。

ToolCall:从 LLM 响应到结构化调用

ToolCall 包含三个字段:name(归一化为小写的工具名)、arguments(已解析的参数,通常是 dict 或 list)、id(OpenAI 兼容的 tool_call_id,用于关联调用与结果)。其核心能力集中在 from_dict() 静态方法——它将 Provider 返回的原始字典(arguments 可能是 JSON 字符串)解析为结构化对象,同时自动处理 JSON 解码失败等边界情况(当 arguments 无法解析时保留原始字符串,而非抛出异常)。

from_list() 则将批量原始数据归一化为 list[ToolCall],同时具备幂等性——已为 ToolCall 实例的元素直接透传。这在 Agent 状态管理中是关键保证:同一批 actions 多次经过归一化不会产生副作用。

ToolResult:归一化的执行结果

ToolResult 的四个字段(tool, ok, output, error)构成工具执行的统一结果模型。其 from_raw() 工厂方法是最复杂的部分——它需要处理六种不同的原始返回格式:已是 ToolResult 的对象、显式 error 字符串、dict 格式(含 ok 字段)、"Error:" 前缀的字符串、以及普通成功返回值。这种容错设计使得不同来源的工具(内置、自定义、MCP)无论返回何种格式,都能统一收敛为 ToolResult。

ToolSet:跨来源工具集的统一装配 Facade

ToolSet 是工具系统的顶层外观——无论工具来自内置注册表、显式实例、还是动态 Provider,最终都汇集到同一个 ToolSet 中,由它提供统一的查询、执行和 Schema 导出接口。其内部维护一个有序的 list[Tool],采用确定性的 last-wins 合并语义:同名工具以后注册者为准,保持插入顺序。

四种构建路径

ToolSet 提供四种类工厂方法,覆盖所有工具来源:

工厂方法 输入 适用场景
from_tools(iter) 显式 Tool 实例列表 直接传入自定义工具
resolve(names, custom) 工具名称字符串列表 + 可选自定义工具 按名称引用内置工具,用自定义工具覆盖同名
from_providers(providers) ToolProvider 列表 动态工具源(如 MCP 客户端)
list_builtin_names() 无 查询所有可用内置工具名称

resolve() 是最常用的路径——Agent 配置中声明的 tools: [calculator, read_file, write_file] 正是通过此方法解析为具体实例。custom_tools 参数允许自定义工具以相同名称覆盖内置工具,实现"用户定制优先"的解析策略。

合并与查询

merge() 和 with_tools() 返回新的 ToolSet(不可变风格),支持链式调用:ToolSet.from_tools([tool_a]).with_tools([tool_b]).merge(other_set)。get(name) 执行大小写不敏感的精确查找,返回 Tool | None。names() 和 describe() 分别返回工具名称列表和人类可读的 bullet list,供 Prompt 构建直接使用。

执行与并行安全检查

execute(name, args, injected, runner) 将调用委托给对应 Tool 实例的 execute() 方法,未找到工具时抛出 KeyError。is_parallel_safe(names) 检查指定工具列表是否全部标记为 parallel_safe=True,这是 ToolExecutor 并行/串行模式自动选择的决策依据。

schemas() 与 specs():批量导出

这两个方法分别调用每个工具的 to_schema()(面向 LLM function-calling)和 spec()(面向 Prompt 与 UI),返回列表。它们是 ToolExecutor 向 AgentLLMNode 提供工具描述的标准通道。

ToolProvider 协议与 BuiltinToolProvider:动态工具源的注册与解析

ToolProvider:生命周期协议

ToolProvider 是一个 Python Protocol(结构化鸭子类型),定义了三个方法:initialize()(异步初始化,如建立 MCP 连接)、get_tools(**kwargs)(同步返回当前可用工具列表)、close()(异步释放资源)。任何实现了这三个方法的对象都可以作为 ToolSet.from_providers() 的输入。BaseToolProvider 提供了默认的 no-op 实现,内置提供者和 MCP 集成均继承自此基类。

BuiltinToolProvider:延迟加载的内置工具注册表

BuiltinToolProvider 是 Jianmu 内置工具的中央注册表,它采用延迟加载 + 注册的设计模式。所有内置工具类通过 register_tool(name, tool_cls) 注册到类级别的 _builtins 字典中,但实际的 import 发生在 _ensure_builtins() 首次被调用时——这意味着如果某个内置工具的可选依赖未安装(如 duckduckgo-search),只有在该工具被实际引用时才会触发 ImportError,不影响其他工具的正常使用。

内置工具注册表包含三组工具:

分组 工具 注册名
核心文件 FileReadTool, FileWriteTool, ListDirTool read_file, write_file, list_dir
编码辅助 GlobSearchTool, GrepSearchTool, FileInfoTool, StrReplaceTool glob_search, grep_search, file_info, str_replace
执行与计算 PythonREPLTool, CalculatorTool, BashTool python_repl, calculator, bash
网络与搜索 HTTPTool, DuckDuckGoSearchTool http_request, duckduckgo_search

别名系统(_DEFAULT_ALIASES)提供向后兼容的名称映射:例如 file_read 自动解析为 read_file,web_search 解析为 duckduckgo_search。通过 register_alias() 可动态注册新的别名。

resolve_named_tools() 是核心解析方法,其优先级规则为:自定义工具 > 内置工具。当 custom_tools 中存在与内置工具同名的实例时,自定义工具优先。解析过程中任何单个工具的导入或实例化失败(ImportError 或构造异常)都会被静默跳过,确保工具集的鲁棒性。

行为树集成:ToolNode 与 ToolExecutor 的两种消费模式

工具系统向行为树暴露两条消费路径,对应两种根本不同的使用场景。

ToolNode:显式工作流节点

ToolNode(继承 AsyncNode)将单个工具包装为行为树节点,适用于开发者显式编排的场景——你明确知道工具在工作流的哪个位置执行。它通过 input_key / output_key 与状态管理通信:tick 时从指定状态键读取输入,执行完成后将结果写回指定输出键。

ToolNode.update_async() 的执行流程包括:输入解析(支持非 dict 类型自动包装为空字典)→ 运行时事件发射(tool.call.started)→ 沙箱感知的 Runner 委托 → 结果归一化(ToolResult.from_raw)→ 输出写回状态 → 运行时事件发射(tool.call.completed 或 tool.call.failed)。字符串结果以 "Error:" 开头时被自动判定为失败。

ToolExecutor:Agent 循环批量执行器

ToolExecutor 面向 LLM Agent 自主选择工具 的场景——它从状态中读取由 AgentLLMNode 填充的 actions 列表,逐一(或并行)解析、守卫检查、执行工具调用,并将观察结果追加到消息历史。

flowchart TD
    A[读取 state.actions] --> B{actions 非空?}
    B -->|否| C[返回 SUCCESS]
    B -->|是| D[确保 action IDs]
    D --> E[preflight: Guard 检查]
    E --> F{Decision?}
    F -->|PENDING| G[挂起等待审批 → RUNNING]
    F -->|DENY/CONFIRM| H[生成拒绝观察 → SUCCESS]
    F -->|ALLOW| I{并行安全?}
    I -->|parallel| J[_execute_actions_parallel]
    I -->|serial| K[_execute_actions_serial]
    J --> L[构建观察消息]
    K --> L
    L --> M[写入 tool_effects]
    M --> N[检查 final_answer / done]
    N --> O[返回 SUCCESS]

ToolExecutor 的关键设计决策包括:

  • Guard 预检(_preflight_actions):在执行任何工具前,整批 actions 先通过 Guard 策略检查。若任何工具需要审批(CONFIRM)或触发挂起(PENDING),则整批阻塞。审批通过的工具 ID 被记录,用于后续进度追踪。

  • 串行/并行自动选择(_resolve_execution_mode):当 execution_mode 为 auto(默认)时,单工具始终串行,多工具则检查全部 parallel_safe 标记后决定。并行执行使用 asyncio.gather,单个工具失败不影响其他工具。

  • 重试机制:单工具执行失败时,若 attempts <= max_retries,以指数退避(retry_backoff * attempts 秒)重试。

  • 副作用追踪(_write_tool_effects):成功执行后,按工具的 effect_tags 聚合副作用计数(如文件写入次数、HTTP 请求次数),写入状态的 tool_effects 键供 Guard 策略的预算控制使用。

  • 观测发布:工具返回值会先归一化成 ToolResult,再作为 tool message 追加到消息历史中。ReAct 的终止仍由 agent loop 决定,而不是由工具 payload 里的控制字段决定。

完整执行链路:从 Agent 配置到工具运行

以下综合序列图展示了一次典型的 Agent 工具调用全链路——从 ReAct 预设通过 ToolExecutor 执行 Calculator 工具:

sequenceDiagram
    participant Preset as ReAct 预设
    participant Executor as ToolExecutor
    participant ToolSet as ToolSet
    participant Provider as BuiltinToolProvider
    participant Tool as CalculatorTool
    participant Guard as GuardEnforcer
    participant Runner as ToolRunner
    participant State as StateManager

    Preset->>Executor: 构造时传入 tools=[CalculatorTool()]
    Executor->>ToolSet: with_tools([CalculatorTool])
    ToolSet-->>Executor: 更新内部工具列表

    Note over Preset,State: --- 运行时 tick ---
    Preset->>State: 写入 actions=[ToolCall("calculator", "2+3")]
    Executor->>State: read_port("actions")
    State-->>Executor: [ToolCall(...)]

    Executor->>Guard: preflight: check("tool_call", context)
    Guard-->>Executor: ALLOW

    Executor->>ToolSet: get("calculator")
    ToolSet-->>Executor: CalculatorTool 实例

    Executor->>Tool: execute("2+3", runner=ToolRunner)
    Tool->>Runner: runner.run(self, "2+3")
    Runner->>Tool: run("2+3") [本地或沙箱]
    Tool-->>Runner: "5"
    Runner-->>Tool: "5"
    Tool-->>Executor: "5"

    Executor->>Executor: ToolResult.from_raw("calculator", "5")
    Executor->>State: append_port_messages(observation)
    Executor->>State: write tool_effects

阅读下一步

工具系统是 Jianmu 能力模块的核心支柱,理解它之后建议按以下路线深入: