工具使用

工具使用(通常称为函数调用)是语言模型突破自身文本框的方式:你给它一份可能要求运行的函数列表,它决定何时使用它们。模型本身不运行任何东西,只是请求调用,你的代码完成实际工作。
模型本身被封闭在一个盒子里。它无法检查今天的天气、在你的数据库中查询用户,或发送电子邮件。它只能从学到的内容预测文本,这些内容在训练时就已冻结,就像 LLM 如何工作 中的那张静止画面。工具使用是让它突破这个盒子的方式,是 agents 构建的基础。
需要记住的关键词是"请求"。模型告诉你它想要哪个函数以及使用什么参数。你的代码运行它并返回结果。你始终保持控制。
循环
工具使用是一个**往返过程**,而不是单一调用:
- 你发送用户的消息加上模型可能使用的工具列表。
- 模型以两种方式之一回复:正常答案,或请求调用工具,带上它想要的参数。
- 如果它请求了工具,你的代码运行该函数并将结果发送回模型。
- 模型使用该结果写出最终答案。
这就是整个模式。模型请求,你的代码运行。 第 2 和 3 步可以重复,如果模型需要多个工具,这就是几章后 agents 的雏形。
实际发生的事情
工具使用可能感觉像模型获得了新能力,但它没有。在幕后,模型做它一直做的一件事:预测文本。关于它所在的盒子,没有任何改变。
当你在请求中包含工具定义时,你是把它们添加到模型预测的上下文中。模型在训练时看过例子,给定像这样的工具,有时正确的延续是一个特殊格式化的消息,意思是"用城市奥斯陆调用 get_weather",而不是正常句子。所以当你的问题让一个工具看起来有用时,最可能的下一个输出是那个结构化的**工具调用消息**,API 作为 tool_calls 呈现给你。
模型没有伸手运行任何东西。它预测了一个工具调用是正确的举动,并以你的代码知道如何读取的格式写了一个请求。
然后你,而不是模型,运行函数。你取真实结果并把它放回消息中作为更多上下文,模型从那个扩大的上下文中预测它的最终答案。整个特性是两个普通的预测,中间由你的代码做真实工作。
你是模型的文本框和真实世界之间的桥梁。 保持这个图景,本章的其余部分,包括 agents,停止感觉神秘:AI 采取的每个行动都是一个预测的请求,你的代码选择执行。
tool_calls 交给你。模型从不运行任何东西,它预测一个请求。你的代码运行函数并反馈结果作为更多上下文用于下一个预测,所以你是模型和真实世界之间的桥梁。 定义工具
你向模型描述每个工具:它的名字、它做什么,以及它接收什么参数。描述不是给你的文档,它是给模型的关于何时使用工具的指令,所以把它写成提示。
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取城市的当前天气。当用户询问天气时使用。",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,例如 '北京'"},
},
"required": ["city"],
},
},
},
]parameters 块是一个**模式**,与 structured output 的想法相同:它定义模型必须产生的参数。当模型决定调用 get_weather 时,它发送回一个匹配这个形状的 city 参数。模糊的描述("获取天气")导致模型在错误的时刻使用工具。清晰的描述("当用户询问天气时使用")很好地引导它。
parameters 模式。描述实际上是一个提示:它告诉模型何时使用工具,所以要仔细写。parameters 模式与结构化输出是相同的机制,约束模型发送的参数。 处理调用
当模型想要一个工具时,回复包含 tool_calls 而不是最终答案。你读取请求的函数和参数,运行真实函数,并作为一个 tool 消息发回结果。然后你再次调用模型这样它可以完成。
import json
# 工具的真实实现
def get_weather(city):
# 在真实应用中这会调用天气 API;这里我们伪造它
return {"city": city, "tempC": 18, "condition": "cloudy"}
messages = [{"role": "user", "content": "北京的天气怎样?"}]
# 1. 第一个调用:提供工具
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
tool_calls = response.choices[0].message.tool_calls
tool_call = tool_calls[0] if tool_calls else None
if tool_call:
# 2. 用模型的参数运行请求的函数
args = json.loads(tool_call.function.arguments)
result = get_weather(args["city"])
# 3. 把模型的请求和你的结果发送回来
messages.append(response.choices[0].message) # 助手的工具请求
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
})
# 4. 再次调用这样模型可以用结果答复
response = client.chat.completions.create(model=MODEL, messages=messages)
print(response.choices[0].message.content)
# "北京目前 18 度,天气阴。"参数以**JSON 字符串**形式到达,所以你 json.loads 它们进入一个真实对象,然后在使用前检查它们。你推送两个消息到历史:模型的工具请求和你的 tool 结果,由 tool_call_id 链接。最后调用把原始天气数据变成一个自然句子。
tool_calls 而不是文本。你从 JSON 字符串解析参数,运行真实函数,推送两个消息:模型的请求和你的结果,由 tool_call_id 链接。最后的模型调用把你的原始结果变成自然答案。 实践中
与可重复使用函数相同的流程。注意唯一站在模型和你系统之间的是你写的并控制的代码:
import json
tool_impls = {"get_weather": lambda args: get_weather(args["city"])}
def answer_with_tools(user_text):
messages = [{"role": "user", "content": user_text}]
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
calls = response.choices[0].message.tool_calls
if not calls:
return response.choices[0].message.content # 模型直接答复
call = calls[0]
args = json.loads(call.function.arguments)
result = tool_impls[call.function.name](args)
messages.append(response.choices[0].message)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
response = client.chat.completions.create(model=MODEL, messages=messages)
return response.choices[0].message.content这处理一个工具调用。一个模型也可以同时请求多个,称为**平行工具调用**,你会通过循环遍历 tool_calls 中的每个条目来处理。如果你让模型在一个循环中保持调用工具直到完成,每次决定自己的下一步,你得到一个 agent,这正是下章去的地方。

