大模型 API 的 SSE 流式输出,我把三个最容易踩的坑都写出来了

高效调试 AI 大模型 API:用 Apipost 实现 SSE 流式解析与可视化 Apipost 是一款专为现代 API 调试场景设计的高效工具,特别是在流式数据(如大模型的 SSE 输出)场景中表现突出。它不仅能自动解析复杂的 SSE 响应,还能提供直观的可视化效果,有效帮助开发者提升效率和体验。 阅读详情

同时接 DeepSeek、Kimi、Qwen——多模型 API 的 SSE 流式输出,我把三个最容易踩的坑都写出来了

在这里插入图片描述
做 AI 应用开发的同学应该都有这个体会:一开始只接一个模型,跑通了就很开心;业务跑起来之后,产品那边说"能不能让用户可以选模型",于是接第二个;后来发现不同模型擅长的事情不一样,写代码用 DeepSeek,写文案用 Qwen,长文档用 Kimi,慢慢就接了三四个。

接的模型多了以后,最明显的一个感觉是:每个模型都有各自的小脾气。

最明显的是流式输出。单模型的时候,前端随便写写就能跑;模型一多,不同模型的 chunk 格式有细微差异,前端解析偶尔会出诡异的问题。我们线上出过一次,高流量时段有用户反馈"回答的最后一句话缺几个字"——不是乱码,就是少了结尾。

一开始怀疑是模型本身截断,换了模型还是有。又怀疑是超时配置,调了 Nginx 的 proxy_read_timeout 也没用。最后排查了两天,抓包一看——是 TCP 拆包。一个完整的 SSE 帧被拆成了两个 TCP 包到达,前端按行解析的时候第二包里的 JSON 是不完整的,JSON.parse 失败了,这一帧直接丢了。

这个问题在只接一个模型的时候没出现过,因为当时前端写得粗糙,刚好绕过去了。接了多个模型之后,不同模型的 chunk 间隔不一样,触发概率一下子高了很多。

下面把完整的链路拆开讲,从 SSE 协议本身开始,到后端适配层,到前端消费与错误恢复。每一步带可运行的代码。


SSE 协议:为什么 LLM API 不用 WebSocket

SSE(Server-Sent Events)本质上是一个单向、长连接、基于 HTTP 的文本流协议。和 WebSocket 的区别:

特性SSEWebSocket
方向服务器→客户端单向双向
协议HTTP/1.1 或 HTTP/2独立协议(ws://)
重连浏览器原生支持需自行实现
数据格式纯文本(text/event-stream)二进制或文本
防火墙友好走标准 HTTP 端口可能被企业代理拦截

LLM API 选 SSE 的原因很实际:大模型生成 Token 是单向输出,客户端只需要收,不需要发。SSE 的协议开销比 WebSocket 低,且天然支持断线重连(Last-Event-ID 头),适合流式文本传输。

一个标准的 SSE 帧长这样:

data: {"id":"chatcmpl-001","choices":[{"delta":{"content":"你好"}}]}

data: {"id":"chatcmpl-002","choices":[{"delta":{"content":"我是"}}]}

data: [DONE]

注意两个细节:每条 data: 行以 \n\n 作为帧分隔符;流结束信号是完整的一行 data: [DONE]\n\n——注意前缀 data: 不能省略,国内几个模型基本也沿用了这个约定。


后端:FastAPI + Async Generator

核心思路:用一个 async generator 逐块 yield SSE 帧,FastAPI 的 StreamingResponse 负责把生成器映射到 HTTP 响应流。

import asyncio
import json
import time
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse

app = FastAPI()

async def generate_sse_stream(model: str, messages: list, max_tokens: int = 2048):
    """核心流式生成器:每个 chunk yield 一个符合 SSE 格式的字符串"""
    chunk_id = 0

    # 实际项目中这里调用下游 LLM API,同样启用 stream=True
    # 这里用模拟数据演示完整链路
    sample_chunks = ["你好", ",", "我是", "一个", "AI", "助手", "。"]
    for chunk_text in sample_chunks:
        chunk_id += 1
        payload = {
            "id": f"chatcmpl-{chunk_id}",
            "object": "chat.completion.chunk",
            "created": int(time.time()),
            "model": model,
            "choices": [{
                "index": 0,
                "delta": {"content": chunk_text},
                "finish_reason": None
            }]
        }
        # 关键:data: {json}\n\n
        yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
        await asyncio.sleep(0.05)  # 模拟生成间隔

    # 流结束信号
    yield "data: [DONE]\n\n"

@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
    body = await request.json()
    messages = body.get("messages", [])
    model = body.get("model", "default")
    max_tokens = body.get("max_tokens", 2048)

    return StreamingResponse(
        generate_sse_stream(model, messages, max_tokens),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",      # 禁用 Nginx 缓冲
            "Access-Control-Allow-Origin": "*",
        }
    )

