Heurion Research

为什么自由 Shell 智能体在科研论文写作中会失效:双表面 CRDT 与字节级 OOXML 补丁架构

告别破坏性整篇覆写与排版坍塌:在单一 ProseMirror + Yjs 真相源上构建人类富文本与智能体结构化原语的对偶投影、Run 级 OOXML 补丁导出与 0.128ms 单轮靶向回滚。

01. 范式错配:为什么不能让 Agent 写脚本直接改论文文件?

在过去两年里,以 Claude Code、Cursor 与各类 Coding Agent 为代表的“自由 Shell 执行 + 直接文件改写(Script-and-Audit)”范式在软件工程领域取得了巨大成功。面对纯文本代码(如 TypeScript、Rust 或 Python),Agent 可以随时调用 python3 -c 或正则表达式替换文件,再通过编译器与单元测试兜底。

然而,当我们把这套“把文档当成平面文件覆写”的范式照搬到严肃的学术科研手稿(Research Manuscripts)与同行评审论文撰写——无论是计算机系统、生命科学、物理学还是社会科学——自由脚本范式都会遭遇两道无法逾越的工程鸿沟:

  • OOXML 结构坍塌与排版漂移(Formatting Drift):全球绝大多数期刊投审稿、基金申请书与跨机构合作重度依赖 Microsoft Word(.docx)与 PowerPoint(.pptx)。一个真实的 .docx 段落内部往往交织着期刊自定义样式(w:pStyle)、原生数学公式(m:oMath)、复杂交叉引用域(w:fldChar begin/separate/end)、书签范围以及合著者的修订留痕(w:ins / w:del)。当 Agent 使用 python-docx 或 Pandoc 重新序列化文档时,模型无法理解的 XML 子树会被大面积剥离或重置。
  • 人机并发共编时的静默覆写与全量重吐浪费:科研人员在审阅 AI 初稿时往往会边读边改。基于文件覆写的 Agent 在长耗时推理(30–90 秒)结束后将文件写回磁盘时,会直接冲掉人类作者在此期间做出的全部手动修改;更糟糕的是,为了改一个 40 字的段落而让大模型重吐整篇 6,000 字的 Markdown,不仅浪费 99% 以上的输出 Token,还让“仅撤回上一轮 AI 修改但保留人类后续打字”变得不可能。
核心设计公理(Design Axiom)

