AI时代,使用大模型学习——LangChain (2)

6 阅读10分钟

前言

上一章学习了基本的agent,下面来学习RAG。 我们在使用大模型时,经常会遇到大模型的信息过时和幻觉问题,主要有以下几方面原因:

  • 知识的局限性:大模型自身的知识完全源于训练数据,而现有的主流大模型(deepseek、gpt、通义千问…)的训练集基本都是构建于网络公开的数据,对于一些实时性的、非公开的或私域的数据是没有。
  • 幻觉问题:所有的深度学习模型的底层原理都是基于数学概率,模型输出实质上是一系列数值运算,大模型也不例外,所以它经常会一本正经地胡说八道,尤其是在大模型自身不具备某一方面的知识或不擅长的任务场景。
  • 数据安全性:对于企业来说,数据安全至关重要,没有企业愿意承担数据泄露的风险,尤其是大公司,没有人将私域数据上传第三方平台进行训练会推理。这也导致完全依赖通用大模型自身能力的应用方案不得不在数据安全和效果方面进行取舍。
  • 上下文长度有限
    即使现在很多模型支持长上下文,也不适合每次把所有文档都塞给模型,成本高、速度慢、噪声大。

RAG就可以解决这些问题

RAG(检索增强生成)

RAG = 先从知识库里搜出相关资料,再把资料和问题一起交给 LLM 回答。让 LLM 能回答它"没见过"的内容。

┌──────────┐    ┌──────────┐    ┌──────────┐
│  Retrieve │ → │ Augment  │ → │ Generate │
│   检索    │    │   增强    │    │   生成    │
└──────────┘    └──────────┘    └──────────┘

1. Retrieve:  用户问题 → 在知识库中找到最相关的文档片段
2. Augment:   把检索到的片段拼进 Prompt 作为上下文
3. Generate:  LLM 基于上下文生成回答(附带来源引用)

以一个基础的RAG为例子,RAG通常有三个步骤

1. Chunking(文本切段)

Chunking是把文本切成一个个小段。这一步的必要性是因为大模型的上下文有限,需要将文档切成大模型好处理的长度。

例如我有一个长段落的文本,可能包含几千个字

