NestJS与LangChainJS构建企业级RAG知识库实战指南
各位做后端或者全栈开发的朋友,应该多少遇到过类似的情况:项目里的技术文档、需求文档、会议记录越来越多,真到写方案或者回答业务问题的时候,反而不知道该去哪份资料里找依据。直接让大模型生成答案,体验虽然流畅,但模型没有看过企业内部资料,容易一本正经地“编造”。在企业真实业务里,这种幻觉是很致命的。
RAG 是目前比较成熟的解决方案。它先把文档切块并向量化,存到向量数据库里;收到用户问题时,先从库里检索出最相关的片段,再把片段拼进 Prompt 交给大模型生成回答。整个链路可以理解为“先搜索,后回答”。答案有出处、可调试、好更新,非常适合企业内部知识库、客服问答、研发助手等场景。
本文围绕 NestJS + LangChainJS 搭建一套完整的 RAG 系统:从项目初始化、文档加载与切块、向量存储、检索与生成,到 API 流式输出,最后给出基于宝塔面板和 Docker 的部署思路。文章中的代码会尽量完整可运行,同时也会解释清楚每一步的“为什么”,方便你照着落地,而不是只复制片段。
1. RAG 系统架构与核心原理
1.1 RAG 解决的是什么问题
传统大模型问答有一个很强的限制:模型学到的知识停留在训练数据截止时间,无法自动覆盖企业私有资料,也无法保证回答内容的真实性。直接问模型“我们系统的登录超时时间是多少”,模型只能根据猜测回答,因为训练数据里根本没有这份内部配置文档。
传统的关键词搜索也有局限。输入“登录超时设置”可能搜出很多包含关键词的文档,但用户需要的是精确答案,而不是一屏搜索结果。更重要的是,不同文档对同一问题可能有不同描述,搜索系统很难自动聚合。
RAG 的思路恰好处于两者之间。它用向量检索的能力代替关键词匹配,把用户问题转换为向量,在向量空间中寻找语义最接近的文本片段。找到候选片段后,再让大模型基于这些片段生成最终回答。这样做有三个好处:
- 答案有依据:模型被约束在知识库片段内回答,减少编造概率。
- 知识更新方便:替换或新增文档即可,不需要重新训练模型。
- 过程可追溯:每个回答都可以回溯到命中了哪些文档片段。
从工程角度看,RAG 比微调模型更容易落地。微调需要准备高质量训练集,成本高、周期长,而 RAG 只需要解决数据导入、向量检索和 Prompt 拼接这些问题。
1.2 为什么选择 NestJS + LangChainJS
在 Node.js/TypeScript 生态中,NestJS 是目前企业级后端项目的首选之一。它借鉴了 Angular 的模块化思想,提供依赖注入、装饰器、拦截器、管道、守卫等成熟的开发模式。对于一个 RAG 服务来说,外部依赖很多:大模型 API、向量数据库、文档解析、任务队列、鉴权系统。如果不用依赖注入和模块化拆分,代码很快会变成一坨难以维护的“胶水代码”。
LangChainJS 是 LangChain 的 TypeScript 版本,提供了一整套构建大模型应用的工具抽象。你可以用它加载文档、切分文本、生成向量、访问向量数据库、调用大模型,甚至编排复杂的 Agent 流程。相比自己从零封装 OpenAI SDK,LangChainJS 最大的价值是“抽象统一”:今天用 MemoryVectorStore,明天想切换到 pgvector 或 Milvus,业务代码改动量很小。
另外,NestJS 和 LangChainJS 都是 TypeScript 技术栈,类型定义完善,前端开发转过来也没有太大门槛。如果你所在团队的前端使用 React/Vue,后端使用 Node,那么整套 AI 全栈项目都可以在一种语言里完成,不用再单独维护一套 Python 服务。
1.3 系统整体流程
本文要搭建的系统包含以下步骤:
- 用户上传文档(txt、md 等)。
- 文档服务把文件解析为纯文本。
- 文本切分器将长文档拆成固定大小的 chunk。
- Embedding 模型把每个 chunk 转换为向量。
- 向量库存储向量和原始文本。
- 用户通过 API 提问,系统把问题也转换为向量。
- 向量库执行相似度检索,返回最相关的 k 个 chunk。
- 系统把 chunk 拼进 Prompt,调用大模型生成回答。
- 响应以 JSON 或 SSE 流式方式返回给前端。
在这个流程里,业务代码层面我们要拆成配置模块、向量存储模块、文档处理模块和 RAG 问答模块。前两步是数据准备,中间两步是数据索引,最后几步是问答链路。
2. 环境准备与项目初始化
2.1 版本与依赖说明
不同技术版本的兼容性问题在 AI 项目里尤其明显。LangChainJS 迭代速度比较快,早些时候所有能力都集中在 langchain 包里,现在拆分出了 @langchain/core、@langchain/openai、@langchain/community 等子包。
本文示例以 Node.js 20 为例,建议使用 18 或 20 的 LTS 版本。Node 版本过低可能导致部分依赖的 ESM 语法或原生模块无法使用。
需要准备的环境如下:
| 依赖项 | 说明 |
|---|---|
| Node.js | 18+ 或 20 LTS |
| pnpm / npm | 包管理器,任选其一 |
| NestJS CLI | 用于创建项目,pnpm add -g @nestjs/cli |
| 大模型 API Key | OpenAI 或兼容 OpenAI 协议的模型服务 |
| 向量数据库 | 演示环境用内存版,生产建议 pgvector / Chroma / Milvus |
如果你使用的是通义、智谱、DeepSeek 等兼容 OpenAI 协议的模型服务,只需要修改 baseURL 和 Key,不需要更换代码主体。
2.2 初始化 NestJS 项目
执行下面的命令创建项目:
安装后续会用到的依赖:
@nestjs/config 用于统一读取环境变量;langchain 聚合包提供文档加载和切分能力;@langchain/openai 提供对话模型和 Embedding 模型;@langchain/community 里有很多向量数据库和文档加载器的社区实现。@types/multer 用于文件上传的类型提示。
2.3 项目目录结构
为了让代码职责清晰,我建议按模块目录组织:
config 目录存放环境配置,document 模块负责文件解析和切块,vector-store 模块负责向量的写入与检索,rag 模块负责问答链路的组装。
3. 核心概念拆解:从文档到向量再到答案
3.1 文档加载
RAG 系统第一步是让程序能读取文档。LangChainJS 提供了很多 Loader,常见的包括:
TextLoader:读取 txt / md 等纯文本文件。PDFLoader:解析 PDF,需要额外安装pdf-parse等依赖。DirectoryLoader:批量读取目录下的多种文件。CSVLoader、DocxLoader等。
在企业项目里,建议先以 txt / md 为主,后续再逐步支持 PDF、Word。PDF 解析很容易出现表格错位、分栏混乱的问题,直接会影响后期检索效果。
下面是最基本的文本加载写法:
loader.load() 返回一个 Document[]。每个 Document 包含两个核心字段:
pageContent:文档正文。metadata:元数据,比如source表示来源文件。
加载完原始文档后,不能直接把整篇文档塞给模型。如果文档很长,一方面可能超过模型上下文长度,另一方面,用户提问时只关心某个细节,整篇文档会引入大量无关信息,降低检索精度。
3.2 文本切块策略
文本切块是 RAG 系统里最容易被低估的环节。切块质量直接决定了检索命中率,而检索命中率又直接决定最终回答质量。
LangChainJS 中最常用的是 RecursiveCharacterTextSplitter。它会按照一组分隔符依次递归切分文本,优先保留语义相对完整的段落。
这里有两个参数需要重点关注:
chunkSize:每个 chunk 的目标字符数。500 是相对稳妥的初始值。太小容易丢失上下文,太大则会使向量包含太多噪声。chunkOverlap:相邻 chunk 之间重叠的字符数。重叠的作用是防止切块时把一句完整语义从中间切断。比如一句话横跨两个 chunk,如果不设置 overlap,后半句在检索时可能因为缺少主语而无法被理解。
中文场景下,默认分隔符对中文的支持并不友好。英文按空格分词效果很好,但中文词与词之间没有空格。所以我建议把分隔符数组改成:
这样切分器会优先按段落、换行、句号、感叹号、问号、分号来切,减少“一句话被腰斩”的情况。
切块大小没有绝对标准。需要根据实际语料去测试。可以先从 300 到 500 开始,再通过后续的问答测试观察命中情况。后续如果引入父子切块、语义切块等进阶方案,效果会更好,但工程复杂度也会上升。
3.3 Embedding 与向量存储
Embedding 的作用是把文本变成一个固定维度的向量数组。语义相近的文本,在向量空间中的距离也更近。LangChainJS 中使用 OpenAIEmbeddings 来调用 OpenAI 的 embedding 模型,也可以替换成其他兼容模型。
Embedding 生成之后,需要存到向量数据库里。本地演示阶段可以使用 MemoryVectorStore:
MemoryVectorStore 是最简单的实现,所有数据保存在进程内存中,没有网络开销,适合学习和功能验证。但它有三个明显缺点:
- 服务重启后数据全部丢失。
- 不能跨实例共享。
- 数据量大了以后,检索性能会下降。
所以生产环境一定要替换为持久化向量数据库。常见选择有:
pgvector:在 PostgreSQL 上加向量字段,适合已有 PostgreSQL 的团队。Chroma:轻量级向量数据库,部署简单。Milvus/Qdrant:更专业的向量数据库,适合海量数据和高并发场景。
替换起来并不困难。LangChainJS 对多种向量库做了统一抽象,核心的 addDocuments 和 similaritySearch 方法签名基本一致。这也是引入 LangChainJS 的价值之一。
3.4 检索、增强、生成
检索时,系统把用户问题也转成向量,然后调用向量库的 similaritySearch 找到最接近的 k 个 chunk:
这里的 k 值需要根据场景调整。k 太小可能漏掉关键信息,k 太大又可能把无关内容塞进 Prompt。一般初始值建议 3 到 5。
得到候选片段后,需要一个 Prompt 模板把它们组织起来: