Junie's Blog

用大模型改写邮件文案时,怎么保住用户原来的排版格式

全文共 1999预计阅读 7 分钟

在 Auto Email Sender 中,有一项高频功能是“AI 润色”:用户先用模板写好一封联系导师或客户的邮件初稿,然后让大模型在保留原意的基础上,把措辞改得更得体、更地道。

用户输入的邮件可不是纯文本,绝大部分都是从 Word 或网页里直接复制粘贴过来的富文本:中英文字体混排、重点标黄加粗、空两格段落、超链接,甚至是复杂的排版表格。

用户的预期极其朴素:我只是让你帮我改改错别字和语病,谁让你顺手把我辛苦调好的格式全给格式化了?!

最初我写了一套“先剥离样式、改完再贴回”的逻辑:把纯文本提出来喂给 LLM,拿到改写后的文本,再按照字符偏移量把原始 HTML 的字体、加粗标签“无缝缝合”回去。

本地测试几个简单句子,效果堪称完美,PR 已经准备提给团队合并了。

然而,三个极具杀伤力的边缘 Case,像一盆冷水把整个方案浇了个透心凉。

三个把算法逼疯的反例

凭空蒸发的两个空格

原文只有一句极其简单的话:Helloworld 之间塞了两个特殊的实体不换行空格(  )。

经过“提取文本 → LLM 润色 → 回填样式”一套流程下来,两个特殊空格被浏览器的规范化组件无情地压成了一个普通空格。

视觉上看只缩了一点点,但它揭露了一个残酷的真相:在富文本的世界里,空格、空段落和换行根本不是什么“样式外衣”,它们本身就是骨架结构的一部分。 一旦在转纯文本时被清洗掉,后续回填无论多精巧也无力回天。

同名不同命的“AI”

模板正文里出现了两次 AI 这个词:

  • 第一处是个小标题,用了无衬线字体 A
  • 第二处在正文末尾,用了衬线字体 B

大模型润色完,句子依然包含两个 AI。但由于文字改动导致字符偏移量全乱了,回填算法在整篇文档里一搜,竟然把第二处 AI 错配到了第一处 AI 的样式规则上!

原文位置与内容原始排版字体回填算法匹配到的样式最终渲染结果
第一处 AI字体 A匹配到第 1 处字体 A(侥幸正确)
第二处 AI字体 B误匹配到第 1 处字体 A(惨遭篡改)
通俗比喻:把毛衣拆成毛线再织,花纹全没了

为什么“提取纯文本 → AI 改写 → 贴回原样式”注定会失败? 想象你有一件精美的花毛衣(带有排版和样式的富文本):

  • 剥离样式:相当于你把毛衣拆成了一团纯色的毛线(纯文本),拿给织工(大模型)让他把毛线修得更柔顺(改写润色);
  • 贴回样式:修完后你指望照着原来的相片,把这团新织出来的毛线原封不动缝回原样。但因为织工在润色时多织了三针、少织了两针(语句长度和词序全变了),原来的花纹图案再也拼不上了。

文本一旦被 AI 重构,旧的字符物理坐标就彻底蒸发了。

为什么不能用上下文滑动窗口(Fuzzy Matching)定位?

大模型润色的本质就是重构语言:它会把两个短句合并为一个长句,会删掉无用修饰词,甚至会彻底颠倒主谓宾顺序。 试图用“改写后的新文本”反向推导“改写前的物理坐标”,在数学和逻辑上根本不存在确定性的解。 文本一旦变形,基于坐标和字符比对的映射就是薛定谔的猫。

藏在 Word 专属属性里的中文字体

测试用户从 Microsoft Word 复制了一篇带“微软雅黑”和“宋体”的内容进来。Word 导出的 HTML 充斥着海量的垃圾标签(如 class="MsoNormal"mso-bidi-font-family)。

