【LangChain 开发】LangChain 聊天模型——流式传输


🚀 欢迎来到我的CSDN博客:Optimistic _ chen
一名热爱技术与分享的全栈开发者,在这里记录成长,专注分享编程技术与实战经验,助力你的技术成长之路,与你共同进步!


🚀我的专栏推荐

专栏内容特色适合人群
🔥C语言从入门到精通系统讲解基础语法、指针、内存管理、项目实战零基础新手、考研党、复习
🔥Java基础语法系统解释了基础语法、类与对象、继承Java初学者
🔥Java核心技术面向对象、集合框架、多线程、网络编程、新特性解析有一定语法基础的开发者
🔥Java EE 进阶实战Servlet、JSP、SpringBoot、MyBatis、项目案例拆解想快速入门Java Web开发的同学
🔥Java数据结构与算法图解数据结构、LeetCode刷题解析、大厂面试算法题面试备战、算法爱好者、计算机专业学生
🔥Redis系列从数据类型到核心特性解析项目必备

🚀我的承诺:
✅ 文章配套代码:每篇技术文章都提供完整的可运行代码示例

✅ 持续更新:专栏内容定期更新,紧跟技术趋势

✅ 答疑交流:欢迎在文章评论区留言讨论,我会及时回复(支持互粉)


🚀 关注我,解锁更多技术干货!
⏳ 每天进步一点点,未来惊艳所有人!✍️ 持续更新中,记得⭐收藏关注⭐不迷路 ✨

📌 标签:#技术博客#编程学习#Java#C语言#算法#程序员

流式处理

流式处理(Streaming)是 LangChain 中非常核心的能力,尤其在构建聊天应用或需要实时反馈的场景中。它能让 LLM 的输出像打字机一样逐步呈现,而不必等整个响应生成完毕,大幅提升用户体验。

  • 我们之前直接使⽤invoke的调⽤⽅式属于⾮流式传输,看到的现象是聊天模型直接返回全量内容,若模型思考时间较⻓,则我们等待的时间就越⻓
  • 流式返回减少用户等待时间使系统与用户的交互性大大提高

stream()同步传输

LangChain 现在推荐使用 LCEL(LangChain Expression Language) 构建的 Runnable 体系(也就是链),其内置 .stream() 方法

from langchain.chat_models import init_chat_model

model = init_chat_model("deepseek-v4-pro", model_provider="deepseek")

chunks=[]
for chunk in model.stream("简单介绍LangChain流式输出"):
    chunks.append(chunk)
    print(chunk.content,end="|",flush=True)

在这里插入图片描述
.stream() 返回一个迭代器,每次产出的是一个 AIMessageChunk,其 .content 是增量文本。

astream()异步传输

import asyncio

from langchain.chat_models import init_chat_model

model = init_chat_model("deepseek-v4-pro", model_provider="deepseek")

async def async_stream():
    print("=========异步调用======")
    async for chunk in model.astream("简单解释LangChain流式传输"):
        print(chunk.content,end="|",flush=True)
asyncio.run(async_stream())

之前定义聊天模型中提到,聊天模型、输出解析器等组件都实现Runnable接口,它们都是Runnable接口的实例:
在这里插入图片描述
所以.stream().astream() ⽅法产⽣的块(chunk)类型取决于正在流式传输的组件。我们当前正在使⽤聊天模型的流式传输,返回的每个块都将是⼀个AIMessageChunk但是,对于其他组件,块类型可能不同。

输出解析器

接下俩我们使用LCEL构建一简单的链,结合模型和解析器来构建

#定义大模型
model = init_chat_model("deepseek-v4-pro", model_provider="deepseek")
#定义输出解析器
parser=StrOutputParser()
# 定义链
chain=model|parser

for chunk in chain.stream("写一首关于程序员的诗词,5句话"):
    print(chunk,end="|",flush=True)

我们使⽤模型和解析器,StrOutputParser 来解析模型的输出,它从AIMessageChunk 中提取内容字段,因为model.stream(...) 每次产出的 chunk 是一个 AIMessageChunk 对象,它包含 .content(文本内容)以及可能的额外字段(如工具调用信息等)
在这里插入图片描述
StrOutputParser 在这里负责把模型输出的消息对象简化为纯文本,让流式循环中拿到的每一块内容直接就是可打印的字符串,并且保留了流式的分块效果。

如果想要更改解析器的输出方法,可以通过在链中使用生成器函数完成自定义解析器。

import re
from typing import Iterator
from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser

# 定义大模型
model = init_chat_model("deepseek-v4-pro", model_provider="deepseek")
# 定义输出解析器
parser = StrOutputParser()
# 定义链
chain = model | parser


