server: PEG chat parser returns HTTP 500 on complete generations containing invalid UTF-8 (no content fallback, unlike legacy parser)

当 llama.cpp 服务端模型在一次完整生成中输出了非法 UTF-8 字节(例如 OCR/视觉模型或字节回退 token 产生的乱码),PEG 聊天解析器会抛出异常并导致 /chat/completions 返回 HTTP 500;优先确认是否是 PEG 解析路径在完整生成上失败,而同样的内容在

快速结论:当 llama.cpp 服务端模型在一次完整生成中输出了非法 UTF-8 字节(例如 OCR/视觉模型或字节回退 token 产生的乱码),PEG 聊天解析器会抛出异常并导致 /chat/completions 返回 HTTP 500;优先确认是否是 PEG 解析路径在完整生成上失败,而同样的内容在旧解析器下会降级为纯文本内容。

适用环境:问题在 llama.cpp commit 634275f 观察到的,aarch64 + CUDA(Jetson Thor),但 Issue 明确说明与平台无关;模型为 PaddleOCR-VL-1.6 GGUF(自定义 chat template,无格式检测器匹配,走 plain/no-tools PEG 解析器)。评论中还提到 macOS / Apple M4 Pro / Metal、Qwen3.8-Flash-Next IQ4_XS、启用 MTP、基于 mihailescu2m/llama.cpp 流式 fork 且固定在 8e0e527f67f92d41d0a41ed7e9e7e0e1dd420429、Pi coding agent 0.84.3、65536 token 上下文,但评论者说明这些是针对该 fork 的编译解析器库测试,并非当前上游 master。

最快修复方案:暂无确认的一步修复方案;Issue 的最终关闭是通过 PR #29161 完成,评论中提到的 common_chat_peg_parse 补丁只是提问者在其生产环境中运行的降级方案,未被维护者确认为最终采用方案。可优先尝试:升级或拉取包含 PR #29161 的 llama.cpp 版本,确认 PEG 解析器在完整生成上遇到无法消费的输入时不再直接抛 500。

注意事项:评论者指出,单纯把完整生成降级为纯文本内容虽然能避免 500,但会丢失其中有效的结构化工具调用(tool call),导致 agent 无法继续工作;因此还需要一个能容忍 reasoning 中非法 UTF-8、同时保留有效工具调用的恢复路径。该补丁保留 partial parse 抛异常的行为以维持流式语义不变,这一点在评论中未被反对,但最终合并方案是否与此一致需以 PR #29161 为准。

问题场景

用户在运行 llama.cpp 的 server,通过 OpenAI 兼容的 /chat/completions 接口进行推理。触发条件是一次本应完整的生成中包含了非法 UTF-8 字节序列。Issue 正文给出的典型场景是 OCR/视觉模型转写热敏小票时,£ 这类符号被字节回退 token 打乱,产生非法 UTF-8。此时使用的模型 PaddleOCR-VL-1.6 GGUF 带有自定义 chat template,没有格式检测器匹配,因此落到 plain/no-tools 的 PEG 解析器。评论中另一名用户报告了同类失败机制,但发生在 Qwen3.8-Flash-Next 的工具调用场景:模型在 <think> 段落中输出了非法 UTF-8 字节 E6 80 后接 3D=),而后面跟着一个完整、本应有效的 XML 风格工具调用。

报错原文

server: PEG chat parser returns HTTP 500 on complete generations containing invalid UTF-8 (no content fallback, unlike legacy parser)

Failed to parse input at pos 0: …

The model produced output that does not match the expected peg-native format

原因分析

Issue 正文给出的机制是:common_chat_peg_parse(common/chat.cpp)在 result.fail()is_partial == false 时直接抛出异常,server 将其映射为 HTTP 500。根因是 common/peg-parser.cpp 中的不一致:common_peg_until_parser 在非法 UTF-8 上硬失败,而它的同类 common_peg_chars_parser 会优雅降级(在坏字节前成功)。Issue 还特别指出,只修 until-parser 并不足够:提前停止 span 会把坏字节留在未消费状态,外层必须到达输入末尾的 sequence 仍会失败。结果就是,对于一个 plain-content 解析器(p.content(p.rest()) + p.end()),输出中任意一个非法字节都会让整个回复被拒绝,即使调用方只想要原始内容。评论中的复现进一步确认:把原始输出中非法 UTF-8 序列替换掉后解析即可成功,并产生恰好一个 bash 工具调用,其命令参数字节完全不变。

环境排查

  • 确认 llama.cpp 的 commit/版本;Issue 正文在 commit 634275f 观察到,正文称当前 master 按代码检查也存在该问题,但评论者测试的是固定在 8e0e527f67f92d41d0a41ed7e9e7e0e1dd420429 的 fork,并说明该 fork 的 PEG 解析器未改动。
  • 确认是否走 PEG 解析路径:模型是否使用自定义 chat template、是否没有格式检测器匹配、是否落到 plain/no-tools PEG 解析器。
  • 确认模型和 tokenizer 是否可能产生字节回退 token,从而生成非法 UTF-8。
  • 评论环境额外涉及:macOS、Apple M4 Pro / Metal、Qwen3.8-Flash-Next IQ4_XS、启用 MTP、Pi coding agent 0.84.3、65536 token 上下文;评论者注明 MTP drafter 与主模型 token 词表相同,但 MTP 是否影响非法输出频率尚未测试。
  • 确认该次生成是否本应完整结束(评论中 n_tokens = 55199, truncated = 0,低于上下文上限)。

解决步骤

  1. 先抓取触发请求对应的原始生成内容,确认其中是否含有非法 UTF-8 字节序列,而不是普通的解析格式错误。
  2. 确认当前 llama.cpp 是否已包含 PR #29161;Issue 通过 #29161 关闭,说明修复应从该 PR 引入。若尚未包含,先更新到包含该 PR 的版本再复测。
  3. 若需要在旧版本上临时绕过,Issue 提问者在生产中使用过的补丁思路是:在 common_chat_peg_parse 中保留 partial parse 的 throw(流式语义不变),但对语法无法消费的完整生成降级为 content-only 消息,模仿旧解析器路径。
  4. 应用该降级思路时注意 Issue 的提醒:只改 until-parser 在非法 UTF-8 上提前停止并不足够,因为坏字节未被消费,外层到达输入末尾的 sequence 仍会失败。
  5. 如果场景涉及工具调用,不要只做纯文本降级;评论指出这样会丢失有效工具调用。需要的是既能容忍 reasoning 中非法 UTF-8、又能保留有效工具调用的恢复路径,并且不能静默修改工具名或参数。
  6. 复测时使用 Issue 提供的直接复现输入检验解析行为:common_chat_peg_parse(empty_arena, "SN\xba" "ES", /*is_partial=*/false, {});评论中的 117 字节复现输入可用于工具调用场景验证。

验证方法

对之前会返回 500 的包含非法 UTF-8 的完整生成,重新请求 /chat/completions,确认不再返回 HTTP 500。若采用 content-only 降级,确认返回的是原始 OCR/生成文本内容。若涉及工具调用,确认有效工具调用仍作为结构化调用返回到 agent,且工具名和参数与原始输出逐字节一致;Issue 评论中提到把非法 UTF-8 替换为合法字节后,解析应产生恰好一个 bash 工具调用且命令参数不变,可作为对照基线。

参考来源

ggml-org/llama.cpp #27543

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 24675

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注