GLM function calling 怎么写:从工具定义到结果回传
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
GLM 的 function calling 不是一次请求就能拿到答案的东西,它天生是两轮:第一轮你把 tools 连同用户问题发过去,模型不直接回答,而是在 message.tool_calls 里告诉你它想调哪个函数、传什么参数;你在自己的代码里真的把这个函数跑一遍,把返回值以 role: "tool" 的消息追加进 messages,第二轮再发一次请求,模型才给出面向用户的自然语言回答。 这条链路上最容易出问题的三个点是:tool_choice 在 GLM 这边按官方文档的说法默认且仅支持 auto,你没有强制指定某个函数的余地;function.arguments 拿到手是一个 JSON 格式的字符串而不是字典,必须先反序列化;以及回传结果时那条 tool 消息里的 tool_call_id 必须对上原来那次调用的 id,对不上模型就没法把结果和请求配起来。下面按真实的编写顺序走一遍。
第一步:把工具定义写成 tools 数组
官方文档里 tools 的定位是「定义可调用的函数列表,包含函数名、描述和参数规范」。它是一个数组,数组里每个元素是一层套一层的结构:最外层有 type,官方示例里取值是 function;里面套一个 function 对象,这个对象里才是 name、description 和 parameters 三件套。
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如:北京、上海"
}
},
"required": ["city"]
}
}
}
]
这个嵌套第一次写的时候很容易少一层,把 name 直接放到和 type 平级的位置上。记一个笨办法:type 说的是「这是个什么类型的工具」,function 里面装的才是「这个函数长什么样」,两件事不在一个层级上。
parameters 用的是 JSON Schema 的写法——type: "object",属性挂在 properties 下面,必填项单独列进 required 数组。文档的「参数设计」小节里还展示了两个常被忽略的字段:enum 用来把取值收敛成有限的几个选项,default 用来标明默认值,另外还可以给 examples 举几个例子。对模型来说这些不是装饰,它判断该不该调、该传什么,靠的就是 description 和这套 schema 的描述力。
第二步:知道 tool_choice 在 GLM 这里能做什么
tool_choice 是「控制函数调用策略」的参数,官方文档写得很直白:默认且仅支持 auto。
这一条值得单独拎出来,因为它和不少人从别家接口带过来的习惯是冲突的。你不能通过 tool_choice 指定「这次必须调 get_weather」,也不能用它强制关掉工具调用。换句话说,调不调、调哪个,决定权在模型手里。
那想要「某个场景必须走某个函数」怎么办?只能从别的地方使劲:一是在 description 里把这个函数的适用场景说清楚,二是在 system 或 user 消息里把意图表达得更明确,三是干脆在这一轮请求里只挂这一个工具。前两条属于提示词工程,第三条是最直接的——tools 里放什么,完全由你在发请求前决定,按业务分支动态拼 tools 数组是完全合法的做法。
至于模型「是否会在某些情况下不调用工具」,代码要按会出现这种情况来写。官方示例里那个 if message.tool_calls: ... else: print(message.content) 的分支不是摆设,模型直接给文字回答是正常路径之一。
第三步:解析第一轮响应
第一轮请求发出去以后,你要看的是 response.choices[0].message。官方文档列出的响应侧关键字段有四个:
tool_calls:模型决定调用的函数信息,是个列表,所以要按可能有多个来遍历function.name:被调用的函数名称function.arguments:函数调用参数,JSON 格式字符串id:这次工具调用的唯一标识符
arguments 是字符串这一点,是这套 API 里最容易让人栽跟头的地方。它长得像字典,打印出来也像字典,但你直接 args.get("city") 会报错,得先 json.loads 一次:
args = json.loads(tool_call.function.arguments)
weather_result = get_weather(args.get("city"))
既然是模型生成的 JSON 字符串,就要按「可能解析失败、可能缺字段」来写代码。取值统一用 .get() 而不是下标,json.loads 外面包一层异常处理,比事后看日志排查划算得多。如果你的服务对返回结构变化比较敏感,可以顺带看看各家 API 返回结构变化怎么兜这类通用做法。
第四步:把函数结果塞回 messages
这一步的顺序很讲究,官方示例里是这么走的:先把模型那条带 tool_calls 的消息本身追加进 messages(Python SDK 里用的是 message.model_dump()),然后才追加函数执行结果。
messages.append(message.model_dump())
if message.tool_calls:
for tool_call in message.tool_calls:
if tool_call.function.name == "get_weather":
args = json.loads(tool_call.function.arguments)
result = get_weather(args.get("city"))
messages.append({
"role": "tool",
"content": json.dumps(result, ensure_ascii=False),
"tool_call_id": tool_call.id
})
三个细节:
第一,模型那条消息不能跳过。它是「模型说我要调这个函数」的记录,你只把结果塞进去而不带上请求本身,上下文就断了。
第二,结果消息的 role 是 tool,不是 assistant 也不是 user,并且必须带 tool_call_id,值取自前面那次调用的 tool_call.id。多个工具调用并存时,配对全靠这个 id。
第三,content 要的是字符串。官方示例里把 dict 用 json.dumps 序列化后再放进去,并且带了 ensure_ascii=False——不带这个参数中文会被转成转义序列,模型还是能读,但你自己看日志会很痛苦。
第五步:第二轮请求还得带 tools
这一点很反直觉,但官方示例就是这么写的:拿到函数结果之后再次调用 chat.completions.create,messages 换成拼好的完整对话,model 不变,tools 依然传进去。
final_response = client.chat.completions.create(
model="glm-5.2",
messages=messages,
tools=tools
)
print(final_response.choices[0].message.content)
为什么第二轮还要带?因为模型看完函数结果之后,完全可能判断信息还不够,需要再调一次工具。你把 tools 摘掉,等于告诉它这条路不通了。真实业务里应该把这一段包成一个循环:只要返回里还有 tool_calls 就继续执行、继续回传,直到拿到纯文本回答为止,同时给循环设一个上限,避免模型和你的代码互相拉扯停不下来。
多工具的场景官方文档也给了范例:把若干个函数定义收在一个列表里,再用一个 execute_function(function_name, arguments) 做分发,按 function_name 路由到对应实现,遇到不认识的名字返回一个明确的错误结构。这个写法的好处是新增工具只改两处——定义和分发表,主流程不动。
函数实现这一侧该怎么写
文档的「实践建议」部分把函数设计原则总结成三条:单一职责,每个函数只做一件事;清晰命名,函数名和参数名要有意义;完整描述,给出详细的函数和参数描述。安全方面同样是三条:输入验证,严格校验所有输入参数;权限控制,限制函数的访问权限;日志记录,把调用过程记下来。
文档给的错误处理范式是统一返回一个结构化字典,里面有 success 布尔位、data 或 error 文本,以及一个 error_code 这样的机器可读标识,参数问题、数据问题、系统问题各用不同的 error_code 区分。这个约定很值得照抄,原因是它同时服务两个读者:模型读 error 那段文字来决定要不要换个参数重试,你的监控读 error_code 来分类告警。
文档还专门用 <Warning> 提醒了两次:函数调用涉及代码执行,对外部 API 和数据库操作要做适当的安全验证和权限控制。示例里的数据库查询工具直接把 SQL 语句作为参数交给模型生成,输入验证的例子里也演示了长度限制、危险字符过滤和 SQL 关键词拦截。真放到线上,把裸 SQL 交给模型是个需要格外谨慎的设计,能收敛成有限的查询模板就别开放自由文本。API Key 之类的凭据也不要顺手塞进工具参数里,那属于另一个话题,可以看API Key 安全管理。
想让工具参数边生成边显示
如果你的产品要在界面上实时展示「模型正在调用某某工具」,GLM 有单独的工具流式输出能力。官方文档《工具流式输出》一页说明,开启需要同时设置 stream=True 和 tool_stream=True,流式响应的 delta 对象里会带 reasoning_content(推理过程文本)、content(回答文本)和 tool_calls(工具调用信息)三类字段。文档的说法是,这样可以在不进行缓冲或 JSON 验证的情况下流式传输工具使用参数,从而减少调用延迟。
代价是参数片段到达时并不是合法 JSON,得自己按 index 拼接完整再解析,这块的坑单开一篇讲,见 GLM 流式输出下的工具调用怎么拼装。至于哪些型号支持工具调用与工具流式输出,官方文档在这两页各自列了适用范围,以官方文档当前版本为准,别按型号名字自己推断。
最后:几个真会让你卡住的点
把上面的流程压缩成一份检查清单:
tools少了一层function嵌套,或者type写错,请求本身就不成立arguments忘了json.loads,拿到的是字符串不是字典- 回传的 tool 消息漏了
tool_call_id,或者 id 和调用对不上 - 只追加了结果、没追加模型那条
tool_calls消息 - 第二轮请求把
tools摘掉了,模型没法继续调用 - 指望用
tool_choice强制指定函数——官方文档明确写了默认且仅支持auto
还有一个成本上的提醒:function calling 天生是多轮的,一次用户提问背后可能是两次甚至更多次模型请求,每一轮都会把之前的 messages(包括工具返回的那一大坨 JSON)重新送一遍。工具返回值的体积直接决定了后续每一轮的输入长度,把没用的字段裁掉是最实在的优化。相关的机制可以对照看 GLM 上下文缓存怎么才能命中——多轮工具调用的前缀重复度很高,是缓存的典型受益场景,但命中条件有讲究,写法不对就白折腾。