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 有调整,动手前以官方文档为准。
三种切法摆在一起看,差别不在代码量,在检索精度:
结论:先按文档结构切,再用 token 上限兜底。顺序反了,你就得靠模型去猜哪半句话才是重点。
spring-ai-tika-document-reader | |
spring-ai-starter-vector-store-pgvector | |
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。
它不会替你保留章节结构。章节得你先自己切,再交给它兜底。
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 是后面做过滤、做灰度重灌的依据。
这三个字段现在不加,等出事那天你会想重灌一遍全库。
这张表是起点,不是答案。真正的验收方式是拿三十个真实问题跑一遍,看命中的块是不是"人能看懂的一整段"。
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,都是在给一个漏水的桶加水。
验证方式也很朴素:随便问十个业务问题,把检索到的原文打印出来自己读一遍。读着别扭的,模型答出来一定也别扭。
你现在这个库,一个块大概多少字?留言说说,我看看多半是切大了还是切小了。















































































