Gemini 工具调用报 malformed_function_call 怎么办

2026-08-25

数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。

如果你接的是 Gemini 的 function calling,看到 malformed_function_call,很容易第一反应就去翻自己的请求体——检查 schema 写错没有、参数类型对不对。方向大概率错了。先把出处钉住:本文引用的这套 snake_case 错误码,出自官方 Interactions API 的错误参考,下面讲的分族、error 对象结构、流式错误传递方式,作用域都只是这份参考本身;换到 generateContent 原生接口或 OpenAI 兼容层上是不是同一套形态,本文不做推断,结尾还会专门再收一次口。在这份参考里,这个码根本不在「标准请求级错误码」那张跟 HTTP 状态码一一对应的表里,它属于另一族:生成结构错误码,含义是模型这一次生成出来的函数调用无法解析。也就是说,问题出在输出侧而不是输入侧。这一族一共六个成员,malformed_function_callmalformed_tool_callunexpected_tool_callno_imagetoo_many_tool_callsmissing_thought_signature,它们和 invalid_request 那种 400 系错误是两回事,处置思路也完全不同。更麻烦的是,官方对这一族只给了码名和一句含义,并没有像被阻止码那样给出统一的推荐处置——所以本文能替你做的,是把官方明文说了什么、没说什么划清楚,剩下的别猜。

第一步:确认你手上的到底是 code 还是 HTTP 状态

这份错误参考把错误响应统一收敛到一个 error 对象上,里面有两个字段:code 是机器可读的 snake_case 字符串,message 是给人看的描述。很多接入代码只把 HTTP 状态码打到日志里,error.code 反而被吞掉了,于是排查的时候你只知道「这次调用失败了」,却不知道失败在哪一层。

这一步之所以关键,是因为这份参考里的错误码分成几族,而只有其中一族跟 HTTP 状态码严格对应。请求级的那一批是:invalid_requestparameter_unknown 对应 400,authentication 对应 401,permission_denied 对应 403,not_foundmodel_not_found 对应 404,rate_limit_exceededquota_exceeded 对应 429,cancelled 对应 499,api_error 对应 500,service_unavailable 对应 503。官方还补了一句:没有列在这张表里的错误码,API 会以标准 HTTP 状态文本的 snake_case 形式返回。

malformed_function_call 不在这张表里。它跟 safetyrecitation 那一批生成被阻止码一样,属于「请求本身发出去了、模型也开始生成了,但结果不成立」的情形。所以你按 400 的思路去逐个字段核对请求参数,很可能核到最后什么也发现不了。

顺便说一句,请求级那两个 429 的语义差别同样容易吃亏——rate_limit_exceeded 是分钟级限流,退避重试有用;quota_exceeded 是每日配额耗尽,退避多少次都是白退。这一层的区分在 Gemini 的两种 429 完全不是一回事 里单独讲过,也可以对照站内的 429 通用处理思路 看。

生成结构错误码这一族,官方明文说了什么

官方文档对这一族的说明相当克制,逐条抄下来是这样的:

  • malformed_function_call:函数调用无法解析
  • malformed_tool_call:官方给出了码名,但没有附带展开的中文含义
  • unexpected_tool_call:调用了请求中未声明的工具
  • no_image:官方给出了码名,但没有进一步的说明
  • too_many_tool_calls:工具调用数超出上限
  • missing_thought_signature:响应缺少必需的 thought 签名

我知道你想问的是:malformed_function_callmalformed_tool_call 到底差在哪?函数调用「无法解析」具体是指 JSON 语法不成立,还是指参数不匹配你声明的 schema?触发之后这次调用算不算消耗、能不能直接重试?这几个问题,官方文档里我没有找到相关说明。官方给的就是上面这行字,再往下就属于推断了。

这里要特别提醒一个很容易滑过去的地方:官方对生成被阻止码那一族(safetyrecitationprohibited_contentspiiblocklist 等)给出了统一处置——修改输入后重试。但这条处置建议是给被阻止码那一族的,不能直接搬到生成结构错误码这一族头上。两族虽然都发生在生成阶段,但一个是内容触了政策,一个是输出的结构不成立,官方并没有把处置建议合并写。如果你在别处看到把「修改输入后重试」当成 malformed_function_call 的官方推荐做法,那是接错了限定条件——官方把这条处置挂在被阻止码那一族下面,没有挂在结构错误码这一族下面,这两件事在文档里是分开列的。

