掌柜智库项目 Day 05|文档精细切割与 item_name 识别入库
在标题切割之后做 refine_chunks 精细切割,并梳理 item_name 识别节点的动机、bge-m3 与 Milvus 的准备工作以及节点的实现步骤。
本篇要点
refine_chunks精细切割:长块短切(带 overlap)、短块合并、parent_title/part补全- 为什么必须识别
item_name:用主体名缩小向量检索范围 - 向量回顾:稠密
dense(注意维度,推荐 1024)与稀疏sparse - 准备工作:bge-m3 的下载与两种加载方案、Milvus 部署与
MilvusGateway封装 item_name识别节点的作用、参数与六步实现流程
内容回顾
1、Day04:node_document_split 节点
- 先定下切块产物结构:
content(标题 + 内容)、title、file_title、parent_title、part,向量相关字段留到后面补 - 四大切割原则:语义优先、长度可控、边界清晰且有合理重叠、可追溯
- 项目切割参数:
CHUNK_MAX_SIZE = 1000、CHUNK_SIZE = 600、CHUNK_OVERLAP = 50、CHUNK_MIN = 400 - 节点实现走到两步:
load_markdown_content负责参数获取、非空校验与\r\n数据清洗,split_document用正则按#标题做语义切割,产出list[dict{title, content, file_title}]
今天内容
1、refine_chunks 精细切割
1.1 为什么还要精细切割
上一节的 split_document 只解决了一件事:按标题切,保证语义不跨域。但它是”跟着标题走”的,切出来的块长度完全不可控——有的标题下面挂了几百行,块远远超过 CHUNK_SIZE;有的标题只有一两行,切出来就是短碎片。
所以要在标题切割之后再补一步精细切割,目标是三件事:
- 长 → 短:过长的块按
CHUNK_SIZE再切一刀,并保留合理的 overlap,切割过程中不能破坏语义。 - 短 → 合并:同一标题下过短的碎片合并起来,让边界清晰、长度可用。
- 属性补全:把
parent_title、part补齐,保证切块可追溯。
调用方式很简单,入参就是上一步的 chunks:
1 | refine_chunks = refine_chunks(chunks) |
篇幅上沿用 Day04 定下的四个常量:CHUNK_SIZE = 600、CHUNK_MAX_SIZE = 1000、CHUNK_MIN = 400、CHUNK_OVERLAP = 50。
1.2 入参与出参
入参 chunks:[{title, content, file_title}]。其中 content 可能过长也可能过短,且此时还没有 parent_title、part。
出参 refine_chunks:精细切割后的新列表,字段含义如下。
| 字段 | 含义 |
|---|---|
title |
切片标题,被长切后会变成 title_子片段_1、title_子片段_2 这样的形式 |
content |
精细切割后的正文,切割时不能破坏语义 |
file_title |
文件名,原样携带 |
parent_title |
原来的 title,用于标识这些子片段属于同一个父标题 |
part |
二次切割产生的序号 1、2、3…,由”过长向短切割”生成 |
也就是说,输出的语义是:title = (title_子片段_1 or 2),content = 精细切割,parent_title = 原来的 title,part = 1 / 2 / ...。
1.3 整体步骤
- 定义一个存储最终结果的
list:refine_list = [dict]。 - 循环处理
chunks,拿到每个chunk的content,先判断长度。 content > 600的块触发长切,交给_split_long_chunk(chunk),返回list[dict](带上parent_title、part),用extend合并进结果集。content <= 600的块直接原样append进结果集。- 把同一个标题下过短的块合并(
< 400,合并后不超过1000),抽成方法_merge_short_chunks_same_parent_title(refine_list)。 - 补全属性:没有
parent_title的用title兜底,没有part的补 1。 return refine_list。
伪代码结构如下:
1 | # 1. 最终结果容器 |
1.4 长块短切:_split_long_chunk
- 判定条件:
content > CHUNK_SIZE(即 600),说明这个块太长了,需要”过长向短切割”。 - 切割方式:用递归切割器,按
CHUNK_SIZE = 600为基准长度切,同时保留CHUNK_OVERLAP = 50的重叠——重叠比例大约落在 8% ~ 12%,保证相邻块之间上下文连贯、语义不被切断。 - 切割产物:每个子片段都要写回
parent_title(原来的title)和part(1、2、3…),title变成title_子片段_x。
注意: 切长块时不能”死按字符数切”,必须在同一个语义边界内切;边界清晰、不跨语义,这是切割原则里排在第一位的。
1.5 短块合并:_merge_short_chunks_same_parent_title
- 判定条件:同一个
parent_title下、长度小于CHUNK_MIN = 400的块,判定为短碎片,需要尝试合并。 - 合并上限:合并之后不能超过
CHUNK_MAX_SIZE = 1000,避免为了消除碎片反而造出过大块、导致向量失真。 - 为什么要合并:碎块比长块更危险——检索召回时碎块语义不完整,会拉低召回率;把碎块并回 600 左右,才能兼顾”召回率”和”准确率”。
1.6 属性补全与结果备份
长切过的块已经有了 parent_title 和 part;没被长切、也没参与合并的块还是”裸的”,所以要统一补一遍:
- 没有
parent_title→ 用原来的title兜底,保证每个块都能追溯到父标题。 - 没有
part→ 补 1。
这样一个 chunks 里的块,无论是长切出来的还是原样保留的,结构都统一了,后续节点不用再判断字段是否存在。
今天的收尾里还提到要做数据备份:精细切割的结果会落成本地文件(后续节点读取的 文件名_new.json),既方便排查切割效果,也让下游节点可以直接从文件恢复 chunks。
1.7 两个待优化点
今日内容里明确记下了 refine_chunks 的两处遗留问题,属于”后面要回来看”的坑:
优化 1:
part断档的问题。 长切 + 短合并组合之后,part序号会出现1、3这种断档,编号不再连续。优化 2:短块合并的策略问题。 例如四段
300 300 300 300应该合并成900这种更大的块,而300应该优先和相邻块凑成600、600,合并粒度需要再调。
2、item_name 识别节点
2.1 为什么要识别 item_name
核心目的只有一句话:通过 item_name 缩小文档检索范围。
检索链路是:item_name -> 文档 -> 向量相似度搜索,先按主体名把候选范围框住,再做向量相似度搜索,结果才更加精准。
这里有两个必须记住的关系:
- 一个文件 → 一个
item_name。 - 一个
item_name→ 对应多个文件。
正因为一个 item_name 下会挂多个文件,所以:
上传向量数据的时候,要清空旧数据,并且要用
file_title做标识。 否则同一个文件重新导入时会留下重复的旧向量。
2.2 向量基础回顾:稠密与稀疏
- 向量:把文字转换成计算机可识别的高维数组。
- 通过向量可以计算相似度,这也是向量检索的基础。
- 向量分两类:
- 稠密向量
dense:表达语义,注意维度,推荐 1024。 - 稀疏向量
sparse:本质是dict(词 → 权重)。
- 稠密向量
2.3 准备工作一:下载 bge-m3
- 下载地址(魔搭直接下载):
https://www.modelscope.cn/models/BAAI/bge-m3/files - 也可以使用项目里的工具脚本下载:
shared / tool / download_bgem3.py(一定要明确下载位置,后面.env要按这个路径配)。
2.4 准备工作二:加载 bge-m3 的两种方案
加载的思路是:用客户端(py 文件和函数)加载权重文件,在代码里使用并生成向量。项目里对比了两种方案。
方案 1:官方提供的客户端(FlagEmbedding)
1 | pip install -U FlagEmbedding |
1 | from FlagEmbedding import BGEM3FlagModel |
方案 2:Milvus 提供的 bge-m3 加载工具
文档地址:https://milvus.io/docs/zh/embed-with-bgm-m3.md(2024 年两边互相道谢的那波联动)。
BGE-M3 因其在多语言、多功能和多粒度方面的能力而得名。BGE-M3 能够支持 100 多种语言,为多语言和跨语言检索任务树立了新的标杆。它在单一框架内执行密集检索、多向量检索和稀疏检索的独特能力,使其成为广泛的信息检索(IR)应用的理想选择。
1 | from pymilvus.model.hybrid import BGEM3EmbeddingFunction |
项目最终走的是 Milvus 的这套 BGEM3EmbeddingFunction,因为后面入库、检索都在 Milvus 里,向量生成标准和向量库保持一致最省事。
2.5 封装到 shared / embedding_utils 与 infra
(1)位置:shared / embedding_utils
(2)配置:
1 | BGE_M3_PATH=D:\ai_models\modelscope_cache\models\BAAI\bge-m3 |
注意: CPU 上要写
BGE_FP16=0,因为 FP16 半精度加速是 GPU 的默认玩法,CPU 上用的是 FP32 单精度。
如果要使用 GPU 怎么办?
1 | BGE_DEVICE=cuda:0 |
注意: 改成 GPU 之后,相关的依赖也要换成 GPU 版本(详见教案)。
(3)对外方法:
1 | def get_bge_m3_ef() # 获取嵌入式模型(不支持直接使用) |
(4)再封装到 infra / llm / providers:
1 | # 获取嵌入式模型 |
2.6 准备工作三:部署 Milvus
部署用的是官方 standalone 的 docker compose 文件:
1 | wget https://github.com/milvus-io/milvus/releases/download/v2.4.11/milvus-standalone-docker-compose.yml -O docker-compose.yml |
- 可视化工具:
attu。 - 连接地址(笔记里记的是):
39.105.7.90:19530。
2.7 Milvus 客户端与 MilvusGateway 封装
(1)Milvus 加载工具
- 工具位置:
shared / clients / milvus_utils.py - 对外函数:
get_milvus_client() -> milvus_client - 配置:
.env(url 交给get_milvus_client()去读,另一个集合的名字写在代码里)
(2)统一封装到 infra
封装位置:/infra/vectorstore/milvus_gateway.py
1 | from app.infra.config.providers import infra_config |
这样节点里只要用 milvus_gateway.chunk_collection_name / milvus_gateway.item_name_collection_name / milvus_gateway.milvus_client,不用再关心配置从哪来。
整条准备链路串起来就是:item_name -> 向量 -> 嵌入式模型 bge3 -> 加载方式(官方 / milvus) -> milvus 向量生成、归一化 -> 准备嵌入式模型 -> milvus 准备。
2.8 节点实现分析
作用: 识别 item_name,即 chunks -> item_name,以及 item_name -> milvus。
参数:
- 使用:
chunks/file_title/md_path(文件名_new.json) - 生成:
item_name,以及带item_name字段的chunks
步骤:
- 获取并校验参数(state)->
chunks/file_title
1.1 获取参数file_title、chunks、md_path;
1.2 非空校验:file_title为空走md_path兜底,chunks为空则从md_path / json读取;
1.3 返回结果。 - 根据 chunks 内容识别 item_name(
chunks,file_title兜底)->item_name
2.1 获取语言模型对象chat();
2.2 加载提示词字符串(.prompt);
2.3 把提示词封装成Message;
2.4 封装 LangChain 调用链(chains);
2.5 执行调用链,拿到item_name;
2.6 非空校验:识别结果为空就用file_title兜底;
2.7 返回item_name。 - 前置准备工作(
item_name,chunks):把item_name补进chunks。 - 如果没有 collection,就创建
item_name对应的集合(prepared_milvus_item_name_collection(None) -> None):
4.1 判断有没有对应的item_name集合;
4.2 有的话打印日志,直接return跳出方法;
4.3 没有则创建集合。 - 删除或者保存数据(
item_name,file_title)(delete_and_insert_item_name(item_name, file_title)):
5.1 给item_name生成稠密和稀疏向量,补全插入数据需要的属性(含 PK);
5.2 插入之前,先删除该文件关联的旧数据(file_title == file_title);
5.3 完成本次数据插入。 - 更新 state:
chunks|item_name。
今日内容小结: refine_chunks 精细切割(递归切割器、短合并问题)、数据备份、嵌入式模型准备与 Milvus 准备工作、参数校验和 item_name 识别。
