LLM Streaming 断流恢复工程实践:SSE 断点续传、Partial Response 重建与生产级容错设计

凌晨 2 点,一个 AI 写作平台的用户正在生成一篇 5 万字的长篇小说。SSE 流已经推送了 80% 的内容,进度条走到了最后几段。然后连接断了------可能是 TCP 超时,可能是负载均衡器主动断连,也可能只是用户的 Wi-Fi 闪了一下。

客户端拿到一个不完整的响应,只能从头重试。用户又等了 3 分钟,看到的是「生成失败,请重试」。

这不是假设场景。在我们团队运营的一个 AI 内容平台上,日均 50 万次流式调用中,128K token 输出场景的断流率超过 8%。而社区里的开发者讨论(Hacker News 2026 年的一个热门帖子)显示,50K+ token 的长文本生成中约 12% 经历至少一次连接断开。

问题不在于断流本身------网络不可靠是常态。问题在于 90% 的团队在断流后只会做一件事:丢弃已收到的所有内容,从头发起一个新请求。已生成的 4 万字 token 全部浪费,用户多等 3 分钟,服务端白白多跑一次完整推理。

本文把我们在过去 6 个月里踩过的坑和验证过的 5 种断流恢复策略整理出来,从最简单的 transport-level retry 到生产级的 semantic resume,每种方案都附带可运行的代码和真实场景下的取舍分析。


一、为什么 LLM Streaming 断流恢复这么难

先搞清楚问题再谈方案。LLM 的流式响应和普通 HTTP 流式下载有本质区别,这也是为什么 SSE 协议原生的重连机制在 LLM 场景下几乎用不上。

1.1 SSE 协议的 Last-Event-ID 为什么不够

SSE(Server-Sent Events)规范确实内置了断点续传能力。客户端断连后重连时,浏览器会自动带上 Last-Event-ID 请求头,服务端理论上可以从这个 ID 继续推送事件。

kotlin 复制代码
// SSE 规范中的重连机制
// 客户端自动发送:
GET /stream HTTP/1.1
Last-Event-ID: evt-4287

// 服务端从 evt-4288 继续推送
data: {"id":"evt-4288","delta":"继续生成的内容..."}

这在传统场景(日志推送、股票行情)下完全够用。但 LLM streaming 有三个特殊性让这套机制失效:

推理有状态,服务端不缓存。 模型推理是逐 token 生成的,服务端通常不会把已生成的 token 缓存在内存里等你重连。一旦推理协程结束(无论正常还是异常),服务端的上下文就释放了。

事件 ID 不等于生成进度。 LLM streaming 的 chunk 粒度不固定------一个 chunk 可能包含 1 个 token,也可能包含 5 个。Last-Event-ID 告诉你从哪个事件继续,但服务端已经没有那个事件之后的内容了。

模型不能从中间 token 继续。 即使你告诉模型「从第 4287 个 token 继续生成」,模型也不知道前面生成了什么。自回归模型需要完整的上下文前缀才能生成下一个 token。

1.2 三大断流场景

在生产环境中,我们把 LLM streaming 的断流分为三类,每类的发生频率和应对策略不同:

断流类型 常见原因 发生频率 可预测性
网络层断开 TCP 超时、中间设备 RST、Wi-Fi/4G 切换 高(占断流 60%+) 低,随机发生
服务端中断 GPU OOM、推理超时、LB 主动断连 中(占断流 25%) 部分可预测
客户端中断 用户刷新页面、标签休眠、App 切后台 中(占断流 15%) 完全不可预测

网络层断开是最常见的,也是断流恢复收益最大的场景------因为此时服务端可能还在正常推理,只是推送不到客户端了。

1.3 现有 SDK 的处理方式

主流大模型 SDK 的处理方式大多停留在 transport-level retry。以某主流大模型厂商的 Python SDK 为例:

python 复制代码
# 某主流大模型厂商 Python SDK 的做法(简化)
class Stream:
    def __iter__(self):
        while True:
            try:
                for chunk in self._stream():
                    yield chunk
                break
            except (ConnectionError, TimeoutError):
                # 自动重连,但从头开始
                self._reconnect()

