博客
文章系列日历
归档关于搜索

鄂ICP备19019526号

© 2026 博客

  1. 文章
  2. AI 应用的 Markdown 与富文本协同编辑工程 2026

AI 应用的 Markdown 与富文本协同编辑工程 2026

2026年8月22日·约 12 分钟·3577 字·0 次阅读
智能体与 AI 应用开发
AI 应用的 Markdown 与富文本协同编辑工程 2026

目录

  • 一、问题的提出:AI 原生编辑器的工程命题
  • 二、形式化:AI 富文本的四元组
  • 三、渲染管线:从 token 流到增量 DOM
  • 四、流式协议:增量 token 的解析容错
  • 五、版本树与撤销栈:人机协作的 diff/patch 系统
  • 六、协同编辑:CRDT/OT 与 AI 介入
  • 七、对工程实践的推论
  • 7.1 渲染管线选型
  • 7.2 版本树落地
  • 7.3 协同框架选型
  • 7.4 性能监控指标
  • 7.5 安全护栏
  • 7.6 与传统富文本编辑器的差异化
  • 八、对比与局限
  • 九、给前端工程师的可落地清单
  • 选 streaming remark 的 5 个判断点
  • 版本树必走的 3 个反模式
  • CRDT 集成的 4 个陷阱
  • 性能预算的红线
  • 参考文献

AI 应用的 Markdown/MDX 富文本渲染与协同编辑工程 2026:从流式增量到人机协作的版本树

把 Markdown 解析器从一次性批处理重构成流式增量引擎,把 AI 生成从"整段替换"重构成"局部 patch",把撤销栈从线性升级为可分支的版本树,人机协作的富文本编辑器才能走出"打字机时代"的工程范式。

一、问题的提出:AI 原生编辑器的工程命题

2026 年的 AI 应用已经全面进入"生成式富文本"时代。当 GPT-4、Claude 3.5、文心 4.0、Qwen2.5、DeepSeek-V3 等模型流式输出 Markdown 时,前端不再是单纯的"展示一段文本",而是"实时渲染一个文档结构"。然而,绝大多数团队仍在沿用 2020 年的工具链——一次性解析完整 Markdown、整段替换 DOM、线性撤销栈、单用户编辑——这就像用蒸汽机驱动高铁:模型能力提升了十倍,前端体验却被自己卡在原地,既无法真正发挥流式生成的优势,也无法承载 AI 介入后的多人协同需求。

一个来自一线的真实现场:某 AI 写作助手产品,流式输出 1500 字文章时,用户看到首字节到完整渲染的平均耗时是 8.2 秒,期间页面会闪烁 4 至 6 次(每次新的 Markdown 块到达就触发整段 re-render),用户中断流式输入时的"保留半句话"概率是 17%(因为半截的反引号没闭合、列表项被截断、表格行残缺),用户尝试撤销 AI 生成时发现"撤销一次就把整段都删了"(因为 AI 是整块替换,没有 transaction 边界),用户想保留 AI 的某些修改同时撤销其他修改时根本无法做到(因为线性栈不支持分支)——这些都不是"模型问题",也不是"网络问题",而是前端架构问题。架构不重构,模型再强也救不了体验。

这不是孤立案例。根据某 AI 应用开发者的内部统计,90% 的"AI 输出很慢"投诉都不是模型推理慢,而是前端渲染慢;80% 的"AI 输出格式错乱"投诉都是流式解析容错缺失;70% 的"AI 编辑体验差"投诉都是撤销栈设计错误。换言之,AI 应用的产品竞争力,越来越不取决于模型本身,而取决于围绕模型的工程架构。

本文聚焦AI 原生富文本编辑器的四大工程命题:渲染管线(从 token 流到增量 DOM 的演化路径)、流式协议(partial markdown 的解析容错与边界 case 处理)、版本树(人机协作的 diff/patch 系统与撤销栈分支化)、协同编辑(CRDT/OT 与 AI 介入的三方冲突解决)。这四个命题环环相扣、缺一不可:渲染管线决定了 AI 输出能否"边生成边看",流式协议决定了边缘情况是否崩溃,版本树决定了用户能否"放心编辑",协同编辑决定了多人+AI 的冲突如何解决。

与传统富文本编辑器(Notion、语雀、飞书文档、WPS 文档、Microsoft Loop)相比,AI 原生编辑器有三个根本差异:增量性(内容是流式到达的,不是一次性输入完成的)、可重写性(AI 可以重写用户的局部内容,不只是简单追加)、三方冲突(人+人+AI 同时编辑同一文档)。这三个差异让传统 WYSIWYG 编辑器的架构不再适用——我们需要重新思考渲染、撤销、协同的底层数据模型与交互模型,而不是在老架构上贴 AI 标签。

