langchain的Tools教程(三)

5、拓展:强制使用工具

5.1 tool_choice参数说明

bind\_tools 可以传递参数 tool\_choice,用于控制是否强制使用工具。

该字段最终会作为 payload 的 tool\_choice 字段传递给模型,OpenAI和Deepseek的官方API服务对于 tool\_choice 的取值做了相同的规定。

none:模型不会调用任何工具。

auto:默认值,模型可以自主决定不调用或调用任意数量的工具。

required:模型必须调用工具,数量不限。

此外,tool_choice 还支持传递 any,等价于 required。

5.2 none值举例

模型初始化:

python 复制代码
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os

from traitlets.utils.descriptions import describe

# 从.env文件中加载环境变量
load_dotenv(override=True)
QWEN_API_KEY = os.getenv("QWEN_API_KEY")
QWEN_BASE_URL = os.getenv("QWEN_BASE_URL")
model = init_chat_model(
    model="qwen-vl-plus",
    model_provider="openai",
    api_key=QWEN_API_KEY,
    base_url=QWEN_BASE_URL,
)

举例:

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

@tool(parse_docstring=True)
def get_weather(city: str) -> str:
    """
    获取当日天气

    Args:
        city: 城市名称
    """
    return f'{city}当天晴朗'
model_with_tools = model.bind_tools([get_weather], tool_choice="none")
messages = [
HumanMessage("今天北京天气如何?别瞎编")
]
response = model_with_tools.invoke(messages)
response.pretty_print()

用户要求查询天气,并提供了天气查询工具,但模型不会调用。

输出:

================================== Ai Message ==================================

抱歉,我无法提供实时的天气信息。不过,你可以通过以下方式获取北京今天的天气情况:

  1. **使用天气应用程序**:如"墨迹天气"、"天气通"等。

  2. **访问天气网站**:如中国天气网(www.weather.com.cn)或中央气象台官网。

  3. **语音助手**:如Siri、小爱同学、天猫精灵等,直接询问"北京今天天气如何"。

希望这些方法能帮到你!

5.3 auto值举例

tool_choice 的默认值就是 auto。

举例1:需要调用工具的场景

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

@tool(parse_docstring=True)
def get_weather(city: str) -> str:
    """
    获取当日天气

    Args:
    city: 城市名称
    """
    return f'{city}当天晴朗'

model_with_tools = model.bind_tools([get_weather], tool_choice="auto")

messages = [
    HumanMessage("今天杭州天气如何?别瞎编")
]

response = model_with_tools.invoke(messages)
response.pretty_print()

输出:

================================== Ai Message ==================================
Tool Calls:
get_weather (call_b153f3a9fca6480e977b4d)
Call ID: call_b153f3a9fca6480e977b4d
Args:
city: 杭州

举例2:不需要调用工具的场景

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

@tool(parse_docstring=True)
def get_weather(city: str) -> str:
    """
    获取当日天气

    Args:
    city: 城市名称
    """
    return f'{city}当天晴朗'
model_with_tools = model.bind_tools([get_weather], tool_choice="auto")
messages = [
HumanMessage("你好啊")
]
response = model_with_tools.invoke(messages)
response.pretty_print()

输出:

================================== Ai Message ==================================

你好!有什么我可以帮你的吗?

5.4 required值举例

举例1:需要调用工具的场景

python 复制代码
from langchain.tools import tool
from langchain.messages import HumanMessage
@tool(parse_docstring=True)
def get_weather(city: str) -> str:
    """
    获取当日天气

    Args:
    city: 城市名称
    """
    return f'{city}当天晴朗'
model_with_tools = model.bind_tools([get_weather], tool_choice="required")
messages = [
HumanMessage("今天杭州天气如何?别瞎编")
]
response = model_with_tools.invoke(messages)
response.pretty_print()

输出:

================================== Ai Message ==================================

Tool Calls:

get_weather (call_4cae305bfc3345e1a13161)

Call ID: call_4cae305bfc3345e1a13161

Args:

city: 杭州

举例2:不需要调用工具的场景

注意这里的模型我切换为了deepseek,因为千问不支持tool_choice="required"

python 复制代码
import os
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv


# 如果系统环境配置了DEEPSEEK_API_KEY和DEEPSEEK_API_SECRET,则优先使用环境变量
# 但是写了override=True,则优先使用.env文件中的配置
load_dotenv(override=True)

DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL")

model = ChatOpenAI(model="deepseek-chat", api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)

from langchain.tools import tool
from langchain.messages import HumanMessage
@tool(parse_docstring=True)
def get_weather(city: str) -> str:
    """
    获取当日天气

    Args:
    city: 城市名称
    """
    return f'{city}当天晴朗'
