企业做 AI 知识库,2025 年最主流的技术方案已经不是“把整份文档喂给大模型”,而是 RAG(Retrieval-Augmented Generation,检索增强生成)。这篇不铺垫背景,直接拆解一个“NestJS + Langchain.js + 向量数据库”的 RAG 全栈知识库项目:从工程目录设计、后端服务搭建、文档解析、文本切块、向量化入库,到检索问答 API、前端接入、Docker 与宝塔部署、效果验证、问题排查,完整走一遍。如果你正在做企业级后台管理系统、AI 客服、内部文档助手,或者打算从前端/后端转 AI 全栈,这篇文章可以直接按流程跟练。
为什么选 NestJS + Langchain.js 这套组合,而不是 Python 后端?核心原因是 TypeScript 全链路。NestJS 提供模块化、依赖注入、守卫、拦截器、生命周期管理等企业级后端能力;Langchain.js 是 LangChain 的 TypeScript 实现,提供文档加载器、文本切块器、Embedding 模型封装、向量库连接器、Prompt 模板和检索链。前后端和 AI 编排共用一套类型系统,工程落地比“Node 前端 + Python 后端”的双语言方案更顺,也更容易在企业团队里维护。
整个系统最核心的链路是:文档上传 → 解析清洗 → 文本切块 → Embedding 向量化 → 写入向量数据库 → 用户提问 → 语义检索 TopK → 组装 Prompt → 大模型生成回答。下面我会把这条链路拆成工程实现,给出可以复制改造的 NestJS 模块代码、Langchain.js 检索链代码、API 调用示例和部署配置。先看这个项目到底能做什么、门槛在哪里。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | AI 全栈应用 / 企业级 RAG 知识库问答系统 |
| 后端框架 | NestJS(Node.js + TypeScript,需 Node.js 18+,建议 20 LTS) |
| AI 编排框架 | Langchain.js(LangChain TypeScript 版本) |
| 核心功能 | 文档管理、文本解析、切块、向量化、语义检索、生成式问答、多轮会话 |
| 典型组件 | PostgreSQL + pgvector、Redis(缓存/任务队列)、MinIO(文件存储)、大模型 API |
| 模型接入方式 | OpenAI 兼容接口或本地模型服务,通过环境变量切换 |
| 前端方案 | 可选 Next.js / Vue3,也可先只做 RESTful API + 简易管理页 |
| 启动方式 | 本地 npm 启动 / Docker Compose 一键编排 |
| 是否支持 API | 支持,提供文档上传、检索问答、会话历史等 RESTful 接口 |
| 是否支持批量任务 | 支持,多文档可批量解析、切块、向量化入库,配合 Redis 任务队列 |
| 显存需求 | 取决于所用的大模型和 Embedding 模型。调云端 API 无需本地 GPU;本地部署模型需按模型量级评估 |
| 适合场景 | 企业内部知识库、客服问答、研发文档助手、产品文档检索、法律/合规文档复核 |
从能力表可以看出,这套方案不是拿一个大模型聊天那么简单。它的重点是把“文档怎么进、知识怎么存、问题怎么检索、答案怎么生成”做成一条标准流水线,最后暴露成接口给前端或第三方系统调用。对企业来说,RAG 的价值在于知识可以持续更新,不依赖微调,也不用把私有数据直接暴露给模型厂商。
要注意的是,显存占用和部署资源不能拍脑袋写死。如果底座模型调用云端 API,后端只需要一台普通 2 核 4G 的云服务器;如果要在本地跑 7B/13B 开源模型,才需要考虑独立 GPU 机器。实际资源消耗取决于模型参数量、Embedding 模型、并发数和向量库规模,建议先以最小配置跑通,再逐步扩容。
2. 适用场景与使用边界
这类企业级 RAG 系统最适合解决三类问题:第一,企业内部文档太多且分散,员工找不到答案;第二,客服或运营团队反复回答相似问题,需要统一知识出口;第三,需要基于指定文档做审查、复核或内容生成,例如产品说明书、操作手册、合同模板。通过接入 NestJS 的管理后台,运营人员可以上传文档、查看入库状态、测试检索质量,而不需要直接接触数据库和模型配置。
这套系统也有明显的边界,不要指望一个 RAG 项目解决所有 AI 问题。RAG 不会“无中生有地学会”文档里没有的知识,检索不到的内容,生成端照样会答错;RAG 也不等于微调,如果目标是让模型学习某种业务写法或风格,应该考虑领域微调,而不是只做知识库;此外,RAG 对文档质量很敏感,扫描版 PDF、手写笔记、表格混排类文档,直接解析入库的效果通常不理想。
合规和授权问题必须放在前面说清楚。上传到知识库的文档,务必确认内容来源合法,拥有使用和分发的授权;涉及客户隐私、内部财务、人员信息的文档,要做权限分级,接口层和文档层都要加访问控制;如果使用云端大模型 API,敏感数据是否可以被模型厂商处理,需要提前确认企业安全合规要求。生产环境建议对知识库做“最小可见范围”设计,而不是所有员工都能检索全部文档。
3. 整体架构与工程目录设计
一个可维护的企业级 RAG 项目,建议拆成六层:接入层(前端/API 网关)、应用层(NestJS 业务模块)、AI 编排层(Langchain.js 链)、数据层(PostgreSQL、pgvector、Redis、MinIO)、模型层(大模型 API 或本地推理服务)、基础设施层(Docker、Nginx、日志监控)。每一层职责单一,后续替换模型或向量库时不会牵动整个系统。
前端页面 / 管理后台 │ REST API / WebSocket ▼ NestJS 应用服务 ├── 用户与权限模块 ├── 文档管理模块 ├── 解析切块模块 ├── 向量化与检索模块 ├── 会话问答模块 └── 批量任务模块 │ ├── PostgreSQL + pgvector(业务数据 + 向量索引) ├── Redis(缓存 / BullMQ 任务队列) ├── MinIO / 对象存储(原始文档) └── 大模型 API / 本地模型服务(Embedding + Chat)NestJS 的模块化在这里很合适。文档管理、知识库、会话、任务队列各自做成 Module,每个 Module 内部再分 Controller、Service、Entity、DTO。实际工程可以按下面的目录组织:
src/ ├── modules/ │ ├── auth/ # 登录、鉴权、用户管理 │ ├── document/ # 文档上传、列表、删除 │ ├── knowledge/ # 知识库配置、切块参数 │ ├── ingestion/ # 解析、切块、向量化入库 │ ├── retrieval/ # 检索服务封装 │ ├── chat/ # 对话接口、会话历史 │ └── task/ # 批量任务队列 ├── common/ │ ├── filters/ # 全局异常过滤器 │ ├── interceptors/ # 日志、响应统一包装 │ └── guards/ # 权限守卫 ├── config/ # 环境变量、数据库配置 └── main.ts推荐用官方脚手架初始化 NestJS 工程,后面所有模块都基于这个骨架扩展。NestJS 的依赖注入让模型客户端、向量库连接、配置中心可以被多个服务复用,不会出现“每个模块各连一次模型”的重复代码。
4. 环境准备与前置条件
先列一份完整的环境清单。软件版本选择以稳定和团队熟悉度为优先,没有绝对标准,但下面的组合是目前企业项目里出现频率较高的。
| 组件 | 建议 | 说明 |
|---|---|---|
| Node.js | 18 或 20 LTS | NestJS 10/11 均支持,20 LTS 更稳 |
| 包管理器 | pnpm 或 npm | 团队统一即可 |
| PostgreSQL | 14 及以上 | 需要支持 pgvector 扩展 |
| pgvector | 最新稳定版 | 向量索引依赖 |
| Redis | 6.x / 7.x | BullMQ 任务队列和会话缓存 |
| MinIO | 最新稳定版 | 可选,文件量大时使用 |
| 大模型 API | OpenAI 兼容接口或本地服务 | 需准备 API Key 和模型名称 |
| Docker | 20.10+ | 可选,但推荐生产环境使用 |
| 代码仓库 | Git | 配合 CI/CD 发布 |
本地开发时,可以先不装 MinIO,文件存本地磁盘即可;但 PostgreSQL 的 pgvector 扩展必须装好,因为向量存储直接依赖它。验证 pgvector 是否可用,执行:
CREATE EXTENSION IF NOT EXISTS vector; SELECT extversion FROM pg_extension WHERE extname = 'vector';模型接入需要确认两件事:Embedding 模型和对话模型。Embedding 负责把文本转成向量,对话模型负责生成最终答案。生产环境可以直接用 OpenAI 兼容接口,也可以接入国内厂商的兼容端点,或者用 Ollama / vLLM 部署本地开源模型。项目里建议把模型配置全部放到.env,这样切换模型不动代码。
5. NestJS 后端工程搭建
先用脚手架创建项目,然后安装数据库和 Langchain.js 相关依赖。
# 创建 NestJS 项目 nest new enterprise-rag-server --package-manager pnpm cd enterprise-rag-server # 数据库与配置 pnpm add @nestjs/config @nestjs/typeorm typeorm pg @nestjs/platform-express # Langchain.js 核心与模型接入 pnpm add langchain @langchain/core @langchain/openai @langchain/community @langchain/textsplitters # 文档解析 pnpm add pdf-parse mammoth marked # 任务队列 pnpm add @nestjs/bullmq bullmq安装完成后,先配置全局模块。app.module.ts里引入 ConfigModule 和 TypeOrmModule,数据库连接信息从环境变量读取:
// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ type: 'postgres', host: config.get('DB_HOST', '127.0.0.1'), port: config.get<number>('DB_PORT', 5432), username: config.get('DB_USER', 'postgres'), password: config.get('DB_PASSWORD', 'postgres'), database: config.get('DB_NAME', 'rag_kb'), autoLoadEntities: true, synchronize: true, // 生产环境请改为 false,使用 migration 管理 }), }), ], }) export class AppModule {}synchronize: true只适合开发环境,因为频繁改动实体可能造成数据不一致。生产环境建议关闭,用 TypeORM migration 管理表结构变更。
文档模块是知识库的入口。实体设计上,文档表和文本块表分开:文档表保存文件元信息,文本块表保存切块内容和向量。这样做的好处是删除文档时能级联清理向量数据,检索时也能快速回溯到来源文档。
// src/modules/document/entities/document.entity.ts import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('documents') export class DocumentEntity { @PrimaryGeneratedColumn('uuid') id: string; @Column() title: string; @Column({ default: 'pending' }) status: string; // pending / processing / completed / failed @Column({ nullable: true }) filePath: string; @Column({ default: 0 }) chunkCount: number; @CreateDateColumn() createdAt: Date; }文本块表需要额外存向量字段。pgvector 配合 TypeORM 时,可以用vector类型,但类型定义需要和 pgvector 类型对应。实际项目中通常写一个自定义类型映射,或者在插入向量前用原生 SQL 设置类型。更简单的做法是:业务元数据放在 TypeORM 实体,向量字段通过 TypeORM 的query()或 Repository 的queryBuilder执行原生 SQL 写入,避免类型映射问题。
6. Langchain.js 与 RAG 核心链路实现
RAG 的核心在 Langchain.js 的检索链。先创建模型客户端和 Embedding 客户端,代码里统一走环境变量:
// src/common/ai/llm.provider.ts import { ChatOpenAI } from '@langchain/openai'; import { OpenAIEmbeddings } from '@langchain/openai'; export function createChatModel() { return new ChatOpenAI({ model: process.env.LLM_MODEL || 'gpt-4o-mini', apiKey: process.env.LLM_API_KEY, configuration: { baseURL: process.env.LLM_BASE_URL, // 兼容 OpenAI 的接口地址 }, temperature: 0.2, }); } export function createEmbeddings() { return new OpenAIEmbeddings({ model: process.env.EMBEDDING_MODEL || 'text-embedding-3-small', apiKey: process.env.EMBEDDING_API_KEY || process.env.LLM_API_KEY, configuration: { baseURL: process.env.EMBEDDING_BASE_URL, }, }); }这里用baseURL是为了兼容各类“OpenAI 协议”的模型服务,包括国内厂商的兼容端点、Ollama 的 OpenAI 兼容接口、以及自建的 vLLM 服务。切换模型时只改.env,不碰业务代码。
检索问答链路推荐用 Langchain.js 的createRetrievalChain组装。先构造 Prompt 模板,再创建“文档拼接链”,最后把检索器接进来:
// src/modules/chat/rag.service.ts import { Injectable } from '@nestjs/common'; import { ChatPromptTemplate } from '@langchain/core/prompts'; import { createStuffDocumentsChain } from 'langchain/chains/combine_documents'; import { createRetrievalChain } from 'langchain/chains/retrieval'; import { createChatModel, createEmbeddings } from '../../common/ai/llm.provider'; import { VectorStoreService } from '../retrieval/vector-store.service'; @Injectable() export class RagService { constructor(private readonly vectorStoreService: VectorStoreService) {} async buildChain() { const prompt = ChatPromptTemplate.fromMessages([ ['system', '你是一个企业知识库助手。请仅根据以下资料回答问题。如果资料中没有相关信息,请直接说“资料中未找到相关内容”,不要编造。\n\n资料:\n{context}'], ['human', '问题:{input}'], ]); const combineDocsChain = await createStuffDocumentsChain({ llm: createChatModel(), prompt, }); const retriever = this.vectorStoreService.getVectorStore().asRetriever(8); return createRetrievalChain({ retriever, combineDocsChain, }); } async ask(question: string) { const chain = await this.buildChain(); const response = await chain.invoke({ input: question }); return response.answer; } }这段代码是 RAG 链路最短也最稳的写法。asRetriever(8)表示每次检索返回 8 个相关文本块;temperature调低让模型更忠实于资料;系统提示词里强制要求“没有资料就说没有”,能明显减少幻觉。
如果要做多轮会话,可以在ask方法里先把用户问题做一次“改写”。常见做法是把历史对话和当前问题一起交给大模型,让它生成一个完整上下文的新问题,再拿这个问题去检索。这样做比直接把历史拼进 RAG 的检索输入更准确,因为向量检索本身不擅长带上下文的模糊表达。
7. 知识库处理:解析、切块与向量化
文档入库是整个 RAG 系统里最影响效果的一环。这部分的顺序是:上传 → 解析 → 清洗 → 切块 → Embedding → 写入向量库。任何一个环节做不好,检索质量都会明显下降。
解析阶段,不同文件类型走不同解析器。Markdown 可以直接读文本;PDF 用pdf-parse;Word 用mammoth把 docx 转成 HTML 或纯文本。要注意的是扫描版 PDF 没有文本层,必须接 OCR,常规解析方案处理不了。解析后要做基础清洗:去掉页眉页脚、多余空行、无意义的超链接和表格乱码。
切块策略是整个项目里最值得调优的参数。Langchain.js 的RecursiveCharacterTextSplitter是最常用的切块器,它会按照分隔符优先级把文本切成块:
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 500, chunkOverlap: 80, separators: ['\n\n', '\n', '。', '!', '?', ';', ' ', ''], }); const chunks = await splitter.splitText(cleanedText); console.log(`切块数量:${chunks.length}`);chunkSize和chunkOverlap要结合文档语言和 Embedding 模型的最大输入长度来调。中文文档建议chunkSize在 400 到 800 之间,太小会导致上下文不完整,太大则 Embedding 语义被稀释。chunkOverlap控制在 10% 到 20%,让相邻块之间有重叠,避免关键句子正好落在切分边界上。
更进阶的方案是语义切块和“小到大检索”。语义切块利用 Embedding 判断文本主题变化,在主题切换处切分;小到大检索则是把小块用于检索,命中后把更大范围的上下文一并交给大模型。这两种方案都值得在文档质量要求高的场景下尝试。
向量化入库时,如果用的是 pgvector,Langchain.js 的PGVectorStore可以直接把Document列表写入数据库:
// src/modules/retrieval/vector-store.service.ts import { Injectable } from '@nestjs/common'; import { PGVectorStore } from '@langchain/community/vectorstores/pgvector'; import { ConfigService } from '@nestjs/config'; import { createEmbeddings } from '../../common/ai/llm.provider'; @Injectable() export class VectorStoreService { constructor(private readonly config: ConfigService) {} async getVectorStore() { return PGVectorStore.initialize(createEmbeddings(), { postgresConnectionOptions: { host: this.config.get('DB_HOST', '127.0.0.1'), port: this.config.get<number>('DB_PORT', 5432), user: this.config.get('DB_USER', 'postgres'), password: this.config.get('DB_PASSWORD', 'postgres'), database: this.config.get('DB_NAME', 'rag_kb'), }, tableName: 'document_embeddings', columns: { idColumnName: 'id', contentColumnName: 'content', metadataColumnName: 'metadata', vectorColumnName: 'embedding', }, }); } async addDocuments(documents: { pageContent: string; metadata: Record<string, any> }[]) { const store = await this.getVectorStore(); await store.addDocuments(documents); } }入库前要检查 pgvector 查询性能。数据量小时无所谓,数据量上来之后必须给向量列建索引。pgvector 支持 HNSW 和 IVFFlat 两种索引,HNSW 查询精度更高、构建慢一些;IVFFlat 构建快、查询快,但参数需要调。生产环境推荐 HNSW:
CREATE INDEX ON document_embeddings USING hnsw (embedding vector_cosine_ops);批量任务用 BullMQ 来做。文档上传后,NestJS 服务把“解析 + 切块 + 向量化”任务投递到 Redis 队列,Worker 消费任务。这样有两个好处:一是大批量文档不会阻塞 API 请求;二是任务失败可以重试,不会因为一条坏文档导致整个队列停住。任务状态可以记录在 documents 表里,前端管理后台轮询即可。
8. 检索问答 API 与批量任务
后端对外暴露的接口建议分为三组:文档管理接口、知识库处理接口、问答接口。下面是最小可用的接口设计:
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/documents/upload | 上传文档 |
| GET | /api/documents | 文档列表与状态 |
| DELETE | /api/documents/:id | 删除文档及对应向量 |
| POST | /api/documents/:id/process | 触发单文档入库 |
| POST | /api/admin/batch-import | 批量导入目录或压缩包 |
| POST | /api/chat/ask | 提问并获取回答 |
| GET | /api/chat/history/:sessionId | 获取会话历史 |
问答接口的请求和返回建议统一成 JSON,方便前端和外部系统对接:
// POST /api/chat/ask { "sessionId": "abc-123", "question": "企业知识库如何配置权限?" }{ "answer": "根据内部文档,企业知识库的权限配置分为三步……", "sources": [ { "documentId": "doc-001", "title": "知识库运维手册", "content": "……权限配置相关段落……" } ] }把sources(来源引用)返回给前端非常重要。企业场景里,用户不仅要答案,还要确认答案来自哪份文档。后续也可以做引用高亮,点击来源直接跳转到原文位置。
批量导入接口适合处理存量文档。常见做法是:管理员把一批 PDF/DOCX/MD 放入某个目录,或者上传 zip,服务端遍历文件后逐个投递任务。批量任务的进度接口返回“总数、成功数、失败数、正在处理数”,这样自然就实现了任务状态可视化。下面是一个 curl 调用批量任务的示例:
# 触发批量导入 curl -X POST http://127.0.0.1:3000/api/admin/batch-import \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <token>" \ -d '{ "sourceType": "directory", "sourcePath": "/data/kb-docs", "chunkSize": 500, "chunkOverlap": 80 }'Python 调用问答接口的示例也附上,方便后续接到其他系统:
import requests url = "http://127.0.0.1:3000/api/chat/ask" headers = {"Authorization": "Bearer <token>", "Content-Type": "application/json"} payload = { "sessionId": "py-client-1", "question": "RAG 的切块参数应该怎么调?" } resp = requests.post(url, json=payload, timeout=120) data = resp.json() print(data["answer"]) for source in data.get("sources", []): print(source["documentId"], source["title"])批量任务必须考虑失败重试策略。建议设置“失败后自动重试 2 次”,并对解析失败的文件记录具体原因。如果是单个文件格式损坏,跳过它比阻塞整批更重要;如果是模型 API 限流,等待时间补偿后重试效果更好。
9. 前端接入与生产部署
前端可以选择 Next.js、Vue3 + Element Plus 或任意能调 REST API 的框架。需要实现的页面包括:文档上传与状态页、知识库管理页、问答聊天页、系统配置页。聊天页建议使用 SSE(Server-Sent Events)或 WebSocket 实现流式输出,体验远好于等完整响应。
NestJS 返回 SSE 流很简单,配合@nestjs/platform-express和 Langchain.js 的stream方法即可。如果第一版只做 Web 端,可以先不接流式,直接返回完整 JSON,等基础功能稳定后再优化交互体验。
生产部署推荐 Docker Compose。下面是一个最小编排示例,包含 NestJS 应用、PostgreSQL、Redis、MinIO 四个服务:
# docker-compose.yml version: "3.8" services: postgres: image: pgvector/pgvector:pg16 container_name: rag-postgres environment: POSTGRES_DB: rag_kb POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: rag-redis ports: - "6379:6379" minio: image: minio/minio:latest container_name: rag-minio command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin ports: - "9000:9000" - "9001:9001" volumes: - miniodata:/data rag-server: build: context: . dockerfile: Dockerfile container_name: rag-server ports: - "3000:3000" env_file: - .env depends_on: postgres: condition: service_healthy redis: condition: service_started restart: unless-stopped volumes: pgdata: miniodata:很多同学问“宝塔支持部署 NestJS 项目吗”,答案是支持。宝塔里有两种方式:一种是直接用“Node.js 项目”功能,选择 Node 版本、入口文件和后端端口,加上 Supervisor 守护进程;另一种更推荐,就是上面这种 Docker Compose 方式,宝塔面板可以通过 Docker 管理器安装 Compose 服务,点击编排即可启动整套环境。两种方式都需要在宝塔的防火墙和安全组里放行对应端口。
部署时还要注意 Nginx 反向代理。前端页面走 80/443,/api路径代理到 3000 端口,WebSocket 或 SSE 需要额外配置proxy_http_version 1.1、proxy_set_header Connection等参数。HTTPS 证书用免费证书即可,接口层再配合 JWT 鉴权,避免服务直接暴露在公网。
10. 功能测试与效果验证
系统跑通之后,最怕的是“接口都通,但回答质量没法验收”。建议从四个维度做测试:基础流程、检索质量、生成质量、稳定性。
基础流程测试覆盖完整链路:上传一篇 PDF → 等待状态变为 completed → 提问文档相关内容 → 返回答案并带来源文档。这个过程建议用脚本自动跑,而不是每次都靠页面点。下面是判断成功的标准:
- 文档状态从 pending → processing → completed,chunkCount 大于 0。
- 问题能命中文档内容,答案包含来源文档。
- 文档删除后,再检索相同问题不再返回该文档内容。
检索质量测试要建立一份测试集。准备 20 到 50 个真实问题,每个问题标注“应该命中的文档或段落”,然后跑检索统计命中率。命中率低于 60% 时,优先调切块参数和检索 TopK,而不是换大模型。这个测试集很关键,之后每次调整 Embedding 模型、切块策略、向量索引,都用同一份测试集对比,效果好坏一目了然。
生成质量测试关注三件事:答案是否忠于资料、是否回答了问题、是否出现幻觉。建议把问题和对应答案整理成表格,由业务人员人工打分,分“准确、部分准确、错误、无中生有”四档。对“无中生有”的回答要重点排查:是检索没召回相关内容,还是 Prompt 约束不够,或者模型本身幻觉严重。从工程经验看,大部分幻觉问题都是检索召回不佳造成的,优先修检索链路。
稳定性测试可以用并发请求验证。比如用脚本同时发起 20 个问答请求,观察接口响应时间和服务内存。问答接口如果依赖外部模型 API,建议加上超时控制和熔断降级:模型 API 超时时,直接返回“服务繁忙,请稍后重试”,而不是让用户一直转圈。
11. 资源占用与性能观察
RAG 系统的性能瓶颈通常不在应用本身,而在三个地方:模型 API 的响应延迟、向量检索的耗时、文档解析的 CPU 占用。
先看延迟链路。一次问答请求的时间大致是:Embedding 检索(几十到几百毫秒) + 大模型生成(根据模型和输出长度,几秒到几十秒)。如果用户体感慢,优先看是不是模型生成太慢,而不是急着加服务器配置。可以在 NestJS 里给/api/chat/ask加日志,记录检索耗时和生成耗时分别多少:
[chat] 检索耗时: 210ms [chat] 生成耗时: 5.8s [chat] 总耗时: 6.1s这样能快速定位瓶颈。检索耗时长,检查 pgvector 索引是否生效,以及是否把全表扫描跑成了线性查询;生成耗时长,检查模型选择、输出长度、并发限流。
内存占用方面,Node.js 服务的堆内存可以通过process.memoryUsage()监控,容器部署用docker stats观察。向量检索本身不占太多应用内存,因为向量在 PostgreSQL 里;但如果用了本地 Embedding 模型,模型加载后的内存和显存占用就不能忽略。实际占用需以本机测试为准,不同模型差异很大。
降低资源占用的常用手段包括:Embedding 结果加缓存,相同或相似文本不重复向量化;检索 TopK 不要盲目调大,8 到 10 通常够用;批量入库任务控制并发数,避免同时解析大量文档导致 CPU 打满;PostgreSQL 开启连接池限制,防止应用实例多了之后把数据库连接耗尽。
12. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| pgvector 扩展无法创建 | 数据库版本过低或未安装插件 | 执行SELECT version();检查 PG 版本 | 使用 pgvector/pgvector 镜像重建数据库 |
| 检索返回空结果 | 向量库里没有数据,或 Embedding 维度不匹配 | 查询 document_embeddings 表是否有记录 | 检查入库任务状态和切块结果 |
| 中文切块效果差 | 分隔符没有包含中文标点 | 打印切块结果,观察断句位置 | 在 separators 里补充“。!?;”等分隔符 |
| 问答接口超时 | 模型 API 响应慢或网络不稳定 | 查看日志中的生成耗时 | 调大超时时间,接入流式输出 |
| Docker 中应用连不上数据库 | 连接地址写成 127.0.0.1 | 检查 compose 网络和环境变量 | 容器内改用服务名 postgres 访问 |
| 宝塔部署后页面 502 | Nginx 没配代理,或服务未启动 | 检查 Nginx 错误日志 | 正确配置 proxy_pass 到 3000 端口 |
| 模型 API 提示限流 | 请求并发过高 | 查看模型服务返回码 | 加入队列限流和退避重试 |
| 文档状态一直 processing | 任务队列消费失败 | 查看 BullMQ worker 日志 | 修复解析异常,设置失败重试 |
补一个容易被忽略的问题:Embedding 模型切换后,向量维度变了,但旧数据还在库里,会导致查询报错。切换 Embedding 模型时必须清空旧向量数据或重建一份新的向量表,不要混用不同模型的向量。建议在建表时把 Embedding 模型名称作为元数据保存,查询时带上模型条件,避免脏数据干扰检索。
另一个高频问题是“为什么答案没有引用我上传的新文档”。多数情况是切块后的向量已经入库,但检索的 TopK 结果里相关度排名不够,或者新文档和旧知识内容重叠,被旧文档挤掉了。解决思路是:把新文档的 metadata 加上时间戳和权重字段,检索时把时间权重纳入排序;或者直接做一个“指定文档 ID 范围检索”的功能,管理员可以先锁定期望的文档范围做验证。
13. 最佳实践与合规提醒
工程上建议先跑通最小闭环,再逐步扩展。第一版只需要:一个上传接口、一个切块入库流程、一个问答接口、一个简单管理页面。确认链路稳定后,再补权限、批量任务、流式输出、监控告警。不要一上来就设计十几个微服务,企业内部系统的核心是稳定,不是技术炫酷。
代码和配置层面,环境变量必须隔离。模型 API Key、数据库密码、Redis 密码统一放.env,并加入.gitignore。.env.example保留一份模板,方便同事快速配置本地环境。生产环境用 Docker 容器时,密钥通过环境变量注入,不要写死在镜像里。
文档管理上,建议为每份文档保存状态和版本。企业知识库不是一次性上传就结束,文档会被修订、删除、过期。给文档加 version 字段,入库时记录文件 hash,内容变化时自动触发重新解析,这样才能保证用户检索到的不是陈旧内容。批量任务要写日志,落库记录每批任务的开始时间、结束时间、成功数和失败原因,方便追溯。
合规提醒再强调一次:图像、文字、语音、人像相关素材必须确认授权;企业内部知识库涉及敏感信息时,要按角色做访问控制;使用云端模型服务时,确认数据的保密协议和输出内容归属。RAG 系统本质上是企业知识的分发中枢,权限设计如果漏了,检索接口可能变成数据泄露出口。上线前至少做一次安全测试,重点覆盖越权访问和未授权文档检索。
14. 总结与下一步
NestJS + Langchain.js 这套组合,最适合的场景就是“用 TypeScript 技术栈做企业级 AI 全栈应用”。它把入口、文档处理、向量化、检索问答、批量任务、部署全部串在一条链路上,相比纯 Python 方案,前后端语言统一,NestJS 的企业工程能力也让长期维护更可控。
如果你准备动手,第一阶段请优先验证三件事:第一,文档上传后能否成功完成切块和向量化入库;第二,中文文档的检索命中率是否达标;第三,问答接口是否稳定返回答案和来源。这三个点验证通过,系统就已经具备内部使用的基础了。最容易踩的坑是切块参数和 Embedding 模型选型,不要凭感觉设参数,一定要建测试集对比效果。
接下来可以继续扩展的方向包括:引入 Agentic RAG,让系统具备多步检索和工具调用能力;结合知识图谱,把实体关系与向量检索叠加,处理复杂关联问题;加入文档权限体系,让不同部门只能检索自己的知识范围;接入流式输出和前端引用高亮,把问答体验做到产品级;最后可以把系统接到企业微信、钉钉或飞书机器人上,让知识库真正成为团队日常工具。建议先收藏这篇文章,按流程跑通最小版本,再根据业务反馈逐步迭代。