更重要的是,这四个工程命题之间存在强烈的耦合关系。一个常见的反模式是"先做渲染,等出问题再做撤销"——结果在撤销栈里发现版本历史无法追溯到 AI 的原始生成,无法做"撤销 AI 但保留我自己的修改"这种细粒度操作,只能全量回滚或全量保留,用户体验崩塌。正确的做法是先把四个命题的数据模型设计好,再考虑 UI 和交互。本文会按照这个顺序展开,先形式化,再工程化,最后给可落地清单。

二、形式化:AI 富文本的四元组

AI 富文本编辑器的核心数据结构可以形式化为四元组 (A, P, V, C),四者紧耦合、缺一不可。下面分别给出严格定义与工程约束。

A — AST(抽象语法树):具体为 MDAST(Markdown AST) 与 HAST(HTML AST) 的双层结构。MDAST 表示语义层,包含 heading、paragraph、list、code、table、blockquote、image 等节点的语义信息;HAST 表示渲染层,包含 div、span、className、style 等浏览器可渲染的结构。两者通过 remark-rehype 桥接,但流式场景下桥接必须是渐进式的——不能等完整 Markdown 都到达再一次性转 HAST,因为中间状态(半截的列表项、不闭合的反引号、未完成的代码块)必须可视化给用户。这种渐进式桥接要求每个块独立解析、独立转换、独立渲染,任意一块失败不影响其他块的呈现。A 的工程约束:块粒度(block granularity)而非行粒度,块边界 = 空行 / # / > / ``` / --- 等 Markdown 控制字符。

P — Patch(增量操作):具体为 JSON Patch (RFC 6902) 操作的集合。每个 Patch 是一个 {op, path, value} 三元组,常见操作包括 add、remove、replace、move、copy、test。Patch Algebra 支持三种基本运算:合成(两个 patch 顺序应用得到一个新 patch)、求逆(撤销时反演所有操作得到 inverse patch)、冲突检测(同一 path 多次 add 时报错或按规则解决)。AI 生成对应一连串 add 与 replace patch,撤销对应一连串 inverse patch,而不是整段 innerHTML 重写。这种基于 patch 的设计天然支持事务(transaction)、乐观锁、回滚等高级特性。P 的工程约束:原子性(transaction 边界,要么全应用要么全不应用)、幂等性(同一 patch 应用多次结果一致)、可序列化(patch 可序列化到 JSON 便于持久化和传输)。

V — Version Tree(版本树):即文档状态的不可变快照树。每个节点是 (timestamp, author, ast, patches, parent) 五元组,父节点是 patch 应用前的状态,子节点是 patch 应用后的状态。根节点是空文档,叶子节点是当前显示状态(可能有多个,代表不同分支)。线性撤销栈是 Version Tree 退化为单链的特殊形态——当用户只进行线性编辑不分支时,Version Tree 就是一条直线;当用户重做、AI 重写、协同分支时,Version Tree 会形成 DAG(有向无环图)。版本树的内存开销可以通过快照压缩(老节点只保留 patch 而不保留完整 AST,需要时 lazy materialization)来控制。V 的工程约束:不可变性(节点一旦创建不可修改)、可追溯性(每个节点都能追溯到根节点)、可分支性(同一节点可有多个子节点)。

C — CRDT(Conflict-free Replicated Data Type):具体为 Yjs 或 Automerge 的实现。CRDT 的核心数学性质是操作满足交换律、结合律、幂等律——任意两个副本接收同一组操作(顺序可能不同)都能收敛到同一状态。这是协同编辑的数学基础,也是 AI 介入协同的关键:AI 生成的操作也必须满足 CRDT 律,才能被所有人的副本同步接受。CRDT 的代价是存储开销(每个字符附带版本向量)和冲突解决策略选择(LWW vs RGA vs Tree-based)。C 的工程约束:收敛性(任意副本最终一致)、无中心性(不需要中央服务器)、因果一致性(操作依赖关系保留)。

四元组的耦合关系严格如下:A 由 P 演化(任意 patch 应用到 ast 上产生新 ast)、V 是 A 的时间序列(每个 version 是某个时刻的 ast 快照)、C 是 A 的多副本同步(多人的 A 通过 CRDT 操作收敛)。流式场景的特殊性在于 P 是异步、增量、可错的(网络可能截断、token 可能残缺、解析可能失败),这是传统富文本编辑器不面临的难题,也是 AI 原生编辑器必须解决的核心工程问题。

