IronClaw 学习笔记 01:Tool + Provider

工具抽象的设计哲学,以及如何统一调用多个 LLM

March 9, 2026·6 min read·Yimin
#Rust#AI#Agent#IronClaw#学习笔记

上一篇我们搭好了骨架,现在给 agent 装上"手"(Tool)和"脑"(LLM Provider)。

🎯 这一篇要解决什么问题?

Phase 00 的 tiny-claw 只是一个 echo 机器——你说什么,它原封不动地还给你。这不是 agent,这是鹦鹉。

一个真正的 agent 需要两个核心能力:

  1. 思考——调用 LLM 理解用户意图,决定下一步行动
  2. 行动——通过 Tool 与世界交互,执行具体任务

这两个能力对应 IronClaw 的两个核心抽象:Tool trait 和 LlmProvider trait。这一篇我们深入理解它们的设计哲学。


🧠 从 OpenCode 到 IronClaw:Tool 的概念迁移

在 OpenCode 系列中,我们已经学过 Tool 的基本概念——每个 Tool 有名称、描述、参数定义,LLM 通过 function calling 来选择和调用工具。

IronClaw 的 Tool 系统在这个基础上增加了三个维度

维度OpenCodeIronClaw为什么需要
安全requires_sanitizationrequires_approvalsensitive_params个人助手处理敏感数据
成本estimated_costestimated_durationrate_limit_config长期运行需要成本控制
隔离进程内执行domain(Orchestrator vs Container)不可信工具需要沙箱

核心骨架是一样的。如果你理解了 OpenCode 的 Tool,IronClaw 的 Tool 只是在同一棵树上长出了更多枝叶。


🏛️ Tool trait:一个工具的完整契约

IronClaw 的 Tool trait 定义了一个工具必须提供什么、可以提供什么:

必须实现的(4 个方法)

方法返回作用
name()&str工具的唯一标识符,LLM 通过这个名字来调用
description()&str告诉 LLM 这个工具能做什么
parameters_schema()serde_json::ValueJSON Schema,定义参数的类型和约束
execute()Result<ToolOutput, ToolError>实际执行逻辑

这四个方法构成了工具的最小契约。一个 echo 工具的实现只需要这四个方法——告诉 LLM "我叫 echo,我能回显消息,我需要一个 string 类型的 message 参数,调用我就返回这个消息"。

可选覆盖的(8 个方法,都有默认实现)

方法默认值作用
estimated_cost()None预估调用成本(API 费用等)
estimated_duration()None预估执行时间
requires_sanitization()true返回值是否需要安全过滤
requires_approval()Never是否需要用户确认才能执行
execution_timeout()60 秒超时时间
domain()Orchestrator执行域(进程内 vs 容器内)
sensitive_params()哪些参数包含敏感信息(日志脱敏)
rate_limit_config()None限流配置

这种"少量必须 + 大量可选"的设计让简单工具保持简单,复杂工具按需扩展。在 tiny-claw 中,我们只实现前 4 个必须方法,跳过所有安全和成本相关的可选方法——那些是 Phase 04 的内容。


🔍 JSON Schema:让 AI 理解参数

Tool 设计中最精妙的部分不是执行逻辑,而是参数声明

parameters_schema() 返回一个 JSON Schema,它告诉 LLM:"你要调用这个工具,需要传什么参数、什么类型、哪些是必填的"。这是 Tool 和 LLM 之间的契约语言。

以 IronClaw 的 echo 工具为例:

{
  "type": "object",
  "properties": {
    "message": {
      "type": "string",
      "description": "The message to echo back"
    }
  },
  "required": ["message"]
}

这个 schema 同时服务两个目的:

  1. 对 LLM——作为 function calling 的参数定义传给 API,LLM 据此生成正确的 JSON 参数
  2. 对系统——在执行前验证参数合法性,拒绝不合规的调用

IronClaw 有一个 validate_tool_schema() 函数来校验 schema 本身的合法性——顶层必须是 "type": "object",必须有 "properties" 字段。这是一种"schema 的 schema"验证,确保工具开发者不会写出 LLM 无法理解的参数声明。

OpenAI Strict Mode 的适配

这里有一个实践中的重要细节:OpenAI 的 strict function calling 要求比标准 JSON Schema 更严格:

  • 所有 properties 都必须出现在 required 数组中
  • 可选字段用 "type": ["string", "null"] 表示
  • 必须有 "additionalProperties": false