const productDocs = `
# 产品介绍

## 概述
SmartChat 是一款基于大语言模型的智能客服系统,支持多轮对话、知识库检索、
情感分析等功能。它能够帮助企业快速搭建智能客服平台,提升客户服务效率。

## 核心功能
...

假设以上是一段长文本,LangChain如何进行分段的过程。

  // Document 是 LangChain 中表示文档的基本单位
  const doc = new Document({
    pageContent: "SmartChat 是一款智能客服系统",
    metadata: { source: "product-intro.md", chapter: "概述" },
  });

  console.log("Document 结构:");
  console.log(`   pageContent: "${doc.pageContent}"`);
  console.log(`   metadata:    ${JSON.stringify(doc.metadata)}`);
  console.log("\n💡 pageContent = 文档的文本内容,metadata = 附加信息(来源、作者等)\n");
  
  // 创建分割器
  // 类比前端的:Array.slice() 但更智能 —— 会尽量在自然边界切分
  const splitter = new RecursiveCharacterTextSplitter({
    chunkSize: 500,       // 每块最多 500 字符
    chunkOverlap: 100,    // 相邻块重叠 100 字符
    separators: ["\n\n", "\n", "。", ",", " ", ""],  // 依次尝试在这些分隔符处切割
  });
  
  // 分割文档
  const chunks = await splitter.splitDocuments([
    new Document({
      pageContent: productDocs,
      metadata: { source: "product-docs.md", type: "产品文档" },
    }),
  ]);
  
  console.log(`原始文档长度: ${productDocs.length} 字符`);
  console.log(`分割后共 ${chunks.length} 个 chunk\n`);

  // 展示每个 chunk
  chunks.forEach((chunk, i) => {
    console.log(`─── Chunk ${i + 1} (${chunk.pageContent.length} 字符) ───`);
    // 显示前 200 个字符作为预览
    const preview = chunk.pageContent.substring(0, 200);
    console.log(preview + (chunk.pageContent.length > 200 ? "...\n" : "\n"));
  });

LangChain中用 Document表示一个文档对象。 RecursiveCharacterTextSplitter是递归字符文本分割器,是 LangChain 提供的一种高效文本分块工具,它会按设定的分隔符列表递归尝试切分文本,直到每个块的大小不超过设定的 chunk_size,并保留一定的 chunk_overlap 以减少上下文丢失。这种方式能最大限度保持段落、句子、单词的完整性,非常适合长文本处理、向量化检索和 RAG 场景。经过chunks之后,文档被分割成几块。

  • chunk_size 通常在 256-1024 之间。
  • chunk_overlap 通常设为 chunk_size 的 10-20%"。
  • 如果检索精度要求高,则用小 chunk。
  • 如果需要完整上下文,则用大 chunk。

2.Embedding(向量化)

向量化是通过一定的算法,将一串字符串转为数字,语义相近的文本,向量也相近。

例如在实际应用时,我们可能会有很多篇文档

/**
 * 一个"宠物护理知识库",包含多个主题的文档。
 * 在实际项目中,这些可能来自多个文件、数据库或 API。
 */
const knowledgeBase: Document[] = [
  new Document({
    pageContent: "猫咪需要定期接种疫苗,包括猫三联和狂犬疫苗。幼猫在8周龄开始接种,每隔3-4周加强一次,共3次。成年猫每年加强一次。",
    metadata: { topic: "医疗", animal: "猫" },
  }),
  new Document({
    pageContent: "狗狗每天需要至少两次户外活动,小型犬每次20-30分钟,大型犬每次40-60分钟。充足的运动可以预防肥胖和行为问题。",
    metadata: { topic: "运动", animal: "狗" },
  }),
  new Document({
    pageContent: "猫咪是专性肉食动物,需要高蛋白低碳水的饮食。优质猫粮的粗蛋白含量应在35%以上。避免喂食洋葱、大蒜、巧克力等对猫有毒的食物。",
    metadata: { topic: "饮食", animal: "猫" },
  }),
  new Document({
    pageContent: "Python 是一门解释型、面向对象的高级编程语言,由 Guido van Rossum 于 1991 年发布。它以简洁的语法和丰富的生态系统而闻名。",
    metadata: { topic: "编程", language: "Python" },
  }),
  new Document({
    pageContent: "TypeScript 是 JavaScript 的超集,添加了静态类型系统。它由 Microsoft 开发,于 2012 年首次发布。TypeScript 代码编译为纯 JavaScript。",
    metadata: { topic: "编程", language: "TypeScript" },
  }),
  new Document({
    pageContent: "金毛寻回犬性格温顺友善,是非常受欢迎的家庭犬。它们智商高,容易训练,但需要大量的运动和精神刺激。",
    metadata: { topic: "品种", animal: "狗" },
  }),
  new Document({
    pageContent: "猫咪每天平均睡眠14-16小时,老年猫甚至可达20小时。这是它们保存能量的自然习性,不必担心。但突然的睡眠习惯改变需要留意。",
    metadata: { topic: "习性", animal: "猫" },
  }),
  new Document({
    pageContent: "React 是一个用于构建用户界面的 JavaScript 库,由 Facebook 开发。它采用组件化和声明式编程范式,使用虚拟 DOM 提高渲染性能。",
    metadata: { topic: "编程", language: "JavaScript" },
  }),
  new Document({
    pageContent: "狗狗的饮食中蛋白质应占干物质的22-25%(成年犬)。可以选择商业狗粮或自制狗饭,但自制需要确保营养均衡,最好咨询兽医。",
    metadata: { topic: "饮食", animal: "狗" },
  }),
  new Document({
    pageContent: "布偶猫是一种大型长毛猫品种,以其蓝色的眼睛和温顺的性格著称。它们喜欢与人互动,被称为'小狗猫'。布偶猫需要定期梳理毛发。",
    metadata: { topic: "品种", animal: "猫" },
  }),
];

下面用LangChain为这些文档进行向量化

// 创建 embedding 模型实例
  const embeddings = new SimpleEmbeddings({ vectorSize: 256 });

// 进行向量化前,仍然要进行切分
  const splitter = new RecursiveCharacterTextSplitter({
    chunkSize: 200,
    chunkOverlap: 30,
  });
  
  const chunks = await splitter.splitDocuments(knowledgeBase);
  
  // 创建向量存储(类似于创建一个带索引的数据库)
  const vectorStore = new SimpleVectorStore(embeddings);

  // 添加文档:自动 embedding + 存储
  await vectorStore.addDocuments(chunks);
  
  console.log(`知识库: ${knowledgeBase.length} 篇文档 → 切分为 ${chunks.length} 个 chunk`);
  console.log(`向量存储: 已建立索引,共 ${vectorStore.documentCount} 条记录\n`);
  
  for (const query of testQueries) {
    console.log(`\n🔍 搜索: "${query}"`);
    const results = await vectorStore.similaritySearch(query, 3);

    for (let i = 0; i < results.length; i++) {
      const { document, score } = results[i];
      const preview = document.pageContent.length > 80
        ? document.pageContent.substring(0, 80) + "..."
        : document.pageContent;

      console.log(`   #${i + 1} [相似度: ${score.toFixed(3)}] ${preview}`);
      console.log(`      来源: ${JSON.stringify(document.metadata)}`);
    }
  }

