夜雨聆风学习资料网

ARTICLE · 1298081

把公司文档灌进向量库:切分比模型选型更决定成败

把公司文档灌进向量库:切分比模型选型更决定成败

最常见的上线状态是这样的:八百份制度文档灌进向量库,问"报销标准是多少",模型答的还是自己编的数。

把检索命中的五段原文打出来看:三段是同一份文档的相邻内容,一段只有半句话,还有一段是目录页。

问题不在模型,也不在 embedding 选型——在切分。

结论先说:切分决定检索的上限

RAG(Retrieval-Augmented Generation,先检索再让模型作答)里,模型只负责读你给的那几段。

你给的段落本身是碎的,再好的模型也只能基于碎片编。

所以顺序是:先把文档切对、把 metadata 带上、把增量更新做出来,最后才去调模型和 topK。

版本前提:Spring AI 1.0.x + Spring Boot 3.3 + JDK 17 + PostgreSQL 16 与 pgvector 扩展。1.0 起 starter 改过名,initialize-schema 也从默认开启改成默认关闭;2.x 的 API 有调整,动手前以官方文档为准。

为什么不把整篇文档直接塞进去

三种切法摆在一起看,差别不在代码量,在检索精度:

切法
检索精度
上下文
什么时候用
整篇不切
低,大部分内容与问题无关
完整
单条 FAQ、短公告
定长硬切
中,句子常被切断
差
结构混乱的纯文本
按结构切加定长兜底
高
好
有标题、条款、问答对

结论:先按文档结构切,再用 token 上限兜底。顺序反了,你就得靠模型去猜哪半句话才是重点。

四步灌进去
坐标先对齐
用途
Maven 坐标
读 docx / pdf / html
spring-ai-tika-document-reader
PGvector 向量库
spring-ai-starter-vector-store-pgvector
PostgreSQL 驱动
org.postgresql:postgresql

前两个的 groupId 都是 org.springframework.ai,版本交给 BOM 管,别自己写死。

配置

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/kb
  ai:
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536
        initialize-schema: true

dimensions 必须等于 embedding 模型的输出维度。1536 是 OpenAI text-embedding-3-small 的,换成 bge-m3 是 1024。

写错的后果很隐蔽:不报错,只是一直检索不到东西。维度在建表那一刻就定死了,改维度得重建表。

initialize-schema: true 只给本地和第一次部署用。上线后关掉,让 DBA 走 DDL 变更,别让应用每次启动都去动表结构。

读和切

var res = new FileSystemResource(path);
var reader = new TikaDocumentReader(res);
List<Document> raw = reader.read();

TokenTextSplitter splitter = TokenTextSplitter.builder()
    .withChunkSize(600)
    .withMinChunkSizeChars(200)
    .withMinChunkLengthToEmbed(20)
    .withKeepSeparator(true)
    .build();

List<Document> chunks = splitter.apply(raw);

默认 chunkSize 是 800,我调到 600。中文一个字常常要一到两个 token,600 token 落到正文上大约半页,正好是一个小节。

但它的边界要说清楚:TokenTextSplitter 是按 token 数切,再在 minChunkSizeChars 之后找标点收尾,默认分词表是 CL100K_BASE。

它不会替你保留章节结构。章节得你先自己切,再交给它兜底。

带上 metadata 再写

List<Document> tagged = chunks.stream()
    .map(c -> c.mutate()
        .metadata("docId", docId)
        .metadata("dept", dept)
        .metadata("version", version)
        .build())
    .toList();

vectorStore.add(tagged);

docId 是逻辑主键,dept 和 version 是后面做过滤、做灰度重灌的依据。

这三个字段现在不加,等出事那天你会想重灌一遍全库。

按文档类型分档的切分表
文档类型
怎么切
目标块大小
必带 metadata
制度、手册(Markdown)
按二级标题切,一节一块
400 到 600 token
章节路径
合同、标书(PDF)
按页加条款切,条款不跨块
300 到 500 token
页码、合同号
FAQ、工单
一问一答一块,不切
100 到 300 token
产品线、工单类型
接口文档
一个接口一块,签名与示例同块
400 到 800 token
模块、版本号
会议纪要
按议题切
300 到 500 token
日期、部门

这张表是起点,不是答案。真正的验收方式是拿三十个真实问题跑一遍,看命中的块是不是"人能看懂的一整段"。

上线前必须处理的三个坑

1.metadata 只能放简单类型。Document.getMetadata() 的值限定 string、int、float、boolean,塞嵌套对象进去,多数向量库会在写入时抛错或静默丢字段。时间统一用 epoch day 或 ISO 字符串。

2.增量更新要"先删后加"。文档改了就按 docId 删掉旧块,再写新块:

var expr = new FilterExpressionBuilder()
    .eq("docId", docId)
    .build();
vectorStore.delete(expr);
vectorStore.add(tagged);

别拿内容 hash 当主键:改一个错别字,整篇 id 全变,旧块永远留在库里,检索时两版一起返回。docId 记得建索引,删除是按过滤条件扫表的。

3.灌库别放在 HTTP 请求里。一次全量几千个块,embedding 是外部调用,几十秒甚至几分钟,还随时可能被限流。走定时任务或消息队列,分批写;max-document-batch-size 默认一万,它兜的是单批大小,不兜限流。

还有一个容易忽略的:pgvector 的 HNSW 索引最多支持 2000 维。换到 3072 维的 embedding 模型,建索引会失败,只能退回精确检索或者先降维。

最后

灌库的代码量比想象中小,四个类、二十行。真正决定成败的是两个决定:块怎么切,metadata 带什么。

这两个决定做错了,后面调 topK、上重排、换更贵的 embedding,都是在给一个漏水的桶加水。

验证方式也很朴素:随便问十个业务问题,把检索到的原文打印出来自己读一遍。读着别扭的,模型答出来一定也别扭。

你现在这个库,一个块大概多少字?留言说说,我看看多半是切大了还是切小了。

— 完 —

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料