【Bug已解决】[Bug]: cc with vllm glm-5-fp8 function call error 解决方案

【Bug已解决】[Bug]: cc with vllm glm-5-fp8 function call error 解决方案 【Bug已解决】[Bug]: cc with vllm glm-5-fp8 function call error 解决方案一、现象长什么样用 vLLM 部署 GLM-5 的 FP8 量化版并开启 function calling工具调用即让模型输出可被解析的 tool call时会撞到一类解析/输出相关的报错ValueError: Failed to parse tool call from model output: |tool_call|{name: get_weather, ...}|/tool_call|... extra tokens或TypeError: tool_call[function][arguments] is not a valid JSON或更隐蔽的模型输出了 tool_call 标记但 vLLM 的 structured/tool parser 返回空列表 请求既没有走普通生成也没触发工具调用。几个典型表征只在 GLM-5 的 FP8 版 function calling 组合出现FP8 量化可能让模型在 tool_call 标记边界处的 logits 轻微偏移输出里混入多余 token 或标记不闭合纯文本生成却正常。报错在tool call 解析阶段不是推理阶段说明 prefill/decode 本身跑通了是 vLLM 的ToolParser把模型输出文本解析成结构化 tool call 的组件没能正确解析 GLM-5 的特殊标记格式。GLM-5 用自定义 tool_call 标记GLM 系列用|tool_call|.../|/tool_call|包裹工具调用和 OpenAI 的function_call/tool_calls格式不同vLLM 若没注册对应的 GLM tool parser就会按通用格式解析失败。这不是 FP8 把模型搞坏了而是vLLM 的 tool-call 解析器没有适配 GLM-5 的标记格式FP8 只是放大了边界处的解析脆弱性。下面给出定位与修复。二、背景vLLM 的 function calling 流程是用户传 tools messages → chat template 把工具描述写进 prompt → 模型生成带 tool_call 标记的文本 → ToolParser 把这段文本解析成结构化 tool_calls 返回给 OpenAI 客户端GLM-5 的关键特征它不在消息里用标准tool_calls字段而是让模型在生成文本里输出|tool_call|JSON|/tool_call|这种特殊标记解析器必须能识别这个标记、抽取中间 JSON、并把它映射成 OpenAI 格式的tool_callsFP8 量化后模型偶尔会在|/tool_call|之后多生成一个 token或标记没干净闭合解析器若严格要求标记严格闭合且无多余内容就会失败。根因有两层(a) vLLM 没为 GLM-5 注册专用 ToolParser(b) 解析器对 FP8 产生的边界噪声多余 token / 不闭合太脆弱。修复就是补一个 GLM-5 ToolParser 做容错解析。三、根因拆成两条根因缺 GLM-5 专用 ToolParservLLM 按模型名/家族选择 tool parser如hermes、mistral、gpt_function_calling。GLM-5 若没注册对应的 parser会用通用/默认 parser 去按 OpenAI 格式解析自然匹配不到|tool_call|标记。根因是模型 → parser 的映射表里缺 GLM-5。解析对边界噪声脆弱即便有了 GLM parser若实现成必须严格|tool_call|...|/tool_call|且整段无其他字符FP8 在标记后多出的 token 就会让json.loads失败。根因是解析缺少抽取中间 JSON 容忍前后噪声的容错。修复方向实现一个GLMToolParser识别|tool_call|标记、用正则抽取中间 JSON、容忍前后噪声并在模型 → parser 映射里注册 GLM-5。四、最小可运行复现下面复现通用解析器解析不了 GLM 的 tool_call 标记import json import re def generic_parse(text): 现状按 OpenAI function_call 格式解析遇 GLM 标记直接失败。 # 假设通用解析只在意 function_call: {...} if |tool_call| in text: raise ValueError(不识别 GLM tool_call 标记) return [] # GLM-5 实际输出FP8 后可能末尾多 token glm_output some text |tool_call|{name:get_weather,arguments:{city:BJ}}|/tool_call| extra try: generic_parse(glm_output) except ValueError as e: print(复现:, e) # 不识别 GLM tool_call 标记复现: 不识别 GLM tool_call 标记即复现了 function call error。下面写 GLM 专用解析器。五、解决方案第一层最小直接修复最小修复实现GLMToolParser用正则从|tool_call|.../|/tool_call|中抽取 JSON并容忍前后噪声。import json import re from typing import List, Dict, Any TOOL_CALL_RE re.compile( r\|tool_call\|(.*?)\|/tool_call\|, re.DOTALL) class GLMToolParser: 解析 GLM-5 的 |tool_call|JSON/|/tool_call| 格式。 def parse(self, text: str) - List[Dict[str, Any]]: calls [] for m in TOOL_CALL_RE.finditer(text): raw m.group(1).strip() # 容错抽取最外层 {...}忽略 FP8 可能混入的边界噪声 obj self._extract_json_object(raw) if obj is None: continue calls.append({ type: function, function: { name: obj.get(name), arguments: json.dumps(obj.get(arguments, {}), ensure_asciiFalse), }, }) return calls def _extract_json_object(self, s: str): 从可能含噪声的字符串里抽取第一个完整 JSON 对象。 start s.find({) if start 0: return None depth 0 for i in range(start, len(s)): if s[i] {: depth 1 elif s[i] }: depth - 1 if depth 0: try: return json.loads(s[start:i 1]) except json.JSONDecodeError: return None return None # 用法含 FP8 噪声 parser GLMToolParser() calls parser.parse(glm_output) print(解析到 tool calls:, calls)这一层改动让 GLM-5 的 tool_call 标记能被正确解析且对标记后的多余 tokenextra有容错。六、解决方案第二层结构化改进把模型 → parser 映射 解析做成结构化组件并把 GLM-5 注册进去同时覆盖单次输出多个 tool call和arguments 已是字符串还是对象两种情形。from typing import Dict, Type class ToolParserRegistry: 模型家族 → ToolParser 类的映射单一事实源。 _registry: Dict[str, type] {} classmethod def register(cls, family: str, parser_cls: type): cls._registry[family] parser_cls classmethod def get(cls, model_name: str) - GLMToolParser: # 按模型名包含的关键字选 parser for family, pc in cls._registry.items(): if family.lower() in model_name.lower(): return pc() return _DefaultToolParser() # 兜底 class _DefaultToolParser(GLMToolParser): 默认解析器同样支持 GLM 标记但额外尝试 OpenAI 格式。 def parse(self, text): calls super().parse(text) if calls: return calls # 退化到尝试 function_call 字段若有 return [] # 注册 GLM-5 ToolParserRegistry.register(glm-5, GLMToolParser) ToolParserRegistry.register(glm5, GLMToolParser) def parse_model_tool_calls(model_name: str, text: str): parser ToolParserRegistry.get(model_name) return parser.parse(text) # 用法 out prefix |tool_call|{name:f,arguments:{x:1}}|/tool_call| tail print(parse_model_tool_calls(glm-5-fp8, out))ToolParserRegistry让哪个模型用哪个 parser集中管理新增模型只注册一行文档也能从它生成。七、解决方案第三层断言 / CI 守护tool call 解析最怕换模型又漏注册或FP8 噪声仍解析失败。用断言守两条不变量def check_glm_parser_invariants(text): parser GLMToolParser() calls parser.parse(text) # 不变量 1含合法 tool_call 标记必须解析出至少 1 个调用 assert len(calls) 1, 合法 tool_call 未解析出 # 不变量 2每个 call 必须是标准 OpenAI 结构 for c in calls: assert c[type] function assert name in c[function] json.loads(c[function][arguments]) # arguments 必须是合法 JSON 串 return True def test_glm_fp8_function_call(): cases [ |tool_call|{name:a,arguments:{k:1}}|/tool_call|, x |tool_call|{name:b,arguments:{}}|/tool_call| y, # FP8 噪声 前缀 |tool_call|{name:c,arguments:{u:v}}|/tool_call| 后缀多余, ] for t in cases: check_glm_parser_invariants(t) print(OK: GLM-5 FP8 function call 解析不变量通过) if __name__ __main__: test_glm_fp8_function_call()把test_glm_fp8_function_call接进 CI任何漏注册 GLM parser或对边界噪声解析失败的改动都会立即红。八、排查清单GLM-5 FP8 function call 报错按序查确认报错在解析阶段还是推理阶段若Failed to parse tool call/arguments is not valid JSON说明推理成功、解析失败按本篇修 parser若是推理期崩那是另一类问题。看模型输出里 tool_call 标记格式GLM 用|tool_call|.../|/tool_call|。若标记根本没出现模型直接输出自然语言那是 chat template / prompt 没正确引导不是 parser 问题。确认 GLM parser 已注册grepToolParserRegistry/ parser 映射确认glm-5/glm5进了表。没注册就会用默认 parser 解析失败。FP8 边界噪声容错FP8 量化后模型可能在|/tool_call|后多 token解析器必须抽取中间 JSON、容忍前后噪声而非要求整段严格等于标记。用_extract_json_object的容错抽取。arguments 类型模型可能输出arguments为对象或字符串解析器统一转成 JSON 字符串json.dumps保证tool_calls[i].function.arguments是合法 JSON 串。多 tool call一次输出多个|tool_call|块时finditer要能全部抽出别只取第一个。CI 接test_glm_fp8_function_call覆盖纯净 / 前缀噪声 / 后缀噪声三类输出锁死 parser 容错。九、小结glm-5-fp8 function call error的根因是vLLM 没为 GLM-5 注册专用 ToolParser 去识别|tool_call|标记且解析对 FP8 在标记边界产生的噪声太脆弱。三层修复第一层GLMToolParser用正则抽取|tool_call|.../|/tool_call|中间 JSON并_extract_json_object容错容忍前后噪声、抽取最外层对象第二层ToolParserRegistry集中管理模型家族 → parser映射并注册 GLM-5解析器统一输出 OpenAI 格式tool_calls第三层CI 断言守住合法标记必解析出标准结构 call / 边界噪声不崩任何漏注册或容错退化立即红。落实后GLM-5 FP8 的 function calling 能稳定把|tool_call|块解析成结构化 tool_callsFP8 产生的边界噪声不再导致解析失败。