这能避免连接层面的失败,但语义层面------已生成的内容------全部丢失。对于短响应(<1000 token),从头重试的成本可以接受。但对于长文本生成,这个浪费是不可接受的。


二、5 种断流恢复策略

按实现复杂度从低到高,以下是我们在生产中验证过的 5 种策略。每种策略独立可用,也可以组合使用。

策略 1:Transport-Level Retry(基线方案)

这是最简单的方案,也是大多数团队当前的做法。核心思路:连接断了就重试,但重试时把已收到的 partial response 保留下来。

python 复制代码
import time

def stream_with_retry(
    client,
    messages: list,
    max_retries: int = 3,
    model: str = "deepseek-chat"
):
    """基线方案:断流后重试,但保留已收到的内容"""
    collected = []
    
    for attempt in range(max_retries + 1):
        try:
            stream = client.chat.completions.create(
                model=model,
                messages=messages,
                stream=True,
            )
            for chunk in stream:
                delta = chunk.choices[0].delta.content
                if delta:
                    collected.append(delta)
            # 正常结束,返回完整内容
            return "".join(collected)
            
        except (ConnectionError, TimeoutError) as e:
            if attempt == max_retries:
                # 最后一次重试也失败了,返回已收集的部分内容
                partial = "".join(collected)
                return partial if partial else None
            # 指数退避
            time.sleep(min(2 ** attempt, 10))
    
    return None

优点: 实现简单,零额外依赖,适用于任何大模型 API。

局限: 重试时从头开始生成,已收集的 partial response 和新请求的响应无法拼接。你只能选择「丢弃已收集内容,用新响应替换」或「保留已收集内容,放弃剩余生成」。前者浪费 token,后者内容不完整。

适用场景: 短响应(<2000 token)、对完整性要求不高的场景(如聊天机器人回复)。

策略 2:Chunk-Level Checkpoint(推荐方案)

这是投入产出比最高的方案,也是我们在大多数场景下的默认选择。核心思路:每收到 N 个 chunk,就把当前累积的内容持久化到 Redis 或数据库。断流后从最后一个 checkpoint 恢复。

python 复制代码
import redis
import json
import hashlib
from datetime import datetime

class StreamingCheckpoint:
    def __init__(self, redis_client: redis.Redis, session_id: str):
        self.r = redis_client
        self.session_id = session_id
        self.key = f"stream:ckpt:{session_id}"
        self.ttl = 3600  # 1 小时过期
    
    def save(self, content: str, chunk_count: int, usage: dict = None):
        """持久化当前进度"""
        data = {
            "content": content,
            "chunk_count": chunk_count,
            "usage": usage or {},
            "updated_at": datetime.now().isoformat()
        }
        self.r.setex(self.key, self.ttl, json.dumps(data))
    
    def load(self) -> dict | None:
        """恢复上一次的进度"""
        raw = self.r.get(self.key)
        if raw:
            return json.loads(raw)
        return None
    
    def clear(self):
        """生成完成后清理"""
        self.r.delete(self.key)


def stream_with_checkpoint(
    client,
    messages: list,
    session_id: str,
    checkpoint_every: int = 50,
    model: str = "deepseek-chat"
):
    """带 checkpoint 的流式生成"""
    ckpt = StreamingCheckpoint(redis.Redis(), session_id)
    
    # 检查是否有未完成的 checkpoint
    existing = ckpt.load()
    if existing:
        collected = [existing["content"]]
        start_chunk = existing["chunk_count"]
        # 将已生成内容加入 messages,让模型从断点继续
        messages = messages + [
            {"role": "assistant", "content": existing["content"]},
            {"role": "user", "content": "请从上次中断的地方继续。"}
        ]
    else:
        collected = []
        start_chunk = 0
    
    chunk_count = start_chunk
    
    try:
        stream = client.chat.completions.create(
            model=model,
            messages=messages,
            stream=True,
        )
        for chunk in stream:
            delta = chunk.choices[0].delta.content
            if delta:
                collected.append(delta)
                chunk_count += 1
                
                # 每 N 个 chunk 做一次 checkpoint
                if chunk_count % checkpoint_every == 0:
                    ckpt.save("".join(collected), chunk_count)
        
        # 正常完成
        result = "".join(collected)
        ckpt.clear()
        return result
        
    except (ConnectionError, TimeoutError):
        # 断流了,但 checkpoint 已经保存了当前进度
        # 下次调用 stream_with_checkpoint 时会自动恢复
        ckpt.save("".join(collected), chunk_count)
        raise StreamingInterruptedError(
            session_id=session_id,
            partial_content="".join(collected),
            chunks_received=chunk_count
        )

