中文加粗为什么会露出星号:Markdown 的标点边界与 AI 输出
最近让 Codex 把一个约 100 GB 的本地 AI 项目迁到另一块硬盘。任务结束后,它给了一段完成说明:项目已经从新位置启动,模型和视频预览正常,旧目录还需要清理。
说明里的列表、链接都能正常显示,偏偏最后一句想要加粗的提醒露出了星号。

这种问题很容易被当成偶发的界面毛病。内容是通顺的,星号也成对出现了,为什么只有这一小段格式失效?我此前在一段关于中文 Markdown 加粗的公开讨论里也追问过:能不能让模型换个写法,能不能让 App 容错,或者干脆输出 HTML?
这些方案对应的是不同层的责任。要判断哪一个有效,得先看看这两个星号到底有没有被解析器当成结束标记。
一个冒号就能改变结果
把提醒缩成最小例子,保留关键的字符关系:
**还差 C 盘清理:**自动审批策略在遵循 CommonMark 这组强调规则的解析器里,这段文字不会生成预期的加粗。把冒号移到双星号外面,就能正常解析:
**还差 C 盘清理**:自动审批策略正常解析时,对应的 HTML 是:
<strong>还差 C 盘清理</strong>:自动审批策略两种写法的文字没有变化,只是加粗范围是否包含冒号不同。失败的那种写法,结束侧的 ** 左边是标点 :,右边紧接汉字 自。这个组合会影响它能否结束强调。
截图能证明星号被显示出来,却不能可靠地证明字符间有没有隐藏空格,也不能证明产品内部用了哪一个解析库。这个最小例子用于解释可复现的语法机制,不是对 Codex 或 ChatGPT 内部实现的鉴定。
Markdown 从来不只是成对替换
Markdown 用普通文本表达标题、列表、链接和强调。写下 **重点**,解析器识别出强强调,再把它交给网页或客户端显示。读者习惯叫它「加粗」,语法层面通常对应 HTML 的 strong 元素。
解析器 负责把字符识别成段落、列表、强调等结构。界面负责把这些结构显示出来;文字是否采用粗体,还受到字体和样式影响。
因此,加粗失效也有两种不同的检查方向:已经识别成强调、只是样式不明显,和根本没有识别出强调、星号仍作为文字显示。截图属于后者的表现,首先应该检查原始文本和解析结果。
Markdown 最初由 John Gruber 在 2004 年发布。它的原始语法说明强调易读、易写,但不少边界情况没有被完整规定。不同实现可能把同一段文本解析成不同结果。CommonMark 从 2014 年开始推动更明确的规范和测试用例,目的正是减少这种分歧。CommonMark 的项目介绍说明了这段背景。
这里有三个容易混用的名字:
| 名字 | 所指的对象 |
|---|---|
| Markdown | 原始格式及其生态,也经常被用作这一类轻量标记语法的统称。 |
| CommonMark | 对 Markdown 语法进行细化、消除歧义的规范,包含强调的边界和嵌套规则。 |
| GFM | GitHub Flavored Markdown,在 CommonMark 基础上增加表格、任务列表等扩展。 |
GFM 的强调规则同样包含这里相关的边界判断。支持表格和任务列表,并不意味着中文标点旁边的加粗会自动获得另一套解释。
截至 2026 年 10 月 8 日,CommonMark 规范索引列出的最新正式版本仍是 0.31.2,发布日期为 2024 年 1 月 28 日。产品可以采用扩展或自己的处理方式,所以只知道「支持 Markdown」,还不足以预测所有输入的表现。
结束星号为什么不算结束
人看到一对 **,自然会想把中间整段文字加粗。解析器还需要判断每一组星号有没有资格开始或结束强调,因为同一字符既能开头,也能结尾,还可能参与嵌套。
分隔符串 是连续的一组
*或_。CommonMark 用 left-flanking 和 right-flanking 描述它与相邻字符的关系,再判断它能否开始或结束强调。这里的「左右」指文本顺序中的相邻字符。
结束侧要看两边的字符
对这个简单的双星号例子,CommonMark 的 right-flanking 定义可以转成如下判断:
- 结束侧星号的前一个字符不能是空白。
- 前一个字符如果不是标点,通常能满足结束侧的边界条件。
- 前一个字符如果是标点,后一个字符就需要是空白或标点;行尾在这个判断里按空白处理。
这只是结束侧的条件,完整强调还要有合格的开始侧和有效配对。CommonMark 0.31.2 在这里使用的 Unicode 标点定义包含 P(标点)和 S(符号)两类,范围比日常所说的逗号、句号更宽。
回到失败的例子,结束侧的局部关系是 :**自:前一个字符是标点,后一个字符既不是空白,也不是标点。这组星号不满足结束条件;前面的开始标记找不到预期的结束标记,整段就可能保留为普通文本。
冒号移到外面后,局部关系变成 理**:。星号前是普通汉字,结束侧的条件成立。

