文本切片与召回(Chunk、Overlap 到混合检索)学习笔记

摘要 :文本切片(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。
  • 向量懂语义,关键词懂名词。
  • 双通道合并重排,召回又准又全。
相关推荐
码农进化录2 小时前
Java 程序员的 AI 进化论 | Spring Boot 搭企业智能客服,从设计到上线
java·spring boot·openai
全栈弄潮儿2 小时前
4 个新手就能直接套用的 AI 编程提示词模板
chatgpt·openai·ai编程
做前端的娜娜子2 小时前
#浏览器存储方案:localStorage、sessionStorage 与 Cookie
前端·面试·掘金·金石计划
赵广陆2 小时前
企业实战:Milvues向量数据库实践
数据库·pycharm·langchain
全栈弄潮儿13 小时前
如何向 AI 清楚描述一个编程需求
chatgpt·openai·ai编程
CC大煊17 小时前
从夯到拉:锐评国内向量模型选型
人工智能·ai·架构·langchain
光影少年19 小时前
react navite性能优化 & 常见坑
前端·react native·掘金·金石计划