从零手搓RAG知识库问答!1套可运行JS代码,搞定LangChain检索增强生成
😩做AI知识库你是不是也踩这些坑
想做私有文档问答,直接丢文本给大模型:
- 模型记不住长文档,上下文超限报错
- 回答凭空编造,出现大量事实幻觉
- 关键词检索只能匹配字面,语义相近内容完全搜不到
试了网上零散教程,要么代码残缺跑不通,要么只讲理论没有完整Demo。
今天这篇Node.js LangChain RAG完整实战,一套代码直接复制运行,手把手拆解向量库、文档、检索器、问答全链路,学完你能独立搭建小型私有知识库问答系统。
读完你能掌握:
- RAG完整底层原理,看懂向量嵌入+检索增强流程
- LangChain核心组件
Document/MemoryVectorStore/Retriever实战用法 - 相似度打分、元数据过滤、自定义Prompt完整实现
- 新手开发RAG必踩5个坑+对应解决方案
- 可直接复用的故事知识库问答Demo
📌先搞懂:RAG到底是什么
RAG全称Retrieval Augmented Generation(检索增强生成),解决大模型知识滞后、幻觉两大核心痛点。
完整执行链路:
- 知识库预处理:文件/文本拆分成段落文档Document
- Embedding向量化:把文本转为高维数字向量,存入向量数据库
- 用户提问:问题同步转为向量,在向量库做相似度匹配
- 检索召回:取出Top-K最相关文档片段
- 增强Prompt:把检索到的上下文塞进提示词一起发给LLM
- 模型生成:模型只能基于给定文档回答,大幅减少瞎编
LangChain核心模块分工
@langchain/openai:大模型对话、文本嵌入能力@langchain/core/documents:文档最小单元,存储文本+元数据@langchain/classic/vectorstores/memory:内存向量库,本地测试首选- Retriever检索器:封装向量搜索,自带去重、过滤、重排能力
🛠️环境准备
1. 安装依赖
npminstalldotenv @langchain/openai @langchain/core @langchain/classic2. .env配置文件
# 大模型配置 OPENAI_API_KEY=你的API密钥 OPENAI_BASE_URL=兼容OpenAI接口地址 MODEL_NAME=gpt-3.5-turbo # 嵌入模型 EMBEDDINGS_MODEL_NAME=text-embedding-v2🔥完整可运行实战代码(故事知识库问答)
新建index.js,全部代码复制即可运行:
import'dotenv/config'import{ChatOpenAI,OpenAIEmbeddings}from'@langchain/openai'import{MemoryVectorStore}from'@langchain/classic/vectorstores/memory'import{Document}from'@langchain/core/documents'// 1. 初始化对话大模型 temperature=0关闭随机,保证回答稳定constmodel=newChatOpenAI({temperature:0,model:process.env.MODEL_NAME,apiKey:process.env.OPENAI_API_KEY,configuration:{baseURL:process.env.OPENAI_BASE_URL,}})// 2. 初始化嵌入模型,文本转向量核心constembeddings=newOpenAIEmbeddings({apiKey:process.env.OPENAI_API_KEY,model:process.env.EMBEDDINGS_MODEL_NAME,configuration:{baseURL:process.env.OPENAI_BASE_URL,}})// 3. 构建知识库文档集合// Document结构:pageContent=待向量化文本,metadata=不参与向量计算的元数据(用于过滤溯源)constdocuments=[newDocument({pageContent:`光光是一个活泼开朗的小男孩,他有一双明亮的大眼睛,总是带着灿烂的笑容。光光最喜欢的事情就是和朋友们一起玩耍,他特别擅长踢足球,每次在球场上奔跑时,就像一道阳光一样充满活力。`,metadata:{chapter:1,character:"光光",type:"角色介绍",mood:"活泼"},}),newDocument({pageContent:`东东是光光最好的朋友,他是一个安静而聪明的男孩。东东喜欢读书和画画,他的画总是充满了想象力。虽然性格不同,但东东和光光从幼儿园就认识了,他们一起度过了无数个快乐的时光。`,metadata:{chapter:2,character:"东东",type:"角色介绍",mood:"温馨"},}),newDocument({pageContent:`有一天,学校要举办一场足球比赛,光光非常兴奋,他邀请东东一起参加。但是东东从来没有踢过足球,他担心自己会拖累光光。光光看出了东东的担忧,他拍着东东的肩膀说:"没关系,我们一起练习,我相信你一定能行的!"`,metadata:{chapter:3,character:"光光和东东",type:"友情情节",mood:"鼓励"},}),newDocument({pageContent:`接下来的日子里,光光每天放学后都会教东东踢足球。光光耐心地教东东如何控球、传球和射门,而东东虽然一开始总是踢不好,但他从不放弃。东东也用自己的方式回报光光,他画了一幅画送给光光,画上是两个小男孩在球场上一起踢球的场景。`,metadata:{chapter:4,character:"光光和东东",type:"友情情节",mood:"互助"},}),newDocument({pageContent:`比赛那天终于到了,光光和东东一起站在球场上。虽然东东的技术还不够熟练,但他非常努力,而且他用自己的观察力帮助光光找到了对手的弱点。在关键时刻,东东传出了一个漂亮的球,光光接球后射门得分!他们赢得了比赛,更重要的是,他们的友谊变得更加深厚了。`,metadata:{chapter:5,character:"光光和东东",type:"高潮转折",mood:"激动"},}),newDocument({pageContent:`从那以后,光光和东东成为了学校里最要好的朋友。光光教东东运动,东东教光光画画,他们互相学习,共同成长。每当有人问起他们的友谊,他们总是笑着说:"真正的朋友就是互相帮助,一起变得更好的人!"`,metadata:{chapter:6,character:"光光和东东",type:"结局",mood:"欢乐"},}),newDocument({pageContent:`多年后,光光成为了一名职业足球运动员,而东东成为了一名优秀的插画师。虽然他们走上了不同的道路,但他们的友谊从未改变。东东为光光设计了球衣上的图案,光光在每场比赛后都会给东东打电话分享喜悦。他们证明了,真正的友情可以跨越时间和距离,永远闪闪发光。`,metadata:{chapter:7,character:"光光和东东",type:"尾声",mood:"温馨"},}),];// 4. 文档批量向量化,存入内存向量库constvectorStore=awaitMemoryVectorStore.fromDocuments(documents,embeddings)// 5. 生成检索器,k=3代表召回相似度最高的3条文档constretriever=vectorStore.asRetriever({k:3})// 测试提问constquestion="东东和光是怎么成为好朋友的?"console.log('='.repeat(80));console.log(`用户问题:${question}`);console.log('='.repeat(80));// 方式1:检索器获取文档(内置去重、rerank重排序)constdocs=awaitretriever.invoke(question);// 方式2:原生向量检索,附带相似度分数(余弦距离,数值越小越相似)constscoreResults=awaitvectorStore.similaritySearchWithScore(question,3);// 打印检索结果+相似度docs.forEach((doc,i)=>{constscoreResult=scoreResults.find(([scoredDoc])=>{scoredDoc.pageContent===doc.pageContent})constscore=scoreResult?scoreResult[1]:null;constsimilarity=score!=null?(1-score).toFixed(4):"N/A"console.log(`\n[文档${i+1}] 相似度:${similarity}| 原始向量分:${score}`);console.log(`片段内容:${doc.pageContent.substring(0,60)}...`);console.log(`元数据:章节${doc.metadata.chapter},类型${doc.metadata.type}`);})// 6. 拼接检索上下文,自定义约束Promptconstcontext=docs.map((doc,i)=>`[片段${i}]\n${doc.pageContent}`).join("\n\n-----\n\n");constprompt=`你是一个讲友情故事的老师。严格基于下方故事片段回答问题,语言温暖生动。 如果片段里没有对应信息,统一回复:"这个故事里还没有提到这个细节",禁止编造内容。 故事片段:${context}问题:${question}老师的回答:`;// 7. 调用大模型生成答案constresponse=awaitmodel.invoke(prompt);console.log("\n=====AI最终回答=====")console.log(response.content);📖代码核心逻辑拆解
1. Document文档对象
RAG最小存储单元,分为两部分:
pageContent:核心文本,唯一会参与Embedding向量化的字段metadata:自定义元数据,不做向量计算,用于过滤、溯源、分类
示例:区分章节、角色、内容类型,后续可实现按章节检索
2. MemoryVectorStore内存向量库
- 优点:零额外服务、开箱即用、适合本地Demo/小型测试
- 缺点:程序重启向量全部丢失,生产环境替换PGVector/Milvus/Chroma
3. 两种检索方式区别
retriever.invoke(question)
封装高阶能力:向量检索+自动去重+过滤+结果重排rerank,业务开发首选vectorStore.similaritySearchWithScore(question, k)
底层原生向量查询,仅返回文档+余弦距离分数,用于调试相似度效果
相似度计算公式:
1 - 向量距离,数值越接近1,文本语义越匹配
4. 增强Prompt设计关键点
- 强制约束模型只能基于检索片段回答,从根源抑制幻觉
- 无相关内容时固定话术,避免模型自由发挥编造信息
- 统一文风约束,控制输出格式
⚠️新手做LangChain RAG必踩5大坑
坑1:metadata也参与向量化
很多新手把链接、章节、标签写进pageContent,导致向量语义混乱。
解决方案:业务属性全部放进metadata,仅正文存入pageContent。
坑2:temperature设置过高
temperature>0会让模型自由发挥,容易脱离检索文本编造内容。
解决方案:知识库问答固定temperature:0,关闭随机性。
坑3:混淆retriever和原生similaritySearch
直接用similaritySearchWithScore做业务检索,缺少去重、重排序,召回脏数据。
解决方案:业务逻辑统一使用retriever.invoke(),打分仅用于调试。
坑4:文档不分块直接全量入库
长篇小说、上万字文档直接存入Document,向量语义混杂,检索精度暴跌。
解决方案:生产环境搭配文本分割器RecursiveCharacterTextSplitter拆分长文本。
坑5:忽略LangChain版本迭代
框架更新速度极快,导入路径、类名频繁变动,网上旧教程直接报错。
解决方案:优先参考官方最新文档,固定依赖版本避免兼容问题。
📊RAG适用场景&选型建议
适合用这套内存RAG
- 本地小型知识库Demo、内部测试工具
- 文档总量百条以内,不需要持久化向量
- 快速验证私有文档问答需求
不适合,建议换持久向量库
- 线上生产服务、十万级以上文档
- 需要程序重启保留向量数据
- 高并发检索、要求毫秒级查询速度
✅全文总结
- RAG核心逻辑:文档向量化存储→语义检索→上下文注入Prompt→模型精准回答,解决大模型幻觉问题
- LangChain标准化组件大幅降低AI应用开发成本,Document、向量库、检索器可自由替换
MemoryVectorStore是入门最佳选择,一套JS代码即可跑通完整问答流程- 开发重点:规范Document结构、关闭模型随机、优先使用Retriever高阶检索、严格约束Prompt减少编造
