1. 项目概述:当LLM遇上工具箱

如果你和我一样,长期在AI应用开发的一线折腾,特别是围绕大语言模型(LLM)构建各种自动化流程或智能体(Agent),那你一定对“工具调用”(Tool Calling)这个概念又爱又恨。爱的是,它让LLM从一个只能“纸上谈兵”的聊天机器,变成了一个能真正“动手做事”的智能体,可以查询天气、发送邮件、操作数据库,甚至控制智能家居。恨的是,要把这套机制从零搭建起来,尤其是在一个团队项目中保持清晰、可维护、可扩展,那真是费时费力,各种胶水代码和重复劳动层出不穷。

最近在GitHub上闲逛时,我发现了 PetroIvaniuk/llms-tools 这个项目。光看标题,它像是一个关于“LLM工具”的仓库,但深入进去你会发现,它远不止是一个简单的工具集合。它更像是一个为LLM应用开发者准备的、开箱即用的“工具箱框架”。这个项目的核心价值,在于它试图标准化和简化“为LLM定义和使用工具”这一过程。它不关心你用哪个具体的LLM(OpenAI、Anthropic、本地模型等),而是专注于解决一个更底层、更通用的问题:如何高效、优雅地管理你的工具集,并让LLM能够方便地调用它们。

简单来说, llms-tools 帮你把那些繁琐的、与具体LLM提供商API强耦合的工具定义、参数解析、错误处理、结果格式化等“脏活累活”给封装和抽象了。它提供了一套清晰的接口和装饰器,让你能用最Pythonic的方式,将一个普通的Python函数“包装”成一个LLM可理解、可调用的工具。对于正在构建复杂LLM智能体、需要集成大量外部API或内部系统的开发者来说,这无疑能极大提升开发效率和代码质量。接下来,我就结合自己的实践经验,深入拆解这个项目的设计思路、核心用法以及如何将它融入到你自己的项目中。

2. 核心设计理念与架构解析

2.1 为什么我们需要一个“工具框架”?

在深入代码之前,我们先聊聊痛点。假设你要让LLM帮你查股票价格。传统的、最直接的做法可能是:

  1. 写一个函数 get_stock_price(symbol: str)
  2. 在调用LLM的代码里,手动描述这个函数的功能和参数(通常是一段JSON Schema格式的文本)。
  3. LLM返回一个包含函数名和参数的JSON。
  4. 你的代码需要解析这个JSON,找到对应的函数,传入参数,执行它。
  5. 捕获函数执行结果(或异常),再格式化成LLM能理解的文本,送回给LLM进行后续对话。

这只是一个工具。当你有10个、50个工具时,问题就来了:

  • 维护地狱 :每个工具的描述(名称、功能、参数schema)散落在代码各处,更新一个参数就得改好几个地方。
  • 重复劳动 :为不同LLM提供商(OpenAI的 function calling , Anthropic的 tools , Google的 tools )适配工具描述,虽然核心逻辑一样,但格式细节有差异。
  • 胶水代码泛滥 :大量的 if-else 来判断该调用哪个工具,手动组装修饰返回结果。
  • 缺乏统一管理 :工具的动态注册、按需加载、权限控制、调用日志记录等高级功能,都需要自己从头搭建。

llms-tools 的出现,正是为了解决这些工程化问题。它的设计理念是 “约定优于配置” “关注点分离” 。开发者只需要关心工具函数本身的业务逻辑,而将“如何让LLM认识并调用这个工具”的复杂性交给框架处理。

2.2 项目架构与核心组件

浏览 llms-tools 的源码,其核心架构非常清晰,主要围绕以下几个关键组件展开:

  1. @tool 装饰器 :这是项目的灵魂。你只需要在普通的Python函数上添加 @tool 装饰器,框架就会自动提取函数的名称、文档字符串(docstring)、参数类型注解,并将其转换成一个标准的工具定义。这极大地减少了样板代码。

  2. 工具注册表(Registry) :所有被 @tool 装饰的函数,默认会被注册到一个全局的或指定的注册表中。这个注册表负责管理所有可用工具的集合,你可以从中查询、过滤、获取工具列表。这实现了工具的集中化管理。

  3. 工具调用引擎 :这是执行调用的核心。它接收来自LLM的“工具调用请求”(通常是一个包含工具名和参数字典的JSON对象),根据工具名从注册表中找到对应的函数,验证参数类型,执行函数,并处理可能发生的异常。它将执行结果封装成一个标准格式。

  4. 提供者适配器(Provider Adapters) :这是框架“不绑定具体LLM”的关键。它提供了将内部标准工具定义,转换为不同LLM提供商所需特定格式(如OpenAI Functions, Anthropic Tools)的能力。你不需要为每个提供商重写工具描述。

  5. 结果格式化 :工具执行后,可能需要将结果(可能是复杂对象、异常信息)格式化成适合LLM理解的纯文本或简单结构。框架提供了默认的格式化逻辑,也允许自定义。

