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 ==================================
抱歉,我无法提供实时的天气信息。不过,你可以通过以下方式获取北京今天的天气情况:
**使用天气应用程序**:如"墨迹天气"、"天气通"等。
**访问天气网站**:如中国天气网(www.weather.com.cn)或中央气象台官网。
**语音助手**:如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 支撑高并发的底层基石。