一句话定义:流式 Markdown 的半截渲染(partial / incremental Markdown rendering),指模型一边逐 token 输出、前端一边解析并渲染时,收到的往往是语法上尚未闭合的片段,于是同一段内容在几帧之内先以错误形态显示、随后又被“纠正”过来的现象。
半截渲染会乱的原因
Markdown 不是“前缀安全”的语法:一段合法文档的任意前缀,本身未必合法,甚至含义会反转。
打个比方:这就像照着菜谱摆盘,食材一样一样送到,你却在只有面粉的时候就按蛋糕的样子摆好,等鸡蛋到了再推翻重来。渲染器就是那个急性子的厨师。
几个高频出问题的地方:
- 表格:
| 名称 | 数量 |单独一行不是表格,只是一段带竖线的普通文字;必须等|---|---|这行分隔符到齐,解析器才敢确认它是表格。在此之前,用户看到的就是一排歪歪扭扭的竖线。 - 代码围栏(连续三个反引号):围栏还没吐出来时,代码正文先被当成普通段落,里面的
#、*、_会被误判成标题和强调符号;围栏到了之后,整块内容重新归类。反过来,只有开头没有结尾的围栏,多数解析器会把后面全部当代码——这一次“错得刚好”,但要不要依赖这种宽松行为,得看具体实现,以各解析器文档为准。 - 加粗与链接:半个
**或[文字](http先以字面量出现在屏幕上,等收尾符号到了才“啪”地变成粗体或链接。 - 列表:行首的
1还没等到.和空格,先被渲染成普通数字。
四种常见处理策略
| 策略 | 流式期间的观感 | 代价 |
|---|---|---|
| 全程纯文本,结束后再渲染 Markdown | 完全不抖 | 失去“内容正在长出来”的反馈 |
| 每个 chunk 都整篇重新解析 | 格式出现最快,抖动也最明显 | 闪烁、布局跳动、算力浪费 |
| 先补齐语法再解析 | 多数情况下平滑 | 需要容错规则,边界上偶尔猜错 |
| 行级/块级增量解析 | 平滑且省算力 | 实现复杂,块边界要切得准 |
工程上常见的补救手段有几种:给未闭合的围栏补一个“虚拟收尾”,渲染完再撤掉;丢弃行尾孤立的语法字符,如末尾单独的 *、|、[;表格在分隔行到齐前先按纯文本处理,或延迟切换;用节流或按帧合并 chunk,减少重解析次数;预留高度、约束布局,避免抖动传导成整页跳动。
容易混淆的相邻概念
- 打字机效果:把已经拿到的完整答案按固定节奏播放,是“假流式”;半截渲染是边接收、边解析,所以才会乱。
- 增量解析(incremental parsing):是一种实现手段;半截渲染是需要解决的问题。不做增量解析,每次全量重新解析,照样会遇到半截问题。
- 解析器容错:容错针对的是“写错的输入”;半截渲染针对的是“还没写完的输入”,是暂态,不是错误。
- 水合不一致(hydration mismatch):视觉表现相似(先看到 A,再变成 B),成因完全不同。
对实际工作的意义
这是个渲染策略问题,不是模型问题。用户对回答的第一印象,往往取决于前几百毫秒画面是否稳定,而不是最终排版有多漂亮。产品上宁可让表格晚一点出现,也别先给出一堆竖线再跳一下;代码块建议在围栏到齐后再整体切换样式,而不是逐字符改样式。
对普通用户来说,看到“乱一下又好了”,通常不是模型说错了,而是界面在猜;如果同一段内容反复来回跳,多半是渲染器既没做补齐、也没做节流。
