一、上一章回顾
模型初始化通过 init_llm.py 使用 LangChain 的 init_chat_model 初始化了 DeepSeek 模型,并成功进行了简单问答。代码:
python
from langchain.chat_models import init_chat_model
from app.config import DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, MODEL_NAME
# 使用 init_chat_model 创建 DeepSeek 聊天模型实例
# 模型标识格式为 "provider:model_name",DeepSeek 兼容 OpenAI 接口,故 provider 写 "openai"
deepseek_llm = init_chat_model(
model=f"openai:{MODEL_NAME}", # 例如 "openai:deepseek-chat"
openai_api_key=DEEPSEEK_API_KEY,
openai_api_base=DEEPSEEK_BASE_URL,
temperature=0,
)
并使用验证了一下调用模型的效果
python
from init_llm import deepseek_llm
response = deepseek_llm.invoke("你好,请介绍一下自己")
print(response.content)
但如果问一个需要实时数据的问题,比如"北京现在天气如何?",模型会怎么回答?
二、示例一:不调用工具的直接问答
2.1 代码 (no_tool_demo.py)
python
"""
示例一:不调用工具 -- 直接问答
"""
from init_llm import deepseek_llm
question = "北京的天气如何?"
response = deepseek_llm.invoke(question)
print("模型回复:", response.content)
2.2 实际运行结果
模型回复: 北京的天气会随季节变化较大,以下是当前和未来几天的概况(截至2023年10月更新,实际天气请以最新预报为准)
2.3 现象分析
模型很诚实,但也很无奈。因为它的知识截止于训练数据,而天气是随时变化的。如果强行回答,就可能产生"幻觉"------胡编一个结果。
这揭示了纯语言模型的根本局限:
- 知识陈旧,无法获取最新信息;
- 无法与外部世界交互(查数据库、调 API、执行操作)。
那么,能不能让模型自己去"查"一下呢?这就是我们今天要学的 工具调用。
三、工具调用核心概念
工具调用 ,也叫 Function Calling,是一种让模型能够"请求"外部函数执行的机制。流程如下:
用户提问
→ 模型判断需要调用工具
→ 返回"工具调用指令"
→ 我们的代码执行该工具
→ 将结果返回给模型
→ 模型结合结果生成自然语言回复
关键角色:
- 工具 (Tool):一个带有描述的实际函数(如查天气)。
- 绑定 (Bind):让模型知道有哪些工具可用。
- 工具调用指令 (Tool Call):模型输出的结构化要求("请执行函数X,参数为Y")。
- 工具消息 (ToolMessage):把执行结果包装后送回模型。
四、示例二:带工具调用的智能问答
现在我们要让模型能真实地查询天气。
4.1 定义工具函数
工具就是一个普通的 Python 函数,但我们用 @tool 装饰器标记它。函数的文档字符串至关重要,模型会根据它来理解工具的功能和使用方法。
python
from langchain_core.tools import tool
@tool
def get_weather(location: str) -> str:
"""查询指定城市的实时天气情况。参数 location:城市名称,例如 '北京'。"""
weather_data = {
"北京": "晴,温度 22°C,湿度 40%",
"上海": "多云,温度 25°C,湿度 70%",
"深圳": "阵雨,温度 28°C,湿度 85%",
}
return weather_data.get(location, f"未找到 {location} 的天气信息")
4.2 完整代码:绑定工具并多步交互
python
"""
示例二:完整的工具调用 (Function Calling) 示例
使用 LangChain + DeepSeek API
前提:已通过 init_llm.py 初始化模型
"""
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
from init_llm import deepseek_llm # 使用已配置好的模型
# ========== 1. 定义工具 ==========
@tool
def get_weather(location: str) -> str:
"""查询指定城市的实时天气情况。参数 location:城市名称,例如 '北京'。"""
weather_data = {
"北京": "晴,温度 22°C,湿度 40%",
"上海": "多云,温度 25°C,湿度 70%",
"深圳": "阵雨,温度 28°C,湿度 85%",
}
return weather_data.get(location, f"未找到 {location} 的天气信息")
# ========== 2. 绑定工具到模型 ==========
model_with_tools = deepseek_llm.bind_tools([get_weather])
# ========== 3. 用户提问,模型返回工具调用指令 ==========
user_question = "北京的天气如何?"
response = model_with_tools.invoke(user_question)
print("工具调用指令:", response.tool_calls)
# ========== 4. 真正执行工具,获取结果 ==========
tool_messages = []
if response.tool_calls:
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
if tool_name == "get_weather":
result = get_weather.invoke(tool_args) # 此处真正执行函数
print(f"工具执行结果:{result}")
# 将结果包装为 ToolMessage(必须携带 tool_call_id)
tool_messages.append(
ToolMessage(content=result, tool_call_id=tool_call["id"])
)
# ========== 5. 将工具结果返回模型,生成最终回答 ==========
if tool_messages:
final_response = deepseek_llm.invoke(
[HumanMessage(content=user_question), response] + tool_messages
)
print("\n最终回答:", final_response.content)
else:
# 如果模型没调用工具,直接输出原回复
print("\n最终回答:", response.content)
4.3 运行结果
工具调用指令: [{'name': 'get_weather', 'args': {'location': '北京'}, 'id': 'call_abc123'}]
工具执行结果:晴,温度 22°C,湿度 40%
最终回答: 北京今天的天气是晴天,温度22°C,湿度40%,非常适合外出。
4.4 逐步解析
| 步骤 | 消息类型 | 发生了什么 | 谁在执行 |
|---|---|---|---|
| ① 用户提问 | 【HumanMessage】 | "北京的天气如何?" |
用户 |
| ② 模型决策 | 【AIMessage】 | 模型发现绑定了 get_weather 工具,判断需要调用,返回指令:get_weather(location="北京") |
模型 |
| ③ 执行工具 | 我们的代码解析指令,真正调用 get_weather("北京"),得到结果字符串 |
我们的程序 | |
| ④ 结果返回 | 【ToolMessage】 | 将天气结果包装成 ToolMessage,连同原问题一起再次发给模型 |
我们的程序 |
| ⑤ 最终生成 | 【AIMessage】 | 模型阅读天气结果,生成流畅自然的中文回答 | 模型 |
要点:
tool_calls是模型输出的结构化指令,不是真正的函数调用。我们必须在代码中手动执行函数。ToolMessage必须附带与对应的tool_call相同的id,这样模型才能将指令和结果正确关联。- LangChain 的
.bind_tools()自动将工具的签名和描述转换为模型能理解的格式。
五、两段代码全面对比
| 对比维度 | 示例一(不调用工具) | 示例二(调用工具) |
|---|---|---|
| 信息准确性 | 可能过时或直接说"不知道" | 基于真实数据,准确 |
| 模型角色 | 仅被动回答 | 主动判断需调用哪个工具,再组织答案 |
| 与外界交互 | 无 | 可连接数据库、API、文件系统等 |
| 交互轮次 | 单轮 | 至少两轮(模型→工具→模型) |
| 代码复杂度 | 极简 | 需处理工具调用指令、工具执行和 ToolMessage |
| 输出示例 | "我无法获取实时天气" | "北京今天晴,22°C,湿度40%" |
| 适用场景 | 常识问答、文本生成 | 实时查询、数据操作、自动化任务 |
核心差异一句话:
不调用工具,模型是"知识有限的作家";
调用工具,模型是"能动手获取知识的智能代理"。
六、总结
通过两段代码的对比,明白为什么需要工具调用 ,以及如何实现它。
关键收获:
- 模型通过
tool_calls输出结构化的调用指令,而非直接执行。 - 我们编写代码执行工具,并用
ToolMessage将结果送回。 tool_call_id是连接指令和结果的关键纽带。- 从简单的问答模型,到可扩展的智能代理,工具调用是最重要的一步。