运行以上示例,就可以从多篇文档中查找到相似的结果。

搜索: "猫应该吃什么?"
   #1 [相似度: 0.228] 猫咪每天平均睡眠14-16小时,老年猫甚至可达20小时。这是它们保存能量的自然习性,不必担心。但突然的睡眠习惯改变需要留意。
      来源: {"topic":"习性","animal":"猫","loc":{"lines":{"from":1,"to":1}}}
   #2 [相似度: 0.165] TypeScript 是 JavaScript 的超集,添加了静态类型系统。它由 Microsoft 开发,于 2012 年首次发布。TypeScript 代码...
      来源: {"topic":"编程","language":"TypeScript","loc":{"lines":{"from":1,"to":1}}}
   #3 [相似度: 0.157] Python 是一门解释型、面向对象的高级编程语言,由 Guido van Rossum 于 1991 年发布。它以简洁的语法和丰富的生态系统而闻名。
      来源: {"topic":"编程","language":"Python","loc":{"lines":{"from":1,"to":1}}}

🔍 搜索: "什么狗适合家里有小孩?"
   #1 [相似度: 0.243] 猫咪每天平均睡眠14-16小时,老年猫甚至可达20小时。这是它们保存能量的自然习性,不必担心。但突然的睡眠习惯改变需要留意。
      来源: {"topic":"习性","animal":"猫","loc":{"lines":{"from":1,"to":1}}}
   #2 [相似度: 0.173] 金毛寻回犬性格温顺友善,是非常受欢迎的家庭犬。它们智商高,容易训练,但需要大量的运动和精神刺激。
      来源: {"topic":"品种","animal":"狗","loc":{"lines":{"from":1,"to":1}}}
   #3 [相似度: 0.171] 猫咪是专性肉食动物,需要高蛋白低碳水的饮食。优质猫粮的粗蛋白含量应在35%以上。避免喂食洋葱、大蒜、巧克力等对猫有毒的食物。
      来源: {"topic":"饮食","animal":"猫","loc":{"lines":{"from":1,"to":1}}}

🔍 搜索: "前端开发用的是什么技术?"
   #1 [相似度: 0.297] React 是一个用于构建用户界面的 JavaScript 库,由 Facebook 开发。它采用组件化和声明式编程范式,使用虚拟 DOM 提高渲染性能。
      来源: {"topic":"编程","language":"JavaScript","loc":{"lines":{"from":1,"to":1}}}
   #2 [相似度: 0.262] 狗狗每天需要至少两次户外活动,小型犬每次20-30分钟,大型犬每次40-60分钟。充足的运动可以预防肥胖和行为问题。
      来源: {"topic":"运动","animal":"狗","loc":{"lines":{"from":1,"to":1}}}
   #3 [相似度: 0.245] 金毛寻回犬性格温顺友善,是非常受欢迎的家庭犬。它们智商高,容易训练,但需要大量的运动和精神刺激。
      来源: {"topic":"品种","animal":"狗","loc":{"lines":{"from":1,"to":1}}}

🔍 搜索: "小狗每天要遛多久?"
   #1 [相似度: 0.235] TypeScript 是 JavaScript 的超集,添加了静态类型系统。它由 Microsoft 开发,于 2012 年首次发布。TypeScript 代码...
      来源: {"topic":"编程","language":"TypeScript","loc":{"lines":{"from":1,"to":1}}}
   #2 [相似度: 0.209] React 是一个用于构建用户界面的 JavaScript 库,由 Facebook 开发。它采用组件化和声明式编程范式,使用虚拟 DOM 提高渲染性能。
      来源: {"topic":"编程","language":"JavaScript","loc":{"lines":{"from":1,"to":1}}}
   #3 [相似度: 0.197] 布偶猫是一种大型长毛猫品种,以其蓝色的眼睛和温顺的性格著称。它们喜欢与人互动,被称为'小狗猫'。布偶猫需要定期梳理毛发。
      来源: {"topic":"品种","animal":"猫","loc":{"lines":{"from":1,"to":1}}}