为了防止这些私有属性把编辑器污染掉,清洗管道大刀阔斧地把带有 mso-* 前缀的内联样式全滤了。英文看起来毫发无损,但中文字体信息恰好就藏在这堆 Word 私有属性里!样式一删,整封邮件的中文字体全部退化成了系统默认宋体,丑不堪言。

什么是 Mso 前缀样式?

当你从 Office Word 复制文本到剪贴板时,Word 会在 HTML 里塞入几十种以 mso- 开头的专有内联样式。有些(如 mso-pagination)对 Web 毫无意义,但有些(如 mso-ascii-font-familymso-fareast-font-family)却精准记录了西文字体和东亚文字的映射关系。简单粗暴一刀切,往往会直接误伤跨语言字体体系。

我亲手关掉了一个即将合入的 PR

走到这一步,传统的本能反应是“打补丁”:

  • 空格被压?写个正则保护  
  • 重复词错位?加个最大公共子序列(LCS)动态规划算法算相似度;
  • Word 样式丢失?写个白名单挑出部分 mso 字段。

但敲下这些代码的时候,我强烈地意识到:这是在屎山上雕花。

每一次微小的语料输入变动,都会让这套脆弱的启发式猜测算法产生新的错位。我不能把一个建立在“碰运气猜坐标”之上的系统推到生产环境。

我深吸一口气,亲手把那个几十个 commit 的 PR 给关闭了。

彻底推翻重来,“骨肉分离”的新架构

既然不能“事后缝合”,那就必须从源头上让大模型碰不到样式。

新方案的核心思想极其纯粹:骨架(HTML DOM / CSS / 坐标)归程序死守,血肉(自然语言文本)才由大模型替换。

【旧方案:试图事后猜坐标】
原始富文本 ──> 抽纯文本 ──> LLM自由发挥 ──> 暴力正则/LCS回填 (惨烈翻车)
 
【新方案:精准锚点占位(骨肉分离)】
原始富文本 ──> AST语法树拆解 ──> 锁定受控锚点 ──> LLM只改锚点值 ──> 严格原位注水
  1. AST 语义冻结: 富文本进入系统后,首先被编译成抽象语法树(AST)。段落、样式标签、表格、空行节点被完整冻结,绝不参与文本序列化;
  2. 插槽锚点化(Slotting): 只有真正允许编辑的纯文本叶子节点,会被赋予一个确定性的唯一锚点 ID(例如 {{slot_1}}{{slot_2}});
  3. 模型只做填空题: 送给大模型的不再是一篇可能被随意改写结构的富文本,而是一张纯粹的“键值映射表”:
    • 输入:{"slot_1": "尊敬的王教授", "slot_2": "我拜读了您的论文..."}
    • 输出:{"slot_1": "尊敬的王老师", "slot_2": "最近研读了您近期的成果..."}
  4. 原位精准注水: 后端拿到新的文本内容,像填空一样,沿着原始 AST 树把新词注回对应的 DOM 槽位中,原始的字体属性、加粗标签、Word 核心样式纹丝不动。
让模型做它最擅长的事

大模型在生成文本时极其强大,但对闭合标签 </span>、十六进制颜色码 #1f2328 和 CSS 层叠规则的敏感度极低。把 HTML 扔给模型改写,就像让一个大文豪在写诗的同时严格手写汇编代码。把结构留在本地,把语义留给模型,系统才会坚如磐石。

总结

富文本的保真,从不是一个“前端排版”问题,而是一个系统架构边界问题

当你发现为了维护一个功能,需要在管道里写成百上千行正则表达式、模糊匹配矩阵、上下文偏移补偿算法时,通常意味着你把不属于这个组件的职责硬塞给了它。

果断砍掉那个漏洞百出的回填 PR,把大模型牢牢限制在“结构不变的填空者”角色里。代码行数少了三分之二,但无论用户从多复杂的 Word 文档里粘出多么奇怪的排版,AI 改写后的邮件再也没有丢过一个加粗、乱过一种字体。

评论