标签 插件化设计 下的文章

核心原则

OpenManus的工具系统基于「插件化设计」,所有自定义工具需继承框架的BaseTool基类,实现标准化接口,再通过配置文件注册,即可被AI智能体识别和调用。

一、前置准备

  1. 确认目录结构:在OpenManus项目根目录下,建议创建custom_tools目录存放自定义工具(便于管理):

    mkdir custom_tools  # 项目根目录执行
  2. 核心依赖:确保已安装基础依赖(无需额外安装,框架自带工具基类):

    pip install openmanus  # 若未安装框架核心包

二、步骤1:编写自定义工具类(核心)

所有自定义工具必须继承BaseTool基类,并实现3个核心要素:

  • name:工具唯一名称(AI通过名称识别工具)
  • description:工具描述(关键!AI通过描述判断何时调用该工具,需清晰说明「用途+输入格式」)
  • run():工具执行逻辑(接收输入参数,返回执行结果)

示例:开发「实时天气查询工具」

创建custom_tools/weather_tool.py文件,写入以下代码(含完整注释):

# 导入框架核心基类和结果封装类
from openmanus.tools.base import BaseTool, ToolResult
# 按需导入第三方依赖(如请求网络需requests)
import requests

# 自定义工具类,必须继承BaseTool
class WeatherQueryTool(BaseTool):
    # 1. 工具唯一名称(不可重复,建议英文)
    name = "WeatherQueryTool"
    
    # 2. 工具描述(核心!需明确:用途+输入格式+输出说明)
    description = """
    用于查询指定城市的实时天气信息,输入格式为「城市名」(如:北京、上海),
    输出格式为「城市名 + 温度 + 天气状况」(如:北京 18℃ 晴)。
    仅当用户询问天气相关问题时调用该工具。
    """

    # 3. 工具执行逻辑,必须实现run方法
    def run(self, city: str) -> ToolResult:
        """
        参数说明:
        - city: 字符串,用户输入的城市名
        返回值:ToolResult对象(封装执行结果,必填)
        """
        try:
            # 步骤1:校验输入(可选,增强健壮性)
            if not city or len(city) > 10:
                return ToolResult(
                    success=False,  # 执行失败标记
                    content="输入无效!请输入正确的城市名(如:北京)"
                )
            
            # 步骤2:核心业务逻辑(调用免费天气API)
            # 替换为可靠的天气API,此处使用wttr.in(无需密钥)
            url = f"http://wttr.in/{city}?format=3"  # 精简格式:城市名: 天气 温度
            response = requests.get(url, timeout=10)
            
            # 步骤3:处理响应并封装结果
            if response.status_code == 200:
                weather_info = response.text.strip()
                return ToolResult(
                    success=True,  # 执行成功标记
                    content=f"✅ {weather_info}"  # 返回给AI的内容
                )
            else:
                return ToolResult(
                    success=False,
                    content=f"❌ 天气查询失败,API响应码:{response.status_code}"
                )
        
        # 异常处理(必加,避免工具崩溃)
        except requests.exceptions.Timeout:
            return ToolResult(success=False, content="❌ 网络超时,无法查询天气")
        except Exception as e:
            return ToolResult(success=False, content=f"❌ 查询出错:{str(e)}")

关键说明:

  • ToolResult:框架规定的结果封装类,必须返回该类型,包含success(布尔值)和content(字符串)两个核心字段。
  • description的精准性:AI完全依赖这段描述判断「是否调用该工具」,需明确:

    • 工具用途(如「查询指定城市实时天气」)
    • 输入格式(如「输入为城市名,例:北京」)
    • 适用场景(如「仅用户问天气时调用」)

三、步骤2:配置文件注册自定义工具

修改OpenManus的核心配置文件config.yaml(无则从config.example.yaml复制),将自定义工具添加到tools列表中:

1. 复制配置模板(首次需做)

cp config.example.yaml config.yaml  # 项目根目录执行

2. 编辑config.yaml,添加工具配置

找到tools节点,新增自定义工具的配置项:

# config.yaml 核心配置片段
llm:
  type: openai
  model: gpt-4-turbo
  api_key: "sk-xxxxxx"  # 替换为你的LLM密钥
  base_url: "https://api.openai.com/v1"

