Skip to content

工具系统架构

本文基于 code-map 快照编写。

MaiBot 的工具系统把插件工具、旧版 Action、MaiSaka 内置能力和外部 MCP 工具收敛到同一套抽象层。它不负责教插件作者如何写一个 @Tool,也不替代 插件 Tool 用法 中的开发教程。本文聚焦内部实现,说明工具声明、工具调用、Provider 适配和 ToolRegistry 路由如何协同工作。

1. 概述

MaiBot 当前统一四类工具来源:

插件 @Tool :插件运行时中的 Tool 组件。插件 SDK 使用 @Tool 声明工具,运行时把声明写入组件注册表,PluginToolProvider 再把这些工具暴露给统一工具层。

@Action :旧版插件中的 Action 组件。SDK 2.0 会把 @Action 自动转换为 Tool 声明,MaiBot 运行时仍保留兼容路径,使旧插件可以继续被 LLM 调用。

MaiSaka 内置 Tool :推理引擎自带的系统级能力,例如 send_emoji、记忆查询、回复、等待、结束本轮等。这些工具由 MaisakaBuiltinToolProvider 提供。

MCP Tool :通过 MCPToolProvider 桥接外部 MCP 服务器的远程工具。MCP 管理器负责连接、发现和调用,MaiBot 工具层只消费统一后的 ToolSpecToolExecutionResult

统一后的目标是让推理引擎只面对一种工具模型:

工具声明 :告诉 LLM 有哪些工具、工具做什么、参数是什么。

工具调用 :把 LLM 的选择转成可执行请求,包含工具名、参数、会话和流信息。

工具执行 :由对应 Provider 执行,返回统一结果,再写回对话历史或触发后续动作。

2. 架构图

这张图说明了两层边界。上层是工具来源,来源可以来自插件、旧 Action、内置模块或 MCP 服务器。下层是统一协议,所有来源都要变成 ToolProvider,再由 ToolRegistry 统一暴露给 MaiSaka 推理引擎。

概念层可以把 Provider 接口理解为 get_tools()execute_tool()。源码中的实际方法名是 list_tools()invoke(),二者职责一致:前者返回工具声明,后者执行工具调用。

3. 核心概念

3.1 ToolCall

定义 :LLM 在推理过程中产生的工具调用意图。

内部模型 :MaiBot 内部执行时使用 ToolInvocation,而不是直接复用模型 API 的原始 ToolCall

关键字段

tool_name :要执行的工具名。

arguments :LLM 生成的参数对象。

call_id :模型工具调用 ID,用于把结果放回正确位置。

session_id :会话 ID。

stream_id :聊天流 ID。

reasoning :模型选择该工具时的推理文本。

metadata :扩展信息,例如 anchor message、来源标记或调试字段。

ToolCall 是推理结果,ToolInvocation 是执行请求。MaiBot 在二者之间做标准化,避免每个 Provider 都理解不同模型的原始格式。

3.2 ToolIcon

定义 :统一工具图标定义。

源码模型ToolIcon

关键字段

src :图标资源地址。

mime_type :资源 MIME 类型。

sizes :图标尺寸列表。

图标不是 LLM 选择工具的必要信息。它主要服务于需要展示工具列表的 UI、监控面板或调试界面。工具声明中 icons 可以为空,不影响推理和调用。

3.3 ToolAnnotation

定义 :统一工具注解信息。

源码模型ToolAnnotation

关键字段

audience :工具面向的使用者或模型集合。

priority :工具优先级。

metadata :注解扩展字段。

注解用于表达工具的非功能信息。它不直接决定工具是否可调用,但可以为未来的调度、过滤、展示或权限判断提供结构化元数据。

3.4 ToolSpec

定义 :统一工具声明。

源码模型ToolSpec

关键字段

name :工具名,必须在统一工具视图中唯一。

description :给 LLM 使用的工具描述。

title :可选展示标题。

parameters_schema :参数 JSON Schema。

output_schema :输出 Schema,供支持结构化输出的模型使用。

provider_name :声明来自哪个 Provider。

provider_type :Provider 类型,例如 pluginbuiltinmcp

enabled :是否启用。

icons :图标列表。

annotation :工具注解。

metadata :扩展元数据。

ToolSpec 是工具系统的核心数据对象。所有来源都必须先变成 ToolSpec,才能进入 LLM 工具定义列表和调用路由。

3.5 ToolProvider

定义 :统一工具提供者接口。

源码模型ToolProvider Protocol。

概念方法

get_tools() :列出当前 Provider 可暴露的工具声明。源码对应 list_tools(context)

execute_tool() :执行指定工具调用。源码对应 invoke(invocation, context)

资源释放 :源码还要求 close(),用于释放 Provider 持有的外部连接或异步资源。

Provider 不关心其他来源如何注册,也不直接参与 LLM 选择。它只负责把自己的工具翻译成统一声明,并在被注册表选中时执行请求。

3.6 ToolRegistry

定义 :统一工具注册表。

源码模型ToolRegistry

职责

注册 Providerregister_provider() 保存 Provider。同名 Provider 后注册会替换先注册者。

