上一篇已经完成了一个工具的参数校验和结果回传。真实应用通常不只有一个能力:用户可能要查订单、查询库存,也可能只是询问普通知识。此时应用不能把所有请求都交给一个函数,而要处理模型选择多个工具、工具不存在或执行失败等分支。本篇只聚焦“多个工具如何安全分发”,使用本地数据演示,代码仍通过 OpenAI Python SDK 调用模型。

多工具调用的基本流程

给模型的 tools 是一个列表,每项描述一个可调用的函数。模型收到用户问题后,可能直接返回文本,也可能返回一个或多个 function_call 项目。应用侧要完成四件事:根据名称从白名单中找到函数,解析并校验参数,执行函数,把每个结果用对应的 call_id 回传。

这里有两个容易忽略的事实。第一,模型选择工具只是建议,真正的执行权在 Python 程序;第二,一次响应中可能出现多个调用,因此不能只读取第一个项目。程序应先收集全部结果,再发起下一次请求,让模型综合这些结果。

声明两个小工具

下面准备一个订单状态查询和一个库存查询。工具描述要写清适用场景,参数 Schema 则限定字段形状;strict 可以减少模型生成额外字段,但业务校验仍然必须存在。

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
30
31
32
33
34
35
36
import json
import os
from openai import OpenAI

client = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
base_url=os.getenv("OPENAI_BASE_URL"),
)
model = os.getenv("MODEL_NAME", "gpt-4o-mini")

tools = [
{
"type": "function",
"name": "get_order_status",
"description": "查询订单状态。用户询问订单进度时使用。",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
"additionalProperties": False,
},
"strict": True,
},
{
"type": "function",
"name": "get_stock",
"description": "查询商品库存。用户询问某商品是否有货时使用。",
"parameters": {
"type": "object",
"properties": {"sku": {"type": "string"}},
"required": ["sku"],
"additionalProperties": False,
},
"strict": True,
},
]

name 是协议中的稳定标识,不是模型要执行的 Python 表达式。不要根据模型返回的名称拼接模块名或使用 eval;应用只应该从自己维护的映射表中选择已允许的函数。

用白名单分发调用

先实现两个没有外部依赖的函数,再把名称映射到函数。示例中的字典模拟数据库,函数内部仍检查参数类型和长度。真实项目应把身份、权限和数据过滤放在服务端上下文中,不能让模型通过参数伪造用户身份。

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
30
31
32
33
34
ORDERS = {"ORD-1001": "已发货", "ORD-1002": "处理中"}
STOCK = {"BOOK-001": 12, "PEN-002": 0}

class ToolError(Exception):
pass

def get_order_status(arguments):
order_id = arguments.get("order_id")
if not isinstance(order_id, str) or len(order_id) > 32:
raise ToolError("order_id 格式不正确")
return {"order_id": order_id, "status": ORDERS.get(order_id, "不存在")}

def get_stock(arguments):
sku = arguments.get("sku")
if not isinstance(sku, str) or not sku or len(sku) > 32:
raise ToolError("sku 格式不正确")
return {"sku": sku, "quantity": STOCK.get(sku, 0)}

DISPATCH = {
"get_order_status": get_order_status,
"get_stock": get_stock,
}

def execute_tool(name, raw_arguments):
function = DISPATCH.get(name)
if function is None:
return {"ok": False, "error": "工具未被应用允许"}
try:
arguments = json.loads(raw_arguments)
if not isinstance(arguments, dict):
raise ToolError("参数必须是 JSON 对象")
return {"ok": True, "data": function(arguments)}
except (json.JSONDecodeError, ToolError, TypeError) as exc:
return {"ok": False, "error": str(exc)}

execute_tool 统一返回 okdataerror,这样模型可以知道查询成功还是失败。生产代码还应限制单次响应可执行的调用数量,并为每次调用记录工具名、耗时和脱敏后的结果。对于写数据、付款等有副作用的工具,应该在分发前增加权限检查和人工确认,而不是自动执行。

收集多个结果并回传

下面是完整的调用部分。第一次响应可能没有工具调用;有工具调用时,要把原始的 response.output 和所有 function_call_output 一起作为下一次输入。每个结果都通过自己的 call_id 与请求配对。

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
30
31
input_items = [{
"role": "user",
"content": "请告诉我订单 ORD-1002 的状态,以及商品 BOOK-001 的库存。",
}]

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

call_outputs = []
for item in response.output:
if item.type != "function_call":
continue
result = execute_tool(item.name, item.arguments)
call_outputs.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(result, ensure_ascii=False),
})

if call_outputs:
final_response = client.responses.create(
model=model,
tools=tools,
input=input_items + response.output + call_outputs,
)
print(final_response.output_text)
else:
print(response.output_text)

这段程序只执行白名单里的只读函数。若模型一次提出订单和库存两个调用,循环会分别执行,再一次性回传;模型随后可以把两个事实组织成一段回答。若某个工具失败,另一个工具的成功结果仍可保留,模型可以明确说明哪一项查询失败,而不是让整个程序因一个异常中断。

常见问题

为什么模型总选错工具? 检查工具描述是否有清晰的适用条件,名称和参数是否表达业务含义。必要时在系统提示中说明“无法确定时先向用户澄清”,但不要只依赖提示词,应用仍要拒绝未知工具。

为什么不能只回传第一个结果? 一次响应可能包含多个调用。漏掉其他调用会使模型缺少上下文,甚至在下一轮重复请求。应遍历全部输出,并为每个 call_id 生成一条回传项。

工具失败时应该抛出异常吗? 可预期的业务失败应转换为脱敏的结构化错误,让模型向用户解释或请求补充信息。程序错误、认证失败和网络故障要写入日志;不要把堆栈、令牌或内部地址直接回传。

是否应该强制每次都调用工具? 不应该。普通问候和不需要实时数据的问题可以直接回答。应用应同时处理普通文本和工具调用两种分支,并对工具调用设置循环次数上限,避免异常情况下无限请求。

小结

多工具 Tool Calling 的核心是“模型提议,应用裁决”:用描述帮助模型选择,用白名单决定是否允许,用参数校验保护函数,用统一结果结构处理成功和失败,再按 call_id 回传全部结果。掌握这个分发闭环后,下一步可以学习多个工具之间更复杂的选择与工作流控制,但权限、日志和停止条件始终要由应用掌握。