我以为 SSE 渲染就是 Agent 流式,直到它停在用户确认前
我做过 SSE 渲染:服务端不断推送事件,前端拿到增量文本就更新页面。所以刚开始学 Agent 的 Streaming 时,我的判断很直接:这不就是模型流式输出,Runtime 接一下,前端渲染一下吗?
这个判断没有错,但它只覆盖了最容易看见的那一段。
真正让我重新理解这件事的,不是"你好"两个字怎样逐步显示,而是下面这个场景:模型已经流式返回了待写入内容,进程却在用户确认之前重启了。此时新启动的 Agent 到底该知道什么?又凭什么不能直接把内容写出去?
顺着这个问题,我把 Streaming、上下文和会话状态连成了一条线。
流式首先是一次尚未结束的调用
一个简化的正常过程可能是:
vbscript
response.created
→ output_text.delta
→ output_text.delta
→ response.completed
SSE 前端通常消费的是中间的 output_text.delta:拿到"你"就显示"你",再拿到"好"就显示"你好"。这正是流式能改善体验的原因------用户不必等全部结果生成完才看到第一个字。
但这里有一个容易混淆的点:首字更早出现,不等于完整结果更早结束。模型仍需要生成剩余内容,所以流式通常降低首字延迟,却不保证总耗时更短。
而且,Agent 的流里不只有文本。中间还可能混进工具参数增量、错误事件和取消事件。于是我不再把流式理解成"前端的渲染技巧",而是理解成 Runtime 正在处理一条尚未结束的事件生命周期。
我补了一个 SSE 增量渲染 Demo。它逐帧解析 SSE,并验证三件事:
- 收到文本增量时,视图立即更新;
response.failed可以保留已显示的部分文本,但不能伪装成完成;response.cancelled同样不是成功结束。
这段 Demo 不需要我重新学习 SSE。它只是把我已有的渲染经验变成一份可检查的工程证据。
重启后,不是把全部历史重新塞给模型
接下来问题变得更具体:如果会话重启,模型没有旧进程的记忆。Runtime 当然要根据会话 ID 找回原来的记录,但找回记录不等于把所有内容都发给 LLM。
本次分析停在 waiting_confirmation:模型已经提出了一个待写入意图,但用户还没有确认。
这时真正会影响下一步判断的信息包括:
当前目标是什么
本次有哪些约束
会话仍在等待用户确认
待执行的写入意图是什么
这个意图是否已经执行过
反过来,上周聊过的电影、午饭、已经与当前任务无关的旧闲聊,即使也存在于持久化会话中,也不应该因为"重启了"就自动成为模型上下文。
这里我把两个层次分开了:
Runtime 恢复完整会话状态
→ 从中选出影响下一步判断的语义信息
→ 再把这部分交给 LLM
例如原始幂等键属于 Runtime 防止重复执行的机制;但"这次待写入意图尚未执行"这个事实,会影响模型和 Runtime 接下来应该停在确认、继续还是拒绝。前者不必原样出现在 Prompt 中,后者可能是有效上下文。
重启确认前的上下文 Demo 把这个场景固定下来:它保留目标、约束、等待确认、待执行意图和执行状态,排除无关旧闲聊。这里的预算数字只是确定性示例,不代表任何真实模型的 token 计算;它证明的是选择规则,而不是某个模型的实际价格或延迟。
模型流结束,不代表 Runtime 可以执行
我原本还会把下面几件事靠得太近:模型流结束、工具参数完整、真正写入发生。
现在看,它们至少是三道不同的门:
vbscript
response.completed
→ 模型这次流正常结束
工具参数可解析且通过 Schema
→ Runtime 可以理解这个调用
用户确认已持久化
→ Runtime 获得产生外部副作用的授权
例如工具参数可能分两段到达:
json
{"title":"
学习记录"}
Runtime 不能在第一段时提前执行;等 JSON 完整后,还要做 Schema 和业务校验。即使参数完全合法、response.completed 也已经到达,只要会话仍是 waiting_confirmation,写入就仍然不能发生。
这也是重启场景最容易出问题的地方:如果新进程只看见"模型曾经要写入",却没有恢复"用户尚未确认",它就可能越权执行;如果只看见"待写入",却不知道是否已经执行过,又可能重复执行。
为此我运行了 状态恢复 Demo:
bash
node examples/2026-07-27-streaming-state-recovery/demo.mjs all
它覆盖正常、错误、取消、重启恢复四条路径。它不代表某个厂商的协议,也不接真实模型 API;它只验证一个 Runtime 最小原则:事件终态、参数校验、确认状态和防重复状态,必须由 Runtime 显式维护。
我今天真正改掉的理解
今天不是重新学习怎样写 SSE,而是把已有经验放回 Agent 的完整链路中。
我现在会这样理解一次带工具的流式调用:
模型持续产生事件
→ Runtime 决定怎样消费和向前端交付
→ Runtime 选择哪些旧状态仍值得交给下一次模型调用
→ Runtime 在确认、校验和幂等边界上阻止越权执行
所以,Agent 的流式体验不只是"字能不能先出来"。真正决定它是否可靠的,是 Runtime 能不能在流没有完成、调用失败、用户取消、进程重启和等待确认这些节点上,始终知道自己处于什么状态。
下一步我会进入 L13,从用户点击发送消息开始,追到 Node/TS 后端、模型、工具、确认节点和聊天 UI 的完整调用链。