基于Spring AI RAG 的AI知识库前端交互实现
在企业时负责企业AI知识库项目的前端模块开发,核心工作是对接 Spring AI RAG 检索服务,落地整套AI问答交互能力。很多人觉得AI前端只是简单调接口,但实际落地中,流式应答、富文本渲染、会话状态管控都是实打实的工程化问题。
本文结合我的项目代码,分享核心实现思路:SSE流式问答、Markdown安全渲染、历史会话管理、RAG检索结果展示,聚焦前端工程化实现与踩坑优化。
一、业务背景与技术选型
后端基于 Spring AI 搭建 RAG 链路:文档向量化、语义检索、上下文增强、大模型流式生成。前端不再是传统HTTP一次性请求响应模式,需要适配服务端分片流式输出特性。
技术选型核心思路:
-
通信:采用 SSE(Server-Sent Events) 替代轮询/WebSocket,适配RAG单向流式推送场景,轻量无心跳开销
-
渲染:自定义 Markdown 解析渲染,解决AI回答代码块、表格、公式解析错乱及XSS问题
-
状态:中心化管理会话列表、当前对话、检索上下文,保证状态可回溯
二、核心代码实现与思路解析
2.1 封装SSE流式请求(对接Spring AI RAG)
RAG问答的核心痛点是响应时长不可控、数据分片返回。传统axios请求会等待全部数据返回后才渲染,用户体验极差。因此我封装了通用SSE请求工具,支持增量接收、自动重连、手动销毁、异常拦截。
typescript
// 封装SSE RAG流式请求工具
export const createRagStreamRequest = (
url: string,
params: Record<string, any>,
onMessage: (chunk: string) => void,
onEnd: () => void,
onError: (err: any) => void
) => {
// 序列化RAG检索参数:问题、知识库ID、检索条数、上下文ID
const searchParams = new URLSearchParams({
...params,
timestamp: String(Date.now())
})
const sse = new EventSource(`${url}?${searchParams.toString()}`)
// 接收Spring AI分片数据
sse.onmessage = (e) => {
if (!e.data) return
// 增量推送分片数据,实时渲染
onMessage(e.data)
}
// 流式响应结束回调
sse.addEventListener('end', onEnd)
// 异常处理:网络中断、服务熔断、超时
sse.onerror = (err) => {
sse.close()
onError(err)
}
// 返回销毁方法,防止内存泄漏、多请求冲突
return {
destroy: () => sse.close()
}
}
核心优化点 :单例管控SSE实例,每次发起新问答前销毁上一次链接,彻底解决快速提问导致的数据流错乱、旧数据残留问题。
2.2 增量数据拼接与流式渲染
Spring AI RAG 返回的是碎片化文本分片,不能直接渲染,需要前端有序拼接、去空、去重,同时实时更新视图,实现打字机效果。
typescript
// 组件内流式数据处理
const streamContent = ref('')
let streamTimer: number | null = null
// 初始化流式问答
const startRagChat = async (question: string) => {
// 重置本次问答内容
streamContent.value = ''
// 销毁上一次SSE链接
if (sseInstance) sseInstance.destroy()
sseInstance = createRagStreamRequest(
'/api/springai/rag/stream',
{ question, sessionId: currentSessionId.value },
// 分片接收回调
(chunk) => {
// 防抖更新视图,避免高频渲染卡顿
if (streamTimer) clearTimeout(streamTimer)
streamTimer = window.setTimeout(() => {
streamContent.value += chunk
}, 16)
},
// 问答结束
() => {
saveChatHistory()
},
// 异常兜底
() => {
streamContent.value += '\n\n问答异常,请重试'
}
)
}
2.3 安全可控的Markdown渲染方案
AI RAG检索结果常包含代码块、表格、引用、公式,原生渲染容易出现样式错乱、XSS注入风险。我基于 marked 做了二次封装,增加白名单过滤、代码高亮、安全转义。
typescript
import { marked } from 'marked'
import DOMPurify from 'dompurify'
// 自定义Markdown渲染
export const renderRagMarkdown = (content: string) => {
// 1. 基础解析MD语法
const rawHtml = marked.parse(content)
// 2. XSS安全过滤,过滤恶意标签
const safeHtml = DOMPurify.sanitize(rawHtml, {
ADD_TAGS: ['iframe'],
REMOVE_ATTR: ['onclick', 'onload']
})
return safeHtml
}
同时针对RAG场景做了适配:统一代码块样式、检索引用文本高亮、知识库来源信息单独展示,极大提升回答可读性。
2.4 历史会话与检索状态管理
为了实现多会话切换、历史问答回溯,我通过状态中心化管理会话生命周期,绑定唯一 sessionId,与Spring AI服务端会话上下文打通。
typescript
// 会话核心状态简易封装
export const useChatSession = () => {
const sessionList = ref<SessionItem[]>([])
const currentSessionId = ref('')
// 创建新会话
const createNewSession = () => {
const id = `rag_session_${Date.now()}`
currentSessionId.value = id
sessionList.value.unshift({
id,
title: '新知识库问答',
createTime: Date.now(),
chatList: []
})
}
// 切换会话、持久化会话数据
const switchSession = (id: string) => {
currentSessionId.value = id
// 切换时销毁当前SSE链接,避免数据串流
if (sseInstance) sseInstance.destroy()
}
return { sessionList, currentSessionId, createNewSession, switchSession }
}
三、项目核心亮点与工程化总结
-
流式体验优化:基于SSE轻量长链接实现RAG问答流式输出,通过单例销毁、防抖渲染解决数据流混乱、页面卡顿问题,体验优于传统轮询方案。
-
安全富文本渲染:结合marked+DOMPurify实现安全MD渲染,适配AI知识库专属内容格式,规避XSS风险,统一全局展示样式。
-
状态闭环管理:会话ID与后端Spring AI上下文联动,实现会话创建、切换、持久化、销毁全生命周期管理,保证检索问答上下文连贯性。
-
高可扩展性:通信、渲染、状态逻辑分层解耦,后续可快速接入知识库检索溯源、问答打分、对话记忆等扩展功能。
四、写在最后
这次开发最大的感悟:AI前端交互核心不在于"调通接口",而在于适配大模型的异步、流式、不确定性特性。通过工程化封装,解决了RAG场景下的体验与稳定性问题,也让整个知识库交互模块具备了生产级落地能力。