基于 Node.js 与智谱 AI 的 RAG 工程实践 前段时间突然奇想想做一个基于本地资料回答的客服聊天,做了个demo放在gitee,个人感觉RAG是较可能应用到企业项目的一种模式,同时也能自己耍耍,故用AI总结出了一下这篇文章(基于自己一步步完善这个RAG项目的demo的提问与回答总结而来) ,遇到的很多问题其实是一些依赖的引入报错,只能退而让ai手搓简易的版本使用,如下面的向量存储一、 为什么我们需要 RAG大语言模型LLM很强大但在企业级应用中存在三个致命缺陷幻觉一本正经地胡说八道无法容忍于专业场景。知识滞后训练数据截止后发生的事情它完全不知道。私域盲区它懂全人类的常识但不懂你公司内部的规章制度、业务文档。最朴素的解决思路是把所有私域文档塞进 Prompt 让它读。但这受限于 LLM 的上下文窗口和高昂的 Token 成本。RAGRetrieval-Augmented Generation检索增强生成应运而生。它的核心哲学是不让大模型翻阅整座图书馆而是先帮它找出最相关的几页纸让它只做这几页纸的阅读理解。二、 核心技术栈与组件选型在本次工程实践中我们摒弃了沉重的 Python 体系采用了纯 Node.js 方案核心技术组件如下分类技术/组件作用说明大语言模型 (LLM)智谱 AI (glm-4-flash)负责理解指令、基于检索上下文生成自然语言回答向量化模型智谱 AI (embedding-2)将文本转换为 1024 维的高维向量语义 DNA文本切分langchain/textsplitters将长文本递归切分为固定长度的 Chunk保留重叠度防语义截断向量存储纯 JS 自研 JSON 向量库避开了 C 原生模块的编译坑实现本地持久化与余弦相似度检索后端框架Express.js提供 HTTP 接口处理 SSE 流式响应环境变量dotenv/zod安全管理 API Key校验环境依赖三、 RAG 全链路架构与核心代码整个 RAG 系统分为两大阶段数据流如下图所示[离线构建阶段] 文档 - 切分 - 调用智谱 Embedding API - 向量数据存入本地 JSON 缓存 ↑ (相似度计算) [在线生成阶段] 用户提问 - 调用智谱 Embedding API - 问题向量 ----------- 检索相关上下文 | 组装 Prompt (上下文 问题) ↓ 调用智谱 GLM-4 API (流式) - 返回给前端1. 离线数据准备构建本地 JSON 向量库为了避免每次启动服务都重新计算向量消耗 Token我们实现了一个基于 JSON 的极简持久化向量库。const fs require(fs); const { RecursiveCharacterTextSplitter } require(langchain/textsplitters); const { OpenAIEmbeddings } require(langchain/openai); // 初始化智谱 Embedding 模型 const embeddings new OpenAIEmbeddings({ openAIApiKey: process.env.ZHIPU_API_KEY, modelName: embedding-2, configuration: { baseURL: https://open.bigmodel.cn/api/paas/v4/ } }); // 极简 JSON 向量库核心逻辑 class JsonVectorStore { constructor(embeddings, filePath) { this.embeddings embeddings; this.filePath filePath; this.data []; // { content: string, embedding: number[] } } // 从文档创建并保存缓存 async saveFromDocuments(docs) { const texts docs.map(doc doc.pageContent); // 核心步骤调用 API 批量生成向量 const vectors await this.embeddings.embedDocuments(texts); this.data docs.map((doc, i) ({ content: doc.pageContent, embedding: vectors[i] })); // 持久化到本地硬盘 fs.writeFileSync(this.filePath, JSON.stringify(this.data), utf-8); } // 从本地缓存加载 load() { if (fs.existsSync(this.filePath)) { this.data JSON.parse(fs.readFileSync(this.filePath, utf-8)); return true; } return false; } // 语义检索计算余弦相似度 async similaritySearch(query, k 3) { const queryVector await this.embeddings.embedQuery(query); // 仅将问题向量化 const results this.data.map(item ({ content: item.content, similarity: this.cosineSimilarity(queryVector, item.embedding) })); results.sort((a, b) b.similarity - a.similarity); return results.slice(0, k); } cosineSimilarity(vecA, vecB) { /* 余弦相似度数学公式... */ } }2. 在线生成路由层的 Prompt 组装与流式输出当用户发起请求时后端的核心职责是检索 - 约束 Prompt - 流式响应。// routes/chatRoute.js router.post(/langchain, async (req, res) { const { messages } req.req.body; const question messages[messages.length - 1].content; // 1. 本地检索纯数学计算毫秒级不消耗大模型 Token const relatedDocs await searchRelatedDocs(question, 2); const contextText relatedDocs.map(d d.pageContent).join(\n---\n); // 2. 构建 Prompt严格防止幻觉最后一道防线 let finalSystemPrompt process.env.SYSTEM_IDENTITY; if (contextText) { // ⚠️ 关键避坑必须明确指示大模型“只依赖资料”否则检索噪音会引发幻觉 finalSystemPrompt 【参考信息】\n${contextText}\n\n请严格根据上面的【参考信息】回答用户问题。如果参考信息中没有包含所需内容请直接回复“根据现有知识库无法回答”严禁编造。; } else { finalSystemPrompt 请根据你的通用知识回答用户的问题。; } // 3. 调用智谱 GLM 大模型进行流式生成 const stream await streamChat([ { role: system, content: finalSystemPrompt }, { role: user, content: question } ]); // 4. 通过 SSE 将流式数据推送给前端... res.setHeader(Content-Type, text/event-stream); for await (const chunk of stream) { res.write(data: ${JSON.stringify({ content: chunk.content })}\n\n); } res.end(); });四、 工程踩坑与深度认知 (面试高光时刻)在真实工程落地中跑通 Demo 只是第一步以下三个深度认知决定了 RAG 系统的可用性1. 为什么非要算成向量关键字匹配不行吗关键字匹配如 SQLLIKE基于字面重合它不懂“失眠”和“睡不着”是一个意思。向量是语义层面的表示在向量空间中意思相近的文本距离天然相近。向量化是把“语义匹配”降维成了“数学计算”从而实现了毫秒级的语义检索。2. 既然检索出的已经是文本为什么还要写严格的 Prompt 约束向量检索存在误召回率。有时用户问“报销流程”检索出的却是“开发流程”因为都有“流程”。如果不限制大模型它会顺着错误资料胡编乱造幻觉放大。严格的 Prompt 是守住准确性的最后一道防线。大模型在 RAG 中的角色不是发散创作而是受限条件下的阅读理解。3. 为什么放弃成熟的向量库HNSWLib/Faiss改用 JSON在 Node.js 环境下hnswlib-node和faiss-node都是 C 原生编译模块。在 Windows 环境下极易因缺少 Visual Studio Build Tools 或 Node.js 版本不匹配导致编译失败ERR_PACKAGE_PATH_NOT_EXPORTED。对于中小型知识库基于文件系统的 JSON 缓存 内存余弦计算零依赖、无需编译、永不报错是最稳健的起步方案。五、 生产级 RAG 的进阶方向当前的极简方案足以应对中小型知识库若要走向生产环境还需考虑智能分块按 Markdown 标题、代码块逻辑切分而非简单按字数。多路召回 Reranker结合 BM25关键字召回和向量召回再用交叉编码器重排序。Agent 融合让大模型自主决定何时检索本地知识库何时调用外部工具。