LangChain 从入门到实践:用最小案例理解 RAG 的 5 个核心抽象

本文是「从零搭建私人 RAG 知识库」专栏的基础篇。 学习 LangChain 时,最容易陷入"API 很多,但不知道每个对象处在什么位置"的困境。本文不讨论复杂架构,也不展开实际项目,只用一个最小示例认识 Document、文本分块、Embedding、VectorStore 和 Retriever。

前言

LangChain 是一个用于开发大模型应用的框架。它提供了很多标准化接口,帮助我们连接文档、模型、向量数据库和检索逻辑。

在一个基础 RAG 应用中,最常见的流程是:

text 复制代码
原始文本
  ↓
Document
  ↓
文本分块
  ↓
Embedding
  ↓
VectorStore
  ↓
Retriever
  ↓
与问题相关的文档片段

本文会先解释这 5 个对象,再用一个可以直接运行的 TypeScript 示例把它们串起来。


一、先认识 5 个核心抽象

抽象 作用 可以简单理解为
Document 统一表示文本和元数据 一条带标签的文本
Text Splitter 把长文本切成较小片段 文本切片器
Embeddings 把文本转换成数字向量 文本的语义坐标
VectorStore 保存向量并执行相似度搜索 支持语义搜索的仓库
Retriever 用统一接口获取相关文档 面向应用的检索器

理解 LangChain 的关键不是记住所有类名,而是知道数据如何在这些对象之间流动。


二、Document:LangChain 中的文档对象

LangChain 使用 Document 表示一段文本。

一个 Document 主要包含两个字段:

  • pageContent:文档正文;
  • metadata:文档的附加信息。
ts 复制代码
import { Document } from "@langchain/core/documents";

const document = new Document({
  pageContent: "TypeScript 是带有类型系统的 JavaScript。",
  metadata: {
    source: "typescript-intro",
    category: "programming",
  },
});

其中,metadata 可以保存来源、标题、分类、时间等信息。检索完成后,这些信息可以帮助我们定位原文。

需要注意,Document 只是 LangChain 对文本的统一表示,它本身不负责读取文件,也不负责生成向量。


三、文本分块:为什么不能直接处理整篇文章?

如果一篇文章很长,并且同时包含多个主题,直接为整篇文章生成一个向量,会让它的语义过于宽泛。

因此,RAG 通常会先把长文档切成多个较小的 Chunk:

text 复制代码
一篇长文档
  ↓
片段 1:TypeScript 的定义
片段 2:TypeScript 的类型检查
片段 3:TypeScript 的编译方式

LangChain 提供了 RecursiveCharacterTextSplitter。它会优先尝试在段落、换行和空格等位置切分文本,尽量避免直接从一句话中间截断。

ts 复制代码
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 100,
  chunkOverlap: 20,
});

const chunks = await splitter.splitDocuments([document]);

两个常用参数分别是:

  • chunkSize:每个片段的目标大小;
  • chunkOverlap:相邻片段之间保留的重叠内容。

重叠内容可以减少上下文恰好被切断的问题,但重叠过大也会增加存储量,并让检索结果出现重复。

对于不同语言、模型和文档类型,合适的分块参数并不相同。入门阶段先使用一个简单配置即可,后续再根据检索效果调整。


四、Embedding:把文本转换成向量

计算机不能直接比较两段自然语言的语义,因此需要使用 Embedding 模型把文本转换成一组数字。

text 复制代码
"TypeScript 如何进行类型检查?"
              ↓
[0.12, -0.37, 0.88, ..., 0.24]

含义相近的文本,其向量通常也更加接近。向量检索正是利用了这一点。

LangChain 将不同厂商的 Embedding 模型封装成相似的接口。本文使用 Ollama 在本地运行 Embedding 模型:

ts 复制代码
import { OllamaEmbeddings } from "@langchain/ollama";

const embeddings = new OllamaEmbeddings({
  model: "nomic-embed-text",
  baseUrl: "http://localhost:11434",
});

