流式输出与增量执行(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):
流式做的是:首 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 流不改端到端时长 —— 总时长仍是 ,流式只是让首 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 秒」—— 这句话哪里错了?
答案
错在把「首字时间」当成了「端到端时间」。端到端时长是 ,流式只让首 token 一到就渲染,把用户感知到的开始等待从 E2E 缩到约等于 TTFT;但生成仍然要 (N) 个 token,TPOT 没变,总时长一点没变,token 用量与成本也没变。所以正确说法是「流式把『等 8 秒』变成『立刻有字』」—— 优化的是体感与可打断性(方向不对能提前叫停),不是时延。
要注意的另一半:流式执行工具调用确实缩短端到端——第一个调用的参数一完整、校验通过就开跑,执行与后续调用的生成重叠。所以这道题的正解是「面向用户的 token 流不改变 E2E;流式执行工具调用才真的改变 E2E」,而不是笼统地说「流式不改延迟」。
2. 一次推理要连出「搜代码 → 查配置 → 读日志」三个调用。哪些可以因为流式提前执行?要防什么?
答案
可以提前执行的是「参数一完整且校验通过」的调用,收益是它与后续调用的生成过程重叠;彼此独立的调用还能并行。书里的机制正是:第一个工具调用的参数一经完整生成并通过校验,即可立即开始执行,无需等待模型生成后续的工具调用。
要防的四件事:
- 校验不足就提前跑:校验有两层 —— schema 合法(结构闭合、类型正确)与业务合法(值在允许域内);schema 合法不等于业务合法。
- 把有依赖的调用并行发出:参数要么是模型瞎猜的、要么直接报错;工具描述里要显式写明依赖。
- 副作用型调用提前跑:下单、发信、写库这类工具默认不允许并发(失败安全),或强制走审批。
- 提前执行后的去重:⚠️ 若半截输出里已有工具因流式提前执行,那么续写或重试之前必须按调用指纹去重,否则同一个副作用会做两遍。
失败时走级联中止:终止同一批并行启动且依赖它的调用,不波及独立调用与父级操作(故障只在同一批并行调用内传播)。
3. 有人报告「流式接口偶尔会永久挂住,日志里什么都没有」。你的排查方向和修复是什么?
答案
先怀疑静默卡死,而不是断开 —— 断开一定会报错,而「连接建立成功、数据流却停了」看起来一切正常,所以日志里什么都没有。根因通常是:SDK 的超时机制只覆盖初始连接,不覆盖传输过程 —— 一个已经建好的流可以永远挂着。
修复是加独立的空闲看门狗:超过设定时间没有新输出就判定卡死,主动杀死挂起的流并触发重试;原则是「每个长连接都需要活性信号,而非仅依赖连接超时」。
⚠️ 一个必须补的细节:判活信号不能只看底层有没有字节 —— 长思考、长工具执行可以合法地几十秒不吐 token,纯字节超时会把正常的长任务误杀。所以看门狗要基于应用层心跳(运行时主动发「还活着,正在执行第 N 步」这类事件),并且这个心跳要区分「在等模型」「在执行工具」两种状态,否则把「慢」判成「死」。
顺带答加分项:如果失败发生在流已经开始之后,就没有状态码可用了(HTTP 200 已经发出),必须在事件协议里定义流内 error 事件 —— 这是前端契约的一部分。
4. 断线重连时,什么时候该「重放」,什么时候该「续传」?混了会怎样?
答案
判据一句话:这段内容要不要新产生?
- 不产生 → 重放(replay):把已经发生过的事件序列再发一遍,数据来自持久化的轨迹 / 事件日志,不重新调模型、不重新执行工具。用于断线后补上漏掉的那一段。
- 要产生 → 续传(resume):从中断的那一步继续往下跑(接着生成、接着执行工具),会重新调模型、可能产生副作用,因此必须幂等 + 去重。
混了的后果是双向的:把「续传」当「重放」→ 重复副作用(邮件发两次、单下两次);把「重放」当「续传」→ 用户永远看不到后半段,因为那半段从来没被产生过。
配套的两条工程纪律:① 事件要带序号(同时用于去重与排序 —— 只带时间戳不够,时钟不可靠、并发事件可能同刻);② 客户端记「最后收到的序号」,服务端记「从起点连续保存到了哪个序号」,不是「最后保存了哪个」 —— 中间有缺口(对应 10-agent-runtime.md 里「第二轮上传失败、第三轮成功」那种情况)就不能声称连续,重放起点要退回到缺口之前。
最后一句:流是易失的,权威状态在持久化那一层;UI 应当能凭持久化状态重建,而不是依赖当时那条流还在。
5. 客户说「我们的流式输出成本比预期高,是不是流式本身更贵?」你怎么答?
答案
流式本身不增加生成成本 —— 它只改分发方式,不改生成量:同样的 token 数、同样的计费。所以成本高要先往别处找。三处值得查:
- 输出太长。长输出是主要成本与延迟来源(
latency-cost.md:限制max_tokens通常是回报最高的那一档)。流式让用户更愿意等长输出,反而可能把平均输出长度推上去 —— 这是「体感变好」的副作用,要显式设上限。 - 取消不省钱。用户看到方向不对就按停止,省的是后面要生成的,已生成已计费的不退;如果系统按「取消即视为无用」来做成本预估,就会低估。
- 流式执行让工具更早启动。如果参数校验不严、调用被丢弃重来,就白花了工具资源;而工具环境的成本要按成功任务算(
10-agent-runtime.md),不能只算单次调用。
所以答法是:先否定「流式更贵」这个前提,再把讨论拉回输出长度、取消口径、工具资源的浪费率这三项,并主张按每成功任务成本而不是每 token 单价来核算。