def stream_sentences(chain, prompt: str) -> Iterator[str]:
    """
    生成器函数:按句输出模型的流式结果
    - chain: 已经组装好的 Runnable
    - prompt: 用户输入
    yield 完整的句子(字符串),每次一句
    """
    # 用于暂存尚未形成句子的文本
    buffer = ""
    # 句子结束符(中文常见)
    sentence_endings = re.compile(r"[。!?;]")

    for chunk in chain.stream(prompt):  # chunk 已经是字符串
        buffer += chunk
        # 查找最后一个句子结束符的位置
        while True:
            match = sentence_endings.search(buffer)
            if not match:
                break  # 没有完整的句子,继续接收下一个 chunk
            # 拿到结束符之前的内容(包含结束符,即一个完整句子)
            end_pos = match.end()
            sentence = buffer[:end_pos]
            buffer = buffer[end_pos:]  # 剩余部分继续保留
            yield sentence  # 生成一个完整的句子

    # 所有 chunk 处理完后,如果 buffer 还有剩余内容,也作为最后一句输出
    if buffer.strip():
        yield buffer


# 调用生成器,一句一句打印
print("=== 一句一句流式输出 ===")
for sentence in stream_sentences(chain, "写一首关于程序员的诗词,5句话"):
    print(sentence)

在这里插入图片描述

SSE协议

在介绍 SSE(Server-Sent Events)协议之前,需要先理解 HTTP 协议的设计。HTTP 协议本身是一种无状态的请求-响应模式,这意味着服务器无法主动向客户端推送消息。然而,通过 Server-Sent Events(服务器发送事件)技术,我们可以实现流式传输,允许服务器主动、持续地向客户端推送数据流。动向客户端推送数据流。

也就是说,服务端向客户端声明,接下来要发送的是流消息,浏览器可以通过内置的EventSoucre API来接收并处理这些实时事件。

SSE特点:

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

每一次发送的消息,由若干个message组成,每个message之间由\n\n分割,每个message内部由若干行组成,每一行都是如下格式:

[field]:value \n

field可以取值:

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

LangChain流式传输

LangChain本身并没有网络协议,而是依赖于底层大模型提供商和自身服务所使用的Web框架的协议
因此对于LangChain的流式传输能⼒,本⾝是因为⼤模型供应商提供了流式传输能⼒,由LangChain进⾏调⽤后接收并处理成⼀个个的AIMessageChunk.