三个容易踩的坑:

X-Accel-Buffering: no——如果前面挂了 Nginx,不加这个头 Nginx 会攒够 buffer 才一次吐出,流式变批处理。这个问题在本地调试的时候发现不了,上线后才暴露。

ensure_ascii=False——中文字符不要转成 \uXXXX,前端收到后不用额外解码。如果忘了这个参数,前端拿到的是 Unicode 转义序列,虽然能解析,但调试的时候看着很痛苦。

生成器内的异常——async generator 中抛出异常时,FastAPI 会中断流,前端收到不完整的响应。生产环境需要用 try-catch 包裹,并发送一个带 error 字段的最后帧,让前端有机会展示错误信息。


多模型 Chunk 格式差异:Adapter 层

不同模型的 SSE 流式响应格式大致兼容 OpenAI,但细节上各有偏差。一个健壮的系统必须做格式归一化。

实测对接下来的差异点:

from typing import Dict, Any, Optional
import json

class ChunkAdapter:
    """各模型 SSE chunk → 统一格式的适配器
    
    如果你用的是器灵模型广场这类统一接入平台,
    网关层已经做了格式归一化,这段 Adapter 代码基本不需要自己写。
    """

    @staticmethod
    def normalize(model: str, raw_line: str) -> Optional[str]:
        """返回归一化后的文本内容;无文本内容时返回 None。

        注意:返回 None 有两种含义,调用方如需区分应改造返回值:
          - 正常无内容(如 finish_reason=stop 的空 delta 帧)
          - JSON 解析失败(数据格式异常)
        本实现选择静默跳过解析失败的帧,生产环境建议改为
        打日志或抛异常,以便及时发现格式变更。
        """
        if raw_line == "[DONE]" or raw_line.startswith("[DONE]"):
            return None  # 流结束信号

        try:
            data = json.loads(raw_line)
        except json.JSONDecodeError:
            return None  # 非 JSON 行

        # 路径1:OpenAI 标准格式(DeepSeek、Qwen、GLM 均兼容此路径)
        choices = data.get("choices", [])
        if choices:
            delta = choices[0].get("delta", {})
            content = delta.get("content", "")
            if content:
                return content

        # 路径2:某些模型在 finish_reason 为 stop 的最后一帧里
        # delta 为空对象 {},只带 role 字段,这时没有 content,返回 None 即可

        # 路径3:Anthropic 旧版 Completion API(非 Chat Completions 格式)
        # 有 top-level text 字段,但 Anthropic 的新版 Messages API 已迁移到 delta 格式
        if "text" in data and "choices" not in data:
            return data["text"]

        return None

实际对接时发现的问题:部分模型在 finish_reason 为 "stop" 的最后一帧里不带 content;某些模型的首帧 delta 为 {}(空对象),只带 role: "assistant"。Adapter 需要正确处理这些边界情况,否则前端会渲染出 undefined 或丢字。

当时我们接了三个模型之后,每个模型在前端各自写了一套解析逻辑,维护起来很痛苦。后来把归一化提到 Adapter 层统一处理,前端只消费统一的文本增量,代码量少了很多。

如果你也在接多个模型,Adapter 层建议尽早抽出来,不要在每个调用方各自处理。

当然,后来我们把模型接入迁到了器灵模型广场,这件事反而省心了——统一的 OpenAI 兼容格式,不管切到哪个模型,chunk 结构完全一致,自己写的 Adapter 基本就剩了个兜底。这是后话,下面先把「自己处理」的完整链路讲清楚。


大 Chunk 拆包:TCP 拆包导致帧边界丢失

这是我们线上那个"丢最后几个字"问题的根因。

TCP 是流式协议,不保证消息边界。一个 data: {...json...}\n\n 可能被拆成两个 TCP 包到达:

包1: data: {"id":"chatcmpl-001","choices":[{"delta":
包2: {"content":"你好"}}]}\n\n

如果接收端按行处理,第二行的 JSON 是不完整的,JSON.parse 直接抛异常,这帧就丢了。

