课程目标
从零构建端到端的 RAG 系统:文档加载 -> 分割 -> 嵌入 -> 存储 -> 检索 -> 生成。整合 RecordManager 增量索引、对话历史、streamEvents 实时追踪。
32.1 RAG 全流程架构
文档源 处理层 存储层 应用层
┌────────┐ ┌───────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ PDF │ │ DocumentLoader │ │ │ │ 用户提问 │
│ Markdown│───>│ TextSplitter │───>│ VectorStore │<───│ Retriever │
│ JSON │ │ Embeddings │ │ (Pinecone等) │ │ Prompt + Model │
│ Web │ │ RecordManager │ │ │ │ OutputParser │
└────────┘ └───────────────────┘ └──────────────┘ └─────────────────┘
32.2 Step 1: 文档加载
基于第 29 课的 BaseDocumentLoader 和 Document 数据模型:
import { Document } from "@langchain/core/documents";
// 手动构建文档(实际项目中使用 DocumentLoader)
const docs = [
new Document({
pageContent: "LangChain.js 是一个用于构建 LLM 应用的 TypeScript 框架。",
metadata: { source: "intro.md", chapter: 1 },
}),
new Document({
pageContent: "Runnable 是 LangChain.js 的核心抽象,所有组件都实现此接口。",
metadata: { source: "core.md", chapter: 2 },
}),
new Document({
pageContent: "VectorStore 负责向量的存储与相似度搜索。",
metadata: { source: "retrieval.md", chapter: 5 },
}),
];
loadAndSplit() 是 BaseDocumentLoader 提供的便捷方法,一步完成加载和分块:
// BaseDocumentLoader 的 loadAndSplit 方法签名
async loadAndSplit(textSplitter: TextSplitter): Promise<Document[]>
32.3 Step 2: 文本分割
源码位置: libs/langchain-textsplitters/src/text_splitter.ts
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500, // 每个分块最大 500 字符
chunkOverlap: 50, // 相邻分块重叠 50 字符
separators: ["\n\n", "\n", "。", ",", " ", ""], // 递归分割优先级
});
const splitDocs = await splitter.splitDocuments(docs);
// splitDocs 保留原始 metadata,并添加分块位置信息
分块参数调优指南:
| 参数 | 建议值 | 过大的影响 | 过小的影响 |
|---|---|---|---|
chunkSize | 300-1000 | 检索精度下降 | 上下文碎片化 |
chunkOverlap | chunkSize 的 10-20% | 冗余增加 | 语义断裂 |
32.4 Step 3: 嵌入与存储
Embeddings 接口: libs/langchain-core/src/embeddings.ts
export interface EmbeddingsInterface {
embedDocuments(documents: string[]): Promise<number[][]>; // 批量嵌入
embedQuery(document: string): Promise<number[]>; // 单条查询嵌入
}
VectorStore 内部流程:
// addDocuments 的典型实现流程
async addDocuments(documents: DocumentInterface[]): Promise<string[]> {
const texts = documents.map((doc) => doc.pageContent);
const vectors = await this.embeddings.embedDocuments(texts);
return this.addVectors(vectors, documents);
}
// similaritySearch 的默认实现(来自 VectorStore 基类)
async similaritySearch(query: string, k = 4): Promise<DocumentInterface[]> {
const queryVector = await this.embeddings.embedQuery(query);
const results = await this.similaritySearchVectorWithScore(queryVector, k);
return results.map((result) => result[0]);
}
32.5 Step 4: 增量索引 — RecordManager
源码位置: libs/langchain-core/src/indexing/
RecordManager 解决的核心问题:当文档源更新时,如何只处理新增和修改的文档?
// RecordManagerInterface 核心方法
export interface RecordManagerInterface {
createSchema(): Promise<void>;
getTime(): Promise<number>;
update(keys: string[], updateOptions: UpdateOptions): Promise<void>;
exists(keys: string[]): Promise<boolean[]>;
listKeys(options: ListKeyOptions): Promise<string[]>;
deleteKeys(keys: string[]): Promise<void>;
}
indexDocs() 函数实现增量索引逻辑:
源码位置: libs/langchain-core/src/indexing/base.ts
import type { IndexingResult } from "@langchain/core/indexing";
// indexDocs 的返回值
type IndexingResult = {
numAdded: number; // 新增文档数
numDeleted: number; // 删除文档数
numUpdated: number; // 更新文档数
numSkipped: number; // 跳过(未变化)文档数
};
内部流程:
- 对每个文档计算内容 hash(
_HashedDocument) - 通过 RecordManager 检查哪些 hash 已存在
- 只对新增/修改的文档执行嵌入和存储
- 可选删除已不存在的文档
32.6 Step 5: 检索与生成
结合第 31 课的检索核心,构建完整 RAG 链:
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
const retriever = vectorStore.asRetriever({ k: 4 });
const ragPrompt = ChatPromptTemplate.fromMessages([
["system", `你是一个知识库助手。根据以下检索到的上下文回答用户问题。
如果上下文中没有相关信息,请说明你不确定。
上下文:
{context}`],
["human", "{question}"],
]);
function formatDocs(docs: Document[]): string {
return docs
.map((doc, i) => `[${i + 1}] ${doc.pageContent}\n来源: ${doc.metadata.source}`)
.join("\n\n");
}
const ragChain = RunnableSequence.from([
{
context: retriever.pipe(formatDocs),
question: new RunnablePassthrough(),
},
ragPrompt,
model,
new StringOutputParser(),
]);
const answer = await ragChain.invoke("什么是 VectorStore?");
32.7 Step 6: 集成对话历史
结合第 10 课的 RunnableWithMessageHistory,让 RAG 支持多轮对话:
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { ChatMessageHistory } from "@langchain/community/stores/message/in_memory";
// 改造 prompt,添加 MessagesPlaceholder
const ragPromptWithHistory = ChatPromptTemplate.fromMessages([
["system", `你是一个知识库助手。根据以下上下文回答问题。
上下文: {context}`],
["placeholder", "{chat_history}"],
["human", "{question}"],
]);
// 构造带历史的检索链
const chainWithHistory = RunnableSequence.from([
{
context: (input: { question: string }) =>
retriever.pipe(formatDocs).invoke(input.question),
question: (input: { question: string }) => input.question,
chat_history: (input: { chat_history: any }) => input.chat_history,
},
ragPromptWithHistory,
model,
new StringOutputParser(),
]);
// 用 RunnableWithMessageHistory 包装
const messageHistories: Record<string, ChatMessageHistory> = {};
const ragWithHistory = new RunnableWithMessageHistory({
runnable: chainWithHistory,
getMessageHistory: async (sessionId: string) => {
if (!messageHistories[sessionId]) {
messageHistories[sessionId] = new ChatMessageHistory();
}
return messageHistories[sessionId];
},
inputMessagesKey: "question",
historyMessagesKey: "chat_history",
});
// 多轮对话
const config = { configurable: { sessionId: "user-1" } };
await ragWithHistory.invoke({ question: "什么是 Runnable?" }, config);
await ragWithHistory.invoke({ question: "它有哪些核心方法?" }, config); // 自动携带上下文
32.8 Step 7: streamEvents 实时追踪
用 streamEvents("v2") 监听 RAG 链执行的每个阶段:
const eventStream = ragChain.streamEvents("什么是 VectorStore?", {
version: "v2",
});
for await (const event of eventStream) {
switch (event.event) {
case "on_retriever_start":
console.log("开始检索...", event.data.input);
break;
case "on_retriever_end":
console.log(`检索完成,找到 ${event.data.output.length} 个文档`);
break;
case "on_llm_stream":
// 实时输出 LLM 生成的 token
process.stdout.write(event.data.chunk.content);
break;
case "on_chain_end":
console.log("\n生成完成");
break;
}
}
这在前端应用中非常有用:可以先展示"检索中...",然后逐 token 展示生成结果。
32.9 生产 RAG 最佳实践
检索质量优化
| 策略 | 描述 |
|---|---|
| 分块大小调优 | 从 500 字符开始,根据领域特点调整 |
| 重叠设置 | 10-20% 重叠防止上下文断裂 |
| MMR 搜索 | searchType: "mmr" 避免返回高度重复的内容 |
| 元数据过滤 | 利用 filter 缩小搜索范围 |
| 混合检索 | 向量搜索 + 关键词搜索互补 |
生成质量优化
| 策略 | 描述 |
|---|---|
| 来源标注 | 在 prompt 中要求模型标注信息来源 |
| 置信度评估 | 要求模型评估回答的确定性 |
| 幻觉检测 | 检查回答是否基于检索到的上下文 |
| 输入校验 | 检查用户问题是否与知识库领域相关 |
32.10 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | langchain-core/src/vectorstores.ts | similaritySearch() 默认实现 |
| P0 | langchain-core/src/retrievers/index.ts | BaseRetriever 的 invoke() 模板方法 |
| P1 | langchain-core/src/indexing/base.ts | _HashedDocument 与增量索引逻辑 |
| P1 | langchain-core/src/indexing/record_manager.ts | RecordManagerInterface |
| P2 | langchain-core/src/embeddings.ts | Embeddings 接口 |
| P2 | langchain-textsplitters/src/text_splitter.ts | RecursiveCharacterTextSplitter |
32.11 实战练习
构建一个完整的知识库问答系统:
- 文档准备: 创建 10+ 个 Document 对象,模拟知识库内容,每个有不同 metadata
- 分块处理: 使用 RecursiveCharacterTextSplitter 分块,打印分块统计
- 检索链: 构建
retriever -> formatDocs -> prompt -> model -> parser完整链 - 对话历史: 用
RunnableWithMessageHistory包装,支持多轮追问 - 事件监控: 用
streamEvents("v2")追踪检索和生成的完整过程 - 测试: 使用
FakeListChatModel编写单元测试,验证链的输入输出
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 完成端到端 RAG 应用:从文档加载到问答生成 |
| 🔵 中阶 | 整合 Document -> Split -> Embed -> Store -> Retrieve -> Generate 全流程 |
| 🟡 高阶 | 理解 RecordManager 增量索引的 hash 去重机制 |
| 🟠 资深 | 能将对话历史与 RAG 结合,支持上下文追问 |
| 🔴 架构 | 能设计 RAG 评估方案:检索召回率、答案准确率、幻觉检测 |
下一课预告
第 33 课深入 MCP 协议适配 — 如何将 MCP (Model Context Protocol) 工具无缝接入 LangChain.js 的 Agent 体系。