上一篇文章里,我们把 Agent 的最小骨架搭起来了:本质上就是循环——把用户问题交给大模型,拿到回答,再交给用户。但你很快会发现,这个骨架单独跑起来没什么用。用户问“今天北京适合穿什么衣服”,模型只会给你一段“我无法获取实时天气数据”的道歉。原因很简单:LLM 是个离线的大脑,它的知识在训练时就冻结了,它没有任何“手”去查天气、查数据库、调接口。
这一篇要解决的问题,就是给这个大脑装上手。我们从零写一个能被 LLM 调用的工具(tool)系统,完整跑通“用户提问 -> 模型决定调用工具 -> 代码执行工具 -> 结果返回模型 -> 模型组织答案”这条链路。
我默认你已经读过了这个系列的第一篇,至少理解了“Agent = 模型 + 循环 + 工具”这个基础框架。如果你还没读过,也不影响,这篇文章会从工具调用的原理开始讲,代码部分我会给到可以直接跑的最小实现。整个项目用 Python + 智谱 GLM-4-Flash 免费模型 + OpenAI SDK 完成,全程没有高门槛依赖,下载完依赖就能跑。
1. 为什么 Agent 非得“自己动手”调工具:从一次失败对话说起
先看一个跑过第一篇文章代码的人大概率都会遇到的场景。用户问:
帮我查一下最近三个月这个账号的订单总数,然后按月份汇总。
如果你的 Agent 只有模型没有工具,模型只能回答类似“我无法访问您的订单数据”这句话,或者更气人的是,它可能会编一个数字给你——这就是行业里常说的幻觉。这个时候你作为一个开发者,心里一定在骂:数据库连接串都写在配置文件里了,查询逻辑也写好了,就差一个让它调用查询函数的方法。
1.1 “让 LLM 调用工具”到底是什么意思
很多人第一次接触 function calling(也叫 tool calling / 工具调用)时会有一个误解:以为是大模型直接执行了代码。不是的。大模型没有执行环境,它做的事情很“笨”——它只是从你提供的工具列表里选一个,按你定义的 JSON Schema 生成一个调用参数,然后返回一堆结构化的 JSON,里面写着“我要调用 get_order_stats 这个函数,参数是 { period: '3m' }”。
真正执行这个函数的是你的代码。执行完之后,你把函数返回的结果——不管是一个数字、一个 JSON 还是一个错误提示——作为一条消息传回对话上下文,模型再基于这个结果组织语言回复用户。
这个过程本质上是一个协议:你(开发者)负责执行,模型(大脑)负责决策。模型不关心函数内部是查询数据库还是请求第三方 API,它只关心两件事:这个工具是干什么的、参数要我传什么。这就是为什么工具描述(description)和参数定义(parameters)写得清不清楚,直接决定模型调用得准不准。
1.2 传统提示词方案为什么不行
读到这里你可能会问:我不搞这套协议,直接把需求写在 System Prompt 里,让模型输出“请调用 get_order_stats(3m)”这句话,我再从回答里用正则提取,不行吗?
可以,但非常脆弱。第一,模型可能把函数名写成 get_order_stats(三个月) 或者 getOrderStats(period='3m'),同一个意思三种写法,正则得写多少种匹配才能兜住?第二,多工具场景下,模型可能在一个回答里既想调用 A 又要调用 B,你无法稳定地从自然语言里拆出结构化调用意图。第三,最关键的是,模型的输出是概率性的,同一句话每次的措辞都有细微差异,任何依赖文本格式的约定都等于在沙滩上盖楼。
function calling 协议这套东西,本质上就是把“调用函数”这个动作从“自然语言约定”变成了“结构化协议约定”,模型在训练阶段就见过海量的这种 JSON 格式输出,它不需要你提示也知道该输出什么格式。你只需要把函数定义按它的规范传过去就行。
1.3 为什么用智谱 GLM 而不是 OpenAI
为了让这篇文章的代码大家都能直接跑通,我选了智谱的 GLM-4-Flash 模型,原因有三:第一,它在智谱开放平台上是免费额度,个人开发者注册就有,不用绑卡,非常适合做实验;第二,它原生支持 OpenAI SDK 兼容接口,也就是说你用 from openai import OpenAI 然后改一下 base_url 和 api_key 就能用,代码成本为零;第三,GLM-4 系列对 function calling 的支持比较标准和稳定,我在实际测试中按 OpenAI 格式传 tools 参数时,没有遇到协议上的兼容性问题。
当然,这篇文章的核心逻辑是通用的,你换成 OpenAI 的 GPT、DeepSeek、Qwen 的 API,或者本地部署的 vLLM 服务,只需要改 base_url 和模型名就可以,后面我会专门写一段讲协议格式在不同平台上的差异。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把工具变成 LLM 看得懂的说明书:注册与 Schema 设计
要让模型“知道”有哪些工具可用,你不能把 Python 函数源码直接丢给它——它没有执行环境,也看不懂源码。你需要把每个函数翻译成一份结构化的说明书,这份说明书在 OpenAI 协议里叫 Function Schema,和 API 一起传给模型。
2.1 一个天气预报工具的最小 Schema
假设我们写一个查天气的函数:
python复制def get_current_weather(location: str, unit: str = "celsius"):
"""查询指定城市当前天气"""
# 模拟真实查询
return {"location": location, "temperature": 22, "unit": unit}
这个函数要被模型调用,你需要给它配一份这样的“说明书”:
python复制tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "获取指定城市当前的天气状况,包括温度、天气现象、风力等信息",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称,例如:北京、上海、广州"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,摄氏或华氏"
}
},
"required": ["location"]
}
}
}
]
这段结构看起来啰嗦,但每一个字段都有它的用处。name 是函数名,模型输出时会原样引用这个名字,你的代码靠它来路由(route)到对应的 Python 函数;description 是模型判断“这个工具要不要用”的依据,描述写得越清楚,模型