这种架构的好处是,你的业务逻辑(工具函数)是纯净的,与任何LLM SDK解耦。当你需要切换LLM提供商,或者将工具集暴露给不同的AI平台时,只需更换或配置对应的适配器即可,工具函数本身一行代码都不用改。

3. 从零开始:定义与使用你的第一个工具

理论说得再多,不如动手试一下。我们来看看如何用 llms-tools 快速创建一个工具。

3.1 基础安装与环境准备

首先,安装这个库。通常可以通过pip从GitHub直接安装开发版本,或者等待其发布到PyPI。这里假设我们已经可以安装:

pip install llms-tools
# 或者从源码安装
# pip install git+https://github.com/PetroIvaniuk/llms-tools.git

安装完成后,在你的Python脚本中导入核心装饰器:

from llms_tools import tool

3.2 使用 @tool 装饰器定义工具

假设我们要创建一个查询天气的工具。在没有框架时,我们可能要先写函数,再手动写一个JSON描述。现在,一切都变得简单:

import requests
from typing import Literal
from pydantic import Field
from llms_tools import tool

@tool
def get_current_weather(
    location: str = Field(..., description="The city and state, e.g. San Francisco, CA"),
    unit: Literal["celsius", "fahrenheit"] = "celsius"
) -> str:
    """
    Get the current weather in a given location.

    Args:
        location: The city and state/country.
        unit: The unit of temperature, either 'celsius' or 'fahrenheit'.

    Returns:
        A string describing the current weather.
    """
    # 这里是模拟数据,真实情况应该调用天气API
    # 例如:response = requests.get(f"https://api.weatherapi.com/v1/current.json?key=YOUR_KEY&q={location}")
    weather_info = {
        "location": location,
        "temperature": 22 if unit == "celsius" else 72,
        "unit": unit,
        "condition": "sunny",
        "humidity": 65
    }
    return f"The current weather in {weather_info['location']} is {weather_info['condition']} with a temperature of {weather_info['temperature']}°{weather_info['unit'][0].upper()}."

# 另一个例子:计算器工具
@tool
def calculator(a: float, b: float, operation: Literal["add", "subtract", "multiply", "divide"]) -> float:
    """Performs a basic arithmetic operation."""
    if operation == "add":
        return a + b
    elif operation == "subtract":
        return a - b
    elif operation == "multiply":
        return a * b
    elif operation == "divide":
        if b == 0:
            raise ValueError("Cannot divide by zero.")
        return a / b

看,代码非常直观。 @tool 装饰器做了以下几件重要的事:

  1. 自动提取元数据 :它读取了函数的 __name__ ( get_current_weather ) 作为工具名。
  2. 解析文档字符串 :将 """Get the current weather...""" 这段文档作为工具的“描述”(description),这对于LLM理解工具用途至关重要。
  3. 分析参数注解 :它利用 pydantic.Field typing.Literal 等类型注解来构建每个参数的详细定义,包括类型、是否必需、默认值以及来自 Field description location: str = Field(..., description="...") 这行不仅定义了类型,还为LLM提供了更友好的参数说明。
  4. 推断返回类型 -> str 指明了返回类型,虽然LLM不直接使用,但对框架内部处理和开发者理解有帮助。

实操心得:善用类型注解和Pydantic 这是让工具定义清晰、减少错误的关键。尽量为每个参数使用具体的类型(如 str , int , List[str] ),并使用 Literal Enum 来限制枚举值。 pydantic.Field description 参数一定要认真写,这直接决定了LLM是否能正确理解和使用这个参数。避免使用过于复杂的泛型,除非你确信目标LLM的 function calling 能力支持它。

