手把手教你实现 LLM 调用外部工具:超详细 Function Calling 教学(含工具参数详解)

179 阅读9分钟

🌟 为什么需要 Function Calling?

大语言模型(如 GPT、DeepSeek)虽然能回答很多问题,但它不知道今天北京是否下雨,因为它:

  • 训练数据截止于过去
  • 无法主动访问互联网

为了解决这个问题,OpenAI 提出了 Function Calling(函数调用) 机制:

让 LLM 在需要时,主动“调用”你提供的工具(比如查天气、查汇率),再根据结果生成回答。

这就像给 AI 配了一个“助手”,它说:“帮我查下上海天气”,你就去查,然后告诉它结果,它再转述给人类。

本文将带你从零实现这个过程,并逐行解释所有关键代码,尤其深入讲解 工具(Tool)定义中的每个参数


第一步:安装依赖

pip install requests openai
  • requests:用于向网络 API 发送请求(比如获取天气)
  • openai:用于调用兼容 OpenAI 协议的大模型(如 DeepSeek、Moonshot、官方 GPT)

❌ 不要用 npm install,那是 JavaScript 的命令!


第二步:编写天气查询函数(工具本身)

这是整个系统的核心工具。我们一步步来写。

2.1 导入 requests 库

import requests

这行代码告诉 Python:“我要用 requests 这个工具去访问网络”。

2.2 定义函数并添加类型提示

def get_weather(location: str) -> str:
  • def 表示定义一个函数。
  • 函数名叫 get_weather(获取天气)。
  • 它接收一个参数 location,类型是字符串(: str)。
  • 它最终返回一个字符串(-> str),比如 "北京当前天气:晴"

✅ 类型提示不是强制的,但能让代码更清晰,编辑器也能帮你检查错误。

2.3 设置 API 地址

url = "https://api.seniverse.com/v3/weather/now.json"

这是 心知天气 提供的实时天气接口地址。
所有天气请求都要发到这个 URL。

2.4 构造请求参数:重点讲解 params

params = {
    "key": "SspUltXUBqSJDjhYz",
    "location": location,
    "language": "zh-Hans"
}

🔍 params 到底是干什么的?

当你在浏览器访问:

https://example.com/api?name=张三&age=25

问号 ? 后面的部分叫 查询参数(Query Parameters)

手动拼接这些参数很麻烦,还容易出错(比如中文要编码)。
requests 库提供了一个聪明的办法:用字典传参,自动拼接!

当你写:

requests.get(url, params=params)

它会自动把 params 转成:

?key=Ssp...&location=北京&language=zh-Hans

并附加到 url 后面,完全不用你操心!

参数说明:

说明
key"SspUltXUBqSJDjhYz"你的 API 密钥(必须注册心知天气获取)
location用户传入的城市名比如 "上海""广州"
language"zh-Hans"返回简体中文结果

⚠️ 重要:示例中的 key 是无效的,请登录 心知天气控制台 获取你自己的免费密钥!

2.5 发送请求并设置超时

resp = requests.get(url, params=params, timeout=10)
  • requests.get():发起 GET 请求(最常用的 HTTP 方法)。
  • timeout=10:如果 10 秒内没收到回复,就放弃请求,防止程序卡死。

2.6 解析 JSON 响应

data = resp.json()

API 返回的是 JSON 格式的数据,看起来像这样(简化版):

{
  "results": [
    {
      "location": { "name": "深圳" },
      "now": {
        "text": "多云",
        "temperature": "28"
      }
    }
  ]
}

.json() 方法会自动把这个 JSON 字符串转换成 Python 的字典(dict),方便我们取值。

2.7 提取关键信息

if "results" in data:
    r = data["results"][0]
    city = r["location"]["name"]
    now = r["now"]
    text = now["text"]
    temp = now["temperature"]
    return f"{city}当前的天气:{text},气温:{temp}℃"
else:
    return "查询失败"
  • 先检查是否有 "results" 字段(避免 API 报错时程序崩溃)。
  • data["results"] 是一个列表,通常只包含一个城市的结果,所以取 [0]
  • 通过层层“下钻”拿到城市名、天气描述、温度。
  • 用 f-string(格式化字符串)组合成一句自然语言。

