Embedding 过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
文本输入
   │
   ▼
┌────────────────────────────────────────┐
│ 1. Tokenization (分词)                 │
│   "苹果手机屏幕碎了怎么办?"          │
│   → ["苹果", "手机", "屏幕", "碎", ...]│
└────────────────┬───────────────────────┘
                 │
                 ▼
┌────────────────────────────────────────┐
│ 2. Token Embedding                     │
│   每个token转换为初始向量               │
│   ["苹果"] → [0.123, -0.456, ...]      │
└────────────────┬───────────────────────┘
                 │
                 ▼
┌────────────────────────────────────────┐
│ 3. Transformer Encoder (12层)          │
│   Layer 1: Self-Attention              │
│   Layer 2: Feed-Forward                │
│   ...                                  │
│   Layer 12: Final Representation       │
└────────────────┬───────────────────────┘
                 │
                 ▼
┌────────────────────────────────────────┐
│ 4. Pooling (池化)                      │
│   方式: Mean Pooling (平均池化)        │
│   所有token向量取平均                   │
└────────────────┬───────────────────────┘
                 │
                 ▼
┌────────────────────────────────────────┐
│ 5. Normalization (归一化)              │
│   L2 Norm: ||v|| = 1                   │
│   便于余弦相似度计算                    │
└────────────────┬───────────────────────┘
                 │
                 ▼
   最终向量 (1024维)
   [0.023, -0.156, 0.089, ...]

切片chunking strategy:

本质上是在做 “语义完整性”“检索粒度” 之间的权衡

固定大小切片 (Fixed-Size Chunking)

这是最简单、最基础的策略,也是 LangChain 等框架的默认配置。

  • 原理: 设定一个固定的 Token 数(例如 512),按照字符长度硬切。为了防止切断句子,通常会设置 Overlap(重叠窗口),比如重叠 50 个 Token。
  • 优点: 简单粗暴,计算开销低,索引规整。
  • 缺点: 极其容易打断语义。比如把“不”和“建议购买”切到了两个 Chunk 里,或者把商品的“型号”和“价格”切开了。
适配维度 建议配置
适合场景 一般性的文档,或者对语义精度要求不高的场景。
Embedding 通用短文本模型 (如 OpenAI text-embedding-3-small, BGE-Base)。因为长度固定,模型容易处理。
检索方式 **Dense Retrieval (纯向量检索)**。因为文本经常被截断,关键词索引(ES)可能会因为关键词缺失而失效。

2. 结构化/内容感知切片 (Structure-Aware / Recursive Chunking)

