news 2026/9/8 14:01:38

长文本生成速度优化:流式输出与分块生成的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
长文本生成速度优化:流式输出与分块生成的工程实践

做后端和 AI 应用的朋友,应该都遇到过这种需求:让模型写一篇足够长的文章,又不能让用户等得想砸电脑。"Make It Long, Keep It Fast" 这句话,放在工程场景里就是一道经典难题——输出变长意味着计算时间变长,响应变快又要求延迟不能突破用户的心理阈值。我自己做长文生成工具时,这两个指标经常打架,后来把问题拆开看,才发现"长"和"快"其实各有各的瓶颈,也各有各的解法。

这篇文章就围绕"Make It Long, Keep It Fast"展开,聊一聊长文本生成场景下的速度优化方案。适用人群是正在做 AI 应用、聊天机器人、报告生成器,或者任何涉及大模型长输出的后端开发。内容包含从思路拆解到代码实现,再到稳定运行和质量评估的完整路径,算是给同样踩坑的同行一份可直接参考的工程笔记。

1. 先搞清楚 "Long" 和 "Fast" 到底指什么

很多方案一开始就做偏,是因为没把目标定义清楚。长不单指字数多,快也不单指返回快,先把维度拆出来,后面选型才不会左右摇摆。

1.1 "Long" 不是单维度的长

我在项目里把"长"拆成了三层:

  • 输出长度:也就是模型生成的内容 token 数量。这是最直观的"长",通常由 max_tokens 控制。
  • 上下文长度:输入侧塞给模型的 token 数量,包括历史对话、文档资料、系统提示词。上下文越长,模型推理阶段的计算量越大。
  • 结构完整度:内容在业务语义上是否覆盖了大纲要求的全部要点。比如一篇行业分析报告,如果模型只写了背景和问题,缺了趋势预测和对策章节,哪怕字数到了 8000,它也不算"长",因为用户拿不到完整交付物。

这三层正好对应了三种不同的优化手段。输出长度可以靠分块生成、推理预算调控;上下文长度要靠摘要压缩、检索增强来降载;结构完整度则更多依赖提示词设计和大纲约束。

单追字数是最容易踩的坑。我在早期版本里让模型"写够 5000 字",结果它写出大量车轱辘话,而且后续章节明显信息密度下降。后来改成"按照 6 个章节展开,每章给出论点和数据",反而内容更扎实,总字数自然就上去了。所以"长"的正确打开方式,是先定结构再谈长度。

1.2 "Fast" 也不是单指标的快

速度这个事,外行看总耗时,内行看分段指标。一次大模型请求的时间可以拆成几个部分:

指标含义用户感知
TTFT(首字延迟)从发起请求到收到第一个 token 的时间决定用户是否觉得"响应快"
TPS(生成吞吐)每秒生成多少个 token决定后续内容刷新的顺滑度
端到端总耗时从请求开始到最后一个 token 到达的时间决定用户最终什么时候能拿到全文

这三个指标经常互相制约。TTFT 受 prefill 阶段影响最大,也就是模型处理输入 prompt 的时间;TPS 受模型大小、量化方式、推理后端影响;总耗时本质上是 TTFT 加上"生成 token 数除以 TPS"的结果。

最容易被忽视的是用户可感知速度。一次 5000 token 的生成长文请求,如果等全部生成完才一起返回,假设 TPS 是 50,用户要干等 100 秒。但如果用流式返回,用户 1.5 秒看到第一个字,接下来内容以稳定的速率滚动出现,哪怕总耗时同样是 100 秒,用户的耐心也会多很多。我实测下来,流式输出能把"用户主动中断"的比例降低一半以上。

所以"Make It Long, Keep It Fast"里的 Fast,优先优化 TTFT,其次是流式的持续输出稳定性,最后才是压 TPS。

2. 让文本更长:三条路线和取舍

要让模型输出更长,不是简单地把 max_tokens 调大就完事。调大 max_tokens 确实能放宽生成上限,但生成中途内容可能开始重复、跑偏,甚至截断在半句话上。我这里梳理了三条我在项目中实际用过的路线。

2.1 一次性生成 vs 分块生成

一次性生成长文,就是一句"帮我写一篇 3000 字的产品分析",然后把解析、排版、扩充的事全交给模型。优点是实现简单,接口调用一次就行;缺点是不受控——模型在长文本生成后段容易忘记前面的论点,出现事实前后矛盾,而且如果网络或推断服务刚好抖动,整篇内容都会丢掉。

