首页 / 文章 / Function Calling 工具调用完整示例

Function Calling 工具调用完整示例

从 JSON Schema 定义工具开始,走完 tool_calls 解析、结果回填与参数校验的完整闭环,附一段可直接跑通的 Python 代码和常见错误处理。

工具定义就是一段 JSON Schema

tools 里每个函数由三部分组成:name、description、parameters。parameters 是一份 JSON Schema,模型照着它生成参数。描述里写清"什么时候该调这个函数",比把字段类型写全更影响准确率。

json
{
  "type": "function",
  "function": {
    "name": "get_order_status",
    "description": "按订单号查询物流状态。用户问到货时间、快递进度时调用。",
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": {"type": "string", "description": "订单号,形如 A123456"},
        "locale": {"type": "string", "enum": ["zh-CN", "en-US"]}
      },
      "required": ["order_id"],
      "additionalProperties": false
    }
  }
}

描述写三件事:函数做什么、什么情况下调用、参数从哪来。"查询订单状态"不如"用户询问快递到哪了、什么时候能到、为什么还没发货时调用"。描述含糊的后果是模型在闲聊时也去查订单,每一次都是白花的 token。

开了 strict 之后,required 里的字段一定会给;没开的时候,写在 properties 却不在 required 里的字段,模型可能直接省略。可选参数就用这种方式表达,别在描述里写"如果用户没提就传空字符串",模型会照做,你的代码还得兼容空串。

一次调用分三段

第一段把问题和工具定义一起发出去,模型返回 tool_calls;第二段由你的代码执行函数,把结果以 role: tool 回填;第三段模型基于结果写答案。tool_calls 是数组,一次可以返回多个,逐个执行完再一起回填。

可复制的完整示例

python
import json
import requests

BASE = "https://ai.idcidiot.com/v1"
KEY = "sk-..."
MODEL = "glm-5.3-flash"

TOOLS = [{
    "type": "function",
    "function": {
        "name": "get_order_status",
        "description": "按订单号查询物流状态。用户问到货时间、快递进度时调用。",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "订单号,形如 A123456"},
            },
            "required": ["order_id"],
            "additionalProperties": False,
        },
    },
}]

def chat(messages, tools=TOOLS):
    r = requests.post(
        f"{BASE}/chat/completions",
        headers={"Authorization": f"Bearer {KEY}"},
        json={"model": MODEL, "messages": messages, "tools": tools,
              "tool_choice": "auto", "max_tokens": 600},
        timeout=90,
    )
    r.raise_for_status()
    return r.json()

def get_order_status(order_id: str) -> dict:
    # 换成你真实的查询
    return {"order_id": order_id, "status": "运输中", "eta": "2026-11-02"}

messages = [{"role": "user", "content": "帮我看看 A123456 到哪了"}]
resp = chat(messages)
msg = resp["choices"][0]["message"]
messages.append(msg)

for call in msg.get("tool_calls") or []:
    args = json.loads(call["function"]["arguments"])
    result = get_order_status(**args)
    messages.append({
        "role": "tool",
        "tool_call_id": call["id"],
        "content": json.dumps(result, ensure_ascii=False),
    })

final = chat(messages)
print(final["choices"][0]["message"]["content"])

注意 messages.append(msg) 这一步:把带 tool_calls 的 assistant 消息原样放回历史,后面的 tool 消息才有对应的调用可挂。

参数不能直接信

arguments 是模型生成的字符串,不是对象。解析可能失败,字段可能缺,类型可能不对。三层防护:

  1. json.loads 失败时,把原始字符串和错误信息回给模型,让它重发。
  2. 用 JSON Schema 校验一遍,必填字段和枚举值都查。
  3. 执行前做业务校验,订单号格式、用户权限、参数范围。

开了 strict: true 也只能保证结构符合 schema,语义仍然要自己查。查不到的订单返回"未找到",别让模型顺着编一个。

校验失败时别只回一句"参数错误",把字段名、期望类型、实际收到的值写清楚,模型第二次通常能改对。连续两次失败就转人工或返回兜底文案,不要在循环里反复烧 token。

多个工具与流式

并行调用时,tool_calls 里每一项有自己的 id,回填的 tool_call_id 必须对得上,顺序不重要。流式下 delta.tool_calls 是分片到达的,按 index 聚合,function.arguments 是一段段字符串,拼完再解析。

流式还容易漏掉收尾那块:finish_reason 变成 tool_calls 时才代表参数收全,提前解析会得到半截 JSON。

工具调用适合"一步查一个东西",不适合当工作流引擎。三步以上的流程、带分支和回滚的业务,把状态机写在自己的代码里,模型只负责决定下一步调哪个函数。否则上下文越滚越长,出错后也说不清是哪一步的问题。

工具定义也花钱

每次请求都要把 tools 发一遍,这部分计入 prompt_tokens。一个五函数的 schema 大约 500 token,glm-5.3-flash 在稳定国模分组输入 0.16 美元每百万 token,十万次调用光工具定义就是 8 美元。函数多的项目按场景拆成几个小工具集,既便宜也更准。

tool_choice 也影响成本。设成 required 会逼模型每次都调工具,闲聊场景下等于多花一次调用的钱;设成指定函数则跳过模型的选择过程,适合流程固定的批处理。

模型对工具的兼容程度不一样,选型前看 模型总览 里的说明,glm-5.3-flash 的上下文和输出上限在 模型页,价格对照在 定价页。写完先跑十轮真实问题,数一下 tool_calls 的命中率,再决定要不要改描述。

用 二五云API 跑通这些模型

OpenAI 兼容接口,一个 Key 调用 GPT、Claude、Gemini、DeepSeek、Kimi 等模型,按量计费。

免费注册查看模型价格

相关文章