本文是 AI Agent 入门系列的第 3 篇。前两篇我们学会了调 LLM API 和结构化输出,现在进入 Agent 最核心的技术:Tool Use(函数调用) 。
学完本篇,你将给 LLM 装上"手"------让它能调用工具来回答问题。
本课目标
- 理解 Function Calling / Tool Use 的原理
- 学会定义工具并绑定到 LLM
- 掌握工具调用的完整流程
本课产出
- 新建文件 :
lesson03_tool_calling.py,粘贴下文完整代码 - 运行 :
python lesson03_tool_calling.py - 效果:LLM 能根据问题自主决定调用计算器或天气工具,拿到结果后给出最终回答
一、什么是 Tool Use?
Tool Use(函数调用)是 Agent 最核心的技术------它让 LLM 从"只能说话"变成"能做事"。
核心流程
用户: "2.5 * 3.7 + 1.8 等于多少?"
LLM 思考:这是一个数学计算,需要调用计算器工具
↓
LLM 输出:{"function": "calculator", "args": {"expr": "2.5 * 3.7 + 1.8"}}
↑ LLM 只输出调用请求,不执行计算
↓
你的代码执行 calculator("2.5 * 3.7 + 1.8")
↓
得到结果:11.05
↓
把结果返回给 LLM,LLM 生成最终回答:"结果是 11.05"
关键理解:
LLM 不执行代码,它只发出"调用请求"。执行由你写代码完成,结果再喂回给 LLM。
这样做的好处:
- LLM 不需要知道工具的实现细节
- 你可以绑定任何工具(搜索、计算、发邮件、操作文件......)
- 每个工具可以用最合适的语言/库实现
二、工具的定义方式
每个工具定义包含三部分:
| 字段 | 作用 | 关键 |
|---|---|---|
name |
工具名称 | 唯一标识,Python 函数名保持一致 |
description |
工具描述 | 最重要 ------ 决定 LLM 什么时候选这个工具 |
parameters |
参数定义 | JSON Schema 格式,告诉 LLM 需要传什么参数 |
工具描述的艺术
python
# ❌ 差 ------ 太简略
"description": "计算"
# ✅ 好 ------ 说明什么时候用、参数怎么传
"description": "执行数学表达式计算,支持 + - * / ** 等运算符。"
"当用户需要计算数值时使用此工具。"
三、完整调用流程
┌──────┐ ① 提问 ┌──────┐ ② 请求调用工具 ┌──────┐
│ 用户 │ ──────────→ │ LLM │ ────────────────→ │ 代码 │
│ │ ←────────── │ │ ←──────────────── │ │
└──────┘ ⑤ 回答 └──────┘ ④ 返回结果 └──────┘
│
│ ③ 执行
↓
┌──────┐
│ 工具 │
└──────┘
关于 tool_choice
默认情况下 LLM 自主决定是否调用工具。你也可以控制:
| tool_choice | 行为 | 适用场景 |
|---|---|---|
"auto"(默认) |
LLM 自主决定 | 不确定是否需要工具时 |
"required" |
强制每次都调用 | 调试、确定性演示 |
多工具调用
LLM 可能一次请求调用多个工具 (如"北京和上海的天气"可能同时触发两次查询)。
代码中可以用循环处理:
python
for tool_call in message.tool_calls:
result = call_tool(tool_call)
四、完整代码
💡 环境准备:本课使用与前两课相同的环境。如尚未配置,请先完成:
pip install openai python-dotenv- 进入
code/目录,复制.env.example为.env,填入你的 API Key(详见第 1 课)
⚠️ 安全说明 :本课计算器使用eval()仅用于教学演示。
eval()有安全风险,生产环境请使用numexpr或ast.literal_eval。
新建 lesson03_tool_calling.py,粘贴以下代码:
python
"""
第 3 课:Tool Use 入门 ------ 函数调用完整实现
"""
import json, os
from dotenv import load_dotenv
load_dotenv()
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
MODEL = "deepseek-chat"
if not client.api_key:
print("❌ 未检测到 API Key!")
exit(1)
# ===========================================
# 工具函数实现
# ===========================================
def calculator(expr: str) -> str:
"""执行数学表达式计算(教学演示用 eval,生产环境请用 ast)"""
try:
# 安全白名单:只允许数字和基本运算符
allowed = set("0123456789+-*/(). ")
if not all(c in allowed for c in expr):
return "错误:表达式包含不允许的字符"
result = eval(expr) # 仅用于教学演示
return f"{expr} = {result}"
except Exception as e:
return f"计算错误:{e}"
def get_weather(city: str) -> str:
"""查询天气(mock 数据,仅用于演示)"""
mock = {
"北京": "晴,25°C,湿度 40%,北风 3 级",
"上海": "多云,28°C,湿度 65%,东南风 2 级",
"深圳": "阵雨,30°C,湿度 80%,南风 4 级",
"广州": "阴天,29°C,湿度 75%,西南风 3 级",
"成都": "多云,26°C,湿度 60%,东北风 2 级",
"杭州": "晴,27°C,湿度 55%,东风 3 级",
}
data = mock.get(city)
if data:
return f"{city}天气:{data}"
return f"暂无 {city} 的数据。支持:{', '.join(mock.keys())}"
# ===========================================
# 工具定义(API 格式)
# ===========================================
TOOLS = [
{
"type": "function",
"function": {
"name": "calculator",
"description": "执行数学表达式计算。支持 + - * / ** 等运算符。"
"当用户需要计算数值时使用。",
"parameters": {
"type": "object",
"properties": {
"expr": {
"type": "string",
"description": "数学表达式,如 '2.5 * 3.7 + 1.8'"
}
},
"required": ["expr"]
}
}
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的实时天气。参数 city 用中文城市名。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名,如 '北京'、'上海'"
}
},
"required": ["city"]
}
}
}
]
TOOL_FUNCTIONS = {"calculator": calculator, "get_weather": get_weather}
# ===========================================
# Agent 主循环
# ===========================================
def call_tool(tool_call):
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
func = TOOL_FUNCTIONS.get(func_name)
if not func:
return f"未知工具 {func_name}"
print(f" → 调用 {func_name}({json.dumps(func_args, ensure_ascii=False)})")
result = func(**func_args)
print(f" ← 结果: {result}")
return result
def run_agent(user_input: str, max_turns: int = 5):
messages = [
{"role": "system", "content": "你是 AI 助手,可使用工具回答问题。"},
{"role": "user", "content": user_input},
]
for turn in range(1, max_turns + 1):
print(f"\n--- 第 {turn} 轮 ---")
try:
response = client.chat.completions.create(
model=MODEL, messages=messages, tools=TOOLS,
)
except Exception as e:
print(f"API 调用失败:{e}")
return
msg = response.choices[0].message
if msg.tool_calls:
messages.append(msg)
for tc in msg.tool_calls:
messages.append({
"role": "tool", "tool_call_id": tc.id,
"content": call_tool(tc),
})
else:
print(f"\n最终回答:{msg.content}")
return
print(f"达到最大轮数 {max_turns},结束。")
if __name__ == "__main__":
print("=" * 50)
print("测试 1:数学计算")
print("=" * 50)
run_agent("23 * 45 + 12 等于多少?")
print("\n\n" + "=" * 50)
print("测试 2:天气查询")
print("=" * 50)
run_agent("北京和上海的天气怎么样?")
print("\n\n" + "=" * 50)
print("测试 3:混合问题")
print("=" * 50)
run_agent("你好!顺便算一下 3.14 * 2.71 等于多少?")
运行结果示例
text
==================================================
测试 1:数学计算
==================================================
--- 第 1 轮 ---
→ 调用 calculator({"expr": "23 * 45 + 12"})
← 结果: 23 * 45 + 12 = 1047
最终回答:23 × 45 + 12 = 1047
==================================================
测试 2:天气查询
==================================================
--- 第 1 轮 ---
→ 调用 get_weather({"city": "北京"})
← 结果: 北京天气:晴,25°C,湿度 40%,北风 3 级
→ 调用 get_weather({"city": "上海"})
← 结果: 上海天气:多云,28°C,湿度 65%,东南风 2 级
最终回答:北京今天晴,25°C;上海多云,28°C。
常见报错
| 问题 | 原因 | 解决 |
|---|---|---|
| LLM 直接回答而不是调用工具 | 模型或 prompt 不支持 tools | 检查模型是否支持 Function Calling,或加 tool_choice="required" |
tool_calls 为空但 finish_reason 不是 stop |
模型不支持工具调用 | 更换为 DeepSeek 或 GPT-4o 等支持工具调用的模型 |
| 计算器返回"表达式包含不允许的字符" | 表达式中含有字母 | 纯数字计算不会有此问题,确保只传入数学表达式 |
动手练习
- 添加第三个工具:
get_current_time(),返回当前时间 - 修改天气 mock 数据,让它支持更多城市
- 故意把工具描述写得很差(如 calculator 的描述写成"处理文本"),观察 LLM 的选择是否出错
思考题
- 如果 LLM 连续请求调用同一个工具(比如不断调用计算器),可能是什么原因?怎么处理?
- 工具的描述写得不好会有什么后果?
- 如果两个工具功能相似(比如两个搜索工具),LLM 怎么决定用哪个?
📦 完整代码
本课程所有代码已托管在 GitCode:
git clone git@gitcode.com:gcw_A202cbBm/ai-agent.git
cd ai-agent/code
也可直接访问:https://gitcode.com/gcw_A202cbBm/ai-agent
下一篇预告:AI Agent 入门(四):手写 Agent 循环 ------ ReAct 模式完整实现
我们将用不到 100 行代码,从零实现一个完整的 Agent 主循环,支持多工具注册、异常处理、调用日志追踪。
如果您觉得有用,欢迎 点赞、转发、评论、关注。