前两行比较 :**自 与 理**:,后两行比较 是**( 与 (**重。这里只解释局部边界资格,不替代完整强调配对规则。
开始侧也会失败
开始侧有相应的 left-flanking 判断:紧接星号的内容不能先是空白;如果先是标点,还需要检查星号之前的字符。把一对括号或引号一起包进加粗,就可能碰到两端的边界问题。
这是**(重点)**后文开始侧是 是**(,结束侧是 )**后。两边都是普通文字与标点隔着星号相邻,这段写法无法按预期加粗。更稳的写法是把括号放在强调范围外:
这是(**重点**)后文括号可以出现在强调内容中,但是否能这样写,仍取决于两端上下文。整段独立写成 **(重点)**,和嵌在连续正文中,不是同一种边界条件。
空格放在哪里,结果完全不同
星号外侧的空格可以帮助建立边界,星号内侧的空格却可能让标记失去资格。
| 原始文本 | 本地解析结果 | 影响结果的条件 |
|---|---|---|
**重点。**后文 |
不加粗 | 结束星号前是标点,后是汉字。 |
**重点。** 后文 |
加粗 | 结束星号后有空格。 |
**重点。** |
加粗 | 结束星号位于输入末尾。 |
**重点。**,后文 |
加粗 | 结束星号后也是标点。 |
**重点。 **后文 |
不加粗 | 空格在强调内部,紧挨结束星号。 |
** 重点**后文 |
不加粗 | 空格在强调内部,紧挨开始星号。 |
这是**重点**后文 |
加粗 | 强调两端都是普通文字,双星号允许这种词内强调。 |
这组结果分别在 commonmark 0.31.2 的 JavaScript 参考实现和博客现有的 marked 15.0.12 上验证过,运行环境为 Node.js 25.8.1。表里的结果用于核对这些输入,不代表每个聊天 App 都使用相同解析器和配置。
这也说明,「中文标点不能加粗」把条件说得太宽了。真正影响结果的是标记两端的空白、标点和普通文字组合。
为什么中文更容易遇到
比较这两段英文:
**Alert:** Next
**Alert:**Next第一段能加粗,第二段也会失败。中文冒号换成英文冒号,并没有自动解决问题;第一段有效,是因为结束星号后还有一个空格。
中文正文通常不会为了分词在相邻词语之间插入空格。把包含冒号、句号的短句加粗后继续写正文,很自然就会形成 标点 + ** + 汉字。英语的常见书写习惯更容易在句间留下空格,于是同一规则造成的摩擦没有那么显眼。
这是一种语法与书写习惯的冲突,不需要归因到中文编码异常,也与显卡、模型推理硬件无关。日语里也有类似问题。相关扩展经常用 CJK 指称中、日、韩文本的兼容处理,但三种语言的空格习惯并不完全相同。
换成下划线也不是通用办法。在这次测试中,__重点__后文 没有加粗。CommonMark 对 _ 还有不同于 * 的词内强调限制,不能把两套标记当成完全等价的替换选项。
规则这么绕,为什么还没有统一改掉
如果强调标记像 HTML 那样分成明确的开始标签和结束标签,配对会容易许多。但 Markdown 的 *、** 同时承担开头和结尾。遇到连续星号、强调嵌套和普通文本里的星号时,解析器必须选择一种解释。
例如下面是一段用于说明嵌套的文本:
**重要:其中有 *特别说明*,然后回到主句。**这里既有双星号,也有单星号。完整的强调规则需要处理它们之间的配对关系;单纯找到下一个同样的标记,还不能覆盖所有歧义。边界规则帮助解析器作出确定判断,也会把一部分人看来很自然的写法排除掉。
这个问题已经有人持续关注。CommonMark 的 Issue #650:Emphasis with CJK punctuation于 2020 年 5 月 26 日提出,最初就展示了日语句号等标点在强调内部、后面紧接正文时失败的现象。2023 至 2024 年的讨论涉及已有文档兼容性、嵌套强调,以及究竟哪些 Unicode 字符应该获得 CJK 特殊处理。维护者也实际尝试过修改参考实现。截至本次查阅,这个 issue 仍为 open。
困难并不只在改一条判断。放宽规则后,原来当成普通文字的星号可能变成强调,已有的嵌套可能得到不同结构;字符范围选得太窄,又会漏掉全角括号、罕见汉字或混排内容。规范变更还需要由各解析器和产品采用。
所以,「有人提出修复」「有解析器扩展」和「所有产品统一改变行为」是三个不同的进度。不能从某个 App 仍然露出星号,推断没有人在做这件事,也不能凭截图替厂商解释优先级。
AI 为什么还会输出这种写法
在 Markdown 聊天界面里,回答通常要经过两个不同步骤:模型生成文本,产品再解析和显示文本。生成出来的字符串能够表达正确意思,不等于它在目标解析器里也能产生预期格式。
模型可能把整段提醒连同冒号一起包进 **。这种写法表达了强调提醒标签的意图,却可能不符合目标解析器的边界规则。若输出没有经过针对目标渲染器的检查,结束标记旁边的正文就可能让它失效。
这是对输出链路的解释,不是对某个模型训练数据或产品内部校验流程的断言。截图只足以支持可见现象,不能确认某次回答为什么选了这个字符序列。
工程上可以分别处理:
| 处理位置 | 能做什么 | 剩下的限制 |
|---|---|---|
| 输出要求 | 要求模型只加粗关键词,把两端标点放在外面。 | 属于生成约束,不能当作每次都满足的保证。 |
| 文本检查 | 用目标解析器验证结果,检查预期强调有没有生成。 | 要知道作者意图;合法的普通星号不能一概报错。 |
| 解析器 | 采用 CJK 友好扩展,容忍这类常见写法。 | 需要明确方言和兼容边界,复制到其他平台可能不同。 |
| 产品界面 | 提供预览、原文查看、复制和反馈入口。 | 显示、复制和导出最好使用一致的处理策略。 |
对大量接收 AI 或用户内容的应用,我更倾向于把显示兼容性放进产品测试。要求每个用户记住一套分隔符规则,成本会反复落到写作和阅读上。
使用者和开发者分别能做什么
个人无法直接更换 ChatGPT 的解析器,但可以要求模型优先采用稳妥的加粗写法。应用开发者还能修改文本检查和解析流程,直接处理这类兼容问题。
普通使用者:缩小加粗范围
最容易迁移到不同平台的习惯,是只强调真正需要突出的词语,把边缘标点放在外面:
**还差 C 盘清理**:旧目录仍然存在。
这是(**重点**),需要保留。不要在 ** 内侧紧挨标记的位置留下空格,也不必为了让中文加粗而在整篇正文中强行增加空格。标点确实需要一起强调时,应该在目标平台预览该写法。
ChatGPT 可以通过自定义指令表达这类输出偏好。在设置中的个性化入口填写,也可以先在当前对话里提出要求,例如:
中文回答使用 Markdown 时,优先只加粗关键词。
把加粗范围两端的冒号、句号、逗号、括号和引号放在双星号外。
双星号内侧不要紧贴空格;不要依赖 HTML 标签来加粗。这是针对常见失败组合的保守写作策略,不是 CommonMark 的完整语法说明。它可以减少这种输出,但不能代替实际渲染检查;普通用户的自定义指令也不会自动改写产品的解析规则。
写作者:在发布前检查失败的强调
对自己维护的 Markdown 文档,可以接入 textlint 的 @lemonadern/textlint-rule-no-broken-cjk-emphasis。它检查普通文本中未能解析成强调的星号,能够报告这里的中文冒号、句号和括号边界问题,并跳过代码示例与转义星号。
这是一条启发式规则,数学记号、通配符和复杂组合仍可能误报或漏报。常见 markdownlint 的内置规则不会自动覆盖这组问题,不能把通用 Lint 通过等同于所有强调都符合作者意图。
textlint 的工作机制、规则配置,以及存量文章扫描中遇到的真实问题和误报,另见给 Markdown 文档接入 textlint:加粗、术语和异常字符检查。它适合在写作和提交阶段提供反馈;应用显示层的兼容处理仍要结合实际解析器评估。接入时遇到的依赖安装故障,另见pnpm 为什么装到了旧版本。
开发者:已有 CJK 友好扩展可以评估
能改自己应用的 Markdown 链路时,可以评估现成的 markdown-cjk-friendly 扩展项目。它提供 markdown-it-cjk-friendly、remark-cjk-friendly 等不同生态的实现,接入位置取决于应用实际使用的解析器,不能只按界面框架的名字选包。
这类扩展调整的是强调边界判断。扩展规范还定义了相关 CJK 字符和标点范围,涉及 Unicode 属性;它并不是用一个只匹配常见汉字的正则表达式补几个例外。
我在独立测试环境里比较了 markdown-it 15.0.2 的 commonmark 预设,以及同一实例配置启用 markdown-it-cjk-friendly 3.0.0 后的结果:
| 原始文本 | 未启用扩展 | 启用扩展 |
|---|---|---|
**还差 C 盘清理:**自动审批策略 |
不加粗 | 加粗 |
**重点。**后文 |
不加粗 | 加粗 |
这是**(重点)**后文 |
不加粗 | 加粗 |
**重点。 **后文 |
不加粗 | 不加粗 |
**Alert:**Next |
不加粗 | 不加粗 |
这次对照只改变了是否启用扩展。结果说明它能够解决所测的中文标点边界问题,也保留了部分原有约束。扩展采用的是另一套兼容规则,不应写成 CommonMark 0.31.2 已经要求所有实现如此处理。
接入前还需要把应用自己的内容加入回归测试:中文冒号和句号、括号与引号、中英混排、单星号与双星号嵌套、链接、行内代码、代码块、转义星号,以及必须按普通文字保留的标记。局部样例成功,不足以证明所有历史内容都不会改变。
如果选择自动移动标点,也要理解 Markdown 的上下文。全局正则替换很容易改到代码示例、转义文本,或者错配嵌套标记。可以利用能保留源码位置的解析流程定位候选区域,再按明确规则处理;但失败的强调往往只会成为普通文本节点,不能假设解析后一定存在一个 strong 节点可供修复。
原始内容应保留。显示时做过兼容处理的话,复制和导出也需要明确是原文还是处理后的内容,否则页面看着正常,粘贴到文档又会露出星号。
HTML 标签:先确认平台允许
原始 Markdown 的确允许混入 HTML,最初的语法文档就有说明。在允许并正确处理行内 HTML 的环境中,下面这种写法能直接表达明确的强调范围:
<strong>还差 C 盘清理:</strong>自动审批策略它绕开了双星号的边界判断,但聊天 App 可以禁止、转义或过滤原始 HTML。语法支持 HTML,不等于产品会把所有标签交给浏览器执行。
例如,react-markdown 的 HTML 说明明确区分了默认转义、通过 skipHtml 忽略,以及额外接入原始 HTML 处理的方式。这里引用的是一个公开组件的行为,不代表 ChatGPT 或 Codex 使用它。
因此,让模型统一输出 <strong> 或 <b>,只在已经验证目标平台支持时才有意义。自建应用如果开放 HTML,也应该继续限制允许的标签、属性和 URL,而不是为了加粗关闭内容安全措施。
反馈问题:把失败输入缩到一行
比起只说「中文加粗经常坏」,下面这种反馈更容易判断问题落在哪一层:
原始文本:**重点。**后文
期望结果:重点和句号加粗,后文保持普通文字。
实际结果:双星号直接显示。
对照文本:**重点**。后文反馈时再补上产品、平台、版本和截图。自己开发的应用还应记录解析器版本、启用的插件,以及完整响应结束后的结果。流式输出尚未补齐结束标记时的临时显示,与完整文本仍无法解析,要分别检查。
如果连 **重点** 这样简单的对照都不能正常显示,就应进一步检查内容是否被整体当成纯文本、星号是否被转义、是否位于代码块中,以及界面的样式。标点边界只是其中一个明确且常见的原因。
回到这句提醒
对截图里的提醒,我会采用 **还差 C 盘清理**:……。它保留中文正文的自然排版,也避免把结束标记放在标点与后续汉字之间。
对自己维护的聊天界面,CJK 友好扩展和目标解析器的回归测试更值得投入。模型能减少容易失败的写法,产品也可以接住常见的中文输入;这两件事可以同时做,不必等到所有 Markdown 实现采用同一套规则。