Grok 工具参数解析失败处理
工具调用最容易崩的地方,不是模型不调工具,而是你把参数当成了字典。Responses API 里 item.arguments 是一个字符串,里面才是 JSON;直接对它取键会报 TypeError,而把异常吞掉又会让循环静默空转。更麻烦的是,参数可能「能解析但形状不对」——JSON 合法、却不是你要的对象。
这一节把「解析」和「校验」拆成两步,并先纠正一个常见误解。
一、先纠正一个误解:流式下工具参数不会分片
很多中文教程写「流式时工具参数会分片到达,要先拼接再解析」。按 2026-11-04 官方 Function Calling 页的提示,这个说法不成立:
With streaming, the function call is returned in whole in a single chunk, not streamed across chunks.
也就是说,工具调用会整块出现在单个 chunk 里,不需要跨 chunk 拼接。真正要处理的是另外两件事:arguments 是字符串形态的 JSON;以及解析成功后的形状校验。
这个区别很关键:如果按「分片」写拼接逻辑,代码会多出一层永远走不到的分支,反而掩盖真实的解析失败。
二、最小可运行示例:解析与校验分开
下面的脚本不联网、不需要真实密钥。它把一次工具调用拆成三步判断:工具是否注册、参数是否合法 JSON、参数是否符合 schema。
"""工具调用解析:unknown_tool / json_error / schema_error 三类分开。"""
import json
SCHEMAS = {
"get_temperature": {
"required": ["location"],
"properties": {"location": {"type": "string"}, "unit": {"type": "string"}},
}
}
def parse_tool_call(item: dict, known: set[str]) -> tuple[dict | None, str, str]:
"""返回 (参数, 错误类型, 错误信息)。成功时错误类型为空串。"""
name = item.get("name", "")
if name not in known:
return None, "unknown_tool", f"未注册工具 {name}"
raw = item.get("arguments", "")
try:
args = json.loads(raw)
except json.JSONDecodeError:
return None, "json_error", "参数不是合法 JSON"
if not isinstance(args, dict):
return None, "schema_error", "参数根不是对象"
missing = [k for k in SCHEMAS[name]["required"] if k not in args]
if missing:
return None, "schema_error", f"缺少必填 {missing}"
return args, "", ""
def offline_check() -> None:
known = set(SCHEMAS)
cases = [
("正常", {"name": "get_temperature", "arguments": '{"location": "上海"}'}),
("非法 JSON", {"name": "get_temperature", "arguments": "location=上海"}),
("缺必填", {"name": "get_temperature", "arguments": '{"unit": "c"}'}),
("未注册", {"name": "delete_all", "arguments": "{}"}),
]
for label, item in cases:
args, kind, msg = parse_tool_call(item, known)
print(f"{label}:{kind or 'ok'} {args if args else msg}")
if __name__ == "__main__":
offline_check()
真正上线时,SCHEMAS 换成你注册工具时的同一份 schema——同一份定义用在两处,才不会再出现「模型按 A 生成、你按 B 校验」的错位。
三、三类错误要分开处理
| 错误类型 | 典型现象 | 处理方式 |
|---|---|---|
json_error |
arguments 不是合法 JSON |
记一次失败,回一条「参数无法解析」给模型 |
schema_error |
JSON 合法但缺字段或类型不对 | 回具体缺什么,让模型重试一次 |
unknown_tool |
模型请求了未注册的工具 | 显式拒绝,不要抛异常打断循环 |
三件要强调的事:
- 不要把三类合成一个
except。JSON 解析失败是格式问题,schema 不匹配是语义问题,未注册是配置问题;混在一起,日志里就看不出该修哪。 - 重试要有上限。官方示例里是「执行函数、把结果回传、继续对话」;如果参数反复不合,回传错误比无限重试更省。给每个工具设一个最大重试次数。
- 错误信息要能回给模型。官方示例对未知函数的处理就是构造一个
{"error": "Unknown function: ..."}作为工具输出。这比在本地抛异常更符合协议:模型能看到错误并调整。
四、预期输出与异常分支
运行解析器,预期输出:
正常:ok {'location': '上海'}
非法 JSON:json_error 参数不是合法 JSON
缺必填:schema_error 缺少必填 ['location']
未注册:unknown_tool 未注册工具 delete_all
换成真实流量时,若出现异常,按下面顺序定位:
| 现象 | 优先怀疑 | 第一步动作 |
|---|---|---|
| 取参数就报错 | 把 arguments 当 dict |
先 json.loads 再取键 |
| 一直重试不成功 | 错误没回给模型 | 构造工具错误结果回传 |
| 同一工具高频失败 | schema 与实现不一致 | 对齐注册与校验的同一份定义 |
| 循环突然中断 | 未注册工具抛异常 | 改成显式拒绝并记录 |
最后一行尤其重要:unknown_tool 属于「模型想做的事你没有」,不是程序 bug。用异常处理它,会让一次正常的「拒绝」看起来像系统故障。
五、判据卡
| 判断项 | 该这么做 | 不该这么做 |
|---|---|---|
| 参数读取 | 先解析字符串再取键 | 直接对 arguments 取键 |
| 分片假设 | 按官方口径当整块处理 | 自己写跨 chunk 拼接 |
| 错误分类 | 三类分开记录 | 一个 except 兜住 |
| 失败回传 | 把错误作为工具结果回给模型 | 本地抛异常打断循环 |
| 重试策略 | 设上限,失败即止 | 无限重试 |
这张卡的重点是解析与校验分离。五条里有三条在说:先确定拿到的是不是 JSON,再确定它是不是你要的形状。
六、来源与核验
- 官方 Function Calling:
https://docs.x.ai/developers/tools/function-calling(核验日期 2026-11-04)。「流式下 function call 整块返回、不跨 chunk」的提示、五步调用流程、item.arguments为 JSON 字符串、function_call_output回传方式与未知函数处理示例,均以该页为准。 - 官方 Structured Outputs:
https://docs.x.ai/developers/model-capabilities/text/structured-outputs(核验日期 2026-11-04)。工具 schema 的strict隐式 true、参数根必须为 object 的规则来自该页。 - 本节三类错误的划分与解析代码为本书原创;官方页只描述正常流程与未知函数示例,未给出错误分类,故此处标为工程框架而非官方口径。
七、动手试
- 给你的解析器加一个计数:每类错误各累计多少次,并在连续失败 3 次时告警。想一想:哪一类错误的告警最该被立刻处理?
- 写一条「参数能解析但根是数组」的用例,确认它落进
schema_error而不是json_error。 - 对照你现有代码,检查有没有为「流式分片」写拼接分支。如果有,说明它为什么不会被执行,以及删掉后是否更清楚。