从源码里深度探索流式推理

目录

前言:

1.SSE协议的前置了解

核心特点

数据格式

[2.LangChain 流式传输流程分析](#2.LangChain 流式传输流程分析)

[2.1 LangChain 请求 OpenAI 使用什么协议?](#2.1 LangChain 请求 OpenAI 使用什么协议?)

[2.2LangChain 如何支持流式传输?](#2.2LangChain 如何支持流式传输?)

[2.3 OpenAI 返回的块是什么格式,如何转换成 AIMessageChunk ?](#2.3 OpenAI 返回的块是什么格式,如何转换成 AIMessageChunk ?)

3.总结,为什么是SSE不是WebSocket?

维度一:底层依赖的统一性

维度二:数据流向与对象转换的单向性

[维度三:状态维护与生命周期的解耦(Stateless vs Stateful)](#维度三:状态维护与生命周期的解耦(Stateless vs Stateful))

[WebSocket 缺陷](#WebSocket 缺陷)

[SSE 原生优势](#SSE 原生优势)


前言:

在传统Web应用中,用户发起请求,服务器处理完毕后一次性返回数据。但在大语言模型(LLM)时代,尤其是随着 DeepSeek-R1、o1 等推理模型(Reasoning Models)的普及,一个长文本或复杂推理的生成可能需要数十秒甚至数分钟。

如果采用传统的"等待-响应"模式,用户将面临漫长的白屏焦虑,首字延迟(TTFT, Time to First Token)高到无法接受。因此,流式传输成为了大模型应用的底层标配 。它不仅是前端的"打字机动画",更是一场从模型微观生成到宏观网络传输的全链路流水线(Pipeline)优化

所以在学习Langchain的过程中,我感觉到流式推理底层的流式传输,或许能为我们打开一种新的剖解AI agent一个个封装的途径,以小窥大,所以写一下自己的所得。

1.SSE协议的前置了解

HTTP 协议本身设计为无状态的请求-响应模式,严格来说,是无法做到服务器主动推送消息到客户端,但通过 Server-Sent Events (服务器发送事件,简称 SSE)技术可实现 流式传输 ,允许服
务器主动向浏览器推送数据流。
也就是说,服务器向客户端声明,接下来要发送的是流消息(streaming),这时客⼾端不会关闭连接, 会⼀直等待服务器发送过来新的数据流。
SSE(Server-Sent Events)是⼀种基于 HTTP 的轻量级实时通信协议,浏览器可以通过内置的
EventSource API 接收并处理这些实时事件。

核心特点

  1. **基于HTTP协议。**复用标准HTTP/HTTPS协议,无需额外端口或协议,兼容性好且易于部署。
  2. **单向通信机制。**SSE仅支持服务器向客户端的单向数据推送,客户端通过普通HTTP请求建立连接后,服务器可持续发送数据流,但客户端无法通过同一连接向服务器发送数据。
  3. **自动重连机制。**支持断线重连,连接中断时,浏览器会自动尝试重新连接(支持retry字段指定重连间隔)。
  4. 自定义消息类型。 客户端发起请求后,服务器保持连接开放,响应头设置Content-Type:text/event-stream,标识为事件流格式,持续推送事件流。

当我们梳理完 SSE 的技术特点后会发现,它与嵌入式数据库领域的常青树 SQLite 拥有一个惊人一致的底层核心特质------极致的轻量化(Lightweight) 。这种轻量化并不是牺牲功能,而是在特定高频场景下,通过做减法来达到性能和部署的最高性价比。

数据格式

既然 SSE 和 SQLite 一样追求极致的轻量化,那它在网络层传输时必然不会引入复杂的二进制帧协议。实际上,SSE 的数据格式纯粹得令人发指------它就是纯文本流(Text Stream)

当大模型后端(如 OpenAI、DeepSeek 或我们自己的 FastAPI 服务)准备好推流时,首先需要在 HTTP 响应头(Response Headers)中做出如下声明,告诉浏览器:"准备好,接下来的数据没有总长度,是个无尽的事件流":

复制代码
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache

这就是我们的HTTP请求头,那再看我们的消息部分。

每一次发送的消息,由若干个message组成,每个message之间由\n\n分隔,表示这一次的消息我已经发完了。而每个message内部由若干行组成,每一行都是如下格式:

复制代码
[field]: value\n

Field可以取值为:

  • data必需:数据内容
  • event非必需:表示自定义的事件类型,默认是message事件
  • id非必需:数据标识符,相当于每一条数据的编号
  • retry非必需:指定浏览器重新发起连接的时间间隔

除此之外,还可以有冒号:开头的行,来表示注释。

如:

复制代码
event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: end\n
data: a bar event\n\n

那了解了SSE的核心,我们接下来就来看一下Langchain的流式传输流程。

2.LangChain 流式传输流程分析

前文我们也提到,LangChain 并不会自行定义或生成底层网络传输协议,它的网络通信完全依赖两层底层组件:一是大模型厂商(例如 OpenAI)提供的服务接口协议,二是业务应用所搭载的 Web 框架(例如 FastAPI)所遵循的网络规范。

基于这一架构,LangChain 具备流式输出能力的核心逻辑可以拆解为两点:

  1. 流式数据的源头由大模型服务商提供:厂商侧原生支持 SSE 等流式传输机制,能够分段返回模型生成内容;
  2. LangChain 仅做封装与数据标准化处理:它封装了厂商流式接口的调用逻辑,持续接收分段返回的数据,再统一解析、封装为标准的 AIMessageChunk 消息块,向上层业务提供统一格式的流式数据,屏蔽不同大模型厂商接口的差异化。

接下来我们将会通过分析相关源码探索整个传输流程。整个过程我们以OpenAI举例,其他大模型方式也是类似的。当我们向OpenAI发起流式请求,LangChain实际上会通过BaseChatOpenAI 类中的 _stream() 方法发起调用。

具体完整源码在class langchain_openai.chat_models.base.BaseChatOpenAI

python 复制代码
def _stream(
    self,
    messages: list[BaseMessage],  # 输⼊消息列表
    stop: Optional[list[str]] = None,  # 可选的停⽌词列表,⽤于指定⽣成终⽌条件
    run_manager: Optional[CallbackManagerForLLMRun] = None,  # 回调管理器,⽤于处理⽣成过程中的回调事件
    *,
    stream_usage: Optional[bool] = None,  # 是否流式返回使⽤量统计信息
    **kwargs: Any,  # 其他可选参数
) -> Iterator[ChatGenerationChunk]:
    # 1. 流式配置
    kwargs["stream"] = True  # 强制启⽤流式模式
    stream_usage = self._should_stream_usage(stream_usage,** kwargs)
    # 2. 请求构建
    payload = self._get_request_payload(messages, stop=stop, **kwargs)
    # 注意这⾥定义了AIMessageChunk传输块
    default_chunk_class: type[BaseMessageChunk] = AIMessageChunk
    base_generation_info = {}
    # 3. 发起调⽤
    # 3.1 指定 response_format 调⽤
    if "response_format" in payload:
        response_stream = self.root_client.beta.chat.completions.stream(**payload)
        context_manager = response_stream
    # 3.2 普通流式调⽤
    else:
        response = self.client.create(**payload)
        context_manager = response
    # 4. 响应处理
    try:
        with context_manager as response:
            for chunk in response:
                # 4.1 将 OpenAI 数据块转换为 AIMessageChunk 数据块
                generation_chunk = self._convert_chunk_to_generation_chunk(
                    chunk,
                    default_chunk_class,
                    base_generation_info if is_first_chunk else {},
                )
                if generation_chunk is None:
                    continue
                # 4.2 触发新token回调
                if run_manager:
                    run_manager.on_llm_new_token(
                        generation_chunk.text,
                        chunk=generation_chunk,
                        logprobs=logprobs,
                    )
                # 4.3 产出⽣成块
                yield generation_chunk
    # 5. 处理OpenAI API错误异常
    except openai.BadRequestError as e:
        _handle_openai_bad_request(e)

从上述流程看来,这就是流式逐块产生AIMessageChunk 聊天消息的核心方法。那么接下来看三个问题:发起调用时,

  1. 底层使用什么协议?
  2. 如何支持流式传输?
  3. 返回的块是什么格式,如何转换成AIMessageChunk?

这三个问题都掌握后,整个流式传输的流程就都能理解了。

2.1 LangChain 请求 OpenAI 使用什么协议?

回答这个问题,需要看LangChain关于OpenAI的客户端是怎么定义的。让我们找到class
langchain_openai.chat_models._client_utils.SyncHttpxclientwrapper
, 如下:

python 复制代码
import openai
import os

class _SyncHttpxClientWrapper(openai.DefaultHttpxClient):
    """Borrowed from openai._base_client"""
    def __del__(self) -> None:
        if self.is_closed:
            return
        try:
            self.close()
        except Exception:  # noqa: S110
            pass

def _build_sync_httpx_client(
    base_url: Optional[str], 
    timeout: Any
) -> _SyncHttpxClientWrapper:
    return _SyncHttpxClientWrapper(
        base_url=base_url
        or os.environ.get("OPENAI_BASE_URL")
        or "https://api.openai.com/v1",
        timeout=timeout,
    )

我们可以看到:

  • openai.DefaultHttpxClient 是 OpenAI 官方 SDK 内置的同步 HTTP 请求客户端,底层基于 httpx 实现 HTTP 通信,定义在 openai._base_client 模块;
  • LangChain 没有自己从零封装 HTTP 请求逻辑,而是直接继承官方 SDK 的原生 HTTP 客户端,仅新增析构函数做资源自动关闭,复用 OpenAI SDK 完整的底层网络逻辑;
  • 构建客户端函数 _build_sync_httpx_client 沿用官方逻辑:优先传入的 base_url → 环境变量 OPENAI_BASE_URL → 官方默认地址 https://api.openai.com/v1,完全对齐 OpenAI SDK 配置规则。

所以,在调用时,发起的是HTTP调用!

2.2LangChain 如何支持流式传输?

开篇我们就提到,LangChain 本身不会自定义、制定底层网络传输协议,而是完全依赖底层大模型厂商(如 OpenAI)自身定义的通信协议。

在流式接口 `_stream()` 源码第一步中,会强制传入 `stream=True` 参数。该参数作用是告知 OpenAI 服务端采用 SSE(Server-Sent Events,服务器推送事件)流式返回响应内容。开启流式后,API 会持续保持 HTTP 长连接,按固定分段格式持续下发生成数据。

举个示例:使用原生 GPT 模型发起携带 `stream=True` 的请求,提问内容为「你好,我是张三。」,服务端会分段返回多条 SSE 数据块,简化后的有效载荷序列示例如下:

python 复制代码
data:{
  "id": "chatcmpl-123",
  "object": "chat.completion.chunk",
  "created": 1717500000,
  "model": "gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "delta": {
        "role": "assistant",
        "content": "你好"
      },
      "finish_reason": null
    }
  ]
}

data:{
  "id": "chatcmpl-123",
  "object": "chat.completion.chunk",
  "created": 1717500000,
  "model": "gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": ","
      },
      "finish_reason": null
    }
  ]
}

data:{
  "id": "chatcmpl-123",
  "object": "chat.completion.chunk",
  "created": 1717500000,
  "model": "gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "张"
      },
      "finish_reason": null
    }
  ]
}