Embedding 对象常见的两个方法是:

ts 复制代码
await embeddings.embedDocuments(["第一段文本", "第二段文本"]);
await embeddings.embedQuery("用户的问题");
  • embedDocuments() 用于批量处理待索引的文档;
  • embedQuery() 用于处理用户查询。

索引文档和查询时必须使用兼容的 Embedding 模型。更换模型后,通常需要重新生成并保存全部文档向量。


五、VectorStore:保存向量并执行语义搜索

VectorStore 用来保存以下内容:

  • 文档正文;
  • 文档元数据;
  • Embedding 模型生成的向量。

本文使用 MemoryVectorStore。它把数据保存在内存中,不需要安装数据库,适合学习和测试:

ts 复制代码
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";

const vectorStore = await MemoryVectorStore.fromDocuments(
  chunks,
  embeddings,
);

创建完成后,可以直接执行相似度搜索:

ts 复制代码
const results = await vectorStore.similaritySearch(
  "TypeScript 有什么作用?",
  2,
);

第二个参数 2 表示最多返回两个相关文档。

需要注意,MemoryVectorStore 在程序退出后不会保留数据。正式应用通常会换成 LanceDB、Chroma、Milvus 或其他支持持久化的向量存储。


六、Retriever:统一检索入口

VectorStore 负责保存和搜索向量,Retriever 则提供了一个更统一的文档检索接口。

可以把一个 VectorStore 转换成 Retriever:

ts 复制代码
const retriever = vectorStore.asRetriever({
  k: 2,
});

const documents = await retriever.invoke(
  "TypeScript 如何提前发现错误?",
);

Retriever 返回的是 Document[],而不是大模型生成的最终答案。

它只负责:

根据用户问题,找到最相关的文档片段。

后续如果要实现完整 RAG,还需要把这些片段和用户问题一起交给大模型。


七、一个可以运行的最小案例

下面把前面的步骤组合起来。

1. 安装依赖

创建一个 TypeScript 项目并安装依赖:

bash 复制代码
npm init -y
npm install @langchain/core @langchain/textsplitters @langchain/ollama @langchain/classic
npm install -D typescript tsx @types/node

确保本地已经启动 Ollama,然后下载 Embedding 模型:

bash 复制代码
ollama pull nomic-embed-text

2. 编写示例

新建 index.ts

ts 复制代码
import { Document } from "@langchain/core/documents";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { OllamaEmbeddings } from "@langchain/ollama";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";

// 1. 准备文档
const documents = [
  new Document({
    pageContent: `
TypeScript 是 JavaScript 的超集,它为 JavaScript 增加了静态类型系统。
开发者可以在编码阶段发现部分类型错误,而不必等到程序运行后再处理。
TypeScript 代码在运行前会被编译成 JavaScript。
    `.trim(),
    metadata: {
      source: "typescript-intro",
    },
  }),
  new Document({
    pageContent: `
Node.js 是一个 JavaScript 运行时。
它允许开发者在浏览器之外运行 JavaScript,常用于开发服务端应用、命令行工具和构建工具。
    `.trim(),
    metadata: {
      source: "nodejs-intro",
    },
  }),
];

// 2. 将文档切成较小片段
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 80,
  chunkOverlap: 20,
});

const chunks = await splitter.splitDocuments(documents);

// 3. 创建 Embedding 模型
const embeddings = new OllamaEmbeddings({
  model: "nomic-embed-text",
  baseUrl: "http://localhost:11434",
});

// 4. 将文档片段写入内存向量库
const vectorStore = await MemoryVectorStore.fromDocuments(
  chunks,
  embeddings,
);

// 5. 创建 Retriever,每次最多返回两个片段
const retriever = vectorStore.asRetriever({
  k: 2,
});

// 6. 使用自然语言检索
const results = await retriever.invoke(
  "哪种技术可以在编码阶段发现类型错误?",
);

for (const [index, document] of results.entries()) {
  console.log(`\n结果 ${index + 1}`);
  console.log(`来源:${document.metadata.source}`);
  console.log(`内容:${document.pageContent}`);
}

3. 运行代码

bash 复制代码
npx tsx index.ts

程序会经历以下过程:

text 复制代码
Document[]
  ↓ splitDocuments()
Chunk[]
  ↓ Embedding
向量
  ↓ MemoryVectorStore
内存向量库
  ↓ Retriever.invoke()
相关 Document[]

对于问题"哪种技术可以在编码阶段发现类型错误?",Retriever 应优先返回介绍 TypeScript 的片段,而不是介绍 Node.js 的片段。

这个结果说明,向量检索并不要求问题和原文使用完全相同的句子。只要语义足够接近,相关内容就有机会被召回。


八、VectorStore 和 Retriever 有什么区别?

初学者经常混淆这两个概念。

对比项 VectorStore Retriever
核心职责 保存向量并进行相似度搜索 根据输入返回相关文档
是否一定使用向量 不一定
常用调用 similaritySearch() invoke()
返回内容 相关文档 相关文档

Retriever 是更上层的抽象。它的底层可以是向量数据库,也可以是关键词搜索、数据库查询,或者多种检索方式的组合。

因此,在应用层使用 Retriever,可以降低业务代码对具体向量数据库的依赖。


九、几个常见问题

1. Retriever 会直接生成答案吗?

不会。Retriever 只返回相关文档。生成自然语言答案还需要调用聊天模型或大语言模型。

2. Embedding 模型和聊天模型一样吗?

不一样。

  • Embedding 模型负责把文本转换成向量;
  • 聊天模型负责理解上下文并生成文本。

3. chunkSize 越大越好吗?

不是。片段过大可能包含多个主题,降低检索精度;片段过小又可能丢失必要上下文。需要根据实际文档和问题进行测试。

4. 为什么要保存 metadata?

metadata 可以记录来源、标题和分类。检索完成后,我们可以用它展示引用来源,或者对检索范围进行过滤。

5. 为什么示例使用 MemoryVectorStore?

因为它不需要额外安装和配置数据库,最适合展示完整流程。但它不会持久化数据,不适合直接用于正式环境。


总结

本文用一个最小案例介绍了 LangChain 在 RAG 中最常见的 5 个抽象:

  1. Document 统一表示正文和元数据;
  2. Text Splitter 把长文档切成较小片段;
  3. Embeddings 把文本转换成向量;
  4. VectorStore 保存向量并执行相似度搜索;
  5. Retriever 用统一接口返回相关文档。

再次回顾这条基础链路:

text 复制代码
原始文本
  ↓
Document
  ↓
文本分块
  ↓
Embedding
  ↓
VectorStore
  ↓
Retriever
  ↓
相关文档片段

只要理解了这条数据流,就掌握了 LangChain 文档检索部分最重要的基础。之后无论替换 Embedding 模型、向量数据库,还是接入大模型生成答案,本质上都是在这条链路上继续扩展。

相关推荐
颜进强1 小时前
MCP 从入门到实践:用 TypeScript 实现第一个 MCP Server
前端·后端·ai编程
Undoom1 小时前
用搜索引擎 API 搭一个 SEO 关键词监控工具:定时追踪排名、广告和相关搜索
后端
程序大爆炸1 小时前
gcsfuse的TypeCache
后端
朕的剑还未配妥1 小时前
CSS 实现渐变毛玻璃:从 backdrop-filter 到多层 mask
前端·css
心运软件1 小时前
Python实战:中国大学排行榜数据采集与可视化大屏
后端·python
程序员黑豆1 小时前
鸿蒙开发实战:使用 List 组件构建新闻列表
前端·华为·harmonyos
xiaobaoyu2 小时前
谈谈你对iframe的了解
前端
GISer_Jing2 小时前
前端转全栈须知后端知识
前端·后端·ai·前端框架
爱勇宝2 小时前
我做了一个排版器,也踩了一遍富文本复制的坑
前端·后端·微信