登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

LiteLLM 流式响应中断后怎样切到备用模型

来源:17golang原创

时间:2026-11-05 13:14:58 325浏览 收藏

LiteLLM 的流式响应已经输出部分内容后,不能把“配置 fallback”理解成备用模型会从原来的字节位置接着写。对于本文讨论的 Router Chat Completions 路径,已有内容后出错会向调用方抛出异常;要切到备用模型,应由应用捕获允许恢复的错误,用原始消息开启新的生成尝试,并明确替换或重新展示回答。

官方地址:https://docs.litellm.ai/docs/proxy/reliability

下面按故障排查顺序说明。依据是 LiteLLM v1.106.0 的 Router 源码及官方修复说明,范围限定为纯文本 Chat Completions,不把该结论直接套到 Responses 或 Messages 的不同实现。示例没有执行,也不是真实生产事故记录。

先区分流已建立与内容已经输出

用户可能说“已经开始流式返回”,实际上存在两种情况:连接已经建立,但没有产生实质输出;或者用户已经看到半句话。前一种还有机会在用户无感的情况下切到备用路径,后一种必须处理已经交付的内容。

状态需要考虑的恢复方式
已建立流,尚无内容由对应版本和端点的重试、fallback路径处理
已有文本等内容保留中断状态,应用开启新的attempt
工具参数只到了一部分不执行残缺参数,独立处理新尝试

只判断HTTP连接是否建立,无法确定是哪一种。排查时应同时记录首个内容事件、异常发生点和正常结束信号。空文本也不一定表示没有输出:Router 的内容判断还覆盖工具调用、推理等非文本增量。

为什么配置了 fallback 仍会中断

普通 fallback 配置可以写在模型组之间:

# 两个名称必须对应现有且可访问的 model_list 模型组。
router_settings:
  fallbacks:
    - chat-primary: [chat-backup]

这个片段依赖已有模型列表,不能单独启动一个完整代理。它声明主组与备用组的关系,不是断点续传协议。当前源码在发现已经产生内容的流错误时直接抛出原始异常或包装异常,避免静默启动另一段回答污染已返回的内容。

因此,业务层收到中断不一定是备用名称写错。还应区分端点、实际部署版本,以及异常究竟发生在创建流时还是遍历流时。只把 router.completion 包在异常处理中,却把 for chunk in stream 留在外面,会漏掉后续读取错误。

流对象与已交付内容的责任边界
图1:流式恢复边界说明图。连接状态、已输出内容与应用尝试状态分别记录,不是运行截图。

已输出内容后采用新的生成尝试

下面提供应用层事件生成器。它要求调用方已经配置两个可用模型组,使用独立的 attempt 标识;前端收到新的 begin 时应创建新回答或替换旧回答,不把不同 attempt 的 delta 直接追加到同一文本。

import copy
from contextlib import suppress
from litellm import APIConnectionError, Timeout, InternalServerError, RateLimitError

RETRYABLE = (APIConnectionError, Timeout, InternalServerError, RateLimitError)

def text_attempts(router, messages):
    # 仅用于纯文本 Chat Completions;新尝试不携带旧的半截回答。
    original = copy.deepcopy(messages)
    for attempt, model in enumerate(("chat-primary", "chat-backup"), start=1):
        stream = None
        finished = False
        failure = None
        yield {"event": "begin", "attempt": attempt, "model": model}
        try:
            # 此处由应用掌管一次切换,避免与 Router 的隐藏切换叠加。
            stream = router.completion(
                model=model, messages=copy.deepcopy(original),
                stream=True, disable_fallbacks=True, num_retries=0,
            )
            for chunk in stream:
                if not chunk.choices:
                    continue
                choice = chunk.choices[0]
                if choice.delta.content:
                    yield {"event": "delta", "attempt": attempt,
                           "text": choice.delta.content}
                if choice.finish_reason is not None:
                    finished = choice.finish_reason == "stop"
            # 流结束不代表成功;没有正常终止信号时保持不完整状态。
        except RETRYABLE as exc:
            failure = exc
        finally:
            if stream is not None and hasattr(stream, "close"):
                with suppress(Exception):
                    stream.close()

        if finished and failure is None:
            yield {"event": "complete", "attempt": attempt}
            return
        yield {"event": "interrupted", "attempt": attempt}
        if failure is None:
            # length、拒绝或不明结束不自动切换,交给业务层判断。
            return
        if attempt == 2:
            raise failure
        # 主尝试发生指定的可恢复异常,循环才进入备用模型。

此处的“备用”是一次新的模型请求,输入仍是原始消息。模型可能重新组织答案,因此它不是精确接续。代码关闭单次请求的自动 fallback,并把同组重试次数设为0,让这个教学片段的一次主尝试和一次备用尝试更容易理解;生产项目应只保留一个明确的恢复责任方。

异常过滤只包含连接、超时、服务端错误和限流类。身份错误、参数错误、内容拒绝等不能通过捕获所有异常后无条件换模型来处理。备用模型也必须满足原请求的访问、业务与内容要求。

把不完整输出和工具调用隔离

收到异常时,将原 attempt 标成 interrupted,不能给它补一个 complete。旧内容可以作为“未完成草稿”保留,也可以在用户同意的产品设计下被新回答替换,但不能悄悄把两次回答混成一份完整输出。

示例只把 finish_reason="stop" 视为正常完成。长度截断、拒绝或没有结束信号时,交给业务层处理,不自动切换。实际产品还应区分用户主动取消与网络错误;用户已经停止生成时,不应继续发起备用请求。

工具调用不适用这个文本片段。参数增量需要完整组装,工具执行需要自己的确认和去重机制。已经发生外部副作用的工具操作,不能因为模型流中断就再次执行。新 attempt 也不应继承一半的工具调用结果。

原始消息、尝试标识与回答状态的组成关系
图2:应用尝试状态结构说明图。每次生成有独立标识,旧草稿与新回答分别管理。

记录能解释切换结果的信息

建议记录业务请求ID、attempt编号、请求模型组、实际服务模型、是否交付过内容、异常类别和结束状态。原始提示词和完整输出未必需要写进日志,排查标识应与内容分开管理。

复盘时可按“主流产生内容、读取出错、旧attempt中断、备用attempt建立、新attempt完成或失败”的事件核对业务逻辑。这里描述的是排查顺序,不代表本文产生过这样的运行数据。

把部分回答加进 messages 能不能续写?可以设计成一个新的续写任务,但要额外处理重复、矛盾和格式完整性,不能称为无损恢复。本文选择原消息重新生成,便于维护清楚的回答边界。

流自然结束就可以认定成功吗?不应只看迭代器退出。还要看端点定义的完成信号,并区分正常结束与内容截断。

所有 LiteLLM 端点都一样吗?不同。本文的源码依据和片段限定在 Router Chat Completions;Responses 与 Messages 应分别依据对应端点和部署版本处理。

参考:LiteLLM fallback 文档;v1.106.0 Router 实现;已输出内容后的错误传播修复说明。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>