IronClaw 学习笔记 01:Tool + Provider
工具抽象的设计哲学,以及如何统一调用多个 LLM
上一篇我们搭好了骨架,现在给 agent 装上"手"(Tool)和"脑"(LLM Provider)。
🎯 这一篇要解决什么问题?
Phase 00 的 tiny-claw 只是一个 echo 机器——你说什么,它原封不动地还给你。这不是 agent,这是鹦鹉。
一个真正的 agent 需要两个核心能力:
- 思考——调用 LLM 理解用户意图,决定下一步行动
- 行动——通过 Tool 与世界交互,执行具体任务
这两个能力对应 IronClaw 的两个核心抽象:Tool trait 和 LlmProvider trait。这一篇我们深入理解它们的设计哲学。
🧠 从 OpenCode 到 IronClaw:Tool 的概念迁移
在 OpenCode 系列中,我们已经学过 Tool 的基本概念——每个 Tool 有名称、描述、参数定义,LLM 通过 function calling 来选择和调用工具。
IronClaw 的 Tool 系统在这个基础上增加了三个维度:
| 维度 | OpenCode | IronClaw | 为什么需要 |
|---|---|---|---|
| 安全 | 无 | requires_sanitization、requires_approval、sensitive_params | 个人助手处理敏感数据 |
| 成本 | 无 | estimated_cost、estimated_duration、rate_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::Value | JSON 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 同时服务两个目的:
- 对 LLM——作为 function calling 的参数定义传给 API,LLM 据此生成正确的 JSON 参数
- 对系统——在执行前验证参数合法性,拒绝不合规的调用
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:
| 字段 | 类型 | 含义 |
|---|---|---|
result | serde_json::Value | 工具输出的 JSON 结果 |
cost | Option<Decimal> | 实际成本(如果有) |
duration | Duration | 执行耗时 |
raw | Option<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 | 用户输入 |
Assistant | LLM 的回复(可能包含文本和工具调用) |
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,处理一些实际问题:
- Schema 适配——不同 LLM 对 JSON Schema 的要求不同,
normalize_schema_strict()统一处理 - Tool call 恢复——有些 LLM(尤其是小模型)不返回结构化的 tool_calls,而是在文本中输出 XML 格式的工具调用。
recover_tool_calls_from_content()能从文本中解析出这些"非标准"的工具调用 - 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 |
| WASM | wasmtime 沙箱 | 受限信任 | 第三方插件 |
| 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 中做以下简化:
| IronClaw | tiny-claw (Phase 01) | 简化了什么 |
|---|---|---|
| 12 个 trait 方法 | 4 个必须方法 | 跳过安全、成本、限流 |
ToolRegistry + 保护 + 限流 | 简单 HashMap 注册 | 无保护、无限流 |
| rig-core + RigAdapter | reqwest 直调 OpenAI API | 只支持 OpenAI-compatible |
| 7 种 ToolError | 3 种错误 | InvalidParameters、ExecutionFailed、Timeout |
| ToolOutput 含 cost/raw | 只有 result + duration | 无成本追踪 |
| 三层 Tool 体系 | 只有 Built-in | WASM 和 MCP 是后续 Phase |
| Schema 适配 + 恢复 | 直传 schema | 假设 LLM 能正确使用 |
目标是用最少的代码让工具系统跑起来,验证核心概念。安全、成本、多 Provider 等高级特性留到后续 Phase。
📝 本篇总结
| 理解项 | 描述 |
|---|---|
| Tool trait | 4 个必须方法 + 8 个可选方法,"少量必须 + 大量可选"的设计 |
| JSON Schema | Tool 和 LLM 之间的参数契约语言 |
| ToolOutput/Error | 结构化结果和类型化错误,让 agent 做有区分的决策 |
| ToolRegistry | 运行时注册表,tool_definitions() 是连接 Tool 和 LLM 的桥梁 |
| LlmProvider | complete() + complete_with_tools() 统一多 LLM 调用 |
| 消息模型 | System/User/Assistant/Tool 四种角色,ReAct 模式的消息层表示 |
| 三层 Tool | Built-in → WASM → MCP,信任递减 |
核心洞察
Tool 系统的设计哲学是声明式——工具不需要知道 LLM 怎么工作,LLM 不需要知道工具怎么实现。JSON Schema 是两者之间的唯一契约。这种松耦合让工具和 LLM 可以独立演进。
下一篇:IronClaw 学习笔记 02:Agent Loop —— ReAct 循环的实现,消息模型设计,以及 Dispatcher 如何路由意图。