核心设计点:

  • Checkpoint 频率: 每 50 个 chunk 存一次,平衡写入开销和恢复粒度。对于长文本,可以适当降低到每 20 个 chunk。
  • 恢复方式: 不是「从断点续传 SSE」,而是「把已生成内容作为上下文前缀,让模型继续生成」。这比尝试续传 SSE 流可靠得多。
  • Session ID: 用业务维度的 ID(如用户+任务ID),方便跨请求关联。

优点: 断流后不丢失已生成内容,恢复延迟低(只需重新建立连接),token 浪费控制在 checkpoint 间隔以内。

局限: 需要一个 Redis 实例;恢复时模型的续写质量取决于 prompt 设计(「请从上次中断的地方继续」不一定能保证无缝衔接)。

适用场景: 绝大多数 LLM 流式应用,特别是长文本生成、AI 写作、代码生成。

策略 3:SSE Resume Offset

这个方案需要服务端配合,适用于自建 LLM Gateway 的团队。核心思路:服务端为每个流式响应维护一个 offset 索引,客户端断连重连时带上 offset,服务端从该位置继续推送缓冲区中的内容。

python 复制代码
# 服务端:LLM Gateway 的 SSE 处理(FastAPI 示例)
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio
import json

app = FastAPI()

# 内存中缓存每个 stream 的已生成 chunks
# 生产环境建议用 Redis Stream 或 Kafka
stream_buffers: dict[str, list[dict]] = {}

@app.post("/v1/chat/completions/stream")
async def stream_chat(request: Request):
    body = await request.json()
    stream_id = body.get("stream_id") or generate_stream_id()
    resume_offset = int(request.headers.get("X-Resume-Offset", "0"))
    
    # 检查是否有可恢复的 stream
    if stream_id in stream_buffers and resume_offset > 0:
        buffer = stream_buffers[stream_id]
        # 先推送已缓存但未送达的 chunks
        async def resume_and_continue():
            for i in range(resume_offset, len(buffer)):
                yield f"data: {json.dumps(buffer[i])}\n\n"
            # 然后继续生成新内容
            async for chunk in generate_remaining(stream_id, body):
                buffer.append(chunk)
                yield f"data: {json.dumps(chunk)}\n\n"
        
        return StreamingResponse(
            resume_and_continue(),
            media_type="text/event-stream",
            headers={"X-Stream-Id": stream_id}
        )
    
    # 新请求
    stream_buffers[stream_id] = []
    
    async def generate():
        async for chunk in llm_generate(body):
            stream_buffers[stream_id].append(chunk)
            yield f"data: {json.dumps(chunk)}\n\n"
    
    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={"X-Stream-Id": stream_id}
    )
python 复制代码
# 客户端:带 resume offset 的流式消费
import httpx

def stream_with_resume(endpoint: str, body: dict, stream_id: str = None):
    """带 resume offset 的客户端"""
    headers = {}
    offset = 0
    collected = []
    
    if stream_id:
        headers["X-Stream-Id"] = stream_id
    
    while True:
        headers["X-Resume-Offset"] = str(offset)
        
        try:
            with httpx.stream("POST", endpoint, json=body, headers=headers) as resp:
                stream_id = resp.headers.get("X-Stream-Id", stream_id)
                
                for line in resp.iter_lines():
                    if line.startswith("data: "):
                        chunk = json.loads(line[6:])
                        collected.append(chunk["content"])
                        offset += 1
            
            # 正常结束
            return "".join(collected)
            
        except (httpx.ReadError, httpx.RemoteProtocolError):
            # 断流了,带着 offset 重连
            continue

