【Bug已解决】[serge] integration failure triage - 2026-07-04 解决方案一、现象长什么样serge 把本地模型包成网页聊天底层调model.generate。某次升级 Transformers 后聊天出现一类新故障用户发长一点的消息prompt 超过几十个 token模型立刻停住只回一个空串或单个换行短消息正常长消息必现「秒回空」日志里偶尔出现UserWarning: max_length is deprecated and will be removed in a future version. Use max_new_tokens instead.或者更硬的错误ValueError: max_length and max_new_tokens were both set. Please only set one.这种「长 prompt 必空回」让 serje 看起来像「模型抽风」其实是生成参数在升级后契约变了集成层没跟上。二、背景serje 早期为了限制回复长度这么写prompt_len input_ids.shape[1] gen model.generate( input_ids, max_lengthprompt_len 256, # 想表达「最多再生成 256 个新 token」 do_sampleTrue, )这里的意图是「prompt 新生成 ≤ prompt_len 256」。在老版本transformers 里如果没设max_new_tokensmax_length确实被当成「序列总长度上限」于是「新 token 数 max_length − prompt_len」≈ 256行为符合预期。但 transformers 后来明确区分了两个参数max_length序列绝对总长度上限含 prompt。max_new_tokens新生成的 token 数上限不含 prompt。而且当两者同时出现、或max_length小于当前序列长度时新版本的行为更严格要么警告后忽略其一要么直接ValueError。更关键的是——max_length是「总长度」如果 serje 传的max_length prompt_len 256在某个版本里被解释成「总长度必须正好 ≤ 这个值」而输入本身已经接近这个值模型就「没有空间生成新 token」于是秒回空。长 prompt 必现、短 prompt 正常正是这个机制的体现prompt 越长max_length - prompt_len越小长 prompt 直接把生成额度吃光。三、根因根因一句话serje 用max_length绝对总长度来表达「新生成长度」的意图且常常和max_new_tokens混用transformers 升级后收紧了二者契约导致长 prompt 时生成额度被 prompt 自身吃掉输出空/截断。三点展开语义错配max_length是总长度却被当成「新 token 数」。prompt 越长留给生成的空间越小长 prompt 直接归零。双参数冲突serje 升级后可能某处又补了max_new_tokens两者同设触发ValueError整次请求失败。缺兜底没有「当max_length ≤ prompt_len时自动改用max_new_tokens」的兼容层于是老代码在新版本直接崩。不是模型问题是「生成长度参数契约」在集成层没对齐。四、最小可运行复现不依赖真实大模型用一个最小生成模拟max_length的「总长度」语义import torch import torch.nn as nn def fake_generate(input_len, max_length, max_new_tokensNone): # 模拟 transformersmax_length 是「总长度上限」 if max_new_tokens is not None and max_length is not None: raise ValueError(max_length 与 max_new_tokens 不能同时设) if max_new_tokens is not None: new max_new_tokens else: new max_length - input_len # serje 的旧算法 new max(new, 0) return new # 短 promptinput_len10, 想生成 256 print(短 prompt 新 token 数:, fake_generate(10, max_length10 256)) # 256 正常 # 长 promptinput_len300 print(长 prompt 新 token 数:, fake_generate(300, max_length300 256)) # 仍是 256 # 但若 transformers 把 max_length 当成「硬性总长度」且输入已300 print(长 prompt 但 max_length 被当硬上限300:, fake_generate(300, max_length300)) # 0 - 空回关键在最后一行max_length被当成「总长度硬上限」且恰好等于 prompt 长度时新 token 数 0模型秒回空——这就是长 prompt 必现空回的精确复现。五、解决方案第一层最小直接修复最小修复彻底改用max_new_tokens表达「生成长度」永远不要再用max_length去减 prompt若必须兼容老代码先算清再用max_new_tokens。from transformers import AutoModelForCausalLM, AutoTokenizer tokenizer AutoTokenizer.from_pretrained(your-model) model AutoModelForCausalLM.from_pretrained(your-model) def chat(input_ids, max_new_tokens: int 256): # 只设 max_new_tokens绝不和 max_length 混用 out model.generate( input_ids, max_new_tokensmax_new_tokens, # 表达「新生成多少」 do_sampleTrue, temperature0.7, pad_token_idtokenizer.eos_token_id, ) return out[0][input_ids.shape[1]:] # 兼容老接口的写法把「max_length 意图」翻译成 max_new_tokens def legacy_compat(input_ids, max_lengthNone, max_new_tokensNone): if max_new_tokens is not None: return chat(input_ids, max_new_tokens) if max_length is not None: # 始终把总长度意图换算成「新 token 数」并兜底 ≥ 1 new max(max_length - input_ids.shape[1], 1) return chat(input_ids, new) return chat(input_ids, 256)要点max_new_tokens语义清晰与 prompt 长度无关长 prompt 也照样生成。不与max_length同设杜绝ValueError。兼容层把老max_length意图换算成max_new_tokens并兜底至少 1 个新 token避免空回。这一步单独就让「长 prompt 秒回空」消失。六、解决方案第二层结构性改进第一层是「改生成调用」。但 serje 里流式、非流式、各种入口都可能各自写max_length。更稳的做法是把「生成长度怎么定」收敛成一个单一策略对象所有入口共用。from dataclasses import dataclass, field from typing import Optional dataclass class SergeGenerationConfig: serge 生成长度与采样参数的单一事实来源。 # 永远用「新 token 数」表达不用总长度 max_new_tokens: int 256 min_new_tokens: int 1 do_sample: bool True temperature: float 0.7 top_p: float 0.95 # 兼容老代码的「总长度意图」上限仅用于换算不参与 generate legacy_max_length: Optional[int] None def resolve(self, prompt_len: int) - dict: new self.max_new_tokens if self.legacy_max_length is not None: # 把老意图换算且保证至少 min_new_tokens new max(self.legacy_max_length - prompt_len, self.min_new_tokens) new min(new, self.max_new_tokens) return { max_new_tokens: new, min_new_tokens: self.min_new_tokens, do_sample: self.do_sample, temperature: self.temperature, top_p: self.top_p, } # 用法 cfg SergeGenerationConfig(max_new_tokens256, legacy_max_length300 256) for prompt_len in [10, 100, 300, 500]: gen_kwargs cfg.resolve(prompt_len) # model.generate(input_ids, **gen_kwargs) print(fprompt_len{prompt_len} - {gen_kwargs[max_new_tokens]} 新 token)结构收益单一事实来源生成长度永远走max_new_tokensmax_length只作为「可选项」被换算杜绝双参数冲突。兜底min_new_tokens保证至少生成 1 个 token长 prompt 也不会空回。可测试resolve(prompt_len)是纯函数CI 可断言「任意 prompt 长度都 ≥ min_new_tokens」。七、解决方案第三层断言 / CI 守护写 pytest 守三条(1) 从不同时设max_length和max_new_tokens(2) 长 prompt 也能生成 ≥1 个新 token(3) 老max_length意图被正确换算。import pytest from your_lib import SergeGenerationConfig def test_never_sets_both_params(): cfg SergeGenerationConfig(max_new_tokens128) kw cfg.resolve(prompt_len10) assert max_length not in kw, 绝不能出现 max_length assert kw[max_new_tokens] 128 pytest.mark.parametrize(prompt_len, [10, 100, 300, 500, 2000]) def test_long_prompt_still_generates(prompt_len): cfg SergeGenerationConfig(max_new_tokens256, legacy_max_lengthprompt_len 256) kw cfg.resolve(prompt_len) assert kw[max_new_tokens] cfg.min_new_tokens, fprompt_len{prompt_len} 不应空回 def test_legacy_max_length_converted(): # 老代码max_length prompt_len 256期望换算后仍是 ~256 cfg SergeGenerationConfig(max_new_tokens256, legacy_max_length300 256) kw cfg.resolve(prompt_len300) assert kw[max_new_tokens] 256, 总长度意图应换算成新 token 数 def test_legacy_too_short_falls_back_to_min(): cfg SergeGenerationConfig(max_new_tokens256, min_new_tokens1, legacy_max_length50) kw cfg.resolve(prompt_len300) # 总长度 50 prompt 300 assert kw[max_new_tokens] 1, 即使老 max_length 小于 prompt也应兜底 ≥1CI 常驻跑这四条后任何「重新混用 max_length」「长 prompt 空回」的回归都会立刻爆红。八、排查清单serje 出现「长 prompt 空回 / 截断」时按顺序查先确认是不是「短消息正常、长消息空回」——是的话高度怀疑max_length契约。全局搜max_length, 看是否和max_new_tokens同时出现或是否用max_length - prompt_len算长度。确认model.generate调用里只设max_new_tokens绝不设max_length。若代码库有老接口传max_length确认有换算层把它变成max_new_tokens且兜底 ≥1。确认没有把max_length当成「硬总长度上限」去限制输入——输入长度应单独用tokenizer.model_max_length截断与生成长度无关。流式streamer生成时确认max_new_tokens在流式中仍生效没有被重置。升级 transformers 后跑一次「长 prompt 冒烟测试」断言返回非空。九、小结serje 升级 transformers 后的「长 prompt 秒回空」根子是集成层用max_length绝对总长度去表达「新生成长度」的意图且常与max_new_tokens混用新版收紧二者契约后长 prompt 把生成额度自身吃掉于是空回或报错。修复三层次第一层彻底改用max_new_tokens、不双设参数、老意图换算并兜底 ≥1第二层用SergeGenerationConfigdataclass 把生成长度收敛为单一策略第三层用 pytest 守「从不双设」「长 prompt 仍生成」「老意图正确换算」。工程启示任何封装model.generate的中间层都把「生成长度」统一用max_new_tokens表达把长度契约和输入截断解耦。max_length是历史包袱新代码一律别碰要兼容老调用就在适配层换算绝不让歧义流到generate里。