LLM 面试备考
首页/Agent 基础设施

流式输出与增量执行(Streaming & Incremental Execution)

所属:01-fundamentals / 06-agent-infra
面试权重:★★
来源:《深入理解 AI Agent:设计原理与工程实践》第 2 章「输出的顺序结构与流式响应」、第 5 章「并行工具调用、流式执行与级联中止」「半截输出的续写恢复」「空闲看门狗」、第 6 章「取消式处理与安全点」、第 10 章「A2A 的任务状态机与流式进度更新」(李博杰,github.com/bojieli/ai-agent-book);传输方式取舍(SSE / WebSocket / HTTP chunked / gRPC streaming)与前端重连那两节书里没有,据 System Design 101 等公开资料,未做一手考证。非一手实测
相关知识点:./latency-cost.md(口径来源:TTFT / TPOT 的定义,「流式不改变端到端时长、只改变体感」这一句以那篇为准)、../04-agent-architecture/09-coding-agent.md(流式执行工具调用、并行调用的级联中止、空闲看门狗的原始出处)、../04-agent-architecture/11-async-event-driven.md(取消式安全点:模型侧取消流式响应、工具侧发取消信号;占位符不能伪造成功)、../04-agent-architecture/14-agent-state.md(重放 vs 续传的前提:轨迹与状态必须能落盘)、./observability.md(Agent 事件流就是 trace 的在线形态)

一句话#

流式是把「一次输出」拆成「一串增量事件」分发出去的工程手段。它不改「一共要生成多少 token」,因此不改变端到端时长;它改的是首字出现的时间(TTFT 体感),以及在 Agent 场景下真正值钱的那件事 —— 第一个工具调用的参数一完整就开跑,执行与后续生成重叠。

为什么重要(面试角度)#

  • 这是最容易被答成一句空话的题。 「流式能提升体验」谁都会说;能分清哪一层流式改变了时长、哪一层只改变了体感的人立刻分层 —— 而这两层在同一道题里同时存在(token 流 vs 工具调用参数流),是最有区分度的一处。
  • 它天然连着成本与延迟的账。 说清「流式不省 token、长输出仍是成本与延迟主项」,就把 ./latency-cost.md 那一套接上了。
  • 流式的失败模式很反直觉。 最危险的不是断开(会报错),而是静默卡死 —— 连接建好了、数据流停了,而 SDK 超时往往只覆盖初始连接。答得出「空闲看门狗」是做过系统的信号。
  • 它把三篇缝起来:取消对齐 ../04-agent-architecture/11-async-event-driven.md 的安全点;「重放 vs 续传」的前提落在 ../04-agent-architecture/14-agent-state.md;事件流就是 ./observability.md 的 trace。
  • 和你的项目接得上:QueWI 网关做的是模型调用层的超时 / 重试 / 熔断(上游那一侧);这一篇是流式消费侧与工具侧的设计。

⚠️ 边界声明:本篇是书里的机制梳理加公开资料(传输协议那部分)的归纳,不是你的生产经验。你没有运营过长时间流式连接的网关、没做过前端的断线重连与去重。面试用「这套我会这么设计」的口径讲,可讲成一手经验的只有 QueWI 的上游超时与重试那一小块;不要说成「我们线上流式就是这么扛的」。

核心内容#

1. 先纠一个最常错的前提:流式改的是体感,不是时长#

端到端时长的构成(口径见 ./latency-cost.md):

E2E=TTFT+TPOT×(N−1)+网络与网关开销\text{E2E} = \text{TTFT} + \text{TPOT}\times(N-1) + \text{网络与网关开销}

流式做的是:首 token 一到就渲染,用户感知到的「开始等待」从 E2E 缩到约等于 TTFT。但生成仍然要 (N) 个 token,TPOT 一点没变,总时长一点没变,总 token 数一点没变。

不流式 流式
用户看到第一个字 约等于 E2E(等全部生成完才显示) 约等于 TTFT
端到端完成时间 E2E E2E(不变)
token 用量与成本 不变 不变
感知到的「快」 慢 快

所以「流式降低了延迟」是错误说法(./latency-cost.md 的易错点里已经列了这一条)。准确说法是:流式把「等 8 秒」变成「立刻有字」,它优化的是体感与可打断性(用户看到方向不对能提前叫停),不是时延本身。

