GLM 流式输出中断了怎么办:先看 finish_reason 再谈重试
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
GLM 的流式中断有一个反直觉的地方,官方文档在错误码页最后一行专门写了它:使用流式(SSE)调用时,如果 API 在推理过程中异常终止,不会返回错误码表里那些业务错误码,而是在响应体的 finish_reason 参数里返回异常原因。 所以你在客户端写的那套「捕获 HTTP 状态码 → 匹配业务错误码 → 决定重试」的逻辑,在流式场景下会整段失效——连接已经建立、HTTP 状态早就是 200 了,异常是在流中间才发生的。排查 GLM 流式中断的正确顺序是:先确认流是不是走到了最后一个 chunk,再看那个 chunk 里的 finish_reason 是什么,最后才轮到重试策略。跳过前两步直接加重试,最常见的结果是把一次被安全审核终止的请求重复发了很多遍。
第一步:分清是谁断的
「流式输出断了」在工程上其实是三件完全不同的事,症状看着像,处置方式完全相反。
第一种是连接层断开。 特征是你的迭代循环直接抛异常或者提前退出,你根本没拿到任何 finish_reason,也没看到官方响应示例里最后那行 data: [DONE]。GLM 的流式响应采用服务器发送事件(SSE)格式,官方给的响应示例结尾是一个带 finish_reason 和 usage 的 chunk,然后才是 data: [DONE]。如果这两样都没出现,问题多半在你和网关之间的链路上——反向代理的读超时、客户端库的读超时、负载均衡把长连接掐了。这一类跟模型本身没关系,改服务端配置解决。
第二种是服务端推理异常终止。 这一类你是能拿到最后一个 chunk 的,finish_reason 的值是 network_error,官方对这个取值的说明就是「模型推理异常」。它可以重试。
第三种是根本没断,只是提前结束了。 length 表示达到 token 长度限制,sensitive 表示内容被安全审核接口拦截。这两种在用户眼里都表现为「话说到一半没了」,但它们是模型正常按规则停下来的,重试解决不了任何问题,重试 sensitive 甚至会带来新的合规风险。
判断这三者的成本很低:在你的流式循环里把最后一个 chunk 的 finish_reason 原样打进日志就行。没打日志的项目,排查时永远只能猜。
finish_reason 的取值,以及文档里的一处不一致
官方接口文档对 finish_reason 的描述里列了这些含义:stop 表示自然结束或触发 stop 词,tool_calls 表示模型命中函数,length 表示达到 token 长度限制,sensitive 表示内容被安全审核接口拦截(文档特别注明用户应判断并决定是否撤回公开内容),network_error 表示模型推理异常,model_context_window_exceeded 表示超出模型上下文窗口。
这里有一个值得留意的细节:同一份接口文档里,finish_reason 字段的 enum 枚举列表写的是 stop、length、tool_calls、sensitive、network_error 五个值,而紧挨着的文字描述里比枚举多出一个 model_context_window_exceeded。也就是说,如果你按 enum 生成客户端的强类型解析代码,遇到超出上下文窗口的返回时可能会解析失败。稳妥的写法是把 finish_reason 当成开放字符串处理,匹配不上的走默认分支并原样记录下来,而不是用穷举枚举去反序列化。以上取值与含义均以官方文档当前版本为准。
从处置角度,这六个值可以分成三组:
- 不必重试:
stop、tool_calls。这是正常路径。 - 改请求再来:
length说明输出被截断了,要么调大max_tokens(官方对不同模型系列的最大输出长度有明确上限,具体数值以官方文档为准),要么让模型分段输出;model_context_window_exceeded说明输入加输出超出了窗口,得先压缩上下文。官方 FAQ 里给了一个换算关系:模型最大输入限制 = 模型上下文 − 最大输出。 - 可以重试:
network_error。 - 绝对不要机械重试:
sensitive。
被安全审核截断的那一种,认准 content_filter
这一类单独拿出来说,因为它是唯一一种「重试有害」的中断。
官方内容安全文档写得很清楚:模型流式输出过程中,平台会分批对生成内容做检测,检测到违法及不良信息时,API 返回错误码 1301,V4 接口返回 "finish_reason":"sensitive"。同步响应场景下返回的结构里带 contentFilter 字段,包含 role(官方对这个字段的说明是安全生效环节:role = user 用户输入、role = assistant 模型推理、role = history 历史上下文)和 level(严重程度,官方说明 level 0 表示最严重、3 表示轻微)。流式响应示例里对应的字段是 content_filter,同样带 role 和 level。
注意两点。一是 role 告诉你命中在哪个环节——用户输入、模型推理还是历史上下文,这决定了你该处理用户输入、该改提示词模板,还是该清理会话里带过来的历史消息;二是官方在这一节挂了一个 Warning,要求开发者识别到相关信息后及时采取终止生成、撤回、修改、清屏、重启等措施删除生成内容,并确保不把含有违法及不良信息的内容传回给模型继续生成。换句话说,sensitive 触发后正确的动作是清掉这一轮的半截内容,而不是把它塞进 messages 里接着聊——后者会让下一轮继续踩线。
限流与平台过载导致的中断
如果中断集中出现在某个时间段,且伴随大量失败,那多半不是流本身的问题,而是速率限制。GLM 的错误码表里,429 对应的业务码有一批,其中和调用节奏直接相关的是两个:1302 是「您的账户已达到速率限制,请您控制请求频率」,1305 是「该模型当前访问量过大,请您稍后再试」。
这两个的归因完全不同,官方速率限制文档把它们分开讲了。1302 属于账户维度:当前模型的并发请求数已达到账户上限,或者短时间内请求过于密集;官方建议的处理方式是降低并发请求数量、增加请求队列或排队机制,必要时提升账户权益等级。1305 属于平台级保护机制,触发条件是某一模型短时间内整体访问量激增、底层算力资源高负载、或者平台在做系统维护扩容,官方明确说这类情况与单一账户的调用行为无直接关系,建议稍后重试、增加重试间隔、避免立即高频重试,业务允许的话做降级或延迟处理。
这里的实践含义是:遇到 1305 就别写固定间隔的密集重试,官方文档把「无限重试」列为速率限制要防范的异常流量之一。另外文档里还提到并发数的定义是「同一时刻正在处理中的请求数量」。按这个定义推,流式请求在整个生成期间都处于「正在处理中」,也就一直占着一个并发位——这一步是按定义推的,官方文档并没有单独说明流式请求的并发占用方式,但做并发预算时值得按这个口径先算一遍。具体的并发上限要去官方控制台的速率限制页面看自己账户的数值,本文不列。通用的退避与排队写法可以参考 API 429 限流的通用处理方式。
还有一个容易被忽略的码:1234,官方给的错误信息是「网络错误」,并在消息里带上一个 error_id 占位、提示联系客服。这个 error_id 遇到时要记下来再去提工单,比描述「我这边偶尔断」有用得多。
重试之前,先把 request_id 传上
GLM 的对话补全接口支持一个 request_id 参数,官方说明是请求唯一标识符,由用户端传递,建议使用 UUID 格式确保唯一性,若未提供平台将自动生成(长度上下限以官方文档为准)。还有一个 user_id,是终端用户的唯一标识符,官方建议使用不包含敏感信息的唯一标识。
对排查流式中断来说,自己生成并传 request_id 有两个直接好处。一是当同一个逻辑请求因为中断被重发多次时,你自己的日志里能把这几次串起来,知道究竟重试了几轮;二是找官方客服时,你能给出确切的标识,而不是让对方在时间范围里翻。还有一个结构上的原因:官方接口文档里,非流式的 ChatCompletionResponse 结构是带 request_id 字段的(描述就是「请求 ID」),但流式的 ChatCompletionChunk 结构列出的顶层字段是 id、created、model、choices、usage、content_filter,里面没有 request_id。也就是说流式场景下,即便平台自动生成了标识,你也没法从 chunk 里读回来。想把一次逻辑请求的多轮重试在日志里串成一条线,只能在发请求之前自己生成并传。
流式工具调用下的中断更难收尾
如果你开了 tool_stream,中断的破坏力会放大一档。
官方工具流式输出文档说明,开启这个能力后,可以在调用 chat.completions 时在不进行缓冲或 JSON 验证的情况下流式传输工具使用参数。关键词是「不进行 JSON 验证」——delta.tool_calls 里的 function.arguments 是一段一段发过来的,官方示例里的处理方式是按 tool_call.index 分组,第一次收到就存下,后续收到就把 arguments 字符串往后追加。
这意味着一旦流在中途断掉,你手里拿到的是一段语法不完整的 JSON 字符串,直接丢给 json.loads 会抛异常。正确做法是:只有在确认收到了带 finish_reason 的收尾 chunk、且值为 tool_calls 之后,才去解析拼接好的 arguments;中途异常退出就整个丢弃,不要试图补全括号。另外 delta 里除了 content 和 tool_calls,还有 reasoning_content(模型推理过程文本),官方流式响应字段说明里三者是分开的,拼装时别把推理内容混进正文。工具调用分片的拼装细节可以看 GLM 流式输出下的工具调用怎么拼装。
中断的请求怎么计费
这是被问得最多、也最没法给准话的一个问题。
官方计费 FAQ 说明的是通用口径:按 token 计费,根据模型输入和输出的总 token 数计费,开启搜索服务的话搜索结果作为输入也计费。至于流式中途异常终止、只产出了部分输出的请求,其输出 token 如何计入账单,官方文档里没有找到相关说明。这里不做推断。
能确定的是数据从哪儿看:流式响应的 usage 字段只在最后一个 chunk 里出现,包含 prompt_tokens、completion_tokens、total_tokens 以及 prompt_tokens_details(其中有 cached_tokens)。所以只要你的循环在中断时提前 break 掉,这次调用的用量数据你自己就没有了。想把消耗对上账,就得保证异常路径下也把已收到的 chunk 信息落盘。跨厂商的流式中断与续传通用思路,可以参考 流式输出中断与续传。
最容易栽的坑
按踩中频率排个序。
第一,在循环里没有对空 choices 做防御。官方 Python 示例第一行判断就是 if not chunk.choices: continue——存在 choices 为空的 chunk,直接取 chunk.choices[0] 会越界,这类崩溃在日志里长得跟「流式中断」一模一样,其实是客户端自己挂的。
第二,把 finish_reason 的判断写在了 content 分支里面。官方示例里 finish_reason 的检查是和内容处理并列的独立分支,因为收尾那个 chunk 的 delta.content 是空字符串。写成嵌套的话,收尾 chunk 会被 if delta.content 直接跳过,于是你永远拿不到中断原因。
第三,对 sensitive 做自动重试。前面说过了,这一条既解决不了问题,也不符合官方在内容安全文档里给出的处置要求。
第四,用枚举强类型解析 finish_reason。文档描述里的取值比 enum 列表多,解析失败会把一次可识别的中断变成一次不可识别的客户端异常。
真要动手改,建议的顺序是:先给流式循环补上完整的日志(记录 request_id、最后一个 chunk 的 finish_reason、以及 usage),跑上几天,你会发现原来笼统归为「中断」的那批请求,其实分属完全不同的几类,各自的解法一点也不一样。GLM 侧其余报错的对照关系,可以接着看 GLM API 报错排查。