工具使用


工具使用(通常称为函数调用)是语言模型突破自身文本框的方式:你给它一个可能会请求运行的函数列表,它决定何时使用它们。模型本身永远不会运行任何东西,它只会请求调用,而你的代码才是做实际工作的。
模型本身被封闭在一个盒子里。它无法检查今天的天气、查找数据库中的用户,或发送电子邮件。它只是根据训练时学到的内容预测文本,这是大语言模型如何工作中相同的静态画面。工具使用就是让它超越盒子的方式,也是智能体构建的基础。
要记住的关键词是"请求"。模型告诉你它想要哪个函数以及什么参数。你的代码运行它并返回结果。你始终保持控制权。
循环
工具使用是一个**往返交互**,而不是单一调用:
- 你发送用户的消息加上一个工具列表,模型可以使用这些工具。
- 模型以两种方式之一回复:一个正常的答案,或一个工具调用请求,附带它想要的参数。
- 如果它请求了工具,你的代码运行那个函数并把结果发送回模型。
- 模型使用结果来编写它的最终答案。
这就是全部模式。模型请求,你的代码运行。第 2 和 3 步可以重复,如果模型需要多个工具,这就是几章后智能体的雏形。
真正发生了什么
工具使用可能感觉像模型获得了新的力量,但它没有。在底层,模型做它总是做的一件事:预测文本。它生活的盒子没有任何改变。
当你在请求中包含工具定义时,你是在把它们添加到模型预测的上下文中。模型在训练时接触了例子,当给定这样的工具时,正确的延续有时是一个特殊格式的消息,意思是"调用 get_weather,参数为 Beijing",而不是一个普通的句子。所以当你的问题使一个工具看起来有用时,最可能的下一个输出是那个结构化的**工具调用消息**,API 把它以 tool_calls 的形式呈现给你。
模型没有伸出去运行任何东西。它预测了工具调用是正确的举动,并以你的代码知道如何读取的格式写了一个请求。
然后是你,不是模型,运行函数。你取得真实结果并把它放回消息中作为更多上下文,模型从那个扩大的上下文预测它的最终答案。整个功能是两个普通的预测,你的代码在中间做真实的工作。
你是模型的文本框和真实世界之间的桥梁。持有这个画面,本章的其余部分,包括智能体,停止感觉神秘:AI 采取的每个行动都是一个预测的请求,你的代码选择执行。
tool_calls 的形式交给你。模型不运行任何东西,它预测一个请求。你的代码运行函数并把结果作为更多上下文反馈给下一个预测,所以你是模型和真实世界之间的桥梁。 定义工具
你向模型描述每个工具:它的名称、它做什么,以及它接受的参数。描述不是给你的文档,它是给模型关于何时使用工具的指令,所以像写提示那样写它。
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取城市的当前天气。在用户询问天气时使用。",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,如 '北京'"},
},
"required": ["city"],
},
},
},
]parameters 块是一个**架构**,与结构化输出的概念相同:它定义了模型必须产生的参数。当模型决定调用 get_weather 时,它发送回一个与这个形状匹配的 city 参数。模糊的描述("获取天气")导致模型在错误的时刻使用工具。清楚的描述("在用户询问天气时使用")可以很好地引导它。
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 中的每个条目来处理。如果你让模型在一个循环中持续调用工具直到完成,每次决定自己的下一步,你就得到了一个智能体,这正是下一章走的地方。

