5.tools

1. 什么是 Tools?为什么要使用它?

大模型本质上是一个被"封印"在训练数据里的推理大脑,它没有联网能力,也无法直接操作你的本地数据库或 API。 引入 Tools 的目的,就是给大模型装上"手和眼"。当用户提出需求时,大模型可以通过调用预先定义好的工具代码,去获取外部事实或执行真实动作。


2. 大模型调用工具的过程 (Message Flow)

在架构层面,大模型并不具备代码执行环境,其核心职责为"规划与决策"。完整的工具调用是一个标准的多轮对话流转闭环(Message Flow):

  1. 用户请求 (HumanMessage):用户向系统发起自然语言查询(例:"北京今天天气如何?")。
  2. 模型决策与指令下发 (AIMessage 携带 tool_calls):模型经推理判断需要借助外部工具获取信息,生成结构化的工具调用指令(如:调用 get_weather 函数,入参 city="北京")。此时模型进入挂起等待状态。
  3. 应用层拦截与执行 (ToolMessage):业务应用框架(如 LangChain)解析并拦截该指令,在服务端运行环境(Server/Runtime)中执行对应的 Python 实体函数。函数执行完毕后,将其返回值封装为 ToolMessage 追加至对话上下文中。
    • 关键约束:生成的 ToolMessage 必须严格携带模型下发指令时的 tool_call_id,以确保在并发调用场景下,执行结果与原始请求能够精确映射。
  4. 模型推理与总结 (AIMessage):大模型接收到包含执行结果的对话上下文后,恢复推理过程,结合获取到的事实数据,最终生成自然语言回复返回给终端用户。

代码级推演 (Message 流转示例):

python 复制代码
from langchain.messages import HumanMessage
from langchain_core.tools import tool

# 定义工具并与模型绑定
@tool
def get_weather(city: str):
    """获取天气的工具"""
    return f"{city}天气晴朗~"

model_with_tools = model.bind_tools([get_weather])

# 1. 用户请求
messages = [HumanMessage("今天北京天气如何")]

# 2. 模型决策并下发调用指令 (此时返回的 response 是 AIMessage,包含 tool_calls)
response = model_with_tools.invoke(messages)
messages.append(response) # 核心步骤:必须把"模型要调工具"这一想法也记录进上下文

tool_calls = response.tool_calls
if tool_calls:
    for tool_call in tool_calls:
        if tool_call["name"] == "get_weather":
            # 3. 应用层拦截并在本地执行代码
            # (注:直接传入 tool_call 字典,底层会自动执行函数并将返回值、tool_call_id 封装为 ToolMessage)
            tool_response = get_weather.invoke(tool_call) 
            messages.append(tool_response) # 将包含真实结果的 ToolMessage 追加进上下文
            
# 4. 模型基于完整的上下文(问题 -> 决策 -> 事实结果)进行最后推理
final_response = model_with_tools.invoke(messages)
print(final_response.content) # 输出最终回答:北京天气晴朗~

2.1 进阶:多工具场景下的优雅调用方式 (工具路由)

在实际开发中,当我们的 Agent 拥有数十个工具时,如果依然使用 if tool_call["name"] == "xxx": 来逐一判断,会导致代码极度臃肿且难以扩展。 为了解决这个问题,我们可以采用 工具字典映射 (Tools Map) 的设计模式来实现自动化的动态分发(工具路由)。针对不同的工具定义方式,有两种优雅的写法:

写法一:针对使用 @tool 包装的工具 (自动封装)

如果你的工具使用了 @tool 装饰器,LangChain 的 invoke 方法会自动处理参数解包,并直接返回标准的 ToolMessage

python 复制代码
# 假设我们有多个工具
tools = [get_weather, search_web, get_time]

# 1. 启动时构建映射字典 (key为工具名,value为工具对象)
tools_map = {tool.name: tool for tool in tools}

# ... 获取 response 后遍历 tool_calls ...
for tool_call in response.tool_calls:
    tool_name = tool_call["name"]
    if tool_name in tools_map:
        # 2. 动态取出对应的工具并直接 invoke,彻底消灭 if-else 嵌套
        selected_tool = tools_map[tool_name]
        tool_response = selected_tool.invoke(tool_call) 
        messages.append(tool_response)

写法二:针对未使用 @tool 的原生 Python 函数 (手动封装)

如果你绑定的是纯原生 Python 函数,由于它们没有被 LangChain 包装,它们无法直接接收 tool_call 字典对象,返回值也不会自动变成 ToolMessage。我们需要手动解包并组装消息:

python 复制代码
# 假设这些都是纯原生 Python 函数,没有加 @tool
raw_tools = [get_weather, search_web, get_time]