✅ 示例输出:杭州当前的天气:小雨,气温:19℃

2.8 添加异常处理(让程序更健壮)

把上面所有逻辑包在 try...except 中:

try:
    # ... 发送请求、解析数据 ...
except Exception as e:
    return f"异常:{e}"

这样即使网络断了、API 挂了、密钥错了,程序也不会崩溃,而是返回友好的错误提示。

2.9 完整函数代码(带注释)

import requests

def get_weather(location: str) -> str:
    """
    获取指定城市的实时天气
    :param location: 城市名称,如 "北京"
    :return: 格式化的天气信息字符串
    """
    url = "https://api.seniverse.com/v3/weather/now.json"
    params = {
        "key": "你的API密钥",       # ← 务必替换成你自己的!
        "location": location,
        "language": "zh-Hans"
    }
    
    try:
        resp = requests.get(url, params=params, timeout=10)
        data = resp.json()
        
        if "results" in data:
            r = data["results"][0]
            city = r["location"]["name"]
            text = r["now"]["text"]
            temp = r["now"]["temperature"]
            return f"{city}当前的天气:{text},气温:{temp}℃"
        else:
            return "查询失败,请检查城市名称或 API 密钥"
            
    except Exception as e:
        return f"请求异常:{e}"

2.10 测试函数

print(get_weather("成都"))
# 输出示例:成都当前的天气:阴,气温:21℃

如果看到正常结果,恭喜!你已经掌握了调用第三方 API 的核心技能。

💡 这个函数就是我们即将“教给 LLM”的工具。但光有函数还不够,我们还得用一种 LLM 能理解的方式“描述”它。


第三步:定义工具(Tool)——重点来了!

LLM 是“读不懂 Python 代码”的。
为了让它知道“你能调用什么工具”,我们必须用 JSON 格式 描述工具的结构。

这就是 tools 列表的作用:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的实时天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "要查询的城市名称,例如‘北京’或‘Shanghai’"
                    }
                },
                "required": ["location"]
            }
        }
    }
]

下面,我们逐字段详细解释每个参数的意义,这是新手最容易卡住的地方!


🔍 3.1 "type": "function"

  • 表示这是一个“函数类型”的工具。
  • 目前 OpenAI 只支持 "function",未来可能支持其他类型(如 "retrieval")。
  • 必须写,不能省略。

🔍 3.2 "name": "get_weather"

  • 这是函数的名字,必须和你实际写的 Python 函数名完全一致
  • LLM 会根据这个名字决定调用哪个函数。
  • ✅ 正确:"name": "get_weather" 对应 def get_weather(...)
  • ❌ 错误:写成 "getName""weather",程序就找不到函数,会报错。

🔍 3.3 "description": "获取指定城市的实时天气"

  • 这是给 LLM 看的“说明书”
  • 它告诉模型:“这个工具能干什么”。
  • 写得越清楚,LLM 越容易在合适的时候调用它。

✅ 好的例子:

“获取用户指定城市的当前天气状况和温度”

❌ 差的例子:

“天气函数” —— 太模糊,LLM 可能不知道何时使用。

💡 小技巧:想象你在教一个实习生,你会怎么描述这个工具的功能?


🔍 3.4 "parameters":定义函数需要哪些输入

这是最复杂的部分,采用 JSON Schema 标准(一种描述数据结构的规范)。

"type": "object"

  • 表示这个函数接收的是一个“对象”(在 Python 中就是字典 dict)。
  • 几乎所有工具都用 "object",因为我们要传多个参数(哪怕只有一个)。

"properties":列出所有可能的参数

这是一个字典,每个键是一个参数名,值是该参数的描述。

以我们的例子为例:

"properties": {
    "location": {
        "type": "string",
        "description": "要查询的城市名称,例如‘北京’"
    }
}
  • "location":参数名,必须和 Python 函数的参数名一致。
  • "type": "string":说明这个参数必须是字符串类型(不能是数字或列表)。
  • "description":再次说明这个参数的含义,帮助 LLM 正确提取用户问题中的信息。