具体情况我们可以阅读源码探索整个传输流程。

    def _stream(
    self,
    messages: list[BaseMessage],          # 对话消息列表
    stop: list[str] | None = None,        # 停止词列表(触发停止的 token)
    run_manager: CallbackManagerForLLMRun | None = None,  # 回调管理器,用于 on_llm_new_token 等事件
    *,
    stream_usage: bool | None = None,     # 是否在流式响应中包含 token 用量信息
    **kwargs: Any,
) -> Iterator[ChatGenerationChunk]:       # 返回值是一个生成器,逐块产出 ChatGenerationChunk
    # ------------------------------------------------------------
    # 1. 准备阶段:确保客户端可用,设置 stream 参数
    # ------------------------------------------------------------
    self._ensure_sync_client_available()  # 确认 OpenAI 客户端已创建(同步)
    kwargs["stream"] = True               # 强制开启流式模式

    # 决定是否需要在流中获取 usage 信息(如 token 消耗)
    stream_usage = self._should_stream_usage(stream_usage, **kwargs)
    if stream_usage:
        # 当需要 usage 时,在请求中加入 stream_options
        kwargs["stream_options"] = {"include_usage": stream_usage}

    # 将消息和停止词等参数整合成最终的 API 请求体
    payload = self._get_request_payload(messages, stop=stop, **kwargs)

    # 记录当前使用的消息块类型(默认为 AIMessageChunk)
    default_chunk_class: type[BaseMessageChunk] = AIMessageChunk
    # 用于存储响应的元信息(例如 HTTP 头),初始为空
    base_generation_info = {}

    try:
        # ------------------------------------------------------------
        # 2. 根据是否使用结构化输出(response_format)分流处理
        # ------------------------------------------------------------
        if "response_format" in payload:
            # ---- 分支 A:使用 JSON 等结构化输出 ----
            # 这种情况下不能同时获取响应头(官方限制)
            if self.include_response_headers:
                warnings.warn(
                    "Cannot currently include response headers when "
                    "response_format is specified."
                )
            # 移除 stream 参数(因为 beta 接口下 stream 由上下文管理器控制)
            payload.pop("stream")
            # 使用 beta 客户端发起流式请求(专门针对 JSON 模式优化)
            response_stream = self.root_client.beta.chat.completions.stream(
                **payload
            )
            context_manager = response_stream  # 后续统一用 with 打开
        else:
            # ---- 分支 B:普通文本输出 ----
            # 根据是否需要包含响应头决定请求方式
            if self.include_response_headers:
                # 需要响应头时,先拿到原始响应对象(带 headers)
                raw_response = self.client.with_raw_response.create(**payload)
                # 解析出正常的响应体(这样才能被上下文管理器识别)
                response = raw_response.parse()
                # 把 headers 存入 base_generation_info,后续会附着在第一个 chunk 上
                base_generation_info = {"headers": dict(raw_response.headers)}
            else:
                # 不需要响应头,直接调用普通 API
                response = self.client.create(**payload)
            context_manager = response  # 统一用 with 打开

        # ------------------------------------------------------------
        # 3. 使用上下文管理器迭代响应块
        # ------------------------------------------------------------
        with context_manager as response:
            is_first_chunk = True  # 标记是否为第一个有效 chunk
            for chunk in response:  # 遍历 API 返回的每一个增量数据
                # 确保 chunk 是字典格式(兼容不同 SDK 返回类型)
                if not isinstance(chunk, dict):
                    chunk = chunk.model_dump()

                # 核心转换:将原始 chunk 转成 LangChain 的 ChatGenerationChunk
                generation_chunk = self._convert_chunk_to_generation_chunk(
                    chunk,
                    default_chunk_class,
                    # 只对第一个 chunk 注入 headers 等基础信息
                    base_generation_info if is_first_chunk else {},
                )

                # 有些 chunk 可能被过滤(返回 None),跳过
                if generation_chunk is None:
                    continue

                # 更新当前使用的消息块类型(因为后续 chunk 的类型可能变化,如 AIMessageChunk -> ToolMessageChunk)
                default_chunk_class = generation_chunk.message.__class__

                # 提取 logprobs(如果存在)
                logprobs = (generation_chunk.generation_info or {}).get("logprobs")

                # ----- 触发回调(关键!这是外部能收到逐 token 推送的地方)-----
                if run_manager:
                    run_manager.on_llm_new_token(
                        generation_chunk.text,   # 当前 token 文本
                        chunk=generation_chunk,  # 完整 chunk 对象
                        logprobs=logprobs,       # 对数概率信息
                    )

                is_first_chunk = False  # 第一个 chunk 已处理
                yield generation_chunk  # 向外产出 chunk,供上层 .stream() 迭代

    except openai.BadRequestError as e:
        # 处理 400 类错误(如参数错误)
        _handle_openai_bad_request(e)
    except openai.APIError as e:
        # 处理其他 API 错误
        _handle_openai_api_error(e)

    # ------------------------------------------------------------
    # 4. JSON 模式下的额外收尾:获取最终完成对象
    # ------------------------------------------------------------
    # 注意这段代码在 try-except 之外,仅在使用了 response_format 且 response 对象支持 get_final_completion 时执行
    if hasattr(response, "get_final_completion") and "response_format" in payload:
        final_completion = response.get_final_completion()
        # 将最终补全转换为 GenerationChunk
        generation_chunk = self._get_generation_chunk_from_completion(
            final_completion
        )
        # 同样触发一次 token 回调(最终汇总或结束标记)
        if run_manager:
            run_manager.on_llm_new_token(
                generation_chunk.text, chunk=generation_chunk
            )
        yield generation_chunk
  1. 整段代码就是一个生成器,从OpenAI API拿到原始数据流,每取到一小块就把它”打成“LangChain标准格式(ChatGenerationChunk),并交给回调系统,然后对外yield
  2. 分两条路径:普通聊天用常规 API,JSON 模式用 beta 接口,但最终都要转换成统一的 chunk 对象。这样就可以以统⼀的⽅式处理来⾃不同模型提供商(OpenAI,Anthropic等)的流式响应。
  3. 回调是同步嵌入的:每生成一个 chunk,立即调用 on_llm_new_token,所以你在外面能实时收到每个 token,而不是等全部结束
  4. langchain-openai包通过集成OpenAIPythonSDK,提供了⼀个HTTP客⼾端。

完结撒花!🎉

如果这篇博客对你有帮助,不妨点个赞支持一下吧!👍
你的鼓励是我创作的最大动力~

想获取更多干货? 欢迎关注我的专栏 → optimistic_chen
📌 收藏本文,下次需要时不迷路!

我们下期再见!💫 持续更新中……


悄悄说:点击主页有更多精彩内容哦~ 😊

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Optimistic _ chen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值