data:{
  "id": "chatcmpl-123",
  "object": "chat.completion.chunk",
  "created": 1717500000,
  "model": "gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "三"
      },
      "finish_reason": null
    }
  ]
}

data:{
  "id": "chatcmpl-123",
  "object": "chat.completion.chunk",
  "created": 1717500000,
  "model": "gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "!"
      },
      "finish_reason": null
    }
  ]
}

data:{
  "id": "chatcmpl-123",
  "object": "chat.completion.chunk",
  "created": 1717500000,
  "model": "gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "请问"
      },
      "finish_reason": null
    }
  ]
}
....

看了上述示例,我们就可以回答第二个问题:在请求参数中配置 stream=True,即可开启 OpenAI 服务端分块流式返回数据。LangChain 正是通过 _stream() 方法的第 1、2 步骤完成该配置与请求封装逻辑。

2.3 OpenAI 返回的块是什么格式,如何转换成 AIMessageChunk ?

OpenAI 返回的数据块格式前文已经展示,将原生 API 分块数据转换为 LangChain 自定义AIMessageChunk对象,核心依靠 _convert_chunk_to_generation_chunk() 方法完成转换逻辑,关键源码如下:

python 复制代码
def _convert_chunk_to_generation_chunk(
    self,
    chunk: dict,  # 从API获取的原始数据块,示例中即为OpenAI返回SSE数据块
    default_chunk_class: type,  # 待创建消息块类型,示例传入AIMessageChunk
    base_generation_info: Optional[dict],  # 基础生成信息
) -> Optional[ChatGenerationChunk]:
    # 1. 提取选择项数据
    choices = (
        chunk.get("choices", [])
        # 兼容beta.chat.completions.stream返回结构
        or chunk.get("chunk", {}).get("choices", [])
    )
    # 2. 处理有效选择项
    choice = choices[0]
    if choice["delta"] is None:
        return None
    # 3. 转换增量数据为消息块,将API的delta格式转换为 AIMessageChunk
    message_chunk = _convert_delta_to_message_chunk(
        choice["delta"], default_chunk_class
    )
    # 4. 构建其他生成信息
    generation_info = {**base_generation_info} if base_generation_info else {}
    # 省略中间无关逻辑...
    # 5. 返回封装完成的流式生成块
    generation_chunk = ChatGenerationChunk(
        message=message_chunk, generation_info=generation_info or None
    )
    return generation_chunk
