手把手驯服 Agent 的工具调用:Function Calling 原理、实战与生产避坑

Agent 0 次阅读
手把手驯服 Agent 的工具调用:Function Calling 原理、实战与生产避坑

你的 Agent 是不是经常"嘴上答应调用工具,实际却编了一个答案"?或者调了工具却把嵌套 JSON 传成了字符串,让下游 API 直接 400?本文用一个电商客服 Agent 的真实场景,把 Tool Calling / Function Calling 从底层约定、五步闭环到生产级防坑,一次讲透。

开篇:从一个真实业务场景说起

你在做一个电商 AI 客服 Agent。用户问:"帮我查一下明天上海适不适合跑步,顺便看看有没有便宜一点的户外外套。"

你的第一版实现很简单:把这个问题丢给大模型,让它在 System Prompt 里"按固定格式输出"。结果模型一会儿输出 ACTION: get_weather(上海),一会儿输出 {"action": "weather", "city": "上海, 明天"},一会儿干脆用自然语言说"我建议您查询天气预报"。你用正则去抠,解析失败率居高不下,Agent 三天两头答非所问。

后来你换成了厂商原生的 Function Calling(函数调用)能力:把 get_weathersearch_products 两个工具的 JSON Schema 声明给模型,模型乖乖返回结构化的 tool_calls,参数还是解析好的 JSON。问题似乎解决了——直到生产环境把你按在地上摩擦:模型在该并行调用时串行、把嵌套对象序列化成字符串、工具超时后整个 Agent 卡死、同一个 Schema 换一家模型就翻车……

这不是你的问题,是工具调用这件事本身,远比"把工具声明给模型"复杂得多。本文就围绕这五个问题展开:模型到底是怎么"调用"工具的?工具 Schema 该怎么设计?并行、重试、校验这些工程细节怎么落地?多模型怎么选型?生产怎么监控? 看完你就能写出一个稳定、可控、可观测的工具调用层。

技术背景与核心概念扫盲

一句话理解工具调用

大模型本质是一个文本生成器(LLM, Large Language Model),它根本不会执行任何函数、发不出任何 HTTP 请求。 所谓"Agent 调用了天气 API",真实发生的事情是:

  1. 你把工具的描述(JSON Schema)随请求一起发给模型;
  2. 模型在需要时,生成一段结构化文本——"我想调用 get_weather,参数是 city=北京";
  3. 这段文本被 API 包装成专门的 tool_calls 字段返回;
  4. 你的代码负责真正执行这个函数,把结果再以 tool 消息喂回给模型;
  5. 模型看到工具结果,生成最终回答。

这个"模型负责决策(调什么、传什么参数),你的代码负责执行(真的去调)"的分工,是理解后面一切问题的基石。字节跳动团队在 Agent 工具设计实践中反复强调同一件事:工具调用的难点不在工具本身,而在"决策"——模型什么时候调、调哪个、顺序是什么、缺信息时要不要追问。

技术演进:从正则硬解析到一等公民

工具调用机制经历了三个阶段(这也是理解各家 API 差异的钥匙):

  • 阶段一:纯 Prompt 硬解析(2022 及更早)。模型没有工具概念,开发者只能在提示词里写"如果需要查天气,请严格按 ACTION: get_weather(城市名) 输出",然后用正则去抠。模型多打个空格、把中文括号写成英文、甚至用自然语言绕开格式,解析就崩。这是早期 Agent 不稳定的主因。
  • 阶段二:原生 Function Calling(2023 年中,OpenAI 首推)。厂商把"工具"变成 API 的一等公民:请求里用专门的 tools 字段传 JSON Schema,模型返回时用专门的 tool_calls 字段给出结构化调用意图,参数已是解析好的 JSON。模型为此做了专门微调,格式遵循度大幅提升。Anthropic 的 Tool Use、Google 的 Function Calling、阿里云百炼的 Function Calling 都是同一思路的平行实现。
  • 阶段三:协议化与代码编排(2024 至今)。MCP(Model Context Protocol,模型上下文协议)把工具声明、发现、调用标准化成统一协议;OpenAI 在 2025 年 11 月又推出 Programmatic Tool Calling(PTC,程序化工具调用),让模型直接生成 Python/JavaScript 代码来编排工具调用,把循环、分支、聚合从"多轮 API 往返"压缩成"一段代码执行"。