什么时候不该流式(这是加分项,多数人只答「应该流式」):

  • 非交互场景:批处理、后台任务、异步跑完再通知 —— 没有人在盯着看,流式只增加复杂度。
  • 输出要整体校验后才交付:例如要先生成完整 SQL / 配置 / 代码,校验通过才执行或展示(Artifact 模式,见 ../04-agent-architecture/09-coding-agent.md)—— 半截产物直接给用户看反而是负担。
  • 下游是程序不是人:程序消费更希望拿到一个完整、可校验的结构,而不是自己拼半截 JSON。

2. 传输方式:四种,按「单向还是双向」先分一次#

方式 方向 走什么 谁负责重连 适配什么
SSE(Server-Sent Events) 单向,服务端→客户端 普通 HTTP,文本协议;浏览器有原生 EventSource 浏览器原生支持(可带 Last-Event-ID 续读) 「一次请求 → 一串输出」的纯流式:聊天 token 流、任务进度流。最省事的默认选择
HTTP chunked(流式 JSON / NDJSON / JSONL) 单向 普通 HTTP 响应分块发送 自己做 需要自定义帧边界与载荷格式时;很多 SDK 的 stream 就是这一层,每行一个 JSON 事件
WebSocket 双向全双工 先 HTTP 升级握手,再走独立协议 自己做(心跳 + 重连 + 序号续读) 需要用户中途插话的交互:语音、协同编辑、回合中途引导(见 ../04-agent-architecture/11-async-event-driven.md)
gRPC streaming 单向或双向(四种形态) HTTP/2 + 二进制 + 强类型 IDL 视实现 内部服务之间:高吞吐、低开销、要 schema 约束。浏览器侧要用 gRPC-Web 且受代理限制

判据(能一口气说清这四条,这题就稳了):① 数据是单向还是双向;② 客户端是浏览器还是内部服务;③ 链路中间有没有老旧的代理 / CDN(对 SSE 的缓冲不友好);④ 消息边界谁定、重连谁负责。SSE 把「重连」白送给你,WebSocket 把「双向」白送给你 —— 换的是复杂度。

⚠️ 两个公开资料里的坑,点到即可、别当定论:SSE 在 HTTP/1.1 下受每域名并发连接数限制(HTTP/2 多路复用缓解;具体数值以各浏览器实现为准),以及中间代理可能缓冲响应导致「流式变批量」。

3. 流式的三层:各有各的消费者,别混为一谈#

层 内容 消费者 用来做什么
① token 流 思考段(<think> 内)→ 正文文本 → 工具调用请求,按固定顺序产出(第 2 章) 用户 / UI 见 <think> 就切「思考中」状态;正文增量渲染
② 工具调用参数流 工具调用的参数是分片产出的;同一轮可能连续产出多个调用 运行时(Agent 框架) 流式执行:参数一完整、校验通过就开跑(见 §4)
③ Agent 事件流 进度、状态、子步骤、子 Agent 状态、审批请求 前端 / 上层编排器 进度条、状态栏、多 Agent 面板;A2A 的「流式进度更新」就是这一层(第 10 章)

关键判断:第 ② 层不是给用户看的,第 ③ 层不是模型的原生输出 —— 它是运行时把 token 流、工具事件与状态变更统一建模出来的事件流(../04-agent-architecture/09-coding-agent.md 的「Agent 状态栏」、AG-UI 这类「事件 / 传输协议」都属这一层)。把三层混在一起,会出现「拿 token 流当进度条」(进度根本不在 token 里)和「把工具参数推进给用户看」(用户看不懂也不想看)这两类典型错误。

4. 流式工具调用:这才是 Agent 里真正的延迟收益#

书里的原话(第 2 章、第 5 章):第一个工具调用的参数一经完整生成并通过校验,即可立即开始执行,无需等待模型生成后续的工具调用。

一次推理要连出「搜代码 → 查配置 → 读日志」三个调用时,第一个参数一完整就能启动,与后两个的生成过程重叠;彼此独立的调用还可并行执行。这与 ./latency-cost.md 的结论不冲突,因为它压的不是同一项:

改动 压的是什么 端到端时长
token 流式渲染 用户感知的等待 不变
流式执行工具调用 工具执行与后续生成的重叠 真的变短

