Skip to content

从零到进阶做 Agent(六)文档加载与向量化

🕒 Published at:

模型的知识停在训练那一刻,也不知道你公司的内部文档。RAG(检索增强生成) 的思路是:先把你的资料变成「可按语义搜索」的形式,提问时检索出最相关的几段,再拼进提示词让模型参考。这一篇讲 RAG 的前半段——加载文档 → 切分 → 向量化 → 存入向量库 → 语义检索。下一篇再把检索结果接给模型。顶部可切换 Python / Node.js。

装依赖

bash
pip install -U langchain langchain-openai langchain-community langchain-text-splitters
bash
npm install langchain @langchain/openai @langchain/textsplitters @langchain/core

为什么要「切分」和「向量化」

切分:一份文档几千字,整篇丢给模型既浪费又不精准。我们把它切成小块(chunk),检索时只取最相关的几块。切分时留一点重叠(overlap),是为了避免把一句话从中间劈开、丢失上下文。

向量化(Embedding):把每一块文字转成一串数字(向量)。语义相近的文字,向量在空间里也相近。于是「按意思搜索」就变成了「找向量最近的邻居」——哪怕你用的词和原文不完全一样,也能命中。

一、加载与切分

先把文本读进来,再用 RecursiveCharacterTextSplitter 切块。它会优先按段落、句子等自然边界切,尽量不破坏语义。

python
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 加载:把 txt 读成一个或多个 Document 对象
docs = TextLoader("扫地机器人100问.txt", encoding="utf-8").load()

# 切分:每块约 300 字,相邻块重叠 50 字
splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50)
chunks = splitter.split_documents(docs)

print(f"切成 {len(chunks)} 块,第一块预览:{chunks[0].page_content[:50]}")
javascript
import { TextLoader } from "@langchain/classic/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";

// 加载:把 txt 读成一个或多个 Document 对象
const docs = await new TextLoader("扫地机器人100问.txt").load();

// 切分:每块约 300 字,相邻块重叠 50 字
const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 300, chunkOverlap: 50 });
const chunks = await splitter.splitDocuments(docs);

console.log(`切成 ${chunks.length} 块,第一块预览:${chunks[0].pageContent.slice(0, 50)}`);

除了 TextLoader,还有 PDF、CSV 等加载器。Python 在 langchain_community.document_loaders 里有 PyPDFLoaderCSVLoader;Node 在 @langchain/community/document_loaders/fs/pdf.../fs/csv。用法都是 .load() 拿到 Document 列表,后续流程完全一样。

二、嵌入模型

向量化要用嵌入模型。通义千问提供 text-embedding-v3,同样走 OpenAI 兼容接口,所以用 OpenAIEmbeddings 指到百炼即可。

python
import os
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(
    model="text-embedding-v3",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

# 试一下:把一句话变成向量
vec = embeddings.embed_query("扫地机器人怎么清洁滤网")
print("向量维度:", len(vec))
javascript
import { OpenAIEmbeddings } from "@langchain/openai";

const embeddings = new OpenAIEmbeddings({
  model: "text-embedding-v3",
  apiKey: process.env.DASHSCOPE_API_KEY,
  configuration: {
    baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  },
});

// 试一下:把一句话变成向量
const vec = await embeddings.embedQuery("扫地机器人怎么清洁滤网");
console.log("向量维度:", vec.length);

三、存入向量库并检索

把切好的块交给向量库,它会调用嵌入模型把每块转成向量存起来。检索时,similarity_search 会把你的问题也向量化,返回最相近的几块。入门先用内存向量库,零配置、开箱即用。

python
from langchain_core.vectorstores import InMemoryVectorStore

# from_documents 会自动对每一块调用嵌入模型并建立索引
vectorstore = InMemoryVectorStore.from_documents(chunks, embeddings)

# 语义检索:返回最相关的 3 块
results = vectorstore.similarity_search("滤网多久清洗一次?", k=3)
for i, doc in enumerate(results, 1):
    print(f"[{i}] {doc.page_content[:60]}")
javascript
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";

// fromDocuments 会自动对每一块调用嵌入模型并建立索引
const vectorstore = await MemoryVectorStore.fromDocuments(chunks, embeddings);

// 语义检索:返回最相关的 3 块
const results = await vectorstore.similaritySearch("滤网多久清洗一次?", 3);
results.forEach((doc, i) => {
  console.log(`[${i + 1}] ${doc.pageContent.slice(0, 60)}`);
});

留意检索的妙处:即便你问「滤网多久清洗一次」,而原文写的是「清洁滤网的频率」,靠语义相近也能命中——这是关键词搜索做不到的。

四、持久化:换成 Chroma

内存向量库进程一关就没了,每次都要重新向量化(费钱费时)。生产里会用持久化向量库,比如 Chroma,把索引存到磁盘,下次直接加载。

python
from langchain_chroma import Chroma

# 首次:建库并落盘到 ./chroma_db
vectorstore = Chroma.from_documents(
    chunks, embeddings, persist_directory="./chroma_db"
)

# 之后:直接从磁盘加载,无需重新向量化
vectorstore = Chroma(
    persist_directory="./chroma_db", embedding_function=embeddings
)
results = vectorstore.similarity_search("滤网多久清洗一次?", k=3)
javascript
import { Chroma } from "@langchain/community/vectorstores/chroma";

// 建库并写入运行中的 Chroma 服务(见下方说明)
const vectorstore = await Chroma.fromDocuments(chunks, embeddings, {
  collectionName: "robot-faq",
  url: "http://localhost:8000",
});

const results = await vectorstore.similaritySearch("滤网多久清洗一次?", 3);

一个真实差异:Python 的 langchain-chroma 可以内嵌运行,指定 persist_directory 就直接落盘,无需额外服务。而 Node 的 Chroma 集成是连接一个 Chroma 服务端的,需要先把服务跑起来——最简单的方式是装好 chromadbpip install chromadb)后执行 chroma run --path ./chroma_db,或用 Docker 启动。跑起来后 Node 用 url 连上即可。若只想零配置体验,Node 侧继续用上面的内存向量库就行。

小结与预告

这一篇你把「一份文档」变成了「可语义检索的知识库」:加载 → 切分(带重叠)→ 嵌入 → 存库 → similarity_search。这套流程是所有 RAG 应用的地基,换成 PDF、CSV 也只是换个 Loader。

现在我们能「检索出相关片段」了,但还没让模型用上它们。下一篇《手搓 RAG 问答链》,我们用 LCEL 把「检索 → 拼进提示词 → 交给模型」串成一条完整的问答链,做出第一个真正「带着资料回答」的问答机器人。