摘要 :文本切片(Chunking)把长文档切成大小合适的片段,让每段能被独立向量化与检索;Overlap 让相邻片段重叠一部分,避免语义在边界被切断。单靠向量检索(稠密)容易漏掉精确关键词,混合检索把向量通道与关键词通道(稀疏,如 BM25)的结果合并重排,显著提升召回率。核心要点:1)chunkSize 决定单片段长度,chunkOverlap 取 10%~20%;2)LangChain 用 RecursiveCharacterTextSplitter 按分隔符递归切;3)切片后每段
embedDocuments成向量入库;4)查询用embedQuery转向量,再用余弦相似度取 TopK;5)混合检索 = 向量 TopK ∪ 关键词 TopK,经 RRF/去重后输出。一句话记忆:长文切段留重叠,向量关键词双通道,合并重排召回高。
一、为什么需要文本切片
LLM 有上下文窗口限制,检索阶段也需要合适的颗粒度:
- 整篇文档直接向量化 → 语义被平均稀释,检索不精准。
- 切得太碎 → 单段语义不全,且向量库膨胀、检索噪声大。
js
// 你的 4.splitter.ts:先切再向量化
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 50, // 每片约 50 字符(demo 值,真实常用 256~512)
chunkOverlap: 2, // 相邻片重叠 2 字符,保持上下文连续
});
const chunks = await splitter.splitText(longText);
console.log("chunks", chunks);
二、Chunk 与 Overlap 参数
| 参数 | 含义 | 取值建议 | 过大 / 过小的问题 |
|---|---|---|---|
| chunkSize | 单个片段的字符数 | 256~512(中文可更小) | 太大稀释语义;太小语义不全 |
| chunkOverlap | 相邻片段重叠字符数 | chunkSize 的 10%~20% | 太小边界切句;太大冗余浪费 |
类比:把长文章切成带"装订边"的活页,每页多印一点上一页的尾巴,翻页时不会读断句子。
三、LangChain 文本切片器
RecursiveCharacterTextSplitter 按分隔符优先级递归切:\n\n → \n → 。 → → "",优先在段落/句子边界切,保证片段尽量完整。
js
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 50,
chunkOverlap: 2,
});
const chunks = await splitter.splitText(
"向量嵌入技术能够将文本转换为高维数值向量,使计算机可以基于语义计算相似度。" +
"通过余弦相似度,我们能衡量两句话在含义上的接近程度,从而支撑语义检索、智能问答与推荐系统等应用场景。"
);
console.log("chunks", chunks);
你踩过的坑:文本太短(仅一句)时切片后只有 1 个 chunk,后面 vectors[1] 是 undefined,余弦相似度 fallback 成 [] 被 0 除得到 NaN。真实文档会切出多个 chunk,应循环遍历比较而非写死下标。
四、从切片到向量召回
切片 → 向量化 → 存库 → 查询 → 相似度排序。
js
const embedding = new OpenAIEmbeddings({
model: getEnv("NANA_EMBEDDING_MODEL"), // BAAI/bge-m3
apiKey: getEnv("NANA_EMBEDDING_API_KEY"), // 硅基流动 key
configuration: { baseURL: getEnv("EMBEDDING_BASE_URL") }, // siliconflow
});
// 文档侧:每段各自向量化
const vectors = await embedding.embedDocuments(chunks);
// 查询侧:query 单独向量化
const inputVector = await embedding.embedQuery("向量");
// 余弦相似度
const cosineSimilarity = (v1: number[], v2: number[]) => {
const dotProduct = v1.reduce((acc, cur, i) => acc + cur * (v2[i] ?? 0), 0);
const m1 = Math.sqrt(v1.reduce((acc, cur) => acc + cur * cur, 0));
const m2 = Math.sqrt(v2.reduce((acc, cur) => acc + cur * cur, 0));
if (m1 === 0 || m2 === 0) return 0;
return dotProduct / (m1 * m2);
};
五、混合检索:向量 + 关键词
向量检索擅长"语义相近",但遇到专有名词、编号、精确关键词时可能漏。关键词检索(BM25 / 全文检索)擅长精确匹配。两者互补:
js
// 你的代码里已出现关键词检索雏形(全文检索)
const k = chunks[0]?.indexOf("向量"); // 命中位置
| 通道 | 原理 | 擅长 | 短板 |
|---|---|---|---|
| 向量(稠密) | 语义向量 + 余弦相似度 | 同义改写、语义泛化 | 精确关键词 / 编号易漏 |
| 关键词(稀疏 BM25) | 词频 / 逆文档频率匹配 | 专有名词、精确字符串 | 不懂同义词 |
| 混合 | 两者结果合并重排 | 兼顾语义与精确 | 实现稍复杂 |
合并常用 RRF(Reciprocal Rank Fusion) :对每个通道的排名取 1/(rank+k) 加权求和,再统一排序输出 TopK。
六、完整流程串联
scss
原始长文档
│ RecursiveCharacterTextSplitter(chunkSize, chunkOverlap)
▼
[Chunk1] [Chunk2] [Chunk3] ← 相邻片有 overlap
│ embedDocuments
▼
向量库 [v1, v2, v3]
│
请求 Query "向量"
├─→ embedQuery → qv ──→ 余弦相似度 → TopK(向量)
└─→ 分词 / BM25 ──→ 命中 → TopK(关键词)
│
▼
向量TopK ∪ 关键词TopK
│
▼
去重 + RRF 重排 → 最终召回片段
七、记忆口诀
- 长文先切段,向量才好算。
- Overlap 留重叠,边界不断句。
- 文档 embedDocuments,查询 embedQuery。
- 向量懂语义,关键词懂名词。
- 双通道合并重排,召回又准又全。