• 请不要在回答技术问题时复制粘贴 AI 生成的内容
1244943563
V2EX  ›  程序员

QuireKit 编辑器的 AI-Native 是如何设计的?谈谈我对真正 AI-Native 的理解

  •  
  •   1244943563 · 20h 27m ago · 1136 views

    先说结论,我个人认为,AI-Native 的编辑器,不在于形式,无论是左编辑器右输入框,还是选中后弹出 AI 润色、续写框,又或是正文下方放类似对话形式的输入框,本质并没有任何不同。在 Tiptap 体系下,只要不是通过命令原生操作,我认为就全都不算 Native 。个人认为真正的 Native ,是模型能原生操作编辑器,不是先生成 markdown 再通过兼容层转为编辑器内容,也不是将 markdown 转为兼容的 HTML 再通过兼容层转为编辑器内容,并且你不需要为每个功能都为模型编写提示词,你只需要一种机制,让模型能够知道编辑器有哪些能力,并且知道这些能力的参数,调用方式就够了,至于如何调用,那就得看模型的调度能力到底几何了。

    下面是 QuireKit Classic 里的一次真实运行:一句话让 AI 把第一行设为标题并居中、改写第二段、把百分比和日期加粗标红,再在文末插入表格。修改先以修订呈现,最后一次批准。演示用的模型是 DeepSeek Flash ,后面两段也一样。

    AI 在 QuireKit 里一句话完成改写、排版和插入表格

    具体的实现方案来自一个之前做的 CLI

    前段时间想要把公司后端的所有接口包装成一套对 AI 友好的 CLI ,让 Codex 、Claude Code 、DSH 、WorkBuddy 等任何 Agent 都能自己学会使用我们的产品。接口有上百个,版本有几十条标准包、几千家定制包,给每个 Agent 写提示词显然不现实,所以我设计了一套自描述的方案,Agent 只需要会三个命令:

    cli api list    --target test --json        # 这个环境能用哪些能力
    cli api explain <id> --target test --json   # 能力的参数 Schema 、风险、认证方式
    cli api call    <id> --target test --input @request.json --json
    

    有哪些接口、参数如何填写,都由 Agent 在运行时向 CLI 询问。这样一来,更换 Agent 不需要做任何修改,新增接口或升级版本之后,Agent 下一次 list 时自然就能知道。

    做 QuireKit 编辑器的 AI 时,面对的其实是同一个问题,只不过对象从成百上千的后端接口换成了成百上千的编辑器命令。

    第一版设计的 AI 输出正确性评测只有 48%

    我最早的做法,是每个插件分别给 AI 注册单独的工具,各写各的说明和提示词。九月底用 20 个日常任务做评测,例如“标题居中”“行距 1.5 倍”“文末加个三列的表”,第一次通过率只有 48.3%。让 AI 分析失败记录后发现,问题基本不在模型:有的能力压根没开放,有的写入失败却回了成功,表格和页眉各有一套定位方式,模型来回尝试,直到轮次耗光,最终以失败结束。

    后来我推翻了让插件各自给 AI 注册工具的方案。仔细想想,这不就跟之前做的 CLI 一样吗?为什么不能让编辑器自己描述自己的能力?于是改成了自描述的方案,定位方式也统一成了一种。同一个模型、同一批任务,通过率到了 98.3%,平均每个任务 1.1 次工具调用,剩下那一次失败也是因为题目本身有歧义。到这里,我感觉应该是对了。下面是重做时改的几件事:

    插件命令自描述的改造

    模型能看到的工具改为固定的几个:读文档、改文档、查帮助。工具叫什么、参数长什么样、出错怎么提示,都由 AI 核心统一定。插件不再专门给模型写工具和提示词来告诉它怎么用,而是只描述自己的情况:节点在视图里怎么显示,有哪些属性能改、取值范围是什么、用哪个命令改。所有开放给 AI 的命令,在定义时都会用 defineCommand 包一层描述:用途说明、参数的 JSON Schema ,以及作用在哪个位置。

    开放给 AI 的命令,插件在 addCommands() 里用 defineCommand 声明。比如书签插件的“加书签”命令:

    addWordBookmark: defineCommand({
      // 给模型看的用途说明
      description: "Adds a bookmark named `name` on `text` in the block (the whole block when `text` is left out), replacing a bookmark of that name. A link to \"#name\" jumps to it.",
      // 作用位置:块里的一段文字
      target: "text",
      // 参数的 JSON Schema
      params: { type: "object", required: ["name"], properties: { name: visibleNameSchema }, additionalProperties: false },
      // 真正执行的命令,和工具栏调用的是同一个
      run: createAddWordBookmarkCommand(startTypeName, endTypeName)
    }),
    

    defineCommand 本身只是一层带类型的包装:它接收一个描述对象,返回的仍然是一个命令函数,调用时原样转给 run;描述被冻结后挂在这个函数的隐藏属性上。Classic 用的核心包里,实现就这几行( MD Editor 用的 MSFE 也有同名的 defineCommand,区别是它直接返回 run 本身,把描述登记在一张以命令函数为键的表里):

    const descriptorKey = Symbol.for("quirekit.command.descriptor");
    
    export function defineCommand(definition) {
      const { run, ...descriptor } = definition;
      // 返回新的函数,调用时转给 run ,对编辑器来说它就是普通命令
      const command = function (...args) { return run.apply(this, args); };
      // 描述挂在不可枚举的 Symbol 属性上
      Object.defineProperty(command, descriptorKey, { value: Object.freeze({ ...descriptor }), enumerable: false });
      return command;
    }
    

    包装之后,插件拿到的仍然是原来的命令函数,工具栏和快捷键照常调用;描述挂在函数的隐藏属性上,AI 核心再从所有命令里把带描述的提取出来,汇总成一张表:

    流程图:命令注册与描述提取

    除了命令,插件还用 addAiCapabilities() 声明格式属性。比如段落对齐,完整声明如下:

    {
      // 能力种类:格式属性
      kind: "format",
      // 模型在文档视图和编辑工具里看到的属性名
      name: "align",
      // 作用在段落上,而不是文字上
      level: "paragraph",
      // 给模型看的含义和取值说明
      description: "Alignment: \"start\", \"center\", \"end\", \"justify\" or \"distribute\".",
      // 合法取值,编辑工具按它校验参数
      schema: { enum: ["start", "center", "end", "justify", "distribute"] },
      // 读:从段落的 Word 属性里取当前对齐方式,没设置时返回 undefined ,表示沿用样式
      read: target => target.level === "paragraph" && target.node.attrs.wordParagraph !== undefined
        ? resolveWordParagraphFormatting(target.node.attrs.wordParagraph).alignment ?? undefined
        : undefined,
      // 写:调用段落插件自己的命令,和用户点工具栏上的“居中”是同一个命令
      apply: ({ commands }, value) => commands.setWordParagraphProperties({ properties: { alignment: value } }),
      // 审阅修改时给人看的名称和取值,不给模型看
      present: {
        label: "对齐",
        value: value => ({ start: "左对齐", center: "居中", end: "右对齐", justify: "两端对齐", distribute: "分散对齐" })[value] ?? String(value)
      }
    }
    

    加一个插件,"改文档"这个工具就多认识一种属性,即多一种操作能力,但模型所会的工具数量不变,已经学会的工具用法也不变。这就是开头说的那种机制:不为每个功能写提示词,只让模型知道有哪些能力、参数是什么、怎么调用。

    比如下面这份简历排版:页边距、字号、加粗、颜色、字距、段落下边框、项目符号、行距和段前段后间距,都是各插件声明的能力。AI 先读取了内置的简历技能,再用一次编辑调用全部改好,最后一次批准。

    AI 一次调用完成简历排版

    写到这里应该能看出来,这跟前面那个 CLI 是一个路子。document_read 相当于 list ,document_help 相当于 explain ,document_edit 相当于 call 。参数写错了,报错会直接说是哪个字段。我们自己的 AI 助手和第三方自己写的 Agent 用的也是同一套工具,没有给自己留后门。

    流程图:QuireKit AI 机制

    确保模型的修改和人工修改走同一套底层命令

    通常情况下,大多数编辑器 AI 生成内容是生成一段 markdown 或 HTML ,再经过兼容层转成编辑器内容替换选区。而由于 markdown 和 HTML 自身的限制,在复杂文档中本就不能完美存储所有有效信息,因此转换必然丢失一部分信息。在 Prosemirror 体系下,HTML 存储 Attrs 还会依赖各种标签属性,当存在大量属性时,生成复杂文档可能还存在出错的问题。

    但是在 QuireKit 里,AI 的每次写入,走的都是跟用户点击工具栏时完全相同的一套底层命令。一次调用里如果有操作不合法,只需退回那几个操作并说明原因,其余照常写入。打开“修订编辑”模式,AI 的修改会记为成修订,这些修订与 Word 修订互通,可在 Word 中逐条通过或拒绝。

    处理复杂格式配置专属命令包

    对于一些特殊格式,我们专门做了相关的命令包,如公文生成。因为即便 AI 可正确操作编辑器,也并不意味着它可以正确理解某个标准。我们最初的想象是美好的,我认为 AI 可正确操作编辑器后,它就拥有了解锁一切文档格式的能力,理论上确实是这样。但实测下来,没有清晰的规则,AI 生成的内容仍需人工持续校对。它可能丢失红头,也可能划线不是红色,又或是字体设置某些地方会有遗漏。

    因此,针对这种随意发挥,我们为编辑器增加了专属命令包以及 skill 。典型的场景就是公文,公文本身有 GB/T 9704—2012 标准要求,因此我们可以将标准封装为不同的命令,再配合公文模板 skill ,即可又快有准地生成公文。

    下面是一份只有正文的通知草稿,一句话排成正式公文:红头、发文字号、红线、标题、版记都由程序按国标生成,模型只负责从草稿里整理出机关、标题、主送、正文层次和抄送这些内容。

    AI 一句话把通知排成正式公文

    Agent 工具确实不能贪多

    通过自描述机制改造过后,其实它撞上的正是社区 Agent 工具设计的理念,Agent 工具设计不应该盲目球多。工具越多,Agent 反而越容易 “变蠢”,一套合理的机制才是让模型发挥最大价值的保障。

    初版的做法是每个插件为 AI 声明注册一个工具,结果是正确性评测只有 48%,当工具数量过多时,模型在相似名称、重叠边界或参数结构相似的工具之间容易犹豫,会导致 “选择困难与误选率上升”。替换为自描述机制后,正确率上升到了 98% 以上,且唯一错误还只是因为输入描述有问题导致。

    参考与使用

    如果你也在给自己的编辑器做智能助手,可以参考 Quirekit 的解决方案是否对自己的产品有用,如果直接用 QuireKit ,那这些都已经做好了:

    • 对于 classic-editor ,我们有 @quirekit/classic-ai(目前暂时为商业包,但可在官网使用,只需配置自己的模型 key 即可),它通过侧边栏助手提供使用入口,并配备模型设置和技能库。
    • 我们也通过 MIT 开源了可以直接使用的 @quirekit/ai-runtime,它是 @quirekit/classic-ai 的底层核心,classic-ai 只是在其之上做了一层包装,将其封装为了可用成品。runtime 已包含 document_read、document_help 以及 document_edit 核心工具机制,任何人均可用它封装自己的 AI 助手。
    • 对于 Markdown 编辑器的,对应的是 @quirekit/md-editor-ai。
    • 针对 QuireKit 写插件,可参考 @quirekit/ai-contract 里的类型声明格式能力,开放给 AI 的命令用 defineCommand 包一层描述,AI 就能用上,不用写提示词。

    除了 Classic AI ,其他 AI 相关的包最近都改成了 MIT 开源,已经发到 npm 的 alpha 版。接入文档在 https://docs.quirekit.com 。

    后续也长路漫漫

    回头看,我们花在模型身上的功夫其实并不多,大部分时间在给编辑器设计合理的工具与机制:让模型通过编辑器自己的命令操作文档,用一种机制让它知道有哪些能力和参数,程序能保证的结构不交给它,然后用评测盯着。这些都做好以后,其实一个便宜的快模型也就够了,目前大多数模型处理文档的能力都非常强了,文中三段演示用的都是 DeepSeek Flash V4.1 ,换成更强的模型结果也一样。虽然已经取得了一部分进展,但其实 AI 的坑目前还有很多,很多犄角旮旯的操作仍有限制,因此长路漫漫,优化仍在进行。


    最后 QuireKit 是我在做的开源文档编辑器框架,有 Word 式的分页编辑器 Classic-editor 、块编辑器 Block-editor 和 Markdown 编辑器框架 MSFE 以及成品 md-editor ,Classic 和 MD Editor 用的是同一套 AI 核心。现在还是 alpha ,block-editor 也会是 classic 同一套,目前正在迁移适配。


    本文首发并持续更新于 yishuai.wang。

    Supplement 1  ·  18h 46m ago
    全文无 AI ,捍卫署名权,AI PTSD 的可以出门右转
    9 replies  •  2026-11-03 11:47:03 +08:00
    laved
    laved
    19h 13m ago❤️1
    1
    开头就是先说结论,差点没绷住,AI 味有点溢出了吧
    1244943563
    1244943563
    OP
    19h 2m ago
    2
    @laved 又来,纯手工,全文无 AI ,不服来战,博客那篇已经闹够了
    1244943563
    1244943563
    OP
    18h 57m ago
    3
    @laved 后续关于这种话题我不再回复,为了避免博客那次评论区骂战风波,我已经单独出过一篇帖子。再那篇帖子中已经截图出本文的初版草稿。如果再有人污蔑,我不会再做任何回复。本篇文章为了避免再有人说是 AI ,我连润色都没做。全文纯手工。你觉得 AI 你可以不看。此楼下方,不会再做任何骂战,我没精力。
    gitlight
    gitlight
    18h 39m ago
    4
    能力设计很重要,要保持简洁,又要尽量让各个工具之间的能力正交
    1244943563
    1244943563
    OP
    18h 25m ago
    5
    @Livid 此贴邀请站长见证,不希望再被移动到 AI 生成分类。本文纯原创,AI 工具都没过,甚至存在一个错字我没审计出来,个人博客迁移帖子个人已经被重伤,为了避免此类事件再次发生,本篇全文手写,AI 顺句也没做。上一篇贴已有本篇草稿的截图。我不想卷入任何纷争,我只想做好我的开源项目跟生态,以后的帖子即便带有 AI 顺句我都会增加附言做声明。楼下还有 AI PTSD 的话,自行离开。

    希望本帖为纯技术交流,目前个人致力于开源富文本框架 quirekit 的开发,维护百万行代码即便有 AI coding 也不是一件易事,当前社区几乎没有此类一样效果的开源编辑器,我个人需要很多精力 focus 到自己的项目跟工作上,没有任何精力再来骂战。而且我不认为我需要再辩解。
    dikcen
    dikcen
    17h 26m ago
    6
    好奇提两个问题:在有比较复杂格式时候(下标、尾注、表格内对齐),如何实现 AI 操作和人看到的一致,并且没有隐藏格式(比如⬇、回车换行符)或符号;不同质量模型接入差别大吗
    1244943563
    1244943563
    OP
    16h 51m ago
    7
    @dikcen prosemirror 体系下,无论是否是复杂格式,其实都是包了 mark ,模型不去自己操作具体的 json ,这样会因为幻觉导致复杂内容操作失误,失败率极高,人类的操作逻辑是,选中内容,点击上标,点击加粗,点击斜体,不关心具体 prosemirror json 怎么变。AI 调用工具后也是这样。选中内容后,直接调用工具操作,减少失败可能。目前不同质量的模型在生成内容的质量上我觉得存在差异,工具调用上可能也有,不过我测试的不是很大,当然我用的是 Qwen 3.8-Max 、deepseek v4 与 v4.1 (略低端) 以及 gpt 跟 claude 的顶尖模型。没有太过明显的差距。甚至 deepseek 中文能力会更强,有些情况下做的还更好。更低端的模型没有尝试,但是目前不允许模型直接修改复杂的文档结果是可以明显缩小这个差别的。只要会调用工具,选区选择正确,基本上是没问题的。
    1244943563
    1244943563
    OP
    16h 48m ago
    8
    @1244943563 打错字了,是模型如果自己操作具体的 json ,这样才会因为幻觉导致复杂内容操作失误。这个错误率超级高,内容越复杂,错误率越高。改成工具调用,由程序操作,就会非常稳定。
    1244943563
    1244943563
    OP
    16h 47m ago
    9
    @gitlight 是的,能力设计越多,乱操作的风险也越大。
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   1319 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 20:34 · PVG 04:34 · LAX 12:34 · JFK 15:34
    ♥ Do have faith in what you're doing.