核心概念速查

概念 英文 一句话说明
工具调用 Tool Calling / Function Calling 模型输出结构化调用意图,由应用执行
工具 Schema Tool Schema 用 JSON Schema 描述工具的名称、描述、参数
工具选择 tool_choice 控制模型何时必须/禁止调用工具
并行工具调用 Parallel Tool Calls 单轮响应返回多个工具调用,应用并发执行
工具消息 Tool Message 把工具执行结果以结构化消息回传给模型
结构化输出 Structured Outputs 强制模型输出符合指定 JSON Schema 的结果

底层原理深度拆解

五步闭环:工具调用是怎么发生的

以 OpenAI Responses API(2026 年官方推荐的新一代端点,取代旧的 Chat Completions API 与即将于 2026 年中期停用的 Assistants API)为例,官方把工具调用流程定义为五个高层步骤:

flowchart LR
    A[1. 请求: input + tools 工具声明] --> B[2. 响应: 模型返回 tool_calls<br/>函数名 + 参数JSON]
    B --> C[3. 应用侧执行工具<br/>调用真实函数/API]
    C --> D[4. 二次请求: 携带 tool 消息<br/>回传执行结果]
    D --> E{5. 模型再决策}
    E -->|还有工具要调| B
    E -->|完成| F[最终自然语言回复]

图 1:工具调用五步闭环(OpenAI 官方流程的工程化示意)。注意第 3 步永远发生在你的代码里,模型只负责第 2 步和第 5 步的"决策"。

这一步拆解很重要,因为它揭示了三个新手最容易忽略的真相:

  1. 工具调用是多轮对话,不是一次请求。一次工具调用至少要两次 API 往返(第一次拿到 tool_calls,第二次带着工具结果回去)。如果模型连续调了 3 个工具,就是 4 次往返。这也是为什么工具执行耗时占 Agent 总延迟的 35%–60%(编码类任务偏高,研究类居中)。
  2. 消息历史必须完整回传。第二次请求时,必须把模型上一条 assistant 消息(含 tool_calls)和工具执行结果按顺序都带上,否则模型不知道"这个结果对应我之前的哪个调用"。OpenAI 用 tool_call_id 建立这种对应关系。
  3. 结果回传的质量直接决定后续决策质量。工具返回空字符串、返回一堆无关日志、返回异常堆栈,模型就会"看不懂",进而产生幻觉或死循环。阿里云百炼的官方文档专门提醒:工具执行是写操作(发邮件、传文件)时,建议返回状态描述信息(如"邮件发送完成")而非空结果,帮助模型理解执行状态。

模型端到底发生了什么

模型之所以能稳定输出符合 Schema 的 tool_calls,是因为厂商对这些模型做了专门的指令微调(Instruction Tuning):训练数据里包含"自然语言问题 → 工具调用 JSON"的配对样本。所以"格式遵循度"本质上是模型能力的一部分,不同模型差异巨大(后面性能实测部分会给出量化数据)。

模型端的关键参数是 tool_choice,它定义了模型调用工具的"自由度":

tool_choice 取值 含义 生产建议
auto 模型自己决定是否调用、调哪个 默认值,通用场景
none 禁止调用任何工具 意图分类、纯对话场景
required 必须调用工具(可从多个中选) 关键链路兜底,防止模型跳过工具
{"type":"function","function":{"name":"xxx"}} 强制调用指定工具 路由已明确的场景

腾讯云 900 用例横评里有个真实教训:Claude 在 auto 模式下约 3% 的概率会跳过工具直接文本回复,生产建议就是关键链路用 required 兜底。

工具注册表与 ReAct 循环:框架层怎么组织

在 OpenHands(开源编码 Agent 框架)里,工具调用不是散落的代码,而是围绕"动作 → 执行 → 观察"三层抽象构建的:

