RAG(Retrieval Augmented Generation,检索增强生成)这个技术方向,我最近花了不少时间研究。简单说就是让大模型在回答问题前先查资料,这样回答就不会胡编乱造了。这篇文章记录了我从零搭建一个智能知识库的完整过程,代码和踩坑经验都在这。
什么是 RAG,为什么前端人该关注
大模型有个老毛病:不知道的事情会编。你问它一个公司内部的政策,它根本没见过你的内部文档,但会编一个看起来合理的答案。
RAG 的思路很简单:回答之前,先从你的私有资料库里检索相关信息,把这些信息和问题一起交给大模型。这样模型回答的依据是真实数据,不是训练记忆。
前端工程师做 RAG 有几个优势:
- 前端天然做用户界面,知识库的交互体验需要前端
- 信息架构是前端的强项,RAG 本质就是把信息组织好、检索准
- 不需要训练模型,用现成的 Embedding API + 向量库就能跑起来
项目目标
能用的智能知识库,需要这几件事:
- 上传 PDF/Markdown 文档作为知识来源
- 自动将文档切块、向量化、存入数据库
- 用户提问时,先检索相关文档片段,再生成回答
- 回答附带引用来源,方便核实
技术选型
试了几种方案,这是目前觉得适合前端开发者的组合:
| 组件 | 选择 | 理由 |
|---|---|---|
| 运行时 | Node.js + TypeScript | 前端友好 |
| Embedding | OpenAI text-embedding-3-small | 便宜、快、效果好 |
| 向量库 | Chroma(本地)/ Pinecone(云端) | Chroma 零配置本地跑 |
| 框架 | LangChain.js | 生态成熟,文档全 |
| 前端 | Next.js + Tailwind | 快速出界面 |
| 文档解析 | pdf-parse + marked | 处理 PDF 和 Markdown |
环境准备
1 2 3 4 5 mkdir rag-knowledge-base && cd rag-knowledge-base npm init -y npm install langchain @langchain/openai @langchain/chroma npm install pdf-parse marked npm install -D typescript @types/node配置 TypeScript:
1 2 3 4 5 6 7 8 9 10 { "compilerOptions" : { "target" : "ES2020" , "module" : "ESNext" , "moduleResolution" : "node" , "outDir" : "./dist" , "rootDir" : "./src" , "esModuleInterop" : true } }文档处理
知识库的第一步是把文档变成可以检索的小片段。流程:读取文件 → 切块 → 向量化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import fs from "fs" ; import pdfParse from "pdf-parse" ; import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" ; // 读取文件(支持 PDF 和 Markdown) async function readFile ( filePath: string ): Promise < string > { if (filePath. endsWith ( ".pdf" )) { const dataBuffer = fs. readFileSync (filePath); const result = await pdfParse (dataBuffer); return result. text ; } return fs. readFileSync (filePath, "utf-8" ); } // 切块 async function chunkText ( text: string ) { const splitter = new RecursiveCharacterTextSplitter ({ chunkSize : 500 , chunkOverlap : 50 , }); return await splitter. splitText (text); } // 测试 const text = await readFile ( "./docs/my-document.pdf" ); const chunks = await chunkText (text); console . log ( `文档被切分为 ${chunks.length} 个片段` );切块这一步很关键。块太大检索不准,块太小上下文不够。500 字符是我试下来比较好的默认值,你可以根据实际文档类型调整。
向量存储
切好的片段通过 Embedding 变成向量,存入向量数据库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import { Chroma } from "@langchain/chroma" ; import { OpenAIEmbeddings } from "@langchain/openai" ; import { Document } from "@langchain/core/documents" ; const embeddings = new OpenAIEmbeddings ({ modelName : "text-embedding-3-small" , apiKey : process. env . OPENAI_API_KEY , }); // 将切块转为 Document 对象 function chunksToDocuments ( chunks: string[], sourceFile: string ) { return chunks. map ( (chunk, i) => new Document ({ pageContent : chunk, metadata : { source : sourceFile, chunkIndex : i }, }) ); } // 存入 Chroma async function storeDocuments ( documents: Document[], collectionName: string ) { const vectorStore = new Chroma (embeddings, { collectionName, url : "http://localhost:8000" , }); await vectorStore. addDocuments (documents); console . log ( `已存储 ${documents.length} 个文档到 ${collectionName}` ); } // 完整流程 const chunks = await chunkText ( await readFile ( "./docs/product-guide.pdf" )); const docs = chunksToDocuments (chunks, "product-guide.pdf" ); await storeDocuments (docs, "my-knowledge-base" );启动 Chroma 本地服务:
1 2 pip install chromadb chroma run --path ./chroma-data检索 + 生成回答
知识库建好了,接下来做核心的检索和回答功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import { ChatOpenAI } from "@langchain/openai" ; import { ChatPromptTemplate } from "@langchain/core/prompts" ; import { StringOutputParser } from "@langchain/core/output_parsers" ; import { Chroma } from "@langchain/chroma" ; const llm = new ChatOpenAI ({ modelName : "gpt-4o" , temperature : 0.3 , }); // 连接已有的知识库 const vectorStore = new Chroma (embeddings, { collectionName : "my-knowledge-base" , url : "http://localhost:8000" , }); // RAG 提示词模板 const RAG_PROMPT = ChatPromptTemplate . fromMessages ([ [ "system" , `你是一个知识库助手,根据检索到的文档片段回答问题。 如果文档中有相关信息,请基于文档内容回答,并在末尾标注引用来源。 如果文档中没有相关信息,请如实告知用户 "知识库中没有找到相关信息" 。 不要编造答案。 `], [ "human" , `检索到的文档: {context} 用户问题:{question} `], ]); // 完整的 RAG 查询流程 async function queryKnowledge ( question: string ) { // 1. 检索相关文档 const results = await vectorStore. similaritySearch (question, 3 ); if (results. length === 0 ) { return { answer : "知识库中没有找到相关内容" , sources : [] }; } // 2. 组装上下文 const context = results. map ( (doc, i) => `[文档${i + 1}] ${doc.pageContent}\n来源: ${doc.metadata.source}` ). join ( "\n\n" ); // 3. 生成回答 const chain = RAG_PROMPT . pipe (llm). pipe ( new StringOutputParser ()); const answer = await chain. invoke ({ context, question }); // 4. 返回回答 + 引用来源 return { answer, sources : results. map ( doc => doc. metadata . source ), }; } // 测试 const result = await queryKnowledge ( "产品的退款政策是什么?" ); console . log ( "回答:" , result. answer ); console . log ( "来源:" , result. sources );前端界面
前端开发者最关心的还是界面。用 Next.js 做一个简单的知识库问答界面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 // app/page.tsx "use client" ; import { useState } from "react" ; interface Message { role : "user" | "assistant" ; content : string ; sources?: string []; } export default function Home () { const [messages, setMessages] = useState< Message []>([]); const [input, setInput] = useState ( "" ); const [loading, setLoading] = useState ( false ); const sendMessage = async () => { if (!input. trim ()) return ; const userMessage : Message = { role : "user" , content : input }; setMessages ( prev => [...prev, userMessage]); setInput ( "" ); setLoading ( true ); const res = await fetch ( "/api/query" , { method : "POST" , headers : { "Content-Type" : "application/json" }, body : JSON . stringify ({ question : input }), }); const data = await res. json (); const assistantMessage : Message = { role : "assistant" , content : data. answer , sources : data. sources , }; setMessages ( prev => [...prev, assistantMessage]); setLoading ( false ); }; return ( <div className= "min-h-screen bg-gray-50 flex flex-col" > <header className= "bg-white shadow p-4" > <h1 className= "text-xl font-bold" >智能知识库</h1> </header> <main className= "flex-1 overflow-y-auto p-4 space-y-4" > {messages. map ( (msg, i) => ( <div key={i} className={ `max-w-2xl p-4 rounded-lg ${ msg. role === "user" ? "bg-blue-100 ml-auto" : "bg-white border" } `} > <p className= "whitespace-pre-wrap" >{msg. content }</p> {msg. sources && msg. sources . length > 0 && ( <div className= "mt-2 text-xs text-gray-500" > 来源: {msg. sources . join ( ", " )} </div> )} </div> ))} {loading && ( <div className= "max-w-2xl p-4 rounded-lg bg-white border" > <p className= "text-gray-400" >正在检索知识库...</p> </div> )} </main> <footer className= "bg-white border-t p-4" > <div className= "max-w-2xl mx-auto flex gap-2" > <input value={input} onChange={ (e) => setInput (e. target . value )} onKeyDown={ (e) => e. key === "Enter" && sendMessage ()} placeholder= "输入你的问题..." className= "flex-1 border rounded-lg px-4 py-2" /> <button onClick={sendMessage} disabled={loading} className= "bg-blue-600 text-white px-6 py-2 rounded-lg disabled:opacity-50" > 提问 </button> </div> </footer> </div> ); }API 路由:
1 2 3 4 5 6 7 8 9 // app/api/query/route.ts import { NextResponse } from "next/server" ; import { queryKnowledge } from "@/lib/rag" ; export async function POST ( request: Request ) { const { question } = await request. json (); const result = await queryKnowledge (question); return NextResponse . json (result); }批量导入文档
实际使用中不可能一条条录入。写一个批量导入脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import glob from "glob" ; import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" ; import { Chroma } from "@langchain/chroma" ; import { OpenAIEmbeddings } from "@langchain/openai" ; import { Document } from "@langchain/core/documents" ; import fs from "fs" ; import pdfParse from "pdf-parse" ; async function importAllDocuments () { const files = glob. sync ( "./docs/**/*.{pdf,md}" ); console . log ( `找到 ${files.length} 个文档` ); const allDocuments : Document [] = []; for ( const file of files) { let text : string ; if (file. endsWith ( ".pdf" )) { const dataBuffer = fs. readFileSync (file); const result = await pdfParse (dataBuffer); text = result. text ; } else { text = fs. readFileSync (file, "utf-8" ); } const splitter = new RecursiveCharacterTextSplitter ({ chunkSize : 500 , chunkOverlap : 50 , }); const chunks = await splitter. splitText (text); const docs = chunks. map ( (chunk, i) => new Document ({ pageContent : chunk, metadata : { source : file, chunkIndex : i }, }) ); allDocuments. push (...docs); console . log ( `处理完成: ${file} → ${chunks.length} 个片段` ); } const embeddings = new OpenAIEmbeddings ({ modelName : "text-embedding-3-small" , }); const vectorStore = new Chroma (embeddings, { collectionName : "my-knowledge-base" , url : "http://localhost:8000" , }); await vectorStore. addDocuments (allDocuments); console . log ( `全部导入完成,共 ${allDocuments.length} 个文档片段` ); } importAllDocuments();踩坑记录
1. 中文检索效果不好
一开始用 text-embedding-ada-002,中文文档的检索准确率只有 60% 左右。换成 text-embedding-3-small 后提升到 85%。如果你的知识库以中文为主,建议直接用 3-small。
2. 切块大小影响很大
块太小(200 字符)检索出的内容缺乏上下文;块太大(1000 字符)又会混入不相关信息。500 字符是我试下来比较合适的值,具体看你的文档类型。技术文档可以偏大一些,FAQ 类偏小。
3. 回答引用的来源不对
有时候回答引用的文档和实际内容对不上。原因是 metadata 没有正确传递。确保每个 Document 对象都有 source 字段,检索结果按源文件过滤。
4. Chroma 本地存储的坑
Chroma 默认数据存在内存里,重启就没了。启动时加上--path ./chroma-data参数才能持久化。
进阶优化方向
| 方向 | 说明 | 难度 |
|---|---|---|
| 混合检索 | 关键词检索 + 向量检索结合 | ⭐⭐ |
| 重排序 | 用 Cross-Encoder 对检索结果重新排序 | ⭐⭐⭐ |
| 多路召回 | 多个知识库并行检索,合并结果 | ⭐⭐⭐ |
| 流式输出 | SSE 实现打字机效果 | ⭐⭐ |
| 问答评估 | 自动化评估回答准确率和忠实度 | ⭐⭐⭐ |
RAG 的入门门槛不高,但要做好需要反复调参。文档切块、检索策略、提示词设计这三个环节最值得花时间优化。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋
📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~