我叫它“大模型返回值的最后一公里”,也是很多大模型应用从 demo 走向产品时绕不开的一道坎:前后端都在等大模型吐出漂亮的 Markdown,但用户的浏览器只认 HTML。如果你正在开发 AI 聊天助手、智能报告生成器、知识库问答系统,或者任何需要把大模型输出直接展示在 Web 页面里的应用,这篇文章就是给你准备的。
我不会讲太多抽象理论,重点放在工程实现路径上:为什么大模型偏爱 Markdown、业界最常用的渲染方案有哪些、真实项目里怎么把 marked + DOMPurify + highlight.js 串起来,以及流式输出(SSE)场景下那些“一改就崩”的细节。文章里所有代码和配置都来自我实际跑过的项目,你拿过去改改就能用。
1. 内容整体设计与思路拆解
1.1 为什么大模型“只说”Markdown,产品却“只认”HTML
现在主流的大模型 API,无论是 OpenAI、Anthropic 还是国内各家开源模型,默认输出格式基本都是 Markdown。这个选择非常聪明:Markdown 是一种轻量标记语言,纯文本就能表达标题、列表、表格、代码块、加粗、斜体等富文本结构,token 消耗小,解析容易,对训练数据也没那么敏感。
但问题来了,用户的浏览器不认 Markdown。如果你把一段 ## 标题 直接塞进 <div>,页面只会老老实实显示两个井号。用户不关心底层格式,他们只看到“排版乱得像接口文档”。
所以工程上必须有一层转换:把大模型吐出来的 Markdown 字符串,解析成浏览器能渲染的 HTML DOM。这个转换器的选型、配置、安全处理,就是整个大模型应用工程化里最容易被低估但实际最影响体验的部分。
我见过不少团队,前期 demo 阶段直接用前端 Markdown 库渲染,效果惊艳;一上生产环境,不是 XSS 漏洞被打穿,就是代码高亮乱成一团,表格直接溢出页面。问题不在模型,而在“渲染管线”没搭好。
1.2 技术选型:不折腾,选三条主流路线
Markdown 转 HTML 的方案很多,工程里真正经得起折腾的就三条路线:
第一条是前端渲染,代表库是 marked、markdown-it、showdown。优势是零后端依赖、部署简单、支持流式增量渲染,适合聊天类应用。劣势是有潜在 XSS 风险,必须配合白名单过滤。我的经验是个人项目或内部系统用 marked,它轻、快、生态好。
第二条是后端渲染,代表方案是 Python 的 markdown 库配 bleach,或者 Node 端用 markdown-it 再输出 HTML 字符串返回前端。优势是安全策略集中管理,适合对内容合规要求高的场景,比如政务、金融、医疗领域的报告生成系统。劣势是每次渲染要走网络或进程间通信,流式场景下频繁请求会拖慢体验。
第三条是双端混合:Markdown 源文本存库,前端实时渲染,后端在必要时做一次安全清洗。这是我现在最推荐的方式,因为它既保留完整源数据,又能在前端秒级反馈。
选型逻辑不要跟风。我踩过最大的坑是团队里有人为了“统一生态”把所有渲染放在后端,结果聊天类页面每收到一个 token 就要向后端发一次渲染请求,服务器 CPU 直接飙满。所以先问自己一个问题:这块内容是实时生成的,还是预先生成的?实时选前端,预生成选后端,混合场景选双端。
1.3 渲染前必须想的三个问题
很多人拿到 Markdown 就匆忙找库,忽略了三个前置问题。
第一,安全边界在哪里?大模型输出不是你写的文案,它可能是训练语料里原封不动带出来的内容,攻击者也可以通过 prompt injection 诱导模型输出恶意 HTML。所以无论选哪条路线,XSS 过滤不是可选项,是必需品。
第二,样式锚点在哪里?HTML 渲染出来只是光秃秃的标签,用户看到的“好看”全部来自 CSS。你没有定义 pre、table、blockquote 的样式,页面就会退回浏览器默认样式,丑得没法看。这一步很多人漏掉,导致“内容渲染出来了,但跟产品设计稿差距巨大”。
第三,流式还是非流式?ChatGPT 风格的打字机效果现在已经是标配,这影响解析器的调用方式。如果一次性渲染整个 Markdown 字符串,流式收到的半截内容(比如代码块只有开头三个反引号)会让解析器行为不可预期,必须做特殊处理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 接入 marked:真正能跑起来的配置
我用 marked 比较多,简单说下核心用法。新版 marked 从 v5 开始 API 有变化,marked.parse() 替代了曾经的 marked()。一个最小可用配置是这样的:
javascript复制import { marked } from 'marked';
// 扩展:让所有链接在新窗口打开
const renderer = {
link(token) {
return `<a href="${token.href}" target="_blank" rel="noopener noreferrer">${token.text}</a>`;
}
};
marked.use({ renderer });
const html = marked.parse(markdownText);
document.getElementById('content').innerHTML = html;
代码里第一件做的事就是 renderer.link 重写,这是所有接了大模型的页面都必须做的动作:默认情况下 marked 生成的链接会在当前页面跳转,用户点一下聊天记录里的 URL,整个应用就跑了,体验非常差。加上 noopener noreferrer 是安全习惯,防止新窗口页面通过 window.opener 操控你的页面。
需要注意,marked 对 Markdown 语法的支持是 CommonMark 子集,不是所有语法都能识别。比如表格,marked 能渲染,但对“单元格内换行”这类 edge case 支持有限。如果产品里有大量复杂表格,我会推荐 markdown-it + multimarkdown 插件,但换来的是体积和复杂度上升。项目初期用 marked 绝对够,先跑通再优化。
2.2 安全防线:DOMPurify 必装,别省
接大模型输出的第一课,就是永远不要相信模型返回的字符串。XSS 攻击在 AI 应用里不是“可能发生”,而是“已经在发生”。攻击者可以在提问里塞一段 <img src=x onerror=alert(1)>,诱导模型在回答里原样输出或者变形输出,前端一旦直接注入,脚本就执行了。
业界标准做法是配合 DOMPurify 使用,把 marked 生成的 HTML 字符串清洗一遍再插入 DOM:
javascript复制import DOMPurify from 'dompurify';
cons
