18、LangChain 前端:模式 => Markdown 消息
·
本文适配前端开发场景,提供 React/Vue/Svelte/Angular 全框架实现方案,附带代码高亮与最佳实践。
文章目录
1. Markdown 渲染原理
LLM 天然支持输出 Markdown 格式内容(标题、列表、代码块、表格等),直接渲染为纯文本会浪费结构化信息。LangChain 前端流式渲染流程分为 3 步:
- 接收流数据:
useStream钩子实时累积流式文本,更新msg.text状态 - 解析 Markdown:将原始文本转换为 HTML 或 React 元素树(单次解析 < 5ms)
- DOM 渲染:React 用虚拟 DOM _diffing,Vue/Svelte/Angular 用安全 HTML 指令渲染
2. 环境搭建:useStream 配置
首先安装依赖(以 npm 为例):
# 核心依赖
npm install @langchain/core @langchain/react
# 类型支持(TypeScript 项目)
npm install -D @types/langchain__core
2.1 类型定义(TypeScript)
import type { BaseMessage } from "@langchain/core/messages";
// 匹配 Agent 状态结构的接口
interface AgentState {
messages: BaseMessage[]; // 消息列表
}
2.2 基础流式组件(React 示例)
import { useStream } from "@langchain/react";
import { AIMessage, HumanMessage } from "@langchain/core/messages";
import { Markdown } from "./Markdown"; // 后续实现的自定义组件
// Agent 服务地址(替换为你的实际地址)
const AGENT_URL = "http://localhost:2024";
export function Chat() {
// 初始化流式连接
const stream = useStream<AgentState>({
apiUrl: AGENT_URL,
assistantId: "simple_agent", // 替换为你的 Agent ID
});
return (
<div className="chat-container">
{/* 渲染消息列表 */}
{stream.messages.map((msg) => (
<div key={msg.id} className="message-bubble">
{AIMessage.isInstance(msg) ? (
// AI 消息:用自定义 Markdown 组件渲染
<Markdown>{msg.text}</Markdown>
) : HumanMessage.isInstance(msg) ? (
// 人类消息:普通文本渲染
<p>{msg.text}</p>
) : null}
</div>
))}
</div>
);
}
3. 框架适配:Markdown 库选择
不同前端框架推荐对应的 Markdown 解析库,兼顾性能与安全性:
| 框架 | 推荐库组合 | 输出格式 | 核心优势 |
|---|---|---|---|
| React | react-markdown + remark-gfm |
React 元素 | 组件化渲染,支持虚拟 DOM diffing,无需 dangerouslySetInnerHTML |
| Vue | marked + dompurify |
安全 HTML | 轻量快速,原生支持 GitHub Flavored Markdown(GFM) |
| Svelte | marked + dompurify |
安全 HTML | 与 Vue 一致的 API,适配 Svelte 模板语法 {@html} |
| Angular | marked + dompurify |
安全 HTML | 兼容 Angular [innerHTML] 指令,防护 XSS 攻击 |
安装对应依赖
# React
npm install react-markdown remark-gfm
# Vue/Svelte/Angular
npm install marked dompurify
4. 自定义 Markdown 组件实现
4.1 React 组件
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
interface MarkdownProps {
children: string; // 接收 Markdown 文本
}
export function Markdown({ children }: MarkdownProps) {
// 过滤空内容,避免渲染空容器
if (!children.trim()) return null;
return (
<div className="markdown-content">
<ReactMarkdown
remarkPlugins={[remarkGfm]} // 启用 GFM 特性(表格、任务列表等)
options={{
breaks: true, // 单换行转为 <br>
}}
>
{children}
</ReactMarkdown>
</div>
);
}
4.2 Vue 组件(Vue 3 + TypeScript)
<template>
<div class="markdown-content" v-html="safeHtml"></div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import marked from "marked";
import DOMPurify from "dompurify";
const props = defineProps<{
content: string;
}>();
// 解析 + sanitize 处理
const safeHtml = computed(() => {
if (!props.content.trim()) return "";
// 启用 GFM 和换行转换
const rawHtml = marked(props.content, { gfm: true, breaks: true });
// sanitize HTML 防止 XSS
return DOMPurify.sanitize(rawHtml);
});
</script>
4.3 Svelte 组件
<script lang="ts">
import marked from "marked";
import DOMPurify from "dompurify";
export let content: string;
$: rawHtml = marked(content, { gfm: true, breaks: true });
$: safeHtml = DOMPurify.sanitize(rawHtml);
</script>
{#if content.trim()}
<div class="markdown-content" {@html} safeHtml />
{/if}
4.4 Angular 组件
// markdown.component.ts
import { Component, Input } from "@angular/core";
import marked from "marked";
import DOMPurify from "dompurify";
@Component({
selector: "app-markdown",
template: `<div class="markdown-content" [innerHTML]="safeHtml"></div>`,
})
export class MarkdownComponent {
@Input() content = "";
get safeHtml(): string {
if (!this.content.trim()) return "";
const rawHtml = marked(this.content, { gfm: true, breaks: true });
return DOMPurify.sanitize(rawHtml);
}
}
5. HTML 输出安全:XSS 防护
LLM 输出可能包含恶意 HTML 片段(如 <script> 标签、onclick 事件),必须进行安全净化:
核心原则
- React 例外:
react-markdown直接生成 React 元素,无需额外 sanitize - 其他框架:使用
dompurify过滤危险内容(官方仓库)
净化示例(通用)
import DOMPurify from "dompurify";
// 原始 HTML(可能含恶意代码)
const rawHtml = marked(llmOutput);
// 安全净化后输出
const safeHtml = DOMPurify.sanitize(rawHtml, {
// 可选:自定义允许的标签/属性
ADD_TAGS: ["iframe"],
ADD_ATTR: ["allowfullscreen"],
});
Dompurify 会自动过滤:
<script>、<iframe>(默认)等危险标签onclick、onload等事件属性javascript:伪协议链接- 其他 XSS 攻击向量
6. 流式渲染性能优化
useStream 会在每个 Token 到达时更新状态,触发 Markdown 重解析。默认方案适用于 99% 场景(单条消息 < 50KB),以下是极端场景优化:
6.1 节流渲染(避免频繁更新)
// React 示例:使用 requestAnimationFrame 节流
import { useRef, useState, useEffect } from "react";
export function ThrottledMarkdown({ children }: { children: string }) {
const [throttledText, setThrottledText] = useState(children);
const textRef = useRef(children);
const frameRef = useRef<number | null>(null);
useEffect(() => {
textRef.current = children;
if (!frameRef.current) {
frameRef.current = requestAnimationFrame(() => {
setThrottledText(textRef.current);
frameRef.current = null;
});
}
return () => {
if (frameRef.current) cancelAnimationFrame(frameRef.current);
};
}, [children]);
return (
<div className="markdown-content">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{throttledText}
</ReactMarkdown>
</div>
);
}
6.2 增量解析(超大文本优化)
// 核心思路:只解析新增内容,避免全量重解析
const parsedBuffer = useRef<string[]>([]); // 存储已解析的 HTML 片段
const lastTextRef = useRef(""); // 存储上一次的完整文本
// 对比新旧文本,只解析新增部分
const parseIncremental = (newText: string) => {
const diff = newText.slice(lastTextRef.current.length);
if (diff) {
const parsedDiff = marked(diff);
parsedBuffer.current.push(parsedDiff);
}
lastTextRef.current = newText;
return DOMPurify.sanitize(parsedBuffer.current.join(""));
};
7. Markdown 样式定制
为聊天场景优化的紧凑样式(适配气泡布局),直接复制到全局 CSS:
/* Markdown 渲染容器基础样式 */
.markdown-content {
font-size: 0.9rem;
line-height: 1.5;
color: #333;
}
/* 段落间距 */
.markdown-content p {
margin: 0.4em 0;
}
/* 列表样式 */
.markdown-content ul,
.markdown-content ol {
margin: 0.4em 0;
padding-left: 1.4em;
}
/* 代码块样式 */
.markdown-content pre {
overflow-x: auto;
border-radius: 0.375rem;
background: rgba(0, 0, 0, 0.05);
padding: 0.8rem;
margin: 0.8em 0;
font-size: 0.85rem;
}
/* 行内代码样式 */
.markdown-content code {
border-radius: 0.25rem;
background: rgba(0, 0, 0, 0.08);
padding: 0.125rem 0.25rem;
font-size: 0.85rem;
}
/* 引用样式 */
.markdown-content blockquote {
margin: 0.8em 0;
padding-left: 0.75em;
border-left: 3px solid #ccc;
opacity: 0.8;
}
/* 表格样式 */
.markdown-content table {
border-collapse: collapse;
margin: 0.8em 0;
width: 100%;
}
.markdown-content th,
.markdown-content td {
border: 1px solid #e5e7eb;
padding: 0.4em 0.6em;
text-align: left;
}
.markdown-content th {
background: rgba(0, 0, 0, 0.03);
}
/* 链接样式 */
.markdown-content a {
color: #2563eb;
text-decoration: underline;
}
.markdown-content a:hover {
color: #1d4ed8;
}
8. 生产环境最佳实践
- 强制启用 GFM:LLM 普遍使用 GitHub 风格 Markdown,必须开启
gfm: true - 严格 sanitize:Vue/Svelte/Angular 务必通过
dompurify处理 HTML - 处理空内容:避免渲染空的 Markdown 容器,提升 UI 整洁度
- 启用换行转换:设置
breaks: true,让 LLM 的单换行正常显示 - 适配聊天布局:使用紧凑样式,避免大间距影响气泡布局
- 测试边界场景:验证以下内容渲染效果:
- 超长代码块(横向滚动)
- 宽表格(响应式适配)
- 嵌套列表(缩进正确)
- 特殊字符(如 $、`、*)
- 监控性能:通过 Chrome DevTools Performance 面板,排查长消息(>50KB)的渲染卡顿
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)