LangChain-Tools工具调用
上一篇(Function-Calling 详解)讲了工具调用的底层原理——JSON Schema 格式定义工具,LLM 返回工具调用请求,开发者代码执行后把结果放回消息历史。这套机制是正确的,但用起来有点繁琐:需要手写 JSON Schema,需要手动维护消息历史,需要手动分发和执行工具。
LangChain Tools:工具调用实战
上一篇(Function-Calling 详解)讲了工具调用的底层原理——JSON Schema 格式定义工具,LLM 返回工具调用请求,开发者代码执行后把结果放回消息历史。这套机制是正确的,但用起来有点繁琐:需要手写 JSON Schema,需要手动维护消息历史,需要手动分发和执行工具。
LangChain 的 Tools 模块把这些重复劳动封装成了更简洁的 API。核心概念一致,用法省事很多。
1.1 LLM 没有工具会怎样
LangChain Tools 架构——从工具定义到 bind_tools 注册,再到 LLM 调用和结果返回的完整结构
在看代码之前,先把问题说清楚——没有工具的 LLM,具体哪里做不到。
知识截止日期:训练数据有截止时间。最新的新闻、实时的股价、今天的汇率,训练结束后发生的事情,LLM 一概不知。你问它,它要么说不知道,要么给你一个旧数据,要么直接编一个听起来合理的数字。
不能执行代码:让它算 sqrt(2) 它能回答 1.414,因为这是常识级别的内容。但让它算一个复杂的金融模型,或者处理你上传的 Excel 表格——光靠语言推理远远不够,需要真正执行代码。LLM 知道怎么写 Python,但它本身不是 Python 解释器。
不能访问外部系统:查数据库、调内部 API、发邮件、操作文件——LLM 做不到,因为它只是一个文本处理器。它和这些系统之间没有任何连接。
工具调用(Function Calling / Tool Use)就是为了突破这三个边界。LangChain Tools 是这一机制的工程化实现。
1.2 @tool 装饰器:定义工具的最简方式
LangChain 里定义工具,最简洁的方式是 @tool 装饰器(Python 语法,在函数上方加 @xxx 给函数添加额外功能,这里是把普通函数变成 LLM 可以调用的工具)。写一个普通 Python 函数,加上装饰器就成了工具:
import os
import requests
from langchain_core.tools import tool
@tool
def get_exchange_rate(from_currency: str, to_currency: str) -> str:
"""
获取两种货币之间的实时汇率。
适用场景:用户询问汇率、货币兑换计算时调用此工具。
Args:
from_currency: 源货币代码,如 USD、EUR、CNY、JPY
to_currency: 目标货币代码,如 USD、EUR、CNY、JPY
Returns:
汇率信息字符串,格式:1 XXX = Y.YYY ZZZ(日期)
"""
url = f"https://api.exchangerate-api.com/v4/latest/{from_currency}"
try:
response = requests.get(url, timeout=5)
data = response.json()
rate = data["rates"].get(to_currency)
if rate is None:
return f"未找到 {to_currency} 的汇率数据,请检查货币代码是否正确"
return f"1 {from_currency} = {rate} {to_currency}(数据更新时间:{data['date']})"
except requests.Timeout:
return "汇率服务超时,请稍后重试"
except Exception as e:
return f"获取汇率失败:{str(e)}"
docstring 是工具的灵魂。LangChain 会从 docstring 提取工具描述,发给 LLM 作为"使用说明书"。写得越清楚,LLM 判断"什么时候用这个工具、传什么参数"就越准确。
参数说明(Args)、返回值说明(Returns)、使用场景——都要写。这比任何 Prompt 调优都更根本:工具描述写烂了,LLM 再聪明也救不回来。
1.3 工具的三要素
每个工具,LLM 能看到三个信息:
名称:工具的函数名,用下划线命名,清晰表达用途。get_exchange_rate 比 fetch_data 好得多。名字本身就是提示信息。
描述:从 docstring 第一段提取,告诉 LLM 这个工具做什么。
参数 schema:从函数签名和类型注解自动生成,告诉 LLM 调用时该传什么参数。
可以直接查看这三个信息:
print(get_exchange_rate.name)
# get_exchange_rate
print(get_exchange_rate.description)
# 获取两种货币之间的实时汇率。适用场景:用户询问汇率...
print(get_exchange_rate.args)
# {
# 'from_currency': {'title': 'From Currency', 'description': '源货币代码...', 'type': 'string'},
# 'to_currency': {'title': 'To Currency', 'description': '目标货币代码...', 'type': 'string'}
# }
1.4 bind_tools:把工具绑定到模型
定义好工具后,用 bind_tools 告诉模型它可以用哪些工具:
from langchain_openai import ChatOpenAI
# 使用 DeepSeek API(OpenAI 兼容格式)
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 绑定工具:让模型知道它有这些工具可用
llm_with_tools = llm.bind_tools([get_exchange_rate])
# 提问
response = llm_with_tools.invoke("今天美元兑人民币的汇率是多少?")
print(response)
此时模型返回的不是普通的文字回答,而是一个 AIMessage 对象,里面包含 tool_calls——模型在说:"我要调用 get_exchange_rate,参数是 from_currency='USD', to_currency='CNY'。"
模型不直接执行工具,它只是请求调用。实际执行由外部代码完成,这是 LangChain 和 Function Calling 一致的核心设计。
1.5 完整的工具调用循环
整个流程:模型决定调哪个工具 → 外部代码执行工具 → 把结果返回给模型 → 模型生成最终回答。
下面是一个完整可运行的示例:
import os
import json
import requests
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
# 初始化 LLM(使用 DeepSeek API)
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
@tool
def get_exchange_rate(from_currency: str, to_currency: str) -> str:
"""
获取两种货币之间的实时汇率。
Args:
from_currency: 源货币代码,如 USD、EUR、CNY
to_currency: 目标货币代码,如 USD、EUR、CNY
"""
url = f"https://api.exchangerate-api.com/v4/latest/{from_currency}"
try:
response = requests.get(url, timeout=5)
data = response.json()
rate = data["rates"].get(to_currency)
if rate is None:
return f"未找到 {to_currency} 的汇率数据"
return f"1 {from_currency} = {rate} {to_currency}({data['date']})"
except Exception as e:
return f"获取汇率失败:{str(e)}"
@tool
def calculate(expression: str) -> str:
"""
安全地计算数学表达式,返回计算结果。适用于数学计算、单位换算等场景。
Args:
expression: 合法的数学表达式,如 "1 + 2 * 3"、"1000 * 7.31"、"100 / 3"
注意:只支持基本数学运算(+、-、*、/、括号),不执行任何代码。
"""
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return f"不支持的表达式:只允许数字和基本运算符(+、-、*、/、括号)"
try:
result = eval(expression) # 生产环境建议用 ast.literal_eval 或 sympy
return str(result)
except ZeroDivisionError:
return "计算失败:除数不能为零"
except Exception as e:
return f"计算失败:{str(e)}"
# 工具列表和映射
tools = [get_exchange_rate, calculate]
tools_map = {t.name: t for t in tools}
# 绑定工具到模型
llm_with_tools = llm.bind_tools(tools)
def run_agent(user_input: str) -> str:
"""支持多工具、多轮调用的 Agent"""
messages = [HumanMessage(content=user_input)]
print(f"\n用户:{user_input}")
while True:
response = llm_with_tools.invoke(messages)
messages.append(response)
# 没有工具调用 = 模型给出了最终答案
if not response.tool_calls:
print(f"\nAI:{response.content}")
return response.content
# 执行所有工具调用
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
tool_id = tool_call["id"]
print(f"\n[调用工具] {tool_name}({tool_args})")
if tool_name in tools_map:
tool_result = tools_map[tool_name].invoke(tool_args)
else:
tool_result = f"未知工具:{tool_name}"
print(f"[工具结果] {tool_result}")
# 把工具结果加入消息列表
messages.append(ToolMessage(
content=str(tool_result),
tool_call_id=tool_id,
))
# 循环:把工具结果发回给模型,继续对话
# 测试
run_agent("今天美元兑人民币多少?如果我有 1000 美元,能换多少人民币?")
预期输出:
用户:今天美元兑人民币多少?如果我有 1000 美元,能换多少人民币?
[调用工具] get_exchange_rate({'from_currency': 'USD', 'to_currency': 'CNY'})
[工具结果] 1 USD = 7.23 CNY(2024-03-15)
[调用工具] calculate({'expression': '1000 * 7.23'})
[工具结果] 7230.0
AI:今天美元兑人民币汇率为 1:7.23。1000 美元可以换到 7230 元人民币。
运行这段代码,模型先调用 get_exchange_rate 获取实时汇率,再调用 calculate 计算 1000 美元能换多少人民币,最后给出最终回答。整个过程,模型是主控,工具是执行者。
1.6 错误处理:工具执行失败怎么办
网络超时、API 限流、参数非法——工具执行随时可能失败。处理好错误,模型才能优雅应对,而不是程序崩溃。
最直接的做法是在工具里返回有意义的错误信息字符串,让模型知道失败了并能决定下一步:
@tool
def get_stock_price(ticker: str) -> str:
"""
获取股票实时价格。适用于用户询问股价时使用。
Args:
ticker: 股票代码,如 AAPL(苹果)、TSLA(特斯拉)、000001.SZ(平安银行A股)
"""
try:
# 实际代码调用股票 API
# 这里模拟网络故障
raise ConnectionError("股票数据服务暂时不可用")
except ConnectionError as e:
# 返回有意义的错误描述,让模型知道发生了什么
# 不要只返回 "error",要告诉模型具体情况
return f"获取 {ticker} 股价失败:服务暂时不可用。请稍后重试,或建议用户到证券交易所官网查询。"
except ValueError:
return f"股票代码格式错误:{ticker}。美股使用字母代码如 AAPL,A股使用如 000001.SZ 格式。"
模型收到这个错误信息后,通常会在回答里告诉用户"目前无法获取实时股价,服务暂时不可用",而不是胡编一个数字。这比程序直接崩溃要好得多。
错误信息的质量决定模型的恢复能力。返回"error: 500"——模型不知道该说什么。返回"获取股价失败:API 限流,每分钟限制 30 次请求,请 60 秒后重试"——模型可以给用户一个有用的回复。
1.7 内置工具:常见场景不用自己写
LangChain 提供了很多开箱即用的内置工具:
# Tavily 搜索(专为 AI Agent 优化的搜索引擎 API,需要注册获取 key)
# 比直接调用 Google API 更适合 Agent,返回格式更友好
from langchain_community.tools.tavily_search import TavilySearchResults
search_tool = TavilySearchResults(
max_results=3,
api_key=os.getenv("TAVILY_API_KEY")
)
# Wikipedia 查询(适合知识问答场景)
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
wiki_tool = WikipediaQueryRun(
api_wrapper=WikipediaAPIWrapper(lang="zh", top_k_results=2)
)
# Python REPL(执行 Python 代码,请在受控环境使用)
from langchain_experimental.tools import PythonREPLTool
python_tool = PythonREPLTool()
# 这些内置工具和自定义工具完全兼容,直接放进 bind_tools 的列表
llm_with_all_tools = llm.bind_tools([search_tool, wiki_tool, get_exchange_rate])
1.8 安全注意事项
工具赋予了 LLM 执行真实操作的能力,需要认真对待安全问题。
不要给 LLM 危险工具:PythonREPLTool 可以执行任意 Python 代码,ShellTool 可以执行任意 shell 命令(可以删除文件、修改系统配置、访问网络)。在受信任的研究环境里也许可以,生产环境里这是定时炸弹——用户可以通过 Prompt 注入攻击(把恶意指令伪装成普通输入,欺骗模型执行)让模型执行恶意命令。
工具权限最小化:工具应该只能做它该做的事。查数据库的工具只给 SELECT 权限,不给 INSERT 和 DELETE。发邮件的工具只能发给白名单内的地址。发送请求的工具只能访问白名单域名。
写入操作要确认:如果工具涉及修改数据、发送消息、扣款,考虑加一个 Human-in-the-loop 步骤,让用户确认后再执行。这在 LangGraph 里有专门的中断机制,第 13 章会讲到。
对工具返回值保持警惕:工具返回的内容会被模型接收并用于生成回答。如果工具从外部 API 获取数据,那些数据里可能包含恶意指令(Prompt Injection,提示注入攻击)。对工具返回值做基本的清洗,不要完全信任。
1.9 工具数量与质量的权衡
工具调用是 Agent 的核心能力,但工具不是越多越好。
根据实际工程经验,一个 Agent 挂 3-5 个工具效果最佳,超过 10 个工具之后模型选择工具的准确率会明显下降——就像餐厅菜单太多反而不知道点什么。
工具描述质量 > 工具数量:一个描述清晰的工具,比五个描述模糊的工具更有价值。定义工具时,先问自己:如果我只看这个工具的名字和描述,能准确判断什么时候该用它吗?
如果工具很多,应该考虑按职责拆分成多个专责 Agent:一个负责数据查询,一个负责计算,一个负责通知推送。多个专责 Agent 协作,比一个什么都能做的臃肿 Agent 更可靠。这种多 Agent 架构是第 13 章 LangGraph 的核心话题。
下一篇介绍 OutputParser——如何把 LLM 的文本输出可靠地解析成结构化的 Python 对象。