# 1. 构建映射字典 (注意这里取的是原生的 func.__name__)
tools_map = {func.__name__: func for func in raw_tools}

for tool_call in response.tool_calls:
    tool_name = tool_call["name"]
    if tool_name in tools_map:
        selected_func = tools_map[tool_name]
        
        # 2. 手动解包 args 并传入原生函数执行
        raw_result = selected_func(**tool_call["args"]) 
        
        # 3. 极其重要:必须手动组装 ToolMessage,并将 call_id 严丝合缝地绑定进去!
        tool_response = ToolMessage(
            content=str(raw_result),       # 函数返回的真实结果
            name=tool_call["name"],        # 被调用的工具名
            tool_call_id=tool_call["id"]   # 大模型下发的唯一 call_id
        )
        messages.append(tool_response)

写法三:使用 LangGraph 的 ToolNode (终极开箱即用方案)

随着生态的发展,官方在 LangGraph 库中提供了一个终极杀器 ------ ToolNode。如果你不想手写字典映射和 for 循环,你可以直接把工具列表丢给它,它会自动在底层完成所有的解析、调用、和 ToolMessage 封装!

python 复制代码
from langgraph.prebuilt import ToolNode

# 假设我们有多个工具
tools = [get_weather, search_web, get_time]

# 1. 极其简单:直接用 ToolNode 包装整个工具列表
tool_node = ToolNode(tools)

# 2. 当模型返回了包含 tool_calls 的 response 时,直接将其装在消息列表里扔给 tool_node
# ToolNode 会自动提取内部的 tool_calls,去对应的工具里执行,并返回封装好的一批 ToolMessage!
tool_node_response = tool_node.invoke({"messages": [response]})

# 3. 将自动执行产生的所有 ToolMessage 追加到对话历史中
messages.extend(tool_node_response["messages"])

3. 工具的入参校验 (Parameter Validation)

虽然大模型可以调用外部工具,但大模型的本质是基于概率的"文字接龙",充满不确定性。在真实的业务落地中(如向数据库写入数据),大模型在调用工具时极有可能产生幻觉、传错参数类型、甚至凭空捏造不存在的参数 。 因此,为了保证执行的确定性和系统的安全性,我们必须对工具的"入参"进行严格管控。在 LangChain 中,入参校验可以分为三种方式:通过 Docstring (弱校验)、通过 Pydantic (强校验) 以及通过手写 JSON Schema (弱校验,极不推荐)。

💡 核心认知:这三种方式到底有什么本质区别?

  • 去程(发送给大模型)完全一样 :无论你用上述哪种方式,LangChain 在底层最终都会把它们统统翻译成 JSON Schema 格式发给大模型。因为大模型的 API 只认识 JSON Schema,这是唯一的通信规范。它们在"去程"都起到了对大模型的约束和提示作用。
  • 回程(收到大模型生成的参数后)才是真正的差距所在
    • Docstring / 手写 JSON Schema(弱校验):大模型把生成的 JSON 参数发回来后,框架直接原封不动地放行了。没有任何强制的验证和拦截,哪怕大模型传错了数据类型或漏了字段,脏数据也会直接冲进你的业务代码导致崩溃。
    • Pydantic(强校验) :大模型返回参数后,Pydantic 会充当铁面无私的"海关/质检员",严格校验字段是否完全合规。如果不合规,立刻在这里抛出异常进行硬拦截!并且,如果此时你是交由 Agent(智能体)来调用的,Agent 还会拿着这个 Pydantic 抛出的报错信息,自动要求大模型重试修正!

3.1 通过 Docstring 自动提取 (弱校验)

什么是弱校验? 大模型仅靠阅读代码里的类型提示和注释来"自觉"遵守传参规则。如果在执行时它依然传错了参数(比如该传数字传了字符串),程序不会在进入底层函数前进行友好的强制拦截,通常会导致业务代码抛出意外的运行时崩溃。

底层本质 :由于大模型底层只认 JSON Schema 格式,因此"通过 Docstring 提取"的本质,其实是 LangChain 框架在底层自动帮你把 Python 注释和类型提示转换成了 JSON Schema。这与后文提到的"手写 JSON Schema"在发给大模型的最终效果上是完全一致的,只不过这里是由框架全自动代劳了。

在弱校验下,定义工具有两种方式:

方式一:使用 @tool 装饰器

  • 机制 :将原函数包装为 StructuredTool,自动提取注释作为说明书。
  • 本地调用 :必须通过 .invoke({"city": "北京"}) 以字典形式传参。
python 复制代码
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """获取指定城市的天气"""
    return f"{city}今天晴天"

