假设用户问助手“明天的天气怎样”,而模型自身没有最新天气。应用可以提供一个查询天气的工具,让模型提出查询请求,再把实时结果交回给模型。这个过程把模型的语言能力与外部系统连接起来,也是工具调用的基本用途。
本课接在RAG 问答之后。RAG 主要帮助系统取得资料,工具调用还可以让系统执行具体动作,因此应用需要同时考虑参数、权限与执行结果。
一次调用怎样往返
一次典型调用由应用开始:应用先提供工具名称、用途和参数格式,模型再提出结构化请求。执行层解析请求并检查权限,工具执行后返回结果,应用最后把调用标识与结果交回上下文,或直接显示给用户。
客户端自定义工具通常在应用侧执行,服务端托管工具则由服务方执行。开发者需要先知道执行发生在哪里,才能正确安排权限和错误处理。工具调用概述
我们可以把应用侧的处理分成五步,逐步理解它怎样接收和执行请求:
| 步骤 | 要做的事 |
|---|---|
| 接收 | 检查调用 ID、工具名与参数是否完整 |
| 解析 | 读取 JSON 等数据,不执行参数中的代码 |
| 格式检查 | 核对类型、范围、必填字段和额外字段 |
| 授权 | 确认当前用户能操作对应资源 |
| 执行 | 控制超时、重试和结果状态 |
格式检查负责判断数据形状是否符合要求。例如,JSON Schema 的 properties 默认允许额外字段,开发者还需要显式声明必填项和额外字段规则。格式通过后,授权逻辑继续判断资源归属、路径和 URL 等业务条件,两部分分别承担不同责任。对象约束
写一个只读执行器
下面的程序提供一个本地天气表。我们用固定数据演示工具允许列表、参数检查和重复调用处理,因此读者无需连接外部账号,也能看清一次请求怎样被接受或拒绝。
import json
WEATHER = {"demo-city": {"temperature_c": 18, "source": "local fixture"}}
completed = {}
def execute_readonly(call):
if not isinstance(call, dict) or set(call) != {"id", "name", "arguments"}:
raise ValueError("unexpected call fields")
call_id = call["id"]
if not isinstance(call_id, str) or not 1 <= len(call_id) <= 64:
raise ValueError("invalid call ID")
if call["name"] != "read_demo_weather":
raise ValueError("tool not allowed")
if not isinstance(call["arguments"], str):
raise ValueError("serialized arguments required")
args = json.loads(call["arguments"])
if not isinstance(args, dict) or set(args) != {"city"}:
raise ValueError("only city accepted")
city = args["city"]
if not isinstance(city, str) or city not in WEATHER:
raise ValueError("unknown city")
fingerprint = (call["name"], city)
if call_id in completed:
previous, result = completed[call_id]
if previous != fingerprint:
raise ValueError("call ID reused with different arguments")
return result
result = {"call_id": call_id, "ok": True, "data": dict(WEATHER[city])}
completed[call_id] = (fingerprint, result)
return result
request = {"id": "demo-1", "name": "read_demo_weather",
"arguments": json.dumps({"city": "demo-city"})}
answer = execute_readonly(request)
assert answer["data"]["temperature_c"] == 18
assert execute_readonly(request) == answer
rejected = 0
for bad in ({**request, "name": "delete_server"},
{**request, "arguments": json.dumps({"city": "demo-city", "admin": True})},
{**request, "arguments": json.dumps({"city": "unknown-city"})}):
try:
execute_readonly(bad)
except ValueError:
rejected += 1
assert rejected == 3
assert len(completed) == 1
print("one result, one replay, three rejected requests")
代码接受一次合法请求,并在相同调用 ID 再次出现时返回已有结果。它还拒绝了未知工具、多余参数和未知城市。上面的字典只在进程存活期间保存状态,生产应用还需要认证、输入长度限制、并发控制和持久化,才能在重启与多请求场景下可靠工作。
超时以后先查状态
天气查询重复一次,通常只多消耗一些资源;发送邮件或创建订单重复一次,则可能产生真实副作用。应用可以为操作保存幂等键和执行状态:确认操作尚未执行时,程序才考虑重试;操作已经成功时,程序返回已有结果;状态不明时,程序先查询或对账。
开发者还应限制调用轮数、总时间和费用,防止错误流程持续运行。工具返回的网页与文档可能带有诱导文字,执行层仍然只能按用户原来的授权办事。日志可以记录调用和错误,但不应记录密钥。
练习:读者可以给示例增加一个只读工具,并为多余字段、错误类型和重复 ID 编写测试。随后,请为“发送通知”设计一张状态表,说明程序超时后怎样避免重复发送;这个练习先完成设计,无需连接真实通知服务。
下一篇进入训练路线:SFT、LoRA 与 DPO。
资料:疏锦行大模型专题 Tool Calling 补充讲义 14 页。执行器练习已运行,真实模型与工具的端到端流程尚未测试。
大模型 04:让模型调用工具之前,要准备什么
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法