我自己更推荐分块生成,特别是面对强结构化内容时。思路是这样的:

  1. 先让模型生成一份大纲,包含章节标题和每节的核心论点。
  2. 按章节拆成独立生成任务,每个任务只负责 500~800 字的内容。
  3. 把各章节的生成请求并发发出。
  4. 全部完成后按顺序合并成最终文本。

这样做的好处有两个:单个子任务短,生成速度快,失败成本低;各章节之间互不影响,一个章节生成失败只需要重试那一块,不需要整篇推倒重来。

下面是我在项目里用过的并行分块生成代码骨架,基于 Python 的 asyncio 和 OpenAI 兼容接口:

import asyncio from openai import AsyncOpenAI client = AsyncOpenAI() async def generate_section(title, point, previous_summary=""): prompt = f""" 请撰写文章章节,标题为「{title}」。 核心论点:{point} 上文背景:{previous_summary} 要求:内容 500-800 字,逻辑清晰,有数据或案例支撑。 """ resp = await client.chat.completions.create( model="qwen-plus", messages=[{"role": "user", "content": prompt}], temperature=0.7, max_tokens=1200, ) return resp.choices[0].message.content async def main(): outline = [ ("行业背景", "市场规模和增长驱动力"), ("核心挑战", "当前方案的主要瓶颈"), ("解决思路", "新架构如何绕过瓶颈"), ] # 并发生成所有章节 results = await asyncio.gather( *[generate_section(title, point) for title, point in outline] ) full_article = "\n\n".join(results) print(full_article) asyncio.run(main())

注意一个细节:如果各章节之间有明显的逻辑依赖,比如第二章要引用第一章的结论,就不能完全并行。这种情况建议每章请求的 prompt 里附带前面章节的摘要,或者采用"先并行生成,再统一润色衔接"的两阶段方案。两阶段方案耗时会长一点,但对文章整体一致性帮助很大。

2.2 用推理预算让模型"自己变长"

除了工程上分块,还有一个让输出变长的思路是增加模型的推理预算。现在很多新模型支持思维链或者推理模式,比如通过 reasoning_effort 参数控制模型的思考深浅。当模型被要求"先思考再回答",它会在内部生成大量推理过程,然后输出更有条理的答案。这种情况下,最终答案的长度和深度都会提升。

我测试过同一家模型的普通模式和推理模式,在同样要求"分析新能源车市场"的情况下,推理模式输出的最终报告长度要多出 40% 左右,而且内容更结构化,小标题、分论点、数据引用都更完整。

使用的时候要注意 max_tokens 的分配。推理模式下模型先消耗 token 做内部思考,再消耗 token 输出最终结果,所以 max_tokens 要适当调大,否则可能出现正确答案想好了,但输出到一半被截断的情况。部分模型还支持把思维链过程单独返回,方便你决定是暴露给用户,还是过滤掉只保留正式内容。

另外,提示词里不要只写"写长一点",这种模糊指令效果不好。我常用的说法是"请从至少三个方面展开论述,每个方面包含背景介绍、案例分析和潜在风险",把"长"具象化成可执行的要求,模型才会真正照做。

2.3 上下文扩展与记忆压缩

长文本生成的另一个瓶颈是输入上下文太大。很多人以为把整本资料都塞进 prompt,模型就能写出长篇大论,结果发现请求速度一落千丈。原因在于 Transformer 的注意力计算复杂度是 O(L²) 级别的,上下文长度翻一倍,prefill 阶段的计算量约变成原来的 4 倍。上下文长了,TTFT 飙升,用户的第一个字迟迟等不到。

所以对输入侧的长文本,工程上要做减法而不是加法。我个人用的组合方案是这样的:

  • 历史对话做滚动摘要,超过一定轮次就压缩成要点,而不是全量拼进 prompt。
  • 参考资料用 RAG 检索,只把和当前话题最相关的 2~3 段塞进上下文。
  • 超过 32k 上下文的场景,优先考虑支持长本文的模型,同时用分层摘要控制信息密度。

这三种方案可以混用。比如一个"产品周报生成器",用户上传了 50 页运营数据,我不会把 50 页全塞给模型,而是先提取关键指标表,再让模型基于指标表生成详细分析,实测 TTFT 下降了约 70%,生成的周报长度反而更稳定。