后端不负责拆包——SSE 帧的边界重建由接收端处理。正确的做法是维护一个行缓冲区。下面这个解析器放在下游 API 消费层(不是网关层,定位要准确):

class SSEParser:
    """流式 SSE 解析器,处理 TCP 拆包导致的帧边界问题
    
    典型使用场景:
        - 在 Python 后端消费第三方 LLM 的流式 API 时
        - 在 Node.js BFF 层转发流式响应时
        - 任何需要逐帧解析 SSE 流的地方
    
    如果使用的是器灵模型广场这类已做归一化的平台,
    前端收到的帧格式很稳定,这个解析器的逻辑可以写得比通用版更简单。
    """

    def __init__(self):
        self._buffer = ""

    def feed(self, chunk: str) -> list[str]:
        """喂入新的数据块,返回本轮解析出的完整消息列表"""
        self._buffer += chunk
        messages = []

        # 以 \n\n 分割帧
        while "\n\n" in self._buffer:
            frame, self._buffer = self._buffer.split("\n\n", 1)
            for line in frame.split("\n"):
                if line.startswith("data: "):
                    data = line[6:]
                    if data == "[DONE]":
                        messages.append("[DONE]")
                    else:
                        messages.append(data)
        return messages

    def flush(self) -> list[str]:
        """消费缓冲区中剩余的数据(连接已关闭时调用)

        注意:如果连接在正常帧边界之前断开,缓冲区里可能残留
        一个不完整的 `data: ...` 行(末尾没有 \\n\\n)。此时直接
        解析会得到残缺的 JSON,JSON.parse 会失败。
        稳妥做法是:只处理缓冲区中**包含完整 \\n\\n 结尾**的内容,
        或者干脆丢弃残余数据并在日志里记录一次 warning。
        下面给出一种保守实现——丢弃不完整残余,避免向调用方传递脏数据。
        """
        messages = []
        if self._buffer and "\n\n" in self._buffer:
            # 缓冲区里还有完整帧,正常解析
            frame, rest = self._buffer.split("\n\n", 1)
            for line in frame.split("\n"):
                if line.startswith("data: "):
                    data = line[6:]
                    if data and data != "[DONE]":
                        messages.append(data)
        if self._buffer:
            # 残余不完整数据,记录后丢弃
            import warnings
            warnings.warn(f"SSEParser: discarded incomplete frame: {self._buffer[:80]!r}")
        self._buffer = ""
        return messages

这个解析器的关键是 _buffer 的设计:不完整的数据永远留在缓冲区,等到下一包数据到达后拼接再解析。避免了 TCP 拆包导致的帧丢失。


断流重连:Last-Event-ID 与恢复机制

大模型生成到一半网络断了——这是生产环境避不开的场景。

SSE 协议原生支持恢复:客户端在重连时带上 Last-Event-ID 请求头,服务器从该 ID 之后继续发送。但大多数 LLM API 的 SSE 实现不支持这个头——断了就断了,不会续传。

实用做法是在自己控制的网关层做断流恢复。下面是一套基于 Redis 缓存的实现思路:

import redis
import json
from fastapi import Request
from fastapi.responses import StreamingResponse

# Redis 连接(用于缓存已生成的 chunk)
redis_client = redis.Redis(host="localhost", port=6379, db=0)

@app.post("/v1/chat/completions")
async def chat_completions_with_recovery(request: Request):
    body = await request.json()
    headers = request.headers
    last_event_id = headers.get("Last-Event-ID")

    if last_event_id:
        cached = redis_client.get(f"stream:{last_event_id}")
        if cached:
            return StreamingResponse(
                replay_from_cache(json.loads(cached)),
                media_type="text/event-stream"
            )

    stream_id = generate_stream_id()
    return StreamingResponse(
        generate_with_cache(stream_id, body),
        media_type="text/event-stream",
        headers={
            "X-Stream-Id": stream_id,
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
        }
    )

async def generate_with_cache(stream_id: str, body: dict):
    """生成流式响应,并缓存每个 chunk 用于断流恢复"""
    chunk_index = 0
    cached_chunks = []

    async for chunk in call_downstream_api(body, stream=True):
        chunk_index += 1
        chunk_data = {
            "id": chunk_index,
            "data": chunk
        }
        cached_chunks.append(chunk_data)

        if chunk_index % 10 == 0:
            redis_client.setex(
                f"stream:{stream_id}",
                300,
                json.dumps(cached_chunks)
            )

        yield chunk

    redis_client.expire(f"stream:{stream_id}", 60)

