你在 Ollama 或 llama.cpp 上跑本地的 Llama 3.1 8B 或 Qwen2.5,模型生成一段回复后,在单词中间戛然而止 —— 比如停在 The recommended approach is to use Docke —— 没有结束符(EOS token)、没有报错、看不出任何异常,客户端就是不再收到数据了。
最快的修法: 先看结束原因(finish reason)。Ollama 的 /api/generate 响应里看 done_reason;任何 OpenAI 兼容客户端(/v1、vLLM、llama-server)里看 choices[0].finish_reason。如果是 length,说明撞到了 token 上限 —— 调大 num_predict / max_tokens(或把 num_predict 设为 -1),截断就消失了。如果是 stop 但文字明显没写完,那通常不是真正的结束:可能是上下文窗口被填满、stop 序列在正文中间被命中、代理缓冲了流,或者你的客户端解码了一个没拼完整的 UTF-8 字符。(有一个例外值得知道:截至 2026 年 6 月,Ollama 的 OpenAI 兼容端点有一个已知 bug,输出实际被 max_tokens 截断时它仍可能返回 finish_reason: stop —— 所以拿不准时,交叉核对一下原生的 done_reason。)按下面的分支逐条往下排查。
你属于哪一类
| 症状特征 | 最可能的原因 | 跳转 |
|---|---|---|
done_reason / finish_reason 是 length | num_predict / max_tokens 上限被撞 | 原因 1 |
不论长短都在同一处截断;stop;prompt 很长 | num_ctx 太小,上下文被填满 | 原因 2 |
停在 \n\n、### 或代码围栏后面 | stop 序列在正文中间被命中 | 原因 3 |
直连 :11434 正常,经过代理就截断 | 反向代理缓冲了流 | 原因 4 |
| 末尾字符乱码,常见于中文/CJK 字符;仅流式 | UTF-8 字符跨 token 分块被切开 | 原因 5 |
| 同一 prompt 每次都在同一处截断,确定性出现 | GGUF 损坏或激进量化的解码 bug | 原因 6 |
常见原因
按命中率从高到低排列。
1. num_predict / max_tokens 在单词中间撞上限
最常见的原因,也是唯一会返回 length 结束原因的那一类。分词器会把单词切成子词片段 —— Docker 可能被分成 ["Do", "cker"]。如果上限恰好落在 Do 这个 token 上,输出就以 Do 结尾,读起来像是在单词中间被截断,尽管上限其实是被精确遵守了的。
有两个坑会让人困惑:
- Ollama 的原生端点(
/api/generate、/api/chat)会静默忽略 OpenAI 风格的max_tokens参数。你必须改在options对象里传num_predict(截至 2026 年 6 月,原生端点支持max_tokens别名仍只是一个功能请求)。 - 只有 OpenAI 兼容端点
/v1/chat/completions接受max_tokens,并在内部映射成num_predict。
怎么判断: 结束原因是 length,而且你的上限是个整数(128、256、512、1024)。Ollama 文档里 num_predict 的默认值是 128,所以在原生调用上没设上限时,常常正好停在第 128 个 token。把上限调大,截断就消失了。
2. 生成过程中 context window(num_ctx)被填满
这是默认值改动最近的一个原因,所以那些写在改动之前的文章和工具最容易踩到它。截至 2026 年 6 月,新版 Ollama 不再使用固定的 2048 token 默认值 —— 它按可用显存来定 num_ctx:大致是显存低于 24 GiB 用 4K 上下文,24–48 GiB 用 32K,48 GiB 及以上用 256K(可用 OLLAMA_CONTEXT_LENGTH 或 num_ctx 选项显式设置)。旧版本、第三方封装以及许多 Modelfile 仍然把它钉在 2048。当 prompt 加上已生成的 token 触及 num_ctx 时,生成就会停下 —— 而 Ollama 是静默丢弃最早的那部分 prompt token 来腾空间,而不是报错,所以回复可能在句子中间结束,且 done_reason: stop。
怎么判断: 数一下 prompt 的 token 数,再加上你的 num_predict。如果两者之和逼近 num_ctx,那就是它。运行 ollama show <model>(或在 llama-server 上看 --ctx-size)查看当前生效值;在长 prompt 场景下那里写着 2048 就是铁证。
调大 num_ctx 时有一点要注意:KV 缓存会随上下文窗口增大,一旦它撑爆显存,Ollama 会把它溢出到 CPU 内存,吞吐可能从 50–100 tok/s 掉到 2–5 tok/s。按 prompt 实际需要来调,不要一上来就拉满。
3. stop 序列在正文中间被命中
像 "\n\n"、"###" 或 "<|eot_id|>" 这样的 stop 序列,命中了回复中间的文本,而不是末尾。在流式模式下,服务端会在命中处立刻切断 —— 往往就切在代码块里(每个三反引号围栏处)或一个段落换行处。
怎么判断: 检查请求里每一个 stop 序列。在 Ollama 上用 ollama show <model> --modelfile | grep -i stop 以及你 API 调用里的 stop 字段;在 llama-server 上看 --stop 标志。用不带任何 stop 序列的同一 prompt 重跑一遍;如果这次写完了,那就是某个 stop 字符串触发的。
4. 反向代理缓冲了流
如果 Ollama 或 llama-server 跑在 nginx、Caddy 或负载均衡器后面,而没有配置流式传输,代理可能会缓冲响应,并在超时时一次性刷出,从而在 token 中间切断了流。模型其实生成了完整答案,只是没有完整送达客户端。
怎么判断: 绕过代理,向 Ollama 端口(http://127.0.0.1:11434)发同一个请求。如果直连能写完、但走代理的路径会截断,那问题就在代理上。
5. UTF-8 字符跨流式分块被切开
在流式模式下,服务端一次发出一个 token,而单个 Unicode 字符(任何 CJK 字、emoji 或带重音的字母)可能横跨 token / 字节边界。如果客户端逐块独立解码字节 —— 而不是缓冲到一个完整字符再解码 —— 就会丢弃或弄乱末尾那半个字符,看起来就像在 token 中间被截断。这正是为什么 CJK 输出比 ASCII 更容易截断和乱码:一个汉字是 3 个 UTF-8 字节,所以分块正好结束在字符中间的概率,大约是 1 字节 ASCII 字母的三倍。
怎么判断: 把同一个调用切换为非流式("stream": false)。如果截断消失了,那 bug 在你客户端的流解码逻辑里,而不在模型里。
6. GGUF 损坏或激进量化的解码 bug
部分下载或损坏的 GGUF 会让 llama.cpp 后端在损坏的张量边界处停止解码。另外,非常激进的量化(IQ2/IQ3 档)在某些 token 序列上可能吐出一个杂散的 NULL 或垃圾字节,被下游读取方当成流结束。
怎么判断: 截断是确定性的 —— 同一 prompt、同一 seed,每次都在完全相同的位置停下,与 prompt 内容无关。换成同一模型的 Q4_K_M 或 Q8_0 版本重跑;如果截断消失了,那之前的文件或量化档就是问题所在。
最短修复路径
Step 1:检查结束原因,然后调大 token 上限
# OpenAI 兼容端点(适用于 Ollama /v1、vLLM、llama-server)
import openai
client = openai.OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
resp = client.chat.completions.create(
model="qwen2.5:7b",
messages=[{"role": "user", "content": "Explain Docker networking in detail"}],
max_tokens=2048, # 调到远高于预期长度
)
print("finish_reason:", resp.choices[0].finish_reason) # 想要 "stop",不要 "length"
print(resp.choices[0].message.content)
在 Ollama 的原生 API 上,max_tokens 会被忽略 —— 要在 options 里设 num_predict。用 -1 表示一直生成到 EOS(或到上下文用完),-2 表示填满整个上下文窗口:
curl -s http://localhost:11434/api/generate -d '{
"model": "llama3.1:8b",
"prompt": "Explain Docker networking in detail",
"stream": false,
"options": { "num_predict": -1, "num_ctx": 8192 }
}' | python3 -m json.tool | grep -E '"(done_reason|response)"'
done_reason: length 确认是撞了上限;done_reason: stop 表示模型吐出了 EOS(那就从 Step 2 往后看)。
Step 2:把 num_ctx 设得足够大,让上下文永远填不满
# llama-server
./llama-server -m models/llama-3.1-8b-instruct-Q4_K_M.gguf \
--ctx-size 8192 --n-predict 2048
# Ollama:按调用单独设置(推荐)……
curl -s http://localhost:11434/api/generate -d '{
"model": "llama3.1:8b", "prompt": "your long prompt",
"options": { "num_ctx": 8192, "num_predict": 2048 }
}'
# ……或给整个服务全局设置(重启 ollama serve 生效)
OLLAMA_CONTEXT_LENGTH=8192 ollama serve
用 ollama show llama3.1:8b 确认这个值真的生效了 —— 这里仍然写着 2048 是「调了 num_ctx 却好像没用」最常见的原因。
Step 3:审查并去掉 stop 序列
ollama show llama3.1:8b --modelfile | grep -i stop
# OpenAI 兼容调用:先禁用 stop 序列以隔离问题
resp = client.chat.completions.create(
model="llama3.1:8b",
messages=[{"role": "user", "content": "Explain Docker networking"}],
max_tokens=2048,
stop=None, # 原本是 ["###", "\n\n"]——逐个加回来
)
如果设 stop=None 后输出能写完,就逐个把 stop 字符串加回来,找出罪魁。永远不要把三反引号围栏或裸 \n\n 放进散文或代码输出的 stop 里。
Step 4:对比直连与代理,再修复代理
# 直连 Ollama,绕过任何代理
curl -N -s http://127.0.0.1:11434/api/generate \
-d '{"model": "llama3.1:8b", "prompt": "Count from 1 to 50", "stream": true}'
如果直连能写完、走代理就截断,那就在代理上关掉缓冲。以 nginx 为例:
location /api/ {
proxy_pass http://127.0.0.1:11434;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
chunked_transfer_encoding on;
}
Step 5:流式字节缓冲到每个 UTF-8 字符完整再输出
# 永远不要逐块单独解码原始字节——累积到能解码为止
import sys
def safe_stream(byte_chunks):
buf = b""
for chunk in byte_chunks:
buf += chunk
try:
text = buf.decode("utf-8") # 只有落在字符边界上才会成功
sys.stdout.write(text); sys.stdout.flush()
buf = b""
except UnicodeDecodeError:
continue # 半个字符:再等更多字节
如果你用的是官方 OpenAI 或 ollama-python SDK,这一步已经替你处理好了 —— 这个 bug 只会出现在手写的字节读取代码里。
Step 6:截断是确定性的就校验 GGUF 完整性
# 不跑推理,直接检查元数据
python3 -c "
import gguf
r = gguf.GGUFReader('models/llama-3.1-8b-instruct-Q4_K_M.gguf')
print('Tensors:', len(r.tensors))
print('Arch:', r.fields['general.architecture'])
"
如果文件不完整,重新下载,并把它的 SHA256 与 Hugging Face 仓库文件卡上的值核对;解码稳定性上优先选 Q4_K_M 或 Q8_0,而不是 IQ2/IQ3 量化。
如何确认已修复
- 结束原因现在读出
stop(Ollama 原生看done_reason,/v1看finish_reason),并且文字在一个完整句子处结束。 - 用大约 2 倍于你最长真实 prompt 的输入重跑一遍 —— 回复依然能写完。
- 对于中文或 emoji 输出,末尾字符正确渲染,没有替换符(
�)。 - 如果你修的是代理,那么走代理的路径现在和直连
:11434的路径逐字节一致。
预防建议
- 永远记录结束原因。
stop是健康的;length表示是你自己设小了上限;一个没写完的stop则意味着上下文、stop 序列、代理或解码器有问题。 - 把
num_predict设为-1,或至少设为你最长预期回复的 2 倍 —— 永远别信框架默认值(Ollama 原生是 128)。 - 把
num_ctx设为(最长 prompt + 最长回复 + 约 512 余量)。别假设还是旧的 2048 默认值;截至 2026 年 6 月它是按显存推算的、因机器而异,所以要显式钉死。 - 别把
\n\n、###和代码围栏放进 stop 序列;任何 stop 字符串先单独测试。 - 在任何 Ollama 前面的反向代理上,部署前先设好
proxy_buffering off和 300s 读超时。 - 把流式字节缓冲到 UTF-8 字符边界,或者用一个已经替你做好这件事的 SDK。
- 大体积 GGUF 下载后校验 SHA256,生产解码优先用
Q4_K_M/Q8_0而非IQ2/IQ3。
常见问答 (FAQ)
Q:模型到底是到了 EOS,还是被截断了?
A:在 Ollama 的 /api/generate 上看 done_reason:stop = 真正的 EOS/stop token,length = num_predict 用尽。在任何 OpenAI 兼容端点(/v1、vLLM、llama-server)上看 choices[0].finish_reason,stop 和 length 含义相同。
Q:Ollama 忽略我的 max_tokens——为什么?
A:因为原生的 /api/generate 和 /api/chat 端点不认识 max_tokens;它们用 options 对象里的 num_predict。只有 /v1/chat/completions 接受 max_tokens(并映射成 num_predict)。截至 2026 年 6 月,原生端点的 max_tokens 别名仍是一个开着的功能请求。
Q:Ollama 聊天界面里输出干净,走 API 就截断——为什么?
A:交互界面会一直生成到 EOS,而你的 API 调用在给输出设上限——通常是 OpenAI SDK 的某个默认 max_tokens,或者原生调用上的 num_predict: 128。把上限显式设大(2048 以上或 -1)。
Q:为什么我的中文(或 emoji)输出只在最末尾乱码,英文却不会?
A:一个 CJK 字符是 3 个 UTF-8 字节,所以一个流式分块结束在字符中间的概率,大约是 1 字节 ASCII 字母的三倍。把字节缓冲到能干净解码为止(Step 5),或者改用 "stream": false。
Q:finish_reason 是 stop,但句子明显被砍断了。
A:通常是模型过早吐出了 EOS——常见原因有 num_ctx 被填满(调大它,Step 2)、stop 序列在正文中间被命中(Step 3)、chat template 不匹配注入了过早的 <|eot_id|> / <|im_end|>,或者代理切断了流(Step 4)。还有一个上游 bug 要排除:截至 2026 年 6 月,Ollama 的 /v1 端点在输出实际被 max_tokens 截断时也可能返回 finish_reason: stop,所以在追查其他原因前,先交叉核对原生 done_reason(或者干脆把上限调大,看文字会不会变长)。
Q:能不能续写被截断的回复,而不是整段重生成?
A:可以——把那段不完整的文本作为 assistant 消息追加进去,再发一个简短的 continue user 轮次。不过每次续写都会让 prompt 变长,最终还是会撞上 num_ctx;一开始就把上限调大更省心。
相关阅读
- Chat template 不匹配导致输出全是乱码
- vLLM 报 context length exceeded
- 本地模型冷启动后首 token 很慢
- RoPE scaling 设错让长上下文输出乱掉
- chatgpt-context-window-exceeded
标签: #local-llm #ollama #排查