使用 Ollama 0.4 调用 Qwen2.5-72B-Instruct 生成一篇文章,输出进行到第三段中间突然停止,没有任何错误信息,API 返回的 finish_reason 是 length 而不是 stop。换成直接用 llama.cpp 调用,输出在某个汉字的第一个字节处截断,终端出现乱码。这两种场景都是截断,但原因完全不同:前者是 max_tokens 到达上限,后者是 UTF-8 多字节字符在字节流中间被切割。
常见原因
1. max_tokens / num_predict 设置过小
最常见的截断原因。API 调用时 max_tokens 默认值因客户端不同而异:Ollama 的 /api/generate 默认 num_predict 为 128,OpenAI 兼容 API 可能是 256 或 512。生成稍长的内容就会触发上限。
怎么判断:检查 API 响应中的 finish_reason 字段,若为 length(而非 stop),确认是 max_tokens 触发了截断;查看调用代码中 max_tokens/num_predict/max_new_tokens 的设置值。
2. 聊天模板中 EOS token 被错误触发
某些模型的聊天模板在特定模式(如 markdown 代码块结束、特定标点符号序列)下会提前生成 EOS token。Llama-3 的 <|eot_id|> 和 Qwen2 的 <|im_end|> 在某些量化版本或错误模板下可能在内容中间被触发。
怎么判断:使用 --verbose 或 --debug 模式运行,观察停止位置前的 token 序列;若看到 EOS/EOT token ID 出现在预期之外的位置,就是这个原因。
3. 流式传输中 UTF-8 字符在字节边界被切割
llama.cpp 的流式模式(--stream)按 token 输出,而一个 Unicode 字符可能对应多个 token 或者 token 在 UTF-8 字节中间,下游应用直接截断字节流而未做字符边界对齐会产生乱码和截断感。
怎么判断:将流式输出改为非流式(等待完整响应),若截断消失,问题在流式传输的缓冲处理逻辑。
4. context window 被填满导致生成提前终止
输入 prompt 过长,加上已生成的 token,超过了模型的上下文窗口。后续生成会被强制终止,有时是在句子中间。
怎么判断:计算 prompt token 数 + 预期输出 token 数,是否超过 --ctx-size(llama.cpp)或 context_length(Ollama)设置的值;若 prompt 超过 80% 的上下文窗口,生成空间不足。
5. stop 序列配置错误匹配到正文内容
在 API 调用中设置了 stop 参数(停止词),若 stop 序列在正文中自然出现,生成会在该位置提前停止。例如将 "\n\n" 设为 stop 序列,但正文使用段落间双换行格式,每段都会触发截断。
怎么判断:检查 API 调用参数中的 stop 字段;临时移除 stop 参数重新测试。
6. 量化模型的 token 解码 bug
某些激进量化版本(如 IQ3_XXS)在解码特定 token 序列时存在 bug,生成到特定字符组合时输出 NULL 字符或垃圾字节,下游接收端截断处理后表现为输出中断。
怎么判断:换成 Q4_K_M 或 Q8_0 版本重复同一 prompt,若截断消失,是量化版本 bug。
最短修复路径
Step 1:检查 finish_reason 并提高 max_tokens
import openai
client = openai.OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama"
)
response = client.chat.completions.create(
model="qwen2.5:72b",
messages=[{"role": "user", "content": "写一篇 500 字的文章"}],
max_tokens=2048 # 从默认值提高到足够大
)
print("finish_reason:", response.choices[0].finish_reason)
# 应该是 "stop",不是 "length"
print(response.choices[0].message.content)
Step 2:Ollama API 设置足够大的 num_predict
# Ollama 原生 API
curl http://localhost:11434/api/generate -d '{
"model": "qwen2.5:72b",
"prompt": "写一篇 500 字的文章",
"options": {
"num_predict": 2048,
"num_ctx": 8192
},
"stream": false
}'
Step 3:llama.cpp 命令行增加生成长度限制
# -n 指定最大生成 token 数,-1 表示不限制(直到 EOS)
./llama-cli -m model-Q4_K_M.gguf \
-p "写一篇 500 字的文章" \
-n -1 \
--ctx-size 8192 \
--temp 0.7
Step 4:处理流式传输的 UTF-8 截断问题
import sys
def stream_safe_decode(chunks):
"""安全处理流式 UTF-8 输出,避免在字符中间截断"""
buffer = b""
for chunk in chunks:
if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content:
buffer += chunk.choices[0].delta.content.encode('utf-8')
# 找到最后一个完整 UTF-8 字符的位置
try:
decoded = buffer.decode('utf-8')
sys.stdout.write(decoded)
sys.stdout.flush()
buffer = b""
except UnicodeDecodeError:
# 继续等待更多字节
pass
Step 5:清除或修正 stop 序列
# 移除可能误触发的 stop 序列
response = client.chat.completions.create(
model="qwen2.5:72b",
messages=[{"role": "user", "content": prompt}],
max_tokens=2048,
stop=None # 明确设为 None 禁用 stop 序列
)
预防建议
- 对于长文生成任务,始终显式设置
max_tokens为任务预期输出的 1.5 倍以上,不依赖默认值。 - API 调用后始终检查
finish_reason,若不是stop则记录警告日志并提示用户内容可能不完整。 - 使用流式传输时,在应用层做完整 UTF-8 字符重组,而不是直接截断字节流显示。
- 避免将常见标点符号或空白字符设为 stop 序列,若必须使用,测试正文内容是否会意外触发。
- 生成前估算 prompt token 数,确保 prompt + 预期输出不超过上下文窗口的 90%。
- 对于代码生成场景,将代码块结束符(如
```)从 stop 序列中移除,避免代码被截断。 - 定期对比不同量化版本的输出,发现截断问题时优先换 Q4_K_M 测试以排除量化 bug。
常见问答 (FAQ)
Q: finish_reason 是 stop 但输出明显不完整,句子没有结尾,怎么解释?
A: 模型生成了 EOS token,认为输出完成了,但实际上是模型提前认为任务结束。这通常是 chat template 问题或 system prompt 影响了模型的完成判断。尝试在 system prompt 中加入”请完整回答,不要中途停止”的指令。
Q: Ollama 的默认 num_predict 是多少?
A: Ollama 0.4 对 /api/generate 端点的默认 num_predict 是 -1(不限制),但 OpenAI 兼容接口 /v1/chat/completions 的默认 max_tokens 是 4096。两个接口的默认值不同,注意区分。
Q: 输出截断后能否续写? A: 可以,将截断的输出作为 assistant 消息放入历史,再发送一个”请继续”的 user 消息。但多次续写会累积 prompt 长度,最终还是会触及上下文窗口限制。
Q: 为什么中文比英文更容易出现截断显示乱码?
A: 中文字符是 UTF-8 3字节编码,一个汉字被切割的概率是英文字符的 3 倍。在流式传输下尤为明显。使用支持 UTF-8 流式处理的客户端库(如 Python 的 httpx 配合 charset-normalizer)可以解决。
相关阅读
- Chat template 不匹配导致输出全是乱码
- vLLM 报 context length exceeded
- RoPE scaling 设错让长上下文输出乱掉
- llama.cpp 换更激进量化后质量明显下降
- Gemini 上下文太短
标签: #local-llm #ollama #排查