前面的文章里,模型只能返回文字;如果用户问“我所在城市今天的天气”,模型本身既不能访问你的数据库,也不能凭空调用天气服务。Function Calling(也叫 Tool Calling)解决的不是让模型直接执行 Python,而是让模型按照你声明的工具格式提出请求,由你的程序决定是否执行,再把结果交回模型。本篇只完成这个闭环,不讨论多工具编排或 Agent 循环。

模型为什么需要工具

一次普通调用的边界很清楚:程序发送消息,模型生成文本。工具调用则多了一个中间步骤:模型根据问题判断是否需要工具,并生成工具名和 JSON 参数;应用程序收到这个结构后,校验参数、执行真实函数;最后应用把函数结果作为新输入,让模型组织成用户能读懂的回答。

因此,模型不是 Python 函数的执行者,而是工具选择器和参数生成器。真正访问文件、数据库、网络或执行命令的始终是你的应用。这个边界很重要:如果工具具有删数据、转账等副作用,必须在程序侧做权限检查和人工确认,不能因为参数来自模型就直接信任。

声明一个最小工具

以查询天气为例,工具定义包含四部分:类型固定为 functionname 是程序识别的名称,description 帮助模型判断何时使用,parameters 用 JSON Schema 描述参数。严格模式下把参数列入 required,并关闭额外属性,可以减少模型传来未知字段的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import json
import os

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
model_name = os.getenv("MODEL_NAME", "gpt-4o-mini")

tools = [
{
"type": "function",
"name": "get_weather",
"description": "查询指定城市的天气。仅在用户明确询问天气时使用。",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,例如北京"}
},
"required": ["city"],
"additionalProperties": False,
},
"strict": True,
}
]

这里的 get_weather 只是协议名称,函数还没有真正写出来。strict 约束的是参数形状,不代表城市一定存在,也不保证天气数据真实;业务校验仍然要由 Python 完成。

第一次请求:识别工具调用

Responses API 使用 client.responses.create(),输入放在 input,工具放在 tools。模型可能直接给文本,也可能在 response.output 中返回一个 typefunction_call 的项目,所以程序不能只读取 output_text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def get_weather(city: str) -> dict:
# 为了让示例不依赖外部天气服务,先返回一份本地演示数据。
return {"city": city, "condition": "晴", "temperature_c": 23}

input_items = [{
"role": "user",
"content": "请查询北京今天的天气,并告诉我是否适合短时间户外散步。",
}]

response = client.responses.create(
model=model_name,
tools=tools,
input=input_items,
)

for item in response.output:
if item.type != "function_call":
continue
if item.name != "get_weather":
continue

arguments = json.loads(item.arguments)
city = arguments["city"]
weather = get_weather(city)
print("程序实际执行的参数:", city)
print("程序实际得到的结果:", weather)

item.arguments 是 JSON 字符串,必须先用 json.loads 解析。不要使用 eval,因为模型输出是外部输入,eval 可能执行任意 Python 表达式。生产代码还应检查 city 是否为字符串、长度是否合理,以及是否在允许查询的城市范围内。

回传结果,完成闭环

模型第一次响应只是“请调用哪个工具、参数是什么”,还没有最终答案。把模型这次的输出项目追加到输入,再追加一条 function_call_output,其中 call_id 必须对应原来的调用,模型才能知道这份结果属于哪一次请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 保留模型生成的 function_call 项目,随后附上工具结果。
input_items += response.output
input_items.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(weather, ensure_ascii=False),
})

final_response = client.responses.create(
model=model_name,
tools=tools,
input=input_items,
)
print(final_response.output_text)

把两段代码合并后即可运行。完整顺序是:定义工具 → 第一次请求 → 找到调用 → 解析参数 → Python 执行 → 用 call_id 回传 → 第二次请求。实际天气服务只需替换 get_weather 的函数体,模型与回传协议不必改变。

需要注意,示例中的 itemweather 只在找到工具调用后才存在。更稳妥的完整程序应设置一个标志位;如果模型没有调用工具,就直接打印 response.output_text,避免访问未赋值变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
called = False
for item in response.output:
if item.type == "function_call" and item.name == "get_weather":
called = True
arguments = json.loads(item.arguments)
weather = get_weather(arguments["city"])
input_items += response.output
input_items.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(weather, ensure_ascii=False),
})
break

if called:
final_response = client.responses.create(
model=model_name, tools=tools, input=input_items
)
print(final_response.output_text)
else:
print(response.output_text)

常见问题

模型总是不调用工具。 工具描述要说明适用条件,用户问题也要明确;同时确认目标模型支持工具调用。不要把所有业务逻辑都塞进一个含糊的工具描述中。

工具被调用了多次。 一次响应可能包含多个 function_call,不能假设永远只有一个。初学时可以逐个执行并逐个回传;后续再学习并行工具调用。若只允许一次,应在程序侧设置上限。

参数合法但业务不合法。 JSON Schema 只能约束类型和字段。城市不存在、用户无权限、查询次数超限等情况都要在函数内部处理,并把可读的错误结果回传,而不是让异常直接终止整个对话。

把工具结果当成模型生成内容。 工具结果应来自你的程序或可信服务,但模型接下来仍会重新组织它。涉及金额、权限和写操作时,结果展示前要记录日志,执行前要二次确认。

接口返回普通文本。 这是允许的分支。工具只是模型可选的能力,不是每个问题都必须调用;应用必须同时处理 function_call 和普通文本两种响应。

小结

Tool Calling 的核心不是让模型直接运行代码,而是建立一套可追踪的请求协议:模型提出函数名和 JSON 参数,应用校验并执行,应用再用对应的 call_id 回传结果。掌握这个最小闭环后,才适合继续学习参数校验、工具错误处理和多个工具之间的选择。