model_with_tools = model.bind_tools([get_weather], tool_choice="required")
messages = [
HumanMessage("你好啊")
]
response = model_with_tools.invoke(messages)
response.pretty_print()

即便此时不需要,模型依然会调用工具

输出:

================================== Ai Message ==================================

Tool Calls:

get_weather (call_00_W5MO0XdQPhNkTXVyOJwR9365)

Call ID: call_00_W5MO0XdQPhNkTXVyOJwR9365

Args:

city: 北京

此外,any 行为和 required 一致。举例:略

5.5 强制调用特定的工具

某些场景下我们希望调用特定的工具,仍然可以用 tool_choice 解决。

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

@tool(parse_docstring=True)
def get_weather1(city: str) -> str:
    """
    获取当日天气

    Args:
    city: 城市名称
    """
    return f'{city}当天晴朗'
@tool(parse_docstring=True)
def get_weather2(city: str) -> str:
    """
    获取当日天气

    Args:
    city: 城市名称
    """
    return f'{city}当天晴朗'
model_with_tools = model.bind_tools([get_weather1, get_weather2],
tool_choice="get_weather2")
messages = [
HumanMessage("杭州今天天气如何?")
]
response = model_with_tools.invoke(messages)
response.pretty_print()

输出:

================================== Ai Message ==================================

Tool Calls:

get_weather2 (call_00_mM8xQ66IycqYLBKX4wko1669)

Call ID: call_00_mM8xQ66IycqYLBKX4wko1669

Args:

city: 杭州

6、实践经验总结

6.1 清晰的描述

python 复制代码
# ☑ 好
@tool(parse_docstring=True)
def search_flights(origin: str, destination: str, date: str) -> str:
    """
    搜索航班信息

    Args:
    origin: 出发城市,如"北京"
    destination: 目的地城市,如"上海"
    date: 出发日期,格式 YYYY-MM-DD

    Returns:
    可用航班的 JSON 列表
    """

6.2 功能单一

python 复制代码
# ✗ 不好:一个工具做太多事
@tool
def do_everything(action: str, data: str) -> str:
python 复制代码
"""做各种事情"""
    if action == "weather": ...
    elif action == "calculate": ...
    elif action == "search": ...

# ☑ 好:每个工具做一件事
@tool
def get_weather(city: str) -> str:
    """获取天气"""
    ...

@tool
def calculator(operation: str, a: float, b: float) -> str:
    """计算"""
    ...

6.3 如何处理工具失败?

三层防护:

第1层:工具内部处理

python 复制代码
@tool
def divide(a: float, b: float) -> str:
    """
    除法计算

    Args:
    a: 被除数
    b: 除数
    """
    try:
        if b == 0:
            return "错误:除数不能为零"
        result = a / b
        return f"{a} / {b} = {result}"
    except Exception as e:
        return f"计算错误:{e}"

第2层:Agent 级重试(使用 prompt)

python 复制代码
agent = create_agent(
    model=model,
    tools=[...],
    prompt="如果工具失败,尝试使用其他方法解决问题。"
)

第3层:调用级重试

在大模型应用(如 LangChain)中,网络请求和外部工具调用是最容易掉链子的地方。@retry 就像是一个容错保险。

python 复制代码
from tenacity import retry, stop_after_attempt
# 1. 配置重试规则:如果失败,最多尝试 3 次(即第 1 次正常调用 + 2 次重试)
@retry(stop=stop_after_attempt(3))
def call_agent(question):
    # 2. 核心业务逻辑:调用 LangChain 的 Agent
    return agent.invoke({"messages": [{"role": "user", "content": question}])

它的工作流程:

① 你调用 call_agent("你好")。

② 程序进入函数,执行 agent.invoke(...)。

③ 如果执行成功:正常返回结果,@retry 什么都不做。

④ 如果执行失败(报错):@retry 会拦截这个错误,不让程序直接崩溃。它会默默地帮你再次触发 agent.invoke(...)。

⑤ 如果连续 3 次都报错:它终于放弃了,把第 3 次的报错真正抛出来,程序此时才会报错中止。

6.4 返回字符串

python 复制代码
# 好:返回字符串
@tool
def get_user_info(user_id: str) -> str:
    """获取用户信息"""
    user = {"id": user_id, "name": "张三"}
    return json.dumps(user, ensure_ascii=False)  # 转成 JSON 字符串

# ✗ 不好:返回字典(某些情况可能有问题)
@tool
def get_user_info(user_id: str) -> dict:
    """获取用户信息"""
    return {"id": user_id, "name": "张三"}

在编写传统的 Python 代码时,返回字典(dict)显然更方便后续代码处理。但在 LangChain 的工具(Tools)生态中,强烈建议工具返回字符串(str)。因为:

1)大模型(LLM)的本质只吃"文本"

