Spring AI + pgvector 实战:把企业文档变成 AI 的记忆
一、为什么企业需要 AI 知识库?——从"年假怎么算"说起
“年假怎么算?”
“报销流程是什么?”
“项目立项需要谁审批?”
如果你的团队成员每天在群里问这些问题,你已经迫切需要一套企业知识库了。Workforce Hub 作为一个面向中小企业的协同办公平台,知识库模块是我们的核心差异化功能——员工在聊天框里 @ 一下 AI,它就能从公司制度文档里找到答案。
说实话,刚开始做这个功能的时候我觉得挺简单的:不就是把文档丢给大模型嘛。结果踩了整整两周的坑才发现,从文档上传到真正"问对问题拿对答案",中间隔着好几座山。
二、技术选型——为什么不用 Elasticsearch ?
RAG(检索增强生成)的核心链路很简单:
文档 → 分段 → Embedding → 向量存储 → 相似度检索 → LLM 生成回答
但每个环节都有不同的技术方案。为了做出最合适的选择,我对比了几种方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Spring AI + pgvector | 零额外依赖,数据不离开 PG | 大规模检索性能不如 ES | 中小企业,文档量 < 10万 |
| Spring AI + Elasticsearch | 高性能全文+向量混合搜索 | 需要额外部署 ES 集群 | 海量文档、高并发 |
| LangChain + Pinecone | 生态成熟,社区活跃 | 引入 Python 技术栈,增加运维成本 | Python 项目 |
| 纯 Spring AI + OpenAI | 最简单 | 每次都传全文,Token 爆炸 | Demo 阶段 |
我们选 Spring AI + pgvector 的原因是:
1. 零额外部署成本。 Workforce Hub 本身就用了 PostgreSQL,pgvector 是 PG 的一个扩展,一行 SQL 就能开启。中小企业没有专职运维,多一个 ES 集群就是多一个故障点。
2. Spring AI 和 Spring Boot 3.x 无缝集成。 不用切 Python,不用学新框架,直接在已有的 Service 层注入 AI 组件就能用。
3. 数据安全。 企业文档不出数据库,不用把内部制度上传到第三方向量服务。
说实话:其实选 pgvector 还有一个很现实的原因——我们服务器只有 2 核 4G,跑 ES 内存直接爆。但事后证明这个"穷人的选择"反而是对的,中小企业根本不需要 ES 那么重的东西。
三、RAG 实现——四个核心环节的踩坑记录
3.1 文档分段:最容易翻车的环节 ⭐
我的第一个版本是直接按固定长度切分(每段 512 字符),结果测试的时候翻车了:
// 我一开始这么写——简单粗暴按字符数切
List<String> chunks = new ArrayList<>();
for (int i = 0; i < text.length(); i += 512) {
chunks.add(text.substring(i, Math.min(i + 512, text.length())));
}
测试文档是一份《员工考勤制度》,里面有一条规则是"年假天数 = 基础天数 + 司龄补贴 - 已休天数"。但由于 512 字符刚好把这条规则切成了两段——
段1:年假天数 = 基础天数 + 司龄补贴
段2:- 已休天数。计算公式详见附表。
AI 查的时候只检索到了段 1,回答"年假天数就是基础天数加司龄补贴",漏掉了"减去已休天数"。这要是在生产环境,HR 看了得气死。
正确做法:按语义边界分段,而不是按字符数。Spring AI 提供了 TokenTextSplitter 和 DocumentReader 组合:
// 改进后的分段策略
var splitter = new TokenTextSplitter(
512, // 目标长度
128, // 重叠长度——关键!防止语义断裂
5, // 最小长度
1000, // 最大长度
true // 按句子边界切分
);
List<Document> chunks = splitter.split(document);
我踩的坑:重叠长度设太小(之前设了 20),结果还是会出现关键信息被切开的情况。后来把重叠调到 128 才稳。
3.2 Embedding:中文模型的坑
vector 的维度直接影响检索精度。我们用 BAAI/bge-m3 模型(1024 维),通过 SiliconFlow API 调用:
@Bean
public EmbeddingModel embeddingModel() {
return new SiliconFlowEmbeddingModel(
"BAAI/bge-m3", // 支持中英文,1024 维
apiKey
);
}
一个小坑:pgvector 建表时向量维度需要和 embedding 模型输出维度一致。我一开始从 1536 维(OpenAI ada-002)切换到 1024 维(BGE-M3)时忘了改表结构,插入一直报错:expected 1536 dimensions, got 1024。排查了半小时才发现是 ALTER TABLE 没执行。
-- pgvector 建表
CREATE TABLE knowledge_chunks (
id SERIAL PRIMARY KEY,
doc_id VARCHAR(64),
chunk_text TEXT,
chunk_index INT,
embedding vector(1024), -- 必须和模型输出维度一致
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 创建 HNSW 索引加速检索
CREATE INDEX ON knowledge_chunks
USING hnsw (embedding vector_cosine_ops);
3.3 相似度检索:余弦距离 vs 欧氏距离
向量检索默认用余弦相似度(cosine similarity),这在高维稀疏向量中最有效。Spring AI 的 pgvector store 默认就用的 cosine。
实际查询代码:
@Service
public class KnowledgeService {
@Autowired
private VectorStore vectorStore;
@Autowired
private ChatClient chatClient;
public String ask(String question) {
// Step 1: 向量检索 TOP 5 相关文档段
List<Document> relevantDocs = vectorStore.similaritySearch(
SearchRequest.query(question)
.withTopK(5) // 取 Top 5
.withSimilarityThreshold(0.7) // 相似度低于 0.7 的不算
);
// Step 2: 拼接上下文 + 提示词
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
String prompt = """
你是一个企业知识库助手。请根据以下文档内容回答问题。
如果文档中没有相关信息,请明确说"暂无相关制度记录"。
文档内容:
%s
问题:%s
""".formatted(context, question);
// Step 3: 调用 LLM 生成回答
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
血泪教训:similarityThreshold(0.7) 这个参数非常重要。一开始我没设阈值,结果用户问"今天天气怎么样",AI 强行从不相关的考勤文档里拼凑了一个回答:“根据考勤制度,天气不影响出勤”。设了阈值后,不相关的问题会直接返回"暂无相关制度记录"——这才是企业场景下该有的行为。
3.4 提示词工程:让 AI 说人话
有了上下文和问题还不够,提示词直接决定了回答质量。我调试了十几个版本,最终确定的有效提示词套路:
系统指令 + 角色设定:
"你是一个企业知识库助手,请严格基于文档内容回答"
约束条件:
"如果文档中没有相关信息,请明确说'暂无相关制度记录'"
"回答要简洁,不超过 200 字"
"涉及具体数字(天数、金额)时,必须引用原文"
格式要求:
"使用 Markdown 格式,关键信息加粗"
小技巧:给提示词里加一个"负面示例"比正面示例更有效。我告诉模型"不要像这样回答——‘根据相关规定,您需要…’"——这种官腔回答比说错信息还让人反感。
四、架构设计——知识库在 Workforce Hub 中的位置
整个知识库模块在系统架构中扮演"AI 大脑"的角色:
📌 图片位置标记:请在此处插入 RAG 架构图(
wh-rag-diagram.png)
图片路径:上传至媒体库后替换此处
内容:左侧"管理员上传文档" → “文档分段” → “Embedding 生成” → “向量存储(pgvector)”;右侧"员工提问" → “相似度检索” → “LLM 生成” → “返回回答”
核心交互流程:
- 文档入库:管理员上传 PDF/Word → Spring AI
DocumentReader解析 →TokenTextSplitter分段 →EmbeddingModel生成向量 → 存入 pgvector - 问答检索:员工 @AI 提问 → 问题转 embedding → pgvector 相似度检索 Top K → 拼接上下文 → LLM 生成回答
- 反馈闭环:员工对回答点 👍/👎 → 记录到数据库 → 后续用于优化检索参数
五、效果与改进
部署后的实际效果:
✅ 做得好的:
- 制度类问题准确率 90%+(年假、报销、考勤规则等有明确文档的问题)
- 响应速度 < 2 秒(512MB 文档库,HNSW 索引)
- 零外部依赖,运维成本极低
❌ 还需要改进的:
- 纯经验类的知识捕捉不到(比如"XX 客户喜欢什么样的提案风格")
- 多轮对话支持不够(问完年假,接着问"那我能休几天"时,AI 不知道"我"是谁)
下一步计划:
- 引入 rerank 模型(BGE-reranker)提升检索精度
- 支持图片和多模态文档
- 自动从聊天记录中提取知识条目
六、总结
核心收获:
- 分段策略比模型选择更重要。 好的分段(按语义、加重叠)对最终效果的影响远大于换一个大模型
- pgvector 对中小企业完全够用。 不需要 ES,不需要 Pinecone,PG 一个扩展就能搞定
- 提示词是最后一道防线。 阈值过滤 + 强制来源引用 + 明确的"不知道"话术,这三条比任何技术优化都管用
给初学者的建议:不要一上来就搞复杂的 Rag Pipeline。先用 Spring AI + pgvector 跑通最简单的"上传→检索→回答"链路,把分段策略和提示词调好再往上加东西。
个人感悟:做企业软件和做个人项目最大的区别是——个人项目你追求技术酷炫,企业软件你追求稳定可靠。pgvector 不酷,但它不崩。
环境信息:Java 17 + Spring Boot 3.x + Spring AI 1.0 + pgvector 0.7 + BAAI/bge-m3 + SiliconFlow API
发布日期:2026年5月21日
默认评论
Halo系统提供的评论