优点: 真正的断点续传,不浪费 token,恢复延迟最低(只补发丢失的 chunks)。

局限: 需要自建 LLM Gateway;服务端需要维护 stream buffer,增加内存开销;不适合直接调用第三方大模型 API(你控制不了模型厂商的服务端)。

适用场景: 自建 LLM Gateway 的中大型团队,日均调用量超过 10 万次,需要精细控制 streaming 行为。

策略 4:Semantic Resume(应用层续写)

这是我们在 AI 写作平台中用得最多的方案。核心思路:不依赖底层协议,而是在应用层把「断流恢复」转化为「续写任务」。已生成的内容作为上下文,加上一个续写 prompt,让模型从语义层面接续。

python 复制代码
from typing import Optional
from dataclasses import dataclass

@dataclass
class StreamState:
    """流式生成的状态快照"""
    content: str
    token_count: int
    is_complete: bool
    finish_reason: Optional[str]
    metadata: dict

class SemanticResumeHandler:
    """语义续写处理器"""
    
    # 续写 prompt 模板------这是关键
    CONTINUE_PROMPTS = {
        "narrative": "以下是已生成的内容。请从最后一句话的语义接续,不要重复已有内容,不要添加过渡句:\n\n{content}\n\n[续写]",
        "code": "以下是已生成的代码。请从最后一行继续,保持相同的代码风格和缩进:\n\n{content}\n\n[继续]",
        "technical": "以下是已生成的技术文章。请从最后一段的论点继续展开,保持相同的技术深度和写作风格:\n\n{content}\n\n[继续]",
    }
    
    def __init__(self, client, model: str = "deepseek-chat"):
        self.client = client
        self.model = model
    
    def resume(
        self,
        original_messages: list,
        partial_state: StreamState,
        content_type: str = "technical",
        max_resume_attempts: int = 2
    ) -> str:
        """从断点语义续写"""
        
        if partial_state.is_complete:
            return partial_state.content
        
        # 构建续写上下文
        # 取最后 2000 字符作为续写参考(避免 token 超限)
        tail = partial_state.content[-2000:]
        continue_prompt = self.CONTINUE_PROMPTS[content_type].format(content=tail)
        
        resume_messages = original_messages + [
            {"role": "assistant", "content": partial_state.content},
            {"role": "user", "content": continue_prompt}
        ]
        
        for attempt in range(max_resume_attempts):
            try:
                response = self.client.chat.completions.create(
                    model=self.model,
                    messages=resume_messages,
                    stream=True,
                    # 续写通常不需要太长的输出
                    max_tokens=4096,
                )
                
                continuation = []
                for chunk in response:
                    delta = chunk.choices[0].delta.content
                    if delta:
                        continuation.append(delta)
                
                continuation_text = "".join(continuation)
                
                # 缝合:找到续写内容和原文的重叠部分,去重
                stitched = self._stitch(partial_state.content, continuation_text)
                return stitched
                
            except (ConnectionError, TimeoutError):
                if attempt == max_resume_attempts - 1:
                    # 续写也失败了,返回已有的部分内容
                    return partial_state.content + "\n\n[生成中断,部分内容可能不完整]"
        
        return partial_state.content
    
    def _stitch(self, original: str, continuation: str) -> str:
        """智能缝合:找到续写内容和原文尾部的重叠,去重"""
        # 取原文最后 100 个字符,在续写内容中查找匹配
        tail_len = min(100, len(original))
        tail = original[-tail_len:]
        
        # 在续写内容的前 200 个字符中查找重叠
        search_window = continuation[:200]
        
        best_overlap = 0
        for i in range(len(tail) - 10, 0, -1):
            suffix = tail[i:]
            if suffix in search_window:
                best_overlap = len(suffix)
                break
        
        if best_overlap > 0:
            # 去除重叠部分
            clean_continuation = continuation[best_overlap:]
            return original + clean_continuation
        else:
            # 没有找到重叠,直接拼接
            return original + continuation

