📖 本章学习目标
完成本章后,你将能够:
- ✅ 理解 RAG 的核心价值和应用场景
- ✅ 掌握 RAG Pipeline 的五个阶段及其实现
- ✅ 选择合适的文档切割策略和 Embedding 模型
- ✅ 根据业务需求选择向量数据库
- ✅ 实现进阶检索策略(混合检索、查询改写、重排序)
- ✅ 诊断和优化 RAG 系统的常见问题
一、为什么需要 RAG
LLM 有两大固有知识型缺陷,RAG 正是为了解决这些问题而生。
💡 提示: RAG是一整套技术体系,涉及文本处理、向量数据、优化等多个知识体系,请关注后续推出的完整的RAG教程。
1、LLM 的局限性
问题 1:知识截止
现象:
typescript
// 用户问:"2025 年诺贝尔文学奖得主是谁?"
// LLM 回答:"抱歉,我的训练数据截止到 2024 年..."
原因:
- LLM 的知识来自静态的训练数据
- 训练完成后,知识就固定了
- 无法知道最新发生的事件
问题 2:私有数据缺失
现象:
typescript
// 用户问:"我们公司的产品退货政策是什么?"
// LLM 回答:"我不知道贵公司的具体政策..."
原因:
- LLM 没有访问你公司内部数据的权限
- 训练数据不包含你的私有文档
- 每个公司的业务流程都不一样
2、RAG 的解决方案
RAG = Retrieval(检索)+ Augmented(增强)+ Generation(生成)
核心思路:在提问时,先从知识库里找到最相关的内容,再把它作为上下文提供给模型,让模型"带着资料"回答。
这有点类似你在参加考试:
- ❌ 不使用 RAG:闭卷考试,只能靠记忆
- ✅ 使用 RAG:开卷考试,可以查阅参考书
显然,开卷考试的答案更准确、更可靠。
3、RAG 的工作流程
找出最相关的文档片段"] R --> P["构建 Prompt
问题 + 检索到的上下文"] P --> LLM["LLM 生成回答"] LLM --> A["最终回答
基于真实知识库"] DB[("知识库
向量数据库")] --> R style DB fill:#f6ffed,stroke:#52c41a,stroke-width:3px style R fill:#fff7e6,stroke:#fa8c16,stroke-width:3px
关键优势:
| 优势 | 说明 | 效果 |
|---|---|---|
| 实时性 | 可以随时更新知识库 | 回答最新信息 |
| 准确性 | 基于真实文档回答 | 减少幻觉 |
| 可追溯 | 可以显示引用来源 | 便于验证 |
| 成本低 | 不需要重新训练模型 | 经济高效 |
二、RAG Pipeline 的五个阶段
完整的 RAG 系统包含两个主要阶段:索引阶段 (离线)和检索生成阶段(在线)。
整体架构图
Loaders"] --> S["2. 文档切割
Splitters"] S --> EM["3. Embedding 生成
Embeddings"] EM --> VS[("4. 向量存储
Vector Store")] end subgraph Retrieval["检索阶段:每次查询执行"] Q["用户问题"] --> QE["问题 Embedding"] QE --> SR["5. 相似度检索"] VS --> SR SR --> Docs["相关文档片段"] end subgraph Generation["生成阶段:每次查询执行"] Docs --> Prompt["构建 Prompt"] Q --> Prompt Prompt --> LLM["LLM 生成"] LLM --> Answer["最终回答"] end style Indexing fill:#e8f4fd,stroke:#1890ff,stroke-width:2px style Retrieval fill:#fff7e6,stroke:#fa8c16,stroke-width:2px style Generation fill:#f6ffed,stroke:#52c41a,stroke-width:2px
接下来,我们逐一详解每个阶段。
三、阶段 1:文档加载(Document Loaders)
文档加载器的作用是将各种格式的文件转换为统一的 Document 对象。
1、Document 对象结构
typescript
interface Document {
pageContent: string; // 文档内容
metadata: Record<string, any>; // 元数据(来源、页码等)
}
示例:
typescript
{
pageContent: "产品的保修期为一年...",
metadata: {
source: "./docs/warranty.pdf",
page: 5,
author: "TechCorp"
}
}
2、常见文档加载器
LangChain.js 提供了丰富的文档加载器,支持几乎所有常见格式。
(1)PDF 文档
typescript
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
const pdfLoader = new PDFLoader("./docs/product-manual.pdf");
const pdfDocs = await pdfLoader.load();
console.log(`加载了 ${pdfDocs.length} 个页面`);
// 每个页面对应一个 Document 对象
(2)Word 文档
typescript
import { DocxLoader } from "@langchain/community/document_loaders/fs/docx";
const docxLoader = new DocxLoader("./docs/guide.docx");
const docxDoms = await docxLoader.load();
(3)CSV 表格
typescript
import { CSVLoader } from "@langchain/community/document_loaders/fs/csv";
const csvLoader = new CSVLoader("./data/products.csv");
const csvDocs = await csvLoader.load();
// 每行数据转换为一个 Document
// pageContent: "产品名称: iPhone 15, 价格: 7999"
// metadata: { row: 1 }
(4)网页内容
typescript
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
const webLoader = new CheerioWebBaseLoader("https://example.com/doc");
const webDocs = await webLoader.load();
// 自动提取网页的文本内容,去除 HTML 标签
(5)GitHub 仓库
typescript
import { GithubRepoLoader } from "@langchain/community/document_loaders/web/github";
const githubLoader = new GithubRepoLoader(
"https://github.com/langchain-ai/langchainjs",
{
branch: "main",
recursive: false, // 是否递归加载子目录
unknown: "warn" // 遇到未知文件类型的处理方式
}
);
const codeDocs = await githubLoader.load();
// 适合构建代码问答机器人
3、批量加载多个文件
使用DirectoryLoader可以实现多个文档的加载。
typescript
import { DirectoryLoader } from "langchain/document_loaders/fs/directory";
// 加载整个目录
const directoryLoader = new DirectoryLoader(
"./docs",
{
".pdf": (path) => new PDFLoader(path),
".docx": (path) => new DocxLoader(path),
".txt": (path) => new TextLoader(path),
}
);
const allDocs = await directoryLoader.load();
console.log(`总共加载了 ${allDocs.length} 个文档`);
四、阶段 2:文档切割(Text Splitters)
LLM 的上下文窗口有限,不能直接塞进整个文档,这是就需要对文档进行适当的切割,而切割策略直接影响检索质量。
1、为什么要切割?
问题场景:
假设你有一个 100 页的产品手册:
- ❌ 不切割:整个手册 50,000 字符,超出 Context Window
- ❌ 按页切割:可能把一个完整的话题截断
- ✅ 智能切割:按语义边界切割,保持话题完整性
2、RecursiveCharacterTextSplitter(推荐)
这是Langchain.js提供的最常用的文档切割器,会尝试多种分隔符,优先在语义边界处切割。
(1)基础用法
typescript
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000, // 每个 chunk 的最大字符数
chunkOverlap: 200, // 相邻 chunk 的重叠字符数
separators: [
"\n\n", // 优先在段落之间切割
"\n", // 其次在换行处切割
"。", // 中文句号
",", // 中文逗号
" ", // 空格
"" // 最后才按字符切割
],
});
const chunks = await splitter.splitDocuments(pdfDocs);
console.log(`切割为 ${chunks.length} 个 chunks`);
参数详解:
| 参数 | 作用 | 推荐值 | 说明 |
|---|---|---|---|
chunkSize |
每个块的最大字符数 | 500-1500 | 太小会丢失上下文,太大会稀释相关性 |
chunkOverlap |
相邻块的重叠字符数 | chunkSize 的 10-20% | 防止关键信息在边界处被截断 |
separators |
分隔符优先级列表 | 如上所示 | 从粗到细尝试切割 |
(2)可视化切割过程
bash
原始文档:
"第一段内容...\n\n第二段内容...\n\n第三段内容..."
切割过程:
1. 尝试在 "\n\n" 处切割 → 成功
2. 如果某段超过 chunkSize,尝试在 "\n" 处切割
3. 如果还超,尝试在 "。" 处切割
4. 依此类推...
结果:
Chunk 1: "第一段内容..."
Chunk 2: "...第一段内容...\n\n第二段内容..." (有重叠)
Chunk 3: "...第二段内容...\n\n第三段内容..."
3、其他切割器
(1)Markdown 专用切割器
MarkdownTextSplitter 专门用于切割Markdown文档。
typescript
import { MarkdownTextSplitter } from "@langchain/textsplitters";
const mdSplitter = new MarkdownTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});
// 会识别 Markdown 标题层级,优先在标题处切割
const mdChunks = await mdSplitter.splitDocuments(mdDocs);
(1)代码专用切割器
typescript
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
const codeSplitter = RecursiveCharacterTextSplitter.fromLanguage(
"typescript", // 支持 python, java, javascript 等
{
chunkSize: 1000,
chunkOverlap: 200,
}
);
// 会识别代码结构(函数、类等),避免在中间切割
const codeChunks = await codeSplitter.splitDocuments(codeDocs);
4、⚠️ 切割策略的常见误区
误区 1:chunkSize 越大越好
❌ 错误做法:
typescript
chunkSize: 10000 // 太大!
问题:
- 一个 chunk 里包含多个话题
- 检索时相关性计算被稀释
- 浪费 Token
✅ 推荐做法:
typescript
chunkSize: 800-1200 // 根据文档类型调整
误区 2:忽略 chunkOverlap
❌ 错误做法:
typescript
chunkOverlap: 0 // 没有重叠
问题:
- 关键信息可能在边界处被截断
- 上下文不连贯
✅ 推荐做法:
typescript
chunkOverlap: chunkSize * 0.15 // 15% 的重叠
误区 3:中文文档用英文分隔符
❌ 错误做法:
typescript
separators: ["\n\n", "\n", " ", ""] // 缺少中文标点
✅ 推荐做法:
typescript
separators: ["\n\n", "\n", "。", ",", ";", " ", ""]
五、阶段 3:Embedding 生成
Embedding 是将文本转换为向量,使得语义相似的文本在向量空间中距离更近。
1、什么是 Embedding?
直观理解:
Embedding 就像给文本分配一个"语义坐标":
- "猫" → [0.1, 0.8, -0.3, ...]
- "狗" → [0.2, 0.7, -0.2, ...] (与"猫"接近)
- "汽车" → [-0.5, -0.1, 0.9, ...] (与"猫"远离)
这样,我们可以通过计算向量距离来判断文本相似度。
2、常用的 Embedding 模型
(1)OpenAI Embeddings(推荐)
typescript
import { OpenAIEmbeddings } from "@langchain/openai";
const embeddings = new OpenAIEmbeddings({
model: "text-embedding-3-small", // 性价比最高
// model: "text-embedding-3-large", // 效果更好,但更贵
});
// 生成向量
const vector = await embeddings.embedQuery("你好,世界");
console.log(vector.length); // 1536 维(small)或 3072 维(large)
模型对比:
| 模型 | 维度 | 性能 | 成本(每 1M tokens) | 适用场景 |
|---|---|---|---|---|
| text-embedding-3-small | 1536 | ⭐⭐⭐⭐ | $0.02 | 大多数场景 |
| text-embedding-3-large | 3072 | ⭐⭐⭐⭐⭐ | $0.13 | 对精度要求极高 |
| text-embedding-ada-002 | 1536 | ⭐⭐⭐ | $0.10 | 旧项目兼容 |
(2)开源模型(自托管)
typescript
import { HuggingFaceTransformersEmbeddings } from "@langchain/community/embeddings/hf_transformers";
const embeddings = new HuggingFaceTransformersEmbeddings({
modelName: "Xenova/all-MiniLM-L6-v2", // 轻量级模型
});
// 优点:免费、隐私性好
// 缺点:效果略逊于 OpenAI,需要本地计算资源
3、批量生成 Embedding
typescript
// 单个文本
const vector1 = await embeddings.embedQuery("第一个文本");
// 批量文本(更高效)
const vectors = await embeddings.embedDocuments([
"第一个文本",
"第二个文本",
"第三个文本",
]);
console.log(vectors.length); // 3
console.log(vectors[0].length); // 1536
六、阶段 4:向量存储
向量数据库专门你负责存储和检索 Embedding 向量。
1、向量数据库选型矩阵
| 数据库 | 部署方式 | 适用场景 | 特点 | 学习曲线 |
|---|---|---|---|---|
| Pinecone | 云端托管 | 生产环境首选 | 零运维,按量付费,高性能 | ⭐⭐ |
| Chroma | 本地/云端 | 开发测试、小规模 | 开源,嵌入式,易上手 | ⭐ |
| PGVector | 自托管 | 已有 PostgreSQL | 直接在 PG 里存向量,运维简单 | ⭐⭐ |
| Weaviate | 自托管/云端 | 混合检索场景 | 支持关键词+向量混合搜索 | ⭐⭐⭐ |
| Qdrant | 自托管/云端 | 高性能需求 | Rust 实现,资源消耗低 | ⭐⭐⭐ |
| Milvus | 自托管 | 超大规模 | 分布式,亿级向量 | ⭐⭐⭐⭐ |
2、Pinecone(生产环境推荐)
(1)安装依赖
bash
pnpm add @langchain/pinecone @pinecone-database/pinecone
(2)创建索引
typescript
import { Pinecone } from "@pinecone-database/pinecone";
const pinecone = new Pinecone({
apiKey: process.env.PINECONE_API_KEY!
});
// 创建索引(只需执行一次)
await pinecone.createIndex({
name: "my-knowledge-base",
dimension: 1536, // 与 Embedding 维度一致
metric: "cosine", // 相似度度量:cosine / euclidean / dotproduct
spec: {
serverless: {
cloud: "aws",
region: "us-east-1",
},
},
});
(3)存储向量
typescript
import { PineconeStore } from "@langchain/pinecone";
import { OpenAIEmbeddings } from "@langchain/openai";
const embeddings = new OpenAIEmbeddings();
const index = pinecone.Index("my-knowledge-base");
// 从 Documents 批量写入
const vectorStore = await PineconeStore.fromDocuments(
chunks, // 切割后的文档块
embeddings, // Embedding 模型
{
pineconeIndex: index,
namespace: "product-docs", // 可选:命名空间隔离
}
);
console.log("向量存储完成");
3、Chroma(开发测试推荐)
bash
pnpm add chromadb
typescript
import { Chroma } from "@langchain/community/vectorstores/chroma";
const vectorStore = await Chroma.fromDocuments(
chunks,
embeddings,
{
collectionName: "my-collection",
url: "http://localhost:8000", // Chroma 服务地址
}
);
启动 Chroma 服务:
bash
docker run -p 8000:8000 chromadb/chroma
4、PGVector(PostgreSQL 用户推荐)
bash
pnpm add @langchain/community pg
typescript
import { PGVectorStore } from "@langchain/community/vectorstores/pgvector";
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const vectorStore = await PGVectorStore.initialize(
embeddings,
{
pool,
tableName: "documents",
columns: {
idColumnName: "id",
vectorColumnName: "embedding",
contentColumnName: "content",
metadataColumnName: "metadata",
},
}
);
// 添加文档
await vectorStore.addDocuments(chunks);
优势:
- ✅ 无需额外部署向量数据库
- ✅ 可以利用 PostgreSQL 的事务和备份
- ✅ 适合已有 PG 基础设施的团队
七、阶段 5:检索与生成
这是 RAG 的在线阶段,每次用户提问时执行。
1、基础检索
(1)创建检索器
typescript
// 从向量存储创建检索器
const retriever = vectorStore.asRetriever({
k: 5, // 返回最相似的 5 个文档
});
// 执行检索
const docs = await retriever.invoke("产品的退货政策是什么?");
console.log(docs.length); // 5
console.log(docs[0].pageContent); // 最相关的文档内容
console.log(docs[0].metadata); // 元数据(来源、页码等)
(2)相似度阈值过滤
typescript
const retriever = vectorStore.asRetriever({
k: 5,
filter: {
similarityThreshold: 0.7, // 只返回相似度 > 0.7 的文档
},
});
2、构建 RAG Prompt
typescript
import { ChatPromptTemplate } from "@langchain/core/prompts";
const ragPrompt = ChatPromptTemplate.fromMessages([
[
"system",
`你是一个专业的知识库问答助手。请根据以下上下文信息回答用户的问题。
重要规则:
1. 只基于提供的上下文回答问题
2. 如果上下文中没有相关信息,请明确告知"根据现有文档,我无法回答这个问题"
3. 不要编造答案
4. 在回答末尾标注信息来源
上下文:
{context}`,
],
["human", "{input}"],
]);
关键点:
- 明确告诉模型只基于上下文回答
- 提供"不知道"的出口,避免幻觉
- 要求标注来源,便于验证
3、完整的 RAG Chain
typescript
import { ChatOpenAI } from "@langchain/openai";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { createRetrievalChain } from "langchain/chains/retrieval";
const model = new ChatOpenAI({
model: "gpt-4o",
temperature: 0 // 确定性输出
});
// 第一步:创建文档合并链
const combineDocsChain = await createStuffDocumentsChain({
llm: model,
prompt: ragPrompt,
});
// 第二步:创建检索链
const ragChain = await createRetrievalChain({
retriever,
combineDocsChain,
});
// 第三步:执行
const result = await ragChain.invoke({
input: "产品的退货政策是什么?",
});
console.log(result.answer); // 基于知识库的回答
console.log(result.context); // 检索到的文档片段
返回结果结构:
typescript
{
input: "产品的退货政策是什么?",
context: [
Document { pageContent: "退货期限为购买后30天...", metadata: {...} },
Document { pageContent: "退款将在7个工作日内处理...", metadata: {...} },
// ...更多相关文档
],
answer: "根据产品手册,退货政策如下:\n1. 退货期限:购买后30天内\n2. 退款时间:7个工作日内处理\n\n来源:product-manual.pdf 第5页"
}
4、显示引用来源
typescript
// 提取来源信息
const sources = result.context.map(doc => ({
content: doc.pageContent.slice(0, 100) + "...",
source: doc.metadata.source,
page: doc.metadata.page,
}));
console.log("引用来源:");
sources.forEach((src, i) => {
console.log(`${i + 1}. ${src.source} (第${src.page}页)`);
console.log(` ${src.content}\n`);
});
八、检索策略进阶
基础的相似度检索能覆盖大多数场景,但在实际业务中,往往需要更精细的检索策略。
1、混合检索(Hybrid Search)
(1)为什么需要混合检索?
纯向量检索的局限:
- 对精确关键词匹配不够敏感
- 例如搜索产品型号"X-2000",向量检索可能找不到
混合检索的优势:
- 结合关键词搜索(BM25)和向量搜索
- 兼顾语义相似性和关键词匹配
(2)实现方式
使用 Weaviate:
typescript
import { WeaviateStore } from "@langchain/weaviate";
const vectorStore = await WeaviateStore.fromDocuments(
chunks,
embeddings,
{
client: weaviateClient,
indexName: "Documents",
textKey: "text",
metadataKeys: ["source"],
}
);
// 混合检索
const hybridRetriever = vectorStore.asRetriever({
searchType: "hybrid",
searchKwargs: {
alpha: 0.5, // 0: 纯关键词, 1: 纯向量, 0.5: 各一半
},
});
const docs = await hybridRetriever.invoke("X-2000 产品规格");
alpha 参数调优:
| alpha 值 | 检索类型 | 适用场景 |
|---|---|---|
| 0.0 | 纯关键词 | 搜索精确的产品型号、代码 |
| 0.5 | 平衡模式 | 大多数场景 |
| 1.0 | 纯向量 | 语义搜索、概念性问题 |
2、查询改写(Query Rewriting)
(1)问题场景
用户的原始问题有时候表述不清:
- "它有几年历史了?"("它"指代什么?)
- "这个怎么样?"("这个"是什么?)
- "多少钱?"(什么的价格?)
(2)解决方案
让 LLM 先对问题做改写,再检索:
typescript
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
// 定义查询改写 Prompt
const queryRewriterPrompt = ChatPromptTemplate.fromMessages([
[
"system",
`你是一个查询优化助手。将用户的问题改写为更适合向量检索的格式:
1. 去除指代词(它、这个、那个)
2. 补充完整语义
3. 提取核心实体
4. 只输出改写后的查询,不要解释`,
],
["human", "原始问题:{question}"],
]);
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const rewriteChain = queryRewriterPrompt.pipe(model);
// 改写查询
const rewrittenQuery = await rewriteChain.invoke({
question: "它有几年历史了?",
});
console.log(rewrittenQuery.content);
// 输出:"Apple 公司成立多少年了?"
// 用改写后的查询进行检索
const docs = await retriever.invoke(rewrittenQuery.content as string);
(3)多步查询改写
对于复杂问题,可以进行多轮改写:
typescript
// 第一轮:澄清指代
const clarified = await clarifyReferences(originalQuestion);
// 第二轮:扩展缩写
const expanded = await expandAbbreviations(clarified);
// 第三轮:提取关键词
const keywords = await extractKeywords(expanded);
// 最终查询
const finalQuery = `${expanded} ${keywords}`;
3、重排序(Reranking)
(1)为什么需要重排序?
问题:
- 向量检索返回的结果按相似度排序
- 但相似度 ≠ 相关性
- 可能返回语义相似但不相关的文档
解决方案:
- 先用向量检索召回较多结果(如 20 个)
- 再用专门的重排序模型精排
- 取前 N 个最相关的
(2)使用 Cohere Reranker
typescript
import { CohereRerank } from "@langchain/cohere";
const reranker = new CohereRerank({
apiKey: process.env.COHERE_API_KEY,
topN: 3, // 重排序后取前 3 个
model: "rerank-multilingual-v2.0", // 支持多语言
});
// 第一步:粗检索(召回 20 个)
const coarseResults = await retriever.invoke(query, { k: 20 });
// 第二步:精排序(取前 3 个)
const refinedResults = await reranker.compressDocuments(
coarseResults,
query
);
console.log(refinedResults.length); // 3
效果对比:
| 策略 | 召回数量 | 准确率 | 延迟 | 成本 |
|---|---|---|---|---|
| 纯向量检索 | 5 | 70% | 快 | 低 |
| 向量 + 重排序 | 20→3 | 90% | 中 | 中 |
| 混合检索 + 重排序 | 20→3 | 95% | 慢 | 高 |
(3)其他重排序模型
| 模型 | 提供商 | 特点 | 成本 |
|---|---|---|---|
| Cohere Rerank | Cohere | 效果好,支持多语言 | $0.02/100次 |
| Jina Reranker | Jina AI | 开源,可自托管 | 免费 |
| BGE Reranker | BAAI | 中文效果好 | 免费 |
九、RAG 的常见问题与调优
1. 问题诊断矩阵
| 问题现象 | 可能原因 | 诊断方法 | 解决方案 |
|---|---|---|---|
| 回答不准确 | chunk 太大,相关内容被稀释 | 检查 chunk 大小和内容 | 减小 chunkSize 到 500-800 |
| 找不到相关内容 | 语义距离太大 | 检查检索结果的相似度分数 | 1. 尝试查询改写 2. 切换更好的 Embedding 模型 3. 增大 k 值 |
| 来源文档不准确 | 关键词检索失效 | 检查是否包含精确关键词 | 使用混合检索 |
| 回答跨段落混乱 | chunk 之间上下文丢失 | 检查 chunkOverlap | 增大 chunkOverlap 到 20-25% |
| 频繁出现"无法回答" | 检索到的文档不相关 | 检查检索结果的相关性 | 1. 加入重排序步骤 2. 调整相似度阈值 3. 优化文档切割 |
| 响应速度慢 | 检索或生成耗时过长 | 监控各环节耗时 | 1. 减小 k 值 2. 使用更快的 Embedding 模型 3. 缓存常见查询 |
2. 调优建议
(1)文档切割优化
typescript
// 针对不同类型的文档使用不同的切割策略
// 技术文档:较小的 chunk
const techSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 600,
chunkOverlap: 100,
});
// 故事/文章:较大的 chunk
const storySplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1200,
chunkOverlap: 200,
});
// 代码:使用语言感知切割
const codeSplitter = RecursiveCharacterTextSplitter.fromLanguage(
"typescript",
{ chunkSize: 800, chunkOverlap: 150 }
);
(2)Embedding 模型选择
typescript
// 根据预算和精度要求选择
// 预算充足,追求精度
const premiumEmbeddings = new OpenAIEmbeddings({
model: "text-embedding-3-large",
});
// 性价比之选(推荐)
const balancedEmbeddings = new OpenAIEmbeddings({
model: "text-embedding-3-small",
});
// 预算有限,自托管
const freeEmbeddings = new HuggingFaceTransformersEmbeddings({
modelName: "Xenova/all-MiniLM-L6-v2",
});
(3)检索参数调优
typescript
// 从小 k 值开始,逐步增加
const retriever = vectorStore.asRetriever({
k: 3, // 从 3 开始测试
});
// 监控检索结果的相关性
const docs = await retriever.invoke(query);
docs.forEach((doc, i) => {
console.log(`文档 ${i + 1} 相关性:${doc.metadata.score}`);
});
// 如果相关性低,增大 k 或改进 Embedding
(4)Prompt 优化
typescript
// 不好的 Prompt
const badPrompt = ChatPromptTemplate.fromMessages([
["system", "回答问题:{input}"],
]);
// 好的 Prompt
const goodPrompt = ChatPromptTemplate.fromMessages([
[
"system",
`你是专业的客服助手。基于以下上下文回答问题:
规则:
1. 只使用提供的信息
2. 如果信息不足,说"我需要更多信息"
3. 引用具体来源
4. 语气友好专业
上下文:
{context}`,
],
["human", "{input}"],
]);
十、本章小结
RAG 是让 LLM 具备实时知识和私有数据能力的关键技术,在减少AI输出幻觉和增加准确度上都非常重要。
📝 核心知识点回顾
| 阶段 | 关键组件 | 最佳实践 |
|---|---|---|
| 1. 文档加载 | PDFLoader, WebLoader 等 | 选择适合的加载器,保留元数据 |
| 2. 文档切割 | RecursiveCharacterTextSplitter | chunkSize 800-1200,overlap 15% |
| 3. Embedding | OpenAI text-embedding-3-small | 平衡成本和效果 |
| 4. 向量存储 | Pinecone, Chroma, PGVector | 根据场景选择 |
| 5. 检索生成 | Retriever + RAG Chain | 添加引用来源,控制幻觉 |
🎯 动手练习
尝试完成以下练习,巩固所学知识:
练习 1:构建产品知识库
- 收集公司产品文档(PDF、Word、网页)
- 实现完整的 RAG Pipeline
- 测试常见问题,评估回答质量
- 优化 chunkSize 和 k 值
练习 2:实现查询改写
- 创建查询改写 Prompt
- 测试指代消解效果
- 对比改写前后的检索质量
练习 3:添加重排序
- 集成 Cohere Reranker
- 对比有无重排序的效果差异
- 分析成本和收益
练习 4:性能优化
- 监控各环节耗时
- 实现查询缓存
- 优化向量数据库配置
- 目标:P95 延迟 < 2 秒