上一篇已经完成了一个工具的参数校验和结果回传。真实应用通常不只有一个能力:用户可能要查订单、查询库存,也可能只是询问普通知识。此时应用不能把所有请求都交给一个函数,而要处理模型选择多个工具、工具不存在或执行失败等分支。本篇只聚焦“多个工具如何安全分发”,使用本地数据演示,代码仍通过 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 统一返回 ok、data 或 error,这样模型可以知道查询成功还是失败。生产代码还应限制单次响应可执行的调用数量,并为每次调用记录工具名、耗时和脱敏后的结果。对于写数据、付款等有副作用的工具,应该在分发前增加权限检查和人工确认,而不是自动执行。
收集多个结果并回传
下面是完整的调用部分。第一次响应可能没有工具调用;有工具调用时,要把原始的 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 回传全部结果。掌握这个分发闭环后,下一步可以学习多个工具之间更复杂的选择与工作流控制,但权限、日志和停止条件始终要由应用掌握。