# 工具注册列表(内置工具 + 自定义工具)
tools:
  # 保留框架内置工具(按需取舍)
  - name: BrowserTool        # 浏览器工具
  - name: CodeExecutorTool   # 代码执行工具
  - name: FileTool           # 文件操作工具
  
  # 新增自定义工具(关键配置)
  - name: WeatherQueryTool   # 必须和工具类的name一致
    path: custom_tools/weather_tool.py  # 工具文件的绝对/相对路径
    enabled: true  # 是否启用该工具(默认true)

配置说明:

  • name:必须和自定义工具类中定义的name完全一致(大小写敏感)。
  • path:工具文件的路径,支持相对路径(相对于项目根目录)或绝对路径。
  • enabled:是否启用该工具,设为false则AI不会调用。

四、步骤3:测试自定义工具

编写测试代码,验证自定义工具是否能被AI智能体识别并调用:

1. 创建测试文件test_custom_tool.py

import asyncio
from openmanus.agent import Agent  # 单智能体
from openmanus.config import Config  # 配置加载类

# 异步测试函数(OpenManus核心逻辑为异步)
async def test_weather_tool():
    # 步骤1:加载配置文件
    config = Config.from_file("config.yaml")
    
    # 步骤2:初始化AI智能体
    agent = Agent(config=config)
    
    # 步骤3:发送包含工具调用的任务指令
    task = "查询深圳市的实时天气"
    
    # 步骤4:执行任务并获取结果
    result = await agent.run(task)
    
    # 步骤5:打印结果
    print("=== 自定义工具调用结果 ===")
    print(result)

# 执行测试
if __name__ == "__main__":
    asyncio.run(test_weather_tool())

2. 运行测试代码

python test_custom_tool.py

预期输出:

=== 自定义工具调用结果 ===
✅ 深圳: 晴 25℃

五、进阶:支持多参数的自定义工具

若工具需要多个输入参数(如「根据城市和日期查询天气预报」),修改工具类的run方法即可:

示例:多参数天气工具

class WeatherQueryTool(BaseTool):
    name = "WeatherQueryTool"
    description = """
    查询指定城市指定日期的天气预报,输入格式为「城市名,日期」(日期格式:YYYY-MM-DD,例:北京,2026-01-20)。
    若未指定日期,则查询实时天气。
    """

    def run(self, input_str: str) -> ToolResult:
        # 解析多参数
        parts = input_str.split(",")
        city = parts[0].strip()
        date = parts[1].strip() if len(parts) > 1 else None
        
        # 核心逻辑(示例)
        if date:
            content = f"✅ {city} {date} 的天气预报:晴 22-30℃"
        else:
            content = f"✅ {city} 实时天气:晴 25℃"
        
        return ToolResult(success=True, content=content)

测试指令可改为:查询上海2026-01-20的天气预报

六、常见问题与排查

问题1:AI不调用自定义工具

  • 原因:description描述不清晰,AI无法判断何时调用;或工具名称/路径配置错误。
  • 解决:

    1. 优化description,明确「触发条件+输入格式」;
    2. 检查config.yaml中工具name是否和类名一致;
    3. 测试时指令明确(如「用WeatherQueryTool查询北京天气」)。

问题2:工具执行报错「找不到模块」

  • 原因:工具文件路径配置错误,或未继承BaseTool
  • 解决:

    1. 确认config.yamlpath是相对项目根目录的路径;
    2. 检查工具类是否正确导入from openmanus.tools.base import BaseTool

问题3:工具返回结果为空

  • 原因:run方法未正确返回ToolResult对象,或业务逻辑出错。
  • 解决:

    1. 确保run方法最后return ToolResult(...)
    2. run方法中添加日志(如print(city)),调试业务逻辑。

总结

  1. 核心步骤:自定义工具开发需遵循「继承BaseTool→实现name/description/run→配置文件注册→测试验证」的流程,缺一不可。
  2. 关键要点description是AI调用工具的核心依据,需精准描述用途和输入格式;ToolResult是结果返回的标准格式,必须使用。
  3. 扩展技巧:单参数工具直接接收字符串,多参数工具可通过分隔符(如逗号)解析输入,复杂场景可使用JSON格式传参。