一文吃透 Tool Calling:用 Python 打通大模型工具调用闭环

141 阅读7分钟

大模型本质上是“只会算、不知道实时世界”的推理引擎。
Tool Calling(工具调用)  的意义,就是让大模型学会在合适的时机调用外部工具,把真实世界的数据拉进对话里。

这篇文章通过一个最小可行的“查天气工具”示例,从零拆解:

  • 用 Python 封装一个可复用的工具函数
  • 用 JSON Schema 把函数描述成大模型可调用的 Tool
  • 大模型如何发起 tool_calls,本地代码如何路由并执行
  • Tool 调用的完整闭环
  • 最后单独总结:哪些是工程&面试中的关键考点

一、用 Python 封装天气 Tool:实现层

先只关注业务逻辑,用 Python 写一个“查天气”的函数:

import requests

def get_weather(location: str) -> str:
    url = "https://api.seniverse.com/v3/weather/now.json"
    params = {
        "key": "YOUR_API_KEY",
        "location": location,
        "language": "zh-Hans",
    }
    try:
        resp = requests.get(url, params=params, timeout=10)
        data = resp.json()
        print(data)  # 调试阶段可查看真实返回

        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 "查询失败"
    except Exception as e:
        return f"异常:{e}"

关键点

  • 类型标注

    def get_weather(location: str) -> str:
    
    • 明确入参和返回值类型,利于 IDE 提示、团队协作。
    • 为后续配合 mypy 等静态工具打基础。
  • HTTP 请求与超时

    resp = requests.get(url, params=params, timeout=10)
    data = resp.json()
    
    • 使用 params 自动拼接查询参数。
    • timeout 防止长时间阻塞,是对接第三方服务的基本要求。
  • JSON 结构解析与异常兜底

    if "results" in data:
        ...
    else:
        return "查询失败"
    
    • 在访问深层字段前先检查顶层 key。
    • 捕获异常,统一转成可读字符串,便于上层处理。

只看这一段,这就是一个标准的业务 Tool 函数:清晰的入参、出参,内部完成外部 API 调用和结果整理。

二、从函数到 Tool:用 JSON Schema 描述工具契约

大模型并不会直接执行你的 Python 代码,它看到的是一段 Tool 描述(JSON 格式):

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市名称,如'北京'",
                    }
                },
                "required": ["location"],
            },
        },
    }
]

关键点

  • name / description

    • name:大模型在 tool_calls 里引用的函数名。
    • description:自然语言说明,影响模型是否、何时选择该工具。
  • parameters(JSON Schema)

    • type: "object":参数是一个对象。
    • properties.location:必须是字符串类型。
    • required: ["location"]:必填字段。

这里实际上定义了一份 Tool 接口契约
Tool 的“长相”和“参数约束”与具体语言无关,可以被任意兼容 OpenAI 协议的大模型理解与使用。

三、初始化大模型客户端:让模型“知道有这些 Tool”

以兼容 OpenAI 协议的客户端为例:

from openai import OpenAI
client = OpenAI(
    api_key="YOUR_DEEPSEEK_API_KEY",
    base_url="https://api.deepseek.com/v1",
)

后续所有与大模型的交互(包括 Tool 调用),都通过这个 client 完成。

四、第一轮调用:模型决定是否使用 Tool(产生 tool_calls)

构造一轮对话,附带上文定义好的 tools

import json
messages = [
    {"role": "user", "content": "北京天气怎么样"}
]

response = client.chat.completions.create(
    model="deepseek-reasoner",
    messages=messages,
    tools=tools,
    tool_choice="auto",   # 让模型自动判断是否调用工具
    temperature=0.3,
)

response_message = response.choices[0].message
print(response_message)
messages.append(response_message)

关键点

  • messages 协议

    • role: "user":用户输入。
    • 后面会出现 role: "assistant"(模型)、role: "tool"(工具结果)。
  • tool_choice="auto"

    • 模型根据问题内容和 Tool 描述决定是否调用 Tool。
    • 问题依赖实时数据(天气、股价)时更容易触发 Tool 调用。
  • tool_calls 字段

    • 当模型认为需要调用 Tool 时,response_message.tool_calls 会包含:

      • 要调用的 function.name
      • 对应的 arguments(JSON 字符串)。

这一轮调用的核心目的,是让模型给出  “我要调用哪个 Tool、用什么参数”  的决策。

五、本地执行 Tool:解析 tool_calls 并路由到实现

接下来处理第一轮回复中的 tool_calls,在本地真正执行对应的 Python 函数:

if response_message.tool_calls:
    for tool_call in response_message.tool_calls:
        function_name = tool_call.function.name
        # arguments 是 JSON 字符串,需要反序列化
        function_args = json.loads(tool_call.function.arguments)

        if function_name == "get_weather":
            function_response = get_weather(function_args["location"])
        else:
            function_response = "未知工具"

        messages.append({
            "tool_call_id": tool_call.id,
            "role": "tool",
            "name": function_name,
            "content": function_response,
        })
