
做后端和 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 字的产品分析然后把解析、排版、扩充的事全交给模型。优点是实现简单接口调用一次就行缺点是不受控——模型在长文本生成后段容易忘记前面的论点出现事实前后矛盾而且如果网络或推断服务刚好抖动整篇内容都会丢掉。我自己更推荐分块生成特别是面对强结构化内容时。思路是这样的先让模型生成一份大纲包含章节标题和每节的核心论点。按章节拆成独立生成任务每个任务只负责 500~800 字的内容。把各章节的生成请求并发发出。全部完成后按顺序合并成最终文本。这样做的好处有两个单个子任务短生成速度快失败成本低各章节之间互不影响一个章节生成失败只需要重试那一块不需要整篇推倒重来。下面是我在项目里用过的并行分块生成代码骨架基于 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( modelqwen-plus, messages[{role: user, content: prompt}], temperature0.7, max_tokens1200, ) 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 秒。实现流式接口通常使用 SSEServer-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( modelqwen-plus, messages[{role: user, content: prompt}], temperature0.7, max_tokens6000, streamTrue, ) 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_typetext/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( modelqwen-plus, messages[{role: user, content: prompt}], max_tokens5000, streamTrue, ) first_token_time None token_count 0 for chunk in stream: if first_token_time is None: first_token_time time.time() print(fTTFT: {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(fToken数: {token_count}) print(fTPS: {token_count / total_time:.2f})跑几次取平均值能很快发现改动前后的性能变化。我自己习惯每做一次优化都跑一组短文本500 token 中文本2000 token 长文本5000 token的基准测试覆盖不同压力场景。需要注意的是单次测试的波动可能比较大特别是公共模型服务白天晚上性能差距明显。测试尽量固定时间段、固定模型版本、固定 prompt 模板并在报告里标注测试环境这样数据才有可比性。5.2 优化优先级建议如果只让我给出三条优化建议顺序是这样的第一先把流式输出做出来。这一步对用户可感知速度的提升最明显改动也相对小。就算后续所有性能优化都不做流式至少能让用户安心等着而不是频繁刷新页面。第二把长文本拆成分块生成。分块后单次请求耗时下降超时率大幅降低失败重试的成本也降下来了。同时配合大纲生成能让长文内容更完整可控。第三再考虑推理加速和成本优化。量化、投机采样、前缀缓存这些手段部署和调优成本都不低。只有当业务流量真的到了瓶颈或者成本账单真的刺痛的时候才值得投入精力去做。从另一个角度看业务产品设计也能帮大忙。与其把一个 5000 字的生成塞给用户干等不如设计成先生成大纲用户确认后逐章生成的交互。这样用户等待每一章的时间都很短而且方案本身就把长切成了快的切片。最后再分享一点个人体会我在做长文本生成的这段时间最大的感悟是不要试图一上来就同时优化所有指标。把Make It Long, Keep It Fast当成一个系统问题拆成内容长度、结构完整度、TTFT、TPS、总耗时几个变量逐个击破比盲目调参靠谱得多。先让用户快起来再让内容长起来顺序反了优化效果会大打折扣。另外一个小技巧在长文请求的提示词里我会要求模型先把文章大纲列出来再逐节展开每节给出核心论点、案例和过渡句。我发现这个大前提一旦立住长文内容的质量和生成稳定性都会上一个台阶。前端再配合 Markdown 流式渲染用户一边看着标题和段落框架陆续出现一边等待详细内容体验会好很多哪怕总耗时没有缩短用户的耐心也已经足够。