Skip to main content
LLM 天然会生成 Markdown 格式的文本,包括标题、列表、代码块、表格和内联格式。将这些内容渲染为纯文本会浪费模型提供的结构。此模式展示了如何在代理流式传输时,实时解析和渲染 Markdown,并支持所有主流前端框架。

Markdown 渲染的工作原理

渲染管道包含三个步骤:
  1. 接收: useStream 将流式文本累积到每条 AI 消息的 msg.text 中,并在新令牌到达时响应式更新。
  2. 解析: Markdown 解析器将原始文本转换为 HTML(或 React 元素树)。每次更新都会运行此步骤,但对于聊天长度的内容(5 KB 消息 < 5ms)来说足够快。
  3. 渲染: 解析后的输出被渲染到 DOM 中。React 使用虚拟 DOM 差异比较;Vue 和 Svelte 使用经过净化的 v-html / {@html} HTML。

设置 useStream

Markdown 模式使用一个简单的聊天代理,无需特殊配置。使用您的代理 URL 和助手 ID 连接 useStream 导入您的代理,并将 typeof myAgent 作为类型参数传递给 useStream,以便对状态值进行类型安全访问:

选择 Markdown 库

每个框架都有一个自然的 Markdown 渲染选择:
React 的 react-markdown 将 Markdown 直接转换为 React 元素,因此不需要 HTML 净化。不涉及 dangerouslySetInnerHTML。对于 Vue、Svelte 和 Angular,在渲染前始终使用 dompurify 净化解析后的 HTML。

构建 Markdown 组件

净化 HTML 输出

当将解析后的 Markdown 渲染为原始 HTML(v-html{@html}[innerHTML])时,必须净化输出以防止跨站脚本攻击(XSS)。LLM 响应可能包含任意文本,包括 Markdown 解析器可能转换为可执行 HTML 的标记。 使用 dompurify 来剥离危险元素:
DOMPurify 会移除 <script> 标签、onclick 属性、javascript: URL 以及其他 XSS 向量,同时保留安全的 Markdown 输出,如标题、列表、代码块、表格和链接。
React 的 react-markdown 不需要 dompurify,因为它直接生成 React 元素,不涉及原始 HTML 注入。

流式传输注意事项

useStream 会在每个令牌到达时响应式地更新 msg.text。Markdown 组件在每次更新时都会重新解析。对于典型的聊天消息,这是性能良好的:
  • marked 的解析速度约为 1 MB/s。一个 5 KB 的消息需要 < 5ms
  • react-markdown + remark 管道对于聊天长度的内容同样快速
  • 浏览器的布局引擎能高效处理 DOM 更新
对于非常长的响应(> 50 KB),请考虑以下优化:
  • 节流渲染: 使用 requestAnimationFrame 以 60fps 批量更新,而不是在每个令牌到达时重新渲染
  • 增量解析: 仅解析新内容并追加到已渲染的缓冲区(高级功能,通常聊天 UI 不需要)
对于大多数聊天应用,每次令牌到达时重新解析完整消息的简单方法就足够了。只有在处理非常长的消息时观察到滚动卡顿或掉帧,才需要进行优化。

样式化 Markdown 内容

将样式应用于 .markdown-content 类以控制渲染的 Markdown 的外观。以下是基本样式:
为聊天气泡保持 Markdown 样式紧凑。聊天消息比博客文章小,因此使用比典型散文样式表更紧凑的边距和更小的字体大小。

最佳实践

  • 始终净化: 当使用 v-html{@html}[innerHTML] 时,始终将解析后的输出通过 dompurify 处理。永远不要信任来自使用 LLM 输出的 Markdown 解析器的原始 HTML。
  • 启用 GFM: GitHub 风格的 Markdown 添加了表格、删除线、任务列表和自动链接。这些功能是 LLM 常用的。
  • 处理空内容: 在解析前检查空字符串,以避免渲染空容器。
  • 使用 breaks: true 启用换行符转换,以便 LLM 输出中的单个换行符渲染为 <br> 而不是被忽略。LLM 经常使用单个换行符进行视觉分隔。
  • 为聊天上下文设计样式: 使用紧凑的边距和适合聊天气泡的大小,而不是全宽的文章布局。
  • 使用丰富内容进行测试: 验证标题、嵌套列表、长行代码块、宽表格和块引用的渲染,以发现溢出或布局问题。