大模型LLMs框架Langchain之工具Tools

写在前文:

下面是官方对工具的流程:

创建工具

创建工具时,必须指定参数:name、description、args_schema、return_direct

初始化环境

python 复制代码
import asyncio
from typing import Annotated, List

from langchain.agents import initialize_agent, AgentType
from langchain_core.messages import HumanMessage

from utils import *
from langchain_core.tools import tool, BaseTool, ToolException, Tool

1、创建工具:使用注解@tool创建一个工具函数并执行

1.1、不带注解信息

python 复制代码
@tool
# @tool(description="Multiply two numbers.")
def multiply_1(a: int, b: int) -> int:
    # 如果不使用"# @tool(description="Multiply two numbers.")"那么必须要有下面这个""""Multiply two numbers."""",这个虽然像是注释,相当于方法描述 主要作用就是让LLMs知道这个方法的功能,即"description"的值,
    # 不添加要报错:ValueError: Function must have a docstring if description not provided.
    """Multiply two numbers."""
    return a * b


print(multiply_1.name)
print(multiply_1.description)
print(multiply_1.args)
print(multiply_1.invoke({"a": 1, "b": 2}))
print("---------------Demo1:使用注解@tool 创建一个工具函数,并执行---------------")

输出:
multiply_1
Multiply two numbers.
{'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
10

1.2、携带注解信息

python 复制代码
@tool
def multiply_by_max_2(
        a: Annotated[int, "参数一"],
        b: Annotated[List[int], "参数二,为一个list列表,选最大的一个值进行计算"],
) -> int:
    """Multiply a by the maximum of b."""
    return a * max(b)


print(multiply_by_max_2.args_schema.model_json_schema())
print(multiply_by_max_2.invoke({"a": 2, "b": [1, 3, 5, 4, 9]}))
print("---------------Demo2:使用注解信息---------------")
打印出:
{'description': 'Multiply a by the maximum of b.', 'properties': {'a': {'description': '参数一', 'title': 'A', 'type': 'integer'}, 'b': {'description': '参数二,为一个list列表,选最大的一个值进行计算', 'items': {'type': 'integer'}, 'title': 'B', 'type': 'array'}}, 'required': ['a', 'b'], 'title': 'multiply_by_max_2', 'type': 'object'}
18

1.3、自定义json实体为参数

python 复制代码
# 此处如果使用v1版本,可能报错"AttributeError: type object 'CalculatorInput' has no attribute 'model_json_schema'" ----- 兼容问题
from pydantic import BaseModel, Field


class CalculatorInput_3(BaseModel):
    a: int = Field(description="first number")
    b: int = Field(description="second number")


@tool("multiplication-tool", args_schema=CalculatorInput_3, return_direct=True)
def multiply_3(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


print(multiply_3.name)
print(multiply_3.description)
print(multiply_3.args)
print(multiply_3.return_direct)
print("---------------Demo3:自定义json实体为参数---------------")

打印出:
multiplication-tool
Multiply two numbers.
{'a': {'description': 'first number', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'second number', 'title': 'B', 'type': 'integer'}}
True

2、创建工具:结构化工具创建StructuredTool.from_function

python 复制代码
from langchain_core.tools import StructuredTool


class CalculatorInput_4(BaseModel):
    a: int = Field(description="first number")
    b: int = Field(description="second number")


def multiply_4(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


calculator_4 = StructuredTool.from_function(
    func=multiply_4,
    name="Calculator",
    description="multiply numbers",
    args_schema=CalculatorInput_4,
    return_direct=True,
)
print(calculator_4.invoke({"a": 2, "b": 3}))
print(calculator_4.name)
print(calculator_4.description)
print(calculator_4.args)
print("---------------Demo4:结构化工具StructuredTool---------------")

输出:
6
Calculator
multiply numbers
{'a': {'description': 'first number', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'second number', 'title': 'B', 'type': 'integer'}}

3、创建工具:使用chain.as_tool创建工具

python 复制代码
from langchain_core.language_models import GenericFakeChatModel
from langchain_core.output_parsers import StrOutputParser, PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from utils import *
# 使用chain.as_tool返回工具
prompt_5 = ChatPromptTemplate.from_messages(
    [("human", "你好,我叫 {name}")]
)

llm_5 = get_llm('ollama')
chain_5 = prompt_5 | llm_5 | StrOutputParser()

as_tool_5 = chain_5.as_tool(name="Style responder", description="输入用户名,返回中文李清照的个性化问候语")
print(as_tool_5.invoke({"name":"张三"}))
print(as_tool_5.args)
print("---------------Demo5:使用langchain的chain---------------")
输出:
你好!我是张三,有什么我可以帮你的吗?😊
{'name': {'title': 'Name', 'type': 'string'}}

4、创建工具:自定义工具(同步和异步)

python 复制代码
from typing import Optional, Type

from langchain_core.callbacks import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)


class CalculatorInput_6(BaseModel):
    a: int = Field(description="first number")
    b: int = Field(description="second number")

# 基本上是固定的

class CustomCalculatorTool_6(BaseTool):
    name: str = "Calculator"
    description: str = "useful for when you need to answer questions about math"
    args_schema: Type[BaseModel] = CalculatorInput_6
    return_direct: bool = True

    def _run(self, a: int, b: int, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
        """Use the tool."""
        print("使用_run工具...")
        return a * b

    async def _arun(self, a: int, b: int, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, ) -> str:
        """Use the tool asynchronously."""
        print("使用_arun工具...")
        # 异步函数await只能在async内部使用
        return self._run(a, b, run_manager=run_manager.get_sync())


multiply_6 = CustomCalculatorTool_6()
print(multiply_6.name)
print(multiply_6.description)
print(multiply_6.args)
print(multiply_6.return_direct)

print(multiply_6.invoke({"a": 2, "b": 3}))

# 使用异步方法
async def main_6():
    print(await multiply_6.ainvoke({"a": 2, "b": 4}))
asyncio.run(main_6())


输出:
Calculator
useful for when you need to answer questions about math
{'a': {'description': 'first number', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'second number', 'title': 'B', 'type': 'integer'}}
True
使用_run工具...
6
使用_arun工具...
使用_run工具...
8

5、单独使用异步方法

python 复制代码
from langchain_core.tools import StructuredTool


def multiply_7(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


calculator_7 = StructuredTool.from_function(func=multiply_7)
print(calculator_7.invoke({"a": 2, "b": 3}))


# 异步调用(需封装在异步函数中)
async def main_7():
    result = await calculator_7.ainvoke({"a": 2, "b": 5})
    print(result)  # 输出: 10

# 运行异步函数
asyncio.run(main_7())

输出:
6
10

6、工具异常以及定义返回类型

工具异常

python 复制代码
import random
from typing import List, Tuple

from langchain_core.tools import tool


def get_weather_8(city: str) -> int:
    """获取一个城市的天气情况"""
    raise ToolException(f"Error: 这个错误是通过handle_tool_error=True定义 {city}.")
get_weather_tool_8 = StructuredTool.from_function(
    func=get_weather_8,
    handle_tool_error=True,
)

# 直接返回handle_tool_error的值。
get_weather_tool_8_1 = StructuredTool.from_function(
    func=get_weather_8,
    handle_tool_error="这是通过handle_tool_error字符串的形式来定义...",
)

# 自定义函数返回
def _handle_error_8(error: ToolException) -> str:
    return f"T这是i通过函数的形式自定义工具异常信息: `{error.args[0]}`"
get_weather_tool_8_2 = StructuredTool.from_function(
    func=get_weather_8,
    handle_tool_error=_handle_error_8,
)
# 这三个都会调用get_weather_8方法,只不过使用handle_tool_error=字符串/函数时,它的返回值会覆盖get_weather_8中抛出来的错误信息
# 输出:Error: 这个错误是通过handle_tool_error=True定义 猫猫城.
print(get_weather_tool_8.invoke({"city": "猫猫城"}))
# 输出:这是通过handle_tool_error字符串的形式来定义...
print(get_weather_tool_8_1.invoke({"city": "喵喵城"}))
# 输出:T这是i通过函数的形式自定义工具异常信息: `Error: 这个错误是通过handle_tool_error=True定义 U.u城.`
print(get_weather_tool_8_2.invoke({"city": "U.u城"}))

指定工具返回类型

返回为"Tuple"

python 复制代码
import random
from typing import List, Tuple

from langchain_core.tools import tool
"""
使用@Tool创建工具,以及定义返回类型为"Tuple"
"""
# 如果我们返回的参数是"Tuple"需要指定 response_format;默认是"content"
@tool(response_format="content_and_artifact")
def generate_random_ints_8(min: int, max: int, size: int) -> Tuple[str, List[int]]:
    """ 工具函数必须要指定docstring否则会报错... """
    array = [random.randint(min, max) for _ in range(size)]
    content = f"Successfully generated array of {size} random ints in [{min}, {max}]."
    return content, array

# 输出:原始调用:Successfully generated array of 10 random ints in [0, 9].
print(f"原始调用:{generate_random_ints_8.invoke({"min": 0, "max": 9, "size": 10})}")
tool_call_8 = generate_random_ints_8.invoke(
    {
        "name": "generate_random_ints",
        "args": {"min": 0, "max": 9, "size": 10},
        "id": "123",  # required
        "type": "tool_call",  # required
    }
)
# 输出:这是使用工具类型调用: content='Successfully generated array of 10 random ints in [0, 9].' name='generate_random_ints_8' tool_call_id='123' artifact=[8, 4, 1, 8, 4, 2, 1, 2, 2, 1]
print(f"这是使用工具类型调用: {tool_call_8}")


"""
我们也可以使用BaseTool自定义工具
"""
class GenerateRandomFloats_8(BaseTool):
    name: str = "generate_random_floats"
    description: str = "Generate size random floats in the range [min, max]."
    response_format: str = "content_and_artifact"
    ndigits: int = 2

    def _run(self, min: float, max: float, size: int) -> Tuple[str, List[float]]:
        range_ = max - min
        array = [
            round(min + (range_ * random.random()), ndigits=self.ndigits)
            for _ in range(size)
        ]
        content = f"Generated {size} floats in [{min}, {max}], rounded to {self.ndigits} decimals."
        return content, array
rand_gen_8 = GenerateRandomFloats_8(ndigits=4)
zidingyi_8 = rand_gen_8.invoke(
    {
        "name": "generate_random_floats",
        "args": {"min": 0.1, "max": 3.3333, "size": 3},
        "id": "123",
        "type": "tool_call",
    }
)
# 输出:自定义工具调用: content='Generated 3 floats in [0.1, 3.3333], rounded to 4 decimals.' name='generate_random_floats' tool_call_id='123' artifact=[0.9652, 1.1374, 1.6313]
print(f"自定义工具调用: {zidingyi_8}")

7、使用内置工具/工具包

工具详情参考

复制代码
中文参考:https://www.langchain.com.cn/docs/integrations/tools/
英文参考:https://python.langchain.com/docs/integrations/tools/
python 复制代码
# 中文参考:https://www.langchain.com.cn/docs/integrations/tools/
# 英文参考:https://python.langchain.com/docs/integrations/tools/

from utils import *
os.environ['serpapi_api_key'] = 'xxxxxx'
os.environ["TAVILY_API_KEY"] = 'tvly-xxxxxxx'

# 提供了很多常用的网站,比如天气网站,可以查询到很多天气
tavily = TavilySearchResults(max_results=2)

# 提供了搜索功能
serpApi = SerpAPIWrapper()

print(tavily.invoke("2025年3月28日成都天气怎么样?"))
print(serpApi.run("什么是langchain"))

8、综合使用:LLMs+Tool+格式化参数模板

python 复制代码
# tool的name要和后面格式化参数的class名称一致,不然需要定义Pydantic来映射
# ### 此处名称略微复杂不太规范,是为了展示那个名称和那个名称是一一对应的。
@tool("Xiang_U_Tool_Name_10")
def xiang_U_fun_10(a: int, b: int) -> int:
    """ a和b参数需要"U"时才会使用
    Args:
        a: First integer
        b: Second integer
    """
    print(f'调用了U函数:[{a},{b}]')
    return a + b


@tool("Xiang_T_Tool_Name_10")
def xiang_T_fun_10(a: int, b: int) -> int:
    """ a和b参数需要"T"时才会使用
    Args:
        a: First integer
        b: Second integer
    """
    print(f'调用了T函数:[{a},{b}]')
    return a * b

# 将工具添加到List集合中
tools_utils_10 = [xiang_T_fun_10, xiang_U_fun_10]
# 这儿需要取开通GLM模型的key...
# ### 本人测试DK系列的好像都不支持tool调用,Qwen系有一部分不支持...
# #### llama 不支持调用 Tools ,所以此处报错...
# 支持工具调用:qwen2.5:latest、llama3.1:latest、llama3-groq-tool-use 
llm_10 = ChatOpenAI(base_url='https://open.bigmodel.cn/api/paas/v4/',
                          api_key='xxxxx.xxxxx',
                          model='glm-4-plus',
                          temperature=temperature,
                          verbose=verbose)
# llm_10 = ChatOllama(model='deepseek-r1:1.5b', temperature=0, verbose=True)

# 将工具集合绑定到LLM中
llm_with_tools_10 = llm_10.bind_tools(tools_utils_10)
query = "What is 8 U 7?what is 11 T 49?"

# 使用llm。正常情况下此时,"U"和"T"LLMs是不知道什么意思的,
# 但是我在@Tool工具函数中有备注"xiang_U_fun_10(a和b参数需要"U"时才会使用)、xiang_T_fun_10(a和b参数需要"T"时才会使用)",
# 所以LLMs模型会自动调用这两个方法 ----- 注意,DK系列和Qwen的好像有些模型不太支持Tools工具调用系统会报错"ollama._types.ResponseError: registry.ollama.ai/library/deepseek-r1:1.5b does not support tools (status code: 400)"
# --- 所以我使用GLM系列的模型是OK的。
ai_msg = llm_with_tools_10.invoke(query)
# 只会显示要调用的工具,但是不会真正调用:content='' additional_kwargs={'tool_calls': [{'id': 'call_-8844174561398977396', 'function': {'arguments': '{"a": 8, "b": 7}', 'name': 'Xiang_U_Tool_Name_10'}, 'type': 'function', 'index': 0}, {'id': 'call_-8844174561398977395', 'function': {'arguments': '{"a": 11, "b": 49}', 'name': 'Xiang_T_Tool_Name_10'}, 'type': 'function', 'index': 1}], 'refusal': None} response_metadata={'token_usage': {'completion_tokens': 42, 'prompt_tokens': 342, 'total_tokens': 384, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'glm-4-plus', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None} id='run-0134a6a4-8487-4f6f-8ab8-49bcda5f2cb4-0' tool_calls=[{'name': 'Xiang_U_Tool_Name_10', 'args': {'a': 8, 'b': 7}, 'id': 'call_-8844174561398977396', 'type': 'tool_call'}, {'name': 'Xiang_T_Tool_Name_10', 'args': {'a': 11, 'b': 49}, 'id': 'call_-8844174561398977395', 'type': 'tool_call'}] usage_metadata={'input_tokens': 342, 'output_tokens': 42, 'total_tokens': 384, 'input_token_details': {}, 'output_token_details': {}}
print(f"只会显示要调用的工具,但是不会真正调用:{ai_msg}")  # 只会显示要调用的工具,但是不会真正调用

## 获得调用工具信息
# 输出:获得调用工具信息:[{'name': 'Xiang_U_Tool_Name_10', 'args': {'a': 8, 'b': 7}, 'id': 'call_-8844184731883541155', 'type': 'tool_call'}, {'name': 'Xiang_T_Tool_Name_10', 'args': {'a': 11, 'b': 49}, 'id': 'call_-8844184731883541154', 'type': 'tool_call'}]
print(f"获得调用工具信息:{llm_with_tools_10.invoke(query).tool_calls}")


# class 名称要与def定义的 @tool 方法名称name对应,不然要报"KeyError"
class Xiang_U_Tool_Name_10(BaseModel):
    """Add two integers."""
    a: int = Field(..., description="First integer")
    b: int = Field(..., description="Second integer")


class Xiang_T_Tool_Name_10(BaseModel):
    """Multiply two integers."""
    a: int = Field(..., description="First integer")
    b: int = Field(..., description="Second integer")


"""
# 如果class名称和def定义的工具名称不对应,可以通过如下方法解决
# 定义工具与 Pydantic 模型的映射
tool_parsers = {
    "xiang_u_10": PydanticOutputParser(pydantic_object=add_U_10),
    "xiang_t_10": PydanticOutputParser(pydantic_object=multiply_T_10),
}
# 创建一个 OutputParser,将工具名称映射到对应的解析器
tools_parser = PydanticToolsParser(tool_parsers=tool_parsers)
# 将解析器绑定到 LLM
chain_10 = llm_with_tools_10 | tools_parser
"""
# ### 通过Pydantic 定义参数模板
from langchain_core.output_parsers import PydanticToolsParser
chain_10 = (llm_with_tools_10
            | PydanticToolsParser(tools=[Xiang_U_Tool_Name_10, Xiang_T_Tool_Name_10]))  # 这里指定的是class Xiang_U_Tool_Name_10
# 输出:展示Pydantic重置映射关系:[Xiang_U_Tool_Name_10(a=8, b=7), Xiang_T_Tool_Name_10(a=11, b=49)]
print(f"展示Pydantic重置映射关系:{chain_10.invoke(query)}")

# 下面才是真正调用LLM的invoke方法获取LLMs最终生成的内容 --- 有点复杂,下章会结合Agent会简化很多,所以此种方法了解即可
# ### 获得最终的消息,原理就是:将用户输入的消息+AI的回复的消息+工具返回的消息进行封装后,再次传递给LLM获得最终的结果
# ### 即手动封装消息...类似于前面的历史记录一样...
# 1、先获得工具返回的消息
messages = [HumanMessage(content=query)]
messages.append(ai_msg)
# 选择要执行的工具
for tool_call in ai_msg.tool_calls:
    # tool_call = [{'name': 'Xiang_T_Tool_Name_10', 'args': {'a': 8, 'b': 7}, 'id': 'call_-8969335028076878645', 'type': 'tool_call'}]
    selected_tool = {"xiang_u_tool_name_10": xiang_U_fun_10, "xiang_t_tool_name_10": xiang_T_fun_10}[
        tool_call["name"].lower()]
    tool_msg = selected_tool.invoke(tool_call)  # 此处要想正常使用invoke,那么在定义工具的时候必须要有@tool注解(或者其他工具化的方法才行)
    messages.append(tool_msg)
    # 输出:工具获得的消息: content='15' name='Xiang_U_Tool_Name_10' tool_call_id='call_-8844174561398977396'
    # 输出:工具获得的消息: content='539' name='Xiang_T_Tool_Name_10' tool_call_id='call_-8844174561398977395'
    print(f"工具获得的消息: {tool_msg}")

# 输出:[What is 8 U 7?what is 11 T 49?]最终消息:8 U 7 的结果是 15,而 11 T 49 的结果是 539。这里的 "U" 和 "T" 可能代表某种特定的运算,但具体是什么运算没有给出。根据结果,我们可以推断 "U" 可能是加法,而 "T" 可能是乘法。
print(f"[{query}]最终消息:{llm_with_tools_10.invoke(messages).content}")
相关推荐
herogus丶7 小时前
【LLM】Elasticsearch作为向量库入门指南
elasticsearch·docker·langchain
处女座_三月21 小时前
大模型架构记录13【hr agent】
人工智能·python·深度学习·langchain
laopeng3011 天前
Spring AI ToolCalling 扩展模型能力边界
java·人工智能·大模型·spring ai
SanMu三木2 天前
LangChain 基础系列之 Prompt 工程详解:从设计原理到实战模板
langchain·prompt
放羊郎2 天前
OpenCV、YOLO与大模型的区别与关系
人工智能·opencv·yolo·大模型
姚家湾2 天前
MCP 学习笔记(1)
大模型·mcp
echola_mendes2 天前
LangChain 结构化输出:用 Pydantic + PydanticOutputParser 驯服 LLM 的“自由发挥”
服务器·前端·数据库·ai·langchain
喜欢吃豆3 天前
LLaMA-Factory使用实战
人工智能·大模型·json·llama
硅谷神农3 天前
LangChain 框架相关概念
人工智能·langchain