Skip to content

AI 编辑的可靠性边界:生成、预览、采纳和保存 ​

用户选中一段文字让 AI 改写,等了一会儿关掉弹窗,继续编辑另一段。旧请求随后返回,如果直接把结果写回编辑器,用户刚输入的内容就可能被覆盖。

另一个常见失败是:AI 结果已经完整生成,也能在预览里显示,但插入现有正文后超过保存容量。用户点了“采纳”,最终却存不下来。

这两类问题都不是换一个提示词就能解决的。生成成功、结果仍适用、用户采纳和持久保存,是四次不同的确认。

本文总结当前源码能证明的行为,示例均为通用重写。建议方案会单独标明;没有把本次只读检查写成业务测试结果。

先给每个阶段一个明确结果 ​

text
用户选择操作与内容
        ↓
提交生成任务 → 失败、超时或取消
        ↓
校验并展示候选结果
        ↓
用户采纳 → 修改编辑器、成为未保存内容
        ↓
按原有保存流程提交 → 持久化成功或冲突

当前 AI 服务端只返回候选结果,不直接写正文。选区编辑和辅助写作在用户采纳后修改编辑器,后续仍经过页面原有保存流程。

这个边界让用户可以放弃一次生成,也让保存冲突继续由已有机制处理。不能因为内容来自 AI,就绕开编辑会话或省略保存失败提示。

阶段可以告诉用户什么还不能承诺什么
生成完成已获得候选结果内容一定正确或适用于当前文档
预览完成可以比较、复制或采纳已改动正文
采纳完成当前编辑器已更新服务端已经保存
保存完成本次提交已被服务端确认用户此后新增的输入也已保存

最后一种边界与普通编辑相同,详见编辑会话与本地草稿。

当前实现:忽略已经过期的生成结果 ​

前端为选区编辑和辅助写作分别维护请求序号。重新生成或关闭对应弹窗时,旧序号失效;响应返回后先核对序号,过期结果不进入预览。

它解决的是“这个响应还属于当前交互吗”,而不是判断模型答案好不好。

下面重新写一个最小示例,只演示结果失效机制:

ts
function createPreviewTask(generate, show) {
  let active = null

  return {
    async run(input) {
      const ticket = Symbol('preview')
      active = ticket
      const result = await generate(input)
      if (active === ticket) show(result)
    },
    dismiss() {
      active = null
    },
  }
}

这不是完整任务管理器:错误展示、loading 清理、网络取消和组件卸载都需要额外处理。它刻意只说明“过期结果不能更新当前界面”。

**建议:**更复杂的编辑场景还应核对账号、文档和编辑会话。请求期间正文继续变化时,采纳前重新确认目标范围,或用编辑器提供的位置映射机制定位选区。

当前序号机制不能被描述为“已经覆盖所有跨文档、跨账号或选区移动竞态”。

关闭弹窗和取消上游是两件事 ​

当前前端关闭这两类预览弹窗时,会作废在途结果,但该操作本身没有在相应交互函数中直接中止网络请求。

服务端另有取消机制:客户端响应连接异常关闭时,向上游请求传播取消信号。它监听响应连接,避免把正常请求体读取结束误判为任务取消。

因此,现状应表述为:前端已防止部分迟到结果进入界面,服务端已响应连接断开;点击关闭是否贯通终止上游,不能仅据这两段能力作出保证。

取消层级目的
界面失效不再展示或采纳这次结果
客户端请求中止结束本次网络等待
服务端传播信号尽量停止继续等待上游
上游实际停止取决于上游协议与执行状态

AbortController.abort() 可以中止 fetch 及其响应体读取,但它不提供远端业务事务回滚保证;后者是必须另行设计的协议边界。MDN AbortController.abort

**建议:**需要完整的“取消生成”体验时,让按钮触发网络中止,同时保留序号校验。取消和响应可能相邻发生,不能只依靠其中一层。

超时覆盖整段等待,清理覆盖每个出口 ​

当前模型调用集中在公共模块,调用方负责构造任务内容;连接错误、超时与部分上游错误在共同出口处理。

一次总超时应包含连接、等待响应和读取响应体。只给建立连接设置超时,不能限制模型长时间思考或响应体迟迟不结束。

