处理自有数据——检索增强生成(RAG)

0 阅读9分钟

检索增强生成(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", "。", ",", " "] → 按顺序尝试分割,保持语义完整

为什么需要

  1. LLM有输入长度限制(如4096 token)

  2. 太长的文档难以精准检索相关信息

  3. 小块文档能更精确地定位到相关信息点

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内容...]

问题:{用户问题}

请提供准确、完整的回答:

设计要点

  1. 明确要求模型基于上下文回答

  2. 提供“不知道”的出口,减少幻觉

  3. 结构化组织上下文,便于模型理解

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));
}

📝 第五章小结

关键知识点

  1. RAG架构:理解文档加载 → 处理 → 向量化 → 检索 → 生成的完整流程

  2. 文档处理:多种格式支持、智能分割、元数据增强

  3. 向量存储:内存存储 vs 持久化存储(ChromaDB)

  4. 检索优化:查询扩展、重排序、多路检索

  5. 生产实践:错误处理、性能优化、监控统计

最佳实践

  1. 文档预处理是关键:好的分割策略能大幅提升检索质量

  2. 元数据利用:利用文档类别、标签等元数据优化检索

  3. 多阶段检索:结合相似度检索、MMR、关键词匹配

  4. 置信度评估:为回答添加置信度评分,提高透明度

  5. 持续优化:基于用户反馈迭代改进知识库

常见问题与解决方案

  1. 检索不相关:调整chunk大小、改进分割策略、使用查询扩展

  2. 回答不准确:增加检索结果数量、添加重排序、优化提示词

  3. 性能问题:分批处理文档、使用缓存、优化向量检索参数

  4. 知识库更新:实现增量更新、版本控制、定期清理

下一步扩展

  1. 多模态RAG:支持图片、表格等非文本内容

  2. 实时数据:集成API、数据库等实时数据源

  3. 个性化:基于用户历史和行为个性化检索

  4. 评估系统:自动评估RAG系统质量 RAG是让大语言模型真正实用的关键技术,掌握了它,你就能构建基于自有数据的智能应用。