方式二:不使用 @tool 装饰器

  • 机制:保留原生 Python 函数。绑定时底层自动解析(必须有类型提示)。
  • 本地调用 :保持原生传参 get_weather("北京"),不会破坏旧代码调用。
python 复制代码
def get_weather(city: str) -> str:
    """获取指定城市的天气"""
    return f"{city}今天晴天"

🚨 核心避坑:两者的"本地调用"存在巨大差异!

无论你选择哪种方式,发给大模型的 Schema 是基本一样的,但在你本地用 Python 直接调用(或写单元测试)时,两者的传参方式有天壤之别,这里是新手的重灾区!

  1. 使用了 @tool 时 (包含后文的 Pydantic 强校验) :你的函数已经被 LangChain 挟持并包装成了 StructuredTool 对象。
    • ❌ 错误写法:get_weather("北京") (按原函数习惯传参会直接抛出 TypeError 崩溃!)
    • ✅ 正确写法:必须写成 get_weather.invoke({"city": "北京"}),即必须把参数包成字典,且必须用 .invoke() 方法触发
  2. 不使用 @tool :它依然是一个纯粹的原生 Python 函数。
    • ✅ 正确写法:get_weather("北京") (一切照旧,完全兼容你原本的业务系统直接调用)。

💡 补充技巧:如何查看底层自动生成的 JSON Schema?

前面提到,无论哪种方式,底层都会将其翻译成 JSON Schema。如果你想在本地亲眼看看这个"说明书"长什么样,可以使用以下方法:

python 复制代码
# 方法 1: 直接查看 @tool 包装后的参数 Schema
print(get_weather.args_schema.schema())

# 方法 2: 使用 LangChain 的底层工具,查看最终发给 OpenAI 的完整格式
from langchain_core.utils.function_calling import convert_to_openai_tool
import json

openai_tool_schema = convert_to_openai_tool(get_weather)
print(json.dumps(openai_tool_schema, indent=2, ensure_ascii=False))

💡 决策总结:何时使用 @tool,何时不使用?

  1. 全新开发的大模型专属工具 ➡️ 必须使用 @tool 。这能统一 LangChain 的组件规范,并且最关键的是,只有加了 @tool 装饰器,你才能接入后面要讲的 Pydantic args_schema 进行强校验
  2. 直接复用已有的旧业务代码 ➡️ 不使用 @tool 。直接把它喂给 bind_tools 即可。因为如果你给一个旧函数强行戴上 @tool 的帽子,它在本地就被转成了 StructuredTool,别的业务代码必须改用字典 .invoke({}) 传参,这会导致你原本正常的业务系统大面积报错瘫痪。

3.2 通过 Pydantic 定义 args_schema (强校验)

企业级应用开发的核心法则 是引入强校验:Docstring 专职负责工具的宏观功能描述,Pydantic 专职负责微观参数的强校验。(99% 的正规商业级场景都必须这么写)

为什么必须用 Pydantic 强校验?

  1. 前置安检拦截 (Hook):在原函数执行前加一层外壳,利用 Pydantic (底层为极速的 Rust 引擎) 严格审查大模型传来的参数类型和必填项。一旦出错,硬性拦截,绝不让脏数据流进你的底层真实业务代码。
  2. 自纠错重试闭环 (Retry Loop) ⚠️ 注意调用方式差异
    • 何时【不会】自动重试 :如果你是**"手工直调模型"**(例如直接调用 model.invoke(),然后自己写代码去执行 tool.invoke()),当遇到 Pydantic 拦截报错时,程序会直接抛出异常并终止,不会自动重试。
    • 何时【会】自动重试 :只有当你把模型和工具交给**"智能体管家"(如 AgentExecutor)**接管时,重试机制才会生效。Agent 底层自带了异常捕获的循环逻辑,它会捕获 Pydantic 抛出的精准异常信息,将其作为"执行失败"的反馈重新发送给大模型,大模型据此进行"自我反思"并修正参数,实现全自动纠错。
  3. 处理复杂嵌套:面对极其复杂的结构化输出需求,Pydantic 的面向对象类定义比手捏 JSON Schema 更加清晰、易维护且支持多处复用。

强校验优雅范例

python 复制代码
from pydantic import BaseModel, Field
from langchain_core.tools import tool

# 1. Pydantic 专职负责入参的强定义与强校验
class WeatherInput(BaseModel):
    city: str = Field(description="具体的城市名称,如:北京")
    unit: str = Field(description="温度单位", default="celsius")

# 2. Docstring 只管宏观介绍,args_schema 接入 Pydantic 安检门
@tool(args_schema=WeatherInput)
def get_weather(city: str, unit: str = "celsius"):
    """
    根据城市名称获取当地的天气情况。
    """
    return f"{city}的天气是晴天,温度单位:{unit}"