当前实现会在结束时清理计时器和取消监听器。否则旧任务的计时器可能干扰后续工作,长期存在的监听器也会增加排查难度。

等待上限是项目的运行选择,不等于模型供应商对推理时长的承诺。浏览器、服务端和代理的限制需要协调,错误文案也应区分主动取消、超时与上游失败。

当前频率限制保存在单进程内存中。它不能直接当作多实例共享额度,也不等于同时运行任务的数量上限。

**建议:**实例数或调用成本增加后,再按实际需求补共享配额、并发限制与排队反馈;文章不据当前限频机制宣称已经具备这些能力。

容量至少有四种单位 ​

“内容不能超过某个长度”如果没有单位,前后端很容易各写出一种解释。

边界常见单位应检查的对象
HTTP 请求体字节编码后的整个请求
编辑器保存容量字符串计数或字节最终持久化正文,含标记和嵌入内容
模型上下文tokens提示词、用户内容及输出预算等上下文组成
文件上传字节独立资源本身

JavaScript 的 string.length 计算 UTF-16 码元,不等于 UTF-8 字节,也不等于人眼看到的字符数,更不能直接充当 token 计数。

当前实现把应用 HTML 容量与已识别模型的容量配置分开:请求的原始 HTML 先受应用限制;模型端负责判断实际上下文是否超限;返回结果清洗后再检查应用容量。

这里的模型配置只代表项目读到的能力假设,不能直接推广到所有兼容服务。适配新模型时,应重新核对参数语义和完成原因。

不要用截断把失败伪装成成功 ​

简单截取字符串尾部,可能丢掉用户最重要的背景;截取输出前半段,则可能留下未闭合结构或未写完的结论。

当前已识别模型的写作上下文不会再按旧字符阈值静默截尾;支持思考模式的模型调用路径会识别长度耗尽的结束原因,拒绝把已知不完整结果作为成功返回。

这个保护有明确适用范围:未知模型仍保留原有兼容限制,不能宣称所有供应商、所有模型都已具备同样的截断检测。

应用容量超限时,当前已识别模型路径会明确失败,而不是把清洗后的 HTML 截成一段返回。对于上游明确报告的上下文超限,也会提示缩小内容或拆分文档。

**建议:**把“缩短范围后重试”作为显式用户动作。需要自动分块时,应设计段落边界、上下文衔接和结果校验,而不是藏在字符串截断里。

采纳前还要检查整篇文档 ​

一段候选结果单独不超限,不代表追加后仍能保存;替换选区也要考虑被删内容与新内容的差额。

通用判断流程可以写成:

text
当前文档 + 本次替换或插入操作
        ↓
在临时文档上应用变更
        ↓
按持久格式序列化并检查容量
        ├─ 超限 → 保留候选结果,提示缩小范围
        └─ 合法 → 允许采纳,再走正常保存

这是进一步加强采纳体验的建议,不表示当前每个采纳入口都已实现临时文档预检查。服务端保存校验仍是最后一道边界,不能只依赖按钮状态。

模型输出也不是可信 HTML。当前结果会经过白名单清洗,但清洗不证明事实正确,也不替代编辑器自己的内容约束。格式与附件身份如何保存,见富文本与附件生命周期。

失败案例与验证建议 ​

下面场景适合在隔离环境配合可控响应验证。本次整理没有调用真实模型执行这些测试。

场景应观察的结果
先发甲请求,再发乙请求,甲最后返回甲不能覆盖乙的预览
生成期间关闭弹窗不改正文,迟到结果不重新打开旧预览
页面连接断开服务端取消等待,计时器和监听器得到清理
上游明确返回长度耗尽对支持该识别的路径,不展示为完整成功
返回 HTML 含不允许的结构清洗后检查可展示内容与容量
候选片段合法,追加后全文超限保留用户原文与候选结果,明确说明无法保存
采纳后发生保存冲突使用正常冲突流程,不绕过版本校验
代理先于应用超时错误可见,不能把无响应当作已保存

验证时同时观察界面状态、编辑器内容和持久数据。只看“AI 请求返回成功”,不足以证明这次编辑可靠完成。

接下来读什么 ​

为复用而记录,为理解而整理。