LangChain 工具定义与工具调用全流程

📅 2026/8/26 16:26:09
LangChain 工具定义与工具调用全流程
目录​编辑一、什么是工具二、创建工具2.1 使用 tool 装饰器创建工具2.1.1 依赖 Pydantic 类2.1.2 依赖 Annotated2.2 使用 StructuredTool 创建工具实例1常规用法示例 2加入配置依赖 Pydantic 类示例 3加入 response_format 配置三、工具的绑定与调用3.1 工具的绑定3.2 工具的调用for循环处理多条tool_calls一、什么是工具工具调用根本作用是让大语言模型LLM具备与外部世界交互的能力。LLM 本身是一个封闭的知识系统其能力受限于其训练数据存在滞后性和内在的文本生成逻辑。它无法执行直接计算、查询实时信息、操作数据库或调用任何外部 API。工具调用打破了这层壁垒其作用具体体现在扩展能力边界模型可以借助工具完成它自身无法完成的任务如执行数学计算、搜索网络、查询数据库等。保证信息实时性通过调用搜索工具或数据库查询工具LLM 可以获取最新的、训练数据中不存在的信息避免回答过时或 “一本正经地胡说八道”。处理复杂任务将一个复杂的用户请求如 “分析我上个月的消费趋势”分解成多个步骤并依次调用不同的工具如 “从数据库获取数据”- “用 Python 进行数据分析” - “生成图表”来协同完成。协调这件事这更体现在 Agent 智能体上。连接现有系统可以将企业内部已有的系统、API 和数据库封装成工具让 LLM 成为一个用自然语言驱动的统一接口极大地提升了自动化和集成能力总的来说大模型本身是静态知识不能联网、不能查数据库、不能调用接口。Tool工具就是给大模型扩展外部能力的组件让模型可以发出调用请求由本地代码执行真实逻辑再把结果返回给大模型继续回答。在 LangChain 中聊天模型提供了额外的功能工具调用。它能使 LLM 与外部服务、API 和数据库进行交互。工具调用还可用于从非结构化数据中提取结构化信息并执行各种其他任务。例如当我们希望获取当前天气情况时由于 LLM 无法获取实时信息此时我们就可以借助工具通过外部服务进行搜索完成查询再例如当我们希望获取数据库表中的数据时由于 LLM 无法直接获取表数据此时我们就可以借助工具通过与数据库交互完成查询二、创建工具2.1 使用 tool 装饰器创建工具在 LangChain 中实现了一个 tool 装饰器来创建工具tool 装饰器是自定义工具的最简单方法。如下所示from langchain_core.tools import import tool tool def multiply(a: int, b: int) - int: Multiply two integers. Args: a: First integer b: Second integer return a * b print(multiply.invoke({a: 2, b: 3})) # 输出: 6 print(multiply.name) # 输出: multiply print(multiply.description) # 输出: Multiply two ...省略...b: Second integer print(multiply.args) # 输出: {a: {title: A, type: integer}, b: {title: B, type: integer}}可以看出工具通过 tool 加 Python 函数实现其中该装饰器默认使用函数名称作为工具名称。该装饰器将使用函数的文档字符串作为工具的描述。因此函数名、类型提示和文档字符串都是传递给工具 Schema 的一部分不可缺失。定义良好的描述是使模型良好运行的重要部分。对于工具 schema它将从函数名、类型提示和文档字符串中获取相关属性以此来声明一个工具包括其名称、描述、输入参数、输出类型等等。这里需要说明的是若是简单定义工具如上述示例工具 schema 需要解析 Google 风格的文档字符串去获取参数描述。什么是 Google 风格的文档字符串Google 风格是 Python 文档字符串的一种写作规范。它并非 Python 语言官方强制要求而是由 Google 为其内部 Python 项目制定的规范后来因为其极高的可读性和简洁性而在整个 Python 社区中变得非常流行。它使用 Args:Returns: 等关键字参数描述简洁明了如下所示def fetch_data(url, retries3): 从给定的URL获取数据。 Args: url (str): 要从中获取数据的URL。 retries (int, optional): 失败时重试的次数。默认为3。 Returns: dict: 从URL解析的JSON响应。 # ... 函数实现 ...综上所述工具的属性主要分为三大类工具名称工具参数工具描述。工具名称告诉大语言模型有哪些工具可以调用工具参数告诉模型如何来进行调用工具描述就类似于给大模型写提示词告诉大模型工具的功能让模型在相应的业务场景选择对应的工具完成业务处理。所以我们也可以说当定义工具的时候工具名称工具参数工具描述是必须要定义的。除了在定义python函数的时候通过文档字符串的方式写明工具描述。也可以通过其他类与方法依赖 Pydantic 类与Annotated就是其中之一2.1.1 依赖 Pydantic 类若使用 tool 定义工具时没有提供文档字符串则会报错from langchain_core.tools import tool tool def add(a: int, b: int) - int: return a b此时在 LangChain 中可以使用 Pydantic 类提供运行时数据验证和类型检查。通过Field(description...)添加字段描述LangChain 会自动提取。from pydantic import BaseModel, Field class AddInput(BaseModel): Add two integers. a: int Field(..., descriptionFirst integer) b: int Field(..., descriptionSecond integer)完整代码# pydantic 数据验证 from pydantic import BaseModel, Field from langchain_core.tools import tool class AddInput(BaseModel): Add two integers. a: int Field(..., descriptionFirst integer) b: int Field(..., descriptionSecond integer) # 定义工具 tool(args_schemaAddInput) def add(a: int, b: int) - int: # 未提供描述 return a b print(add.invoke({a: 2, b: 3})) print(add.name) print(add.description) print(add.args)2.1.2 依赖 Annotated在 LangChain 中可以依赖 Annotated 和文档字符串传递给工具 Schema。如下所示from langchain_core.tools import import tool from typing_extensions import Annotated tool def add( a: Annotated[int, ..., First integer], b: Annotated[int, ..., Second integer] ) - int: Add two integers. return a b tool def multiply( a: Annotated[int, ..., First integer], b: Annotated[int, ..., Second integer] ) - int: Multiply two integers. return a * b2.2 使用 StructuredTool 创建工具class langchain_core.tools.structured.StructuredTool类用来初始化工具其中from_function类方法通过给定的函数来创建并返回一个工具。from_function类方法定义如下classmethod def from_function( func: Callable | None None, coroutine: Callable[[...], Awaitable[Any]] | None None, name: str | None None, description: str | None None, return_direct: bool False, args_schema: type[BaseModel] | dict[str, Any] | None None, infer_schema: bool True, *, response_format: Literal[content, content_and_artifact] content, parse_docstring: bool False, error_on_invalid_docstring: bool False, **kwargs: Any, ) - StructuredTool关键参数说明func: 要设置的工具函数coroutine: 协程函数要设置的异步工具函数name: 工具名称。默认为函数名称。description: 工具描述。默认为函数文档字符串。args_schema: 工具输入参数的 schema。默认为 None。response_format: 工具响应格式。默认为content。实例1常规用法对于用该类方法创建的工具同样函数名、类型提示和文档字符串也都是传递给工具 Schema 的一部分不可缺失。from langchain_core.tools import StructuredTool def add(a:int,b:int)-int: 两数相加 Args: a:第一个参数 b:第二个参数 return ab addtoolStructuredTool.from_function(funcadd) print(addtool.name) print(addtool.invoke({a: 2, b: 6})) print(addtool.func) print(addtool.description)示例 2加入配置依赖 Pydantic 类同样的让工具函数不提供描述、文档字符串等需要传递给工具 Schema 的内容此时可以使用args_schema参数依赖 Pydantic 类定义并提供工具输入参数的 schema 属性。使用description参数替代文档字符串中对于工具描述的 schema 属性。from langchain_core.tools import StructuredTool from pydantic import BaseModel,Field class AddTool(BaseModel): a:int Field(...,description这是第一个参数); b:int Field(...,description这是第二个参数); def add(a:int,b:int)-int: return ab addtoolStructuredTool.from_function( funcadd, args_schemaAddTool, description这是完成两数相加的工具, nameADD ) print(addtool.name) print(addtool.invoke({a: 2, b: 6})) print(addtool.func) print(addtool.description) print(addtool.args_schema)示例 3加入 response_format 配置如果希望我们的工具区分消息内容content和其他工件artifact让大模型读取 content而一些用来构造 content 的原始数据保存下来若后续有一些记录、分析的步骤就可以派上用场了这就是 artifact。artifact 通常需要使用字典 Dict 或列表 List 保存。接下来举个例子再来理解下。例如我们定义了一个搜索天气的 tool若使用搜索引擎工具询问 “今天的天气如何” 时content可能是“根据最新搜索结果今天北京晴气温在25℃到32℃之间。建议穿短袖衣物。”artifact可能是某搜索引擎 API 返回的完整 JSON 响应其中包含多个搜索结果条目、每个条目的标题、链接、摘要、排名等元数据。如下所示# Artifact 的示例结构 { results: [ { title: 北京天气预报 - 中国天气网, link: https://weather.com.cn/..., snippet: 北京今天白天晴最高气温32℃夜间晴最低气温25℃... }, { title: 北京实时天气 - Weather.com, link: https://www.weather.com/..., snippet: Bejing, China Weather. Mostly sunny. High 32C... } # ...更多结果 ], search_parameters: { ... }, search_information: { ... } }则对于以上原生数据无论我们今后做日志记录、分析或自定义后续的处理都很方便。例如存在以下场景我们不仅仅想要一个总结性的答案还想要具体的链接、来源或多个备选答案。工具的 content 输出不符合你的预期我们想查看原始数据来理解问题出在哪里是工具解析的问题还是 API 本身返回的问题。需要记录每次工具调用的完整原始响应以满足数据分析的要求。……从这里就可以对比出只返回 content 无法做到这些事情。如何做到我们需要在定义工具时指定response_formatcontent_and_artifact参数并确保我们返回一个元组(content, artifact)代码如下from langchain_core.tools import StructuredTool from pydantic import BaseModel, Field from typing import List, Tuple class AddTool(BaseModel): a: int Field(..., description这是第一个参数); b: int Field(..., description这是第二个参数); def add(a: int, b: int) - Tuple[str, List[int]]: nums[a,b] contentf{nums}运算后的结果是{ab} return content, nums addtool StructuredTool.from_function( funcadd, args_schemaAddTool, description这是完成两数相加的工具, nameADD, response_formatcontent_and_artifact ) print(addtool.invoke( { name: ADD, args: {a: 2, b: 6}, type: tool_call, id: 123 } ))需要注意的是在这里调用工具需要模拟大模型调用姿势如下所示。这将返回一个 ToolMessage{ name: ADD, args: {a: 2, b: 6}, type: tool_call, #必填 id: 123 #必填 }其中type与id是必填字段typedef表示这次调用是一次工具调用而id则负责将工具调用请求与工具调用结果关联。如果我们直接使用工具参数调用工具将只返回输出的 content 部分print(addtool.invoke({a:2,b:8}))三、工具的绑定与调用3.1 工具的绑定为了实际将这些工具绑定到聊天模型可以使用聊天模型的.bind_tools()方法。如下所示from langchain_openai import ChatOpenAI # 定义大模型 model ChatOpenAI(modelgpt-4o-mini) ... # 绑定工具返回一个 Runnable 实例 tools [add, multiply] model_with_tools model.bind_tools(tools)bind_tools () 方法定义def bind_tools( tools: Sequence[dict[str, Any] | type | Callable | BaseTool], *, tool_choice: dict | str | Literal[auto, none, required, any] | bool | None None, strict: bool | None None, parallel_tool_calls: bool | None None, **kwargs: Any, ) - Runnable[PromptValue | str | Sequence[BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]], BaseMessage]请求参数tools绑定到此聊天模型的工具定义列表。支持的类型为字典、pydantic.BaseModel 类、Python 函数和 BaseTool如tool装饰器创建的类。tool_choice默认空要求模型调用哪个工具。可以设置为形式为tool_name的 str调用tool_name工具。auto自动选择工具包括无工具。none不调用工具。any或required或True强制调用至少一个工具。False或None无效果默认 OpenAI 的行为。strict默认空如果为True则保证模型输出与工具定义中提供的 JSON Schema 完全匹配。输入也将根据提供的 Schema 进行验证。如果为False则不会验证输入也不会验证模型输出。如果为None则不会将 strict 参数传递给模型。parallel_tool_calls默认为None允许并行工具使用。设置为False以禁用并行工具。kwargs(Any)任何附加参数都直接传递给bind()。返回值返回一个Runnable实例。3.2 工具的调用通过.bind_tools()方法我们可知它返回了一个 Runnable 实例因此我们可以使用该 Runnable 实例调用.invoke()方法完成工具调用。示例如下from langchain_core.tools import StructuredTool from pydantic import BaseModel, Field from typing import List, Tuple class AddTool(BaseModel): a: int Field(..., description这是第一个参数); b: int Field(..., description这是第二个参数); class MulTool(BaseModel): a: int Field(..., description这是第一个参数); b: int Field(..., description这是第二个参数); def add(a: int, b: int) - Tuple[str, List[int]]: nums[a,b] contentf{nums}运算后的结果是{ab} return content, nums def Mul(a: int, b: int) - Tuple[str, List[int]]: nums[a,b] contentf{nums}运算后的结果是{a*b} return content, nums addtool StructuredTool.from_function( funcadd, args_schemaAddTool, description这是完成两数相加的工具, nameADD, response_formatcontent_and_artifact ) Multool StructuredTool.from_function( funcMul, args_schemaMulTool, description这是完成两数相乘的工具, nameMul, response_formatcontent_and_artifact ) model ChatDeepSeek( modeldeepseek-chat, # 或 deepseek-reasoner ) tools[addtool,Multool] model_with_toolsmodel.bind_tools(toolstools) print(model_with_tools.invoke(23等于多少))这里我们定义了两个工具addtool与Multool分别完成了两数相加与两数相乘的运算并将两个工具绑定到了model聊天模型并返回了一个Runable实例我们用model_with_tools来接受。接着我们传递给大模型一个问题“23等于多少”并运行model_with_tools实例我们就会得到这样一个运行结果AIMessagecontent additional_kwargs{refusal: None} response_metadata{token_usage: {completion_tokens: 58, prompt_tokens: 380, total_tokens: 438, completion_tokens_details: None, prompt_tokens_details: {audio_tokens: None, cache_write_tokens: None, cached_tokens: 0}, prompt_cache_hit_tokens: 0, prompt_cache_miss_tokens: 380}, model_provider: deepseek, model_name: deepseek-v4-flash, system_fingerprint: a26a7955944dc5c60445bff77fac9c8e, id: 19475149-fa40-403b-a052-872d0b48479f, finish_reason: tool_calls, logprobs: None} idlc_run--01a033f1-7102-7d90-8bc1-9506449bd099-0 tool_calls[{name: ADD, args: {a: 2, b: 3}, id: call_00_MIN1pIWaDzy9f0rLoxtY3928, type: tool_call}] invalid_tool_calls[] usage_metadata{input_tokens: 380, output_tokens: 58, total_tokens: 438, input_token_details: {cache_read: 0}, output_token_details: {}}AIMessage来自 AI 的消息。从聊天模型返回作为对提示输入的响应。content消息的内容。additional_kwargs与消息关联的其他有效负载数据。对于来自 AI 的消息可能包括模型提供程序编码的工具调用。response_metadata响应元数据。例如响应标头、logprobs、令牌计数、模型名称。其中的tool_calls字段这样描述tool_calls[{name: ADD, args: {a: 2, b: 3}, id: call_00_MIN1pIWaDzy9f0rLoxtY3928, type: tool_call}]这表明大模型准确识别到了我们提出的计算问题并在AIMessage中添加了一个tool_calls属性此属性包括执行该工具所需的一切包括工具名称和输入参数。其中name表示大模型也需要调用的工具名称args则表示需要传递给工具的参数。同样的如果我们换一个问题让模型计算两个数字的乘积的时候返回的tool_calls属性就会又有所不同print(model_with_tools.invoke(2*3等于多少))tool_calls[{name: Mul, args: {a: 2, b: 3}, id: call_00_HH8WqZWJIcn6ftg2r7xL0599, type: tool_call}]同样的如果我们提出的问题模型进行识别之后发现没有可以使用的自定义工具来解决时tool_calls属性则不会出现相应的工具调用元素print(model_with_tools.invoke(你在干嘛))tool_calls[]但是到这里我们发现当我们绑定工具并调用返回的model_with_tools实例时大模型并没有完全地解决问题而是告诉我们调用工具的姿态与方法并添加到tool_calls属性之中。此时我们需要完成的是利用tool_calls中调用工具必须的字段自己完成工具的调用首先我们需要获取到返回的AIMessage的tool_calls属性然后取出第0个元素也就是{name: Mul, args: {a: 2, b: 3}, id: call_00_HH8WqZWJIcn6ftg2r7xL0599, type: tool_call}其中name字段则表示应该调用哪一个工具我们可以利用字典进行匹配如果是Mul则说明要进行乘法运算返回Multool如果是ADD则说明要进行加法运算返回addtool。tool_callai_mess.tool_calls[0] #tool_call:{name: ADD, args: {a: 2, b: 3}, id: call_00_MIN1pIWaDzy9f0rLoxtY3928, type: tool_call} select_tool{ADD:addtool,Mul:Multool}[tool_call[name]] #select_tool:addtool print(select_tool.invoke(tool_call).content)然后我们将获取到的tool_call也就是一整个工具调用字段传递给工具就能获得到最终的ToolMessage了。for循环处理多条tool_calls这里我们只是实现了一条AIMessage的处理如果用户的问题是多个呢。比如同时让model_with_tools处理两条运算请求分别是结算2和3的和与乘积ai_messmodel_with_tools.invoke(23的结果是多少2*3的结果是多少) print(ai_mess)此时打印出来的ai_mess的tool_calls属性会有两个成员一个用来调用加法运算工具另一个用来调用乘法运算工具tool_calls[{name: ADD, args: {a: 2, b: 3}, id: call_00_Z9CSujEDRr7TRwJn0IWL1528, type: tool_call}, {name: Mul, args: {a: 2, b: 3}, id: call_01_o4NIUTWQIKbgFhEiRLDO4321, type: tool_call}]那么我们就可以使用for循环的方式逐条获取tool_calls列表中的工具调用条目并挨个比对匹配其中的name字段最后完成工具的调用ai_messmodel_with_tools.invoke(23的结果是多少2*3的结果是多少) for tool_call in ai_mess.tool_calls: select_tool{ADD:addtool,Mul:Multool}[tool_call[name]] print(select_tool.invoke(tool_call).content)但是这里有个问题用户提问23 的结果是多少2*3 的结果是多少我们期望模型最终输出23 的结果是 52*3 的结果是 6 这类贴合用户原始问题的自然语言回答。模型识别出需要计算生成 tool_calls 发起工具调用工具执行完成后返回 toolmessage。但工具返回的仅仅是原始运算数值格式和内容并不直接匹配用户需要的自然语言答案。这时必须把工具返回的 toolmessage 再次送入大模型做二次整理。但仅仅把 toolmessage 丢给大模型是不够的底层 API 调用本身是无状态、没有会话记忆的大模型只拿到一堆计算结果丢失了用户最初的原始问题上下文不知道这组数字要对应回答什么问题也就无法生成贴合提问场景、通顺合理的最终回答。要理解这段话我们可以举一个例子我们可以创建一个聊天模型并告诉大模型自己的名字然后再次提问让其说出我们的名字此时我们就能发现大模型底层 API 调用是无状态、没有会话记忆的。所以结果就是大模型不知道我们是谁model ChatDeepSeek( modeldeepseek-chat, # 或 deepseek-reasoner ) print(model.invoke(我叫王小明)) print(model.invoke(我叫什么))所以完整链路必须携带完整上下文用户原始 query 模型生成的 tool_call 工具返回 toolmessage三者一起送入大模型模型才能把工具原始输出映射回用户问题组装成符合预期的自然语言回复。代码实现如下message[ HumanMessage(23的结果是多少2*3的结果是多少) ] ai_messmodel_with_tools.invoke(message) message.append(ai_mess) for tool_call in ai_mess.tool_calls: select_tool{ADD:addtool,Mul:Multool}[tool_call[name]] tool_messselect_tool.invoke(tool_call) message.append(tool_mess) print(message) print(model.invoke(message).content)message最终内容[HumanMessage(content23的结果是多少2*3的结果是多少, additional_kwargs{}, response_metadata{}), AIMessage(content, additional_kwargs{refusal: None}, response_metadata{token_usage: {completion_tokens: 103, prompt_tokens: 386, total_tokens: 489, completion_tokens_details: None, prompt_tokens_details: {audio_tokens: None, cache_write_tokens: None, cached_tokens: 384}, prompt_cache_hit_tokens: 384, prompt_cache_miss_tokens: 2}, model_provider: deepseek, model_name: deepseek-v4-flash, system_fingerprint: a26a7955944dc5c60445bff77fac9c8e, id: 3abda4b4-4a79-4c3f-9349-7a2ae106050a, finish_reason: tool_calls, logprobs: None}, idlc_run--01a03726-86f3-79c1-89d4-9ca28a213eb9-0, tool_calls[{name: ADD, args: {a: 2, b: 3}, id: call_00_ckywMhghy01Wqmqxt1pR8268, type: tool_call}, {name: Mul, args: {a: 2, b: 3}, id: call_01_GB8yZFAjELboC1F8vfhS2664, type: tool_call}], invalid_tool_calls[], usage_metadata{input_tokens: 386, output_tokens: 103, total_tokens: 489, input_token_details: {cache_read: 384}, output_token_details: {}}), ToolMessage(content[2, 3]运算后的结果是5, nameADD, tool_call_idcall_00_ckywMhghy01Wqmqxt1pR8268, artifact[2, 3]), ToolMessage(content[2, 3]运算后的结果是6, nameMul, tool_call_idcall_01_GB8yZFAjELboC1F8vfhS2664, artifact[2, 3])]运行结果