python 复制代码
def _convert_delta_to_message_chunk(
    _dict: Mapping[str, Any], default_class: type[BaseMessageChunk]
) -> BaseMessageChunk:
    # 1. 提取OpenAI格式增量数据
    id_ = _dict.get("id")
    role = cast(str, _dict.get("role"))  # 获取消息角色
    content = cast(str, _dict.get("content") or "")  # 获取分段文本内容
    additional_kwargs: dict = {}

    # 解析函数调用数据
    if _dict.get("function_call"):
        function_call = dict(_dict["function_call"])
        if "name" in function_call and function_call["name"] is None:
            function_call["name"] = ""
        additional_kwargs["function_call"] = function_call

    tool_call_chunks = []
    # 解析工具调用分段数据
    if raw_tool_calls := _dict.get("tool_calls"):
        additional_kwargs["tool_calls"] = raw_tool_calls
        try:
            tool_call_chunks = [
                tool_call_chunk(
                    name=rtc["function"].get("name"),
                    args=rtc["function"].get("arguments"),
                    id=rtc.get("id"),
                    index=rtc["index"],
                )
                for rtc in raw_tool_calls
            ]
        except KeyError:
            pass

    # 2. 根据角色字段,构造对应LangChain消息块对象
    if role == "user" or default_class == HumanMessageChunk:
        return HumanMessageChunk(content=content, id=id_)
    elif role == "assistant" or default_class == AIMessageChunk:
        return AIMessageChunk(
            content=content,
            additional_kwargs=additional_kwargs,
            id=id_,
            tool_call_chunks=tool_call_chunks,  # type: ignore[arg-type]
        )
    elif role in ("system", "developer") or default_class == SystemMessageChunk:
        if role == "developer":
            additional_kwargs = {"__openai_role__": "developer"}
        else:
            additional_kwargs = {}
        return SystemMessageChunk(
            content=content, id=id_, additional_kwargs=additional_kwargs
        )
    elif role == "function" or default_class == FunctionMessageChunk:
        return FunctionMessageChunk(content=content, name=_dict["name"], id=id_)
    elif role == "tool" or default_class == ToolMessageChunk:
        return ToolMessageChunk(
            content=content, tool_call_id=_dict["tool_call_id"], id=id_
        )
    elif role or default_class == ChatMessageChunk:
        return ChatMessageChunk(content=content, role=role, id=id_)
    else:
        return default_class(content=content, id=id_)  # type: ignore