flowchart TB
    LLM[大模型] -->|生成 tool_calls JSON| P[Pydantic 校验]
    P -->|Action 对象| CTRL[AgentController 调度]
    CTRL --> RT[Runtime 执行]
    RT -->|执行结果| OBS[Observation 结构化反馈]
    OBS -->|回传结果| LLM
    REG[工具注册表 ToolRegistry] -.注册/查询.-> RT
    MCP[MCP 工具] -.统一接入.-> REG
    API[外部 API] -.封装.-> REG

图 2:Agent 框架中的工具调用架构(以 OpenHands 为例)。工具注册表(ToolRegistry)实现工具与 LLM 的解耦,LLM 只通过注册表感知工具的"声明信息",执行由调度层完成。

这套架构背后是 ReAct(Reasoning + Acting,推理与行动交替)循环:模型在推理中意识到"内部知识不足以支撑下一步决策",就调用工具获取外部事实,再基于结果继续推理。设计要点:

  • 工具抽象与标准化:无论底层是函数、API、数据库还是另一个 Agent,都封装为"名称 + 用途描述 + 参数 Schema + 返回值格式"的统一对象。
  • 结构化交互:LLM 与框架之间用 JSON 而非自然语言交互,避免歧义。
  • 结果闭环:工具结果必须完整回传,形成"请求-决策-调用-反馈-再决策"的反思式推理。

手把手实战落地

下面我们用一个"订单查询 + 物流跟踪"客服 Agent 走通全流程。环境:Python 3.11+,openai>=2.25.0,模型 gpt-5.6(Responses API)。

第一步:环境准备与工具定义

# 安装最新版 OpenAI SDK(2026-08 当前稳定版本要求 openai>=2.x)
pip install -U openai pydantic tenacity
# 配置 API Key(生产环境请用环境变量或密钥管理服务,切勿硬编码)
export OPENAI_API_KEY="sk-..."

定义工具是整个链路中投入产出比最高的一环。字节跳动团队的建议是:用 Python 类型系统 + Pydantic 自动生成 Schema 并做数据校验,把 50% 的时间花在打磨 Docstring 上——因为 LLM 是"读说明书用工具"的,它不看你代码实现,只看你的描述。

from pydantic import BaseModel, Field
from openai import OpenAI

client = OpenAI()

# 工具1:查订单 —— 参数用类型注解 + Field 约束,自动生成 JSON Schema
def get_order_status(order_id: str = Field(description="订单号,如 ORD-20260801-001")) -> str:
    """
    根据订单号查询订单当前状态。

    返回 JSON 字符串,包含字段:status(处理中/已发货/已签收)、
    courier(快递公司)、tracking_no(运单号)。
    """
    # 真实项目中这里调用订单系统 API
    return '{"status": "已发货", "courier": "顺丰", "tracking_no": "SF1234567890"}'

# 工具2:查物流轨迹
def get_tracking(tracking_no: str = Field(description="快递运单号")) -> str:
    """
    根据运单号查询最新物流轨迹。

    返回 JSON 字符串,包含字段:location(最新位置)、time(更新时间)、
    description(物流描述)。若运单号不存在返回 {"error": "not_found"}。
    """
    return '{"location": "上海转运中心", "time": "2026-08-10 06:00", "description": "快件已到达【上海转运中心】"}'

为什么返回值用 JSON 字符串而不是直接 return dict? Responses API 的 function tool 输出会作为文本注入上下文,结构化字符串(JSON)既能让模型精确理解,又能控制 Token 开销。返回纯文本描述也是允许的,但明确的结构化更利于模型做后续决策。

第二步:用 Responses API 完成一轮完整工具调用

OpenAI 官方推荐新项目直接用 client.responses.create。下面这段代码演示五步闭环的最小实现:

import json

# 步骤1:把两个工具组装成 tools 声明列表
tools = [
    {
        "type": "function",
        "name": "get_order_status",
        "description": "根据订单号查询订单当前状态",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "订单号,如 ORD-20260801-001"}
            },
            "required": ["order_id"],
            "additionalProperties": False,  # 禁止模型塞多余参数
        },
        "strict": True,  # 开启严格模式,强制输出符合 Schema 的 JSON
    },
    {
        "type": "function",
        "name": "get_tracking",
        "description": "根据运单号查询最新物流轨迹",
        "parameters": {
            "type": "object",
            "properties": {
                "tracking_no": {"type": "string", "description": "快递运单号"}
            },
            "required": ["tracking_no"],
            "additionalProperties": False,
        },
        "strict": True,
    },
]

