什么时候用流式输出,什么时候必须等模型完整返回?
简化版
看输出是给人读的,还是给代码用的。给人读的长文本(分析、建议、改写、聊天回答)适合流式:前面的内容本身就能用,边生成边显示能把用户对着空白页面的等待从十几秒压到一秒以内。下面几种情况要等模型完整返回:输出要被程序解析(JSON、分数、工具参数),半截内容没法解析;结果要先校验或核验才能展示;生成过程中答案可能被推翻重来(Agent 循环、生成后再校验),逐字发出去的内容撤不回;后台批处理,没有人在看。流式只改善首字等待时间,不会减少生成的计算量和总耗时;需要让用户知道进度又不能流式出正文时,可以流式推送阶段事件,正文整段发。
详细版
| 场景 | 选择 | 原因 |
|---|---|---|
| 聊天回答、分析报告、改写正文 | 流式 | 部分内容可读,首字快,用户能提前判断方向 |
| 评分、分类、抽取等 JSON 输出 | 同步 | 要拿到完整 JSON 才能解析,半截没用 |
| 生成后要校验、核验、过滤再展示 | 同步,或流式配合增量过滤 | 已经发出去的内容撤不回 |
| Agent 循环、生成后回炉重写 | 正文整段发,过程用状态事件 | 中途结果可能被推翻 |
| 后台任务、批量生成 | 同步 | 没人在看,流式只增加复杂度 |
选了流式,就要一起处理几件同步调用不用操心的事:截断只能在最后一帧判断;前端要自己拆事件、处理分片;中途失败要显式发错误事件;什么时候落库、用户停止后怎么办要事先定好。
完整版教学
一、两种调用在协议上差在哪
同步调用发一次请求、收一个完整响应,正文在 choices[0].message.content 里。流式调用在请求里加上 stream: true,服务商改为用 SSE 一帧一帧地推:
data: {"choices":[{"delta":{"content":"这份"}}]}
data: {"choices":[{"delta":{"content":"简历"}}]}
data: {"choices":[{"delta":{"content":"的问题"}}]}
data: {"choices":[{"delta":{},"finish_reason":"stop"}]}
data: [DONE]
每一帧只带一小段增量文本,结束原因、Token 用量这些元数据要到最后几帧才出现。所以「等全部到齐再处理」和「来一段处理一段」是两种完全不同的代码结构,不只是换一个参数。
二、流式改善的是等待感,不是速度
用一组数字对比。假设一次回答输出 800 个 token,模型生成速度每秒 40 个 token,首个 token 在 0.8 秒后到达:
同步:用户在 0.8 + 800 / 40 = 20.8 秒后一次看到全部内容
流式:用户在 0.8 秒后看到第一段,之后内容持续出现,20.8 秒后结束
两种方式模型干的活一样多,总耗时也差不多。区别在于流式让用户在第 1 秒就知道系统在工作、回答的方向对不对,不对可以提前停下来换个问法。回答越长,这个差别越明显;只输出十几个字的分类结果,流式几乎没有体验收益。
记忆钩子:流式省的是用户的等待感,不省模型的计算量。
三、必须等完整结果的四种情况
输出要被程序解析。 评分要从回答里取出分数和等级,分类要取出类别和优先级。收到一半的 JSON 解析不了,边收边显示也没有意义,因为用户要看的是解析后的卡片,不是一串花括号。
结果要先检查再展示。 医疗、金融这类场景,模型给出的结论要先经过代码核验,比如推荐的科室是否存在、金额是否超出范围。核验之前把内容推给用户,等于绕过了核验。
生成过程可能被推翻。 带自我校验的链路里,生成完的答案可能被判为「没有资料支撑」而重新生成;Agent 循环里,中间某一步的想法可能被后面的观察结果否定。已经逐字推给用户的内容无法撤回,用户会看到一个答案被换成另一个。
没有人在看。 夜间批量生成报告、导入时批量抽取字段,结果直接写库。这时流式只是多了一层事件处理,没有任何收益。
四、选了流式要一起解决的问题
| 问题 | 同步调用 | 流式调用 |
|---|---|---|
| 输出被截断 | 拿到响应就能看结束原因 | 结束原因在最后一帧,前面的内容已经显示了 |
| 内容过滤 | 整段过滤后再返回 | 要边收边判,已发出的部分收不回 |
| 失败通知 | 接口直接返回错误 | 流中途断开,要显式推一个错误事件 |
| 落库时机 | 拿到结果后写库 | 要决定边收边写还是结束后整段写 |
| 前端解析 | 普通请求 | 自己拆事件、处理网络分片和中文字节被切开 |
这些问题都有成熟解法,但每一项都是额外的代码和测试。输出不是给人实时阅读的,就没必要承担这些成本。
前端怎么读流、怎么取消和设超时,见「大模型应用为什么要做流式输出、取消和超时控制?」;落库时机和四种中断的收尾,见「流式对话的消息什么时候落库?中途失败、被截断或用户停止时怎么处理?」。
五、折中:过程流式,结果整段
有些场景两头都要:Agent 执行要十几秒,用户需要知道它在干什么,但最终答案又必须在校验完才能给出。做法是把「过程」和「结果」拆成两类事件:
event: status data: {"step": "检索资料", "count": 1}
event: status data: {"step": "判断相关性"}
event: status data: {"step": "检索资料", "count": 2}
event: answer data: {"text": "……完整答案……", "retrieve_count": 2}
event: done
状态事件让等待有了解释,正文仍然是校验通过后才整段发出。状态事件里只放步骤名和计数这类受控信息,不要把模型的中间推理原样推给用户。
六、一张判断流程
输出给谁用?
├─ 给代码(解析、入库、调工具) ─────────────▶ 同步
└─ 给人读
├─ 展示前必须校验 / 过滤?
│ ├─ 能按行增量过滤 ──────────────────▶ 流式 + 增量过滤
│ └─ 只能整段判断 ────────────────────▶ 同步,或过程流式、结果整段
├─ 答案可能被推翻重来? ────────────────▶ 过程流式、结果整段
└─ 以上都不是,而且输出较长 ───────────▶ 流式
同一个应用里两种方式并存很正常。把模型调用封装成同步和流式两个出口,共用同一套配置、报错翻译和截断检查,各功能按上面的规则选。
七、常见误区与追问
- 误区:流式能让回答更快生成完。 模型生成的 token 数一样,总耗时基本不变,改善的是首字等待。
- 误区:所有 AI 功能都应该流式。 结构化输出、需要核验的结论、后台任务流式不但没收益,还多出截断、失败通知、落库一系列问题。
- 误区:JSON 也可以边收边解析给用户看。 半截 JSON 解析不了;即使用增量解析器,缺字段的中间状态也不该当成结果展示。
- 误区:Agent 答案流式输出更流畅。 答案可能被后续步骤推翻,已推给用户的内容撤不回,应该推送过程状态、整段给出答案。
- 误区:流式时截断检测照同步的写法就行。 结束原因只在最后一帧,要一路记着最后一帧,流结束后再判断。
- 追问:一个应用里怎样同时支持两种方式? 模型调用封装成同步、流式两个出口,共用配置、报错翻译和截断检查,业务按场景选择。
- 追问:流式时用户中途停止,已经生成的部分怎么处理? 前端停止读取,这一轮的回答通常不落库;要同时考虑把取消传到后端和模型调用。
八、加强记忆
流式还是同步,先问输出给谁用。给代码用的(JSON、分数、工具参数、批量任务)等完整结果;给人读的长文本流式输出,首字在一秒内出现。要先核验再展示、或者答案可能被推翻的,推送过程状态,正文整段给出。选了流式,就要一并处理最后一帧才出现的结束原因、中途失败的错误事件、落库时机和前端分片解析。
项目实战落地
项目里怎么做的
《AI智能简历助手》的工作台有五个 AI 功能,按输出怎么用分成两条链路:
| 功能 | 调用方式 | 输出怎么用 |
|---|---|---|
| 诊断评分 | ChatClient 的 call(),等完整响应 | 解析出 score、level、analysis 三个字段 |
| 问题分析、优化建议、简历改写、模拟面试题 | stream(),边生成边推送 | 长段纯文本,直接显示 |
四个流式功能共用一个 generateTextStream 方法,只是传入的记录类型和 Prompt 构造方法不同。这条链路上有两层 SSE:模型到后端是服务商的 delta 帧,由 Spring AI 解析成 Flux<ChatResponse>;后端到浏览器是项目自定义的 message、finish、error 三种事件。问题分析的 Prompt 末尾还写明「请直接返回分析文本,不要返回 JSON」。
《AI Agentic RAG高级企业知识库平台》的问答应用也分两种:走直线链路时检索一次、流式生成;打开 Agentic 模式后跑状态图,不做逐字流式,跑完整段发出,另发一个事件告诉前端这次跑了几步、检索了几次、改写了几次。
为什么这样取舍
- 评分必须等全。 后端要拿到完整 JSON 才能解析出分数和等级,残缺的 JSON 没法解析;边生成边显示,很难稳定解析评分字段。
- 分析类文本适合流式。 问题分析输出的是长段纯文本,没有「不完整就不能用」的问题,整段等完反而要让用户对着空白页面等很久。
- Agentic 链路不逐字发。 状态图跑到一半时答案可能被推翻重来,生成完被判无支撑就要重新生成,逐字流式发出去的内容没法撤回;多发的那个事件让用户在等得更久的时候,能看到 Agent 的工作量。
面试官还会追问
- 流式调用为什么放在异步线程里执行,而取模型输出时又用阻塞的方式逐段取?
- 同步评分和流式生成两条链路的超时各设多少?为什么取同一个值?
学完《AI智能简历助手》,上面这些追问你都会迎刃而解。