输出到一半就停,先别继续盲调参数。把「停」拆成三类可验证原因:长度上限(max tokens)被填满、迭代/去噪步数不足导致提前收敛、以及提前终止条件(结束标记、停止词、提示里的收尾指令)触发。这三类原因在返回结构里留下的读数不一样,先打印出来,再用同一输入做两次对照,多数情况当场就能分清。
先看返回里的结束原因和输出 token 数:token 数顶到你设的上限、结束原因是 length 类,就是长度截断;结束原因是 stop/eos 但正文收在半句、半个 JSON,多半是提前终止条件命中;结束原因正常、文本也自然收尾,却内容明显没展开,再去调去噪步数。固定 prompt、温度和步数只改长度上限做一组对照,加长后仍截断就把注意力转到步数和提示上。不同接入层的字段名不一致,读数要以你自己环境的返回对象为准。
先把返回结构和结束原因打出来
这一步的目的不是调试模型,而是让「停」有一个可读的证据。不同 SDK 封装字段名差别很大,先用 keys 把结构看一眼,再按实际字段取值,不要照抄某个字段名就往里塞。下面是一个通用骨架,client 和字段替换成你自己接入层的东西:
resp = client.generate(
model="llada2.2-flash",
prompt=prompt,
max_new_tokens=MAX_NEW_TOKENS, # 长度上限,按你的配置项名替换
steps=STEPS, # 迭代/去噪步数,按你的配置项名替换
stop=["..."], # 有停止词就先清空,排除干扰
)
print("keys:", list(resp.keys()))
print("finish:", resp.get("finish_reason") or resp.get("stop_reason"))
print("tokens_out:", (resp.get("usage") or {}).get("completion_tokens"))
print("text_len:", len(resp.get("text") or ""))
print("tail:", repr((resp.get("text") or "")[-40:]))
两种情况的读数差异要盯三处:
- 结束原因:出现 length、max_tokens、budget 一类值,属长度截断;出现 stop、eos、end_turn 一类值,属模型或停止条件主动收尾;字段缺失时不要猜,看 tokens_out 是否正好等于你设置的上限,等于就按截断处理。
- tokens_out 与上限的关系:明显小于上限且结束原因正常,说明模型自己停的,长度不是主因。
- tail 的形态:结尾是半句话、JSON 缺右括号、列表断在中间,通常是硬截断;结尾是完整句号或总结句,是自然收尾,需要往步数和提示方向查。
顺手把停止词配置清空一次再跑,有些接入层默认带停止序列,命中后结束原因看起来很正常,但正文就是断在那里,这一步能把这类干扰先排除掉。
固定输入,只改长度上限做两组对照
把 prompt、temperature、seed、步数全部固定,只动长度上限,跑两组就够。小值取你当前在用的数,大值取小值的两到四倍,注意别超过模型或接口本身允许的上限,超了会直接报错或被静默压回。
| 组别 | 长度上限 | 其余参数 | 实测输出 token 数 | 结束原因 | 正文是否完整收尾 |
|---|---|---|---|---|---|
| A 组 | 当前值(如 256/512) | 固定不动 | 待填 | 待填 | 待填 |
| B 组 | A 组的 2–4 倍 | 与 A 组完全一致 | 待填 | 待填 | 待填 |
判读逻辑:B 组输出长度明显变长、文本收尾完整,说明原因就是长度上限设小了,把上限调到能覆盖该任务典型输出长度即可。B 组 token 数还是顶在 B 组的上限、结束原因仍是 length 类,说明这个任务本身输出就长,继续加上限直到不再顶格,同时评估耗时是否可接受。B 组输出长度和 A 组几乎一样、结束原因也正常,那长度上限基本可以排除,下一步去查步数和停止条件。
再改迭代或去噪步数,看输出是否变完整
扩散式生成里,步数决定去噪迭代做多少轮。步数偏小容易在语义还没铺开时就收敛,表现出来很像「写到一半停了」,但结束原因往往是正常的。这一步建议把长度上限放到足够大(用上一节确认过的 B 组值),只动步数,取低、中、高三档跑同一输入。
| 步数档位 | 具体取值 | 输出长度 | 完整度观察(是否收尾、逻辑是否连贯) | 耗时 | 备注 |
|---|---|---|---|---|---|
| 低 | 待填 | 待填 | 待填 | 待填 | |
| 中 | 待填 | 待填 | 待填 | 待填 | |
| 高 | 待填 | 待填 | 待填 | 待填 |
趋势上,步数从低档往上加,输出完整度和连贯性通常会有改善,耗时也随之增加;到了某个档位后改善会变缓,继续加只是白花时间。判断方法是看低档到中档有没有质的差别:有差别说明之前确实是步数不够,把中档设为该任务的下限;没差别说明步数不是瓶颈,回上一节重新确认长度上限,并去第四节查输入。记录时把「完整度」写具体一点,比如「结尾有总结句」「JSON 能解析」「列表条数符合预期」,比写「好一些」有用。
回头查输入侧:提示里有没有让它停的表述
参数都对、输出却还是在某个位置收住,就要怀疑提示本身在暗示收尾。常见的是「简要回答」「一句话说明」「只给结论」「不要展开」,以及示例里给了一个很短的参考答案,模型会照着那个长度写。
做两到三组改写对照,长度上限和步数保持上一节确认的值不变:
- 原提示原样跑一次,记录输出长度和断点位置。
- 删掉所有长度类限定词,改成「完整给出」「逐条列全」「不要省略中间步骤」,再跑一次。
- 保留限定词但补一句「先列完整清单,再给一句话总结」,看输出是先变长再收尾,还是仍然提前断。
判断是输入影响而非参数:第 2 组长度明显变长且收尾完整,说明前面那次「停」是被指令带偏的;三组长度几乎不变、断点位置也一致,基本可以判定与提示无关,回到参数侧继续查。另外看一眼提示末尾是不是带了模仿性很强的短样例,这类样例对输出长度的影响经常比参数更大。
把可用的参数组合登记下来
上面几轮跑完,手上会有一批「这个任务在这个配置下能出完整结果」的组合。不登记的话,下次换个任务又从零试一遍。建议按任务类型建一张表,只记可核验的读数,不写主观评价。
| 任务类型 | 长度上限 | 步数 | 实测输出长度 | 是否截断 | 结束原因 | 记录时间/备注 |
|---|---|---|---|---|---|---|
| 短摘要 | ||||||
| 结构化 JSON 输出 | ||||||
| 多步推理/长文 |
登记时把「是否截断」写成明确结论(是/否),并附上结束原因,下次出现同样的半截输出,先翻这张表比对读数,能省掉大半排查。长度上限建议留一点余量,不要刚好卡在实测输出长度上;步数按任务类型分别记,长文和结构化输出对步数的需求本来就不一样,混着记会让结论失真。所有读数都以你自己环境的实际返回为准,换模型版本或接入层后需要重新确认一遍。