第34期 | RAG前端实现 今天你将学会理解 RAG检索增强生成的完整流程——不只是概念是前端要实现什么实现知识库管理界面上传文档 → 分片 → embedding → 存储实现向量搜索交互用户输入 → 搜索相关文档 → 展示结果 引用来源理解前端在 RAG 系统中的角色——不是调用 API而是管理整个数据流 核心知识RAG 是什么为什么需要它LLM 有一个致命缺陷幻觉——它会编造不存在的事实自信满满地给你错误答案。RAG 解决这个问题让 AI 在回答前先从真实文档库中搜索相关内容基于真实数据回答而不是凭记忆编造。没有 RAG 的回答“React useEffect 的 cleanup 在组件重新渲染前执行。” ← 这句话是编造的实际是组件卸载和下次 effect 执行前。有 RAG 的回答“根据 React 官方文档useEffect 的 cleanup 函数在组件卸载时和下次 effect 执行前运行。” ← 基于真实文档附引用来源。RAG 的完整流程前端视角1. 知识库构建阶段离线/管理界面 上传文档 → 文档分片(chunking) → 生成 embedding → 存入向量数据库 2. 查询阶段在线/用户交互 用户提问 → 问题生成 embedding → 向量搜索找最相关文档片段 → 构建 Promptsystem 检索到的文档 问题→ LLM 生成回答 → 前端展示回答 引用来源前端在 RAG 系统中的职责环节前端做什么后端做什么文档上传上传界面 进度显示 文档列表管理接收文件 → 分片 → embedding → 存储向量搜索搜索输入 结果展示 引用标注问题 embedding → 向量搜索 → 返回结果LLM 回答聊天界面 Markdown渲染 来源链接RAG Prompt → LLM → 流式返回知识库管理界面核心组件KnowledgeBase知识库管理页面 ├── DocumentUpload文档上传组件 │ ├── DropZone拖拽上传区域 │ └── UploadProgress上传 处理进度 ├── DocumentList文档列表 │ ├── DocumentCard单文档卡片名称/状态/操作 │ └── ChunkPreview文档分片预览 └── SearchBar知识库内搜索DocumentUpload 组件// features/knowledge/components/DocumentUpload.tsx import { useState, useCallback } from react; import { Upload, FileText, Loader2 } from lucide-react; interface DocumentUploadProps { onUpload: (files: File[]) Promisevoid; } interface UploadStatus { fileName: string; status: uploading | processing | completed | error; progress: number; error?: string; } export function DocumentUpload({ onUpload }: DocumentUploadProps) { const [uploadStatuses, setUploadStatuses] useStateUploadStatus[]([]); const [isDragging, setIsDragging] useState(false); const handleFiles async (files: File[]) { // 支持的文件类型 const validTypes [text/plain, text/markdown, application/pdf]; const validFiles files.filter(f validTypes.includes(f.type) || f.name.endsWith(.md)); if (validFiles.length 0) { alert(请上传 Markdown、PDF 或纯文本文件); return; } // 初始化状态 const statuses: UploadStatus[] validFiles.map(f ({ fileName: f.name, status: uploading, progress: 0, })); setUploadStatuses(statuses); try { // 逐个上传 for (let i 0; i validFiles.length; i) { // 上传阶段 setUploadStatuses(prev prev.map((s, j) j i ? { ...s, status: uploading, progress: 30 } : s )); // 等后端处理分片 embedding await new Promise(resolve setTimeout(resolve, 500)); // 模拟 setUploadStatuses(prev prev.map((s, j) j i ? { ...s, status: processing, progress: 60 } : s )); // 等 embedding 完成 await new Promise(resolve setTimeout(resolve, 1000)); // 模拟 setUploadStatuses(prev prev.map((s, j) j i ? { ...s, status: completed, progress: 100 } : s )); } await onUpload(validFiles); } catch (error) { setUploadStatuses(prev prev.map(s s.status ! completed ? { ...s, status: error, error: 上传失败 } : s )); } }; return ( div {/* 拖拽上传区域 */} div onDragOver{(e) { e.preventDefault(); setIsDragging(true); }} onDragLeave{() setIsDragging(false)} onDrop{(e) { e.preventDefault(); setIsDragging(false); handleFiles([...e.dataTransfer.files]); }} className{border-2 rounded-lg p-8 text-center cursor-pointer transition-colors ${isDragging ? border-blue-500 bg-blue-50 : border-gray-200 hover:border-gray-300}} onClick{() { const input document.createElement(input); input.type file; input.multiple true; input.accept .md,.txt,.pdf; input.onchange (e) handleFiles([...(e.target as HTMLInputElement).files!]); input.click(); }} Upload size{32} classNamemx-auto mb-2 text-gray-400 / p classNametext-gray-500拖拽文件到此处或点击上传/p p classNametext-xs text-gray-400 mt-1支持 Markdown、PDF、纯文本/p /div {/* 上传进度 */} {uploadStatuses.length 0 ( div classNamemt-4 space-y-2 {uploadStatuses.map((status) ( div key{status.fileName} classNameflex items-center gap-2 text-sm FileText size{16} / span classNameflex-1{status.fileName}/span {status.status uploading ( span classNametext-blue-500上传中 {status.progress}%/span )} {status.status processing ( span classNametext-yellow-500处理中.../span )} {status.status completed ( span classNametext-green-500✅ 完成/span )} {status.status error ( span classNametext-red-500❌ {status.error}/span )} {(status.status uploading || status.status processing) ( Loader2 size{16} classNameanimate-spin text-gray-400 / )} /div ))} /div )} /div ); }向量搜索交互界面用户在聊天时提问 → 后端同时做向量搜索 → 返回回答 引用来源。核心在聊天界面中展示引用来源// features/chat/components/SourceReference.tsx interface SourceReference { id: string; title: string; content: string; // 引用的文档片段 source: string; // 文档来源文件名/URL relevance: number; // 相关度分数 (0-1) } interface SourceReferenceProps { sources: SourceReference[]; } export function SourceReference({ sources }: SourceReferenceProps) { const [expanded, setExpanded] useState(false); if (sources.length 0) return null; return ( div classNamemt-3 border-t border-gray-100 pt-2 button onClick{() setExpanded(!expanded)} classNametext-xs text-blue-500 hover:underline flex items-center gap-1 BookOpen size{12} / {expanded ? 收起引用来源 : 查看 ${sources.length} 个引用来源} /button {expanded ( div classNamemt-2 space-y-2 {sources.map((source, idx) ( div key{source.id} classNamerounded border border-gray-200 p-3 text-xs dark:border-gray-600 div classNameflex items-center justify-between mb-1 span classNamefont-medium text-blue-600{source.title}/span span classNametext-gray-400 相关度: {Math.round(source.relevance * 100)}% /span /div p classNametext-gray-600 line-clamp-3{source.content}/p span classNametext-gray-400 mt-1 block来源: {source.source}/span /div ))} /div )} /div ); }RAG 聊天的后端接口设计前端需要一个新的 API 接口同时返回 LLM 回答 引用来源// app/api/ai/rag-chat/route.tsimportOpenAIfromopenai;exportasyncfunctionPOST(req:NextRequest){const{message,messages}awaitreq.json();// 1. 向量搜索找到最相关的文档片段constsearchResultsawaitsearchKnowledgeBase(message,{topK:3});// 2. 构建 RAG PromptconstsystemPromptbuildRAGPrompt(searchResults);// 3. 流式调用 LLMconststreamawaitopenai.chat.completions.create({model:gpt-4o-mini,messages:[{role:system,content:systemPrompt},...messages,{role:user,content:message},],stream:true,});// 4. 流式返回回答 最后附带引用来源// SSE 流中两种数据// data: { type: content, content: ... } — AI 回答内容// data: { type: sources, sources: [...] } — 引用来源最后一条}functionbuildRAGPrompt(searchResults:SearchResult[]):string{return你是一个技术助手。回答问题时请基于以下参考资料。如果参考资料中没有相关信息请说明根据现有文档没有找到相关信息。 参考资料${searchResults.map((r,i)[${i1}]${r.title}\n${r.content}).join(\n\n)}回答时请标注引用来源格式[1] [2] 等。;}前端处理 RAG 流式响应RAG 的 SSE 流有两种数据类型content sources前端需要区分处理// 解析 RAG SSE 流asyncfunctionhandleRAGStream(stream:ReadableStreamstring){letfullContent;letsources:SourceReference[][];constreaderstream.getReader();while(true){const{done,value}awaitreader.read();if(done)break;// RAG SSE 可能包含两种类型的数据try{constparsedJSON.parse(value);if(parsed.typecontent){fullContentparsed.content;// 更新 UI追加文字到消息updateLastAssistantMessage(fullContent);}elseif(parsed.typesources){sourcesparsed.sources;// 更新 UI显示引用来源updateSourceReferences(sources);}}catch{// 无法解析的行跳过}}return{content:fullContent,sources};}知识库的状态管理// features/knowledge/store/knowledgeStore.tsinterfaceKnowledgeState{documents:Document[];searchQuery:string;searchResults:SearchResult[];isSearching:boolean;isUploading:boolean;fetchDocuments:()Promisevoid;uploadDocument:(file:File)Promisevoid;deleteDocument:(id:string)Promisevoid;search:(query:string)Promisevoid;}常见误区误区1RAG 只需要后端实现前端需要管理文档上传、展示搜索结果、标注引用来源——这些是用户体验的关键部分。误区2所有文档都直接丢给 LLM文档太大 → token 超限。必须分片chunking只搜索最相关的片段传给 LLM。误区3引用来源不重要没有引用来源 → 用户无法验证 AI 回答的可靠性 → RAG 的核心价值就没了。 AI协作实战实战场景设计知识库管理界面的完整交互我给 AI 的 prompt设计一个技术文档知识库管理界面的完整交互流程 1. 文档上传区支持拖拽上传 点击上传支持的格式md/pdf/txt上传后显示处理进度 2. 文档列表每个文档卡片显示名称、状态、分片数量、上传时间支持搜索和删除 3. 分片预览点击文档可以看到它的分片列表每个分片是一段 500 字左右的文本 4. 搜索测试在知识库中搜索一个关键词看到最相关的分片和相似度分数 用 React TypeScript Tailwind shadcn/ui 风格。 每个组件给出 Props 类型定义和核心渲染逻辑。AI 输出的核心组件代码经过审查修改后✅ 上传区拖拽 点击进度条完整✅ 文档列表带搜索、状态标签、删除按钮❌ 分片预览太简单——追加要求分片内容可编辑用户可以微调分片内容以提高搜索精度✅ 搜索测试输入关键词 → 显示相关分片 相似度分数 来源文档名学到了什么AI 生成的知识库界面基本完整但分片编辑功能是我追加的——因为实际使用中自动分片可能切得不理想用户需要微调。 动手练习练习1简单实现文档上传界面用 DocumentUpload 组件实现拖拽上传 进度显示。先不连后端用 setTimeout 模拟上传进度。练习2中等实现知识库搜索 引用展示在前端实现搜索输入框 → 调用搜索 API → 展示搜索结果分片内容 相似度分数在聊天消息中展示 SourceReference引用来源面板练习3挑战实现完整的 RAG 聊天组合所有组件聊天界面 RAG 搜索 引用来源用户提问 → 后端同时做向量搜索 LLM 回答前端流式渲染回答 最后展示引用来源引用来源可展开/收起显示相关度分数和原始文档片段 本期要点RAG 解决幻觉让 AI 基于真实文档回答而不是凭记忆编造前端职责文档上传/管理界面、向量搜索交互、引用来源展示知识库管理界面拖拽上传 进度显示 文档列表 分片预览 搜索测试引用来源是 RAG 的灵魂没有引用来源 → 用户无法验证 → RAG 的核心价值没了RAG SSE 流有两种数据contentAI回答 sources引用来源前端要区分处理 下期预告下一期进入 AI Agent 前端交互——工具调用展示、思考过程可视化、多轮对话。你将让 AI 不仅会「说话」还能「做事」——调用工具、执行操作并把整个过程可视化展示给用户。如果你没有苹果电脑需要上传ios到APPStore可以访问以下网站iPA上传工具 - IPA解析与AppStore提交
第34期 | RAG前端实现
第34期 | RAG前端实现 今天你将学会理解 RAG检索增强生成的完整流程——不只是概念是前端要实现什么实现知识库管理界面上传文档 → 分片 → embedding → 存储实现向量搜索交互用户输入 → 搜索相关文档 → 展示结果 引用来源理解前端在 RAG 系统中的角色——不是调用 API而是管理整个数据流 核心知识RAG 是什么为什么需要它LLM 有一个致命缺陷幻觉——它会编造不存在的事实自信满满地给你错误答案。RAG 解决这个问题让 AI 在回答前先从真实文档库中搜索相关内容基于真实数据回答而不是凭记忆编造。没有 RAG 的回答“React useEffect 的 cleanup 在组件重新渲染前执行。” ← 这句话是编造的实际是组件卸载和下次 effect 执行前。有 RAG 的回答“根据 React 官方文档useEffect 的 cleanup 函数在组件卸载时和下次 effect 执行前运行。” ← 基于真实文档附引用来源。RAG 的完整流程前端视角1. 知识库构建阶段离线/管理界面 上传文档 → 文档分片(chunking) → 生成 embedding → 存入向量数据库 2. 查询阶段在线/用户交互 用户提问 → 问题生成 embedding → 向量搜索找最相关文档片段 → 构建 Promptsystem 检索到的文档 问题→ LLM 生成回答 → 前端展示回答 引用来源前端在 RAG 系统中的职责环节前端做什么后端做什么文档上传上传界面 进度显示 文档列表管理接收文件 → 分片 → embedding → 存储向量搜索搜索输入 结果展示 引用标注问题 embedding → 向量搜索 → 返回结果LLM 回答聊天界面 Markdown渲染 来源链接RAG Prompt → LLM → 流式返回知识库管理界面核心组件KnowledgeBase知识库管理页面 ├── DocumentUpload文档上传组件 │ ├── DropZone拖拽上传区域 │ └── UploadProgress上传 处理进度 ├── DocumentList文档列表 │ ├── DocumentCard单文档卡片名称/状态/操作 │ └── ChunkPreview文档分片预览 └── SearchBar知识库内搜索DocumentUpload 组件// features/knowledge/components/DocumentUpload.tsx import { useState, useCallback } from react; import { Upload, FileText, Loader2 } from lucide-react; interface DocumentUploadProps { onUpload: (files: File[]) Promisevoid; } interface UploadStatus { fileName: string; status: uploading | processing | completed | error; progress: number; error?: string; } export function DocumentUpload({ onUpload }: DocumentUploadProps) { const [uploadStatuses, setUploadStatuses] useStateUploadStatus[]([]); const [isDragging, setIsDragging] useState(false); const handleFiles async (files: File[]) { // 支持的文件类型 const validTypes [text/plain, text/markdown, application/pdf]; const validFiles files.filter(f validTypes.includes(f.type) || f.name.endsWith(.md)); if (validFiles.length 0) { alert(请上传 Markdown、PDF 或纯文本文件); return; } // 初始化状态 const statuses: UploadStatus[] validFiles.map(f ({ fileName: f.name, status: uploading, progress: 0, })); setUploadStatuses(statuses); try { // 逐个上传 for (let i 0; i validFiles.length; i) { // 上传阶段 setUploadStatuses(prev prev.map((s, j) j i ? { ...s, status: uploading, progress: 30 } : s )); // 等后端处理分片 embedding await new Promise(resolve setTimeout(resolve, 500)); // 模拟 setUploadStatuses(prev prev.map((s, j) j i ? { ...s, status: processing, progress: 60 } : s )); // 等 embedding 完成 await new Promise(resolve setTimeout(resolve, 1000)); // 模拟 setUploadStatuses(prev prev.map((s, j) j i ? { ...s, status: completed, progress: 100 } : s )); } await onUpload(validFiles); } catch (error) { setUploadStatuses(prev prev.map(s s.status ! completed ? { ...s, status: error, error: 上传失败 } : s )); } }; return ( div {/* 拖拽上传区域 */} div onDragOver{(e) { e.preventDefault(); setIsDragging(true); }} onDragLeave{() setIsDragging(false)} onDrop{(e) { e.preventDefault(); setIsDragging(false); handleFiles([...e.dataTransfer.files]); }} className{border-2 rounded-lg p-8 text-center cursor-pointer transition-colors ${isDragging ? border-blue-500 bg-blue-50 : border-gray-200 hover:border-gray-300}} onClick{() { const input document.createElement(input); input.type file; input.multiple true; input.accept .md,.txt,.pdf; input.onchange (e) handleFiles([...(e.target as HTMLInputElement).files!]); input.click(); }} Upload size{32} classNamemx-auto mb-2 text-gray-400 / p classNametext-gray-500拖拽文件到此处或点击上传/p p classNametext-xs text-gray-400 mt-1支持 Markdown、PDF、纯文本/p /div {/* 上传进度 */} {uploadStatuses.length 0 ( div classNamemt-4 space-y-2 {uploadStatuses.map((status) ( div key{status.fileName} classNameflex items-center gap-2 text-sm FileText size{16} / span classNameflex-1{status.fileName}/span {status.status uploading ( span classNametext-blue-500上传中 {status.progress}%/span )} {status.status processing ( span classNametext-yellow-500处理中.../span )} {status.status completed ( span classNametext-green-500✅ 完成/span )} {status.status error ( span classNametext-red-500❌ {status.error}/span )} {(status.status uploading || status.status processing) ( Loader2 size{16} classNameanimate-spin text-gray-400 / )} /div ))} /div )} /div ); }向量搜索交互界面用户在聊天时提问 → 后端同时做向量搜索 → 返回回答 引用来源。核心在聊天界面中展示引用来源// features/chat/components/SourceReference.tsx interface SourceReference { id: string; title: string; content: string; // 引用的文档片段 source: string; // 文档来源文件名/URL relevance: number; // 相关度分数 (0-1) } interface SourceReferenceProps { sources: SourceReference[]; } export function SourceReference({ sources }: SourceReferenceProps) { const [expanded, setExpanded] useState(false); if (sources.length 0) return null; return ( div classNamemt-3 border-t border-gray-100 pt-2 button onClick{() setExpanded(!expanded)} classNametext-xs text-blue-500 hover:underline flex items-center gap-1 BookOpen size{12} / {expanded ? 收起引用来源 : 查看 ${sources.length} 个引用来源} /button {expanded ( div classNamemt-2 space-y-2 {sources.map((source, idx) ( div key{source.id} classNamerounded border border-gray-200 p-3 text-xs dark:border-gray-600 div classNameflex items-center justify-between mb-1 span classNamefont-medium text-blue-600{source.title}/span span classNametext-gray-400 相关度: {Math.round(source.relevance * 100)}% /span /div p classNametext-gray-600 line-clamp-3{source.content}/p span classNametext-gray-400 mt-1 block来源: {source.source}/span /div ))} /div )} /div ); }RAG 聊天的后端接口设计前端需要一个新的 API 接口同时返回 LLM 回答 引用来源// app/api/ai/rag-chat/route.tsimportOpenAIfromopenai;exportasyncfunctionPOST(req:NextRequest){const{message,messages}awaitreq.json();// 1. 向量搜索找到最相关的文档片段constsearchResultsawaitsearchKnowledgeBase(message,{topK:3});// 2. 构建 RAG PromptconstsystemPromptbuildRAGPrompt(searchResults);// 3. 流式调用 LLMconststreamawaitopenai.chat.completions.create({model:gpt-4o-mini,messages:[{role:system,content:systemPrompt},...messages,{role:user,content:message},],stream:true,});// 4. 流式返回回答 最后附带引用来源// SSE 流中两种数据// data: { type: content, content: ... } — AI 回答内容// data: { type: sources, sources: [...] } — 引用来源最后一条}functionbuildRAGPrompt(searchResults:SearchResult[]):string{return你是一个技术助手。回答问题时请基于以下参考资料。如果参考资料中没有相关信息请说明根据现有文档没有找到相关信息。 参考资料${searchResults.map((r,i)[${i1}]${r.title}\n${r.content}).join(\n\n)}回答时请标注引用来源格式[1] [2] 等。;}前端处理 RAG 流式响应RAG 的 SSE 流有两种数据类型content sources前端需要区分处理// 解析 RAG SSE 流asyncfunctionhandleRAGStream(stream:ReadableStreamstring){letfullContent;letsources:SourceReference[][];constreaderstream.getReader();while(true){const{done,value}awaitreader.read();if(done)break;// RAG SSE 可能包含两种类型的数据try{constparsedJSON.parse(value);if(parsed.typecontent){fullContentparsed.content;// 更新 UI追加文字到消息updateLastAssistantMessage(fullContent);}elseif(parsed.typesources){sourcesparsed.sources;// 更新 UI显示引用来源updateSourceReferences(sources);}}catch{// 无法解析的行跳过}}return{content:fullContent,sources};}知识库的状态管理// features/knowledge/store/knowledgeStore.tsinterfaceKnowledgeState{documents:Document[];searchQuery:string;searchResults:SearchResult[];isSearching:boolean;isUploading:boolean;fetchDocuments:()Promisevoid;uploadDocument:(file:File)Promisevoid;deleteDocument:(id:string)Promisevoid;search:(query:string)Promisevoid;}常见误区误区1RAG 只需要后端实现前端需要管理文档上传、展示搜索结果、标注引用来源——这些是用户体验的关键部分。误区2所有文档都直接丢给 LLM文档太大 → token 超限。必须分片chunking只搜索最相关的片段传给 LLM。误区3引用来源不重要没有引用来源 → 用户无法验证 AI 回答的可靠性 → RAG 的核心价值就没了。 AI协作实战实战场景设计知识库管理界面的完整交互我给 AI 的 prompt设计一个技术文档知识库管理界面的完整交互流程 1. 文档上传区支持拖拽上传 点击上传支持的格式md/pdf/txt上传后显示处理进度 2. 文档列表每个文档卡片显示名称、状态、分片数量、上传时间支持搜索和删除 3. 分片预览点击文档可以看到它的分片列表每个分片是一段 500 字左右的文本 4. 搜索测试在知识库中搜索一个关键词看到最相关的分片和相似度分数 用 React TypeScript Tailwind shadcn/ui 风格。 每个组件给出 Props 类型定义和核心渲染逻辑。AI 输出的核心组件代码经过审查修改后✅ 上传区拖拽 点击进度条完整✅ 文档列表带搜索、状态标签、删除按钮❌ 分片预览太简单——追加要求分片内容可编辑用户可以微调分片内容以提高搜索精度✅ 搜索测试输入关键词 → 显示相关分片 相似度分数 来源文档名学到了什么AI 生成的知识库界面基本完整但分片编辑功能是我追加的——因为实际使用中自动分片可能切得不理想用户需要微调。 动手练习练习1简单实现文档上传界面用 DocumentUpload 组件实现拖拽上传 进度显示。先不连后端用 setTimeout 模拟上传进度。练习2中等实现知识库搜索 引用展示在前端实现搜索输入框 → 调用搜索 API → 展示搜索结果分片内容 相似度分数在聊天消息中展示 SourceReference引用来源面板练习3挑战实现完整的 RAG 聊天组合所有组件聊天界面 RAG 搜索 引用来源用户提问 → 后端同时做向量搜索 LLM 回答前端流式渲染回答 最后展示引用来源引用来源可展开/收起显示相关度分数和原始文档片段 本期要点RAG 解决幻觉让 AI 基于真实文档回答而不是凭记忆编造前端职责文档上传/管理界面、向量搜索交互、引用来源展示知识库管理界面拖拽上传 进度显示 文档列表 分片预览 搜索测试引用来源是 RAG 的灵魂没有引用来源 → 用户无法验证 → RAG 的核心价值没了RAG SSE 流有两种数据contentAI回答 sources引用来源前端要区分处理 下期预告下一期进入 AI Agent 前端交互——工具调用展示、思考过程可视化、多轮对话。你将让 AI 不仅会「说话」还能「做事」——调用工具、执行操作并把整个过程可视化展示给用户。如果你没有苹果电脑需要上传ios到APPStore可以访问以下网站iPA上传工具 - IPA解析与AppStore提交