用大模型改写邮件时,如何保留用户原有的排版格式
1807 字约 7 分钟
摘要邮件改写看似只是普通的润色,但用户从 Word 粘贴进来的字体、字号、下划线和表格,经常在改写后丢失。通过三个反例,我放弃了一个即将合并的 PR。
在 Auto Email Sender 中,有一个常用功能是“AI 润色”:用户先用模板写好一封联系导师或客户的邮件初稿,然后让大模型在保留原意的前提下,把措辞改得更得体、更自然。
用户输入的邮件通常不是纯文本,大多是从 Word 或网页复制过来的富文本:中英文字体混排、重点内容标黄加粗、段落首行缩进、超链接,甚至还有复杂的排版表格。
用户的预期很简单:我只是让你修改错别字和语病,不应该顺便把我调整好的格式全部改掉。
我最初的方案是:先去掉样式,把纯文本交给 LLM,拿到改写后的文本,再根据字符偏移量,把原始 HTML 中的字体和加粗标签重新放回去。
用几个简单句子测试时,效果很好,PR 也准备提交给团队合并了。
但是,三个边界情况让这个方案无法继续。
三个反例
消失的两个空格
原文中,Hello 和 world 之间有两个不换行空格( )。
经过“提取文本、LLM 润色、回填样式”的流程后,这两个空格被浏览器的规范化处理合并成了一个普通空格。
视觉上的变化很小,但它说明:在富文本中,空格、空段落和换行本身就是结构的一部分,不只是样式。 一旦在提取纯文本时丢失,之后的回填就无法恢复。
同名不同样式的“AI”
模板正文中出现了两次 AI:
- 第一处是一个小标题,使用的是无衬线字体 A;
- 第二处在正文末尾,使用的是衬线字体 B。
大模型改写之后,句子中仍然有两个 AI。但由于文字变化导致字符偏移量全部错位,回填算法在整篇文档中搜索时,把第二处 AI 错误地匹配到了第一处的样式规则上。
| 原文位置 | 原始字体 | 回填匹配到的样式 | 最终渲染结果 |
|---|---|---|---|
第一处 AI | 字体 A | 第 1 处 | 字体 A(正确) |
第二处 AI | 字体 B | 误匹配到第 1 处 | 字体 A(被改错) |
通俗比喻:拆掉毛衣重织,花纹也就找不回来了为什么“提取纯文本,改写后再贴回原样式”一定会失败? 想象你有一件有花纹的毛衣(带排版的富文本):
- 去掉样式:相当于把毛衣拆成一团纯色毛线(纯文本),交给织工(大模型)修整得更柔顺(改写润色);
- 贴回样式:你希望照着原来的照片,把新毛线原样织回去。但织工在修整时增加了几针、减少了几针(句子长度和词序都变了),原来的花纹位置就对不上了。
文本一旦被 AI 重写,原来的字符位置也就失去了对应关系。
为什么不能用模糊匹配(Fuzzy Matching)来定位?大模型改写的本质是重新组织语言:它可能把两个短句合并成一个长句,删掉多余的修饰词,甚至调换主谓宾的顺序。 用改写后的新文本反推改写前的位置,在逻辑上没有确定的解。文本一旦改变,依靠字符位置和字符比对建立的映射就不可靠了。
Word 私有属性中的中文字体
测试时,有用户从 Microsoft Word 复制了一段包含“微软雅黑”和“宋体”的内容。Word 导出的 HTML 包含大量无关的标签,例如 class="MsoNormal" 和 mso-bidi-font-family。
为了防止这些私有属性污染编辑器,清洗流程把所有带有 mso-* 前缀的内联样式都过滤掉了。英文部分看起来没有受影响,但中文字体信息恰好就存放在这些 Word 私有属性中。样式被删除之后,整封邮件的中文字体都退回到了系统默认的宋体。
什么是 mso 前缀样式?从 Office Word 复制文本时,剪贴板中的 HTML 会包含几十种以
mso-开头的专有样式。其中有些(如mso-pagination)对网页没有意义,但有些(如mso-ascii-font-family和mso-fareast-font-family)记录了西文字体和东亚文字字体的对应关系。一刀切地删除这些属性,容易误伤跨语言的字体设置。
放弃这个 PR
遇到这些问题后,最直接的反应是逐个打补丁:
- 空格被合并?写一个正则保护
; - 重复词错位?加入最长公共子序列(LCS)动态规划算法来计算相似度;
- Word 样式丢失?写一个白名单,挑出需要保留的
mso字段。
但在写这些代码时,我意识到这种做法只会让系统越来越复杂。
每次输入的内容稍有变化,这套依赖启发式猜测的算法就可能出现新的错位。我不能把一个依靠“碰运气猜位置”的系统部署到生产环境。
于是,我关闭了这个包含几十个 commit 的 PR。
重新设计:结构与内容分离
既然不能事后拼接,就应该让大模型从一开始就接触不到样式。
新方案的核心是:结构(HTML DOM、CSS、位置)由程序负责,文字内容由大模型替换。
【旧方案:事后猜测位置】
原始富文本 ──> 提取纯文本 ──> LLM 自由改写 ──> 正则 / LCS 回填(容易出错)
【新方案:锚点占位,结构与内容分离】
原始富文本 ──> 解析为 AST ──> 标记可编辑的锚点 ──> LLM 只改写锚点内容 ──> 按原位置写回- 冻结 AST 结构: 富文本进入系统后,先被解析为抽象语法树(AST)。段落、样式标签、表格和空行节点都被完整保留,不参与文本序列化;
- 为文本设置锚点(Slotting): 只有允许编辑的纯文本叶子节点,才会被分配唯一的锚点 ID,例如
{{slot_1}}、{{slot_2}}; - 模型只负责填空: 发送给大模型的不再是可能被改写结构的富文本,而是一张键值表:
- 输入:
{"slot_1": "尊敬的王教授", "slot_2": "我拜读了您的论文..."} - 输出:
{"slot_1": "尊敬的王老师", "slot_2": "最近研读了您近期的成果..."}
- 输入:
- 按原位置写回: 后端拿到新的文本后,沿着原始 AST 把内容写回对应的位置。字体、加粗标签和 Word 的核心样式都保持不变。
让模型只做它擅长的事大模型擅长生成文本,但对闭合标签、十六进制颜色码(如
#1f2328)和 CSS 层叠规则并不敏感。如果把 HTML 直接交给模型改写,相当于要求它一边写文章,一边手写汇编代码。结构由本地程序保留,语义交给模型处理,系统才会稳定可靠。
总结
富文本保真的问题,本质上是系统架构的边界问题,而不只是前端排版问题。
当为了维护一个功能,需要在流程中加入大量正则、模糊匹配和偏移量补偿算法时,通常说明这个组件承担了不属于它的职责。
放弃那个问题很多的回填 PR,把大模型限定在“只填空、不改结构”的角色中,代码量减少了约三分之二。之后,无论用户从多复杂的 Word 文档中粘贴内容,AI 改写后的邮件都没有再丢失加粗,也没有出现字体错位。