3.3 注册工具与获取工具列表

定义好的工具会自动注册到默认的全局注册表。你可以方便地获取所有工具的定义,这些定义已经被转换成了标准的、结构化的格式(通常是Pydantic模型列表),方便你传递给LLM。

from llms_tools import get_tools_list

# 获取所有已注册工具的定义
tools_definitions = get_tools_list()
print(f"Registered tools: {[t.name for t in tools_definitions]}")
# 输出可能:['get_current_weather', 'calculator']

# 查看某个工具的详细定义
weather_tool_def = next(t for t in tools_definitions if t.name == "get_current_weather")
print(weather_tool_def.json_schema()) # 输出该工具的JSON Schema

这个 tools_definitions 列表,就是你需要注入到LLM对话上下文中的“工具清单”。不同的LLM SDK接收这个清单的方式不同,这正是适配器要解决的问题。

3.4 调用工具:处理LLM的请求

当LLM在对话中决定要调用一个工具时,它会返回一个结构化的消息。以OpenAI的格式为例,它可能长这样:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "get_current_weather",
        "arguments": "{\"location\": \"Beijing, China\", \"unit\": \"celsius\"}"
      }
    }
  ]
}

你的应用程序需要解析这个消息,并执行对应的工具。 llms-tools 提供了 call_tool 函数来优雅地处理这个过程:

from llms_tools import call_tool

# 模拟从LLM响应中提取出的工具调用请求
tool_call_request = {
    "name": "get_current_weather",
    "arguments": {"location": "Beijing, China", "unit": "celsius"}
}

try:
    # 调用工具
    result = call_tool(tool_call_request)
    print(f"Tool execution result: {result}")
    # 输出: Tool execution result: The current weather in Beijing, China is sunny with a temperature of 22°C.
except Exception as e:
    # 处理工具执行中可能出现的错误(如参数验证失败、API调用异常等)
    result = f"Error executing tool {tool_call_request['name']}: {str(e)}"
    print(result)

call_tool 函数内部会:

  1. 根据 name 在注册表中查找工具。
  2. 使用Pydantic模型验证 arguments 字典,确保类型和约束符合定义。
  3. 执行工具函数。
  4. 捕获异常并转化为统一的错误格式。
  5. 返回执行结果(或错误信息)。

这个结果,你需要再作为一条“工具调用结果”消息,附加到对话历史中,发送回LLM,让它基于结果生成后续的回复。

4. 高级用法与集成实践

掌握了基础用法,我们来看看如何在实际项目中更有效地利用 llms-tools

4.1 与主流LLM SDK集成

llms-tools 的核心优势之一是易于集成。它不替代OpenAI或LangChain等库,而是与它们协同工作。下面以OpenAI SDK和LangChain为例。

与OpenAI SDK集成: 你需要将 get_tools_list() 得到的工具定义,转换成OpenAI API要求的格式。虽然可以手动转换,但 llms-tools 通常提供了便捷方法或适配器。

import openai
from llms_tools import get_tools_list_for_openai # 假设存在这样的辅助函数

client = openai.OpenAI(api_key="your-api-key")

# 1. 准备工具列表
tools = get_tools_list_for_openai() # 此函数将内部格式转为OpenAI格式

# 2. 在对话中传入工具定义
response = client.chat.completions.create(
    model="gpt-4-turbo-preview",
    messages=[{"role": "user", "content": "What's the weather like in Tokyo?"}],
    tools=tools, # 关键:传入工具定义
    tool_choice="auto", # 让模型自行决定是否调用工具
)

# 3. 检查响应中是否有工具调用
if response.choices[0].message.tool_calls:
    for tool_call in response.choices[0].message.tool_calls:
        # 4. 使用 llms-tools 执行调用
        tool_name = tool_call.function.name
        import json
        tool_args = json.loads(tool_call.function.arguments)
        result = call_tool({"name": tool_name, "arguments": tool_args})

        # 5. 将结果追加到消息历史,再次请求LLM
        messages.append(response.choices[0].message) # 添加助理的消息(包含工具调用)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": str(result) # 结果需要是字符串
        })
        # ... 继续下一轮对话

与LangChain集成: LangChain本身有强大的 Tool 抽象和 bind_tools 方法。 llms-tools 可以作为一个更轻量、更Pythonic的工具定义层,然后将其转化为LangChain的 Tool 对象。