Last-Event-ID 与 X-Stream-Id 的对应关系:上面的服务端代码用 X-Stream-Id 响应头把 stream_id 传给客户端;客户端重连时需要把这个值放进 Last-Event-ID 请求头发回来。浏览器不会对 POST 请求自动处理这个头,需要前端手动实现:

// 前端重连时手动携带 Last-Event-ID
const streamId = response.headers.get("X-Stream-Id");
// 重连请求
await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Last-Event-ID": streamId ?? "",
  },
  body: JSON.stringify(body),
});

如果使用的是器灵模型广场这类托管平台,断流恢复通常由平台侧处理,前端不需要自己实现这套逻辑。

如果你没有自己实现网关层,一个更轻量的客户端方案是游标偏移重试:前端记录已接收的字符数,重连时把完整对话历史发过去,服务端跳过已生成的部分继续生成。代价是 Token 浪费(重传上下文),但实现简单,适合中小项目。


前端消费:React Hook 完整实现

前端的核心任务:消费 SSE 流,处理 TCP 拆包,展示流式输出,支持中断和重连。

import { useState, useCallback, useRef } from "react";

interface UseSSEStreamOptions {
  url: string;
  body: Record<string, any>;
  onError?: (error: Error) => void;
}

interface UseSSEStreamReturn {
  text: string;
  isStreaming: boolean;
  start: () => Promise<void>;
  abort: () => void;
}

