假设用户问助手“明天的天气怎样”,而模型自身没有最新天气。应用可以提供一个查询天气的工具,让模型提出查询请求,再把实时结果交回给模型。这个过程把模型的语言能力与外部系统连接起来,也是工具调用的基本用途。

本课接在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 页。执行器练习已运行,真实模型与工具的端到端流程尚未测试。