工具定义就是一段 JSON Schema
tools 里每个函数由三部分组成:name、description、parameters。parameters 是一份 JSON Schema,模型照着它生成参数。描述里写清"什么时候该调这个函数",比把字段类型写全更影响准确率。
{
"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 是数组,一次可以返回多个,逐个执行完再一起回填。
可复制的完整示例
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 是模型生成的字符串,不是对象。解析可能失败,字段可能缺,类型可能不对。三层防护:
json.loads失败时,把原始字符串和错误信息回给模型,让它重发。- 用 JSON Schema 校验一遍,必填字段和枚举值都查。
- 执行前做业务校验,订单号格式、用户权限、参数范围。
开了 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 的命中率,再决定要不要改描述。