检索增强生成(RAG)是LangChain最强大的能力之一,它让你能够使用自有数据扩展大语言模型的知识。本章将带你从零构建一个完整的RAG系统。
🎯 RAG到底解决了什么问题?
传统LLM vs RAG对比:
| 传统LLM直接回答 | RAG增强回答 | |
|---|---|---|
| 知识来源 | 仅限训练时的数据 | 训练数据 + 你的专属文档 |
| 信息时效性 | 训练截止日期(如2023年7月) | 可包含最新文档 |
| 回答准确性 | 可能“编造”事实 | 基于真实文档,减少幻觉 |
| 适用场景 | 通用知识问答 | 企业知识库、法律文档、技术手册等专业领域 |
RAG的解决方案:
用户问题 → 从知识库检索相关文档 → 组合文档和问题 → 生成准确回答
1. RAG系统架构
1.1 RAG工作流程
1.2 核心组件详解
1. 文档加载器 (Document Loaders)
作用:从各种来源“读取”文档,转换成统一格式
| 加载器类型 | 处理格式 | 输出 |
|---|---|---|
| PDF加载器 | PDF文件 | 提取文本和元数据(页码等) |
| 网页加载器 | 网页URL | 网页主要内容(可过滤广告等) |
| 文本加载器 | .txt, .md等 | 纯文本内容 |
| 数据库加载器 | SQL, NoSQL | 查询结果作为文档 |
为什么需要:现实中的数据分散在各种格式中,需要统一转换为LangChain能处理的Document对象。
2. 文本分割器 (Text Splitters)
作用:将长文档切成适合处理的“小块”(chunks)
关键概念:
-
Chunk Size:每个块的大小(如1000字符)
-
Chunk Overlap:块之间的重叠(如200字符)→ 避免在分割点丢失上下文
-
分隔符优先级:
["\n\n", "\n", "。", ",", " "]→ 按顺序尝试分割,保持语义完整
为什么需要:
-
LLM有输入长度限制(如4096 token)
-
太长的文档难以精准检索相关信息
-
小块文档能更精确地定位到相关信息点
3. 向量化模型 (Embedding Models)
作用:将文本转换为数学向量(一组数字),让计算机能“理解”语义
核心思想:
-
语义相似的文本 → 向量在空间中位置接近
-
向量相似度计算(如余弦相似度)→ 判断文本相关性
示例:
"我喜欢苹果" → [0.23, -0.45, 0.78, ...] (300维向量)
"我爱吃水果" → [0.25, -0.42, 0.75, ...] ← 这两个向量很接近!
"今天天气很好" → [-0.12, 0.33, -0.56, ...] ← 这个就远多了
4. 向量数据库 (Vector Store)
作用:存储文档向量,实现快速相似度搜索
类比:就像图书馆的索引系统
-
传统数据库:按书名、作者精确查找
-
向量数据库:按“内容相似度”模糊查找
| 存储类型 | 特点 | 适用场景 |
|---|---|---|
| 内存向量库 | 快,但程序关闭后丢失 | 开发测试、临时处理 |
| ChromaDB | 轻量级,可持久化 | 中小型项目、生产环境 |
| Pinecone | 云端托管,高性能 | 大规模生产系统 |
5. 检索器 (Retriever)
作用:根据用户问题,从向量库中找到最相关的文档块
检索策略:
-
相似度检索:找向量最相似的文档
-
MMR检索:在相似性和多样性间平衡(避免返回内容重复的文档)
-
元数据过滤:只检索特定类别/标签的文档
关键参数:k=4 → 返回最相关的4个文档块
6. RAG提示词模板 (Prompt Template)
作用:将“用户问题”和“检索到的文档”组合成给LLM的完整提示
标准格式:
基于以下上下文信息回答问题。如果上下文不包含答案,请说明你不知道。
上下文:
[文档1内容...]
[文档2内容...]
问题:{用户问题}
请提供准确、完整的回答:
设计要点:
-
明确要求模型基于上下文回答
-
提供“不知道”的出口,减少幻觉
-
结构化组织上下文,便于模型理解
2. 构建基础RAG系统
2.1 环境准备
# 安装所需依赖
npm install langchain @langchain/openai chromadb pdf-parse
2.2 完整RAG实现
import { ChatOpenAI } from "@langchain/openai";
import { OpenAIEmbeddings } from "@langchain/openai-embeddings";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { Document } from "@langchain/core/documents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables";
class BasicRAGSystem {
private vectorStore: MemoryVectorStore | null = null;
private model: ChatOpenAI;
private embeddings: OpenAIEmbeddings;
constructor() {
// 初始化模型
this.model = new ChatOpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/v1",
model: "deepseek-chat",
temperature: 0.3
});
// 初始化嵌入模型
this.embeddings = new OpenAIEmbeddings({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/v1"
});
}
/**
* 步骤1:处理文档
*/
async processDocuments(documents: string[]): Promise<void> {
console.log("开始处理文档...");
// 1. 创建文本分割器
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000, // 每个块的最大字符数
chunkOverlap: 200, // 块之间的重叠字符数
separators: ["\n\n", "\n", "。", "!", "?", ";", ",", "、", " "] // 中文友好的分隔符
});
// 2. 分割文档
const docs: Document[] = [];
for (const [index, content] of documents.entries()) {
const splits = await textSplitter.splitText(content);
for (const split of splits) {
docs.push(new Document({
pageContent: split,
metadata: {
source: `doc_${index}`,
length: split.length,
processed_at: new Date().toISOString()
}
}));
}
}
console.log(`文档分割完成,共 ${docs.length} 个文本块`);
// 3. 创建向量存储
this.vectorStore = await MemoryVectorStore.fromDocuments(
docs,
this.embeddings
);
console.log("向量存储创建完成");
}
/**
* 步骤2:构建检索链
*/
async buildRetrievalChain(k: number = 4) {
if (!this.vectorStore) {
throw new Error("请先处理文档");
}
// 创建检索器
const retriever = this.vectorStore.asRetriever({
k: k, // 检索结果数量
searchType: "similarity" // 或 "mmr" 最大边际相关性
});
// RAG提示词模板
const prompt = ChatPromptTemplate.fromTemplate(`
基于以下上下文信息回答问题。如果上下文不包含答案,请说明你不知道。
上下文:
{context}
问题:{question}
请提供准确、完整的回答:
`);
// 构建RAG链
const ragChain = RunnableSequence.from([
{
context: retriever.pipe(this.formatDocuments),
question: new RunnablePassthrough()
},
prompt,
this.model,
new StringOutputParser()
]);
return ragChain;
}
/**
* 辅助方法:格式化检索到的文档
*/
private formatDocuments(docs: Document[]): string {
return docs
.map((doc, i) => `[文档${i + 1}]\n${doc.pageContent}\n`)
.join("\n---\n");
}
/**
* 步骤3:测试RAG系统
*/
async testSystem() {
const ragChain = await this.buildRetrievalChain();
// 测试查询
const testQueries = [
"文档的主要内容是什么?",
"有哪些关键概念?",
"请总结文档的要点。"
];
for (const query of testQueries) {
console.log(`\n=== 查询:${query} ===`);
const startTime = Date.now();
const response = await ragChain.invoke(query);
console.log(`回答(耗时:${Date.now() - startTime}ms):`);
console.log(response);
}
}
}
// 使用示例
async function main() {
const ragSystem = new BasicRAGSystem();
// 示例文档(实际中可以从文件加载)
const sampleDocuments = [
`LangChain是一个用于开发大语言模型应用的框架。它提供了模块化组件,包括模型调用、提示词模板、链、记忆和代理等。
核心优势:
1. 简化与各种LLM的交互
2. 提供可组合的工作流(链)
3. 支持检索增强生成(RAG)
4. 内置工具和代理系统
适用场景:
- 智能客服
- 文档分析
- 代码生成
- 数据提取`,
`RAG(检索增强生成)技术通过以下步骤工作:
1. 文档处理:加载、分割、向量化文档
2. 检索:根据问题找到相关文档片段
3. 生成:结合检索结果和问题生成回答
这种方法可以:
- 减少模型幻觉
- 提供最新信息
- 利用私有数据`
];
// 处理文档
await ragSystem.processDocuments(sampleDocuments);
// 测试查询
await ragSystem.testSystem();
}
3. 高级文档处理
3.1 支持多种文档格式
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
import { TextLoader } from "langchain/document_loaders/fs/text";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { DirectoryLoader } from "langchain/document_loaders/fs/directory";
class MultiFormatDocumentProcessor {
/**
* 从PDF加载文档
*/
async loadPDF(filePath: string): Promise<Document[]> {
const loader = new PDFLoader(filePath, {
splitPages: true,
parsedItemSeparator: "" // 页面内容分隔符
});
const docs = await loader.load();
// 添加PDF特定元数据
return docs.map(doc => ({
...doc,
metadata: {
...doc.metadata,
format: "pdf",
source: filePath
}
}));
}
/**
* 从网页加载文档
*/
async loadWebPage(url: string): Promise<Document[]> {
const loader = new CheerioWebBaseLoader(url, {
selector: "body" // 或特定选择器如 "article", ".content"
});
const docs = await loader.load();
return docs.map(doc => ({
...doc,
metadata: {
...doc.metadata,
format: "web",
url: url,
fetched_at: new Date().toISOString()
}
}));
}
/**
* 从目录加载多种格式文档
*/
async loadDirectory(directoryPath: string): Promise<Document[]> {
const loader = new DirectoryLoader(directoryPath, {
".pdf": (path: string) => new PDFLoader(path),
".txt": (path: string) => new TextLoader(path),
".md": (path: string) => new TextLoader(path),
});
return await loader.load();
}
/**
* 智能文档分割策略
*/
async smartSplitDocuments(documents: Document[]): Promise<Document[]> {
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
separators: [
"\n\n", "\n", "。", "!", "?", ";", // 中文标点
".", "!", "?", ";", // 英文标点
"</p>", "</h1>", "</h2>", "</h3>", // HTML标签
"## ", "### ", "#### ", // Markdown标题
"\n• ", "\n- ", "\n* ", // 列表项
"\n1. ", "\n2. ", "\n3. " // 有序列表
]
});
const allSplits: Document[] = [];
for (const doc of documents) {
const splits = await textSplitter.splitDocuments([doc]);
// 为每个分割块添加位置信息
const enhancedSplits = splits.map((split, index) => ({
...split,
metadata: {
...split.metadata,
chunk_index: index,
total_chunks: splits.length,
original_length: doc.pageContent.length
}
}));
allSplits.push(...enhancedSplits);
}
console.log(`分割完成:${documents.length} 个文档 → ${allSplits.length} 个文本块`);
return allSplits;
}
}
3.2 元数据增强检索
import { MemoryVectorStore } from "langchain/vectorstores/memory";
class MetadataEnhancedRAG {
private vectorStore: MemoryVectorStore | null = null;
/**
* 创建带元数据的向量存储
*/
async createVectorStoreWithMetadata(documents: Document[]) {
// 处理文档时添加丰富元数据
const enhancedDocs = documents.map((doc, index) => {
const content = doc.pageContent;
// 提取文档特征
const wordCount = content.split(/\s+/).length;
const charCount = content.length;
const hasCode = /```[\s\S]*?```|`[^`]+`/.test(content);
const hasLinks = /https?://[^\s]+/.test(content);
const hasQuestions = /[??]$/.test(content);
return new Document({
pageContent: content,
metadata: {
...doc.metadata,
doc_id: `doc_${index}_${Date.now()}`,
word_count: wordCount,
char_count: charCount,
has_code: hasCode,
has_links: hasLinks,
has_questions: hasQuestions,
content_type: this.detectContentType(content),
language: this.detectLanguage(content),
keywords: this.extractKeywords(content),
processed_at: new Date().toISOString()
}
});
});
// 创建向量存储
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/v1"
});
this.vectorStore = await MemoryVectorStore.fromDocuments(
enhancedDocs,
embeddings
);
}
/**
* 基于元数据的混合检索
*/
async hybridRetrieval(query: string, options = {
similarityWeight: 0.7,
metadataWeight: 0.3,
topK: 5
}) {
if (!this.vectorStore) {
throw new Error("向量存储未初始化");
}
// 1. 基于相似度的检索
const similarityResults = await this.vectorStore.similaritySearch(
query,
options.topK * 2
);
// 2. 基于元数据的检索(示例:优先检索包含代码的文档)
const queryKeywords = this.extractKeywords(query);
const queryHasCode = /代码|编程|函数|算法/.test(query);
const filteredResults = similarityResults
.filter(doc => {
// 根据查询特性调整权重
let score = 1.0;
if (queryHasCode && doc.metadata.has_code) {
score *= 1.5; // 提升代码相关文档的权重
}
// 关键词匹配加分
const docKeywords = doc.metadata.keywords || [];
const keywordMatches = queryKeywords.filter(kw =>
docKeywords.includes(kw)
).length;
score *= (1 + keywordMatches * 0.2);
doc.metadata._hybrid_score = score;
return true;
})
.sort((a, b) => (b.metadata._hybrid_score || 0) - (a.metadata._hybrid_score || 0))
.slice(0, options.topK);
return filteredResults;
}
// 辅助方法
private detectContentType(content: string): string {
if (/```[\s\S]*?```|function|class|def|import/.test(content)) {
return "code";
} else if (/#+|##+ |\n\d+./.test(content)) {
return "structured";
} else if (/\n-|\n*|\n•/.test(content)) {
return "list";
} else {
return "paragraph";
}
}
private detectLanguage(content: string): string {
// 简单的语言检测(实际项目中可使用专业库)
const chineseChars = content.match(/[\u4e00-\u9fff]/g) || [];
const englishWords = content.match(/\b[A-Za-z]+\b/g) || [];
if (chineseChars.length > englishWords.length * 2) {
return "zh";
} else if (englishWords.length > chineseChars.length * 2) {
return "en";
} else {
return "mixed";
}
}
private extractKeywords(content: string, maxKeywords = 5): string[] {
// 简化的关键词提取(实际项目可使用NLP库)
const words = content.toLowerCase().split(/\s+/);
const stopWords = new Set(["的", "了", "在", "是", "和", "与", "this", "that", "the", "a", "an"]);
const frequency: Record<string, number> = {};
for (const word of words) {
if (word.length > 1 && !stopWords.has(word)) {
frequency[word] = (frequency[word] || 0) + 1;
}
}
return Object.entries(frequency)
.sort(([, a], [, b]) => b - a)
.slice(0, maxKeywords)
.map(([word]) => word);
}
}
4. 生产级RAG系统
4.1 使用ChromaDB持久化向量存储
import { Chroma } from "@langchain/community/vectorstores/chroma";
import { OpenAIEmbeddings } from "@langchain/openai-embeddings";
class ProductionRAGSystem {
private vectorStore: Chroma | null = null;
private collectionName: string;
constructor(collectionName: string = "knowledge_base") {
this.collectionName = collectionName;
}
/**
* 初始化或连接ChromaDB
*/
async initializeChromaDB(persistDirectory: string = "./chroma_db") {
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/v1"
});
// 检查是否已存在向量存储
try {
this.vectorStore = await Chroma.fromExistingCollection(
embeddings,
{
collectionName: this.collectionName,
url: "http://localhost:8000" // ChromaDB服务地址
}
);
console.log(`连接到现有集合: ${this.collectionName}`);
// 验证连接
const collection = await this.vectorStore._collection.get();
console.log(`集合统计: ${collection.count} 个文档`);
} catch (error) {
console.log("创建新的向量存储...");
// 创建新的向量存储
this.vectorStore = await Chroma.fromTexts(
["初始文档"],
[{ source: "init" }],
embeddings,
{
collectionName: this.collectionName,
url: "http://localhost:8000"
}
);
}
}
/**
* 批量添加文档到向量存储
*/
async addDocumentsToVectorStore(documents: Document[], batchSize: number = 100) {
if (!this.vectorStore) {
await this.initializeChromaDB();
}
console.log(`开始添加 ${documents.length} 个文档...`);
// 分批处理,避免内存溢出
for (let i = 0; i < documents.length; i += batchSize) {
const batch = documents.slice(i, i + batchSize);
await this.vectorStore!.addDocuments(batch);
console.log(`已添加 ${Math.min(i + batchSize, documents.length)}/${documents.length}`);
// 添加延迟,避免请求过多
if (i + batchSize < documents.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
console.log("文档添加完成");
// 更新集合统计
const collection = await this.vectorStore!._collection.get();
console.log(`当前集合: ${collection.count} 个文档`);
}
/**
* 高级检索选项
*/
async advancedRetrieval(query: string, options: {
searchType?: "similarity" | "mmr";
k?: number;
filter?: Record<string, any>;
scoreThreshold?: number;
} = {}) {
if (!this.vectorStore) {
throw new Error("向量存储未初始化");
}
const retriever = this.vectorStore.asRetriever({
searchType: options.searchType || "similarity",
k: options.k || 4,
filter: options.filter,
scoreThreshold: options.scoreThreshold
});
return await retriever.invoke(query);
}
/**
* 删除文档
*/
async deleteDocuments(ids?: string[], filter?: Record<string, any>) {
if (!this.vectorStore) return;
if (ids && ids.length > 0) {
await this.vectorStore.delete({ ids });
console.log(`删除 ${ids.length} 个文档`);
} else if (filter) {
await this.vectorStore.delete({ filter });
console.log(`根据筛选条件删除文档`);
}
}
/**
* 备份和恢复
*/
async backupCollection(backupPath: string) {
if (!this.vectorStore) return;
// 获取所有数据
const results = await this.vectorStore.similaritySearch("", 10000); // 获取大量文档
// 保存到文件
const fs = require('fs').promises;
const backupData = {
collectionName: this.collectionName,
documents: results.map(doc => ({
content: doc.pageContent,
metadata: doc.metadata,
// 注意:实际向量数据需要特殊处理
})),
timestamp: new Date().toISOString()
};
await fs.writeFile(
backupPath,
JSON.stringify(backupData, null, 2)
);
console.log(`备份已保存到: ${backupPath}`);
}
}
4.2 RAG优化技术
class RAGOptimizer {
/**
* 查询扩展:丰富用户查询
*/
async expandQuery(originalQuery: string): Promise<string[]> {
const model = new ChatOpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/v1",
model: "deepseek-chat",
temperature: 0.3
});
const prompt = ChatPromptTemplate.fromTemplate(`
为以下查询生成3个相关的查询变体,用于改进文档检索:
原始查询:{query}
请生成:
1. 更具体的版本
2. 更广泛的版本
3. 同义词版本
用JSON格式返回:
{{
"variants": ["变体1", "变体2", "变体3"]
}}
`);
const chain = prompt.pipe(model).pipe(new StringOutputParser());
try {
const response = await chain.invoke({ query: originalQuery });
const parsed = JSON.parse(response);
return [originalQuery, ...parsed.variants];
} catch (error) {
console.warn("查询扩展失败,使用原始查询");
return [originalQuery];
}
}
/**
* 重排序检索结果
*/
async rerankResults(query: string, documents: Document[]): Promise<Document[]> {
if (documents.length <= 1) return documents;
const model = new ChatOpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/v1",
model: "deepseek-chat",
temperature: 0.1
});
// 创建重排序提示
const rerankPrompt = ChatPromptTemplate.fromTemplate(`
根据相关性对以下文档进行排序。最相关的排在最前面。
查询:{query}
文档:
{documents}
返回排序后的文档索引(从0开始),用逗号分隔。
例如:2,0,1,3
`);
// 格式化文档用于提示
const formattedDocs = documents
.map((doc, i) => `[文档${i}]\n${doc.pageContent.substring(0, 500)}...`)
.join("\n\n---\n\n");
const chain = rerankPrompt.pipe(model).pipe(new StringOutputParser());
const response = await chain.invoke({
query,
documents: formattedDocs
});
// 解析排序结果
const order = response.split(',').map(num => parseInt(num.trim())).filter(num => !isNaN(num));
// 应用排序
const reranked = order.map(idx => documents[idx]);
// 添加未包含的文档(如果有)
const allIndices = new Set(order);
for (let i = 0; i < documents.length; i++) {
if (!allIndices.has(i)) {
reranked.push(documents[i]);
}
}
return reranked;
}
/**
* 多路检索
*/
async multiRetrieval(query: string, vectorStore: any) {
// 1. 标准相似度检索
const similarityResults = await vectorStore.similaritySearch(query, 6);
// 2. MMR检索(多样性)
const mmrResults = await vectorStore.maxMarginalRelevanceSearch(query, {
k: 4,
fetchK: 20,
lambda: 0.5
});
// 3. 关键词检索(备用)
const keywordResults = await this.keywordSearch(query, vectorStore, 4);
// 合并并去重
const allResults = [...similarityResults, ...mmrResults, ...keywordResults];
const uniqueResults = this.removeDuplicates(allResults);
// 重排序
const reranked = await this.rerankResults(query, uniqueResults);
return reranked.slice(0, 5); // 返回前5个
}
private async keywordSearch(query: string, vectorStore: any, k: number): Promise<Document[]> {
// 简化的关键词匹配
const keywords = this.extractKeywords(query);
// 实际项目中,这里可以实现基于关键词的检索
// 例如:检查文档元数据中的关键词字段
return []; // 简化实现
}
private removeDuplicates(documents: Document[]): Document[] {
const seen = new Set<string>();
const unique: Document[] = [];
for (const doc of documents) {
const hash = this.createContentHash(doc.pageContent);
if (!seen.has(hash)) {
seen.add(hash);
unique.push(doc);
}
}
return unique;
}
private createContentHash(content: string): string {
// 创建内容的简单哈希用于去重
return Buffer.from(content).toString('base64').slice(0, 50);
}
private extractKeywords(text: string): string[] {
// 简化的关键词提取
const words = text.toLowerCase()
.replace(/[^\w\u4e00-\u9fff\s]/g, '')
.split(/\s+/);
const stopWords = new Set(['的', '了', '在', '是', '和', '与', 'this', 'that', 'the']);
const freq: Record<string, number> = {};
for (const word of words) {
if (word.length > 1 && !stopWords.has(word)) {
freq[word] = (freq[word] || 0) + 1;
}
}
return Object.entries(freq)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([word]) => word);
}
}
5. 实战:智能客服知识库系统
import { Chroma } from "@langchain/community/vectorstores/chroma";
import { ChatOpenAI } from "@langchain/openai";
import { OpenAIEmbeddings } from "@langchain/openai-embeddings";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { Document } from "@langchain/core/documents";
import {
RunnableSequence,
RunnablePassthrough,
RunnableBranch
} from "@langchain/core/runnables";
class CustomerServiceKnowledgeBase {
private vectorStore: Chroma;
private model: ChatOpenAI;
private embeddings: OpenAIEmbeddings;
private collectionName: string;
constructor(collectionName: string = "customer_service_kb") {
this.collectionName = collectionName;
this.model = new ChatOpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/v1",
model: "deepseek-chat",
temperature: 0.2,
maxTokens: 500
});
this.embeddings = new OpenAIEmbeddings({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/v1"
});
this.initializeVectorStore();
}
private async initializeVectorStore() {
this.vectorStore = await Chroma.fromExistingCollection(
this.embeddings,
{
collectionName: this.collectionName,
url: "http://localhost:8000"
}
).catch(async () => {
console.log("创建新的知识库集合");
return await Chroma.fromTexts(
["欢迎使用智能客服系统"],
[{ type: "welcome" }],
this.embeddings,
{
collectionName: this.collectionName,
url: "http://localhost:8000"
}
);
});
}
/**
* 加载知识库文档
*/
async loadKnowledgeBase(documents: Array<{
content: string;
category: string;
tags: string[];
priority: number;
}>) {
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 800,
chunkOverlap: 150,
separators: ["\n\n", "\n", "。", "!", "?", ";", ","]
});
const allDocs: Document[] = [];
for (const doc of documents) {
const splits = await textSplitter.splitText(doc.content);
for (const [index, split] of splits.entries()) {
allDocs.push(new Document({
pageContent: split,
metadata: {
category: doc.category,
tags: doc.tags,
priority: doc.priority,
chunk_index: index,
total_chunks: splits.length,
source: "knowledge_base",
added_date: new Date().toISOString()
}
}));
}
}
// 添加到向量存储
await this.vectorStore.addDocuments(allDocs);
console.log(`知识库加载完成:${allDocs.length} 个文档块`);
}
/**
* 构建智能客服链
*/
buildCustomerServiceChain() {
// 1. 检索器
const retriever = this.vectorStore.asRetriever({
k: 5,
searchType: "mmr", // 最大边际相关性,增加多样性
filter: (doc) => {
// 可以基于元数据筛选
return true;
}
});
// 2. 查询分类器
const queryClassifier = ChatPromptTemplate.fromTemplate(`
分类用户问题类型:
1. "product_info" - 产品信息查询
2. "technical_support" - 技术支持
3. "billing" - 账单问题
4. "general" - 一般咨询
用户问题:{question}
只返回类型名称,不要其他文字。
`).pipe(this.model).pipe(new StringOutputParser());
// 3. 根据分类调整检索
const adaptiveRetriever = RunnableSequence.from([
async (input) => {
const category = await queryClassifier.invoke(input);
return { ...input, category };
},
async (state) => {
// 根据问题类型调整检索参数
let filter;
let k = 5;
switch (state.category) {
case "technical_support":
filter = { category: { $in: ["technical", "troubleshooting"] } };
k = 7; // 技术问题需要更多上下文
break;
case "billing":
filter = { category: "billing" };
break;
case "product_info":
filter = { category: "product" };
break;
}
const docs = await this.vectorStore.similaritySearch(
state.question,
k,
filter
);
return { ...state, context: docs };
}
]);
// 4. 回答生成模板
const answerTemplate = ChatPromptTemplate.fromMessages([
["system", `你是一个专业的客服助手。请基于以下知识库信息回答问题。
知识库信息:
{context}
回答要求:
1. 基于知识库信息,不要编造
2. 如果知识库中没有相关信息,请礼貌说明
3. 保持专业、友好的语气
4. 提供清晰、具体的解决方案`],
["human", "用户问题:{question}"]
]);
// 5. 构建完整链
return RunnableSequence.from([
RunnablePassthrough.assign({
context: adaptiveRetriever.pipe((state) =>
state.context.map((doc: Document, i: number) =>
`[来源${i+1}] ${doc.pageContent}`
).join("\n\n")
),
category: queryClassifier
}),
answerTemplate,
this.model,
new StringOutputParser(),
// 添加对话历史记录(可选)
async (response, state) => ({
answer: response,
category: state.category,
sources_used: state.context.length,
confidence: this.calculateConfidence(state.context, state.question)
})
]);
}
/**
* 计算回答置信度
*/
private calculateConfidence(documents: Document[], question: string): number {
// 简化的置信度计算
if (documents.length === 0) return 0.1;
// 基于文档相关性和数量的简单评分
const baseScore = Math.min(documents.length / 5, 1) * 0.7;
// 检查问题关键词是否在文档中
const questionKeywords = this.extractKeywords(question);
let keywordMatchScore = 0;
for (const doc of documents) {
const docText = doc.pageContent.toLowerCase();
const matches = questionKeywords.filter(kw =>
docText.includes(kw.toLowerCase())
).length;
keywordMatchScore += matches / questionKeywords.length;
}
keywordMatchScore = keywordMatchScore / documents.length * 0.3;
return Math.min(baseScore + keywordMatchScore, 0.95);
}
private extractKeywords(text: string): string[] {
const words = text.split(/[\s,,。!?;]+/);
const stopWords = new Set(['的', '了', '在', '是', '和', '与', '吗', '呢', '啊']);
return words
.filter(word => word.length > 1 && !stopWords.has(word))
.slice(0, 5);
}
/**
* 对话历史管理
*/
async handleConversation(sessionId: string, userMessage: string) {
// 这里可以集成第四章的记忆系统
// 检索 + 对话历史 = 更准确的回答
const serviceChain = this.buildCustomerServiceChain();
const result = await serviceChain.invoke({
question: userMessage,
session_id: sessionId
});
// 记录交互日志
await this.logInteraction({
sessionId,
question: userMessage,
answer: result.answer,
category: result.category,
confidence: result.confidence,
timestamp: new Date().toISOString()
});
return result;
}
private async logInteraction(log: any) {
// 实际项目中可保存到数据库
console.log("交互日志:", log);
}
/**
* 知识库统计
*/
async getKnowledgeStats() {
try {
const collection = await this.vectorStore._collection.get();
// 获取所有文档的元数据
const allDocs = await this.vectorStore.similaritySearch("", 1000);
// 统计分类
const categories: Record<string, number> = {};
const tags: Record<string, number> = {};
for (const doc of allDocs) {
const meta = doc.metadata;
// 分类统计
if (meta.category) {
categories[meta.category] = (categories[meta.category] || 0) + 1;
}
// 标签统计
if (Array.isArray(meta.tags)) {
for (const tag of meta.tags) {
tags[tag] = (tags[tag] || 0) + 1;
}
}
}
return {
total_documents: collection.count,
total_chunks: allDocs.length,
categories,
top_tags: Object.entries(tags)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([tag, count]) => ({ tag, count }))
};
} catch (error) {
console.error("获取统计信息失败:", error);
return null;
}
}
}
// 使用示例
async function demoCustomerService() {
const csKB = new CustomerServiceKnowledgeBase();
// 1. 加载知识库
await csKB.loadKnowledgeBase([
{
content: `产品退货政策:
1. 收到商品7天内可无理由退货
2. 商品需保持原包装完整
3. 退货流程:登录账户 → 我的订单 → 申请退货
4. 退款将在审核通过后3-5个工作日内退回原支付方式`,
category: "policy",
tags: ["退货", "退款", "政策"],
priority: 1
},
{
content: `常见技术问题解决方案:
Q: 无法登录账户
A: 1. 检查网络连接 2. 清除浏览器缓存 3. 重置密码
Q: 订单状态不更新
A: 1. 刷新页面 2. 联系客服提供订单号 3. 检查邮箱垃圾箱`,
category: "technical",
tags: ["登录", "订单", "故障排除"],
priority: 1
}
]);
// 2. 测试客服
const chain = csKB.buildCustomerServiceChain();
const testQuestions = [
"如何退货?",
"我的账户登录不上怎么办?",
"退款需要多久?"
];
for (const question of testQuestions) {
console.log(`\n=== 用户问题:${question} ===`);
const response = await chain.invoke({ question });
console.log("客服回答:", response.answer);
console.log("问题分类:", response.category);
console.log("置信度:", response.confidence.toFixed(2));
}
// 3. 查看知识库统计
const stats = await csKB.getKnowledgeStats();
console.log("\n=== 知识库统计 ===");
console.log(JSON.stringify(stats, null, 2));
}
📝 第五章小结
关键知识点
-
RAG架构:理解文档加载 → 处理 → 向量化 → 检索 → 生成的完整流程
-
文档处理:多种格式支持、智能分割、元数据增强
-
向量存储:内存存储 vs 持久化存储(ChromaDB)
-
检索优化:查询扩展、重排序、多路检索
-
生产实践:错误处理、性能优化、监控统计
最佳实践
-
文档预处理是关键:好的分割策略能大幅提升检索质量
-
元数据利用:利用文档类别、标签等元数据优化检索
-
多阶段检索:结合相似度检索、MMR、关键词匹配
-
置信度评估:为回答添加置信度评分,提高透明度
-
持续优化:基于用户反馈迭代改进知识库
常见问题与解决方案
-
检索不相关:调整chunk大小、改进分割策略、使用查询扩展
-
回答不准确:增加检索结果数量、添加重排序、优化提示词
-
性能问题:分批处理文档、使用缓存、优化向量检索参数
-
知识库更新:实现增量更新、版本控制、定期清理
下一步扩展
-
多模态RAG:支持图片、表格等非文本内容
-
实时数据:集成API、数据库等实时数据源
-
个性化:基于用户历史和行为个性化检索
-
评估系统:自动评估RAG系统质量 RAG是让大语言模型真正实用的关键技术,掌握了它,你就能构建基于自有数据的智能应用。