3. 保证"快":一个可落地的流式长文接口

到了具体实现环节。要让长文本接口在用户侧"看起来快",核心手段就是流式返回。这一节我会给出一个可以跑起来的 FastAPI 接口示例,并解释关键参数的设定原因。

3.1 核心思路:TTFT 拉低 + 总耗时可控

先看一组实际估算。假设目标输出 5000 token,模型生成速度是 50 TPS。非流式接口,用户提交请求后要等约 100 秒才能看到任何内容;流式接口,TTFT 按 1.5 秒算,用户 1.5 秒后就能看到第一个字,之后内容以每分钟约 3000 字的速率持续输出。区别就在这里:总耗时没有缩短,但"用户第一次看到内容的时间"从 100 秒变成了 1.5 秒。

实现流式接口通常使用 SSE(Server-Sent Events)或者 Chunked Transfer Encoding。服务端边生成边写入响应体,客户端边接收边渲染。对大多数文本生成场景,SSE 足够简单可靠。

下面是一个基于 FastAPI 和 OpenAI 异步客户端的流式接口示例:

from fastapi import FastAPI from fastapi.responses import StreamingResponse from openai import AsyncOpenAI app = FastAPI() client = AsyncOpenAI() async def generate_long_text(prompt: str): stream = await client.chat.completions.create( model="qwen-plus", messages=[{"role": "user", "content": prompt}], temperature=0.7, max_tokens=6000, stream=True, ) async for chunk in stream: if not chunk.choices: continue delta = chunk.choices[0].delta if delta and delta.content: yield delta.content @app.post("/v1/long-text") async def long_text(prompt: str): return StreamingResponse( generate_long_text(prompt), media_type="text/plain", )

这个接口做的事情很简单:收到 prompt 后发起流式请求,把模型返回的每个 token 碎片通过 StreamingResponse 转发给客户端。客户端用 fetch 或者 axios 的流式读取模式,在页面上做打字机效果就可以了。

有几个容易被忽略的点:

  • FastAPI 的 StreamingResponse 要求生成器是异步的,这样才不会阻塞事件循环。如果这里用了同步生成器,一旦高并发,服务端整体响应都会变慢。
  • 如果推理服务返回的内容里带 reasoning_content,也就是思维链,记得单独处理。可以把思维链直接丢弃,只向前端输出正式内容;也可以把思维链放进单独的字段,让前端选择是否展示。
  • 建议在响应头里加Cache-Control: no-cache,避免中间代理层把流式响应缓存起来,导致内容要等全部生成完才到用户手上。

3.2 关键参数与实现细节

max_tokens 应该怎么定?我一般按照目标字数来估算:中文场景下,1 个汉字大约对应 1~1.5 个 token,所以目标 5000 字,max_tokens 至少要给 6500 到 7500。如果是推理模式,还要再往上加 20%~30%,因为内部思考链也要占用 token。给的太少,结果会在结尾被硬生生切断,这是长文生成最常见的问题之一。

temperature 和 top_p 也要注意。长文生成最怕内容失控,temperature 建议设在 0.6~0.8 之间,太高了内容天马行空,太低了语言变得干瘪。top_p 配合 temperature 一起控制采样范围,我通常保持默认或者设到 0.9 左右,不做过深调整。

如果服务端同时支持多种输出格式,这里推荐以纯文本流输出,前端自行按段落、标题渲染。虽然 JSON 格式方便结构化,但每个 chunk 都要带 JSON 包裹,数据量会膨胀,也会增加前端的解析成本。真要加 id 或者状态信息,可以用 SSE 事件格式,把元数据放在 event 行,内容放在 data 行。

客户端读取流时也要处理断线。网络抖动时,长文本生成到一半,客户端连接可能断开,服务端继续生成的内容就白费了。我的做法是在客户端记录已经收到的文本长度,断线后带上"续写点"重新请求,让模型从断点继续生成,而不是从头再来。

4. 稳定运行:超时、并发与性能优化

流式接口做出来以后,真正的考验在稳定运行。长文本请求的耗时动辄几十秒甚至上百秒,远超普通 HTTP 请求的合理范围,网关、代理、负载均衡都会成为压死会话的最后一根稻草。

4.1 长文请求常见故障与排查思路

我在调试过程中记录了几个高频故障,这里整理成速查表:

