Gemini 4 Carbon 输出变短——是输入太长被截断还是参数设错了?

文章导读
输出变短往往不是单一原因。Gemini 4 Carbon 这类模型本身不会因为输入长就主动“少说两句”,但接入层会:历史消息被裁剪、输出长度参数被改小、停止条件被命中、系统提示词要求简短,都会让返回长度下降。判断顺序建议固定成一条:先量输入规模,确认输入是否真的变长;再固定输入、只调输出长度参数做对照;两者都不动,再去查停止条件和提示词。每一步都留下请求体和返回原因的记录,凭感觉对比“好像变短了”
📋 目录
  1. Ⅰ 先量出每次请求的输入规模
  2. Ⅱ 检查应用层是否在拼接时裁剪了内容
  3. Ⅲ 单独调整输出长度参数做对照
  4. Ⅳ 核对停止条件与系统提示词
  5. Ⅴ 把结论固化到默认配置与测试用例
A A

输出变短往往不是单一原因。Gemini 4 Carbon 这类模型本身不会因为输入长就主动“少说两句”,但接入层会:历史消息被裁剪、输出长度参数被改小、停止条件被命中、系统提示词要求简短,都会让返回长度下降。判断顺序建议固定成一条:先量输入规模,确认输入是否真的变长;再固定输入、只调输出长度参数做对照;两者都不动,再去查停止条件和提示词。每一步都留下请求体和返回原因的记录,凭感觉对比“好像变短了”很容易误判。

建议用控制变量法分流:固定提示词和参数,只缩短输入,看输出是否恢复;再固定输入,只改输出长度参数,看返回长度是否跟着变。两个变量都不影响时,重点查应用层拼接裁剪、停止条件命中、系统提示词里的“简短”表述。判断依据应是请求体字节数、参数取值和返回的停止原因;在确认原因前,不要直接把输出上限调大当作修复。

先量出每次请求的输入规模

“输入变长挤占输出空间”这个怀疑,必须先有数字支撑。输入长度有两个口径:一是纯文本字符数,二是序列化后请求体的字节数,两者在中文和 JSON 转义下差别不小,建议都记。下面这段是通用骨架,字段名按你实际的请求结构替换即可,在发请求前调用:

import json, hashlib

def measure(payload: dict) -> dict:
    msgs = payload.get("messages") or payload.get("contents") or []
    text = "".join(str(m.get("content", "")) for m in msgs)
    body = json.dumps(payload, ensure_ascii=False)
    return {
        "input_chars": len(text),
        "body_bytes": len(body.encode("utf-8")),
        "msg_count": len(msgs),
        "body_sha1": hashlib.sha1(body.encode("utf-8")).hexdigest()[:12],
    }

把 measure() 的返回值和模型返回长度写进同一行日志,按会话逐条比对。表里至少要这些字段,缺一个就可能把结论归错:

  • 会话 ID、请求序号、时间戳(用于确认“最近”是从哪一条开始变短的)
  • 输入字符数、请求体字节数、历史消息条数
  • 输出长度参数的实际取值(而不是配置文件里以为的值)
  • 返回字符数、返回的停止原因字段(finish_reason 或同类字段,按实际 SDK 确认)

如果对照发现输入字符数这条曲线在变短的时间点上并没有明显抬升,那“输入太长被截断”这个假设基本可以先放下,转向参数和停止条件。反之,如果输入确实一路增长,还要看增长来自用户新输入还是历史消息累积——这决定了该做摘要还是该调整裁剪策略。

检查应用层是否在拼接时裁剪了内容

很多“输入太长被截断”的现场,其实是自己的代码在拼历史消息时悄悄砍掉了内容,模型根本没收到完整上下文。拼接通常发生在构造 messages 数组的函数里,找 `build_messages`、`prepare_prompt`、`format_history` 这类命名,或者直接搜 `[-10:]`、`len(` 加 `>` 的组合。常见裁剪写法有两类:

# 写法一:按条数截断,最容易在无人察觉时生效
history = history[-8:]

# 写法二:按字符数截断,注意保留方向
if len(prompt) > 8000:
    prompt = prompt[-8000:]   # 保留尾部,头部被丢掉
    # 也有写成 prompt[:8000] 的,保留的是开头,丢的正好是最近几轮

# 写死空值的写法同样会压缩输出空间
if not params.get("max_output_tokens"):
    params["max_output_tokens"] = 256

确认方法很直接:在真正发请求的那一行之前,把最终请求体完整打印一次,开关用环境变量或 debug 级别控制,避免把用户内容长期留在日志里。

