AI应用工程 · KNOWLEDGE CHAPTER
K32

流式协议与会话状态

把聊天理解为“事件驱动的运行记录”,而非不断拼接的一条字符串。网络负责运送字节,协议负责划分事件,运行状态决定内容归属,界面负责按用户能阅读的速度呈现。四层拥有不同的完成条件,任何一层结束都不能替其他层宣布成功。

AI应用工程2 道章节练习8 道主练关联

核心模型

把聊天理解为“事件驱动的运行记录”,而非不断拼接的一条字符串。网络负责运送字节,协议负责划分事件,运行状态决定内容归属,界面负责按用户能阅读的速度呈现。四层拥有不同的完成条件,任何一层结束都不能替其他层宣布成功。

原理与机制

从字节恢复完整事件

一次 read 可能切断中文字符、SSE 行或 JSON,也可能同时带来多个事件。因此先流式解码 UTF-8,再处理 LF、CR、CRLF 和空行边界,合并同一事件的多行 data,最后按应用契约解析 JSON。注释心跳不算正文,结束标记也不属于 SSE 标准。读取前检查 HTTP 状态、类型和 body;EOF 前缺少结束空行的半条事件必须丢弃,即使其 data 恰好可解析。已确认正文可以保留,但运行应标为中断,直到取得可信终态。

让每次状态写入都有归属

会话、分支、消息和生成尝试分别需要身份;messageId 与 parentId 描述历史关系,runId 描述一次尝试。编辑祖先问题应建立新分支,多设备写入通过版本检查处理冲突。AbortController 只能尽力取消传输,已经排队的 delta、error、finally 仍可能执行,所以每次写 UI 前还要检查本分支的运行版本。取消时先失效版本再 abort,可避免同步监听器重入破坏新状态。切换显示分支不必取消所有后台任务,清理范围须明确。

把恢复与再次执行分开

断网后首先用 runId 查询原任务,并从最后确认序号重放或获取快照;重新创建生成是另一项操作。客户端忽略重放,发现序号缺口就恢复,不能跳过缺口继续拼接。快照必须携带身份、覆盖序号和状态,原子替换文本与游标,再应用其后的事件。创建任务及副作用工具还需服务端持久化幂等记录和并发唯一约束。超时代表结果未知;上游缺乏查询或幂等能力时,应核对结果,不能保证盲重试不会重复执行或计费。

控制渲染频率并保护阅读位置

增量先进入有容量上限的分块缓冲,按帧或短窗口合并为快照,只更新活跃消息。每个 token 都 join 全文并重解析 Markdown,会产生随长度放大的累计工作;高亮和表格可延后处理,净化规则不能放松。用户向上阅读或选中文字时暂停跟随,保留锚点并提示新内容。视觉更新与读屏播报分离,用礼貌状态区按句段或阶段提示,不反复朗读全篇。窗口化不能移除正在聚焦的节点;本地背压也不保证远端立即停算。

把草稿转为可提交结果

结构化流可以展示字段草稿,但提交必须等待完整终态、排除拒绝,并通过 schema、业务校验和必要确认。合法字符串仍可能表示不存在的日期,半个 JSON 不能靠补括号取得执行资格。用户改过的字段通过 dirty 标记与版本保留,后续模型值只能作为候选差异。上下文摘要同样是有损派生物;会话删除和重新生成不能撤销已发送邮件等现实动作,执行回执应独立保留并按目的限制访问和保存期限。

最小示例

javascript
javascript
function createLatestGate() {
  let epoch = 0, controller = null;
  return {
    begin() {
      const previous = controller;
      const own = new AbortController();
      const id = ++epoch;
      controller = own;
      previous?.abort();
      return {
        signal: own.signal,
        commit(write) {
          if (id !== epoch || own.signal.aborted) return false;
          write();
          return true;
        }
      };
    },
    cancel() {
      const previous = controller;
      controller = null;
      ++epoch;
      previous?.abort();
    }
  };
}
const gate = createLatestGate();
const old = gate.begin(), current = gate.begin();
let text = '';
old.commit(() => { text = '旧回答'; });
current.commit(() => { text = '新回答'; });
console.assert(text === '新回答');

每个会话分支各建一个 gate,正文、错误和结束状态都走 commit。write 必须同步、短小;异步计算结束后再调用 commit,不能把 async 回调放进去。示例验证本地写入归属,不提供服务端取消、持久化幂等或事务隔离。

核验:Node v24.19.0:旧运行不回写、最新回写、同步abort重入及取消后禁写断言通过;未测试SSE解析或服务端恢复。

边界与取舍

边界

  • SSE 的 Last-Event-ID 只是恢复线索;服务端必须实际保存可重放事件或快照。
  • 缓冲上限需配合暂停、持久化或显式失败;静默丢字与无限增长都不可取。
  • 普通浏览器埋点只能近似可见时间,EOF、JSON.parse 成功和用户批准是不同证据。

取舍

  • 批处理降低主线程压力,但窗口过大会削弱流式反馈,应联合测量首字、输入响应和峰值内存。
  • 事件重放恢复细,成本较高;只存最终快照更简单,但中途只能展示待完成状态。

口述示范

我先按协议恢复事件,再按分支与 run 校验状态归属;取消、恢复和重新生成各有契约。正文批量呈现,读屏与滚动尊重用户。只有可信终态和校验通过后,生成草稿才能进入提交。

章节练习

练习 1

把同一条中文 SSE 在所有字节位置切开,再模拟停止旧运行并立即重生成。

展开检查点
  • 覆盖 CRLF 跨块及缺最终空行;不得派发半事件。
  • 旧 delta、error、finally 和同步 abort 重入都不能覆盖新运行。
练习 2

假设 TranslateFlow 增加流式表单与历史分支,设计断流恢复和人工修改的验收流程。

展开检查点
  • 恢复同一 run,快照正文与游标一起替换;副作用核对幂等记录。
  • 用户字段不被覆盖,读屏不过度播报,切回旧分支不混入新结果。

参考来源与核验边界

核验日期 2026-10-04。后台恢复参数、SDK 支持与数据保留依服务版本而变;不要照搬为所有 SSE 服务的默认能力。官方说明 previous_response_id 仍会计入链中历史输入 token,状态托管不代表上下文免费。

内容版本:2026-10-04.2 · 题目和来源 ID 保持原样,本站不将面经标签解释为企业官方出题或高频保证。