当前位置:首页 > 文章列表 > 科技周边 > 人工智能 > Transformers TextIteratorStreamer 怎样流式读取生成结果

Transformers TextIteratorStreamer 怎样流式读取生成结果

来源:17golang原创 2026-10-15 02:05:56 0浏览 收藏

我第一次把本地模型接到聊天接口时,最明显的不适应不是模型调用本身,而是 model.generate() 默认要等整段结果准备好。界面只能在最后一次性拿到文本,用户感受到的就是“没有响应”。Transformers 提供的 TextIteratorStreamer 把这个问题拆成了两个角色:生成线程负责调用模型,消费端负责迭代已经准备好的文本片段。

官方地址:https://huggingface.co/docs/transformers/internal/generation_utils

最小可用结构是:创建 TextIteratorStreamer,把它作为 streamer 传给 model.generate,再让独立线程运行生成,主线程通过 for new_text in streamer 持续取得文本。

先理解 TextIteratorStreamer 的职责

它不是一个更快的模型,也不是网络传输协议,而是一个把可读文本放入队列、同时实现迭代接口的中间对象。生成侧调用 streamer 的写入方法,读取侧从 streamer 的迭代器中取出已经整理好的文本片段;两边不需要共享一个不断变化的字符串。

这个职责划分很重要:model.generate 仍然是耗时的生成动作,TextIteratorStreamer 只负责把生成结果交给下游。Hugging Face 官方文档给出的典型组合也是 tokenizer、model、streamer、Thread 和一个消费循环。

tokenizer、模型生成、TextIteratorStreamer、输出队列和消费迭代器的静态关系图
图1:线程、streamer 与队列之间的静态关系示意图,不是运行截图或执行证据。

把 generate 放进后台线程

实际接入时,先准备好 tokenizer 和模型输入,再创建 streamer。关键点是把 streamer 放进同一组 generation_kwargs,随后在线程里调用 model.generate,当前消费侧才有机会同时读取文本。

from threading import Thread
from transformers import TextIteratorStreamer

# tokenizer、model 和 inputs 假设已经由上游准备好
streamer = TextIteratorStreamer(
    tokenizer,
    skip_prompt=True,
    timeout=30.0,
)

# 将 streamer 放进 generate 的参数,生成线程会把文本片段交给它
generation_kwargs = dict(
    inputs,
    streamer=streamer,
    max_new_tokens=128,
    do_sample=False,
)

# 生成放到后台线程,当前线程专门消费可迭代文本
thread = Thread(target=model.generate, kwargs=generation_kwargs)
thread.start()

chunks = []
for new_text in streamer:
    # 每次只处理当前片段,避免等待完整结果才刷新下游
    chunks.append(new_text)

thread.join()
answer = "".join(chunks)

这里的 for 循环不是在轮询模型状态,而是在迭代 streamer 暴露出来的文本。生成线程结束后,streamer 会放入结束信号,消费循环自然结束。thread.join() 则让调用方在离开函数前确认后台线程已经收尾。

控制提示词与解码结果

如果把 skip_prompt 保持默认值,消费端可能同时拿到提示词和新生成的文本。聊天接口通常只希望返回新内容,因此可以创建 streamer 时显式设置 skip_prompt=True。这不是删掉模型输入,而是控制输出侧是否跳过输入提示部分。

解码相关选项可以通过额外关键字参数传入 streamer。不要把它和 generate 的采样参数混在一起:max_new_tokens、do_sample 属于生成调用;跳过提示词、跳过特殊 token 等则属于文本解码边界。


# 只把新生成的文本交给消费端,避免重复显示用户提示
streamer = TextIteratorStreamer(
    tokenizer,
    skip_prompt=True,
    skip_special_tokens=True,
    timeout=30.0,
)

generation_kwargs = {
    **inputs,
    "streamer": streamer,
    # 这是生成上限,不是 streamer 的队列超时时间
    "max_new_tokens": 128,
}

thread = Thread(target=model.generate, kwargs=generation_kwargs)
thread.start()
for piece in streamer:
    # 下游可以把 piece 写入响应、拼接到缓冲区或交给 UI
    print(piece, end="", flush=True)
thread.join()

中文或子词模型经常不是每生成一个 token 就立即形成一个完整词,因此消费端应把片段当作增量文本,不要假设每个片段都能独立解析成完整句子。

设置 timeout 并处理异常

默认情况下,streamer 在队列没有新文本时可能持续等待。如果生成线程因为模型、输入或设备错误提前退出,而消费端又没有超时边界,就很容易表现成“接口一直卡着”。构造 TextIteratorStreamer 时设置 timeout,可以让队列等待具备上限。

