一、工具调用基础:什么是工具调用

各位同学,咱们今天聊点实在的。工具调用(Tool Calling)这个概念,说白了就是让大模型能「动手干活」,而不只是「动嘴说话」。

我刚开始接触这个领域时,也困惑过:模型不是已经能写诗、写代码了吗?为什么还需要工具调用?后来在做一个智能客服项目时,模型能回答「今天天气怎么样」,但死活查不了实时数据——因为它没有「手」去调用天气API。嗯,这就是工具调用要解决的问题。

1.1 工具调用的本质

工具调用,就是让大语言模型(LLM)在生成回复的过程中,能够主动请求调用外部函数或API,并把返回结果融入对话上下文。你可以把它理解成给模型装了一双「机械臂」——它能感知外部世界,也能改变外部世界。

核心一句话:工具调用 = 模型决定「什么时候该用工具」 + 系统执行「具体怎么调用」 + 结果反馈给模型「继续推理」。

举个例子。你问模型:「帮我查一下北京到上海的机票,明天下午的。」没有工具调用时,模型只能瞎编一个航班。有了工具调用,模型会输出一个类似 search_flights(city_from="北京", city_to="上海", date="2025-01-15") 的请求,系统去调真实API,拿到数据后再让模型整理成自然语言回复。

1.2 核心概念:Function Calling

Function Calling 是 OpenAI 在 2023 年 6 月提出的概念,现在已经成为行业标准。它不是什么黑魔法,而是一种结构化的协议——模型输出一个 JSON 对象,描述它想调用哪个函数、传什么参数。

我个人习惯把 Function Calling 拆成三个角色:

角色 职责 示例
模型 根据用户意图,决定是否调用工具,并生成调用参数 输出 {"name": "get_weather", "arguments": {"city": "北京"}}
系统 接收模型输出的调用请求,执行实际函数 调用 get_weather("北京") 并返回结果
上下文 把函数执行结果注入到对话中,让模型继续推理 模型看到「北京当前温度 25°C」后,组织回复

避坑指南:我曾经犯过一个低级错误——把函数名写成了驼峰式,但模型注册时用的是下划线式。结果模型死活调不对函数。后来我统一用 snake_case,再也没出过问题。记住:函数命名风格一定要一致。

1.3 工具注册与发现机制

工具注册,就是告诉模型「你有这些工具可以用」。发现机制,则是模型在推理时「知道该选哪个工具」。这两者缺一不可。

咱们来看一个标准的工具注册流程。以 OpenAI 的 API 为例:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如北京、上海"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_flights",
            "description": "查询航班信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "from": {"type": "string"},
                    "to": {"type": "string"},
                    "date": {"type": "string"}
                },
                "required": ["from", "to", "date"]
            }
        }
    }
]

你想想看,这段代码其实就是在给模型「画地图」。每个工具都有:

  • name:工具的唯一标识,模型靠它来「点名」
  • description:告诉模型这个工具是干什么的,描述越清晰,模型选得越准
  • parameters:参数定义,模型必须按这个 schema 来生成参数
  • required:哪些参数是必填的,少了模型会报错

注意:description 不是写给人看的,是写给模型看的。我见过有人写「这个函数用来获取天气数据,注意不要频繁调用」,结果模型真的以为「不要频繁调用」是约束条件,导致它死活不调。description 里只写功能描述,别写指令。

1.4 工具调用的完整流程

咱们用一张图来梳理整个链路。下面这个 SVG 展示了从用户提问到工具返回结果的完整闭环:

工具调用完整流程图 工具调用完整流程图 用户提问 模型推理(是否调工具?) 否 直接回复用户 是 生成函数调用请求 系统执行实际函数 结果注入对话上下文 模型继续推理 说明:模型可能多次调用工具(多轮工具调用),每次结果都会回注上下文

这个流程里有个关键点:模型不是只调一次工具就完事。它可能先查天气,再根据天气查航班,最后订酒店——这就是多轮工具调用。每次调用结果都会回注到上下文,模型基于新信息继续推理。

1.5 工具发现的策略

当工具数量多了(比如几十个甚至上百个),模型怎么知道该用哪个?这就涉及到工具发现机制。目前主流做法有三种:

策略 原理 适用场景 我踩过的坑
全量注册 把所有工具定义一次性传给模型 工具少于 20 个,且模型上下文窗口够大 工具超过 30 个时,模型容易「选择困难」,响应变慢
语义检索 用 embedding 把用户问题向量化,检索最相关的 5-10 个工具 工具数量 50+,需要快速筛选 检索召回率不够时,模型找不到正确工具
分层路由 先让模型选「工具类别」,再在类别内选具体工具 工具数量 100+,且有清晰的分类体系 分类边界模糊时,模型会「串门」

我的建议:刚开始做项目时,别一上来就搞语义检索或分层路由。先用全量注册跑通流程,等工具数量确实撑不住了,再升级策略。过早优化是万恶之源——这话在工具调用领域同样适用。

1.6 工具注册的元数据设计

工具注册不只是写个函数名和参数。好的元数据设计,能让模型「用得更顺手」。我总结了一个「三要素」原则:

  • 描述要具体:别写「获取数据」,要写「根据城市名称获取当前天气数据,包括温度、湿度、风速」
  • 参数名要自解释:city_name 比 c 好,temperature_unit 比 unit 好
  • 枚举值要完整:如果参数只有几个可选值,一定要用 enum 列出来,模型会优先选枚举里的值

举个例子,同样是查天气,两种注册方式的效果天差地别:

// 不好的注册方式
{
    "name": "get_data",
    "description": "获取数据",
    "parameters": {
        "type": "object",
        "properties": {
            "p1": {"type": "string"},
            "p2": {"type": "string"}
        }
    }
}

// 好的注册方式
{
    "name": "get_weather",
    "description": "根据城市名称获取当前天气数据,返回温度、湿度、风速和天气状况",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "城市中文名称,如'北京'、'上海'"
            },
            "unit": {
                "type": "string",
                "enum": ["celsius", "fahrenheit"],
                "description": "温度单位,默认摄氏度"
            }
        },
        "required": ["city"]
    }
}

我曾经踩过的坑:有次我把参数描述写成了「城市名称,如 Beijing、Shanghai」,结果模型死活传英文名,而我的后端只认中文名。后来我把描述改成「城市中文名称,如'北京'、'上海'」,问题立刻解决。记住:模型会严格按照 description 里的示例来生成参数。

1.7 小结

工具调用不是什么高深的技术,它本质上就是「模型 + 函数 + 上下文」的三方协作。核心要点就三个:

  • Function Calling 是模型输出结构化调用请求的协议
  • 工具注册是给模型画「能力地图」,描述越清晰,模型选得越准
  • 工具发现机制决定了模型在众多工具中如何「精准定位」