从结果可以知道,向量化后,可以根据问题来匹配最接近的文本,这就能为大模型找到正确的参考资料,帮助大模型正确回答问题。

到了这一步之后,只需要将向量搜索的结果给大模型,就可以创建一个简单的RAG了

RAG

/**
 * 完整的 RAG 流程实现
 * 输入向量数据库
 */
async function ragQuery(
  model: ChatDeepSeek,
  vectorStore: SimpleVectorStore,
  question: string,
  options: {
    topK?: number;
    verbose?: boolean;
  } = {}
): Promise<{ answer: string; sources: Document[] }> {
  const { topK = 3, verbose = true } = options;

  // ── 步骤 1: Retrieve(检索)─────────────────
  // 在知识库中找到最相关的文档
  if (verbose) {
    console.log(`📚 检索中... 查找 top-${topK} 最相关文档\n`);
  }

  const searchResults = await vectorStore.similaritySearch(question, topK);

  if (verbose) {
    for (let i = 0; i < searchResults.length; i++) {
      const r = searchResults[i];
      const source = r.document.metadata?.source ?? "未知";
      const preview = r.document.pageContent.substring(0, 100).replace(/\n/g, " ");
      console.log(`   #${i + 1} [${source}] 相似度: ${r.score.toFixed(3)}`);
      console.log(`      "${preview}..."\n`);
    }
  }

  // ── 步骤 2: Augment(增强)──────────────────
  // 将检索到的文档拼接到 prompt 中作为上下文
  const context = searchResults
    .map((r, i) => `[文档${i + 1}]\n${r.document.pageContent}`)
    .join("\n\n---\n\n");

  const systemPrompt = `你是一个专业的企业知识助手。请根据以下知识库内容回答用户问题。

## 知识库内容(来自公司内部文档)

${context}

## 回答要求
1. 只使用上面提供的知识库内容来回答
2. 如果知识库中没有相关信息,请如实告知
3. 在回答末尾标注引用了哪些文档
4. 用简洁清晰的中文回答`;

  // ── 步骤 3: Generate(生成)──────────────────
  // LLM 基于增强后的 prompt 生成回答
  if (verbose) console.log("🤖 LLM 生成中...\n");

  const messages: BaseMessage[] = [
    new SystemMessage(systemPrompt),
    new HumanMessage(question),
  ];

  const response = await model.invoke(messages);
  const answer = typeof response.content === "string"
    ? response.content
    : "[复杂内容]";

  return {
    answer,
    sources: searchResults.map((r) => r.document),
  };
}

async function main() {
  console.log("═══════════════════════════════════════");
  console.log("  步骤 9:基础 RAG 链 ⭐");
  console.log("═══════════════════════════════════════\n");

  validateConfig();

  // 1. 初始化各个组件
  console.log("🔧 初始化组件...\n");

  const model = new ChatDeepSeek({
    model: config.model,
    apiKey: config.apiKey,
    temperature: 0, // RAG 场景用低温度,减少幻觉
  });

  const embeddings = new SimpleEmbeddings({ vectorSize: 256 });

  const splitter = new RecursiveCharacterTextSplitter({
    chunkSize: 300,
    chunkOverlap: 50,
  });

  // 2. 构建知识库索引
  console.log(`📄 加载 ${companyDocs.length} 篇公司文档...\n`);

  const docs = companyDocs.map(
    (content, i) =>
      new Document({
        pageContent: content,
        metadata: { source: `doc-${i + 1}.md` },
      })
  );

  const chunks = await splitter.splitDocuments(docs);
  console.log(`   切分为 ${chunks.length} 个 chunk\n`);

  const vectorStore = new SimpleVectorStore(embeddings);
  await vectorStore.addDocuments(chunks);
  console.log("   ✅ 知识库索引建立完成\n");

  // ─── 3. 测试 ─────────────────────────
  console.log("\n" + "━".repeat(55));
  console.log("\n📌 RAG 查询测试\n");

  const moreQuestions = [
    "WhaleDesk 产品多少钱?有什么功能?",
    "新人入职需要多久的试用期?考核维度是什么?",
    "公司后端用的什么技术栈?",
  ];

  for (const q of moreQuestions) {
    console.log(`\n💬 ${q}`);
    const { answer } = await ragQuery(model, vectorStore, q, {
      topK: 2,
      verbose: false,
    });
    console.log(`🤖 ${answer}`);
  }
}