# 步骤1:发送请求,携带工具声明
response = client.responses.create(
    model="gpt-5.6",
    input="我的订单 ORD-20260801-001 发货了吗?到哪了?",
    tools=tools,
    tool_choice="auto",          # 模型自主决定是否调用
    parallel_tool_calls=True,    # 允许并行调用(默认即为 True)
)

# 步骤2:解析模型返回的 tool_calls
output = response.output
for item in output:
    if item.type == "function_call":
        print(f"模型要调用: {item.name}")
        print(f"参数: {item.arguments}")

        # 步骤3:应用侧执行真实函数(这里按名字分发)
        if item.name == "get_order_status":
            args = json.loads(item.arguments)
            result = get_order_status(args["order_id"])
        elif item.name == "get_tracking":
            args = json.loads(item.arguments)
            result = get_tracking(args["tracking_no"])
        else:
            result = '{"error": "unknown_tool"}'

        # 步骤4:携带工具结果发起第二次请求
        response2 = client.responses.create(
            model="gpt-5.6",
            input=[
                *response.output,          # 必须回传上一条完整输出(含 function_call)
                {
                    "type": "function_call_output",
                    "call_id": item.call_id,  # 用 call_id 关联"哪个调用对应哪个结果"
                    "output": result,
                },
            ],
            tools=tools,
        )

        # 步骤5:打印模型基于工具结果的最终回答
        print("最终回答:", response2.output_text)

运行输出类似:

模型要调用: get_order_status
参数: {"order_id": "ORD-20260801-001"}
模型要调用: get_tracking
参数: {"tracking_no": "SF1234567890"}
最终回答: 您的订单 ORD-20260801-001 已发货(顺丰,运单号 SF1234567890),
最新轨迹显示快件已于 2026-08-10 06:00 到达上海转运中心。

第三步:把手工流程封装成通用的工具循环

上面的代码手写分发逻辑,工具多了就乱。封装成一个通用 run_agent_with_tools 循环,支持多轮、并行、自动分发:

import asyncio, json
from typing import Callable, Awaitable

# 工具注册表:name -> (定义, 执行函数)
TOOL_REGISTRY: dict[str, tuple[dict, Callable]] = {
    "get_order_status": (tools[0], get_order_status),
    "get_tracking": (tools[1], get_tracking),
}

async def run_agent_loop(user_input: str, max_rounds: int = 5) -> str:
    """通用工具循环:发请求 → 执行工具 → 回传结果 → 直到模型不再调用工具"""
    current_input = user_input

    for _ in range(max_rounds):  # 限制轮数,防止 Agent 死循环烧钱
        resp = client.responses.create(
            model="gpt-5.6", input=current_input, tools=tools,
        )
        calls = [i for i in resp.output if i.type == "function_call"]
        if not calls:
            return resp.output_text          # 没有工具调用,返回最终答案

        # 并发执行所有工具(并行工具调用落地)
        results = await asyncio.gather(*[
            execute_tool(c.name, c.arguments) for c in calls
        ])

        # 组装下一轮输入:完整回传 assistant 输出 + 每个调用的结果
        current_input = [
            *resp.output,
            *[
                {"type": "function_call_output", "call_id": c.call_id, "output": r}
                for c, r in zip(calls, results)
            ],
        ]
    raise TimeoutError(f"超过 {max_rounds} 轮仍未结束,触发熔断")

async def execute_tool(name: str, arguments: str) -> str:
    """按注册表分发并执行工具,带参数解析兜底"""
    if name not in TOOL_REGISTRY:
        return '{"error": "unknown_tool", "message": f"工具 {name} 不存在"}'
    _, func = TOOL_REGISTRY[name]
    try:
        args = json.loads(arguments)
        return func(**args)   # 解包参数调用真实函数
    except TypeError as e:
        return f'{{"error": "invalid_args", "detail": "{e}"}}'

第四步:工具执行的超时与重试