线程中的异常不会自动原样抛回当前消费循环,所以生产代码应当在线程目标函数里捕获异常,并把异常保存到一个线程安全可读的位置。下面的例子不把异常伪装成普通生成文本,而是在消费结束后明确抛出。

from threading import Thread
from transformers import TextIteratorStreamer

streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, timeout=30.0)
worker_error = []

def run_generation():
    try:
        # 生成失败时由这里捕获,避免消费端只看到空流
        model.generate(
            **inputs,
            streamer=streamer,
            max_new_tokens=128,
        )
    except Exception as exc:
        # 只记录异常对象,交给外层决定响应格式和日志策略
        worker_error.append(exc)

thread = Thread(target=run_generation)
thread.start()
pieces = []
try:
    for piece in streamer:
        # 超时或结束信号应当被当作控制边界处理
        pieces.append(piece)
finally:
    # 无论消费是否正常结束,都等待生成线程离开
    thread.join()

if worker_error:
    # 让调用方看到真实生成错误,而不是误以为得到了空答案
    raise worker_error[0]

answer = "".join(pieces)

这段代码的边界是“记录并上抛”异常。实际服务还可以把异常转换成结构化错误、取消响应或重试信号,但不要在消费端静默吞掉它。timeout 也不是生成总时长,它控制的是 streamer 等待文本片段的队列等待边界。

TextIteratorStreamer 的 tokenizer、skip_prompt、timeout、生成线程和消费循环关系图
图2:提示词、超时与线程消费边界的静态配置示意图,不是运行截图或执行证据。

选择同步迭代还是异步扩展

当应用本身就是普通同步函数时,TextIteratorStreamer 加 Thread 足够直观:后台线程负责生成,当前线程直接用 for 循环拿结果。它的优势是改动小、职责清晰,也容易在最后调用 join 收尾。

如果消费端是原生异步应用,可以了解 AsyncTextIteratorStreamer。官方文档将它设计为异步迭代器,并提示应在协程内部初始化。不要为了“看起来更现代”而把同步线程方案和异步方案混写;先按调用栈选择一种边界。

import asyncio
from threading import Thread
from transformers import AsyncTextIteratorStreamer

async def read_stream():
    # 异步 streamer 在协程内部创建,保持事件循环边界清晰
    streamer = AsyncTextIteratorStreamer(tokenizer, skip_prompt=True)
    kwargs = {**inputs, "streamer": streamer, "max_new_tokens": 128}

    # 模型生成仍放在线程中,避免阻塞事件循环
    thread = Thread(target=model.generate, kwargs=kwargs)
    thread.start()

    result = []
    async for piece in streamer:
        # 异步消费端可以把片段交给 SSE 或 WebSocket 层
        result.append(piece)

    thread.join()
    return "".join(result)

# 由应用入口负责启动协程
# asyncio.run(read_stream())

同步和异步 streamer 的共同点是“生成端持续写、消费端持续读”;差别只在消费接口和事件循环边界。若只是要在同步请求里逐段返回文本,第一种写法通常更容易维护。

常见问题

TextIteratorStreamer 为什么要配合线程?

因为 model.generate 本身会占用当前调用路径。独立线程让消费端可以同时迭代 streamer;streamer 只是传递生成片段,不会自动把阻塞生成变成异步函数。

timeout 设置得越小越好吗?

不是。它是队列等待边界,过小会把正常的长推理间隔误判为超时。应结合模型首 token 延迟、设备负载和上层接口的取消策略设置。

为什么每次 piece 不是一个完整句子?

streamer 依据 tokenizer 的解码结果提供可读片段,token 边界和自然语言句子边界不同。下游应按增量文本拼接,若要按句展示,需要另加自己的缓冲和分隔判断。

生成线程报错时为什么消费端没有看到异常?

线程异常不会自动穿透到另一个线程。在线程目标函数中捕获并保存异常,消费结束后检查错误集合,才能让调用方获得真实失败原因。

小结

TextIteratorStreamer 的核心不是改变生成算法,而是把“生成”和“读取”拆开。准备好 tokenizer 与 inputs 后,把 streamer 传入 model.generate,用独立线程运行生成,再通过迭代器逐段消费;需要控制输出边界时使用 skip_prompt,需要故障边界时设置 timeout 并显式传递线程异常。同步应用从线程版开始,已经处于异步调用栈时再选择 AsyncTextIteratorStreamer。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Transformers 怎样按模型 chat template 组织消息Transformers 怎样按模型 chat template 组织消息
上一篇
Transformers 怎样按模型 chat template 组织消息
Transformers 生成文本怎样理解 past_key_values 缓存
下一篇
Transformers 生成文本怎样理解 past_key_values 缓存
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    474次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    533次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    552次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    506次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    324次使用