IronClaw 通过 normalize_schema_strict() 自动将普通 JSON Schema 转换为 strict mode 格式。工具开发者只需要写标准的 JSON Schema,系统自动适配不同 LLM 的要求。


🧠 ToolOutput 和 ToolError:结果的标准化

工具执行完成后,返回的不是裸数据,而是结构化的 ToolOutput

字段类型含义
resultserde_json::Value工具输出的 JSON 结果
costOption<Decimal>实际成本(如果有)
durationDuration执行耗时
rawOption<String>原始文本输出(可选)

这种标准化让系统可以统一处理所有工具的输出——无论是简单的 echo 还是复杂的 HTTP 请求,结果格式一致。duration 字段特别有用,它让系统可以追踪哪些工具执行慢、哪些成本高。

错误也是类型化的——ToolError 不是一个裸字符串,而是枚举了 7 种错误场景:

错误含义处理方式
InvalidParameters参数不合法告诉 LLM 参数错了,让它重试
ExecutionFailed执行失败报告错误,LLM 决定是否重试
Timeout超时报告超时时间
NotAuthorized没有权限阻止执行
RateLimited触发限流等待后重试
ExternalService外部服务错误报告错误详情
Sandbox沙箱错误报告隔离环境的问题

类型化错误让 agent 可以做出有区分的决策——参数错误应该重新生成参数,而不是换一个工具;限流应该等待,而不是报错。这比 OpenCode 中 throw new Error("something went wrong") 丰富得多。


🏛️ ToolRegistry:工具的注册与发现

工具写好了,怎么让系统知道它们的存在?这就是 ToolRegistry 的工作。

IronClaw 的 ToolRegistry 是一个运行时注册表:

ToolRegistry
├── tools: HashMap<String, Arc<dyn Tool>>    // 名称 → 工具实例
├── builtin_names: HashSet<String>           // 受保护的内置工具名
└── rate_limiter: RateLimiter                // 全局限流器

它提供三个核心操作:

操作方法说明
注册register()添加工具到注册表,拒绝覆盖内置工具
查找get(name)按名称获取工具实例
列表tool_definitions()生成 LLM function calling 所需的工具定义列表

tool_definitions() 是连接 Tool 和 LLM 的桥梁——它遍历所有注册的工具,把每个工具的 name()description()parameters_schema() 组装成 LLM API 需要的格式。

保护机制值得注意:内置工具(echo、time、shell 等)的名称被标记为"受保护",外部插件不能注册同名工具来覆盖它们。这防止了一种攻击——恶意 WASM 插件替换核心工具来窃取数据。

在 tiny-claw 中,我们简化为最基本的 HashMap 注册,暂不需要保护机制和限流——那些是后续 Phase 的内容。


🔍 LlmProvider:统一调用多个 LLM

Tool 是 agent 的"手",LlmProvider 是 agent 的"脑"。

为什么需要 Provider 抽象?

LLM 的竞争格局意味着你不可能只用一家。OpenAI、Anthropic、本地 Ollama——每家的 API 格式不同、能力不同、成本不同。Provider 抽象的目标是让上层代码不关心具体用哪个 LLM

IronClaw 的 LlmProvider trait 定义了两个核心方法:

方法作用
complete()纯文本对话,不涉及工具
complete_with_tools()带工具调用能力的对话(function calling)

为什么分两个方法?因为不是所有 LLM 都支持 function calling。complete() 是兜底方案——即使 LLM 不支持结构化工具调用,agent 也能用纯文本 prompt 引导 LLM 输出类似的结果。

消息模型

LLM 对话建立在消息序列上。IronClaw 定义了统一的消息类型:

角色含义
System系统提示词,定义 agent 的行为
User用户输入
AssistantLLM 的回复(可能包含文本和工具调用)
Tool工具执行结果

一次完整的工具调用对话看起来是这样的:

[System]    你是一个 AI 助手,可以使用以下工具...
[User]      现在几点了?
[Assistant]  → tool_call: time({ "operation": "now" })
[Tool]      2026-03-09T10:30:00+08:00
[Assistant]  现在是 2026 年 3 月 9 日上午 10:30。

