前言: 本文是《企业级 AI 应用引擎》系列第二篇。在上一篇中,我们完成了工作流编排系统和多 LLM 提供商集成。本篇将聚焦 RAG(检索增强生成) 知识库系统的完整实现,展示如何将企业私有知识与工作流系统深度融合。
项目地址:GitHub
一、为什么需要 RAG?
在构建企业级 AI 应用时,我们面临着几个核心挑战:
- 数据隔离: 企业知识需要与公有模型隔离,确保数据安全
- 减少幻觉: 通用 LLM 容易产生事实性错误,需要私有知识校正
- 领域专业化: 企业特有的术语、流程、规范需要被 AI 理解
- 实时性: 企业知识频繁更新,无法通过微调及时同步
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) |
| FileParser | 5 种格式文件解析 | 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);
}
为什么不用数据库的向量索引?
- B-Tree 限制: PostgreSQL B-Tree 索引行大小限制 2704 字节,1024 维 float32 向量占用 4096 字节
- Ivfflat 需要扩展: Pgvector 的 Ivfflat 索引需要单独安装,增加运维复杂度
- 规模适中: 对于 < 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
原因: CreateKnowledgeBaseDto 在 RAGConfigDto 之前定义,导致循环引用。
解决方案: 调整类定义顺序
// ✅ 正确顺序
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.js | v24.13.0 |
| PostgreSQL | 15 + Pgvector |
| 向量模型 | Ollama mxbai-embed-large |
5.2 测试结果汇总
| 测试类别 | 总数 | 通过 | 失败 | 通过率 |
|---|---|---|---|---|
| 后端 API | 11 | 10 | 1 | 91% |
| 前端功能 | 8 | 8 | 0 | 100% |
| 工作流集成 | 5 | 5 | 0 | 100% |
| 总计 | 24 | 23 | 1 | 96% |
唯一失败项: 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 < 500ms | 1 万条分块规模 |
| 文档上传速度 | ~100KB/s | 包含解析 + 分块 + 向量化 |
| 向量生成延迟 | ~200ms/次 | Ollama 本地部署 |
| 内存占用 | ~2GB | 空闲状态,峰值 6GB |
6.2 短期优化 (1-2 周)
- PDF 解析: 安装
pdfjs-dist解决 PDF 解析问题 - 全局导航: 添加统一的 Sidebar 或 Header 导航
- 错误处理: 完善前端加载状态和错误提示
6.3 中期优化 (3-4 周)
- 批量上传: 支持多文件同时上传,提升效率
- 文档预览: 上传前预览文档内容
- OCR 功能: PDF 转图片,支持扫描版文档
6.4 长期优化 (1-2 月)
- 向量索引: 安装 Pgvector Ivfflat 索引,提升大规模检索性能
- 多语言支持: 支持中英文混合检索
- 权限管理: 知识库级别的访问控制
- 性能基准: 建立性能测试基准,持续优化
七、总结
本文详细介绍了 RAG 知识库系统从 0 到 1 的完整实现过程,包括:
- 技术选型: 为什么选择 Pgvector 而非专用向量数据库
- 架构设计: 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 全栈架构设计与实践