早上好,我是まさきん。
我把10篇文章一次性丢给 Gemini 的 API,结果返回的正文是空的。原因和解决办法都能在官方文档里找到依据,所以记下来留个备忘。
※本文内容以发布时为准。最新条件请以官方网站为准。
- Gemini API 的响应为空时,先看
finishReason和usageMetadata - 官方说明,输出令牌上限(maxOutputTokens)是针对包含思考令牌在内的总量
- gemini-2.5-flash 把
thinkingBudget设为 0 就能关闭思考(范围是 0〜24576) - 我把 thinkingBudget 设为 0,再把一次10篇拆成每次5篇,就跑通了
- 模型有时会编出正文里没有的截止日期,所以日期要在后处理里和正文核对
我在做什么
我搭了一套流程,每天去抓取某个会员制网站的新文章。每篇文章“是不是优惠信息”的判断和摘要,都交给 Gemini 来做。
通知是发到家人的聊天群里。全部都推的话就没人看了,所以只保留被判定为优惠信息的内容。
模型用的是 gemini-2.5-flash。摘要和分类属于固定套路的处理,我觉得又快又便宜的模型比较合适。
一次丢10篇,正文却是空的
一开始,我把10篇新文章合并到一次请求里。因为想减少调用次数。
可是返回的响应正文是空的。并不是报错中断。响应是回来了,里面却没有内容,就是这样一种状态。
如果是报错,靠异常就能发现。空响应会悄无声息地通过,所以有点麻烦。把它嵌进固定流程时,我觉得这是第一个坑。
原因是思考也会占用输出额度
官方文档说明,Gemini 3 系列和 2.5 系列是使用“思考过程”的模型。也就是在给出回答之前,模型会在内部先做推理。
这个思考默认是开启的。按官方的说法叫“动态思考”,思考量会根据请求的复杂程度自动变化。
关键在于,这部分思考也要消耗令牌。官方写明,max_output_tokens 是包含思考令牌在内的上限。
如果在思考途中就碰到上限,finish_reason 会变成 MAX_TOKENS。此时输出要么中途被截断,要么返回为空。让模型一次思考10篇的判断,思考和输出的总量大概就容易膨胀。
官方文档里还写了关于计费的提醒。即使达到上限、输出为空,已经生成的思考令牌也要计费。返回为空的状态,可能意味着既没有结果,钱却照样花出去。
返回为空时的排查表
空响应其实分好几种。我根据 API 参考文档里的字段,整理了该看哪里。
| 症状 | 在响应里看哪里 | 可能的情况 | 处理方向 |
|---|---|---|---|
| 正文为空或中途截断 | finishReason 为 MAX_TOKENS | 达到了指定的最大令牌数 | 减少思考、减少单次篇数、重新考虑上限 |
| 正文为空且思考很多 | usageMetadata.thoughtsTokenCount 很大 | 思考占用了输出额度 | 调低 thinkingBudget(2.5系列) |
| 正文为空但原因不同 | finishReason 为 SAFETY 等 | 因安全等原因停止 | 重新检查输入内容和提示词 |
| 返回了 JSON 但内容不对 | finishReason 为 STOP | 格式正确但值有误 | 在应用端校验值 |
finishReason 的 STOP 表示在自然的结束点停下。MAX_TOKENS 的定义则是达到了请求中指定的最大令牌数。
所以响应为空时,我觉得最先要看的是 finishReason。只要不是 STOP,就按正文不可用来处理,可能更稳妥。
usageMetadata 的读法
响应里会附带 usageMetadata。API 参考文档里是这样说明的。
| 字段 | 含义 |
|---|---|
promptTokenCount | 提示词(输入)的令牌数 |
candidatesTokenCount | 生成的候选回答的令牌数合计 |
thoughtsTokenCount | 思考模型在思考上用掉的令牌数 |
totalTokenCount | 提示词+思考+候选回答的合计 |
合计被定义为“提示词+思考+回答”,这一点很好理解。思考和回答是作为不同的字段分别统计的。
如果思考的数字大而回答的数字小,就有可能是额度被思考占用了。把这两个数字记进日志,之后追查原因会容易得多。
thinkingBudget 的指定方法
在 2.5 系列中,用 thinkingBudget 这个参数来指定思考量。官方说明,它是给模型一个用于思考的令牌数参考值。
设为 0 可以关闭思考。设为 -1 则是动态思考。在 Python 的 SDK 里,照着官方示例写出来是这样的。
from google import genai
from google.genai import types
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
config=types.GenerateContentConfig(
thinking_config=types.ThinkingConfig(thinking_budget=0)
),
)
用 REST 调用时,要在 generationConfig 里放一个 thinkingConfig。然后在其中用数值传入 thinkingBudget。
不过官方也有一句提醒。据说根据提示词的不同,模型可能会超出预算,也可能用不满。与其当成严格的上限,不如当成参考值来看待。
各模型的 thinkingBudget 范围
范围和默认值因模型而异。我把官方文档里的表格,只挑 2.5 系列的主要模型抄了过来。
| 模型 | 未指定时 | 可指定的范围 | 关闭思考 | 动态思考 |
|---|---|---|---|---|
| 2.5 Pro | 动态思考 | 128〜32768 | 不可以 | -1(默认) |
| 2.5 Flash | 动态思考 | 0〜24576 | 指定为 0 | -1(默认) |
| 2.5 Flash Lite | 不思考 | 512〜24576 | 指定为 0 | -1 |
2.5 Pro 没法关闭思考,我觉得这是很重要的区别。如果把“在 Flash 上设为 0 的配置”原样搬到 Pro 上,前提可能就不成立了。
2.5 Flash Lite 是什么都不指定就不思考的模型。如果只是做摘要和分类,选它也是一种思路。
Gemini 3 系列用 thinkingLevel 来指定
按本文发布时的官方文档,Gemini 3 系列之后的指定方式变了。官方推荐的是 thinkingLevel。用 minimal、low、medium、high 这样的档位来指定。
2.5 系列则相反,并不支持 thinkingLevel。官方写明,2.5 系列请使用 thinkingBudget。
文档里也列出了 Gemini 3 系列的注意事项。Gemini 3.1 Pro 不能关闭思考。Gemini 3 的 Flash 和 Flash-Lite 也被说明不支持完全关闭思考。就连 minimal,官方的说法也是“并不保证关闭思考”。
也就是说,换成新模型之后,“设为 0 来关掉”这一招可能就用不了了。换模型的时候,最好事先想到需要改写配置。
先确认能不能用 2.5 系列
官方的模型列表里,对 2.5 系列有一条备注。内容是仅限过去实际使用过的用户继续使用。
备注里也写着它并非弃用,目前仍会继续通过 API 提供。不过官方建议新项目使用最新的模型。
我的流程是跑在 gemini-2.5-flash 上的。接下来要新搭的朋友,可能得先确认自己的项目里能用哪些模型。
思考令牌的计费
价格页面上明确写着,输出的价格“包含思考令牌”。gemini-2.5-flash 那一栏也是同样的写法。
思考的指南里也有同样意思的说明。思考开启时,回答的费用是输出令牌与思考令牌的合计。
即使设置成只接收思考摘要,计费对象也仍是原始的全部思考令牌。可以理解为:实际被计入的量,可能比看得见的要多。
摘要和分类这类固定处理是每天反复运行的。即便单次很小,思考的部分会不断累积,了解这个结构总没有坏处。
结构化输出(JSON)的指定方法
如果要用程序接收判断和摘要,让模型以 JSON 返回最好处理。Gemini API 为此提供了结构化输出的机制。
按 API 参考文档,做法是在 generationConfig 里放两个字段。把 responseMimeType 指定为 application/json。同时把模式传给 responseJsonSchema。
"generationConfig": {
"responseMimeType": "application/json",
"responseJsonSchema": {
"type": "object",
"properties": {
"is_deal": {"type": "boolean"},
"summary": {"type": "string"},
"deadline": {"type": "string"}
}
}
}
字段名是我为了说明而放的例子。参考文档里写着,传入模式时必须指定 responseMimeType。
另外,SDK 的指南里也有写法略有不同的示例。最好按自己所用 SDK 的版本,重新对照一下官方示例。
官方的最佳实践里有很重要的一句话。结构化输出能保证语法上正确的 JSON,但不保证值在语义上正确。文档写明,使用之前要在应用端做校验。
拆成每次5篇后跑通了
我实际做的处理有两项。把 thinkingBudget 设为 0。然后把一次10篇拆成每次5篇。
加上这两项之后,正文就能正常返回了。至于只做其中一项够不够,我没有分开验证。
| 比较项 | 一次10篇 | 每次5篇分批 |
|---|---|---|
| 调用次数 | 少 | 增加 |
| 单次输出的长度 | 变长 | 变短 |
| 碰到上限的风险 | 容易变大 | 容易变小 |
| 失败时的影响 | 全部重来 | 只重跑那一批 |
| 文章之间张冠李戴 | 可能发生 | 更容易减少 |
一次性处理看上去效率更高。不过一旦失败,全部都得重来。分批之后,失败的范围就被限制在那一批里。
每批几篇最合适,我想会随文章长度和输出形式而变。5篇这个数字,只是在我的用途里碰巧跑通的数字。
有时会编出正文里没有的截止日期
还有一件头疼的事,是截止日期。Gemini 有时会编出正文里根本没写的截止日期。
优惠信息的通知里,截止日期出错是很麻烦的。太早会错过机会,太晚又发现不了已经过期。
我觉得这和结构化输出的那句提醒是相通的。JSON 的形式正确,不代表值就正确。模式里有“截止日期”这一栏,模型或许就会有一股想把它填上的劲头。
日期字符串不在正文里的截止日期一律丢弃
作为对策,我加了一段后处理。思路很简单。
- 让 Gemini 把截止日期作为 JSON 的一个字段返回
- 检查与该日期对应的字符串是否出现在原文正文里
- 如果正文里找不到,就丢弃截止日期这个字段
- 作为没有截止日期的优惠信息来通知
我觉得光是拜托模型“不要编”,并不可靠。所以改成由程序端去和正文对照。
这个方法也有弱点。如果正文的写法和模型返回的写法不一样,连正确的截止日期也会被丢掉。这是一种“与其给出错误的截止日期,不如没有截止日期”的取舍设计。
嵌入固定流程时的要点
经过这次的事,我对在固定流程里加入生成式AI该怎么看,稍微理清了一些。
- 把空响应当作错误处理(看正文长度和 finishReason)
- 把 usageMetadata 记进日志,以便追踪思考和输出的明细
- 对摘要、分类这类任务,优先考虑减少思考的设置
- 一次不要塞太多,控制在失败了也只需重跑那一批的规模
- 模型返回的值,要和原文核对之后再用
官方的最佳实践里也有相近的例子。像事实提取、分类这类简单任务,把思考设为最少或偏低。不需要复杂推理的任务,大概也用不着深度思考。
在工作中,数字也是和出处核对之后才用的。对AI的输出也同样对待,我觉得是最让人安心的做法。
关于每天早上让AI汇总信息的事,我在让AI生成今日简报的文章里也写过。无人值守运行时的权限问题,则和claude -p 中 MCP 被拒绝的原因一文是相近的话题。
常见的失误与陷阱
把容易栽跟头的地方列一下。
- 只看到响应返回了,却漏掉了正文为空
- 想通过调小 maxOutputTokens 来压低费用
- 把 2.5 Flash 用的配置,原样用在无法关闭思考的模型上
- 因为返回的是 JSON,就以为内容也是对的
- 一次合并的篇数太多,一篇失败就全部重来
第二点在官方文档里也有明确提醒。想减少费用和等待时间时,应该调低思考,而不是调小上限。把上限收紧,反而会成为回答被截断的原因。
我踩到的是第一点和第五点。API 的使用和应用版的付费套餐是两回事。应用端的价格,我整理在梳理 Google AI Pro 功能与价格的文章里。
开始之前要确认的事
给想搭类似流程的朋友,整理了一份确认清单。
- 已确认要用的模型在自己的项目里可以使用
- 已对照官方表格确认该模型能否关闭思考
- 已决定 finishReason 不是 STOP 时如何处理
- 把 usageMetadata 中思考与输出的令牌数记入日志
- 单次提交的篇数控制在失败时可以重跑的规模
- 已加入将模型返回的日期和数字与正文核对的处理
我觉得第三项和第六项尤其管用。因为空响应和编造出来的值,都会悄无声息地通过。
常见问题
Q. Gemini API 的响应为空时,最先看什么?
我觉得先看 finishReason 比较好。如果是 MAX_TOKENS,就说明达到了指定的最大令牌数。再结合 thoughtsTokenCount,就能知道思考用掉了多少。
Q. 有没有不能把 thinkingBudget 设为 0 的模型?
按官方的表格,2.5 Pro 不能关闭思考。范围是 128〜32768。Gemini 3 系列是用 thinkingLevel 来指定,其中也有不支持完全关闭思考的模型。
Q. gemini-2.5-flash 的 thinkingBudget 范围是多少?
官方表格里是 0〜24576。未指定时为动态思考。0 表示关闭,-1 表示动态思考。
Q. 思考令牌也要收费吗?
要收费。价格页面上写着,输出的价格“包含思考令牌”。即使达到上限、输出为空,已生成的思考部分也会被计费。
Q. 把 maxOutputTokens 调大就能解决吗?
如果是因为碰到上限被截断,留出余量算是一个办法。不过思考量仍然很大的话,费用和等待时间都会增加。官方给出的指引是调低思考的设置。
Q. 用了结构化输出,就能防止内容出错吗?
防不了。官方写明,能保证的只到语法上正确的 JSON 为止。值在语义上是否正确并不保证。需要在应用端加入校验处理。
Q. 每批拆成几篇比较好?
我觉得没有固定的正确答案。因为会随文章长度和输出形式而变。我把10篇拆成每次5篇就跑通了,但这只是我这个用途下的结果。
Q. 关闭思考之后,判断的质量会不会下降?
我想这取决于任务内容。官方把分类和事实提取列为低思考就足够的例子。我没有用数字比较过质量的变化,在意的朋友最好拿手头的文章试一试。
参考的官方信息
- Gemini thinking(Google AI for Developers)
- Gemini thinking·generateContent 版(Google AI for Developers)
- Generating content·API 参考文档(Google AI for Developers)
- Structured outputs·generateContent 版(Google AI for Developers)
- Gemini models(Google AI for Developers)
- Gemini 2.5 Flash(Google AI for Developers)
- Gemini Developer API pricing(Google AI for Developers)
- Troubleshooting guide(Google AI for Developers)
总结
返回为空的原因,是思考也会占用输出的额度。把 thinkingBudget 设为 0,再拆成每次5篇,就跑通了。
话虽如此,靠配置能解决的只到“能不能返回”为止。返回的内容对不对,还得另外确认。把截止日期和正文核对的后处理,就是为此而设的。
在固定流程里用上生成式AI的朋友,不妨看一看自己是怎么处理空响应的。
本文包含联盟营销广告。如果您通过本站链接申请相关商品或服务,我们可能会从合作企业获得报酬。此外,运营者是乐天集团的员工,也可能通过员工推荐计划获得报酬。文章内容与评价均基于运营者的真实体验与调查完成,与是否存在广告无关,但请您在阅读前了解上述关系。详情请参阅免责声明与联盟营销说明。 免责声明与联盟营销说明
本文包含机器翻译内容。正式条件请以乐天移动官方网站的多语言页面为准。
如果您对乐天移动的通信区域·信号状况有疑问,可以通过官方的 电波改善调查申请表 进行咨询。