unexpected_tool_call:先去比对官方列出的工具类型

这一族里唯一有明确排查方向的是 unexpected_tool_call,官方含义写得很直白——调用了请求中未声明的工具。方向很清楚:去比对你这次请求里声明的工具列表,和模型实际发起调用的那个工具名。

有一份可以拿来对照的清单。官方的一条错误 message 里枚举了受支持的工具类型:functioncode_executionmcp_serverfilesystemgoogle_mapsgoogle_searchbashcomputer_usefile_searchurl_context。注意这份枚举的来源是错误信息里的取值列表,不是一张能力对照表,最终以官方参考文档当前版本为准。

它的用处在于帮你定位一类结构性失误:如果你在请求里声明的工具类型名不在这份枚举里,或者拼写与枚举里的取值对不上,模型侧对「这次允许调什么」的理解就和你以为的不一致。多工具编排的场景尤其容易出这种问题——一份工具声明在多个链路间传递、被中间层裁剪或改写过之后,实际发出去的那份和你在代码里写的那份未必还是同一份。排查 unexpected_tool_call 的第一件事,是把最终真正发到 API 的请求体打出来看,而不是看你的工具注册表。

missing_thought_signature 和思考签名有关

这个码在中文圈几乎没人写过。官方含义是响应缺少必需的 thought 签名。

能和它对上的另一条官方事实是:Gemini 3 支持在 chat completions API 中使用 OpenAI 兼容的思考签名(thought signatures)。也就是说,签名这套机制在 OpenAI 兼容层里是存在的,跟推理相关。至于在什么条件下这个签名会变成「必需」、缺失之后应该怎么补、在原生 API 和兼容层下行为是否一致,官方文档里我没有找到相关说明,不做展开。

如果你正在走 OpenAI 兼容层,顺带值得留意的是思考参数的映射关系:OpenAI 的 reasoning_effort 会映射到 Gemini 的 thinking_levelthinking_budget,而且这两组参数功能重叠、不能同时使用。这块的细节在 Gemini 的 reasoning_effort 和 thinking_level 怎么对应 里讲得更细。做 Agent 的时候思考相关参数和思考签名往往是同一批代码在管,出问题的时候一起看效率更高。

too_many_tool_calls:官方只说了「超出上限」

这个码的含义是工具调用数超出上限。具体上限是多少,属于会变的阈值,本站不写数字,也建议你别把某个数值硬编码进重试逻辑——今天对的,下个月未必还对,去查官方文档当前版本才是稳妥做法。

从机制上讲,会撞到这个码的场景,是让模型自主多轮调工具的编排:一次生成里模型连续发起多个调用,或者你的循环允许模型不断追加工具调用而没有自己的收敛条件。这里能给的建议不是「设成几次」,而是:你的编排层自己要有一个调用次数上限和终止条件,别把收敛这件事完全交给模型和服务端的上限去兜底。服务端上限触发的表现是一个错误码,你的编排层上限触发的表现可以是一次可控的降级——后者显然更好排查。

流式请求下,这个错很可能被你的客户端漏掉

这是本篇最需要落到代码里的一条。

这份错误参考里明确写了:标准请求与流式请求的错误传递方式不同。非流式的情况下,服务端设置 HTTP 状态码,同时在 JSON body 里返回 error 对象;而流式(SSE,stream: true)的情况下,错误是通过 SSE 流发送 event_type: "error" 事件error 字段的结构是一样的。

直接后果是:一个只判 HTTP 状态码的客户端,会漏掉流式过程中发生的错误。放到工具调用的场景里,这个后果特别刺眼——流式返回工具调用参数是很常见的做法,如果生成到一半出了 malformed_function_call,而你的客户端只在拿到响应头时判过那一次 HTTP 状态码、之后就闷头拼接分片,那你拿到的会是一段不完整的流,然后在自己的解析代码里报一个跟真实原因毫无关系的异常。日志里留下的是你自己的 JSON 解析错误,真正的 error.code 从来没有被记下来。

