RAG 不是一个"搭起来就能用"的系统。从文档的清洗、切片、向量化,到检索时的重排序、查询增强,每一个环节都决定了最终回答的质量。本文将带你构建基于 PGvector 的生产级 RAG 知识库。
为什么选择 PGvector?
PGvector 是 PostgreSQL 的向量扩展,核心优势:向量和业务数据在同一张表——不需要单独维护向量数据库集群;可利用 PostgreSQL 的行级安全、事务、备份机制;支持混合检索(向量相似度 + SQL 条件过滤)。百万级以上纯向量检索场景,Milvus 和 Qdrant 更高效。
文档 ETL Pipeline
// Step 1: 文档加载
DocumentLoader loader = DocumentLoader.create()
.registerSource(new FileSystemSource("docs/", "*.md"))
.registerSource(new DatabaseSource(dataSource, "SELECT id, content FROM kb"));
// Step 2: 文档清洗
DocumentCleaner cleaner = DocumentCleaner.create()
.addFilter(new RemoveEmptyLinesFilter())
.addFilter(new RemoveHTMLTagsFilter())
.addFilter(new MarkdownCodeBlockPreserver());
// Step 3: 智能切片——按语义边界而非字符数
SemanticChunker chunker = SemanticChunker.builder()
.embeddingModel(embeddingModel)
.maxChunkSize(1000)
.minChunkSize(200)
.overlapSize(100)
.breakpointThreshold(0.7) // 相似度低于此值才切分
.build();
// Step 4: 元数据增强
chunk.setMetadata(Map.of(
"source", "docs/java-virtual-threads.md",
"section", "## 性能对比",
"lastUpdated", "2025-10-20"
));PGvector 表结构与索引
CREATE EXTENSION vector;
CREATE TABLE document_chunks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
embedding VECTOR(1536), -- OpenAI ada-002: 1536 维
source VARCHAR(500),
chunk_index INTEGER,
metadata JSONB,
created_at TIMESTAMP DEFAULT now()
);
-- HNSW 索引(写入和查询都比 IVFFlat 快)
CREATE INDEX ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- 混合查询:语义 + 关键词 + 元数据过滤
SELECT content,
1 - (embedding <=> query_embedding) AS similarity,
ts_rank(to_tsvector('chinese', content),
plainto_tsquery('chinese', '虚拟线程 性能')) AS keyword_score
FROM document_chunks
WHERE metadata->>'source' LIKE '%java%'
AND 1 - (embedding <=> query_embedding) > 0.75
ORDER BY similarity * 0.7 + COALESCE(keyword_score, 0) * 0.3 DESC
LIMIT 5;查询增强(Query Enhancement)
// 1. 查询改写(Query Rewriting)
// 用户:"那个线程的东西怎么用?"
// 改写:"Java 虚拟线程 VirtualThread 使用方法"
// 2. 查询扩展(Query Expansion)
// "Spring Cloud" → ["Spring Cloud", "微服务", "Nacos", "Gateway"]
// 3. HyDE(Hypothetical Document Embedding)
// 先让 LLM 生成假设的完美答案,用答案做向量检索
String hypothetical = llm.generate("请假设性回答:Java 虚拟线程的核心优势?");
List<Document> results = vectorStore.search(hypothetical);检索后处理:ReRank
// 向量检索返回 Top-20,用 ReRanker 精选 Top-5
List<Document> candidates = vectorStore.search(query, 20);
Reranker reranker = new BgeReranker();
List<Document> reranked = reranker.rerank(query, candidates, 5);
// Cross-Encoder 比 Bi-Encoder(向量相似度)更精确Prompt 工程进阶
结构化 Prompt 模板
String prompt = """
# 角色定义
你是一个专业的技术文档助手,擅长回答 Java 和 Spring 相关问题。
# 安全规则
- 如果参考资料中没有相关信息,请明确说明
- 不要编造任何不存在的 API、参数或版本号
# 参考资料(按相关性排序)
{context}
# 输出格式
1. 先给出简洁的答案(1-3句话)
2. 然后提供代码示例(如果有)
3. 最后列出注意事项
# 用户问题
{query}
""";CoT(Chain of Thought)
String cotPrompt = """
请逐步思考:
1. 这个问题涉及哪些技术概念?
2. 这些概念之间的关联是什么?
3. 最佳实践是什么?
4. 有没有常见的坑?
请在每个步骤后说明推理过程,然后给出最终答案。
""";Few-Shot Prompting
"""
示例1:
Q: Spring Boot 怎么连接 MySQL?
A: [配置] application.yml 中添加 datasource... [代码] @Autowired DataSource... [注意] driver-class-name 用 com.mysql.cj.jdbc.Driver
示例2:
Q: 什么是 AOP?
A: [定义] AOP 面向切面编程... [原理] 动态代理... [使用] @Aspect + @Around...
现在回答:{query}
""";总结
RAG 的质量由ETL Pipeline 质量 + 检索策略精度 + Prompt 设计水准共同决定。一个"能用"的 RAG 只需 100 行代码,一个"好用"的 RAG 需要对每个环节精细调优。Prompt 工程是最后一道关卡——Context 再好,Prompt 不好,回答也会跑偏。
评论 (0)