关键设计点:

  1. 续写 Prompt 设计: 不同类型的文本需要不同的续写策略。叙事文本需要语义接续,代码需要保持风格一致,技术文章需要保持论点连贯。
  2. 缝合算法: 模型可能重复生成原文尾部的内容,需要一个去重逻辑。上面的 _stitch 方法用滑动窗口查找重叠。
  3. 续写长度控制: 续写不需要再生成完整的剩余内容,通常补上缺失的部分就够了。

优点: 不依赖任何底层协议改动,纯应用层实现,适用于任何大模型 API。对于长文本生成,续写质量可以接近无缝。

局限: 续写质量依赖 prompt 工程;多轮续写可能累积风格偏移;每次续写都是一次新的 API 调用,有额外成本。

适用场景: 长文本生成(文章、小说、报告)、AI 写作平台、需要高内容完整性的场景。

策略 5:Dual-Stream Redundancy(双链路冗余)

这是成本最高但可靠性也最高的方案。核心思路:同时向两个模型(或同一个模型的两个实例)发起流式请求,主链路断开时自动切换到备用链路。

python 复制代码
import asyncio
from typing import AsyncIterator

class DualStreamManager:
    """双链路流式管理器"""
    
    def __init__(
        self,
        primary_client,
        fallback_client,
        primary_model: str = "deepseek-chat",
        fallback_model: str = "qwen-max"
    ):
        self.primary = primary_client
        self.fallback = fallback_client
        self.primary_model = primary_model
        self.fallback_model = fallback_model
    
    async def stream_with_fallback(
        self,
        messages: list,
        switch_threshold: int = 30  # 前 N 个 chunk 内断流才切换
    ) -> AsyncIterator[str]:
        """
        双链路策略:
        - 主链路正常时使用主链路
        - 主链路在前 N 个 chunk 内断流,切换到备用链路(因为此时浪费的 token 少)
        - 主链路在 N 个 chunk 后断流,使用 Semantic Resume(因为切换成本太高)
        """
        primary_stream = await self._start_stream(
            self.primary, self.primary_model, messages
        )
        
        chunk_count = 0
        collected = []
        
        try:
            async for chunk in primary_stream:
                chunk_count += 1
                collected.append(chunk)
                yield chunk
        except StreamInterruptedError:
            if chunk_count < switch_threshold:
                # 早期断流,直接切换备用链路从头开始
                fallback_stream = await self._start_stream(
                    self.fallback, self.fallback_model, messages
                )
                async for chunk in fallback_stream:
                    yield chunk
            else:
                # 已经生成了大量内容,用 Semantic Resume 续写
                partial = "".join(collected)
                resume_handler = SemanticResumeHandler(self.fallback, self.fallback_model)
                state = StreamState(
                    content=partial,
                    token_count=chunk_count,
                    is_complete=False,
                    finish_reason=None,
                    metadata={}
                )
                remaining = resume_handler.resume(messages, state)
                yield remaining
    
    async def _start_stream(self, client, model, messages):
        """启动一个流式请求"""
        response = await client.chat.completions.create(
            model=model,
            messages=messages,
            stream=True,
        )
        
        async def iterate():
            async for chunk in response:
                delta = chunk.choices[0].delta.content
                if delta:
                    yield delta
        
        return iterate()

优点: 最高的可用性和最快的恢复速度。对于关键业务(如实时客服、金融报告生成),双链路可以保障 SLA。

局限: 成本翻倍(同时调用两个模型);两个模型的输出风格可能不一致;实现复杂度高。

适用场景: 高 SLA 要求的 B2B 场景、实时交互场景、断流不可接受的关键业务。


三、策略对比与选型决策

5 种策略的对比

策略 实现复杂度 恢复延迟 Token 浪费 额外成本 适用场景
Transport Retry ★☆☆ 高(重新生成) 高 无 短响应,聊天机器人
Chunk Checkpoint ★★☆ 低(2-5s) 低(checkpoint 间隔内) Redis 大多数流式应用
SSE Resume Offset ★★★ 最低(<1s) 零 自建 Gateway 自建 LLM Gateway
Semantic Resume ★★☆ 中(3-8s) 低 续写 API 调用 长文本生成
Dual-Stream ★★★★ 最低(<1s) 零或低 翻倍 API 成本 高 SLA 关键业务