2)避免大模型"胡思乱想"(乱码与格式问题)

如果你返回一个包含中文的字典 {"name": "张三"}, LangChain 在强制将其转换为字符串时,默认可能会采用 Unicode 编码,变成 {"name": "\u5f20\u4e09"}。

大模型虽然能理解 Unicode,但极易受到干扰。直接看到中文张三的大模型,和看到 \u5f20\u4e09 的大模型,其输出的稳定性和准确率是有差距的。通过手动 json.dumps(..., ensure_ascii=False),你确保了喂给大模型的是最干净、最直观的纯文本。

json.dumps()是 Python 标准库 json 模块中的函数,用于将 Python 对象(如字典、列表)序列化成一个 JSON 格式的字符串。

ensure_ascii=False:这个参数默认值为 True,表示所有非 ASCII 字符(如中文)会被转换成 \uXXXX形式的转义序列。设置为 False后,中文、表情符号等字符就能在 JSON 字符串中正常显示,而不是一堆乱码。

6.5 选择同步 vs 异步

  • 同步工具:简单场景,CPU密集型任务
  • 异步工具:IO密集型(API调用、数据库、文件操作)
python 复制代码
# 同步
@tool
def sync_tool(x: str) -> str:
    return process(x)

# 异步
@tool
async def async_tool(x: str) -> str:
    return await async_process(x)

以下是对同步异步的讲解(回顾):

7. 扩展知识讲解

7.1 讲解一:同步 vs 异步开发决策(核心速查)

维度 同步 (def) 异步 (async def)
执行模型 普通函数,调用时独占解释器 协程函数,返回 Coroutine,需 await 执行
阻塞行为 阻塞式:全程占用线程,无法处理其他任务 非阻塞式await 时主动让出 CPU,事件循环调度其他任务
返回机制 直接返回结果 返回可等待对象,由事件循环调度执行
适用场景 CPU 密集型:计算 Embedding、文本解析、本地模型推理 IO 密集型:调用 LLM API、向量数据库查询、文件读写
关键特征 函数内await 函数内有 await client.post(...)

选型口诀

计算密集用同步,IO 密集用异步;

FastAPI 中混用时,同步任务用 asyncio.to_thread 隔离。


7.2 讲解二:await 与事件循环调度(深入原理)

1. await 在等什么?

  • 不是等 CPU 时间片 ,而是等 IO 事件完成(网络响应、磁盘读取)。

  • 流程:发起 IO → 挂起协程 → 让出 CPU → 事件循环执行其他任务 → IO 完成唤醒 → 恢复执行。

2. 多任务如何调度?

  • 不是轮询时间戳 (低效忙等),而是 事件循环 + 就绪队列 ,底层由操作系统异步通知(epoll / IOCP)驱动。

  • 调度机制:谁的数据先准备好,谁先恢复执行(按完成顺序,非公平但高效)。

3. 类比:点外卖

你(协程)下单(发起 IO)后,该干嘛干嘛(让出 CPU),外卖到了(IO 完成)手机响铃(唤醒),你再去取餐(继续执行)。多个外卖单由平台(事件循环)统一调度,谁先到先处理谁。

4. 致命警告

异步代码中不要写 while True 纯计算循环(无 await,它会永久霸占 CPU,卡死事件循环,导致所有其他任务无法执行。

一句话总结

await 等的是 IO 中断信号,调度靠操作系统通知机制,不是轮询。这是 FastAPI 支撑高并发的底层基石。

相关推荐
言乐62 小时前
Python实现可运行解密游戏游戏框架
python·游戏·小程序·游戏程序·关卡设计
YUS云生3 小时前
Python学习笔记·第31天:FastAPI入门——路由、路径参数、查询参数与请求体
笔记·python·学习
智写-AI3 小时前
真实有效的免费降英文AI工具服务商
人工智能·python
yuhuofei20214 小时前
【Python入门】了解掌握Python中函数的基本使用
python
白帽小阳5 小时前
2026前端面试题!(附答案及解析)
javascript·网络·python·安全·web安全·网络安全·护网行动
乱写代码5 小时前
Python开发技巧--类型注解Literal
python
卷无止境5 小时前
Python FFI 技术深度解析:ctypes、cffi 与 pybind11 的性能差异与实践挑战
后端·python
郝学胜-神的一滴6 小时前
算法实战:最小k个数——大顶堆的优雅解法
开发语言·数据结构·c++·python·程序人生·算法·排序算法