工具调用的是真实外部系统,网络抖动、下游 5xx 是家常便饭。没有超时保护,一个慢接口能拖死整个 Agent;没有重试,一次抖动就导致用户看到"系统繁忙"。OpenAI Agents SDK 为 function tool 提供了内置的 timeout 参数,配合 tenacity 做指数退避重试:

import asyncio
from tenacity import (
    retry, stop_after_attempt, wait_exponential,
    retry_if_exception_type,
)

class ToolTimeoutError(Exception):  # 自定义超时异常,便于分类处理
    pass

# 指数退避重试:最多 3 次,等待时间 1s -> 2s -> 4s(上限 10s)
@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, max=10),
    retry=retry_if_exception_type((TimeoutError, ConnectionError)),
)
async def call_tracking_api(tracking_no: str) -> str:
    """模拟真实物流 API:可能超时或连接失败"""
    await asyncio.sleep(0.1)   # 模拟网络 IO
    # 真实项目:async with httpx.AsyncClient(timeout=5) as client: ...
    return '{"location": "上海转运中心", "time": "2026-08-10 06:00"}'

async def safe_tool_wrapper(name: str, arguments: str, timeout: float = 5.0) -> str:
    """工具统一包装:加超时 + 重试,任何异常都转成结构化错误返回给模型"""
    try:
        # asyncio.wait_for 兜底硬超时,防止重试也救不回来的慢接口
        return await asyncio.wait_for(call_tracking_api(arguments), timeout=timeout)
    except asyncio.TimeoutError:
        return '{"error": "tool_timeout", "message": "物流接口响应超时,请稍后重试"}'
    except Exception as e:
        # 关键:不要直接抛异常终止 Agent,而是把错误信息反馈给模型
        return f'{{"error": "tool_failed", "detail": "{type(e).__name__}: {e}"}}'

第五步:参数校验与"带反馈的重试"

模型生成的参数偶尔会出错——漏了必填字段、把嵌套对象序列化成字符串、日期格式不对。校验失败不要直接报错,而是把具体的校验错误信息反馈给模型,让它修正后重试。这是小红书面试题"Function Calling 的可靠性怎么保证"的标准答案,也是生产中修复大部分参数错误的通用手段:

from pydantic import BaseModel, ValidationError

class OrderQuery(BaseModel):      # 用 Pydantic 定义严格的参数契约
    order_id: str                 # 必填
    # 可以加 Field(pattern=...) 约束格式,例如订单号正则

def validate_and_execute(name: str, arguments: str) -> str:
    """参数校验 → 执行 → 失败反馈模型修正,而非直接抛异常"""
    # 1. 先做 JSON 解析与格式修复(见"踩坑指南"中的 sanitize 函数)
    args = sanitize_tool_arguments(arguments)

    # 2. Pydantic 校验
    try:
        if name == "get_order_status":
            validated = OrderQuery(**args)
            return get_order_status(validated.order_id)
    except ValidationError as e:
        # 3. 把校验错误详情反馈给模型,让它自己修正参数再试
        return (f'{{"error": "validation_failed", '
                f'"detail": "{e.errors()}"}}')

    return '{"error": "unknown_tool"}'

第六步:生产级多模型路由与降级(可选进阶)

当你有多个模型供应商时,按请求复杂度动态选模型、失败时按降级链切换,是降本增效的关键:

async def call_with_fallback(primary_model: str, messages, tools):
    """主模型失败时按降级链切换到备用模型"""
    fallback_chain = {
        "gpt-5.6": ["claude-opus-4-6", "gemini-2.5-pro"],
        "claude-opus-4-6": ["gpt-5.6", "gemini-2.5-pro"],
        "gemini-2.5-pro": ["gpt-5.6", "claude-opus-4-6"],
    }
    models = [primary_model] + fallback_chain[primary_model]
    for model in models:
        try:
            resp = await model_call(model, messages, tools)
            if validate_against_schema(resp.arguments, tools):
                return resp
        except (json.JSONDecodeError, ValidationError) as e:
            print(f"模型 {model} 调用失败: {e},切换降级模型")
            continue
    raise AgentError("所有模型均失败,触发人工兜底")