这是目前最推荐的“基准策略”。

  • 原理: 尊重文档的物理结构。优先在 \n\n (段落) 处切分,切不开再在 \n (换行) 处切,再不行在 . (句号) 处切。
  • 针对代码/Markdown: 会按照 Header (#, ##) 或代码块函数定义来切。
  • 优点: 保持了段落的完整性,语义比固定大小要好得多。
  • 缺点: Chunk 大小不均匀。有的段落极长,有的极短,可能会影响向量相似度的计算分布。
适配维度 建议配置
适合场景 结构清晰的文档(Markdown, HTML, 论文, 说明书)。
Embedding 支持变长的模型 (如 Jina-Embeddings-v2, BGE-M3)。这些模型对长短不一的输入适应性较好。
检索方式 **Hybrid Search (混合检索)**。因为保留了结构,段落标题通常包含很好的关键词,适合 ES 倒排索引发挥作用。

3. 语义切片 (Semantic Chunking)

这是“AI 原生”的高级策略,成本较高但效果极佳。

  • 原理: 不按标点符号切,而是一边读一边算向量
    1. 把文章按句子切开。
    2. 计算相邻两句的 Embedding 相似度。
    3. 如果相似度很高(在聊同一个话题),就合并;如果相似度突然暴跌(话题转折),就在这里切一刀。
  • 优点: 语义纯度极高。每个 Chunk 都在讲同一件事,检索匹配度极高。
  • 缺点: 。在入库(Ingestion)阶段就需要调用大量 Embedding API,构建成本高。
适配维度 建议配置
适合场景 话题跳转频繁的对话记录、会议纪要、长篇叙事文章。
Embedding 高灵敏度模型 (如 OpenAI, Cohere)。需要模型对语义变化非常敏感。
检索方式 **Dense Retrieval (纯向量)**。因为切分依据就是向量距离,所以用向量检索是绝配。

4. 父子索引/小切片 (Parent-Child / Small-to-Big)

这是我们在上一轮对话中讨论的策略,是解决“检索”与“生成”矛盾的神器。

  • 原理:
    • Child Chunk (检索用): 切得很碎(比如 128 Token),甚至是一句话。
    • Parent Chunk (生成用): 对应的大段落或整篇文档。
    • 检索时搜 Child,给 LLM 时送 Parent。
  • 优点: 检索极准(因为 Child 语义聚焦),生成极好(因为 Parent 上下文全)。
  • 缺点: 存储空间翻倍(要存两份数据),元数据管理复杂。
适配维度 建议配置
适合场景 精细化问答(如电商参数查询、合同条款审查)。
Embedding 多粒度模型 (BGE-M3, ColBERT)。特别是 ColBERT,非常适合这种细粒度的短语匹配。
检索方式 混合检索 + ID 后处理。先用向量搜 Child,再用 ID 查库捞 Parent。

5. 摘要/命题切片 (Summary / Propositional Chunking)

这是利用 LLM 辅助切片的终极策略。

  • 原理 (两种流派):
    1. Summary-Based: 对每个 Chunk,让 LLM 写一个摘要。索引时存摘要向量,生成时返回原 Chunk。
    2. Propositional (命题式): 让 LLM 把复杂的长难句拆解成一个个独立的简单事实陈述(Propositions)。
      • 原句: “小明昨天没去上学因为他生病了。”
      • 命题: [“小明昨天没去上学。”, “小明生病了。”]
  • 优点: 彻底解决了“指代不清”和“复杂逻辑干扰”的问题。
  • 缺点: 。处理数据时要消耗大量 LLM Token。
适配维度 建议配置
适合场景 逻辑复杂的法律文书、含有大量代词(他、它、那个)的上下文。
Embedding 指令微调模型 (Instruction-tuned Embeddings)。因为摘要和原文件是不同性质的文本,需要模型理解“这是一个摘要”。
检索方式 Dense Retrieval。摘要通常浓缩了语义,非常适合向量匹配。

稀疏Embedding

稀疏向量的特点是:维度极高(等于词表大小,如 30,000 ~ 100,000 维),但只有极少数位置有值(非零),其他全是 0。

  • 统计类 (Traditional/Statistical):
    • Bag-of-Words (词袋): 简单的 0/1 编码,有这个词就是 1。
    • BM25 / TF-IDF: 经典的加权算法。Milvus 现在支持直接把 BM25 算分结果存为稀疏向量。
  • 学习类 (Learned/Neural) —— 目前的 SOTA(State of the Art):
    • SPLADE (Sparse Lexical and Expansion): 这是目前最火的稀疏模型。
      • 魔法: 它不仅能给句子里的词算权重,还能做“词扩展”。比如原句只有“手机”,SPLADE 生成的稀疏向量里,”智能”、”电话”、”通讯” 这些词的位置也会有值(虽然原句没出现)。
    • BGE-M3: 智源的这个模型,除了生成稠密向量,也能生成类似 SPLADE 的稀疏向量。

BM25

将文本字段转换为稀疏向量用于计算!

  1. bm25_k1=1.2:适度重视术语频率,避免过度加权;
  • k1 越大(比如 2.0+):词出现的次数越多,分数涨得越猛。
  • k1 越小(比如 1.2):词出现 1-2 次后,再多出现几次,分数也不会涨太多了(饱和了)。
  1. bm25_b=0.75:对较长文档施加适度惩罚,兼顾结果准确性与全面性。
  • b = 1:完全惩罚。严格按长度归一化,长文很吃亏。
  • b = 0:不惩罚。长文和短文一视同仁。
  1. inverted_index_algo: ‘DAAT_MAXSCORE’,- 如果某个文档的理论最高分 < 当前第 10 名的分数,系统就直接跳过不算了。 结果: 极大地提升了查询速度(QPS),尤其是在数据量大的时候

科研成果需求匹配系统

  • PDF/Word/Excel,自动进行清洗和语义切片关于Token超长: “如果用户上传了一本500页的书,超过了模型的上下文限制,你怎么处理?”(考察切片、滑窗、摘要总结策略)。目前尚未实现。
  • 混合检索(Hybrid Search) + 重排序(Re-rank) 提高检索精度(Milvus + BGE) 目前是Dense Retrieval盲目相信向量检索results = self.collection.query(...)在精确匹配(Exact Match)上很弱。
  • 切片策略 (Chunking Strategy)。 你需要在向量化之前,加一个TextSplitter初级做法: 固定字符数切分(如每500字切一段)。高级做法(面试加分): 递归字符切分 (Recursive Character Splitter) 或者 **语义切分 (Semantic Chunking)**。保证句子不被切断,保持语义连贯性。 目前直接拼接text = f"{title}\n{abstract} embedding = self.embed_text(text)"
  • 集成Ragas进行自动化效果评估
  • 关于幻觉: “如果知识库里没有答案,大模型开始胡编乱造,怎么抑制?”(考察Prompt约束、引用溯源、置信度阈值)。
  • 关于数据脏乱: “PDF里有很多表格和图片,简单的文本提取会乱码,你怎么处理表格数据的RAG?”(考察对多模态解析或表格转Markdown的处理能力,这是一个痛点)。

为什么目前不进行进一步切片?

在经典的 RAG 教程中,我们经常会看到 RecursiveCharacterTextSplitter
等工具把文档切成 500 字一段并保留 50
字的重叠(Overlap)。但当前系统没这么做,主要基于以下三个设计考量:

  1. 数据长度刚刚好(刚好在模型处理范围内)
    学术论文的摘要(Abstract)通常只有 200~300
    个英文单词;成果描述也不会特别长。而当前使用的向量化模型
    paraphrase-multilingual-MiniLM-L12-v2 的最大上下文输入长度(Max Sequence
    Length)通常是 256 或 512 个
    Token。把“标题+摘要”拼在一起的长度,恰好落在这个模型能一口吃下的“最佳射程”内,既
    不会被截断,也不会因为太长而导致重点模糊。

  2. 保证了“语义的完整性”
    一篇优秀的论文摘要是一个完整的逻辑闭环:背景痛点 -> 提出方法 ->
    实验结果。如果强行把它切成两半(比如前一半切到了痛点,后一半切到了方法),当用户
    搜索“某种解决方法”时,带有“痛点”的那半块向量就永远不会被搜出来,反而导致召回率下
    降。摘要本身就是天然的、完美的语义 Chunk。

  3. 采用了“轻量级元数据召回”而非“全文向量化”
    最重要的一点是:当前系统存进 ChromaDB
    向量库的,只有论文的“标题”和“摘要”这层表皮。长达几十页的 PDF
    论文正文并没有被存进向量库里!

现在的架构是一种“粗排召回 + 大模型直接通读全文”的策略:
4. 向量召回(粗排):只用摘要去向量库搜,搜出最相关的 5 篇。
5. 通读全文(精排与生成):当用户真的需要这 5 篇生成实现路径时,系统是利用
pdf_service.py 把 PDF
下载下来,并一字不落地丢给大模型(DeepSeek)高达几万甚至十万 Token
的超长上下文窗口去硬读,而不是像传统 RAG 那样从 PDF 里切块抽段落。


如果想要升级系统,什么时候必须引入“切片 (Chunking)”?

目前的做法应对“找方向、找摘要”已经足够,但如果你想实现“深度的全文 RAG
检索”(例如:用户想搜“基于 ResNet
的某种特定损失函数的代码实现细节”,而这个细节只写在某篇论文的第 5 页第 2
段,摘要里完全没提),你就必须引入切片:

改进方案(引入 LangChain 切片器):
你需要把后台的爬虫逻辑改掉,在抓取时顺便下载全文,然后引入切片逻辑:

1 from langchain.text_splitter import RecursiveCharacterTextSplitter
2
3 # 假设通过 PDF 服务获取到了 1万字的全文
4 full_pdf_text = pdf_service.get_full_text(pdf_url)
5
6 # 设置分块器:每 500 字符一块,保留 50 字符的上下文重叠
7 text_splitter = RecursiveCharacterTextSplitter(
8     chunk_size=500,
9     chunk_overlap=50

10 )
11 chunks = text_splitter.split_text(full_pdf_text)
12
13 # 然后循环把每一块存入 ChromaDB,并记录它们属于哪篇论文的哪一页
14 for i, chunk in enumerate(chunks):
15 embedding = self.embed_text(chunk)
16 self.collection.add(
17 embeddings=[embedding],
18 ids=[f”{paper_id}chunk{i}”], # 细化 ID
19 metadatas=[{“arxiv_id”: paper_id, “text”: chunk}]
20 )
这样改造后,你的 ChromaDB
就从一个“目录索引柜”变成了一个真正的“海量知识片段库”了!