更精细的形式化还包括会话(Session) 与 认领(Claim) 两个辅助概念:会话是一次编辑活动的完整生命周期(从打开文档到关闭),每次会话产生一个 author ID;认领是 AI 生成内容时标记 AI 是该段落的"原作者",用户接受后转化为"合著者"。这两个概念支撑"作者归属"和"贡献度统计",是企业级 AI 编辑器(法律、金融、学术)必须的元数据。

三、渲染管线:从 token 流到增量 DOM

流式 Markdown 渲染的核心是增量解析器(Incremental Parser)。主流方案是 streaming remark(remark-parse 的流式扩展)或 mdast-util-from-markdown 的块级分段 API。其原理是把 Markdown 按块边界(空行、# 开头、> 开头、``` 开头、--- 分隔线等)切分成可独立解析的块,每个块解析后立即送入渲染管线,不需要等全文到达。

一个典型的增量渲染管线实现如下:

// streaming remark 模式
const parser = new StreamingRemarkParser();
const renderer = new IncrementalRenderer(rootEl);

tokenStream.on('token', (token) => {
  const block = parser.feed(token);
  if (block) {
    renderer.appendBlock(block);
  }
});

// 块级占位:不完整的块用灰色 skeleton 显示
parser.on('incomplete', (partialBlock) => {
  renderer.showPlaceholder(partialBlock);
});

// 增量更新:同一个块的新版本替换旧版本,而不是追加
parser.on('update', (oldBlockId, newBlock) => {
  renderer.replaceBlock(oldBlockId, newBlock);
});

块级占位(Block Placeholder)是不完整块的处理范式:用户看到 AI 生成到一半时,已完成的块正常渲染,未完成的块以骨架屏或灰色低饱和度显示,让用户清楚"这里还在写"。这避免了"白屏等 8 秒"和"闪烁 4 次"的糟糕体验,同时也降低了用户的焦虑感——他们能直观看到"AI 在工作"。占位策略有三种:隐藏(完全不渲染)、灰显(低饱和度渲染)、骨架屏(灰色矩形 + 微动画)。生产实践推荐灰显(用户能感知进度但不刺眼)。

懒加载与隔离是性能优化的关键。代码块(尤其是 Python、JavaScript 等长代码)、Mermaid 图表、LaTeX 公式、大表格(超过 5 列)都应该在进入视口时才渲染,并通过 requestIdleCallback 或 Web Worker 隔离,避免阻塞主线程。一个生产环境验证的经验值:首字节渲染 < 50ms(纯 JS 解析的开销)、增量块渲染 < 16ms(一帧 60fps 的预算)、完整文档渲染 < 200ms(10k 字以内的硬上限)。超出这些预算,用户就能明显感知延迟或卡顿。

Web Worker 隔离的具体实现:用 comlink 或原生 postMessage 把解析逻辑放到 worker 线程,主线程只负责 DOM 更新。需要注意Markdown → HAST → DOM 这一链条中,HAST → DOM 必须放主线程(浏览器只允许主线程操作 DOM),Markdown → HAST 可以放 worker。同时,Web Worker 不能访问 localStorage,所以解析器需要的任何配置(主题、扩展、规则)必须通过 postMessage 传递。

渲染管线的回滚与重渲染策略:当用户编辑导致某个块失效(如修改代码块标识符把 python 改成 javascript),需要重渲染该块但保留其他块。这要求渲染管线支持块级 key,每个块有稳定的 blockId(基于内容 hash 或显式 ID)。删除块、修改块、追加块都通过 blockId 定位,而不是数组下标。这与 React/Vue 的 key 机制一致,经验丰富的开发者应该不陌生。

渲染管线还有一个隐含陷阱:MDX 与 Markdown 的兼容性。MDX 允许在 Markdown 中嵌入 JSX 组件(如 <Chart data={data} />),但 JSX 必须是闭合的、完整的,无法流式解析(半截的 <Chart 没办法编译)。工程上的折衷方案是"MDX 块用整段解析,普通 Markdown 块用流式解析"——通过检测 ```mdx fence 自动切换模式,虽然损失一些流式体验,但保证 JSX 不会半截崩溃。生产实践中,这种混合模式在 90% 的场景下体验与纯流式几乎无差异。

四、流式协议:增量 token 的解析容错

partial markdown 的解析容错是流式编辑器的"暗礁"。常见的边界 case 包括以下五类:

  1. 反引号不闭合:AI 生成到 使用 \python` 时被截断,后续所有内容会被错误解析为 inline code,直到下一个反引号出现才恢复。修复策略:检测到奇数个反引号时,把后续内容暂时按 plain text 渲染(直到下一个反引号出现或流结束),并用样式提示用户"代码块未闭合"。这种 case 在 Claude 输出"代码 + 解释 + 代码"的场景特别常见,因为 Claude 喜欢用反引号引用变量名,容易形成跨段落的反引号对。

  2. 列表项被截断: - 项目 1\n- 项目 2\n- 项 第三项没写完就停了。修复策略:不完整列表项以灰色 placeholder 显示,提示"AI 还在写"。如果流长时间停止(< 200ms),自动补全一个空字符串,让列表项保持闭合。还可以保留列表项编号(即使文字未完成,序号也正确),让用户清楚"这是第几项"。

  3. 表格行残缺: | 列1 | 列2 |\n| --- | --- |\n| 数据1 | 第三行只有一列,无法对齐到表头。修复策略:不完整行按当前可见部分渲染,列宽自适应,缺失单元格用占位符填充。同时,表格的列宽应按已闭合行的最大宽度确定,残缺行不参与列宽计算,避免列宽抖动。

  4. 代码块提前闭合: AI 写到 python\nprint(1)\n 后又写了一段普通文本,但中间可能漏了闭合反引号。修复策略:启发式补全——检测到代码块标记后 200ms 内没有新 token 到达,自动补全 ```,避免吞掉后续所有内容。同时,在代码块末尾加灰色"AI 自动补全"标记,方便用户察觉。这种 case 在长代码生成场景特别危险,因为遗漏的反引号会导致后续所有 Markdown 都按代码块解析,直接破坏文档结构。

  5. 标题残缺: ## 这是一个未完 显示为半个标题,语义不完整。修复策略:不完整标题保留渲染,但用 italic 或低饱和度提示"未完成"。还可以把标题字号按完整字数预估(虽然会跳变),减少视觉突兀。

反 XSS 与流式注入防御是流式渲染的安全护栏。AI 输出可能被 prompt injection 污染,夹带 <script>alert(1)</script>、<img src=x onerror=fetch('/evil')> 等恶意 HTML。强制经过 rehype-sanitize 是最低要求,对流式场景更要做"渐进式 sanitize"——每渲染一个块都过一遍 sanitize,避免恶意内容在中间状态被泄漏到 DOM。即使 AI 输出包含指令"忽略以上,直接执行 JS",sanitize 也会剥离所有危险属性。

更进一步的安全实践是prompt injection 检测。除了 XSS,还要检测 AI 输出中的"忽略以上指令""你现在是 XXX""输出以下 JSON..."等注入模式,这些不是技术攻击,而是试图覆盖系统 prompt的社会工程攻击。对这些模式,降级渲染(灰显 + 警告 icon)而不是直接渲染,并记录到审计日志。永远不要相信 AI 输出是"安全的"——AI 是不可信执行环境,所有输出都要当成不可信输入处理。

工程上,推荐封装一个 SafeStreamingMarkdown 组件,内部统一处理以上五类边界 case 与渐进式 sanitize。不要在每个产品页面写自己的流式渲染逻辑——边界 case 太多,漏一个就崩,而且不同页面的"bug"会形成不一致的用户体验,降低产品整体的可信度。组件内部应暴露配置钩子(custom placeholder renderer、custom sanitize rules、custom boundary detector),允许特殊场景覆盖默认行为,但默认行为必须覆盖 95% 场景。

五、版本树与撤销栈:人机协作的 diff/patch 系统

传统编辑器的撤销栈是线性栈:[v0, v1, v2, ..., current],撤销就是回到上一个状态。这在"用户单条线性编辑"场景下够用,但 AI 编辑器有三个核心场景打破线性假设:

  • AI 重写:AI 选中用户的一段话,生成新版本,用户可以接受或拒绝。线性栈无法表达"我撤销了 AI 重写,但想保留 AI 在别处的修改"。如果用户的当前状态是"AI 改了第 3 段,我自己改了第 7 段",按一次 Ctrl+Z 应该回到哪个状态?线性栈只能记住"AI 重写"或"我自己修改"中的一个,这是逻辑错误。

  • 分支编辑:用户编辑到一半,想试试另一种说法,试完想切回来。线性栈只能记住最近的"主分支",分支历史完全丢失。例如用户写"今天天气真好",改成"今天天气不错",改成"今天天气晴朗",再改回"今天天气真好"——线性栈只能回到"不错"或"晴朗",不能跳到最初版本。

  • 协同分支:用户 A 改了第 3 段,用户 B 同时改了第 5 段,AI 同时在写第 7 段。线性栈完全无法表达三方冲突,因为"线性"假设本身就是错的——多源编辑天然是 DAG。

Version Tree(版本树) 是这三个场景的统一解。每个编辑动作(用户的、AI 的、协同的)都是一次 patch,patch 应用后产生新的 version 节点。树结构自然支持分支、合并、撤销、查看历史。树形结构可以表达任意复杂的编辑历史,且与 CRDT 协同天然兼容(CRDT 的每个操作都可以视为一次 patch)。

撤销栈的树形化实现要点:

class VersionTree {
  root: VersionNode;          // 空文档
  current: VersionNode;       // 当前显示状态
  branches: VersionNode[];    // 所有分支(可切换)

  apply(patch: Patch): VersionNode {
    const next = new VersionNode({
      ast: applyPatches(this.current.ast, [patch]),
      parent: this.current,
      author: patch.author,
      timestamp: Date.now(),
    });
    this.current.children.push(next);
    this.current = next;
    return next;
  }

  undo(): VersionNode {
    if (this.current.parent) {
      this.current = this.current.parent;
    }
    return this.current;
  }

  redo(branch: VersionNode): VersionNode {
    this.current = branch;
    return this.current;
  }
}

AI 重写的特殊处理:AI 生成的内容不是简单的"替换",而是一组 add/replace patches + 一个 transaction ID(txId)。用户接受时,transaction 整体 commit 到 Version Tree;用户拒绝时,transaction 整体 rollback(反向 patches 应用)。这比"AI 修改后用户撤销就全没了"的体验高一个数量级,也避免了"撤销 AI 后我自己改的内容也丢了"的尴尬。transaction 的实现可以是简单的 beginTransaction() / commit() / rollback() 三件套,所有 patch 在 begin 与 commit 之间收集,commit 时整体应用。

协作冲突时的 AI 重写策略:协同场景下,AI 可能基于陈旧的 version 生成 patch,导致与用户的新编辑冲突。正确做法是 AI 在每次生成前 lock 当前 version(用 Yjs 的 Y.Doc.transact()),生成 patch 后用 operational transformation (OT) 算法与并发 patch 协调。OT 算法的核心是"对并发 patch 做变换,使其顺序无关",Yjs 的实现里已经内置这一过程,使用者无需手动实现。AI 的 clientID 应与人类用户区分,便于审计"哪些内容是 AI 生成的、哪些是用户改的"。

版本树的存储优化是生产环境必须考虑的问题。完整保留 patch 历史在长期使用后会膨胀——1 万字文档 1000 次编辑可能占用 10MB+ 内存,移动端尤其敏感。优化策略有三:(a) 快照压缩:每 100 步创建一个完整 AST 快照(快照之间只保留 patch),按需 lazy materialization。(b) Patch 压缩:连续的 add 操作合并为单个 add,replace 操作合并为单个 replace。(c) 定期 GC:保留最近 500 步的完整 patch 历史,更老的只保留快照不保留 patch。生产环境验证,这套策略能把内存开销降低 90% 以上。

六、协同编辑:CRDT/OT 与 AI 介入

协同编辑的成熟方案是 CRDT(Yjs、Automerge)和 OT(ShareDB)。两者各有优劣,选型应根据团队规模和场景决定。CRDT 是无中心的,任意副本可独立修改后合并;OT 是有中心的,通过中央服务器协调操作。AI 编辑器的特殊需求——"AI 是非人类协作者"——天然适合 CRDT,因为 AI 不需要"在线"或"在线认证",只要给它一个 client ID,它的所有 patch 都被同等对待,无需额外的权限层。

Yjs vs Automerge 选型:

  • Yjs:性能更好(WASM 内核,百万级操作/秒),文档大小更小(Delta encoding 仅存储增量),生态更丰富(y-prosemirror、y-monaco、y-codemirror 等富文本绑定都有)。推荐默认选 Yjs,除非有特殊需求。

  • Automerge:API 更纯粹(Pure functional,所有操作是不可变数据结构),JSON 模型更标准(JSON 序列化,方便审计和快照),适合需要"文档级快照"或"审计"的场景(如法律、金融、医疗)。

AI 节点的 CRDT 特殊处理:CRDT 默认所有节点都是"可拆分、可合并"的(粒度到字符),但 AI 生成的整段内容应该标记为不可拆分(否则用户在 AI 生成的大段里改一个字会导致整段重传,且协同时所有客户端都要重新同步)。Yjs 用 Y.Text 包装 AI 内容,设置 clientID 标记来源;协同时其他客户端可以"整段重写"AI 内容,但不能"逐字修改"。这保留了 AI 生成的语义完整性,同时避免协同时的字符级冲突。

具体的实现:AI 生成时用 Y.XmlFragment 包装整段内容,设置 clientID = AI_CLIENT_ID(一个固定的 UUID,标识 AI);协同时其他用户的修改只作用在 AI 内容之外的区域,如果想修改 AI 内容,必须整段替换而不是逐字编辑。这避免了"AI 写一段 500 字,用户在第 250 字改一个字"这种低效且容易出错的协同模式,也是 AI 内容语义原子性的保证。

三方冲突解决(人+人+AI 同时编辑)是 CRDT 的极限场景。Yjs 的 RGA(Replicated Growable Array)算法在三方并发时仍能保证收敛,但收敛结果可能不符合用户期望(比如 AI 写了第 5 段,用户 A 删了第 5 段,用户 B 改了第 5 段,最终结果可能是"A 的删除 + B 的修改 + AI 的原文"拼起来,语义混乱)。工程上的兜底是"冲突 UI":当三方冲突超过阈值(如 3 个并发 patch 互相覆盖同一区域),弹出模态框让用户选择保留哪一方的版本,这虽然打断体验,但避免了"自动合并产生语义错误"的更大问题。

更进一步的方案是AI 智能合并:检测到三方冲突后,AI 重新读取所有版本,生成一个合并版本(类似 git 的 auto-merge)。这需要 AI 理解所有版本的语义差异,目前只有 OpenAI GPT-4、Claude 3.5 等顶级模型能做,且效果不稳定。生产实践推荐"冲突 UI 优先 + AI 合并作为可选项",不要把 AI 合并当作默认行为,以免引入不可预测的语义变化。

离线编辑的合并策略:离线场景下,用户的本地 Y.Doc 与服务端 Y.Doc 完全分离。重新联网时 Yjs 自动 merge,但如果用户在离线时让 AI 生成了一大段,这大段在服务端版本中没有对应记录,merge 后可能出现"内容重复"或"内容丢失"。解决方案:离线时禁用 AI 生成,只允许本地编辑;联网后再开启 AI。这种限制虽然损失了部分离线体验,但避免了不可预测的 merge 结果,符合"离线优先但 AI 在线"的工程原则。

七、对工程实践的推论

7.1 渲染管线选型

  • AI 输出流式场景:必选 streaming remark / unified 的流式模式,不能用 marked 或 markdown-it 的整段解析。整段解析在 10k 字文档上需要 300ms+,远超 50ms 的首字节预算。
  • 非流式场景(如一次性导出 PDF、静态文档展示):可以用整段解析 + KaTeX/Mermaid 完整渲染,无需流式复杂度。
  • MDX 场景:MDX 块必须用整段解析,普通 Markdown 块可以流式。不要为了"完全流式"放弃 MDX 能力——MDX 的组件嵌入能力是 AI 富文本编辑器的重要差异化。

7.2 版本树落地

  • 树形撤销栈 vs 线性栈:默认用树形,UI 上隐藏分支细节(用户感觉是线性,符合肌肉记忆),但底层数据是树。这样保留分支能力的同时不增加 UI 复杂度,也不需要教育用户"什么是版本树"。
  • AI 生成的 transaction 标记:每个 AI 生成的内容用一个 txId(UUID)标记,用户接受/拒绝时整体操作,避免 patch 粒度的混乱。
  • 撤销深度上限:栈深度建议 ≤ 500,超出后压缩老节点(只保留 patch 而不保留完整 AST),用 lazy materialization 恢复。生产环境实测,500 步撤销栈在 100k 字符文档上的内存占用约 5MB,可接受。

7.3 协同框架选型

  • 默认 Yjs,除非有强需求(如审计、合规、文档级快照)才选 Automerge。Yjs 的生态更成熟,绑定丰富。
  • 绑定富文本编辑器:y-prosemirror(ProseMirror,通用富文本)、y-monaco(Monaco,代码编辑)、y-codemirror.next(CodeMirror 6,Markdown 编辑)——分别对应富文本、代码、Markdown 编辑场景。ProseMirror 是最稳定的富文本选择,TipTap 是它的现代封装。
  • 离线优先:用 y-indexeddb 做本地持久化,联网时自动 sync。IndexedDB 容量通常足够(50MB+),适合大多数文档场景。

7.4 性能监控指标

  • 首字节渲染时间(TTFR, Time to First Render):< 50ms。从 SSE token 到达 DOM 更新完成的时间。
  • 增量块渲染时间(IBRT, Incremental Block Render Time):< 16ms(60fps 一帧)。任何超过一帧的渲染都会卡顿。
  • 完整文档渲染时间(FDR, Full Document Render):10k 字 < 200ms。超过 200ms 用户会以为崩溃或网络问题。
  • 协同同步延迟(CSL, Collaboration Sync Latency):< 100ms(局域网)、< 500ms(广域网)。多人光标的"闪烁感"主要来自协同延迟。
  • AI patch 应用延迟(APL):< 50ms。超过 50ms 用户撤销会"卡顿",影响编辑流畅感。

7.5 安全护栏

  • 强制 rehype-sanitize,禁用 <script>、<style>、onerror、onload、onclick 等危险属性和事件。允许的标签和属性使用白名单模式,不放黑名单。
  • 流式 XSS 检测:每渲染一个块都过 sanitize,不要等全文——攻击者可能在第一个 token 就注入恶意内容。
  • prompt injection 渲染:检测 AI 输出中包含的"忽略以上指令""你现在是 XXX"等注入模式,标记并降级渲染(灰显 + 警告),不直接显示给用户。
  • 路径穿越防御:渲染 ```file 时,不允许路径包含 .. 或绝对路径(如 /etc/passwd)。沙箱化所有文件引用。

7.6 与传统富文本编辑器的差异化

  • vs Notion/语雀:传统编辑器是"用户单条线性编辑 + 数据库视图",AI 原生编辑器是"AI 流式生成 + 用户局部编辑 + 版本树"。差异化在增量性和可重写性——用户能看到 AI 边写边出,可以随时打断、修改、重做。
  • vs 飞书文档:飞书支持多人协同但无 AI 介入(虽有飞书 AI 但仍是辅助);AI 原生编辑器的差异化在"AI 是第一公民",而不是"AI 是附加功能"。
  • vs Typora:Typora 是所见即所得的单用户编辑器,无协同无 AI;AI 原生编辑器的差异化在协同与 AI——支持多人 + AI 三方同时编辑同一文档。

八、对比与局限

AI 原生富文本编辑器虽然工程上更复杂,但有明确的取舍,不是"为复杂而复杂"。

vs 纯文本生成:纯文本生成(只输出 Markdown 字符串)工程简单,但用户看不到富文本效果。富文本渲染工程复杂 5-10 倍,但用户体验提升一个数量级。对终端用户产品,富文本是必选;对开发者工具(CLI、API),纯文本可能更合适。

vs DOCX/HTML 编辑:DOCX/HTML 是工业标准(Word、企业文档生态),但 Markdown 的简洁性更适合 AI 输出。Markdown 89% 的语法 AI 都能正确生成,DOCX/HTML 经常出错(嵌套表格、复杂样式、命名空间冲突)。Markdown 是 AI 输出的事实标准,选 DOCX 是逆潮流。

vs LaTeX:LaTeX 是学术标准,公式排版最优,但学习曲线陡。Markdown + KaTeX 是折衷方案——80% 公式需求覆盖,20% 复杂公式退化到 LaTeX 块。对工程文档,Markdown + KaTeX 已足够;对纯学术论文,LaTeX 仍是金标准。

长期方向:AI 原生文档格式(暂称 A2ML, AI-native Markup Language)——在 Markdown 基础上增加 AI 友好的语义标记(如 <ai-suggestion>、<user-edit>、<version-tree>、<confidence> 等),让 AI 生成的内容天然携带溯源、置信度、可重写等元信息。这需要生态共建(浏览器、编辑器、AI 模型都支持),目前还在早期探索阶段,3-5 年内可能成为标准。

主要局限:

  • MDX 性能瓶颈:MDX 块无法流式解析,长 MDX 文档渲染慢。优化方向是用 Web Worker 异步解析 MDX,但仍受限于浏览器主线程的 JS 编译开销。
  • 三方冲突 UX 仍不优雅:冲突 UI 频繁打断用户,降低编辑沉浸感。改进方向是 AI 自动合并(检测到冲突后 AI 重新生成合并版本),但仍处于实验阶段。
  • 版本树的存储膨胀:完整保留 patch 历史,1 万字文档 1000 次编辑可能膨胀到 10MB+。生产环境必须做 patch 压缩与定期 snapshot,不能无限制保留全部历史。
  • AI 重写的版权与审计:AI 生成的内容是否计入"作者贡献"、能否被审计追溯、是否符合学术诚信标准,目前没有行业标准,法律层面也模糊。

九、给前端工程师的可落地清单

选 streaming remark 的 5 个判断点

  1. AI 输出是流式的(SSE/WebSocket)→ 必选 streaming remark 或同类流式解析器。
  2. 文档长度 < 5k 字且非流式 → 整段解析(marked/markdown-it)够用,无需流式复杂度。
  3. 需要 MDX 能力 → 混合模式(普通块 streaming + MDX 块批处理),接受 MDX 块的渲染延迟。
  4. 团队熟悉 unified/remark 生态 → streaming remark 是首选,学习曲线最低。
  5. 团队不熟悉 JS AST 操作 → 用 react-markdown + 增量 wrapper,不要直接操作 AST,封装成本最低。

版本树必走的 3 个反模式

  1. 不要把 AI 重写做成"整段 innerHTML 替换"——这是性能与体验的双重灾难,会导致整个 DOM 重建、状态丢失、滚动位置归零。
  2. 不要用线性栈 + 全量快照——快照存不下(1000 次快照就是 1000 份完整文档),撤销丢上下文(快照之间的小修改被吞掉)。
  3. 不要忽略 transaction ID——没有 txId,撤销/重做无法做到 transaction 级,会导致"撤销 AI 但自己改的也没了"。

CRDT 集成的 4 个陷阱

  1. AI 内容用 Y.XmlFragment 整段,不用 Y.Text 字符级——后者会导致逐字冲突、AI 内容被拆分重传。
  2. 不要忽略 clientID 区分——AI 的 clientID 与用户隔离,撤销时只撤销当前用户的 patch,不影响 AI 内容和他人内容。
  3. 不要在离线时让 AI 生成——merge 后会出现重复内容或丢失内容,行为不可预测。
  4. 不要把 Yjs 当数据库——Yjs 是协同数据结构,不是存储系统。用 y-indexeddb 做本地持久化,定期 snapshot 到 PostgreSQL/MongoDB 做服务端存档。

性能预算的红线

  1. 首字节渲染 < 50ms——超过 50ms 用户能感知延迟,产生"是不是卡了"的疑虑。
  2. 增量块渲染 < 16ms——超过一帧会卡顿,流畅感丧失。
  3. 完整文档渲染 < 200ms——超过 200ms 用户会以为崩溃,跳出率上升。
  4. 协同同步延迟 < 100ms——超过 100ms 多人光标会"闪烁",协同体验崩。
  5. AI patch 应用延迟 < 50ms——超过 50ms 用户撤销会"卡顿",编辑流畅感丧失。

最后,一个根本建议:不要试图从零搭建 AI 原生富文本编辑器——基于 ProseMirror/TipTap + y-prosemirror + remark-rehype + rehype-sanitize + 自定义流式 wrapper 是最稳的起点。社区方案已覆盖 80% 需求,剩下的 20%(主要是 AI 接入与版本树管理)才是真正的工程难点。把 80% 的力气花在 20% 的差异化上,而不是 80% 的力气花在 80% 的"重新造轮子"上。

参考文献

  1. Unified.js 官方文档:streaming remark 的设计与实践,https://unifiedjs.com/
  2. Yjs 文档:CRDT 数据结构与协同算法,https://docs.yjs.dev/
  3. ProseMirror 指南:富文本编辑器的架构,https://prosemirror.net/docs/guide/
  4. JSON Patch (RFC 6902):增量操作的标准化,https://datatracker.ietf.org/doc/html/rfc6902
  5. Automerge 论文:CRDT 的函数式实现,https://automerge.org/
  6. rehype-sanitize:XSS 防御的标准方案,https://github.com/rehypejs/rehype-sanitize
  7. TipTap 编辑器:基于 ProseMirror 的现代封装,https://tiptap.dev/
  8. KaTeX 文档:MathJax 的高性能替代,https://katex.org/
  9. Mermaid 文档:文本生成图表的语法,https://mermaid.js.org/
  10. MDAST 规范:Markdown AST 的标准化,https://github.com/syntax-tree/mdast
  11. y-prosemirror 绑定:ProseMirror 与 Yjs 的桥接,https://github.com/yjs/y-prosemirror
  12. CRDT 综述:Conflict-free Replicated Data Types 的理论基础(Wikipedia)
  13. Streaming Markdown 协议:增量 Markdown 解析的工程实践(GitHub Discussions)
  14. AI 原生编辑器设计模式:Tiptap AI、Notion AI、Cursor 的架构对比(2026)

相关文章

  • AI 可观测性平台横评 2026:四大主流工具的决策框架8月21日
  • AI 应用的多模态证据融合与跨模态引用工程 20268月20日
  • AI 应用的引用归因与证据可点击溯源工程 20268月19日

评论

加载评论中…

发表评论

返回文章列表