至此,一个具备"工具注册、多轮循环、并行执行、超时重试、参数校验、多模型降级"的工具调用层就成型了。整个实战流程如下:

flowchart LR
    U[用户问题] --> A[工具循环引擎]
    A -->|请求+工具声明| M[LLM gpt-5.6]
    M -->|tool_calls| A
    A -->|并发分发| R[工具注册表]
    R -->|超时重试包装| T1[订单API]
    R -->|超时重试包装| T2[物流API]
    T1 -->|结构化结果| A
    T2 -->|结构化结果| A
    A -->|校验+反馈| M
    M -->|最终回答| OUT[用户]

图 3:实战 Agent 的工具调用实现流程。工具循环引擎负责消息组装与轮数控制,注册表负责分发,超时/重试/校验全部收口在工具包装层。

关键细节与踩坑指南

坑 1:嵌套对象被序列化成字符串

这是多模型环境中最高频的坑。腾讯云横评实测数据:GPT 系列模型约 8.8% 的概率把嵌套 object 参数错误序列化成字符串

// 期望
{ "price_range": { "min": 100, "max": 500 } }
// 实际(偶发)
{ "price_range": "{\"min\": 100, \"max\": 500}" }

解法:在 API Gateway 或工具入口统一做 sanitize(清洗)——检测字符串化的 JSON 并反序列化回来:

import json, re

def sanitize_tool_arguments(raw_args: str | dict) -> dict:
    """统一处理各家模型参数格式差异(实测 GPT 嵌套序列化 / Gemini JSON 格式问题)"""
    if isinstance(raw_args, dict):
        for key, value in raw_args.items():
            # 修复"字符串化的嵌套对象"(GPT 偶发问题)
            if isinstance(value, str) and value.strip().startswith("{"):
                try:
                    raw_args[key] = json.loads(value)
                except json.JSONDecodeError:
                    pass
        return raw_args

    if isinstance(raw_args, str):
        # 修复 Gemini 常见的 JSON 格式问题:漏逗号、多余逗号、括号不匹配
        cleaned = re.sub(r",\s*}", "}", raw_args)   # 去掉对象末尾多余逗号
        cleaned = re.sub(r",\s*]", "]", cleaned)    # 去掉数组末尾多余逗号
        return json.loads(cleaned)

    raise ValueError(f"Unexpected argument type: {type(raw_args)}")

坑 2:并行调用暴露隐藏耦合——工具在并发下"悄悄变错"

很多团队开并行是为了提速(工具执行占 Agent 总延迟 35%–60%),结果一开就翻车。并行工具调用是模型做出的决定,不是你的编排层做出的决定——当模型在单轮响应中发出多个 tool_use 块时,你的 runner 应当同时分派所有调用,并统一返回整批结果,模型看不到中间结果。三个经典静默失败模式:

  1. 上下文依赖(Context dependency):工具 A 偷偷读取应由工具 B 填充的共享变量,并行时 A 读到空值,返回"看似正确实则错误"的结果,且不抛异常。
  2. 共享状态变更(Shared state mutation):两个工具对同一资源做"读-改-写",产生竞态,最终值悄悄丢失一个增量。
  3. 执行时序依赖(Execution timing dependency):工具 A 的副作用是工具 B 的前提(A 建记录,B 写关联记录),并行时前提被破坏。

解法:并行化之前先做幂等性审计,问三个问题——它是原子的吗(Is it atomic)?它是幂等的吗(Is it idempotent)?它能合并吗(Is it mergeable)?查询类工具(GET)可以并行;创建/通知类工具(POST、发邮件)默认不幂等,谨慎并行。有依赖关系的工具请用 DAG(有向无环图)编排,让依赖显式化,而不是依赖"顺序执行恰好掩盖了依赖"。

坑 3:各家模型行为不一致,别假设并行行为稳定

  • OpenAI:parallel_tool_calls 默认 true;但推理模型(o3、o4-mini 等)要么忽略、要么完全拒绝该参数,显式设置会返回 400 错误
  • Anthropic:没有直接开关,Claude 根据"工具看起来是否独立"自行决定,倾向保守(实测约 8.7% 概率在该并行时串行)。
  • 多模型路由或版本升级时,编排层必须无条件处理多工具响应,无论你是否请求了并行。

