返回博客列表
技术教程2026-03-2922 分钟阅读

Agent工具注册全链路:从BaseTool到意图识别的标准化接入

从 BaseTool 到意图识别:一个新工具的标准化接入全流程。

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,需实现两个核心方法:

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 参数:

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 兼容格式:

# 函数名格式:{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 声明式注册

{
    "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 — 关键词 → 意图

INTENT_CATEGORIES: dict[str, list[str]] = {
    "communication": [
        "发送文件", "发文件到手机", "传文件", "分享文件",
        "发到PWA", "发送到手机", "传到手机", "发给手机",
        "发送语音", "语音消息", "发语音", "录音发送",
        "远程分享", "远程发送", "文件传输", "文件分享",
        "发到浏览器", "发送到浏览器",
    ],
    # ... 其他 17 个意图维度
}

表二:INTENT_TOOL_MAPPING — 意图 → 工具列表

INTENT_TOOL_MAPPING: dict[str, list[str]] = {
    "communication": ["wechat", "remote_file_share"],
    # ... 其他意图
}

表三:INTENT_PRIORITY_MAP — 工具优先级

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┘

但简单按第一个下划线拆分会得到错误结果:

"remote_file_share_send_file".split("_")[0]  # → "remote" ❌

解决方案:known_prefixes 前缀表

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 — 构造参数映射

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 ← 延迟注入

注入代码

# 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")

工具端接口

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 用户:

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 七项检查

$ 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.pyknown_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.pyINTENT_CATEGORIES 关键词
src/core/prompts.pyINTENT_TOOL_MAPPING + PRIORITY
src/core/tool_exposure.pyknown_prefixes 前缀
src/tools/registry.py_build_init_kwargs 分支
src/ui/gui_app.py延迟依赖注入(按需)
scripts/validate_tool_chain.py全链路校验

字数统计: 约 5,200 字
阅读时间: 约 14 分钟
代码行数: 约 350 行