到此我们就知道了 LangChain 流式传输的完整流程与底层协议。总结一下:

  1. langchain-openai 包通过集成 OpenAI Python SDK,提供了一个 HTTP 客户端。
  2. 因此,支持 LangChain 向 OpenAI 的 API 发起调用请求。
  3. 若希望发起流式传输请求,则需在请求中加入 stream=True,向 OpenAI 说明以 SSE 协议进行流式返回。
  4. LangChain 接收 OpenAI 的 SSE 格式的响应,并将其转换为 LangChain 自封装的消息格式,如 AIMessageChunk 消息。这样就可以以统一的方式处理来自不同模型提供商(OpenAI, Anthropic 等)的流式响应。

3.总结,为什么是SSE不是WebSocket?

通过前文对 LangChain 核心源码的拆解,我们可以上升一下维度调用栈、数据结构转换和状态机三个维度,推导出大模型流式传输必然选择 SSE(而不是 WebSocket)的深层技术必然性:

维度一:底层依赖的统一性

根据前文源码,LangChain_SyncHttpxClientWrapper 直接继承 OpenAI SDK 的 DefaultHttpxClient,底层通信完全依托基于标准 HTTP 的 httpx 库。 如果改用 WebSocket: 需要新增 websockets、aiohttp 等 WebSocket 专用第三方库,同时改造厂商 API 网关,和当前 RESTful HTTP 整体架构冲突。