注意 Assistant 消息有两种形态——纯文本回复和工具调用请求。当 LLM 决定使用工具时,它不会返回文本,而是返回一个结构化的 ToolCall(工具名 + JSON 参数)。系统执行完工具后,把结果作为 Tool 消息追加到对话中,再让 LLM 继续推理。

这就是 ReAct(Reasoning + Acting)模式的消息层表示——我们会在 Phase 02 深入展开。

OpenAI-Compatible 实现

IronClaw 通过 rig-core 库来统一多个 LLM 的调用。rig-core 是一个 Rust LLM 框架,封装了 OpenAI、Anthropic、Ollama 等多个 provider 的 API 差异。

但 rig-core 的抽象不够——IronClaw 在它上面又包了一层 RigAdapter,处理一些实际问题:

  1. Schema 适配——不同 LLM 对 JSON Schema 的要求不同,normalize_schema_strict() 统一处理
  2. Tool call 恢复——有些 LLM(尤其是小模型)不返回结构化的 tool_calls,而是在文本中输出 XML 格式的工具调用。recover_tool_calls_from_content() 能从文本中解析出这些"非标准"的工具调用
  3. ID 生成——有些 LLM 不返回 tool_call_id,IronClaw 自动生成

这种"适配器上的适配器"在实际工程中很常见——第三方库解决了 80% 的问题,剩下的 20% 需要你自己处理。

在 tiny-claw 中,我们不用 rig-core,直接用 reqwest 调用 OpenAI-compatible API。这更简单、更容易理解,也足够覆盖学习目标。


🧠 三层 Tool 体系

IronClaw 有一个 tiny-claw 不会完整实现、但值得理解的设计——三层 Tool 体系

层级执行环境信任级别例子
Built-in进程内完全信任echo、time、memory_write
WASMwasmtime 沙箱受限信任第三方插件
MCP外部进程需要能力声明外部 MCP server

对 LLM 来说,这三种工具看起来完全一样——都是 name + description + parameters_schema。差异在执行层:Built-in 直接在进程内调用,WASM 工具被加载到 wasmtime 沙箱中执行(有文件系统和网络限制),MCP 工具通过进程间通信调用外部服务。

ToolDomain 枚举区分了 Orchestrator(进程内)和 Container(容器内)两个执行域。Dispatcher 根据 domain 决定工具在哪里执行。

这种分层设计的哲学是信任递减——你自己写的工具完全信任,第三方插件部分信任,外部服务最小信任。每一层的权限边界不同。


📝 tiny-claw Phase 01 的简化策略

理解了 IronClaw 的完整设计后,我们在 tiny-claw 中做以下简化:

IronClawtiny-claw (Phase 01)简化了什么
12 个 trait 方法4 个必须方法跳过安全、成本、限流
ToolRegistry + 保护 + 限流简单 HashMap 注册无保护、无限流
rig-core + RigAdapterreqwest 直调 OpenAI API只支持 OpenAI-compatible
7 种 ToolError3 种错误InvalidParameters、ExecutionFailed、Timeout
ToolOutput 含 cost/raw只有 result + duration无成本追踪
三层 Tool 体系只有 Built-inWASM 和 MCP 是后续 Phase
Schema 适配 + 恢复直传 schema假设 LLM 能正确使用

目标是用最少的代码让工具系统跑起来,验证核心概念。安全、成本、多 Provider 等高级特性留到后续 Phase。


📝 本篇总结

理解项描述
Tool trait4 个必须方法 + 8 个可选方法,"少量必须 + 大量可选"的设计
JSON SchemaTool 和 LLM 之间的参数契约语言
ToolOutput/Error结构化结果和类型化错误,让 agent 做有区分的决策
ToolRegistry运行时注册表,tool_definitions() 是连接 Tool 和 LLM 的桥梁
LlmProvidercomplete() + complete_with_tools() 统一多 LLM 调用
消息模型System/User/Assistant/Tool 四种角色,ReAct 模式的消息层表示
三层 ToolBuilt-in → WASM → MCP,信任递减

核心洞察

Tool 系统的设计哲学是声明式——工具不需要知道 LLM 怎么工作,LLM 不需要知道工具怎么实现。JSON Schema 是两者之间的唯一契约。这种松耦合让工具和 LLM 可以独立演进。


下一篇IronClaw 学习笔记 02:Agent Loop —— ReAct 循环的实现,消息模型设计,以及 Dispatcher 如何路由意图。