选型决策树

vbnet 复制代码
你的流式响应通常超过 2000 token 吗?
├── 否 → Transport-Level Retry 足够
└── 是
    ├── 你有自建的 LLM Gateway 吗?
    │   ├── 是 → SSE Resume Offset(最优解)
    │   └── 否
    │       ├── 你的场景是长文本生成(文章/小说/报告)吗?
    │       │   ├── 是 → Semantic Resume(续写质量最好)
    │       │   └── 否 → Chunk-Level Checkpoint(通用方案)
    │       └── 你的业务 SLA 要求 99.9%+ 可用性吗?
    │           ├── 是 → Dual-Stream + Chunk Checkpoint 组合
    │           └── 否 → Chunk-Level Checkpoint

实际生产中的组合使用: 我们团队的默认方案是 Chunk Checkpoint + Semantic Resume 的组合。正常情况下用 checkpoint 恢复,如果 checkpoint 恢复后内容质量不达标(通过一个简单的 coherence score 检测),自动降级到 semantic resume。


四、生产落地 Checklist

把断流恢复推上生产,除了选对策略,还需要关注这些工程细节。

4.1 监控指标

断流恢复系统的核心监控指标:

python 复制代码
# Prometheus 指标定义
from prometheus_client import Counter, Histogram, Gauge

stream_total = Counter(
    "llm_stream_total",
    "Total streaming requests",
    ["model", "endpoint"]
)

stream_interrupted = Counter(
    "llm_stream_interrupted_total",
    "Interrupted streaming requests",
    ["model", "endpoint", "interrupt_type"]
)

stream_resume_success = Counter(
    "llm_stream_resume_success_total",
    "Successful stream resumptions",
    ["model", "strategy"]
)

stream_resume_latency = Histogram(
    "llm_stream_resume_latency_seconds",
    "Time to resume after stream interruption",
    ["model", "strategy"],
    buckets=[0.5, 1, 2, 5, 10, 30]
)

stream_tokens_wasted = Counter(
    "llm_stream_tokens_wasted_total",
    "Tokens generated but discarded due to stream failure",
    ["model", "endpoint"]
)

stream_checkpoint_lag = Gauge(
    "llm_stream_checkpoint_lag_chunks",
    "Number of chunks since last checkpoint",
    ["session_id"]
)

关键告警规则:

  • stream_interrupted_total / stream_total > 5%:断流率超过 5%,检查网络或 provider 状态
  • stream_resume_success_total / stream_interrupted_total < 90%:恢复成功率低于 90%,检查恢复逻辑
  • stream_tokens_wasted_total 突增:大量 token 浪费,可能是 checkpoint 频率太低

4.2 降级策略

当断流恢复系统本身出问题时的降级链:

vbnet 复制代码
正常模式:Chunk Checkpoint + Semantic Resume
    ↓ (Redis 不可用)
降级模式 1:Transport Retry + 保留 partial response
    ↓ (重试 3 次仍失败)
降级模式 2:返回 partial response + 标记「内容不完整」
    ↓ (partial response < 100 token)
降级模式 3:切换到非流式同步调用(牺牲体验换可靠性)

4.3 Checkpoint 容量规划

Checkpoint 存储的容量估算:

diff 复制代码
存储量 = 并发流数 × 平均 checkpoint 大小 × TTL

假设:
- 并发流数:1000
- 平均每个 checkpoint:50KB(约 2000 个中文字符)
- TTL:1 小时

存储量 = 1000 × 50KB = 50MB(Redis 内存占用可忽略)

但如果 TTL 设太长或忘记清理:
- 1000 并发 × 50KB × 24 小时 = 1.2GB/天
- 一周不清理就是 8.4GB

所以:TTL 一定要设,清理任务一定要有。

4.4 客户端 SDK 封装建议

如果你在封装面向业务的 SDK,建议把这些策略内聚到 SDK 层,让业务代码无感知:

python 复制代码
class ResilientLLMClient:
    """对业务层透明的容错 LLM 客户端"""
    
    def __init__(
        self,
        api_client,
        checkpoint_store: CheckpointStore,
        strategy: str = "checkpoint",  # checkpoint | semantic | auto
        content_type: str = "technical"
    ):
        self.api = api_client
        self.store = checkpoint_store
        self.strategy = strategy
        self.content_type = content_type
    
    def stream(self, messages: list, session_id: str, **kwargs) -> Iterator[str]:
        """
        业务层只需要调用 stream(),断流恢复逻辑完全内聚。
        
        用法:
            client = ResilientLLMClient(api_client, redis_store)
            for chunk in client.stream(messages, session_id="user-123-task-456"):
                process(chunk)
        """
        # 自动选择策略
        if self.strategy == "auto":
            # 根据预估输出长度选择策略
            estimated_tokens = self._estimate_output_length(messages)
            if estimated_tokens > 10000:
                strategy = "checkpoint+semantic"
            elif estimated_tokens > 2000:
                strategy = "checkpoint"
            else:
                strategy = "retry"
        else:
            strategy = self.strategy
        
        # 分发到具体实现
        if strategy == "checkpoint+semantic":
            yield from self._stream_with_checkpoint_and_semantic(
                messages, session_id, **kwargs
            )
        elif strategy == "checkpoint":
            yield from self._stream_with_checkpoint(
                messages, session_id, **kwargs
            )
        else:
            yield from self._stream_with_retry(messages, **kwargs)
    
    def _estimate_output_length(self, messages: list) -> int:
        """粗略估计输出长度,用于策略选择"""
        # 简单启发式:根据 user message 中的关键词判断
        last_message = messages[-1]["content"].lower()
        if any(kw in last_message for kw in ["长篇", "详细", "完整", "全文", "小说"]):
            return 20000
        elif any(kw in last_message for kw in ["总结", "简短", "一句话"]):
            return 500
        return 2000

五、写在最后

LLM streaming 断流恢复不是一个「有就行」的锦上添花功能,而是长文本生成场景下的必备工程能力。当你的用户等了 3 分钟看到 80% 进度条消失时,他们的耐心不会给你第二次机会。

回顾一下核心策略:

  • 短响应(<2000 token):Transport Retry 足够,别过度工程化
  • 大多数场景:Chunk Checkpoint 是投入产出比最高的方案,半天就能接入
  • 长文本生成:Semantic Resume 能提供最好的续写质量,但需要调 prompt
  • 自建 Gateway:SSE Resume Offset 是最优解,但需要服务端改造
  • 关键业务:Dual-Stream 保障 SLA,但要做好成本翻倍的预算

最后一条建议:别等到线上出了断流事故才开始想这个问题。今天就在你的 streaming 调用外面包一层 checkpoint,10 行代码的事。

相关推荐
林墨聊AIGC1 小时前
AI短剧分镜图生成后下一步:从AI到成片的完整制作流程高效策略
大数据·人工智能·ai作画·aigc·音视频·ai写作
ServBay9 小时前
被催了大半年,谷歌端出 Gemini 4 Argon。跑分硬刚 GPT-6,但...
aigc·ai编程·gemini
AI测试布道师14 小时前
CodexQA Code Wiki:让一个陌生仓库,先回答“从哪读起”
aigc·测试
摩羯123015 小时前
把长期记忆当作可验证数据:知芽 Notebook Skill 与 Agent Memory Repo 的工程拆分
人工智能·aigc
万联WANFLOW15 小时前
LLM 调用很快,AI Agent 为什么还是慢?关键路径分析与优化方法
性能优化·llm·aigc
唐青枫15 小时前
RAG 从原理到实战:让大模型先查资料,再回答问题
aigc
读完一本书15 小时前
RAG 到底是什么?用一个图书馆的比喻讲透
aigc
flash俊杰16 小时前
零模型调用的知识编译器:重叠分片缝合、规则抽取与世代事务
后端·aigc
Rocky Ding*16 小时前
DeepSeek-Bot技术拆解:持久化Agent团队如何管理记忆、上下文与协作成本
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·智能体