💡 举例:当用户问“上海天气怎么样?”,LLM 会根据 description 知道要把“上海”作为 location 的值。

"required": ["location"]

  • 这是一个列表,列出哪些参数是必须提供的。
  • 如果 LLM 没有提供这些参数,调用就会失败。
  • 我们的函数只有一个参数 location,所以写 ["location"]

✅ 如果有多个必填参数:

"required": ["city", "unit"]

✅ 如果某些参数可选(比如单位默认是摄氏度),就不放进 required


🧩 总结:Tool 定义的核心逻辑

字段作用类比
name函数名字工具的“身份证号”
description功能说明工具的“使用说明书”
parameters.properties参数列表及说明工具的“输入接口说明”
parameters.required必填参数“不填就不能用”的字段

⚠️ 写错任何一个,LLM 要么不会调用,要么调用失败!


第四步:让 LLM 使用这个工具

现在,我们把工具“教给”大模型,并让它处理用户问题。

4.1 初始化客户端

from openai import OpenAI

client = OpenAI(
    api_key="你的大模型API密钥",
    base_url="https://api.deepseek.com/v1"  # 使用 DeepSeek
)

4.2 用户提问

messages = [{"role": "user", "content": "成都现在多少度?"}]

4.3 第一次请求:让 LLM 决定是否调用工具

response = client.chat.completions.create(
    model="deepseek-reasoner",
    messages=messages,
    tools=tools,          # ← 把我们定义的工具传进去!
    tool_choice="auto",   # 让 LLM 自动决定是否调用
    temperature=0.3
)
  • tools=tools:把前面定义的工具列表传给 LLM。
  • tool_choice="auto":让模型自己判断要不要调用工具(也可以强制指定)。

4.4 执行工具调用

# 1. 获取 LLM 的回复(可能包含 tool_calls)
response_message = response.choices[0].message
messages.append(response_message)  # 加入对话历史

# 2. 如果有工具调用请求
if response_message.tool_calls:
    for tool_call in response_message.tool_calls:
        # 3. 提取函数名和参数
        func_name = tool_call.function.name
        func_args = json.loads(tool_call.function.arguments)  # JSON 字符串 → dict
        
        # 4. 执行对应函数
        if func_name == "get_weather":
            result = get_weather(func_args["location"])
        else:
            result = "未知工具"
        
        # 5. 将结果反馈给 LLM(关键!)
        messages.append({
            "tool_call_id": tool_call.id,      # 必须匹配
            "role": "tool",                    # 角色必须是 "tool"
            "name": func_name,
            "content": result
        })

✅ 关键:tool_call_id 必须原样返回,用于关联调用与结果。

4.5 第二次请求:生成最终回答

final_response = client.chat.completions.create(
    model="deepseek-reasoner",
    messages=messages,
    temperature=0.3
)
print(final_response.choices[0].message.content)

✅ 输出示例:

成都当前气温为21℃,天气阴,建议适当增添衣物。

第五步:常见错误与调试建议

问题可能原因解决方案
LLM 直接瞎编天气没传 tools,或 tool_choice="none"确保 tools=toolstool_choice="auto"
调用时参数为空description 写得太模糊明确写“从用户问题中提取城市名”
程序报错“KeyError: 'location'”LLM 没生成 location 参数检查 required 是否包含 location
返回“查询失败”心知天气 key 无效或城市名不支持Seniverse 控制台 测试 API

结语:你已经掌握了 AI 联网的核心技能!

通过本文,你不仅学会了:

  • 如何调用第三方 API
  • 如何定义 LLM 工具(Tool)
  • 如何处理 Function Calling 的完整流程

更重要的是,你理解了 每个参数背后的设计意图——这才是真正掌握技术的关键。

🚀 下一步你可以尝试:

  • 添加“查汇率”工具
  • 支持多城市查询
  • 用自然语言解析更复杂的问题(如“明天上海会下雨吗?”)

动手实践吧!AI 的未来,由你构建 💪


附:完整代码 + 注释版已整理至 GitHub,欢迎 Star & 提问!