上一篇我们让模型提出工具调用请求,但“模型给出的 JSON 能解析”并不等于“参数可以执行”。本篇只解决一个核心问题:应用收到工具调用后,怎样完成参数校验、实际执行和结果回传。示例使用本地订单查询函数,不访问外部系统;把边界掌握清楚后,再替换成数据库或业务服务。
为什么不能直接执行模型参数
工具参数虽然符合 JSON Schema,仍可能存在三类问题。第一类是格式问题,例如缺少 order_id、字段类型错误或多出程序没有设计的字段;第二类是业务问题,例如订单号不存在、用户没有查看权限;第三类是安全问题,例如把一个本应只读的工具改造成执行任意查询。
因此,工具调用应经过一条明确的流水线:解析 JSON → 校验字段和业务规则 → 执行最小权限函数 → 把可序列化的结果或错误回传模型。模型负责提出请求,Python 负责做最后决定。尤其是删除、付款、发邮件等有副作用的操作,不能因为参数“看起来正确”就自动执行。
让 Schema 先挡住明显错误
工具声明是给模型看的第一层约束。Responses API 的 function tool 使用 name、description 和 parameters 描述函数;严格模式下,属性应全部列入 required,并设置 additionalProperties 为 False:
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("订单不存在或当前用户无权查看")
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_KEY;OPENAI_BASE_URL 和 MODEL_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"] 回传,因为失败时没有这个字段;统一的 ok、data、error 结构让模型能分别处理成功和失败。call_id 必须原样对应本次调用,且模型的 response.output 也要一并放回后续输入,否则模型可能丢失工具调用上下文。一个响应可能有多个工具调用,所以示例收集全部结果后再发起下一次请求;若业务只允许一个调用,应在程序侧明确拒绝多余调用。
常见问题
Schema 已经 strict,为什么还要校验? strict 主要约束字段形状,不负责权限、资源存在性和业务范围。它是减少错误的协议,不是安全边界。
工具执行失败要不要直接抛异常? 面向模型的闭环通常应回传可读的失败结果,让模型说明“订单不存在”或请求用户补充信息;但网络故障、程序 bug 等内部异常应记录完整日志,并向外返回经过脱敏的错误。
能否把用户身份交给模型传入? 不应这样做。身份应来自登录会话或服务端上下文,像示例中的 user_id 一样由应用注入,不能信任模型生成的身份字段。
结果回传后模型说了错误内容怎么办? 工具结果只提供事实,最终文字仍由模型生成。金额、权限、状态变更等关键内容应由程序直接展示或再次核验,不能把模型表述当作审计记录。
小结
参数校验不是 Tool Calling 的附属步骤,而是模型与真实系统之间的安全边界。先用 Schema 限定协议,再用 Python 检查类型、白名单、业务规则和权限;工具执行后统一返回成功或失败结果,并用原始 call_id 回传。这样即使模型选择错误工具或生成异常参数,应用也能拒绝执行并保持可诊断。下一篇再讨论多个工具如何选择与错误处理。