export function useSSEStream({
  url,
  body,
  onError,
}: UseSSEStreamOptions): UseSSEStreamReturn {
  const [text, setText] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);
  const abortRef = useRef<AbortController | null>(null);

  const start = useCallback(async () => {
    setText("");
    setIsStreaming(true);
    abortRef.current = new AbortController();

    try {
      const response = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        // 注意:这里显式覆盖 stream 字段,确保始终启用流式
        // body 类型为 Record<string, any>,展开时若调用方传入了
        // stream: false 会被正确覆盖为 true
        body: JSON.stringify({ ...body, stream: true }),
        signal: abortRef.current.signal,
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${await response.text()}`);
      }

      const reader = response.body?.getReader();
      if (!reader) throw new Error("ReadableStream not supported");

      const decoder = new TextDecoder();
      let buffer = "";

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });

        const parts = buffer.split("\n\n");
        buffer = parts.pop() || "";

        for (const part of parts) {
          for (const line of part.split("\n")) {
            if (!line.startsWith("data: ")) continue;

            const data = line.slice(6);
            if (data === "[DONE]") {
              setIsStreaming(false);
              return;
            }

            try {
              const parsed = JSON.parse(data);
              const content = parsed.choices?.[0]?.delta?.content;
              if (content) {
                setText((prev) => prev + content);
              }
            } catch {
              console.warn("SSE frame parse failed, skipping:", data.slice(0, 50));
            }
          }
        }
      }
    } catch (err) {
      if (err instanceof Error && err.name !== "AbortError") {
        onError?.(err);
      }
    } finally {
      setIsStreaming(false);
    }
  }, [url, body, onError]);

  const abort = useCallback(() => {
    abortRef.current?.abort();
    setIsStreaming(false);
  }, []);

  return { text, isStreaming, start, abort };
}

这个 Hook 的 url 参数,如果接的是器灵模型广场,直接填 https://api.extratoken.cn/api/v1/chat/completions 就行,所有模型共用同一个地址,切模型只改 model 字段。

stream: true 参数保证 TextDecoder 不会把跨字节边界的多字节字符(如中文)切碎。这一点在只传输英文的场景里不容易发现,但中文内容一旦出现了乱码,用户会直接感知到。


实测:流式 vs 非流式的延迟差异

我们在一个真实场景里测了两组数据。问题是「介绍一下 Python 的 GIL」,回答长度约 420 个 Token:

方式首 Token 延迟完整响应时间用户可开始阅读的时间
非流式(等完整响应)-3.2s3.2s
SSE 流式0.28s3.4s0.28s

流式输出的总生成时间其实略长(因为网络传输和计算重叠度不同),但用户感知到的响应速度是快一个数量级的——因为第一个字 280ms 就出来了。

这个数据也解释了一个现象:为什么有些产品的 API 响应很快,但用户感觉"慢"。如果不是流式输出,用户要在空白页等 3 秒;如果是流式,280ms 出第一个字,用户的心理等待时间就消失了。


多模型切换的实际成本

接一个模型的流式输出,上面这些代码基本够用。

但当你要同时接 DeepSeek、Qwen、Kimi 三个模型的流式 API 的时候,问题就不是"怎么解析 SSE"了,而是:

  • 三个模型的 model 参数格式不一样,切换时要改的不只是值,还有命名规则
  • Kimi 的 base_url 和 DeepSeek 不一样,每个调用方都要维护一套配置
  • 格式差异虽然不大,但每个前端开发者各自写一套 Adapter,重复劳动

有一个做法可以省掉上面这些麻烦:用一个统一接入平台,把所有模型的 base_url 和格式差异在平台侧处理好,对外只暴露一个固定的 OpenAI 兼容接口。前端切换模型只改 model 参数,不需要改其他任何地方。我们以器灵模型广场为例,调用方式长这样:

# 所有模型共用同一个 base_url 和同一套鉴权
# 切换模型只改 model 参数,其他代码完全不变

# DeepSeek
resp = requests.post(
    "https://api.extratoken.cn/api/v1/chat/completions",
    headers={"Authorization": "Bearer sk-你的密钥"},
    json={
        "model": "deepseek-v3",
        "messages": [{"role": "user", "content": "介绍一下你自己"}],
        "stream": True
    },
    stream=True
)

# 切换到 Qwen,只改 model 参数
resp = requests.post(
    "https://api.extratoken.cn/api/v1/chat/completions",
    headers={"Authorization": "Bearer sk-你的密钥"},
    json={
        "model": "qwen-max",
        "messages": [{"role": "user", "content": "介绍一下你自己"}],
        "stream": True
    },
    stream=True
)

这种方式的另一个好处是:如果某个模型的 API 出了问题,可以在平台层做 fallback,前端无感知切换到备用模型。这个在生产环境里价值很大,比每个前端各自维护 fallback 逻辑要可靠。


一些个人判断

SSE 流式输出本身不复杂,复杂的是多个模型格式差异的归一化和生产环境的容错处理。

如果你只接一个模型,上面那些代码里去掉 Adapter 层,直接用官方 SDK,基本够用。

如果你要接两个以上的模型,建议在网关层或 BFF 层统一处理格式归一化,不要在前端各自写解析逻辑。后期维护成本低很多。如果觉得自建网关层成本太高,用现成的统一接入平台也是个务实的选择。

另外,流式输出的调试比非流式难——因为问题是"偶尔丢字",不是"每次都报错",需要在高并发场景下才能复现。TCP 拆包那个问题,我们是在上线后第三天、刚好有一波流量高峰的时候才抓到的。所以如果你的项目还没上线,建议在压测阶段专门测一下流式输出在高并发下的表现,不要等上线后再排查。

本地大模型编程实战(33)用SSE实现大模型的流式输出 我们使用大语言模型**qwen3**实现翻译功能。它可以自动识别源语言,就可以翻译为目标语种。 > 像千问这种大模型是基于多语言训练的,所以它支持中文、英文、法文、西班牙等多个语种的翻译。 阅读详情

相关推荐

uni-app App端SSE流式输出踩坑:renderjs + XHR解决方案

在大模型对话类应用中,流式输出是提升交互体验的关键技术。前端常用的SSE(Server-Sent Events)协议在H5端实现简单,但移植到uni-app App端时却常遇到EventSource不可用、无法自定义Header、nvue页面不兼容等问题。本文从流式传输的基本原理出发,剖析传统EventSource在跨端场景下的局限性,并给出基于renderjs与XMLHttpRequest的替代方案。通过XHR的readyState=3状态实现数据增量读取,配合缓冲解析与节流渲染,可在App端稳定模拟SS

weixin_42602241的博客 332

大模型应用之:SSE流式响应

与 WebSocket 技术相比,SSE 使用的是基于 HTTP 的长轮询机制,而不需要建立全双工的网络连接。相比传统的请求-响应模式,SSE 提供了一种持久连接,允许服务器随时向客户端发送事件和数据,实现了实时性的消息传递。相反,逐字蹦出回复可以实现更快的交互响应。此外,逐字蹦出的回复还有助于用户跟踪模型的思考过程,看到它逐步构建回答的方式。实际很多大模型接入的商用场景,并非采用标准化的api-key/base-url的配置化方法,因为出于数据安全因素,大模型服务商并不采用云服务接入方法。

Diligence is the mother of success. 1153

大模型流式输出必备:SSE协议详解与生产环境避坑指南

大模型应用开发中,用户期待的回答边生成边显示,这正是流式输出的体验。基于HTTP的Server-Sent Events(SSE)通过服务端持续推送增量数据,实现了低延迟的单向事件流,成为大模型接口token级输出的核心协议。与轮询或WebSocket相比,SSE无需复杂握手和二进制帧,天然穿透代理与网关,在单向推送场景下具备简单直接的工程优势。但生产环境中,代理缓冲、分块边界乱码、鉴权与重连语义等细节,常常让流式通道“流而不动”。掌握SSE的报文格式、字段含义和传输机制,能帮助开发者快速定位链路卡点,构建稳

weixin_33508411的博客 147

SSE流式输出

用过 ChatGPT 的同学一定对那个“一个字一个字往外蹦”的效果印象深刻。很多人以为这是前端做的动画,其实它来自模型 API 本身的能力。如果你还在用同步调用让用户干等 10 秒才看到完整回复,那这篇文章就是为你准备的。本文将带你完整实现一个生产可用的流式对话系统:从 Spring AI 的基础,到 SSE 推送到前端,再到多轮记忆、异常处理和性能选型。全程有代码、有测试命令、有前端示例,跟着做就能直接搬进你的项目。

· 483

前端接入大模型实践:SSE流式输出与工程落地指南

以“大模型应用”和“流式输出”为切入点,本文从技术科普与工程实践结合的角度,系统梳理前端集成AI能力的核心路径。首先介绍自然语言交互、文档摘要生成等典型场景,明确前端作为模型能力包装者的定位;继而解析SSE(Server-Sent Events)协议原理,对比WebSocket说明其在AI对话中的优势,并阐述流式数据解析、TextDecoder处理半包等关键技术细节;同时强调API密钥安全、成本控制、降级方案等工程化要点。通过一个AI文档助手的完整实现,展示从接口设计、状态机管理到Markdown增量渲染的

weixin_32943857的博客 168

大模型流式输出首选SSE:协议核心、前端接入与生产环境避坑指南

在AI产品体验中,流式输出让用户感知等待时间大幅缩短,已成为大模型对话场景的标配。支撑这一能力的关键技术之一,是服务器主动推送数据到客户端的HTTP长连接方案——SSE(Server-Sent Events)。它基于标准HTTP协议,无需额外的双向通道,就能以极低接入成本实现单向后持续推送,天然适配模型Token逐字生成的过程。理解SSE的事件流格式、浏览器原生EventSource的重连机制,以及在FastAPI等后端框架中如何写流式响应,对前端工程实践尤为重要。相比WebSocket,SSE在代理兼容性

weixin_29012765的博客 221

Spring AI 流式对话踩坑:SSE 已关闭,为什么大模型还在继续生成?

大模型执行停止问题解决方案 摘要 在企业AI对话项目中,当用户点击"停止"按钮时,发现大模型仍在后台运行,导致Token浪费和资源泄漏问题。经过排查发现根本原因在于:1)缺乏停止接口机制;2)无会话管理;3)浏览器断连无感知;4)缺少兜底清理。解决方案采用Reactor取消传播机制,通过维护StreamRegistry会话管理,实现前端停止信号→取消Flux订阅→中断HTTP连接的完整链路。架构设计上采用策略模式+工厂模式,分离意图分析和处理逻辑,确保系统可扩展性。该方案有效解决了资源泄漏问题,降低了Tok

分享 AI Agent、大模型与后端开发实战; 空门技术栈。 609

SSE流式输出与LangChain结构化输出:大模型应用实战指南

SSE(Server-Sent Events)是一种基于HTTP的服务器单向推送技术,通过持久连接实现数据帧的实时传输,天然适合大模型流式输出场景。其核心原理是服务端保持连接并逐帧发送文本数据,客户端按双换行符切分解析,从而实现打字机效果。在工程实践中,SSE结合fetch流式读取与缓冲区机制,能有效解决中文乱码、帧粘连等问题,同时通过心跳保活和代理配置优化连接稳定性。LangChain的结构化输出则通过Pydantic解析器、Function Calling和JSON Mode等方案,确保模型输出符合预定

weixin_32495691的博客 332

大模型流式输出实战:从SSE到Fetch API与ReadableStream的完整解析

在构建现代AI应用时,提升用户体验的核心技术之一是流式输出。其本质是一种数据推送机制,允许服务器在生成内容的同时,将数据分块实时传输至客户端,从而消除用户等待。从技术原理看,这依赖于HTTP长连接和特定的数据流协议。在工程实践中,开发者常面临传统SSE协议的限制,如仅支持GET请求和单向通信,因此转向更底层的Fetch API与ReadableStream方案。Fetch API提供了对网络请求的精细控制,而ReadableStream则作为处理二进制数据流的抽象接口,结合Uint8Array进行高效解码。

weixin_29009339的博客 256

前端转AI开发实战:SSE流式对话Demo从原理到踩坑

流式输出是AI应用开发中的核心交互范式,其底层依赖HTTP长连接与Server-Sent Events(SSE)协议实现服务端单向推送。SSE基于标准HTTP协议,通过text/event-stream格式持续传输数据碎片,前端利用ReadableStream逐块消费并实时渲染,从而在AI对话场景中实现逐字输出的流畅体验。相比WebSocket的全双工通信,SSE在单向推送场景下实现更简单、部署更友好,是当前AI对话产品的主流技术选型。该技术广泛应用于智能客服、AI助手、实时翻译等场景。本文以一个最小可运行

weixin_29682503的博客 279

KIMI API流式输出实战:从SSE原理到Vue3前端联调与踩坑指南

在大模型对话应用中,流式输出是提升用户体验的关键技术,其底层基于SSE(Server-Sent Events)协议,允许服务器将生成的文本分片持续推送至客户端,使用户无需等待完整回复即可看到逐字出现的内容。与WebSocket不同,SSE采用单向HTTP传输,实现简单且天然支持自动重连,尤其适合请求一次、持续接收的对话场景。通过合理配置max_tokens、temperature等参数,并结合后端转发层隔离密钥与鉴权逻辑,可以构建稳定高效的大模型交互链路。此类技术广泛应用于ChatBot、AI写作助手、知识

weixin_29016865的博客 226

AI时代必备技能:深度解析流式输出与SSE实战

文章摘要: 2025年AI智能体爆发时代,流式输出技术成为前端开发的核心竞争力。本文解析流式输出的必要性:大模型Token逐生成特性、用户体验优化(降低等待焦虑),并重点介绍SSE(Server-Sent Events)技术实现方案。通过对比SSE与WebSocket,提供前端到后端的实战代码(模拟AI聊天逐字输出),涵盖生产级优化(错误重连、内存管理)及打字机动效等最佳实践。流式输出是AI产品实时交互的关键,适用于代码生成、写作助手等场景,助力开发者打造极致用户体验。

Q2024107的博客 1027

Kimi API流式输出实战:SSE协议、前后端对接与踩坑全解析

大语言模型接口调用中,流式输出已成为提升交互体验的关键技术。相比传统的一次性返回,流式输出通过SSE(Server-Sent Events)协议在HTTP长连接上持续推送数据,让用户在第一字生成时即可看到反馈,显著缓解等待焦虑。SSE基于纯文本格式,以data:开头,实现简单且适合单向推送场景。这一机制广泛应用于聊天机器人、智能客服等AI应用,是构建“打字机式”输出体验的核心。基于OpenAI兼容接口,Kimi API支持流式调用,本文从参数配置、Python后端对接、Vue3前端解析到常见错误排查,完整梳

weixin_34221654的博客 256

SSE(使用SSE完成大模型流式输出)

经常使用大模型AI的小伙伴应该不难发现,AI的回答都是一个字一个字蹦出来的,而不是整段文字直接出现。对大模型并不了解的小伙伴可能会误以为这是前端的动画效果。实际上,大模型的回复本身是逐词( 严格来说是逐token) 生成的,如果等全部生成完再返回,用户会明显感到卡顿。所以在实际部署和应用中,大模型问答应用通常采用,也就是所谓的“一个字一个字蹦出来”。既然流式文本输出并非前端特效,那么要实现这样的效果,自然就要用到。在常见的实时通讯方式中,我们。

fakeend的博客 584

KIMI API流式输出实战:SSE协议、前后端代码与避坑指南

大模型API调用中,同步等待的响应方式在复杂任务下动辄延迟数十秒,用户体验极差。流式输出通过Server-Sent Events(SSE)协议,将模型生成的文本以增量块(chunk)形式持续推送,将首字延迟(TTFT)压缩到几百毫秒,显著提升交互反馈感。SSE基于普通HTTP长连接,实现简单、兼容性强,成为主流大模型厂商的通用选择。这一技术广泛适用于聊天机器人、智能客服、实时翻译等需要逐字展示的Web应用,尤其在Vue3等前端框架中可轻松实现打字机效果。本文围绕KIMI API的流式输出实践,剖析SSE数据

weixin_32715349的博客 318

【Proteus】NE555纯硬件实现LED呼吸灯效果,附源文件,效果展示

本设计通过NE555与三极管的巧妙配合,仅用基础元件实现了呼吸灯效果。核心在于利用RC积分电路将PWM信号转换为控制电压,并通过负反馈动态调节占空比。Proteus仿真验证了方案的可行性,读者可进一步优化参数或扩展功能,探索更多硬件设计可能性。

2201_75797484的博客 2978

第七篇:大模型API调用——从Token到流式输出

Token作为计费单位,反映计算量,中英文Token比例不同。Temperature控制输出随机性,Top-p限制候选词范围,建议先固定Top-p再调Temperature。 流式输出提升响应感知速度,优于普通请求。SSE比WebSocket更轻量,适合单向AI回答场景。 生产环境需注意Nginx代理配置和API密钥安全。

2503_90542405的博客 487

海康威视VS互联网大厂:薪资、福利、工作强度全方位对比

本文全面对比了海康威视与互联网大厂在薪资、福利和工作强度等方面的差异。海康威视提供稳定的薪资结构和较低的工作强度,适合追求专业深度的职场人;而互联网大厂则以高薪和快速成长吸引人才,但工作压力较大。文章还分析了职业发展路径和长期价值,帮助求职者做出明智选择。

weixin_29171129的博客 386

大模型流式输出前端实战:ReadableStream+SSE打字机效果全踩坑指南

AI打字机效果不是前端动画造假,是一套完整的字节传输链路:LLM生成Token → 后端封装SSE事件 → HTTP分块流 → ReadableStream接收二进制 → Uint8Array字节 → TextDecoder流式解码 → buffer缓存拆分完整事件 → JSON解析delta增量 → Vue响应式实时渲染字节边界:流式TextDecoder处理中文分片,杜绝乱码;事件边界:buffer缓存不完整SSE片段,防止解析报错;安全边界:密钥存后端BFF,前端不暴露任何大模型鉴权信息。

HHX_01的博客 277

VConsole动态加载

// 移动端真机调试工具 vconsole import VConsole from "vconsole"; import Vue from "vue"; // 动态使用VConsole if(process.env.NODE_ENV==='production'){ if(process.env.VUE_APP_BASEURL.includes('test')||process.env.VUE_APP_BASEURL.includes('pre')){ Vue.prototype

aliven1的博客 1041

保姆级教程:用Unidbg搞定阿里系App的x-sign和长x-mini-wua签名(附完整Java代码)

本文详细介绍了如何使用Unidbg框架模拟执行阿里系App的原生库,生成关键的x-sign和x-mini-wua签名参数。通过完整的Java代码示例和实战技巧,帮助开发者高效实现签名算法,适用于安全研究、数据分析和第三方应用开发场景。

weixin_29243279的博客 483

阿里云 ESA AI 助手是什么?五大核心能力(智能问答/诊断/域名迁移/智能配置/应用部署)全解析

基于 ESA 的边缘函数和 Pages 能力,支持"一句话生成应用"——你可以描述一个网站页面、一个游戏小程序;开通ESA,进入 ESA 控制台的 AI 助手页面,在对话框里输入你的问题或任务就能开始。,就是想解决这个"碎"和"门槛"的问题——一个基于大语言模型、深度融合 ESA 产品知识与运维场景的智能运维辅助工具。从"翻文档、提工单、抄配置",到"说句话就办好"——边缘安全加速的运维方式,正在被重新定义。不知道 ESA 有哪些套餐。诊断结果会告诉你"为什么"和"怎么办",但最终处置权始终在你手里;

云布道师 103

UEFI开发探索102 – ACPI探究01(UEFI配置表)

(请保留-> 作者: 罗冰 https://blog.youkuaiyun.com/luobing4365) ACPI探究01(UEFI配置表)1 大致规划2 UEFI Configuration Table(配置表)3 代码实现 最近有项工作,是修改ACPI表。问题本身并不复杂,但是由于代码需要移植到Option ROM上,遇到不少奇怪的现象。因此,花了不少时间,对ACPI进行研究。 任务是完成了,我的好奇心又被勾起来了。准备发挥“格物致知”的信念,把我认为的ACPI各方面的知识,好好地捋一捋。 1 大

8244
上一篇: Seedance 2.0 短剧分镜实战:运镜、节奏、角色保持全攻略
下一篇: 多模型 API 网关压测:并发、延迟与计费的三角平衡
器灵科技
博客等级 码龄174天 3粉丝 105原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值