from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
from llms_tools import get_tools_list

# 假设我们将 llms-tools 定义的工具转为 LangChain Tool
def convert_to_langchain_tool(llm_tool_def):
    from langchain.tools import Tool
    # 这里需要根据 llm_tool_def 创建一个可调用函数
    # 可能需要一个包装器来匹配 call_tool 的调用方式
    # 这是一个简化的示意
    def wrapper(**kwargs):
        return call_tool({"name": llm_tool_def.name, "arguments": kwargs})
    return Tool(
        name=llm_tool_def.name,
        func=wrapper,
        description=llm_tool_def.description,
        args_schema=... # 可以从 llm_tool_def 生成
    )

llm = ChatOpenAI(model="gpt-4-turbo")
tools = [convert_to_langchain_tool(td) for td in get_tools_list()]

prompt = ChatPromptTemplate.from_messages([...])
agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = agent_executor.invoke({"input": "What is 15度 Celsius in Fahrenheit? Use calculator."})

4.2 工具的组织与管理:模块化与动态加载

在大型项目中,工具可能分散在不同的模块或文件中。 llms-tools 的自动注册机制(基于装饰器)通常依赖于模块导入。因此,良好的做法是创建一个专门的包或模块来集中管理工具。

my_llm_app/
├── tools/
│   ├── __init__.py
│   ├── weather.py      # 包含 @tool 装饰的 get_current_weather
│   ├── calculator.py   # 包含 @tool 装饰的 calculator
│   ├── web_search.py   # 搜索工具
│   └── database.py     # 数据库操作工具
├── agents/
│   └── main_agent.py
└── main.py

tools/__init__.py 中,你可以显式导入所有工具模块,确保它们被注册:

# tools/__init__.py
from . import weather
from . import calculator
from . import web_search
from . import database

# 也可以提供一个便捷函数来获取所有工具
from llms_tools import get_tools_list as _get_tools_list
def get_all_tools():
    return _get_tools_list()

main.py 或你的Agent启动脚本中,只需导入 tools 包,所有工具就自动就绪了。

# main.py
import tools # 这行导入会执行tools/__init__.py,从而注册所有工具
from llms_tools import get_tools_list
# 现在 get_tools_list() 已经包含了所有定义的工具

对于需要动态加载工具的场景(例如,根据用户权限加载不同工具集),你可以使用非全局的注册表,或者通过编程方式控制哪些工具模块被导入。

4.3 错误处理、日志与监控

健壮的工具调用离不开良好的错误处理。 call_tool 函数会抛出异常,你需要捕获并妥善处理。

  • 参数验证错误 :如果LLM提供的参数不符合工具定义(例如,缺少必需字段、类型错误、枚举值无效),Pydantic验证会抛出 ValidationError 。你应该将这个错误信息清晰地返回给LLM,让它有机会纠正。
  • 工具执行错误 :工具函数内部可能出错(如网络超时、API返回错误、除零错误)。框架会捕获这些异常。最佳实践是将原始异常信息记录到日志(用于调试),但返回给LLM一个更通用、更友好的错误信息,避免泄露内部细节。
  • 工具未找到错误 :如果请求的工具名未注册, call_tool 会抛出 KeyError 或自定义异常。

建议在你的调用逻辑外层包裹一个统一的错误处理层:

def safe_call_tool(tool_call_request):
    """
    安全地调用工具,并返回适合LLM理解的结果字符串。
    """
    try:
        result = call_tool(tool_call_request)
        # 成功,返回结果字符串
        return str(result)
    except ValidationError as e:
        # 参数错误,给LLM清晰的反馈
        error_msg = f"Parameter validation failed for tool '{tool_call_request['name']}': {e.errors()}"
        logger.warning(error_msg)
        return error_msg
    except Exception as e:
        # 其他执行错误
        error_msg = f"Tool '{tool_call_request['name']}' execution failed."
        logger.exception(f"{error_msg} Request: {tool_call_request}") # 记录详细异常到日志
        return error_msg + " Please try again or contact support."

此外,为重要的工具调用添加日志记录和监控(如调用次数、成功率、耗时)是非常有价值的,这有助于你了解智能体的行为模式和性能瓶颈。

5. 常见问题、排查技巧与进阶思考

