AI生成的文本可能含有零宽空格、非断行空格、曲引号等不可见字符,会静默破坏搜索匹配、复制粘贴、表单提交和简历解析,并给出检测清理方案。
你把 AI 对话中的一段话粘贴到求职申请里。看起来很干净,提交了。
但你看不到的是,有几个字符跟着一起混了进去:一个零宽空格夹在两个单词之间、一个不换行空格替代了本该出现的普通空格、引号变成了花体弯引号、破折号键盘上打出来的是一个字符但显示的是另一个完全不同的字符。这些都看不见,但确实都在那儿。
大多数时候这没什么大碍。但有时候它会静悄悄地搞破坏:搜索匹配不上、复制粘贴出来的是个小方块而不是字母、表单毫无原因地拒绝你的输入、或者招聘系统把你的简历解析成一团乱码。近来这类字符又有了新的曝光度,因为同样的隐藏字符出现在了 AI 水印和隐藏文本的相关讨论中。
我在做自己的求职工具时频繁遇到这个问题,于是写了一个清理工具。以下是这背后的真相,以及我的处理方法。
计算机里的文本不只是你看到的那些字母。它是一串字符流,其中有不少字符本身就是设计成不可见或几乎不可见的。它们的存在有其合理原因:用来连接单词、控制间距、支持某些语言需要的字符。问题出在它们悄悄混进你的文本时你毫不知情。
几种常见的混入途径:
复制粘贴。从网页、PDF 或聊天窗口复制内容,往往会把那些本来不该离开该页面的格式控制字符一并带走。
智能格式化。很多工具会"贴心"地把直引号变成弯引号、把两个连字符变成长破折号。印出来确实好看,但放到表单字段或代码框里就不一定受欢迎了。
AI 写作工具。AI 助手生成的文本往往带有它自己的排版风格:特定的间距、特定的标点、以及偶尔出现的不可见分隔符。
普通人为什么要关心这个?因为你的文字最关键的那些场景,往往也是最挑剔的:
招聘系统会把简历当作纯文本读取。一个多余的隐藏字符可能把一个单词拆开、隐藏掉关键词、或者把一行内容弄乱,而你永远不知道为什么没收到回复。
搜索和表单按精确字符匹配。花体引号和直引号不是同一个字符,所以搜你的名字或职位标题时可能悄无声息地漏掉。
渲染问题。把带有奇怪字符的文本发给一个不支持它的应用,读者看到的就是方块或问号而不是应有的字母。
这并不稀奇。就等于面试时袖口上还贴着价格标签。很小、你自己看不见,但值得在别人发现之前清理掉。
如果你关注 AI 新闻,大概见过两个相关报道。
一个是水印:AI 生成的文本可以携带隐藏信号,有时通过不可见字符实现,以便日后识别。另一个是用隐藏文本 smuggling 指令,把不可见字符藏在文档或网页里,悄悄影响读取它的 AI 系统。两者都是真实存在的,都值得了解。
我要明确说明这篇文章不是什么。它不是规避水印或躲过 AI 检测的指南。我认为那是错误的目标,而且说实话也是一场必输的游戏。我的论点更简单、更古老:你应该能看见自己写的东西里的每一个字符,你应该能把它干净利落地交付出去。干净的文本是便携的、专业的、可预期的。无论不可见字符是来自聊天工具、复制粘贴还是水印,处理方法都一样,原因也一样。你是作者,文件里放什么由你决定。
剥离不可见字符也不会真正击败水印。真正被读作机器写的,是节奏——每句话长短形状都一样,再怎么清理字符也救不回来。这是关于干净、诚实的文本,不是伪装。
叫它文本卫生吧。和拼写检查是一个精神,只不过检查的是你看不见的那些字符。
上面写的对大多数读者来说就是全部重点了。如果你想了解技术细节,如下。
值得关注的字符大致分几类:
零宽字符。零宽空格(U+200B)、零宽非连接符(U+200C)、零宽连接符(U+200D)以及单词连接符(U+2060)。这些完全不占可见空间,这也正是它们容易被忽视的原因。
字节顺序标记 / 零宽不换行空格(U+FEFF)。经常在复制文本的开头搭便车。
不换行空格和特殊空格。不换行空格(U+00A0)是最常见的一种。还有一整族特殊空格(thin、hair、figure 等),位于 U+2000 到 U+200A 范围。
智能标点。花体单引号和双引号(U+2018、U+2019、U+201C、U+201D),以及自动格式化工具喜欢插入的半破折号(U+2013)和全破折号(U+2014)。
标签字符(U+E0000 到 U+E007F)。一个冷僻的字符区段,已经成为所谓 ASCII smuggling 的载体——看起来正常的文本里藏着一个不可见的 payload。
清理逻辑在原理上不复杂:剥离永远不该出现在纯文本中的字符、把有明确纯文本对应物的字符规范化、其余合法字符保持不动。库暴露了几个层级,让你可以根据文本要去的地方匹配清理的激进程度。用 npm install ai-text-hygiene 安装,然后:
import { clean, stripInvisible, cleanConservative } from 'ai-text-hygiene';
// 全面 house-style 清理散文:剥离不可见字符、把弯引号转为直引号、
// em dash 转为逗号、省略号转为三个点。
clean('We "delivered" results — on time…');
// => 'We "delivered" results, on time...'
// 仅剥离层级,适用于任何语言(不改动标点)。
const zwsp = String.fromCharCode(0x200B); // 一个零宽空格,输入中不可见
stripInvisible('in' + zwsp + 'visible');
// => 'invisible'
// 长度稳定层级,适用于有字符限制的字段:一对一替换,不增长。
cleanConservative('curly "quotes" to straight');
重要的设计选择在于你不剥离什么。规范化是一种判断:把弯引号转成直引号是安全的、在表单字段里几乎总是你想要的,但你不会想把依赖某些字符的合法内容扁平化——那些字符可能被过于激进的过滤器误伤。这就是为什么仅剥离层级是对任何语言都安全的,而完整清理针对的是英文 house style。真正的工作在于选择好要移除的集合与要规范化的集合,并且每次都按可预期的方式执行。
在我的项目里,这套逻辑放在一个独立模块(text-hygiene-core)中,这样应用发出的所有文本都适用同一套规则,同时也作为开源库以 MIT 许可证独立发布。如果你想阅读实际代码或集成到自己的工具里,仓库在这里:
ai-text-hygiene on GitHub
我最初没打算写一个 Unicode 清理器。我做的是 trajecktory,一个把求职当 pipeline 来跑的工个,而文本卫生是那种不起眼、没什么噱头但悄悄重要的小问题。每一份简历、每一封求职信、每一条从应用发出的消息都应该是干净、朴素、确确实实是我写的。没有隐藏的东西,没有我看不见的东西。
如果你曾经疑惑为什么表单拒绝了看起来完全正常的文本,这往往就是原因。现在你看到了,也能清理掉了。
trajecktory 是开源的,而且还在持续迭代。如果这篇文章对你有用,代码在 GitHub 上,我会持续写关于做这个项目的过程。