从零构建企业级 AI 应用引擎(二):RAG 知识库系统从 0 到 1 落地

2 阅读10分钟

前言: 本文是《企业级 AI 应用引擎》系列第二篇。在上一篇中,我们完成了工作流编排系统和多 LLM 提供商集成。本篇将聚焦 RAG(检索增强生成) 知识库系统的完整实现,展示如何将企业私有知识与工作流系统深度融合。


项目地址:GitHub

首页.png

应用管理.png

工作流编辑.png

知识库管理.png

一、为什么需要 RAG?

在构建企业级 AI 应用时,我们面临着几个核心挑战:

  1. 数据隔离: 企业知识需要与公有模型隔离,确保数据安全
  2. 减少幻觉: 通用 LLM 容易产生事实性错误,需要私有知识校正
  3. 领域专业化: 企业特有的术语、流程、规范需要被 AI 理解
  4. 实时性: 企业知识频繁更新,无法通过微调及时同步

RAG(Retrieval-Augmented Generation) 应运而生。它的核心思想是:在生成回答前,先从私有知识库中检索相关信息,作为 LLM 的上下文输入

┌─────────────────────────────────────────────────────────────┐
│                     RAG 工作流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  用户提问 ──→ 向量化 ──→ 相似度搜索 ──→ 检索相关文档片段       │
│                              ↑                              │
│                              │                              │
│  最终回答 ←── LLM 生成 ←── 组合上下文 ←── 格式化             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、技术选型与架构设计

2.1 核心技术栈

组件技术选型理由
向量模型Ollama mxbai-embed-large开源可本地部署,1024 维,精度优秀
向量数据库PostgreSQL + Pgvector复用现有数据库,避免引入新组件
分块策略递归字符分块支持多格式智能分段,保持语义完整
相似度计算余弦相似度 (应用层)避免 B-Tree 索引限制,灵活可控

2.2 为什么不用专用向量数据库?

在技术选型阶段,我们对比了以下几种方案:

方案优点缺点决策
Pgvector复用 PostgreSQL,运维简单性能略逊于专用库✅ 选用
Milvus性能优秀,支持大规模需要额外部署维护❌ 过重
Chroma轻量级,易上手生产环境成熟度不足❌ 观望
Redis Stack高性能,多功能内存成本高❌ 成本考虑

对于中小规模企业应用 (知识库 < 100GB, 查询 QPS < 100),Pgvector 是性价比最高的选择

2.3 系统架构

┌──────────────────────────────────────────────────────────────────┐
│                        应用层 (Next.js + NestJS)                  │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐           │
│  │ 知识库管理   │     │ 文档管理     │    │ RAG 搜索测试 │           │
│  └─────────────┘    └─────────────┘    └─────────────┘           │
│                                                                  │
│  ┌─────────────────────────────────────────────────────┐         │
│  │              工作流编辑器 (RAG 节点)                   │         │
│  │  ┌─────────┐  ┌─────────┐  ┌─────────┐              │         │
│  │  │ 开始节点 │→ │ RAG 节点 │→ │ LLM 节点 │→│ 结束节点    │         │
│  │  └─────────┘  └─────────┘  └─────────┘              │         │
│  └─────────────────────────────────────────────────────┘         │
│                                                                  │
├──────────────────────────────────────────────────────────────────┤
│                        服务层 (KnowledgeBaseModule)               │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐          │
│  │KnowledgeBase│    │  Document   │    │   Vector    │          │
│  │  Service    │    │  Service    │    │  Service    │          │
│  └─────────────┘    └─────────────┘    └─────────────┘          │
│                                                                  │
│  ┌─────────────┐    ┌─────────────┐                             │
│  │  Chunking   │    │   File      │                             │
│  │  Service    │    │   Parser    │                             │
│  └─────────────┘    └─────────────┘                             │
│                                                                  │
├──────────────────────────────────────────────────────────────────┤
│                        数据层 (PostgreSQL + Pgvector)             │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────────┐  ┌──────────────────┐                     │
│  │ knowledge_bases  │  │    documents     │                     │
│  ├──────────────────┤  ├──────────────────┤                     │
│  │ id               │  │ id               │                     │
│  │ name             │  │ knowledge_base_id│                     │
│  │ app_id           │  │ file_path        │                     │
│  │ config (JSON)    │  │ status           │                     │
│  └──────────────────┘  └──────────────────┘                     │
│                                                                  │
│  ┌──────────────────────────────────────────┐                   │
│  │           document_chunks                 │                   │
│  ├──────────────────────────────────────────┤                   │
│  │ id, document_id, content                 │                   │
│  │ embedding vector(1024)  ← Pgvector       │                   │
│  │ metadata (JSON)                          │                   │
│  └──────────────────────────────────────────┘                   │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

三、核心功能实现

3.1 后端模块设计

RAG 知识库系统由 5 个核心服务组成,每个服务职责清晰:

// apps/server/src/modules/knowledge-base/knowledge-base.module.ts
@Module({
  imports: [ConfigModule, PrismaModule],
  controllers: [KnowledgeBaseController],
  providers: [
    KnowledgeBaseService,  // 知识库 CRUD + 配额管理
    DocumentService,       // 文档状态追踪
    VectorService,         // 向量生成 (Ollama API)
    ChunkingService,       // 递归字符分块
  ],
})
export class KnowledgeBaseModule {}

服务职责划分

服务核心职责关键方法
KnowledgeBaseService知识库 CRUD、配额管理、统计信息create(), search(), getStats()
DocumentService文档上传、状态管理、删除upload(), delete(), updateStatus()
VectorService调用 Ollama 生成向量嵌入generateEmbedding(text)
ChunkingService文本分块、保持语义完整chunk(text, options)
FileParser5 种格式文件解析parse(file)

3.2 向量相似度搜索实现

这是 RAG 系统的核心。由于 Pgvector 的 B-Tree 索引对 1024 维向量有限制 (最大 2704 字节),我们采用应用层计算余弦相似度的方案:

// apps/server/src/modules/knowledge-base/knowledge-base.service.ts
async search(
  knowledgeBaseId: string,
  query: string,
  topK: number,
  threshold: number,
): Promise<RAGSearchResult[]> {
  // 1. 生成查询向量
  const queryEmbedding = await this.vectorService.generateEmbedding(query);
  
  // 2. 获取该知识库的所有分块
  const chunks = await this.prisma.documentChunk.findMany({
    where: { document: { knowledgeBaseId } },
  });
  
  // 3. 应用层计算余弦相似度
  const results = chunks.map(chunk => ({
    chunk,
    similarity: this.cosineSimilarity(queryEmbedding, chunk.embedding),
  }));
  
  // 4. 过滤并排序
  return results
    .filter(r => r.similarity >= threshold)
    .sort((a, b) => b.similarity - a.similarity)
    .slice(0, topK);
}

private cosineSimilarity(a: number[], b: number[]): number {
  const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
  const normA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
  const normB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
  return dotProduct / (normA * normB);
}

为什么不用数据库的向量索引?

  1. B-Tree 限制: PostgreSQL B-Tree 索引行大小限制 2704 字节,1024 维 float32 向量占用 4096 字节
  2. Ivfflat 需要扩展: Pgvector 的 Ivfflat 索引需要单独安装,增加运维复杂度
  3. 规模适中: 对于 < 10 万条分块的场景,应用层计算性能可接受 (P95 < 500ms)

3.3 递归字符分块算法

文本分块的质量直接影响 RAG 检索效果。我们采用递归字符分块策略,按照分隔符优先级逐级分割:

// apps/server/src/modules/knowledge-base/chunking.service.ts
chunk(text: string, options: ChunkOptions): string[] {
  const { chunkSize = 500, chunkOverlap = 50, separators = ['\n\n', '\n', '.', '!', '?', ' '] } = options;
  
  // 递归分块函数
  const splitText = (text: string, separators: string[]): string[] => {
    if (text.length <= chunkSize) {
      return [text];
    }
    
    // 尝试用当前分隔符分割
    for (const separator of separators) {
      const splits = text.split(separator);
      if (splits.length > 1) {
        // 递归处理每个分段
        const chunks = splits.reduce((acc: string[], split) => {
          const last = acc[acc.length - 1];
          if (last && (last + separator + split).length <= chunkSize) {
            acc[acc.length - 1] = last + separator + split;
          } else {
            acc.push(split);
          }
          return acc;
        }, []);
        
        return chunks.flatMap(chunk => splitText(chunk, separators.slice(1)));
      }
    }
    
    // 无法分割时,按 chunkSize 硬分割
    return text.match(new RegExp(`.{1,${chunkSize}}`, 'g')) || [text];
  };
  
  return splitText(text, separators);
}

分隔符优先级策略:

段落 (\n\n) → 行 (\n) → 句子 (.!?) → 单词 (空格) → 硬分割

这种策略确保分块在语义上尽可能完整,避免在句子中间切断。

3.4 文件格式解析

支持 5 种常见格式的文档解析:

// apps/server/src/modules/knowledge-base/utils/file-parser.ts
async parse(file: Express.Multer.File): Promise<string> {
  const ext = path.extname(file.originalname).toLowerCase();
  
  switch (ext) {
    case '.txt':
      return file.buffer.toString('utf-8');
    
    case '.json':
      const json = JSON.parse(file.buffer.toString('utf-8'));
      return this.jsonToText(json);
    
    case '.csv':
      const rows = parse(file.buffer.toString('utf-8'), { columns: true });
      return rows.map(row => Object.entries(row)
        .map(([k, v]) => `${k}: ${v}`).join(', ')
      ).join('\n');
    
    case '.docx':
      const result = await extractText(file.buffer);
      return result.body;
    
    case '.pdf':
      // 临时方案:pdf-parse 存在 ESM/CJS 兼容性问题
      return '[PDF content - parsing not available]';
    
    default:
      throw new BadRequestException(`Unsupported file type: ${ext}`);
  }
}

PDF 解析问题: 由于 pdf-parse 库的 ESM/CJS 兼容性问题,目前返回占位符文本。长期解决方案是安装 pdfjs-dist 或使用命令行工具。

3.5 工作流 RAG 节点集成

RAG 节点是工作流系统中的"知识检索器",允许用户在工作流中动态查询知识库:

节点设计

// apps/web/src/components/workflow/nodes/rag-node.tsx
export const RagNode = memo(function RagNode(props: NodeProps) {
  return (
    <BaseNode
      {...props}
      icon={<Database className="w-full h-full" />}
      colorClass="bg-cyan-500"  // 青色主题
      gradientFrom="from-cyan-500"
      gradientTo="to-cyan-600"
    />
  );
});

配置面板

// apps/web/src/components/workflow/config-panel.tsx
interface RAGNodeConfig {
  knowledgeBaseId: string;      // 选择的知识库
  query: string;                // 查询模板,支持变量引用
  topK: number;                 // 返回数量 (1-20)
  similarityThreshold: number;  // 相似度阈值 (0-1)
  outputFormat: 'raw' | 'combined'; // 输出格式
}

变量引用语法:

{{ nodes.start.outputs.query }}
{{ nodes.llm_node.outputs.answer }}

在运行时自动替换为上游节点的实际输出。

执行器实现

// packages/core/src/node-executors/rag-executor.ts
async execute(config: RAGNodeConfig, context: ExecutionContext) {
  // 1. 渲染查询模板
  const renderedQuery = this.renderTemplate(config.query, context);
  
  // 2. 执行 RAG 搜索
  const searchResults = await this.executeRagSearch(
    config.knowledgeBaseId,
    renderedQuery,
    config.topK || 5,
    config.similarityThreshold || 0.3,
  );
  
  // 3. 格式化输出
  const output = config.outputFormat === 'combined'
    ? searchResults.combinedContext
    : searchResults.results;
  
  return { success: true, output };
}

四、踩坑记录

4.1 向量索引 B-Tree 限制

现象: 插入向量时报错 index row size 4096 exceeds maximum 2704

原因: PostgreSQL B-Tree 索引对行大小有限制 (2704 字节),而 mxbai-embed-large 生成的 1024 维 float32 向量占用 4096 字节。

解决方案:

-- 删除 B-Tree 索引
DROP INDEX IF EXISTS idx_chunk_embedding;

-- 改用应用层计算余弦相似度
-- (见 3.2 节代码)

后续优化: 安装 Pgvector 的 Ivfflat 索引扩展,支持大规模向量检索。

4.2 API 路径双重前缀

现象: 前端请求返回 404,URL 为 /api/api/knowledge-bases

原因: api.ts 的 baseURL 已配置 /api,但 hooks 中又添加了 /api 前缀。

// ❌ 错误写法
const api = axios.create({ baseURL: '/api' });
export const useKnowledgeBases = () => {
  return useQuery(['kb'], () => api.get('/api/knowledge-bases')); // 双重前缀
};

// ✅ 正确写法
export const useKnowledgeBases = () => {
  return useQuery(['kb'], () => api.get('/knowledge-bases')); // 移除前缀
};

修复范围: 修改 9 个 hooks 文件,共 10 处路径。

4.3 DTO 参数验证类型转换

现象: 搜索接口返回 400 topk must be a number

原因: URL 查询参数是字符串,但 DTO 验证需要 number 类型。

解决方案: 使用 class-transformer@Transform@Type 装饰器

// apps/server/src/modules/knowledge-base/dto/knowledge-base.dto.ts
export class SearchKnowledgeBaseDto {
  @Transform(({ value }) => parseInt(value, 10))
  @Type(() => Number)
  @IsInt()
  @Min(1)
  @Max(20)
  topk: number;
  
  @Transform(({ value }) => parseFloat(value))
  @Type(() => Number)
  @IsNumber()
  @Min(0)
  @Max(1)
  threshold: number;
}

4.4 DTO 循环引用

现象: 后端启动失败 Cannot access RAGConfigDto before initialization

原因: CreateKnowledgeBaseDtoRAGConfigDto 之前定义,导致循环引用。

解决方案: 调整类定义顺序

// ✅ 正确顺序
export class RAGConfigDto { ... }  // 先定义被引用的类

export class CreateKnowledgeBaseDto {
  @ValidateNested()
  @Type(() => RAGConfigDto)
  config: RAGConfigDto;  // 后引用
}

4.5 OOM 内存溢出

现象: 后端服务崩溃 JavaScript heap out of memory

原因: Node.js 默认内存限制约 4GB,处理大文件时不足。

解决方案: 增加内存限制

# 启动脚本中添加
NODE_OPTIONS="--max-old-space-size=8192" pnpm dev:server

五、测试与验收

5.1 测试环境

组件配置
操作系统macOS (M4 16GB)
Node.jsv24.13.0
PostgreSQL15 + Pgvector
向量模型Ollama mxbai-embed-large

5.2 测试结果汇总

测试类别总数通过失败通过率
后端 API1110191%
前端功能880100%
工作流集成550100%
总计2423196%

唯一失败项: PDF 解析 (返回占位符文本)

5.3 测试数据

  • 创建知识库: 2 个
  • 上传文档: 3 个 (1 TXT + 1 PDF + 1 测试文档)
  • 执行搜索: 10+ 次查询
  • 最高相似度: 0.59 (查询"机器学习")
  • 相似度分布: 0.3-0.7 (符合预期)

5.4 核心功能验收

功能状态说明
知识库 CRUD创建、查询、更新、删除正常
文档上传5 种格式支持,状态追踪正常
文本分块ChunkingService 工作正常
向量生成Ollama mxbai-embed-large 正常
RAG 搜索余弦相似度计算正常
RAG 节点拖拽可拖拽到画布,显示正确
RAG 节点配置配置面板完整,参数可调节
工作流保存保存成功,配置正确持久化