坑 4:工具数量失控导致上下文混淆

一次性给模型超过 100 个工具会导致上下文混淆(Context Confusion),模型开始幻觉参数、张冠李戴。经验法则:

  • 单 Agent 暴露的工具尽量控制在 10–20 个(OpenAI 官方 workshop 的建议),超过就要分层。
  • 命名空间(namespace) 分组:OpenAI Agents SDK 的 tool_namespace(name="crm", tools=[...]),让模型按需加载。
  • 延迟加载(defer_loading) + ToolSearchTool:工具 Schema 不一次性全塞进请求,模型需要时才搜索加载子集,大幅节省 tool-schema tokens。

坑 5:工具返回垃圾内容毒化上下文

工具输出会原样进入模型上下文(记费、占窗口、干扰决策)。生产规范:

  • 查询类工具限制返回行数(如 max_query_result_rows=50),必要时只返回摘要而非全文;
  • 写操作类工具返回状态描述而非空串("邮件发送完成");
  • 大结果用 {"truncated": true, "summary": "..."} 结构,防止 Context 溢出。

生产环境最佳实践

Schema 工程:description 是最便宜的优化

腾讯云横评的一个反直觉结论:把 Schema 的 description 写详细,GPT 的嵌套对象错误率从 8.8% 降到 2.1%,Gemini 的格式错误下降约 40%。这是投入产出比最高的优化。

{
  "price_range": {
    "type": "object",
    "description": "价格筛选区间,JSON 对象格式。示例:{\"min\": 100, \"max\": 500}",
    "properties": {
      "min": {"type": "number", "description": "最低价格,单位人民币元"},
      "max": {"type": "number", "description": "最高价格,单位人民币元"}
    }
  }
}

监控指标:四率一延迟

生产环境至少监控这些指标(数据来自小红书可靠性面试题总结与业界共识):

指标 含义 异常信号
工具调用触发率 该调的时候是否调了 过低 → 模型跳过工具(考虑 required 兜底)
参数校验通过率 参数质量 过低 → Schema 描述不清晰 / 工具过碎
工具执行成功率 下游服务健康度 关注 4xx/5xx 分布
重试/降级率 链路稳定性 持续上升 → 排查下游与网络
端到端延迟 用户体感 分解为"模型往返时间 + 工具执行时间"

安全管控

  • 最小权限原则:工具只暴露任务必需的参数与功能,去掉无意义或恒定的参数;
  • Human-in-the-loop:关键写操作(转账、下单、发邮件)定义 require_confirmation,敏感操作执行前人工确认;
  • 允许调用者控制(allowed_callers):PTC 场景下,只有声明了 allowed_callers=["programmatic"] 的工具才能被生成的代码调用,高影响工具保持 direct 调用,让每次动作都可审查。

成本控制

  • 提示词缓存(Prompt Caching):工具 Schema 声明基本不变,是缓存命中率最高的部分;
  • Batch API:非实时场景(批量订单处理)用离线批处理,价格低 50%;
  • 路由降本:简单工具调用路由到便宜模型(如 Gemini Flash、GPT-5.6-mini 级别),复杂推理路由到旗舰模型,可降 80%–90% 成本且准确率损失极小。

横向对比与选型建议

三家主流厂商的 Function Calling 横向对比

基于腾讯云 900 用例 × 3 轮横评(2026-03)与 BFCL v3 榜单(2026-08-07)数据:

维度 Claude Opus 4.6 GPT-5.4 / 5.6 Gemini 2.5 Pro
综合得分(900 用例) 94.5% 94.6% 89.7%
嵌套复杂参数 96.7%(最稳) 91.2%(8.8% 序列化坑) 84.3%(15.7% JSON 格式错误)
多接口并行编排 偏保守(8.7% 不必要串行) 最强 一般
重试成本占比 3.6% 3.4% 17.0%
适用场景 参数格式严格、安全敏感 高并发、多 API 编排 成本敏感简单场景

选型决策路径

