上一篇我们让模型提出工具调用请求,但“模型给出的 JSON 能解析”并不等于“参数可以执行”。本篇只解决一个核心问题:应用收到工具调用后,怎样完成参数校验、实际执行和结果回传。示例使用本地订单查询函数,不访问外部系统;把边界掌握清楚后,再替换成数据库或业务服务。

为什么不能直接执行模型参数

工具参数虽然符合 JSON Schema,仍可能存在三类问题。第一类是格式问题,例如缺少 order_id、字段类型错误或多出程序没有设计的字段;第二类是业务问题,例如订单号不存在、用户没有查看权限;第三类是安全问题,例如把一个本应只读的工具改造成执行任意查询。

因此,工具调用应经过一条明确的流水线:解析 JSON → 校验字段和业务规则 → 执行最小权限函数 → 把可序列化的结果或错误回传模型。模型负责提出请求,Python 负责做最后决定。尤其是删除、付款、发邮件等有副作用的操作,不能因为参数“看起来正确”就自动执行。

让 Schema 先挡住明显错误

工具声明是给模型看的第一层约束。Responses API 的 function tool 使用 namedescriptionparameters 描述函数;严格模式下,属性应全部列入 required,并设置 additionalPropertiesFalse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tools = [{
"type": "function",
"name": "get_order_status",
"description": "查询订单状态,仅允许查询当前用户自己的订单。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单号,例如 ORD-1001"
}
},
"required": ["order_id"],
"additionalProperties": False
},
"strict": True
}]

这能减少错误参数,但不能替代服务端校验。Schema 不知道当前用户是谁,也不知道某个订单是否存在;应用必须把这些规则写进真正执行工具的函数中。

用 Python 做第二层校验

下面的工具只接受形如 ORD-数字 的订单号,并用白名单模拟权限检查。使用 json.loads 而不是 eval,因为参数内容来自模型,eval 可能执行任意 Python 表达式。校验失败统一抛出 ToolError,调用方再把错误转成普通 JSON。

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
import json
import re

ORDERS = {
"ORD-1001": {"status": "已发货", "updated_at": "2026-09-07"},
"ORD-1002": {"status": "处理中", "updated_at": "2026-09-08"},
}

class ToolError(Exception):
pass

def get_order_status(arguments: dict, user_id: str) -> dict:
if not isinstance(arguments, dict):
raise ToolError("参数必须是 JSON 对象")
if set(arguments) != {"order_id"}:
raise ToolError("只允许传入 order_id 字段")

order_id = arguments["order_id"]
if not isinstance(order_id, str) or not re.fullmatch(r"ORD-\d{4}", order_id):
raise ToolError("order_id 格式应为 ORD-四位数字")
if order_id not in ORDERS:
raise ToolError("订单不存在或当前用户无权查看")

# 真实系统应在查询条件中同时使用 user_id 做权限过滤。
return {"order_id": order_id, **ORDERS[order_id], "user_id": user_id}

def execute_tool(name: str, raw_arguments: str, user_id: str) -> dict:
if name != "get_order_status":
return {"ok": False, "error": "未知工具"}
try:
arguments = json.loads(raw_arguments)
result = get_order_status(arguments, user_id)
return {"ok": True, "data": result}
except (json.JSONDecodeError, ToolError) as exc:
return {"ok": False, "error": str(exc)}

execute_tool 还有两个值得保留的边界:工具名称不在白名单时不执行;异常被转换为稳定的数据结构,而不是把 Python 堆栈暴露给模型或用户。生产环境还应限制字符串长度、记录调用日志,并在函数内部接入真正的身份和权限系统。

把结果回传给模型

完整闭环需要先发起请求,再执行所有 function_call,最后用对应的 call_id 回传结果。以下程序可以直接运行:先安装 openai,再通过环境变量提供 OPENAI_API_KEYOPENAI_BASE_URLMODEL_NAME 都是可选的。

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
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")
input_items = [{
"role": "user",
"content": "请查询订单 ORD-1002 的状态。",
}]

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, user_id="user-42")
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)

这里不能只把 result["data"] 回传,因为失败时没有这个字段;统一的 okdataerror 结构让模型能分别处理成功和失败。call_id 必须原样对应本次调用,且模型的 response.output 也要一并放回后续输入,否则模型可能丢失工具调用上下文。一个响应可能有多个工具调用,所以示例收集全部结果后再发起下一次请求;若业务只允许一个调用,应在程序侧明确拒绝多余调用。

常见问题

Schema 已经 strict,为什么还要校验? strict 主要约束字段形状,不负责权限、资源存在性和业务范围。它是减少错误的协议,不是安全边界。

工具执行失败要不要直接抛异常? 面向模型的闭环通常应回传可读的失败结果,让模型说明“订单不存在”或请求用户补充信息;但网络故障、程序 bug 等内部异常应记录完整日志,并向外返回经过脱敏的错误。

能否把用户身份交给模型传入? 不应这样做。身份应来自登录会话或服务端上下文,像示例中的 user_id 一样由应用注入,不能信任模型生成的身份字段。

结果回传后模型说了错误内容怎么办? 工具结果只提供事实,最终文字仍由模型生成。金额、权限、状态变更等关键内容应由程序直接展示或再次核验,不能把模型表述当作审计记录。

小结

参数校验不是 Tool Calling 的附属步骤,而是模型与真实系统之间的安全边界。先用 Schema 限定协议,再用 Python 检查类型、白名单、业务规则和权限;工具执行后统一返回成功或失败结果,并用原始 call_id 回传。这样即使模型选择错误工具或生成异常参数,应用也能拒绝执行并保持可诊断。下一篇再讨论多个工具如何选择与错误处理。