现有架构优势: 仅需一行 kwargs["stream"] = True,无需修改已有 HTTP 连接、无需新增依赖,服务端就能直接切换成长连接流式响应,最大化复用标准 80/443 端口的 HTTP/HTTPS 架构。

维度二:数据流向与对象转换的单向性

从 _convert_delta_to_message_chunk 源码可看出,数据流是纯单向流水线:

流程特征:

  1. 客户端通过 _stream() 一次性提交完整请求载荷;
  2. 服务端持续单向产出 AIMessageChunk 并通过 yield 推送;
  3. 整个流式生命周期,客户端不需要反向发送任何指令、数据。

WebSocket 全双工双向能力在此场景完全冗余,额外的帧封包、拆包逻辑会增加 CPU 性能损耗。

维度三:状态维护与生命周期的解耦(Stateless vs Stateful)

在分布式 AI、Agent 业务中,Nginx、K8s Ingress 等网关稳定性十分关键。

WebSocket 缺陷

属于有状态连接,握手后客户端固定绑定单一后端实例;大模型推理时常达数十秒至数分钟,极易触发网关超时断开,集群扩容、负载均衡难以平滑处理。

SSE 原生优势

代码中 with context_manager as response: 上下文依然保留标准 HTTP 语义:

  1. 出现 openai.BadRequestError 等异常时,可直接复用 HTTP 标准异常捕获、网关重试能力;
  2. SSE 自带 retry 机制,断连后客户端携带 chatcmpl-xxx 事件 ID 自动重连恢复生成;
  3. 传输层流式、应用层接近无状态,对长耗时推理模型容错能力更强。