flowchart TD
    Q{工具数量与复杂度} -->|>3 个且含嵌套对象| A[Claude 系<br/>嵌套处理最稳]
    Q -->|>3 个无嵌套/多接口编排| B[GPT 系<br/>并行编排最强]
    Q -->|简单 CRUD 单接口| C[Gemini Flash 等<br/>成本最优]
    A --> D[统一 API 网关<br/>多模型路由 + 降级链]
    B --> D
    C --> D

图 4:多模型选型决策路径。别迷信单一模型的 benchmark 分数,在自己的 API Schema 上实测才是王道——腾讯云横评的结论。

原生 Function Calling vs MCP vs PTC

  • 原生 Function Calling:单应用内、直接集成,延迟最低,适合内部工具;
  • MCP(Model Context Protocol):标准化的工具接入协议,解决"一套工具被多家模型/框架复用"的生态问题,适合跨应用、跨团队的工具共享(站内已有 MCP 入门文章,本文不展开);
  • PTC(Programmatic Tool Calling,2025-11 发布):模型生成代码编排工具调用,适合"循环、分支、聚合"型工作流,把多轮 API 往返压缩为一段沙箱代码执行,延迟与成本双降。

一句话建议:工具少、链路简单用原生 Function Calling 自己控制循环;工具生态要复用用 MCP;工作流含大量数据编排用 PTC。

性能实测与效果验证

实测一:并行调用对延迟的收益

工具执行占 Agent 总延迟 35%–60%。以三个独立的 200ms 工具调用为例:

执行模式 端到端耗时 说明
串行 600ms 3 × 200ms 顺序执行
并行 200ms 墙上时钟时间重叠,等待最慢的一个
并行 + 模型往返 约 400ms+ 仍需一次额外模型调用整合结果

并行收益可观,但收益的前提是工具之间真正独立(见坑 2 的耦合测试)。

实测二:BFCL v3 最新榜单(截至 2026-08-07)

Berkeley Function Calling Leaderboard(伯克利函数调用排行榜)v3 是工具调用准确率的权威基准,覆盖函数名准确率、参数正确性、并行调用、多轮工具使用:

排名 模型 BFCL v3 得分
1 GLM 4.5 76.7%
2 Claude Opus 4.7 76.6%
3 Gemini 3.1 Flash Lite Preview 76.5%
23 个模型平均分 58.5%(标准差 17.5)

注意两点:① 头部模型差距在 0.2% 以内,说明工具调用能力已趋同,选型应更多看成本与生态;② 榜单平均分仅 58.5%,顶尖模型在真实复杂场景也只有约 3/4 的正确率——这就是为什么"参数校验 + 带反馈重试 + 多模型降级"这三层防御缺一不可。

实测三:Schema 工程的效果量化(腾讯云横评)

优化项 优化前 优化后
GPT 嵌套对象错误率 8.8% 2.1%(写入详细 description)
Gemini JSON 格式错误 15.7% 下降约 40%
Claude 不必要串行调用 8.7% 减少 60%(System Prompt 声明并行规则)

结论:工具调用可靠性 ≈ 模型能力(约 70%)+ Schema 工程(约 20%)+ 防御性编程(约 10%)。前两者决定上限,后者决定下限。

总结与未来展望

工具调用是 Agent 从"会聊天"到"能做事"的分水岭,其本质是"模型决策 + 应用执行"的结构化分工。要写好工具调用层,你需要抓住五件事:Schema 是给模型看的说明书(description 要精雕细琢)循环必须完整回传消息历史并用 call_id 关联并行要建立在幂等性审计之上任何异常都要转成结构化错误反馈给模型而非直接终止多模型路由 + 统一网关是生产可靠性的兜底

展望未来,三个方向值得关注:一是 PTC 式的"代码编排工具"正在把工具调用从"逐轮对话"推向"程序化执行",对循环聚合类任务效率提升是数量级的;二是 MCP 协议持续迭代(已更新到第五版),工具生态标准化将进一步降低接入成本;三是 Agent 可观测性(Tracing、Eval)会像今天的 APM 一样成为标配——毕竟"工具调用失败不报错、自信地返回错误答案"这类静默失败,只能靠全链路观测去发现。工具调用不会消失,它只会越来越像一门严谨的工程学科。

延伸阅读