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 请求返回成功”,不足以证明这次编辑可靠完成。
接下来读什么
- 笔记应用开发实践总览:理解 AI 能力在整个系统里的位置。
- 快照同步与并发冲突:采纳后的内容怎样接受版本保护。
- 懒加载与发布恢复:长时间打开的编辑页面怎样应对发布变化。