WeClaw_42_Agent工具注册全链路:从BaseTool到意图识别的标准化接入
作者 : WeClaw 开发团队
日期 : 2026-03-29
版本 : v1.0
标签: Agent 工具、BaseTool、意图识别、渐进式暴露、延迟注入

📖 摘要
本文系统讲解 WeClaw Agent 工具注册的完整链路 。当需要将一个新功能(如远程文件分享)注册为 LLM 可调用的 Agent 工具时,需要经历 8 个步骤的标准化流程。文章以 remote_file_share 工具为实战案例,深入剖析 BaseTool 抽象基类、ActionDef 定义、tools.json 配置、意图识别三表联动、渐进式工具暴露引擎、延迟依赖注入等核心机制。
核心收获:
-
🧩 掌握 BaseTool 抽象基类和 ActionDef 模型
-
⚙️ 理解 tools.json 声明式工具注册
-
🎯 学会意图识别三表联动(CATEGORIES + MAPPING + PRIORITY)
-
🔍 掌握渐进式工具暴露的三级策略
-
💉 了解延迟依赖注入模式的应用场景
🎯 需求背景:为什么需要工具注册?
从"能做"到"会调"
在第 41 篇中,我们实现了 send_file_to_pwa() 等文件传输方法。但这些方法只是 RemoteBridgeClient 的内部 API,大模型(LLM)无法自动调用。
用户: "帮我做个 PPT 发到手机上"
❌ 没有工具注册时:
AI: "PPT 已生成,保存在 generated/xxx.pptx" ← 无法发送
✅ 注册为 Agent 工具后:
AI: 1. 调用 ppt_generator 生成 PPT
2. 自动调用 remote_file_share_send_file(file_path="generated/xxx.pptx")
3. "PPT 已生成并发送到你的手机!"
完整注册链路一览
① 创建工具模块 → ② tools.json → ③ 意图关键词 → ④ 工具映射
↓ ↓ ↓ ↓
BaseTool 子类 声明式注册 INTENT_CATEGORIES INTENT_TOOL_MAPPING
↓
⑧ 全链路校验 ← ⑦ GUI 注入 ← ⑥ 构造参数 ← ⑤ 工具名前缀
↓ ↓ ↓ ↓
validate.py gui_app.py registry.py tool_exposure.py
🧩 步骤一:创建工具模块
BaseTool 抽象基类
所有 Agent 工具都继承自 BaseTool,需实现两个核心方法:
python
from src.tools.base import ActionDef, BaseTool, ToolResult, ToolResultStatus
class RemoteFileShareTool(BaseTool):
"""远程文件分享工具。"""
name = "remote_file_share"
emoji = "📤"
title = "远程文件分享"
description = "将桌面端文件发送到 PWA 端"
timeout = 180 # 大文件上传超时
def get_actions(self) -> list[ActionDef]:
"""定义工具支持的动作列表。"""
...
async def execute(self, action: str, params: dict) -> ToolResult:
"""执行指定动作。"""
...
ActionDef 定义
每个 Action 定义为一个 ActionDef,包含名称、描述和 JSON Schema 参数:
python
def get_actions(self) -> list[ActionDef]:
return [
ActionDef(
name="send_file",
description=(
"将桌面端本地文件发送到 PWA 端用户。"
"当你为远程 PWA 用户生成了文件,必须调用此工具将结果发送回去。"
),
parameters={
"file_path": {
"type": "string",
"description": "要发送的本地文件绝对路径",
},
"description": {
"type": "string",
"description": "文件描述(会显示在 PWA 端)",
},
"user_id": {
"type": "string",
"description": "目标 PWA 用户 ID(留空则发送给当前会话用户)",
},
},
required_params=["file_path"],
),
ActionDef(
name="send_files",
description="将多个桌面端本地文件批量发送到 PWA 端用户。",
parameters={
"file_paths": {
"type": "array",
"items": {"type": "string"},
"description": "要发送的本地文件路径列表",
},
# ...
},
required_params=["file_paths"],
),
ActionDef(
name="send_voice",
description="将语音/音频文件作为语音消息发送到 PWA 端。",
parameters={
"file_path": {"type": "string", ...},
"transcript": {"type": "string", ...},
# ...
},
required_params=["file_path"],
),
]
Schema 生成机制
BaseTool.get_schema() 自动将 ActionDef 转换为 OpenAI Function Calling 兼容格式:
python
# 函数名格式:{tool_name}_{action_name}
# 例如:remote_file_share_send_file
# 生成的 schema 示例:
{
"type": "function",
"function": {
"name": "remote_file_share_send_file",
"description": "将桌面端本地文件发送到 PWA 端用户...",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string", ...},
"description": {"type": "string", ...},
"user_id": {"type": "string", ...},
},
"required": ["file_path"]
}
}
}
⚙️ 步骤二:tools.json 声明式注册
json
{
"remote_file_share": {
"enabled": true,
"module": "src.tools.remote_file_share",
"class": "RemoteFileShareTool",
"display": {
"name": "远程文件分享",
"emoji": "📤",
"description": "将桌面端本地文件发送到 PWA 端",
"category": "communication"
},
"config": {},
"security": {
"risk_level": "low",
"require_confirmation": false
},
"actions": ["send_file", "send_files", "send_voice"]
}
}
字段说明:
| 字段 | 作用 |
|------|------|
| module / class | 动态导入路径 |
| display.category | 意图分类关联 |
| config | 传递给构造函数的参数 |
| security.risk_level | 高风险工具需用户确认 |
| actions | 声明支持的动作(用于校验) |
🎯 步骤三~四:意图识别三表联动
WeClaw 的意图识别系统基于三张核心映射表协同工作:
表一:INTENT_CATEGORIES --- 关键词 → 意图
python
INTENT_CATEGORIES: dict[str, list[str]] = {
"communication": [
"发送文件", "发文件到手机", "传文件", "分享文件",
"发到PWA", "发送到手机", "传到手机", "发给手机",
"发送语音", "语音消息", "发语音", "录音发送",
"远程分享", "远程发送", "文件传输", "文件分享",
"发到浏览器", "发送到浏览器",
],
# ... 其他 17 个意图维度
}
表二:INTENT_TOOL_MAPPING --- 意图 → 工具列表
python
INTENT_TOOL_MAPPING: dict[str, list[str]] = {
"communication": ["wechat", "remote_file_share"],
# ... 其他意图
}
表三:INTENT_PRIORITY_MAP --- 工具优先级
python
INTENT_PRIORITY_MAP: dict[str, dict[str, list[str]]] = {
"communication": {
"recommended": ["wechat", "remote_file_share"],
"alternative": [],
},
# ... 其他意图
}
三表协同工作流
用户输入: "把这份报告发到手机上"
↓
INTENT_CATEGORIES 匹配: "发到手机" → communication (置信度 0.9)
↓
INTENT_TOOL_MAPPING 查找: communication → ["wechat", "remote_file_share"]
↓
INTENT_PRIORITY_MAP 排序: recommended → ["wechat", "remote_file_share"]
↓
渐进式暴露引擎: 高置信度 → 仅暴露 recommended 工具
↓
LLM 可见工具: [wechat, remote_file_share] (而非全部 51+ 工具)
🔍 步骤五:工具名前缀注册
多下划线工具名的解析难题
LLM 调用函数时使用 {tool_name}_{action_name} 格式:
remote_file_share_send_file
│ │ │ │
└─── tool_name ───┘ └ action┘
但简单按第一个下划线拆分会得到错误结果:
python
"remote_file_share_send_file".split("_")[0] # → "remote" ❌
解决方案:known_prefixes 前缀表
python
def _extract_tool_name(func_name: str) -> str:
"""从函数名中提取工具名。"""
known_prefixes = [
"browser_use", "app_control", "voice_input",
"remote_file_share", # 本次新增
"daily_task", "medication",
# ... 共 40+ 前缀
]
for prefix in known_prefixes:
if func_name.startswith(prefix + "_") or func_name == prefix:
return prefix
# 默认:取第一个下划线前的部分
return func_name.split("_")[0] if "_" in func_name else func_name
注意事项 :validate_tool_chain.py 中也有一份硬编码的前缀列表,需同步更新。
🔧 步骤六~七:构造参数与依赖注入
registry.py --- 构造参数映射
python
def _build_init_kwargs(self, tool_name: str, cfg: dict) -> dict:
"""从工具配置中提取构造参数。"""
kwargs = {}
tool_config = cfg.get("config", {})
if tool_name == "shell":
kwargs["timeout"] = tool_config.get("timeout", 30)
elif tool_name == "browser":
kwargs["headless"] = tool_config.get("headless", False)
elif tool_name == "remote_file_share":
# 无构造参数,bridge_client 通过延迟注入
pass
# ... 50+ 工具分支
return kwargs
延迟依赖注入模式
RemoteFileShareTool 依赖 RemoteBridgeClient,但两者的创建时机不同:
时间线:
1. gui_app.py 创建 ToolRegistry → 注册所有工具(含 RemoteFileShareTool)
2. gui_app.py 创建 MainWindow → MainWindow 创建 RemoteBridgeClient
3. gui_app.py 将 bridge_client 注入到 RemoteFileShareTool ← 延迟注入
注入代码:
python
# gui_app.py --- MainWindow 创建后
remote_bridge = getattr(self._window, '_remote_bridge', None)
if remote_bridge:
rfs_tool = self._tool_registry.get_tool("remote_file_share")
if rfs_tool and hasattr(rfs_tool, "set_bridge_client"):
rfs_tool.set_bridge_client(remote_bridge)
logger.info("已为 remote_file_share 工具注入 bridge_client")
工具端接口:
python
class RemoteFileShareTool(BaseTool):
def __init__(self) -> None:
self._bridge_client = None # 延迟注入
def set_bridge_client(self, bridge_client) -> None:
"""注入 RemoteBridgeClient 实例。"""
self._bridge_client = bridge_client
def _check_bridge(self) -> ToolResult | None:
"""检查 bridge 是否可用。"""
if not self._bridge_client:
return ToolResult(status=ToolResultStatus.ERROR,
error="远程桥接客户端未初始化")
if not self._bridge_client.is_connected:
return ToolResult(status=ToolResultStatus.ERROR,
error="远程桥接未连接")
return None
自动选取 PWA 用户
当 LLM 未指定 user_id 时,自动选取第一个在线 PWA 用户:
python
def _resolve_user_id(self, params: dict) -> str:
"""解析目标用户 ID。"""
user_id = params.get("user_id", "")
if user_id:
return user_id
# 自动选取当前在线的 PWA 用户
if self._bridge_client and self._bridge_client.stats.pwa_connections:
first_conn = self._bridge_client.stats.pwa_connections[0]
return first_conn.user_id
return ""
✅ 步骤八:全链路校验
validate_tool_chain.py 七项检查
bash
$ python scripts/validate_tool_chain.py
============================================================
WinClaw 工具全链路一致性校验
============================================================
已加载 tools.json: 58 个启用工具
✅ [1/7] INTENT_TOOL_MAPPING 覆盖: 58 工具
✅ [2/7] INTENT_TOOL_MAPPING 引用有效
✅ [3/7] INTENT_PRIORITY_MAP 引用有效
✅ [4/7] _extract_tool_name 已知前缀覆盖
✅ [5/7] dependencies 引用有效
✅ [6/7] _build_init_kwargs 覆盖
✅ [7/7] 三表 key 对齐: 18 个意图
结果: 7 通过, 0 警告, 0 失败
============================================================
校验项说明
| 校验 | 检查内容 | 确保 |
|------|---------|------|
| [1] | tools.json 中的工具是否都在 MAPPING 中 | 不遗漏 |
| [2] | MAPPING 引用的工具是否都在 tools.json 中 | 不多引 |
| [3] | PRIORITY 引用的工具是否都在 tools.json 中 | 不多引 |
| [4] | 多下划线工具名在 known_prefixes 中 | 解析正确 |
| [5] | 依赖的 input_sources 工具存在 | 依赖有效 |
| [6] | _build_init_kwargs 有对应分支 | 构造正确 |
| [7] | 三表的 key 完全对齐 | 意图一致 |
💡 经验教训
1. 前缀表需双重同步
教训 :只更新了 tool_exposure.py 的 known_prefixes,校验仍然失败。
原因 :validate_tool_chain.py 中有一份硬编码的副本,也需要同步更新。
最佳实践:每次新增多下划线工具,必须同时更新两处。
2. 延迟注入的时序依赖
教训:工具注册在 ToolRegistry 创建时完成,但 bridge 在 MainWindow 创建时才存在。
解决方案 :使用 set_bridge_client() 模式,与 CronTool 的 set_agent_dependencies() 保持一致。
3. 意图关键词的覆盖度
教训:初始只添加了"发送文件"一个关键词,用户说"传到手机"时无法匹配。
解决方案:充分枚举同义变体(18 个关键词覆盖各种口语表达)。
📊 架构总结
工具注册八步标准流程
| 步骤 | 文件 | 内容 |
|------|------|------|
| ① | src/tools/xxx.py | 创建 BaseTool 子类 |
| ② | config/tools.json | 声明式注册 |
| ③ | src/core/prompts.py | INTENT_CATEGORIES 关键词 |
| ④ | src/core/prompts.py | INTENT_TOOL_MAPPING + PRIORITY |
| ⑤ | src/core/tool_exposure.py | known_prefixes 前缀 |
| ⑥ | src/tools/registry.py | _build_init_kwargs 分支 |
| ⑦ | src/ui/gui_app.py | 延迟依赖注入(按需) |
| ⑧ | scripts/validate_tool_chain.py | 全链路校验 |
字数统计: 约 5,200 字
阅读时间: 约 14 分钟
代码行数: 约 350 行
4. 3. 2. 4. 3. - - - - - > > > >