注销 Providerunregister_provider() 按 Provider 名称移除。

列出工具list_tools() 按 Provider 顺序收集工具,并跳过重复名称。

查询工具get_tool_spec()has_tool() 用于判断某个工具是否存在。

生成 LLM 定义get_llm_definitions()ToolSpec 转为模型层可消费的 ToolDefinitionInput

执行调用invoke() 根据工具名找到负责 Provider,并返回统一结果。

关闭资源close() 关闭所有 Provider。

ToolRegistry 是工具系统的调度中心。它让 MaiSaka 推理引擎不需要知道工具来自插件、内置模块还是 MCP。

3.7 ToolExecutionContext

定义 :工具执行上下文。

关键字段

session_id :会话 ID。

stream_id :聊天流 ID。

reasoning :模型选择工具的推理文本。

is_group_chat :是否为群聊。

group_id :群 ID。

user_id :用户 ID。

platform :平台名称。

metadata :扩展上下文。

执行上下文把模型调用时的会话状态传给 Provider。插件工具尤其依赖这些字段,例如通过 stream_id 找到可发送消息的聊天流。

3.8 ToolAvailabilityContext

定义 :工具暴露可用性判断上下文。

关键字段

session_id :会话 ID。

stream_id :聊天流 ID。

is_group_chat :是否为群聊。

group_id :群 ID。

user_id :用户 ID。

platform :平台名称。

可用性上下文用于决定某个工具在当前聊天中是否应该暴露给 LLM。内置工具会根据群聊、私聊和配置过滤;插件工具也可以基于运行时状态做可见性判断。

3.9 ToolExecutionResult

定义 :统一工具执行结果。

关键字段

tool_name :被执行的工具名。

success :是否成功。

content :文本结果。

error_message :错误信息。

structured_content :结构化结果,通常是 dict 或 list。

content_items :可包含图片、音频、资源链接等多媒体结果项。

post_history_messages :执行后需要追加到历史的消息。

metadata :扩展元数据。

ToolExecutionResult.get_history_content() 会把结果转成适合写入历史消息的文本。若存在 content_items,会优先拼合可读摘要,避免直接把媒体二进制塞进 LLM 上下文。

4. 四类工具来源详解

4.1 插件 @Tool

源码入口maibot/src/plugin_runtime/tool_provider.py

ProviderPluginToolProvider

provider_nameplugin_runtime

provider_typeplugin

插件 @Tool 由 SDK 注册为插件组件。插件运行时启动后,Host 侧 ComponentRegistry 保存 Tool 条目,ComponentQueryService 提供只读查询视图。PluginToolProvider 不直接持有插件对象,而是通过 component_query_service 读取当前可用的工具声明。

声明阶段:

插件加载 :Runner 子进程加载插件,并注册 Tool 组件。

组件注册表 :Host 侧 ComponentRegistry 记录工具名、插件 ID、调用方法、参数 Schema、可见性和启用状态。

查询视图ComponentQueryService 把注册表条目转换为 ToolSpec

Provider 暴露PluginToolProvider.list_tools() 返回统一工具列表。

执行阶段:

工具名匹配PluginToolProvider.invoke() 根据 ToolInvocation.tool_name 找到 Tool 条目。

IPC 调用 :Host 通过插件运行时 RPC 调用 Runner 子进程中的插件方法。

结果归一化 :插件返回值被转换为 ToolExecutionResult

历史兼容 :旧 @Action 转换后的工具也走同一执行路径。

4.2 旧 @Action

源码入口 :插件 SDK 转换层和 plugin_runtime/tool_provider.py

Provider :仍由 PluginToolProvider 暴露。

兼容方式 :SDK 内部把 @Action 转换为 @Tool 声明。

旧 Action 的兼容重点不是让 LLM 知道它曾经是 Action,而是让它以 Tool 语义进入统一系统。转换后,旧 Action 会获得工具名、描述、参数 Schema 和调用入口。运行时保留必要的元数据,用于区分它来自 legacy component。

兼容边界:

不鼓励新插件使用 :新插件应直接使用 @Tool

保留执行路径 :MaiBot 运行时仍能调用由旧 Action 转换来的工具。

不重复 API 教程 :Action 到 Tool 的转换细节属于插件开发文档边界,本文只说明架构位置。

统一结果模型 :执行完成后仍返回 ToolExecutionResult,MaiSaka 不关心它来自旧 Action 还是新 Tool。

4.3 MaiSaka 内置 Tool

源码入口maibot/src/maisaka/builtin_tool/

ProviderMaisakaBuiltinToolProvider

provider_namemaisaka_builtin

provider_typebuiltin

内置工具是 MaiSaka 推理引擎的一部分,用于完成模型自身不能直接完成的核心动作。例如 send_emoji 发送表情包,记忆查询类工具读取长期记忆或人物画像,reply 发送回复,finish 结束本轮思考。

内置工具特点:

强绑定推理流程 :内置工具服务于 Planner、Timing Gate 和 Action Loop。

声明集中管理BUILTIN_TOOL_ENTRIES 集中声明工具名、spec 构造器和 handler。

阶段控制 :工具可标记为 timingactionboth