故障现象常见原因解决方案
请求生成到一半网关返回 504网关或代理层的请求超时设得太短拉长代理超时时间,或用异步任务绕过同步 HTTP 等待
接口明明用了流式,前端还是转圈Nginx 开启了 buffering,把流式响应缓存了在 Nginx 配置中对该路由关闭 proxy_buffering
返回了内容,但首字等了很久prefill 阶段输入 prompt 太长压缩输入上下文、缩短系统提示词、用前缀缓存
流式输出断断续续,甚至中途停止模型在后段开始输出无意义内容降低 temperature,增加内容质量约束,或对输出做异常检测

关于 Nginx buffering,这里多说一句。默认情况下 Nginx 会缓冲上游响应,等拿到完整响应后再转发给客户端,这样即使上游是流式输出,用户侧也会变成一次性收到全量内容。解决方法是针对流式接口单独配置:

location /v1/long-text { proxy_pass http://backend; proxy_buffering off; proxy_cache off; proxy_read_timeout 300s; proxy_send_timeout 300s; }

proxy_buffering off是关键,它让 Nginx 边收边转,不再做全量缓冲。proxy_read_timeout也很重要,长文本接口的响应时间可能超过默认的 60 秒,不调大一样会断。

如果整个系统要承载大量长文本请求,我建议在普通同步接口之外再提供一套异步任务方案:请求进来先生成任务 ID,后台任务队列去调用模型,前端通过轮询或 WebSocket 获取进度和结果。这类方案牺牲了一点实时性,换来了极高的稳定性,特别适合报告生成、批量写作这类对实时性要求不高的业务。

4.2 并发上不去和成本控制

长文本生成对并发的影响很大。一个 1000 token 的请求可能只要 10 秒,但一个 6000 token 的请求要 60 秒,长时间占用推理服务的并发额度。我遇到过的情况是,几个用户在写长文,其他人的短请求全部排队,整体体验直线下降。

对这种问题,常见的优化手段有这么几个方向:

  • 给长文本请求和短文本请求做服务拆分,长文任务走独立的队列和模型实例,避免互相挤兑。
  • 推理服务本身做优化,比如用量化压缩模型体积、用投机采样加速 token 生成、开启前缀缓存复用公共的 prompt 部分。
  • 请求入口做合并和排队,限制同一时刻的最大长文请求数,其余请求先排队,前端显示"前面还有 2 位等待"。

成本控制更直接。长文本生成是按 token 计费的,5000 字的文本加上推理链消耗,可能要用掉 8000~10000 个 token,成本是短文本请求的几十倍。我在项目里做三件事来控制成本:

  • 第一步先让模型出大纲,确认无误后再逐节生成,避免生成完发现方向不对全量返工。
  • 对模型的输出做长度兜底,超过业务需要的部分直接截断,不无谓地继续生成。
  • 在提示词里明确要求输出格式,比如"每章 300 字以内,共 5 章",让模型在合理范围内收敛。

5. 更深一层:让"长"和"快"可量化、可度量

做工程优化最怕的就是没有度量。性能有没有变好,不能靠感觉,得靠数据。我给自己建了一套轻量级的评估流程,每次调整后都跑一遍基线,用数字说话。

5.1 快速建立基准测试

工欲善其事,必先利其器。我用一个简单的 Python 脚本收集三个指标:TTFT、TPS、总耗时。脚本逻辑不复杂,就是记录流式响应中第一个 token 到达的时间和全部 token 收完的时间,再统计 token 总数。

import time from openai import OpenAI client = OpenAI() prompt = "请撰写一篇关于智慧农业的详细分析报告,要求不少于3000字。" start = time.time() stream = client.chat.completions.create( model="qwen-plus", messages=[{"role": "user", "content": prompt}], max_tokens=5000, stream=True, ) first_token_time = None token_count = 0 for chunk in stream: if first_token_time is None: first_token_time = time.time() print(f"TTFT: {first_token_time - start:.2f}s") if chunk.choices and chunk.choices[0].delta.content: token_count += 1 total_time = time.time() - start print(f"总耗时: {total_time:.2f}s") print(f"Token数: {token_count}") print(f"TPS: {token_count / total_time:.2f}")

跑几次取平均值,能很快发现改动前后的性能变化。我自己习惯每做一次优化,都跑一组"短文本(500 token)+ 中文本(2000 token)+ 长文本(5000 token)"的基准测试,覆盖不同压力场景。

需要注意的是,单次测试的波动可能比较大,特别是公共模型服务,白天晚上性能差距明显。测试尽量固定时间段、固定模型版本、固定 prompt 模板,并在报告里标注测试环境,这样数据才有可比性。

5.2 优化优先级建议

如果只让我给出三条优化建议,顺序是这样的:

第一,先把流式输出做出来。这一步对用户可感知速度的提升最明显,改动也相对小。就算后续所有性能优化都不做,流式至少能让用户安心等着,而不是频繁刷新页面。

第二,把长文本拆成分块生成。分块后单次请求耗时下降,超时率大幅降低,失败重试的成本也降下来了。同时配合大纲生成,能让长文内容更完整可控。

第三,再考虑推理加速和成本优化。量化、投机采样、前缀缓存这些手段,部署和调优成本都不低。只有当业务流量真的到了瓶颈,或者成本账单真的刺痛的时候,才值得投入精力去做。

从另一个角度看,业务产品设计也能帮大忙。与其把一个 5000 字的生成塞给用户干等,不如设计成"先生成大纲,用户确认后逐章生成"的交互。这样用户等待每一章的时间都很短,而且方案本身就把"长"切成了"快"的切片。

最后再分享一点个人体会

我在做长文本生成的这段时间,最大的感悟是:不要试图一上来就同时优化所有指标。把"Make It Long, Keep It Fast"当成一个系统问题,拆成"内容长度、结构完整度、TTFT、TPS、总耗时"几个变量,逐个击破,比盲目调参靠谱得多。先让用户快起来,再让内容长起来,顺序反了,优化效果会大打折扣。

另外一个小技巧:在长文请求的提示词里,我会要求模型"先把文章大纲列出来,再逐节展开,每节给出核心论点、案例和过渡句"。我发现这个大前提一旦立住,长文内容的质量和生成稳定性都会上一个台阶。前端再配合 Markdown 流式渲染,用户一边看着标题和段落框架陆续出现,一边等待详细内容,体验会好很多,哪怕总耗时没有缩短,用户的耐心也已经足够。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 14:01:12

两周吃透AI大模型应用开发:从API调用到RAG与Agent实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 14:00:56

WinForm DataGridView自定义列实战:封装按钮、进度条与状态灯

简介:面向C# WinForms开发者的DataGridView自定义控件源代码包,主要解决标准DataGridView列类型无法直接嵌入时间选择控件等编辑需求的问题。代码演示了通过派生DataGridViewColumn、DataGridViewCell创建日历列,并在编辑状态下嵌套DateTimeP…

作者头像 李华
网站建设 2026/9/8 13:57:13

Apache Tomcat 8.0.47 生产实践:部署调优、安全加固与升级迁移指南

简介:Apache Tomcat 8.0.47 是一个轻量级、高性能的 Java Web 应用服务器,面向 Java 后端开发者与运维人员,用于部署和运行基于 Servlet 与 JSP 的 Web 应用。压缩包共 628 个文件,大小约 9.75MB,涵盖 HTML 静态页面、…

作者头像 李华
网站建设 2026/9/8 13:56:49

Python手写计算器:从命令行到Tkinter的表达式解析实战

计算器这个项目,可以说是 Python 入门路上绕不开的“第二块敲门砖”,第一块当然是 Hello World。我最初自学 Python 的时候,写完打印语法之后总觉得不够过瘾,想做一个能交互、能看见实际反馈的东西,于是就盯上了计算器…

作者头像 李华
网站建设 2026/9/8 13:55:28

机器视觉工控机为何需要GPU?从原理到选型实战

1. 机器视觉到底在算什么:先搞清楚工控机的活有多重一个很常见的场景:产线上装好了工业相机,软件也调通了,图像能实时显示在屏幕上。但等到真正跑检测程序的时候,工控机卡成幻灯片,帧率掉到个位数&#xff…

作者头像 李华
网站建设 2026/9/8 13:55:24

MCU芯片深度解读:从选型、启动流程到量产避坑指南

芯片赛道解读(2)MCU芯片 进嵌入式的圈子久了,你会发现自己慢慢分不清“芯片”到底是哪颗芯片——手机里跑的SoC、路由器里的交换芯片、充电器里那颗小小的控制IC,名字都叫芯片,干的活却天差地别。这次聊的MCU&#xf…

作者头像 李华