Gemini 4 Carbon 输出变短——是输入太长被截断还是参数设错了?
logging.debug("FINAL_BODY %s", json.dumps(payload, ensure_ascii=False))
# 或只在本地调试时:
# print(json.dumps(payload, ensure_ascii=False)[:2000])

拿到 FINAL_BODY 后与第一步记的字符数对照。如果日志里的最终请求体比预期短,问题在应用层,不在模型;如果请求体是完整的,才继续查模型侧参数。

单独调整输出长度参数做对照

这一步只允许改一个变量。输出长度参数一般叫 max_output_tokens、max_tokens 或 generation_config 下的同义字段,位置在请求体顶层或嵌套在生成配置里,具体路径需要按你接入的 SDK 确认,不要照抄别人的字段名。做法是:输入固定成同一段文本,其他参数全部不动,只把输出上限从一个较小值改到较大值,各发若干次。

{
  "model": "按你实际接入的模型名填写",
  "messages": [{"role": "user", "content": "同一段固定输入,两次请求必须完全一致"}],
  "max_output_tokens": 256
}

{
  "model": "按你实际接入的模型名填写",
  "messages": [{"role": "user", "content": "同一段固定输入,两次请求必须完全一致"}],
  "max_output_tokens": 1024
}

结果按下列字段记,不要只记“变长/没变”:

  • 组别(小上限 / 大上限)、输出长度参数取值、输入字符数
  • 返回字符数、返回的停止原因
  • 停止原因是命中上限、命中自定义停止串,还是模型自然结束

如果小上限组的返回长度贴近上限、大上限组明显更长,说明输出确实被参数卡住,这时要回头查是谁把这个值改小或写死的。如果两组返回长度差不多,且停止原因都不是上限,输出变短和这个参数关系不大,继续看下一步。

核对停止条件与系统提示词

停止条件(stop、stop_sequences 等)一旦命中所配置的字符串,生成就会提前结束,表现是返回内容明显偏短但停止原因不是上限。配置位置通常在请求体顶层或生成配置里,也可能来自服务端模板,需要结合环境确认。排查时可以先把自定义停止串清空跑一次对照:如果清空后返回变长,就说明是停止串命中,再去检查停止串是不是过短或过常见。

Gemini 4 Carbon 输出变短——是输入太长被截断还是参数设错了?

系统提示词的问题更隐蔽。用了“简洁”“一句话说明”“不超过三行”“只给结论”“concisely”这类表述后,模型会按此压缩输出,而开发者往往只盯着代码参数。检查方法是对系统提示词做关键词检索:

grep -nE "简短|简洁|一句话|不超过|只给结论|要点即可|concisely|be brief" prompt/*.txt
# 同时看模板渲染后的最终系统提示词,而不是模板源文件

找到后不要直接删,先做一次只改系统提示词、其他全固定的对照请求,确认返回长度变化确实来自提示词表述,再把改动合并进配置。

把结论固化到默认配置与测试用例

排查完很容易再次被改回去。建议把默认值和原因写在同一处,让下一个人看到取值就知道为什么不能动:

DEFAULTS = {
    # 输出上限:低于该值正常回答会被截断,调整需走评审
    "max_output_tokens": 1024,
    # 输入预算:超出时先做历史摘要,不要在拼接处静默截断
    "max_input_chars": 12000,
    # 不使用自定义停止串;如需启用必须写明命中场景
    "stop": None,
}

# 拼接历史时显式记录裁剪行为,避免静默丢内容
if total_chars > DEFAULTS["max_input_chars"]:
    logging.warning("input over budget: %s chars, summarize instead of truncate", total_chars)

再补一条断言输出长度下限的测试。下限值按业务里最短可接受的回答设定,不要照抄示例里的数字:

def test_answer_not_truncated():
    resp = call_model(SAMPLE_PROMPT, max_output_tokens=DEFAULTS["max_output_tokens"])
    text = extract_text(resp)
    assert resp.get("finish_reason") != "max_tokens", "输出被上限截断"
    assert len(text) >= MIN_EXPECTED_CHARS, f"输出过短:{len(text)} 字符"

这条测试的价值在于:后续任何人改小输出上限、加回裁剪逻辑、或在系统提示词里塞进“只给结论”,都会在这里暴露出来,而不是等用户在会话里发现回答变短。上述结论都建立在请求体、参数取值和停止原因可观测的前提下;如果接入层拿不到返回的停止原因,需要先在网关或封装层把这个字段透出来,再做判断。