所以流式链路的错误处理要单独写一套:解析 SSE 事件时判 event_type,遇到 error 事件就把 error.codeerror.message 原样记录并中止本次生成。要提醒的是,这条判断逻辑的出处同样是这份错误参考,能确定的也只有这一条通道的形态;generateContent 原生接口与 OpenAI 兼容层的流式错误以什么形态送达,官方文档里没有找到相关说明。这处空白不要拿「应该也是一样的」去填——落到代码上,稳妥做法是先把实际收到的原始帧原样打印一轮,看清事件类型字段叫什么、错误信息挂在哪一层,再按看到的结构写分支。这一点在 Gemini 流式请求的错误不走 HTTP 状态码 里有更完整的说明。

一个能照着做的排查顺序

把上面几节串起来,遇到工具调用相关的报错,按这个顺序走:

  1. 先确认你拿到的是 error.code 还是 HTTP 状态码。如果日志里只有状态码,先把 error 对象完整打出来再谈其他——这一步不做,后面全是猜。
  2. code 去对请求级那张表。能对上(invalid_requestparameter_unknown 之类),那就是请求侧的问题,按官方给的处置走:对照 API 参考检查输入,或者移除无法识别的参数后重试。
  3. 对不上、且落在结构错误码这一族里,说明问题在模型输出侧。这时候按码分头查:unexpected_tool_call 去比对实际发出的工具声明;too_many_tool_calls 去看编排层的循环有没有收敛条件;malformed_function_callmalformed_tool_call 官方没给更多细节,只能从缩小工具声明复杂度、简化输出结构这类方向试。
  4. 如果你走的是流式,回头确认客户端确实在解析 SSE 的 error 事件。漏了这一步,前三步做得再细也可能一直在查一个假象。

最容易栽的两个坑

第一个坑是把这个码当成 400 处理,然后在请求参数上无休止地打转。它不在这份参考的请求级错误码表里,官方的分族写得很清楚,认清族属能省掉大半时间。

第二个坑是拿别族的处置建议来套。官方给「修改输入后重试」这条建议的对象是生成被阻止码那一族,生成结构错误码这一族的推荐处置,官方文档里没有对应说明。碰到这种情况,诚实的做法是记下码名和 message 原文、缩小可变量逐步定位,而不是照抄一条听起来合理但限定条件对不上的建议。

顺带说一句选型上的判断:如果你还没有历史包袱、不是从 OpenAI 库迁过来的,官方自己的建议是直接调用 Gemini 原生 API,而不是走 OpenAI 兼容层。兼容层做的是参数映射,中间多一层映射就多一层可能对不齐的地方,排查工具调用这类结构问题时尤其如此。已经在兼容层上的也不必急着改。这里有一句话很容易被两头套用,得说清方向:官方讲的是「把 OpenAI 客户端指向 Gemini 兼容层只需改三行」——api_keybase_urlmodel,这条限定的是从 OpenAI 库切到兼容层这一个方向。反过来,从兼容层迁回原生 API 要付出多少成本,官方文档里没有找到相关说明,也别默认它是对称的:原生 REST 走的是 x-goog-api-key 请求头这套鉴权,官方原生示例用的是另一套 SDK,不是同一份客户端换个参数就完事。真要评估,就照着官方原生接入文档自己数一遍要动哪些地方,别拿那句「改三行」当反向的成本估算。

最后把作用域再收一次口,这一步比前面所有排查步骤都重要。上面所有的码名、分族关系、error 对象结构、SSE 错误事件的判断方式,出处都是官方 Interactions API 的这一份错误参考。它们在 generateContent 原生接口和 OpenAI 兼容层上是不是同一套形态,官方文档里没有找到相关说明,本文不做推断。所以真正能带走的,其实不是那张码表本身,而是它逼你养成的两个习惯:把 error.code 原样记进日志,别让它在中间层被吞掉;以及在动手改请求之前,先弄清这次失败到底属于哪一族——是请求没写对,是内容被拦了,还是模型这一次的输出结构不成立。这三种情况的下一步动作完全不同,认错了族,后面花多少时间都是白花。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。