else:
    # 没有使用工具,模型直接给出了答案
    print(response_message.content)

关键点

  • arguments 反序列化

    function_args = json.loads(tool_call.function.arguments)
    
    • arguments 本质是一个 JSON 字符串。
    • 需要转成字典后再从中取出 location 等字段。
  • Tool 路由分发

    if function_name == "get_weather":
        ...
    
    • 简单情况下使用 if-else 即可。

    • 多 Tool 场景可以用映射表统一管理,例如:

      TOOL_MAP = {"get_weather": get_weather}
      
  • 把 Tool 结果写回 messages

    messages.append({
        "tool_call_id": tool_call.id,
        "role": "tool",
        "name": function_name,
        "content": function_response,
    })
    
    • role: "tool":告诉大模型这是“工具执行结果”。
    • tool_call_id:把这条结果和前面那次调用关联起来。

这一段是整个 Tool 调用链路中的“执行中枢”:
从大模型的调用意图(tool_calls)过渡到本地实际执行的工具函数。

六、第二轮调用:模型基于 Tool 结果生成自然语言回答

到这里,messages 已经包含了:

  • 用户原始问题。
  • 模型发起的工具调用信息。
  • 工具执行后的结果(以 role: "tool" 的消息形式)。

再发起一次对话,让模型利用这些信息组织自然语言回复:

final_response = client.chat.completions.create(
    model="deepseek-reasoner",
    messages=messages,
    temperature=0.3,
)

print(final_response.choices[0].message.content)

这一轮调用的重点,是让模型把工具结果视为“外部事实”,然后用自然语言进行总结、解释和扩展,例如:

  • 当前天气现象、温度
  • 简单的穿衣/出行建议
  • 其他基于天气信息的补充说明

至此,一个标准的 Tool Calling 闭环 完成:

  1. 模型基于用户问题和 Tool 描述,决定是否要调用 Tool。
  2. 代码本地执行 Tool,实现真实世界数据抓取。
  3. 工具结果回流到对话上下文中。
  4. 模型基于结果输出自然语言回答。

七、Tool Calling 全流程小结

把整条链路压缩成几步,方便整体把握:

  • 实现层
    用 Python 写出一个职责单一的业务函数(如 get_weather)。
  • 契约层(Tool 描述)
    用 JSON Schema 描述函数的名称、用途和参数结构,形成语言无关的工具契约。
  • 决策层(第一轮模型调用)
    携带 tools 调用聊天接口,让模型产生 tool_calls
  • 执行层(本地路由 & 调用)
    解析 tool_calls,映射到真实函数执行,把结果写入 messages 作为 role: "tool"
  • 生成层(第二轮模型调用)
    再次调用聊天接口,让模型基于工具结果生成自然语言回答。

八、面试与工程实践中可重点展开的考点

  • 外部 HTTP 调用的健壮性

    外部 API 调用中,超时、重试、状态码检查、JSON 解析错误处理等都是必备要素;需要区分网络错误和业务错误,并为上层提供清晰的错误信息。

  • Tool 的 JSON Schema 设计

    使用 JSON Schema 精确定义参数类型、必填字段和取值范围,是大模型正确构造 arguments 的基础;工具功能演进时,需要保证 schema 兼容性和易扩展性。

  • Tool 路由与扩展机制

    对多个 Tool 的管理可以从 if-else 过渡到集中式路由表或注册中心;保证新增 Tool 时最小化对现有代码的入侵,形成插件化结构。

  • 两段式 Tool 调用交互模式

    Tool Calling 通常采用“先让模型生成调用计划,再执行工具并返回结果,最后生成回答”的两段式交互;这为并行调用多个 Tool、缓存部分结果等优化提供了空间。

总结

这套示例从一个简单的 get_weather 函数出发,完整走了一遍:

  • 用 Python 调天气接口。
  • 用 JSON Schema 描述本地函数,暴露给大模型。
  • 通过 tools 和 tool_calls 协议,让模型自己“决定用什么工具、用什么参数”。
  • 把工具执行结果重新注入对话,生成自然语言回答。

可以把 tool 理解成:

“对大模型公开的一个函数接口(JSON 描述) + 在你自己程序里的真实实现,它让大模型不只会说话,还能让你这套代码去『干活』。”

  • 对模型:只看到 tools 数组里的 JSON,知道「有什么能力」「需要什么参数」。
  • 对你:就是把各种 API、数据库查询、本地逻辑封装成函数,然后注册成 tool。
  • 作用:把“纯语言模型”升级成“会用工具的软件中枢”。