在高严肃度科研与长文档工程中,永远不要把原始文件(Raw File)暴露给自治智能体直接覆写。必须将论文手稿抽象为带生命周期稳定寻址({#id})的确定性状态机(CRDT),在内存中以结构化算子完成精准局部修改,并在导出时对底层 OOXML 执行最小颗粒度的字节补丁。

02. 双表面对偶投影:同一棵 ProseMirror + Yjs 状态树,两套正交视图

为了同时满足“人类需要所见即所得的富文本与多人实时协同”与“大模型需要高信噪比、低 Token 开销的确定性块寻址”,我们在单一 ProseMirror + Yjs CRDT 状态树之上构建了双表面对偶投影(Dual-Surface Projection):

HUMAN SURFACE · WYSIWYG 富文本手稿 & 幻灯片画布 • y-prosemirror 实时协同 • 隐藏底层 {#id} 与 XML 属性 • 产生 userEditedIds 实时锁 • 行内锚定批注线程 {#c:id} SINGLE CRDT STATE MACHINE ProseMirror PMNode + Y.Doc • 块级稳定属性 attrs.id = "p8f2" • O(1) 哈希索引 indexById(doc) • 行内原子 citation / word_inline • Turn 前后快照 base / current AGENT SURFACE · ANNOTATED MD 紧凑结构投影 & 算子接口 • serializeBlock(n, {ids:true}) • 仅暴露 {#id} + Markdown 语义 • applyOps(): 6 类靶向修改算子 • 节省 99.6% 单轮输出 Token OOXML PATCH EXPORTER (patchParagraph) Clean 块字节级直拼 + Dirty 块段落内 Run 级 LCS 补丁 SELECTIVE TURN REVERT (revertByNodes) 0.128ms 逆向恢复 AI 修改块,自动跳过 userTouched 块
图 1:双表面 CRDT 对偶投影(Human Surface vs. Agent Surface)及其与底层 OOXML 补丁导出器、单轮逆向回滚器的协同关系。

2.1 内部状态树与两套表面的具体编码对比

在底层文档模型的 Schema 定义中,每一个可寻址节点(heading、paragraph、table、figure)都在其属性字典中持有一个跨编辑周期恒定的短 ID(由字母前缀加无碰撞伪随机字符组成,例如 h3a1、p8f2、tbl1)。而在段落内部,文献引用(citation)、动态统计测量值(measurement)与不可拆解的原生 Word 对象(word_inline,如 OMML 公式或复杂域)均被建模为行内不可分割原子节点(Inline Leaf Atoms)。

下面展示同一个段落节点在三层空间中的精确形态:

// (A) 内存中的 ProseMirror + Yjs 状态树节点 (PMNode JSON)
{
  "type": "paragraph",
  "attrs": { "id": "p8f2", "style": "Normal", "align": "justify" },
  "content": [
    { "type": "text", "text": "Primary endpoint events occurred in 16.3% vs. 21.2% of subjects (" },
    { "type": "measurement", "attrs": { "m_id": "m_hr01" } },
    { "type": "text", "text": "; P < 0.001) " },
    { "type": "citation", "attrs": { "cite_id": "ref_2019" } },
    { "type": "text", "text": "." }
  ]
}

当智能体调用 doc_read 读取该章节时,结构化投影函数 serializeBlock(node, { ids: true }) 将其无损投射为紧凑的 Agent Surface(带块级锚点与行内评论的扩展 Markdown):

{#h3a1} ## 3. Primary Efficacy Outcomes

{#p8f2} Primary endpoint events occurred in 16.3% vs. 21.2% of subjects ([m:m_hr01]; P < 0.001) [@ref_2019].
  > [Comment {#c:rev1}]: 请补充次要终点住院率的风险比说明。

{#tbl1}
| Endpoint | Treatment (n=2373) | Control (n=2371) | Hazard Ratio (95% CI) | P value |
| --- | --- | --- | --- | --- |
| Primary composite | 386 (16.3%) | 502 (21.2%) | 0.74 (0.65–0.85) | <0.001 |

2.2 结构化修改算子(applyOps)与 userEditedIds 实时防撞

当智能体根据上述批注 {#c:rev1} 修改段落 {#p8f2} 时,它不需要重吐整个章节或表格,只需向算子执行器 applyOps(doc, ops, { taken, userEditedIds }) 发送一条仅 20 余个 Token 的类型化指令:

// 智能体发出的结构化算子 (DocOp)
{
  "op": "replace_text",
  "scope": "p8f2",
  "find": "([m:m_hr01]; P < 0.001) [@ref_2019].",
  "replace": "([m:m_hr01]; P < 0.001) [@ref_2019], driven by a 30% reduction in recurrent hospitalizations."
}

在 applyOps 内部,算子执行器通过以下三步保证绝对的确定性:

  1. O(1) 块寻址与跨格式范围定位:通过 indexById(tr.doc).get("p8f2") 直接定位目标块在文档树中的精确偏移量 pos,并在块内构建展平文本切片(将 citation 映射为 [@cite_id]、measurement 映射为 [m:m_id]),使得 find 即使跨越粗体、斜体或行内引用标记也能精确命中对应的 ProseMirror 坐标区间 [from, to]。
  2. 行内格式自动继承(Inherited Marks):当用新文本替换原区间时,replace_text 自动读取被替换首字符的样式标记(inheritedMarks,如原有的字体、字号或超链接),让 AI 改写后的文字天然融入周围排版。
  3. 人类编辑行级锁(userEditedIds):若在 AI 思考期间,人类作者正在前端编辑器中手动修改 {#p8f2},前端协同网关会将 "p8f2" 标记进当前回合的 userEditedIds 集合。此时任何试图覆写该块的 AI 算子都会在 <0.2 ms 内被拒绝并抛出 USER_EDITED_CONFLICT,彻底杜绝静默覆写。

03. 100% 字节级 OOXML 树拼接与段落内 Run 级 LCS 补丁(patchParagraph)

双表面 CRDT 解决了内存中的协同问题,但科研论文最终必须交付为期刊系统要求的 .docx 文件。如果导出 .docx 时把内存里的 ProseMirror 树简单地全量序列化成新的 XML,那么原文档里那些 ProseMirror 模型无法完备表达的复杂 Word 特性——如期刊页眉页脚、奇偶页分节符、OMML 数学公式(<m:oMath>)、EndNote/Zotero 复杂域代码(w:fldChar)、书签(w:bookmarkStart)以及合作者留下的修订留痕(<w:ins>)——就会在一次导入导出后荡然无存。

为此,我们在底层文档引擎中实现了一套两级字节保真 OOXML 补丁导出架构(Two-Level Byte-Preserving OOXML Patch Exporter):

IMPORTED .DOCX 1. 原字节包 zipArchive 2. 块级原始 XML 字典: rawXmlById.get("p8f2") 3. 导入基线快照: beforeNode.get("p8f2") 4. 现态节点 afterNode LEVEL 1 · CLEAN BLOCK (after.eq(before)) 未修改块:100% 原始 XML 字节流直拼 outXml += rawXmlById.get(id) // 零解析损耗 LEVEL 2 · DIRTY BLOCK (patchParagraph) • 拆解原 <w:p> 为 RNode 树 (keep/atom/box/run) • 字符+样式签名 LCS 对齐 align(old, now) • 原样保留 m:oMath 公式、w:fldChar 域、书签 • 冻结他人修订容器 <w:ins> (frozen: true) EXPORTED .DOCX • 未动块 100.0% 字节恒等 • 改动块内公式/域零丢失 • 合作者修订归属不串味
图 2:两级 OOXML 导出流水线——块级 Clean Splicing 与段落内 Run 级最小差分补丁(patchParagraph)。

3.1 第一级:未修改块(Clean Node)的 100% 原始字节流直拼

当一份 .docx 被导入时,解析器不仅生成 ProseMirror 节点,还将原始 word/document.xml 中每个顶级块(<w:p>、<w:tbl>)的原始 XML 字符串按块 ID {#id} 缓存在原件映射表中,并保存导入时刻的基线模型快照 before。导出时,只要某个块满足 after.eq(before)(即人机均未修改该段),导出器直接将该块对应的原始 XML 字节切片原封不动地写回 word/document.xml。这意味着整篇论文中 95% 未被触碰的段落与原文件实现严格的 100.0% 字节恒等(Byte-Identical)。

3.2 第二级:已修改块(Dirty Node)的段落内 Run 级 LCS 补丁算法

更具挑战性的问题是:如果某个段落内部既包含复杂的 Word 原生数学公式(<m:oMath>)、书签和合作者的 Track Changes 修订(<w:ins w:author="Co-Author">),而 AI 恰好调用 replace_text 改了这同一段里的几个词,怎么保住该段内其余的复杂 XML?

在段落内补丁导出器中,patchParagraph(raw, before, after, render) 实现了一套精巧的段落内 Run 级最小补丁算法:

  1. 将原段落 XML 解析为四类结构节点(RNode):
    • keep:非文本控制节点(如段落属性 <w:pPr>、书签起止 <w:bookmarkStart/>、批注范围标记),无条件原样保留;
    • atom:复杂域(从 w:fldCharType="begin" 到配对的 end 跨多个兄弟元素打包为一个原子)、原生数学公式(<m:oMath>)与脚注引用(<w:footnoteReference>);
    • box:超链接(<w:hyperlink>)、内容控件(<w:sdt>)以及冻结修订容器(<w:ins> / <w:moveTo>,标记为 frozen: true);
    • run:普通文本运行(<w:r>),拆分为样式属性 rPr 与字符序列 parts。
  2. 带格式签名的字符级 LCS 对齐与语义清理(Semantic Cleanup):算法将导入时的模型 before 与当前模型 after 分别展开为带格式签名的记号序列 sig = ch + "|" + markSig,运行最长公共子序列(LCS)动态规划 align(old, now),并执行类似 diff-match-patch 的语义清理(将夹在两处大改动之间的零碎短匹配并入改动区,防止把一个英文单词拆得支离破碎)。
  3. 最小侵入式写回与修订容器冻结保护:
    • 若某个原始 <w:t> 内的所有字符在 keep[] 中均为 true 且无新字符插入,直接输出原 <w:t> 原始 XML;
    • 若在某个 <w:r> 中插入了相同格式的新文字,直接写进当前 run(并自动剥离旧的加粗/斜体增量后继承原 run 的字体、字号、语言等底层 w:rPr 属性,效果等同于人类直接在 Word 中打字);
    • 对于 frozen: true 的合作者修订容器(<w:ins>),算法强制校验容器内字符未被篡改,并将紧贴容器边界的新增文字精准排斥在 </w:ins> 容器之外——绝不把 AI 新增的文字伪造成合作者的修订记录。
<!-- 补丁前:原段落包含期刊自定义样式、合作者修订 <w:ins> 与原生 OMML 公式 -->
<w:p w14:paraId="3A8F12B0">
  <w:pPr><w:pStyle w:val="JournalBody"/></w:pPr>
  <w:bookmarkStart w:id="0" w:name="_Ref1042"/>
  <w:r><w:rPr><w:rFonts w:ascii="Times New Roman"/></w:rPr><w:t>The estimator converges at rate </w:t></w:r>
  <m:oMath><m:r><m:t>O(n^{-1/2})</m:t></m:r></m:oMath>
  <w:ins w:id="4" w:author="CoAuthor">
    <w:r><w:t> under weak regularity</w:t></w:r>
  </w:ins>
  <w:r><w:rPr><w:rFonts w:ascii="Times New Roman"/></w:rPr><w:t> in our simulation.</w:t></w:r>
  <w:bookmarkEnd w:id="0"/>
</w:p>

<!-- 经 AI replace_text("in our simulation.", "across all 1,000 Monte Carlo replications.") 后,
     patchParagraph() 导出的 XML:w14:paraId、pStyle、书签、m:oMath 公式与 CoAuthor 修订 100% 完好保留! -->
<w:p w14:paraId="3A8F12B0">
  <w:pPr><w:pStyle w:val="JournalBody"/></w:pPr>
  <w:bookmarkStart w:id="0" w:name="_Ref1042"/>
  <w:r><w:rPr><w:rFonts w:ascii="Times New Roman"/></w:rPr><w:t>The estimator converges at rate </w:t></w:r>
  <m:oMath><m:r><m:t>O(n^{-1/2})</m:t></m:r></m:oMath>
  <w:ins w:id="4" w:author="CoAuthor">
    <w:r><w:t> under weak regularity</w:t></w:r>
  </w:ins>
  <w:r><w:rPr><w:rFonts w:ascii="Times New Roman"/></w:rPr><w:t xml:space="preserve"> across all 1,000 Monte Carlo replications.</w:t></w:r>
  <w:bookmarkEnd w:id="0"/>
</w:p>

04. 0.128ms 单轮 AI 靶向回滚:块级逆变换与人类修改优先(revertByNodes)

在真实科研写作中,一个常见场景是:AI 在第 T1 回合修改了 {#p01}、{#p02} 并新增了 {#p03};随后人类作者在第 T2 时刻顺手润色了 {#p02} 并在 {#p09} 写下了新的结论;到了第 T3 时刻,作者决定“撤回第 T1 回合的 AI 修改”。

如果是传统的线性 Undo 栈或 Git Revert,撤回 T1 要么会连带把人类在 T2 对 {#p02} 和 {#p09} 的修改一起冲掉,要么直接报合并冲突失败。而在块级逆变换引擎中,我们利用块级寻址实现了延迟仅 0.128 ms、且严格遵循“人类修改绝对优先(Human-Edits-Win)”的靶向回滚函数 revertByNodes(current, base, changes, userTouched):

// 单轮块级靶向回滚核心算法精简展示
export function revertByNodes(
  current: PMNode,
  base: PMNode,
  changes: Array<{ node_id: string; kind: 'added' | 'modified' | 'removed' }>,
  userTouched: Set<string>,
): { doc: PMNode; skipped: string[] } {
  const baseIndex = indexById(base)
  const ids = new Set(changes.map(c => c.node_id))
  const skipped: string[] = []
  const tr = new Transform(current)

  for (const id of ids) {
    // 1. 人类在该回合之后改过的块直接跳过(人类修改优先,绝不倒退人类工作)
    if (userTouched.has(id)) { skipped.push(id); continue }
    const now = indexById(tr.doc).get(id)
    const before = baseIndex.get(id)

    if (now && !before) {
      // 2. 该回合 AI 新增的块 (#p03) → 精准删除该块区间
      tr.delete(now.pos, now.pos + now.node.nodeSize)
    } else if (now && before && !now.node.eq(before.node)) {
      // 3. 该回合 AI 修改过且人类未再触碰的块 (#p01) → 换回回合前基线节点 before.node
      tr.replaceWith(now.pos, now.pos + now.node.nodeSize, before.node)
    }
  }
  // 4. 该回合被 AI 删除的块 → 寻址原前序兄弟锚点 (anchor.pos + anchor.node.nodeSize) 原位放回
  return { doc: tr.doc, skipped }
}

回到刚才的例子:执行 revertByNodes 后,{#p01} 瞬间恢复为 T1 之前的原貌,AI 新增的 {#p03} 被干净移除,而人类在 T2 亲手润色过的 {#p02}(在 skipped 列表中返回提示)与 {#p09} 则完好无损地保留在文档中。

4.1 综合性能与保真度实测对比

评测维度 (Metric) 自由脚本覆写 (python-docx / Bash) 全量 Markdown 重写基线 Heurion 双表面 CRDT + OOXML Patch
未修改 OOXML 节点字节保留率 31.4%(样式/域代码漂移) 0.0%(全量重生成) 100.0% (Byte-Identical)
修改段落内公式 (m:oMath) 与他人修订保留率 0.0%(重写段落时丢失) 0.0%(Markdown 无此语义) 100.0% (Run 级 LCS 补丁)
人机并发编辑防覆写保护 0.0%(静默覆盖人工修改) 0.0%(整篇替换) 100.0% (userEditedIds 块锁)
局部修订单次输出 Token 消耗 ~1,850 tokens (脚本生成) ~6,400 tokens (整篇重吐) ~26 tokens (-99.6%)
跨人类编辑后的单轮 AI 靶向回滚耗时 不支持(需整文件还原) 不支持(连带冲掉人工修改) 0.128 ms (revertByNodes)

通过将双表面 CRDT 投影、段落内 Run 级 OOXML 补丁导出与块级靶向逆变换三者闭环结合,我们第一次让自治智能体具备了在不破坏任何原始排版与人类协作心血的前提下,深度参与严肃学术手稿共编的底层工程能力。

读者评论

0 条留言

暂无留言,欢迎在下方分享你的见解或技术讨论。