
流式 Markdown 渲染
编写于 2026-08-13
模型流式输出 Markdown 表格时,表头往往先于分隔行到达。只有表头的那一帧会被解析成普通段落,分隔行到达后,同一段旧文本才变成表格。Peri 曾复用前一帧的段落缓存,结果完整表格一直显示为竖线原文。
增量渲染缓存保存已经解析和转换的稳定前缀,只处理后续新增文本。它不能把输入当作普通字符串追加,因为新后缀会改变旧前缀的语法结构。
后缀会改变旧块结构
Markdown 解析器需要表头和分隔行同时存在,才能识别表格。第一帧只有表头时,抽象语法树里得到普通段落。第二帧补充分隔行与数据后,原来的段落会被替换成表格块。
旧缓存只比较已处理块数量。前后两帧都可能只有一个块,数量相同却不代表类型与内容相同。缓存跳过这个块后,旧段落保留,新的表格没有机会转换。
只在分隔行到达后局部替换渲染结果不可靠。类似翻转还会出现在水平分隔线、列表延续和未闭合代码块中。缓存需要表达结构是否稳定,而不是为单个 Markdown 语法写界面补丁。
只缓存稳定前缀
当前转换状态会记录已处理段落中是否存在潜在表头。段落首行以竖线开头时,系统不把这段前缀视为可安全复用。下一帧到达后会执行全量转换,解析器因此能够重新判定块类型。
已经形成的表格也不会盲目复用。追加数据行会改变同一个表格块,缓存检测到稳定状态中含表格后,同样让该部分失效。这里选择了正确性优先,没有把动态表格强行拆成可增量追加的局部结构。
缓存还会在持久化前回滚尾部不稳定块。未被空行闭合的段落、标题、列表项、代码块和分隔线都可能随下一段输入变化,只有更早的稳定块继续复用。这个规则比检查输入是否以换行结束更接近 Markdown 的块级语义。
布局状态进入缓存键
同一份 Markdown 在不同终端宽度下会得到不同换行和表格列宽。缓存若只比较文本,用户缩放终端后仍可能复用旧布局。当前缓存同时比较文本前缀、终端宽度和颜色主题。
极窄终端还暴露了另一类一致性问题。列宽计算和渲染缓冲区必须使用同一套宽度钳制规则,否则列宽仍要求写入多个单元格,实际缓冲区却更窄,最终发生越界。局部给某个数值加下限无法保证两侧一致。
宽度变化时重新计算是布局正确性的要求。仓库没有为这次修复提供可用于文章的帧率或吞吐对比,不能把缓存策略写成已实测的性能提升。
用全量解析验证缓存路径
最关键的回归测试先解析单独表头,确认它还是普通段落,再追加分隔行和数据。测试要求缓存路径产生表格,并与同一完整文本的全量解析结果相同。
另一组测试逐帧追加 Markdown,将每一帧的缓存输出与全量解析比较。宽度变化、颜色变化、列表增长、代码块增长、CJK 表格和窄屏表格也有独立覆盖,保证缓存路径与全量路径使用同一结果标准。
这些用例固定了当前解析器与渲染器的行为。升级 Markdown 解析库后仍要重新运行,因为块边界与中间态可能变化。不同终端对字符宽度的处理也可能带来仓库测试没有覆盖的显示差异。
限制与边界
增量缓存只处理当前解析器能够识别的块级稳定性,遇到潜在表头、现有表格或未闭合尾块会回退到更多重算。仓库没有帧率与吞吐基准,因此可验证结果只包括缓存路径和全量解析路径一致。
终端字符宽度仍受具体终端实现影响,依赖升级也可能改变块边界。相关变更需要重新运行逐帧、宽度变化、CJK 表格和窄屏用例。
开头那个竖线表头不再被当作永久稳定前缀。分隔行到达后,旧段落会重新参与解析,最终画面与全量 Markdown 结果保持一致。