codecamp

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 的规则来自该页。
  • 本节三类错误的划分与解析代码为本书原创;官方页只描述正常流程与未知函数示例,未给出错误分类,故此处标为工程框架而非官方口径。

七、动手试

  1. 给你的解析器加一个计数:每类错误各累计多少次,并在连续失败 3 次时告警。想一想:哪一类错误的告警最该被立刻处理?
  2. 写一条「参数能解析但根是数组」的用例,确认它落进 schema_error 而不是 json_error。
  3. 对照你现有代码,检查有没有为「流式分片」写拼接分支。如果有,说明它为什么不会被执行,以及删掉后是否更清楚。
Grok JSON Schema 输出约束
Grok 未知工具白名单
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }