深度解析:如何用 RAG 构建企业级智能知识库客服系统
基于 RAG 技术的企业级智能知识库系统实战经验分享
本文分享一套完整的企业级 RAG 知识库客服系统架构设计与实现方案
前言
大模型时代,企业接入 AI 客服似乎只需要调个 API。但真正落地时,你会发现核心问题不是模型不够强,而是模型不懂你的业务知识。
产品手册、服务政策、操作指南、FAQ……这些资料分散在各个角落。当客户问一个问题时,如何让 AI 基于这些资料给出准确、可信、可溯源的答案?
这就是 RAG(Retrieval-Augmented Generation)要解决的问题。
本文将分享如何构建一套完整的 RAG 智能知识库系统,支撑企业 AI 客服场景。
一、整体架构
先看整体架构:
核心流程:
文档上传 → 解析分块 → 向量化 → 存储入库
↓
用户提问 → 混合检索 → 重排序 → LLM 生成 → 流式返回(附引用)
系统技术栈:- 后端:NestJS 10 + TypeScript
- 数据库:PostgreSQL 18 + pgvector(1024 维向量,HNSW 索引)
- 向量模型:BGE-large-zh-v1.5(中文优化)
- LLM:阿里云 DashScope(可替换为其他 LLM 服务)
- Reranker:SiliconFlow(可替换为其他 Reranker 服务)
- 实时通信:SSE 流式输出—
二、文档处理:从文件到可检索的知识
2.1 多格式文档解析
企业文档格式多样,我们支持 PDF、Word、Excel、Markdown、TXT、CSV 等格式。核心是统一提取文本内容:
// 文档解析核心逻辑
async function parseDocument(file: Express.Multer.File): Promise<string> {
const ext = path.extname(file.originalname).toLowerCase();
switch (ext) {
case '.pdf':
return parsePDF(file.buffer); // pdf-parse
case '.docx':
return parseWord(file.buffer); // mammoth
case '.xlsx':
return parseExcel(file.buffer); // xlsx
case '.md':
return file.buffer.toString('utf-8');
case '.txt':
return file.buffer.toString('utf-8');
case '.csv':
return parseCSV(file.buffer); // csv-parse
default:
throw new UnsupportedFormatException(ext);
}
}
2.2 智能分块策略
分块是 RAG 的关键环节。块太大,检索精度低;块太小,丢失上下文。
我们采用语义分块策略:
// 语义分块配置
interface ChunkConfig {
maxChunkSize: number; // 最大块大小(tokens)
overlapSize: number; // 重叠大小
separators: string[]; // 分隔符优先级
keepMetadata: boolean; // 保留元数据
}
const defaultConfig: ChunkConfig = {
maxChunkSize: 512,
overlapSize: 50,
separators: ['\n\n', '\n', '。', ';', ',', ' '],
keepMetadata: true,
};
// 分块逻辑
function chunkText(text: string, config: ChunkConfig): Chunk[] {
const chunks: Chunk[] = [];
let start = 0;
while (start < text.length) {
let end = findChunkEnd(text, start, config);
chunks.push({
content: text.slice(start, end),
metadata: {
startIndex: start,
endIndex: end,
chunkIndex: chunks.length,
},
});
start = end - config.overlapSize; // 重叠部分
}
return chunks;
}
2.3 向量化存储
分块后,使用 BGE-large-zh-v1.5 模型生成 1024 维向量,存入 PostgreSQL + pgvector:
// 向量化并存储
async function vectorizeAndStore(knowledgeBaseId: string, chunks: Chunk[]) {
for (const chunk of chunks) {
// 生成向量
const embedding = await siliconflow.embed({
model: 'BAAI/bge-large-zh-v1.5',
input: chunk.content,
});
// 存入数据库
await prisma.knowledgeChunk.create({
data: {
knowledgeBaseId,
content: chunk.content,
embedding: embedding.data[0].embedding, // 1024维向量
metadata: chunk.metadata,
},
});
}
}
数据库模型设计:
model KnowledgeChunk {
id String @id @default(cuid())
knowledgeBaseId String
content String @db.Text
embedding Unsupported("vector(1024)")
metadata Json
createdAt DateTime @default(now())
knowledgeBase KnowledgeBase @relation(fields: [knowledgeBaseId], references: [id])
@@index([knowledgeBaseId])
@@index("embedding", ops: VectorCosineOps) // HNSW索引
}
三、混合检索:为什么单一检索不够?
3.1 三种检索方式的优劣
| 检索方式 | 优势 | 劣势 |
|---|---|---|
| 向量检索 | 语义理解,"意思相近"也能命中 | 对精确术语、型号不敏感 |
| 全文检索 | 精确匹配,速度快 | 无法理解语义 |
| 关键词检索 | 关键术语精准命中 | 无法理解上下文 |
企业场景中,用户提问往往混合了语义描述和精确术语:
“BGE-large-zh-v1.5 模型的向量维度是多少?”
这里 “BGE-large-zh-v1.5” 需要精确匹配,“向量维度” 需要语义理解。单一检索无法同时满足。
3.2 RRF 融合排序
我们采用 Reciprocal Rank Fusion(RRF) 算法融合三种检索结果:
// RRF 融合排序
function reciprocalRankFusion(
results: SearchResult[][],
k: number = 60
): SearchResult[] {
const scoreMap = new Map<string, number>();
for (const rankList of results) {
rankList.forEach((result, index) => {
const rank = index + 1;
const rrfScore = 1 / (k + rank);
const currentScore = scoreMap.get(result.id) || 0;
scoreMap.set(result.id, currentScore + rrfScore);
});
}
// 按融合分数排序
return Array.from(scoreMap.entries())
.sort((a, b) => b[1] - a[1])
.map(([id, score]) => ({ id, score }));
}
// 混合检索
async function hybridSearch(
query: string,
knowledgeBaseId: string
): Promise<SearchResult[]> {
const [vectorResults, fullTextResults, keywordResults] = await Promise.all([
vectorSearch(query, knowledgeBaseId),
fullTextSearch(query, knowledgeBaseId),
keywordSearch(query, knowledgeBaseId),
]);
return reciprocalRankFusion([
vectorResults,
fullTextResults,
keywordResults,
]);
}
3.3 Reranker 二次排序
RRF 融合后,再通过 Reranker 模型进行精细排序:
// Reranker 重排序
async function rerank(
query: string,
candidates: SearchResult[],
topK: number = 5
): Promise<SearchResult[]> {
const reranked = await siliconflow.rerank({
model: 'BAAI/bge-reranker-v2-m3',
query,
documents: candidates.map(c => c.content),
top_n: topK,
});
return reranked.results.map(r => ({
...candidates[r.index],
rerankScore: r.relevance_score,
}));
}
完整检索流程:
用户提问
↓
┌─────────────────────────────────────┐
│ 混合检索 │
│ ┌──────────┐ ┌──────────┐ ┌────────┐│
│ │ 向量检索 │ │ 全文检索 │ │关键词 ││
│ └────┬─────┘ └────┬─────┘ └───┬────┘│
│ └────────────┼────────────┘ │
│ ↓ │
│ RRF 融合排序 │
└────────────────────┬────────────────┘
↓
Reranker 重排序
↓
返回 Top-K 结果
四、可信问答:让每个答案都有出处
4.1 检索增强生成(RAG)
检索到相关文档块后,将其作为上下文传给 LLM:
// RAG 问答
async function ragAnswer(
query: string,
knowledgeBaseId: string,
chatHistory: Message[]
): Promise<ReadableStream> {
// 1. 检索相关文档
const searchResults = await hybridSearch(query, knowledgeBaseId);
const reranked = await rerank(query, searchResults, 5);
// 2. 构建 Prompt
const context = reranked
.map((r, i) => `[${i + 1}] ${r.content} (来源: ${r.source})`)
.join('\n\n');
const prompt = `你是一个专业的客服助手。请基于以下参考资料回答用户问题。
参考资料:
${context}
要求:
1. 只基于参考资料回答,不要编造信息
2. 如果参考资料中没有相关内容,明确告知用户
3. 在回答中标注引用来源,格式:[1][2]
4. 回答要准确、简洁、专业
用户问题:${query}`;
// 3. 流式生成回答
return streamCompletion(prompt, chatHistory);
}
4.2 引用溯源
每个回答都附带来源文件和页码,用户可以验证:
// 引用格式化
interface Citation {
index: number; // 引用编号 [1][2]
content: string; // 原文内容
source: string; // 来源文件名
page?: number; // 页码(PDF)
score: number; // 相关度分数
}
// 格式化回答
function formatAnswerWithCitations(
answer: string,
citations: Citation[]
): FormattedAnswer {
return {
answer,
citations: citations.map(c => ({
...c,
label: `[${c.index}] ${c.source}${c.page ? ` P${c.page}` : ''}`,
})),
};
}
五、流式输出:实时返回提升体验
5.1 SSE 流式传输
使用 Server-Sent Events 实现流式输出,用户无需等待完整回答:
// SSE 流式响应
async function streamToSSE(
response: Response,
stream: ReadableStream
) {
const reader = stream.getReader();
const decoder = new TextDecoder();
response.setHeader('Content-Type', 'text/event-stream');
response.setHeader('Cache-Control', 'no-cache');
response.setHeader('Connection', 'keep-alive');
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
response.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
}
response.write('data: [DONE]\n\n');
response.end();
}
前端接收:
// 前端流式接收
async function streamChat(message: string) {
const response = await fetch('/api/chat/stream', {
method: 'POST',
body: JSON.stringify({ message }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let fullAnswer = '';
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') break;
const { content } = JSON.parse(data);
fullAnswer += content;
updateUI(fullAnswer); // 实时更新界面
}
}
}
}
六、反馈闭环:越用越准的知识库
6.1 用户反馈收集
用户可以对 AI 回答点赞或点踩:
// 反馈收集
interface ChatFeedback {
chatId: string;
messageId: string;
rating: 'helpful' | 'not_helpful';
comment?: string;
createdAt: Date;
}
// 记录反馈
async function recordFeedback(feedback: ChatFeedback) {
await prisma.chatFeedback.create({ data: feedback });
// 更新知识块质量分
if (feedback.rating === 'not_helpful') {
await updateChunkQualityScore(feedback.messageId, -1);
} else {
await updateChunkQualityScore(feedback.messageId, +1);
}
}
6.2 知识库优化
基于反馈数据,持续优化知识库:
// 知识库优化策略
interface OptimizationStrategy {
// 1. 低质量回答分析
analyzeLowQualityAnswers(): Promise<LowQualityReport>;
// 2. 高频未覆盖问题
findUncoveredQuestions(): Promise<Question[]>;
// 3. 知识块质量评分
updateChunkScores(): Promise<void>;
// 4. 生成优化建议
generateSuggestions(): Promise<Suggestion[]>;
}
七、客服 Widget:一行代码嵌入
7.1 Widget 嵌入
在灵应 AI 平台创建知识库后,企业只需一行代码,即可在网站中嵌入 AI 客服:
<!-- 灵应 AI 客服 Widget - 一行代码嵌入 -->
<script
src="https://cdn.qiyeszh.com/widget.js"
data-kb-id="your-knowledge-base-id"
data-theme="light"
data-position="bottom-right"
data-primary-color="#4F46E5"
data-title="AI 客服助手"
data-welcome="你好!有什么可以帮您的?"
data-lang="zh"
></script>
7.2 可配置项
| 配置项 | 说明 | 默认值 |
|---|---|---|
data-kb-id | 知识库 ID | 必填 |
data-theme | 主题(light/dark) | light |
data-position | 位置 | bottom-right |
data-primary-color | 主题色 | #4F46E5 |
data-title | 标题 | AI 客服助手 |
data-welcome | 欢迎语 | 你好! |
data-lang | 语言(zh/en) | zh |
八、踩坑记录
8.1 分块大小选择
问题: 分块太小导致上下文丢失,分块太大导致检索不精准。
解决方案: 根据文档类型动态调整:
- 技术文档:400-600 tokens(细节密集)
- FAQ 类:200-300 tokens(一问一答)
- 长文章:500-800 tokens(保持上下文)
8.2 向量模型选择
问题: 通用多语言模型对中文支持不够好。
解决方案: 使用 BGE-large-zh-v1.5 中文优化模型,1024 维向量,中文语义理解更准确。
8.3 检索性能优化
问题: 数据量大时,向量检索变慢。
解决方案:
- 使用 HNSW 索引替代 IVFFlat
- 按知识库 ID 分区,减少扫描范围
- 热点查询缓存
-- HNSW 索引创建
CREATE INDEX ON knowledge_chunk
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 按知识库分区
CREATE INDEX ON knowledge_chunk (knowledge_base_id);
8.4 流式输出兼容性
问题: 部分浏览器或代理不支持 SSE。
解决方案: 降级为长轮询或 WebSocket:
// 降级策略
function getStreamMethod(request: Request): 'sse' | 'websocket' | 'polling' {
const accept = request.headers.get('accept');
if (accept?.includes('text/event-stream')) {
return 'sse';
}
if (request.headers.get('upgrade') === 'websocket') {
return 'websocket';
}
return 'polling';
}
九、性能指标
| 指标 | 数值 |
|---|---|
| 文档解析速度 | 100 页 PDF / 10 秒 |
| 向量化速度 | 1000 chunks / 分钟 |
| 检索延迟(Top-10) | < 200ms |
| 首 token 响应时间 | < 1 秒 |
| 完整回答时间(500字) | 3-5 秒 |
| 检索准确率(Top-5 命中) | > 90% |
十、总结
构建企业级 RAG 知识库,核心是解决三个问题:
- 知识怎么存? —— 多格式解析 + 智能分块 + 向量化
- 知识怎么找? —— 混合检索 + RRF 融合 + Reranker
- 知识怎么用? —— RAG 问答 + 引用溯源 + 流式输出
当这三个环节打通,企业文档就不再是躺在网盘里的静态文件,而是能够直接服务客户的活知识。
标签
#RAG #AI客服 #知识库 #向量检索 #混合检索 #NestJS #PostgreSQL #pgvector

246

被折叠的 条评论
为什么被折叠?