六、性能优化与后续规划

6.1 当前性能指标

指标数值说明
单次搜索延迟P95 < 500ms1 万条分块规模
文档上传速度~100KB/s包含解析 + 分块 + 向量化
向量生成延迟~200ms/次Ollama 本地部署
内存占用~2GB空闲状态,峰值 6GB

6.2 短期优化 (1-2 周)

  1. PDF 解析: 安装 pdfjs-dist 解决 PDF 解析问题
  2. 全局导航: 添加统一的 Sidebar 或 Header 导航
  3. 错误处理: 完善前端加载状态和错误提示

6.3 中期优化 (3-4 周)

  1. 批量上传: 支持多文件同时上传,提升效率
  2. 文档预览: 上传前预览文档内容
  3. OCR 功能: PDF 转图片,支持扫描版文档

6.4 长期优化 (1-2 月)

  1. 向量索引: 安装 Pgvector Ivfflat 索引,提升大规模检索性能
  2. 多语言支持: 支持中英文混合检索
  3. 权限管理: 知识库级别的访问控制
  4. 性能基准: 建立性能测试基准,持续优化

七、总结

本文详细介绍了 RAG 知识库系统从 0 到 1 的完整实现过程,包括:

  1. 技术选型: 为什么选择 Pgvector 而非专用向量数据库
  2. 架构设计: 5 个核心服务的职责划分和协作关系
  3. 核心实现: 向量相似度搜索、递归字符分块、文件格式解析、工作流集成
  4. 踩坑记录: 5 个典型问题及解决方案
  5. 测试验收: 96% 通过率,核心功能全部可用

关键收获:

  • ✅ 复用现有 PostgreSQL 数据库,降低运维复杂度
  • ✅ 应用层计算余弦相似度,规避 B-Tree 索引限制
  • ✅ 递归字符分块策略,保持语义完整性
  • ✅ 工作流 RAG 节点集成,实现知识与编排的深度融合

下一步: 在下一篇中,我将探索多 Agent 协作系统,展示如何让多个 AI Agent 在工作流中协同完成复杂任务。


附录:核心代码文件索引

后端文件

apps/server/src/modules/knowledge-base/
├── knowledge-base.module.ts       # 模块定义
├── knowledge-base.controller.ts   # API 控制器 (11 个端点)
├── knowledge-base.service.ts      # 知识库服务
├── document.service.ts            # 文档服务
├── vector.service.ts              # 向量服务
├── chunking.service.ts            # 分块服务
├── dto/knowledge-base.dto.ts      # DTO 定义
└── utils/file-parser.ts           # 文件解析器

前端文件

apps/web/src/
├── app/(dashboard)/knowledge-bases/
│   ├── page.tsx                   # 知识库列表页
│   └── [id]/
│       ├── documents/page.tsx     # 文档管理页
│       └── settings/page.tsx      # 配置页
├── components/workflow/
│   ├── nodes/rag-node.tsx         # RAG 节点组件
│   └── knowledge-base-select.tsx  # 知识库选择器
├── hooks/
│   ├── use-knowledge-bases.ts     # 知识库 Hooks
│   └── use-documents.ts           # 文档 Hooks
└── lib/rag-config.ts              # RAG 配置常量

核心包

packages/core/src/
├── node-executors/rag-executor.ts  # RAG 执行器
└── workflow-executor.ts            # 工作流执行器 (集成 RAG)

数据库迁移

prisma/migrations/
├── 20260320114653_add_knowledge_base_rag/
│   └── migration.sql              # 新增 3 个表
└── 20260320142000_fix_vector_index/
    └── migration.sql              # 修复向量索引

项目地址: GitHub
上一篇: 从零构建企业级 AI 应用引擎(一):NestJS + Next.js 全栈架构设计与实践