这是本仓库里最容易把两个「流式」讲混的地方 —— 一个只改体感,一个真的改时长,答题时务必分开说。

前提与坑(缺一条就不能提前执行):

  • 参数完整且通过校验。 校验有两层(../04-agent-architecture/tool-use.md):schema 合法(结构闭合、类型正确)与业务合法(值在允许域内)。schema 合法不等于业务合法。
  • 调用之间没有依赖。 有依赖被并行发出,参数要么是模型瞎猜的、要么直接报错。
  • 副作用型调用要谨慎。 默认不允许并发(失败安全);../04-agent-architecture/tool-use.md 还提醒:有副作用的工具(下单、发信、写库)应显式禁用并行或强制走审批。
  • 失败要级联中止,但只波及同批。 某个调用失败时终止同一批并行启动且依赖它的调用,不波及独立调用和父级操作 —— 这就是「故障只在同一批并行调用内传播」。
  • 去重。 ⚠️ 若半截输出里已有工具因流式提前执行,那么续写 / 重试之前必须按调用指纹去重,否则同一个副作用会做两遍(第 5 章的续写实验专门验这一项)。这条直接接上 ../04-agent-architecture/14-agent-state.md 的幂等要求。

5. 流式与结构化输出的张力:半截 JSON 怎么处理#