可见性控制 :工具可标记为 visibledeferredhidden

配置过滤 :部分工具根据全局配置启用或禁用。

聊天范围过滤 :部分工具只在群聊或私聊中暴露。

MaisakaBuiltinToolProviderlist_tools() 会调用内置工具聚合函数,按当前可用性上下文过滤工具。invoke() 则通过工具名找到对应 handler 并执行。

4.4 MCP Tool

源码入口maibot/src/mcp_module/provider.py

ProviderMCPToolProvider

provider_namemcp

provider_typemcp

MCP 工具来自外部 MCP 服务器。MaiBot 通过 MCPManager 连接服务器、发现工具、调用工具并关闭连接。MCPToolProvider 是这个能力的适配器。

MCP 工具特点:

外部能力 :工具实现位于 MaiBot 进程外。

运行时连接 :MaiBot 启动时根据 MCP 配置初始化管理器。

工具发现MCPManager.get_tool_specs() 返回统一 ToolSpec 列表。

工具调用MCPManager.call_tool_invocation() 执行远程调用。

资源释放MCPToolProvider.close() 会关闭 MCP 连接。

MCP 工具扩展了 MaiBot 的能力边界,但调用链仍保持统一。MaiSaka 只看到 ToolSpec,执行时只提交 ToolInvocation,最后只接收 ToolExecutionResult

5. 关键流程

5.1 工具注册

注册发生在 MaiSaka 运行时初始化阶段。内置 Provider 和插件 Provider 是默认注册的。MCP Provider 只有在 MCP 启用且成功发现工具时才会注册。

注册规则:

同名替换ToolRegistry.register_provider() 会先移除同名 Provider,再加入新 Provider。

顺序保留 :列出工具时按注册顺序遍历 Provider。

去重保护 :如果多个 Provider 暴露同名工具,先注册的保留,后出现的跳过并记录警告。

启用过滤ToolSpec.enabled 为 false 的工具不会进入统一列表。

5.2 工具发现

工具发现不是一次性静态快照。每次 MaiSaka 需要给模型准备工具定义时,都会通过 ToolRegistry.list_tools() 收集当前可用工具。

发现阶段会处理三类差异:

来源差异 :插件、内置、MCP 的声明来源不同,但最终都是 ToolSpec

上下文差异 :不同聊天流、群聊或私聊可能暴露不同工具。

可见性差异 :隐藏工具不会进入 LLM 工具列表,延迟发现工具也可能暂时不暴露。

5.3 推理引擎选择

MaiSaka 通过 ChatLoopService 设置统一 ToolRegistry。当 Planner 需要工具定义时,ToolRegistry.get_llm_definitions() 会把 ToolSpec 转为模型层工具定义。

选择过程:

模型看到工具列表 :LLM 根据 prompt、上下文和工具描述决定是否调用工具。

模型返回 ToolCall :模型返回工具名和参数。

MaiBot 构建 ToolInvocation :把模型调用标准化为内部请求。

Registry 查找 Provider :按工具名遍历 Provider,找到声明包含该工具且启用的 Provider。

执行对应 Provider :调用 provider.invoke()

5.4 工具调用

调用阶段的关键是定位 Provider。ToolRegistry.invoke() 会把 ToolExecutionContext 转成 ToolAvailabilityContext,用于匹配当前聊天环境中的工具声明。找到 Provider 后,它会执行工具并返回统一结果。

异常处理:

Provider 抛出异常 :Registry 捕获异常,返回失败的 ToolExecutionResult

Provider 未找到工具 :返回 未找到工具:{tool_name}

工具自身失败 :Provider 应返回 success=False,并填写 error_message

资源清理 :运行时关闭时调用 ToolRegistry.close(),由每个 Provider 自行释放资源。

5.5 结果返回

工具结果进入 MaiSaka 后,会被写入对话历史或触发后续动作。结果可能包含:

纯文本结果 :写入 content,适合简单查询类工具。

结构化结果 :写入 structured_content,适合模型继续分析的数据。

媒体内容项 :写入 content_items,例如图片、音频或资源链接。

后续消息 :写入 post_history_messages,用于补充工具执行后的上下文。

ToolExecutionResult.get_history_content() 负责生成历史摘要。它优先使用文本内容,其次使用内容项摘要,再其次使用结构化内容 JSON,最后才使用错误信息。

6. 与插件开发的关系

插件开发文档 Tool 组件 关注的是插件作者如何使用 @Tool、如何声明参数、如何返回值、如何处理图片和媒体。本文不重复这些 API 用法,只说明它们在内部架构中的位置。

对插件作者而言,需要理解三条边界:

声明边界 :插件使用 @Tool 声明能力,运行时把它变成 Tool 组件。

发现边界 :MaiSaka 通过 PluginToolProviderToolRegistry 发现插件工具。

执行边界 :插件方法在 Runner 子进程中执行,Host 通过统一工具协议接收结果。

对 MaiBot 内部实现而言,插件工具只是 ToolProvider 的一个来源。无论工具来自插件、旧 Action、内置模块还是 MCP,MaiSaka 最终只面对同一套 ToolSpecToolInvocationToolExecutionResult