在实际使用中,你可能会遇到一些典型问题。这里分享一些排查思路和进阶技巧。

5.1 常见问题速查表

问题现象 可能原因 排查步骤与解决方案
LLM不调用工具 1. 工具描述(description)不清晰。
2. 提示词(Prompt)未引导LLM使用工具。
3. 工具定义格式与LLM提供商不兼容。
1. 检查工具函数的docstring是否清晰描述了功能和每个参数。用更自然、详细的语言重写。
2. 在系统提示词(System Prompt)中明确告诉LLM它可以使用哪些工具,并给出示例。
3. 确认传递给LLM API的 tools 参数格式正确。使用框架提供的适配器函数进行转换。
LLM调用工具时参数错误 1. 参数类型或约束定义不明确。
2. LLM对参数理解有偏差。
1. 强化参数的类型注解和 Field(description=...) 。对于分类参数,使用 Literal Enum
2. 在工具描述中提供参数示例。例如: location (str): The city and state, e.g. 'San Francisco, CA'
call_tool 抛出 ValidationError 1. LLM提供的参数字典键名与函数参数名不匹配。
2. 参数值类型无法转换(如字符串“abc”传给int)。
1. 确保函数参数名是清晰的英文单词,避免缩写。LLM有时会“创造”参数名。
2. 检查LLM返回的 arguments JSON字符串,确保其结构正确。考虑在调用前进行轻量级的预处理或后处理。
工具执行结果LLM无法理解 工具返回的对象太复杂(如字典、列表),LLM难以解析。 始终让工具函数返回字符串 。这是最稳妥的方式。在函数内部将复杂数据格式化成一段连贯的自然语言描述。例如,不要返回 {'temp':22, 'condition':'sunny'} ,而是返回 "The temperature is 22°C and the weather is sunny."
动态工具集加载失败 工具模块未被Python解释器导入,装饰器未执行。 确保在调用 get_tools_list() 之前,已经通过 import 语句或动态导入( importlib.import_module )加载了包含 @tool 装饰器的模块。

5.2 性能与优化考量

  • 冷启动延迟 :如果工具函数内部需要加载大型模型或建立连接池,第一次调用会较慢。可以考虑在应用启动时进行“预热”调用,或使用懒加载配合缓存。
  • 工具数量膨胀 :当工具数量非常多(例如上百个)时,将它们全部一次性塞给LLM会消耗大量上下文令牌,可能影响模型性能和理解。解决方案是:
    • 路由(Routing) :设计一个主“路由”Agent,它根据用户意图,调用不同的子Agent或工具集。每个子集只包含相关工具。
    • 动态工具检索 :维护一个工具向量数据库,根据用户查询的语义相似度,动态检索最相关的几个工具提供给LLM。
  • 异步工具调用 :对于I/O密集型工具(如网络请求、数据库查询),应将其定义为异步函数( async def ),并在异步上下文中调用,以避免阻塞主线程。确保你的LLM调用框架(如LangChain、Semantic Kernel)支持异步工具执行。

5.3 安全与权限控制

在开放给用户使用的系统中,工具调用必须考虑安全:

  • 输入验证与净化 :即使LLM提供了参数,在工具函数内部也必须对输入进行再次验证和净化,防止注入攻击(特别是涉及数据库、系统命令的工具)。
  • 权限分级 :不是所有用户都能使用所有工具。可以在工具注册时附加权限标签,在 call_tool 之前,根据当前用户上下文检查是否有权调用该工具。 llms-tools 本身不提供此功能,但你可以通过自定义装饰器或包装 call_tool 函数来实现。
  • 沙箱环境 :对于执行代码、访问敏感系统的工具,应考虑在沙箱或受限环境中运行。

PetroIvaniuk/llms-tools 项目为我们提供了一个优雅的抽象层,让我们能更专注于工具本身的业务逻辑,而不是繁琐的集成细节。它特别适合那些希望快速构建原型,同时又需要代码保持整洁、可维护的中大型LLM应用项目。当然,它目前可能还不是一个功能极其完备的企业级框架(例如在权限、审计、分布式调用方面),但其设计理念和实现方式,为我们构建自己的LLM工具基础设施提供了极佳的范式和起点。在实际项目中,你可以直接使用它,也可以借鉴其思想,打造更适合自己团队内部使用的工具管理框架。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