配合 Agent 实现自动重试的代码范例

为了让上述 Pydantic 的强校验真正发挥"自动重试"的威力,必须搭配 AgentExecutor 使用,而非纯手工调用 model.invoke()

python 复制代码
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate

# 1. 准备工具列表
tools = [get_weather]

# 2. 定义提示词模板 (注意必须包含 agent_scratchpad 用来暂存工具调用的报错信息)
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个有用的助手,请利用工具回答用户问题。"),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"), 
])

# 3. 创建智能体
agent = create_tool_calling_agent(model, tools, prompt)

# 4. 创建智能体执行器 (也就是那个帮我们写好了 while 循环和 try...except 的"管家")
# max_iterations = 3 表示最多允许大模型犯错重试 3 次
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, max_iterations=3)

# 5. 极简调用。遇到 Pydantic 报错时,管家会自动拦截并把错误信息塞回给大模型让其修正重做
response = agent_executor.invoke({"input": "今天北京天气如何"})
print(response["output"])

3.3 通过手写 JSON Schema (弱校验,极不推荐)

除了使用 Pydantic,你也可以选择直接手写标准的 JSON Schema 字典,并把它传给 @tool(args_schema=...) 或者在底层的 API 调用中直接指定。

机制:大模型底层 API(如 OpenAI)原生接收的就是 JSON Schema 格式的参数描述说明书。Pydantic 的底层机制,其实也是在运行时把类结构自动转换成了 JSON Schema 发给大模型。因此,手写 JSON Schema 相当于"越过 Pydantic 这个中间商,直接用底层协议对话"。

为什么在实战中极不推荐?

  1. 地狱级的编写与维护体验:手写深层嵌套的字典结构非常痛苦、极易少写括号、拼错字段名,且在 IDE 中毫无自动补全和代码重构支持。
  2. 致命缺陷------缺乏运行时强拦截:手写的 JSON Schema 仅仅是一份发给大模型的"静态说明书",它只能在生成前尽量规范大模型。但如果大模型依然产生了幻觉,传回了错乱的参数,由于缺少类似 Pydantic 这样真正的验证引擎把关,脏数据会直接长驱直入你的底层业务函数,导致意想不到的运行时崩溃。

(结论:在工程实战中,永远优先使用 3.2 的 Pydantic。把 JSON Schema 仅当作了解底层通信原理的知识储备即可,千万不要手写它。)


4. 核心控制开关:tool_choice 的使用

当我们将工具绑定给大模型后,可以通过 tool_choice 参数来强制管控大模型的工具使用行为:

  • tool_choice="auto" (默认状态:自由发挥)
    • 含义:工具交给你,用不用你自己根据用户问题智能判断。
    • 场景:99% 的日常通用智能体开发。
  • tool_choice="none" (强制禁用)
    • 含义:严禁调用任何工具,哪怕你觉得需要用,也只能用纯文本回答。
    • 场景:临时限制大模型权限,只让其做文本总结或闲聊,防止误触敏感操作。
  • tool_choice="required" / "any" (强制必用)
    • 含义:不管用户问什么,你今天必须给我挑一个工具调用,绝不准只回纯文本!
    • 场景:定向的 API 路由节点或信息提取器,强迫大模型输出结构化的工具调用参数。
python 复制代码
# 强制大模型必须调用工具
model_with_tools = model.bind_tools([get_weather], tool_choice="required")
相关推荐
要开心吖ZSH2 小时前
AI医疗分诊与健康咨询助手agent开发——(4-2)从零到一:我给AI医疗分诊助手加了个“知识库大脑“,终于搞懂了RAG是什么
java·docker·agent·医疗·rag
smartfish_liu5 小时前
LLM-笨笨的agent 闭环沙盒任务测试: 把{private}/LLM/story/I_am_back.md发表到博客网
agent
Emerson_20265 小时前
通用 AI Agent 开发步骤
人工智能·agent·agent开发步骤
JouYY6 小时前
大模型底层学习(一)-Transformer 架构总览
架构·llm·agent
迷糊小羊6 小时前
给 OpenCode 上的 Spec-Kit 加一层"上下文隔离":从踩坑到 subagent 改造
agent
武子康7 小时前
OpenAI 的首款硬件不是“音箱“问题:无屏 AI 伴侣成立的十二个条件(2026)
人工智能·openai·agent
小林ixn8 小时前
从零实现AI流式对话:一篇搞懂SSE、ReadableStream与JSON截断处理
人工智能·agent·交互设计
我要割麦子8 小时前
从零到一手撸 Agent 系列 — 第 1 篇:一个 Coding Agent 是什么?
agent
名侦探7228 小时前
Agent 的工作原理与理解方式
agent