这是流式落地时最具体的工程问题。模型输出的参数是 JSON(或结构化字段),而 JSON 是整体结构 —— 增量到达时天然是半截的:{"city": "Van 不是合法 JSON。

三个层次要分开:

  • ① 解析器必须容忍不完整输入。 不能一遇到截断就抛异常,要能区分「暂时不完整」与「确实畸形」。工程做法是边收边尝试解析、或用能报告「目前解析到哪」的流式解析器;解析失败不等于参数错,也可能只是还没收完。
  • ② 「参数完整」的判据是「结构闭合 + 校验通过」,不是「字符串结束了」。 结构闭合只说明 JSON 对象配平了,对象闭合 ≠ 语义完整(必填字段可能缺失、值可能超域)。所以触发执行的条件应当是配平 + schema 校验 + 业务校验全部通过。
  • ③ 受约束解码(结构化输出)管的是采样,不是让你提前知道哪里结束。 它保证「每个 token 都落在合法集合里」,从而让中途不会出现语法上根本不可能的字符 —— 但这只能保证最终输出合法,不保证中途每一段都合法,也不替你判断「语义上够不够了」。所以解析器仍然必须容忍不完整,不能因为开了结构化输出就把解析逻辑写脆。

一个实战提醒:拼接处容易多出空白或重复字符,书里的续写实验明确说「合法不等于正确」—— 补完后参数的结构合法率与语义正确率要分开统计。

6. 流式下的失败模式:最危险的是静默卡死#

失败 表现 危害 应对
断开 连接报错 低 —— 会立即报错 重连 + 重放(见 §8)
静默卡死 连接建立成功,但数据流停了(像水管通着但不出水) 最高 —— 看起来一切正常,实际永不推进 空闲看门狗(见下)
流中途失败 HTTP 200 已经发出去了,后面的失败没法用状态码表达 中 —— 前端要能解析流内错误事件 在事件协议里定义 error 事件;别指望状态码
背压 生产快、消费慢 中 —— 缓冲膨胀、内存涨 消费者侧做背压 / 丢弃策略(进度类事件可丢,状态类不可丢)

空闲看门狗(idle watchdog)是本节最值钱的一条(出处:../04-agent-architecture/09-coding-agent.md,第 5 章):

流式连接最危险的失败模式不是断开(这会立即报错),而是静默卡死 —— 连接建立成功但数据流停止。而 SDK 的超时机制往往只覆盖初始连接,而非传输过程。 因此生产级 Agent 需要独立的看门狗:超过设定时间没有新输出就判定卡死,主动杀死挂起的流并触发重试。

推广成一条原则:每个长连接都需要活性信号,而非仅依赖连接超时。

⚠️ 看门狗不能只看「有没有字节」。 一个正在做长思考或长工具的 Agent 可以合法地几十秒不吐 token。判活的信号应当来自应用层心跳(运行时主动发「还活着,正在执行第 N 步」这类事件),而不是底层 TCP 有没有数据 —— 否则看门狗会把正常的长任务误杀。

7. 取消与中断:按停止之后,模型侧和工具侧各做什么#

对齐 ../04-agent-architecture/11-async-event-driven.md 的取消式处理(它的本质是为紧急事件提前制造一个安全点)。四件事要分开:

侧 要做什么 不能假设
模型侧 取消流式响应(停止 decode) 已生成、已计费的 token 不会退还 —— 取消省的是「后面的」,不是「已花的」
工具侧 给正在执行的同步工具发取消信号;工具在安全点响应(类似 Go 里查 ctx.Done()) 接收一条「停止」消息本身不会撤销已经发生的动作
轨迹侧 未完成的工具结果用显式占位符表示(「工具正在后台执行,请优先处理新事件」) 不能伪造成功;同时防住占位符的语义风险 —— 模型可能把「已启动」误当「已完成」
副作用侧 已发生的副作用(发了邮件、下了单)显式补偿或标记待人工处理 取消不回滚已发生的副作用

还有一句必须记住(第 6 章):修改计划 ≠ 停止正在运行的工具,也不自动撤销已经发生的动作;执行、取消与状态管理是运行时的职责。

前端侧:取消后要能区分「用户主动停」与「系统超时停」,并如实呈现 —— 绝不能显示成「完成」。这与 ../04-agent-architecture/11-async-event-driven.md 的「在确认无法恢复之前不暴露中间态」是同一条纪律。

8. 前端消费:增量渲染、断线重连,以及「重放 ≠ 续传」#

① 增量渲染。 按事件增量更新,不要每来一个 token 就全量重渲染;思考段 / 正文 / 工具调用分区域渲染(三者顺序固定、语义不同)。

② 事件要带序号。 断线重连后可能收到重复事件,也可能乱序;只带时间戳不够(时钟不可靠、并发事件可能同刻)。序号同时承担去重与排序两个职责。

③ 把「重放」和「续传」分开 —— 这是本篇最值钱的一句。

重放(replay) 续传(resume)
做什么 把已经发生过的事件序列再发一遍 从中断的那一步继续往下跑
数据来源 持久化的轨迹 / 事件日志 运行时重新推进:接着生成、接着执行工具
会不会重新调模型 / 重新执行工具 不会 会(因此必须幂等 + 去重)
用来解决 断线后补上漏掉的那一段 进程挂了 / 用户暂停后接着做

混淆的后果是双向的:把「续传」当「重放」→ 重复副作用(邮件发两次、单下两次);把「重放」当「续传」→ 用户永远看不到后半段(因为后半段从来没被产生过)。

判据一句话:这段内容要不要新产生? 不产生就是重放,要产生才是续传。

④ 重连的纪律与 ./10-agent-runtime.md 是同一条。 客户端记「最后收到的序号」,重连时带上,服务端从该序号之后重放。而服务端要记的是「从起点连续保存到了哪个序号」,不是「最后保存了哪个」—— 若中间还缺着序号(对应那篇里「第二轮上传失败、第三轮成功」的缺口),就不能声称连续,应把重放起点退回到缺口之前。

⑤ 别把流当唯一交付通道。 流是易失的;权威状态在持久化那一层(../04-agent-architecture/14-agent-state.md)。UI 刷新后应当能凭持久化状态重建界面,而不是依赖「当时那条流还在」。

9. 一句话接回成本#

流式不省 token。 它只改分发方式,不改生成量;长输出仍然是主要成本与延迟来源(./latency-cost.md:限制 max_tokens 通常是回报最高的那一档)。反过来,流式还有两处可能更贵的地方:① 取消流式响应省不了已经生成 token 的钱;② 流式执行让工具更早启动,如果校验不严、调用被丢弃重来,就白花了工具资源。成本口径仍要按成功任务算,而不是按调用次数算(./10-agent-runtime.md)。

常见面试问法#

Q:流式能降低延迟吗? A:分两件事说,这个题最容易答混。 ① 面向用户的 token 流不改端到端时长 —— 总时长仍是 TTFT+TPOT×(N−1)\text{TTFT}+\text{TPOT}\times(N-1),流式只是让首 token 一到就渲染,把用户的「开始等待」从 E2E 缩到约等于 TTFT。它优化的是体感和可打断性,不是时延,也不省 token。② 流式执行工具调用确实缩短端到端:第一个调用的参数一完整、校验通过就开跑,执行与后续调用的生成重叠。一个只改体感、一个真的改时长,答题时必须分开说。另外要能说出什么时候不该流式:非交互场景、输出要整体校验后才交付、下游是程序不是人。

Q:SSE、WebSocket、HTTP chunked、gRPC streaming 怎么选? A:先问四个判据:单向还是双向、客户端是浏览器还是内部服务、链路中间有没有老旧代理 / CDN、消息边界与重连谁负责。SSE 是单向、走普通 HTTP、浏览器原生带重连(可带 Last-Event-ID 续读)—— 「一次请求 → 一串输出」的默认选择,最省事。HTTP chunked / NDJSON 也是单向,但帧边界与格式要自己定,很多 SDK 的 stream 就在这一层。WebSocket 把「双向」白送给你(用户中途插话、语音、回合中途引导),代价是心跳、重连、序号续读全得自己做。gRPC streaming 适合内部服务之间:二进制、强类型 IDL、四种流形态,但浏览器侧要用 gRPC-Web 且受代理限制。一句话:SSE 换来的是重连,WebSocket 换来的是双向,两者都要拿复杂度换。

Q:为什么说流式执行工具调用是「真正的」延迟优化? A:因为面向用户的 token 流不改变端到端时长,而流式执行工具调用改变的是「执行」与「生成」的重叠关系。书里的机制是:模型的输出顺序固定为「思考 → 正文 → 工具调用请求」,第一个工具调用的参数一经完整生成并通过校验,即可立即开始执行,无需等待模型生成后续调用。三个调用时,第一个刚完整就启动,与后两个的生成重叠;彼此独立的调用还能并行。但它有条件:参数要完整且通过两层校验(schema + 业务)、调用之间无依赖、有副作用的工具要谨慎(默认禁并发或走审批)、失败时只级联中止同批依赖调用而不波及独立调用与父级;如果半截输出里已有工具因为流式提前执行了,续写或重试前必须按调用指纹去重,避免重复副作用。

Q:流式连上了但一直没有数据,SDK 超时也不报错,你怎么查? A:这是静默卡死 —— 流式连接最危险的失败模式,它和「断开」不一样:断开会立即报错,而它连接建立成功、数据流却停了,看起来一切正常。根因是 SDK 的超时机制往往只覆盖初始连接,不覆盖传输过程。做法是引入独立的空闲看门狗:超过设定时间没有新输出就判定卡死,主动杀死挂起的流并触发重试;原则是「每个长连接都需要活性信号,而非仅依赖连接超时」。⚠️ 但判活信号不能只看底层有没有字节 —— 长思考、长工具可以合法地几十秒不吐 token,看门狗要基于应用层心跳(运行时主动发「还在执行第 N 步」),否则会把正常任务误杀。

Q:用户点了「停止」,你要做哪几件事? A:对齐取消式处理,四件事分开。① 模型侧取消流式响应(停止 decode)—— 注意已生成、已计费的 token 不退,取消省的是后面的不是已花的;② 工具侧发取消信号,工具在安全点响应(类似 ctx.Done())—— 而且收到「停止」不撤销已经发生的动作;③ 轨迹侧:未完成的工具结果用显式占位符表示,不能伪造成功,并防住「模型把『已启动』误当『已完成』」的语义风险;④ 副作用侧:已经发出去的邮件、已经下的单不会因为取消而回滚,要显式补偿或标记待人工。另外两句硬话:取消点必须是能安全收尾的位置;修改计划 ≠ 停止正在运行的工具。

Q:断线重连之后,前端怎么把没看到的内容补上? A:先把重放(replay)和续传(resume)分开。重放是把已发生的事件序列再发一遍,数据来自持久化的轨迹,不重新调模型、不重新执行工具;续传是从中断那一步继续往下跑,会重新生成、可能有副作用,所以必须幂等 + 去重。判据是「这段内容要不要新产生」:不产生就重放,要产生才续传。混淆的后果是双向的 —— 把续传当重放会重复副作用,把重放当续传会让用户永远看不到后半段。实现上事件要带序号(同时用于去重与排序,只带时间戳不够),客户端记「最后收到的序号」,服务端记「从起点连续保存到哪个序号」(不是「最后保存了哪个」,中间有缺口就不能声称连续)。最后:流是易失的,权威状态在持久化那一层,UI 应当能凭持久化状态重建,而不是依赖当时那条流还在。

易错点#

易错说法 问题 应该说
「流式降低了延迟」 混淆了体感与时长 面向用户的 token 流不改变 E2E,只把「等 8 秒」变成「立刻有字」;不省 token
「流式都是同一件事」 没分层 token 流只改体感;流式执行工具调用(参数一完整就开跑)真的缩短时长。两者要分开说
「流式一定更好」 忽略了不该流的场景 非交互场景、输出要整体校验后再交付、下游是程序 —— 这些场合流式只增加复杂度
「SSE 和 WebSocket 差不多,随便选」 没看方向性 SSE 单向(重连白送);WebSocket 双向(心跳 / 重连 / 序号续读自己做)。判据是方向、客户端、中间链路、边界与重连归属
「半截 JSON 解析失败 = 参数有错」 分不清「不完整」与「畸形」 解析器必须容忍不完整输入;执行触发条件是结构闭合 + schema 校验 + 业务校验,不是「字符串结束」
「开了结构化输出就不会有半截问题」 高估了受约束解码 受约束解码保证最终输出合法,不保证中途每一段都合法,也不替你判断语义够不够
「流断了会报错,不用管」 漏掉最危险的失败 最危险的是静默卡死(连上但数据没动);SDK 超时只覆盖初始连接 —— 要独立的空闲看门狗
「看门狗盯着 TCP 有没有数据就行」 会误杀长任务 长思考 / 长工具可以合法地几十秒不吐 token;判活要靠应用层心跳
「流中途失败直接返回错误码」 状态码已经发出去了 HTTP 200 已发出,后续失败只能用流内 error 事件表达 —— 这是前端契约的一部分
「用户按了停止,工具就停了」 混淆了「收到信号」与「动作撤销」 收到停止不会撤销已发生的动作;未完成的工具结果用占位符,不能伪造成功
「取消能省回已经花掉的 token 钱」 记错了省的是哪一段 取消省的是后面要生成的,已生成已计费的不退
「重连就把流重新放一遍」 混淆重放与续传 重放不重跑(来源是持久化轨迹);续传会重跑(必须幂等 + 去重)。判据是「要不要新产生」
「用时间戳给事件排序去重就够了」 时钟不可靠、并发同刻 事件要带序号,序号同时承担去重与排序
把本篇讲成「我们线上就是这么扛的」 触碰仓库硬规矩 这是书里的机制 + 公开资料,非一手经验;用「这套我会这么设计」的口径讲

自测#

1. 「流式把首字时间从 8 秒压到 0.5 秒,所以端到端延迟降低了 7.5 秒」—— 这句话哪里错了?

答案

错在把「首字时间」当成了「端到端时间」。端到端时长是 TTFT+TPOT×(N−1)+网络\text{TTFT}+\text{TPOT}\times(N-1)+\text{网络},流式只让首 token 一到就渲染,把用户感知到的开始等待从 E2E 缩到约等于 TTFT;但生成仍然要 (N) 个 token,TPOT 没变,总时长一点没变,token 用量与成本也没变。所以正确说法是「流式把『等 8 秒』变成『立刻有字』」—— 优化的是体感与可打断性(方向不对能提前叫停),不是时延。

要注意的另一半:流式执行工具调用确实缩短端到端——第一个调用的参数一完整、校验通过就开跑,执行与后续调用的生成重叠。所以这道题的正解是「面向用户的 token 流不改变 E2E;流式执行工具调用才真的改变 E2E」,而不是笼统地说「流式不改延迟」。

2. 一次推理要连出「搜代码 → 查配置 → 读日志」三个调用。哪些可以因为流式提前执行?要防什么?

答案

可以提前执行的是「参数一完整且校验通过」的调用,收益是它与后续调用的生成过程重叠;彼此独立的调用还能并行。书里的机制正是:第一个工具调用的参数一经完整生成并通过校验,即可立即开始执行,无需等待模型生成后续的工具调用。

要防的四件事:

  1. 校验不足就提前跑:校验有两层 —— schema 合法(结构闭合、类型正确)与业务合法(值在允许域内);schema 合法不等于业务合法。
  2. 把有依赖的调用并行发出:参数要么是模型瞎猜的、要么直接报错;工具描述里要显式写明依赖。
  3. 副作用型调用提前跑:下单、发信、写库这类工具默认不允许并发(失败安全),或强制走审批。
  4. 提前执行后的去重:⚠️ 若半截输出里已有工具因流式提前执行,那么续写或重试之前必须按调用指纹去重,否则同一个副作用会做两遍。

失败时走级联中止:终止同一批并行启动且依赖它的调用,不波及独立调用与父级操作(故障只在同一批并行调用内传播)。

3. 有人报告「流式接口偶尔会永久挂住,日志里什么都没有」。你的排查方向和修复是什么?

答案

先怀疑静默卡死,而不是断开 —— 断开一定会报错,而「连接建立成功、数据流却停了」看起来一切正常,所以日志里什么都没有。根因通常是:SDK 的超时机制只覆盖初始连接,不覆盖传输过程 —— 一个已经建好的流可以永远挂着。

修复是加独立的空闲看门狗:超过设定时间没有新输出就判定卡死,主动杀死挂起的流并触发重试;原则是「每个长连接都需要活性信号,而非仅依赖连接超时」。

⚠️ 一个必须补的细节:判活信号不能只看底层有没有字节 —— 长思考、长工具执行可以合法地几十秒不吐 token,纯字节超时会把正常的长任务误杀。所以看门狗要基于应用层心跳(运行时主动发「还活着,正在执行第 N 步」这类事件),并且这个心跳要区分「在等模型」「在执行工具」两种状态,否则把「慢」判成「死」。

顺带答加分项:如果失败发生在流已经开始之后,就没有状态码可用了(HTTP 200 已经发出),必须在事件协议里定义流内 error 事件 —— 这是前端契约的一部分。

4. 断线重连时,什么时候该「重放」,什么时候该「续传」?混了会怎样?

答案

判据一句话:这段内容要不要新产生?

  • 不产生 → 重放(replay):把已经发生过的事件序列再发一遍,数据来自持久化的轨迹 / 事件日志,不重新调模型、不重新执行工具。用于断线后补上漏掉的那一段。
  • 要产生 → 续传(resume):从中断的那一步继续往下跑(接着生成、接着执行工具),会重新调模型、可能产生副作用,因此必须幂等 + 去重。

混了的后果是双向的:把「续传」当「重放」→ 重复副作用(邮件发两次、单下两次);把「重放」当「续传」→ 用户永远看不到后半段,因为那半段从来没被产生过。

配套的两条工程纪律:① 事件要带序号(同时用于去重与排序 —— 只带时间戳不够,时钟不可靠、并发事件可能同刻);② 客户端记「最后收到的序号」,服务端记「从起点连续保存到了哪个序号」,不是「最后保存了哪个」 —— 中间有缺口(对应 10-agent-runtime.md 里「第二轮上传失败、第三轮成功」那种情况)就不能声称连续,重放起点要退回到缺口之前。

最后一句:流是易失的,权威状态在持久化那一层;UI 应当能凭持久化状态重建,而不是依赖当时那条流还在。

5. 客户说「我们的流式输出成本比预期高,是不是流式本身更贵?」你怎么答?

答案

流式本身不增加生成成本 —— 它只改分发方式,不改生成量:同样的 token 数、同样的计费。所以成本高要先往别处找。三处值得查:

  1. 输出太长。长输出是主要成本与延迟来源(latency-cost.md:限制 max_tokens 通常是回报最高的那一档)。流式让用户更愿意等长输出,反而可能把平均输出长度推上去 —— 这是「体感变好」的副作用,要显式设上限。
  2. 取消不省钱。用户看到方向不对就按停止,省的是后面要生成的,已生成已计费的不退;如果系统按「取消即视为无用」来做成本预估,就会低估。
  3. 流式执行让工具更早启动。如果参数校验不严、调用被丢弃重来,就白花了工具资源;而工具环境的成本要按成功任务算(10-agent-runtime.md),不能只算单次调用。

所以答法是:先否定「流式更贵」这个前提,再把讨论拉回输出长度、取消口径、工具资源的浪费率这三项,并主张按每成功任务成本而不是每 token 单价来核算。