从零搭建一个本地文档智能问答系统:Spring AI + PGvector + DeepSeek 实战
·
从零搭建一个本地文档智能问答系统:Spring AI + PGvector + DeepSeek 实战
前言
不知道你有没有这样的困扰:电脑里的文档越来越多,想找某段话却只能靠“搜索文件名 + 肉眼扫描”。有没有办法像 ChatGPT 一样,直接问“帮我找找上个月项目总结里关于预算的部分”?
这篇文章将带你从零搭建一个本地文档智能问答系统。你只需要上传 PDF,就可以像跟人聊天一样提问,AI 会基于文档内容给你准确的回答。
最终效果:
curl -X POST -F "file=@D:/年终总结.pdf" http://localhost:8080/rag/upload
curl "http://localhost:8080/rag/ask?question=明年的主要目标是什么"
# 输出:根据年终总结文档,明年的主要目标是...
技术选型
| 组件 | 选型 | 理由 |
|---|---|---|
| 后端框架 | Spring Boot 3.5 + Spring AI 1.0 | Java 开发者友好,约定大于配置 |
| 向量数据库 | PGvector | 基于 PostgreSQL,一条 Docker 命令搞定,无兼容性问题 |
| 大模型 | DeepSeek | 国内可用,价格便宜,中文效果好 |
| Embedding | ONNX 本地模型 | 免费、离线、数据隐私安全 |
踩坑经历:为什么不用 Chroma?
一开始我选择了 Chroma,但遇到了各种问题:
- 版本不兼容(v1/v2 API 混乱)
- Windows 下编译失败
- 与 Spring AI 集成需要大量手动配置
最终切换到 PGvector,一条 Docker 命令 + 几行配置就搞定了。结论:如果是 Java 开发者,选 PGvector 比 Chroma 省心太多。
第一步:启动 PGvector
PGvector 是 PostgreSQL 的向量扩展,用 Docker 一键启动:
docker run -d \
--name postgres-pgvector \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=vectordb \
-e POSTGRES_HOST_AUTH_METHOD=trust \
-p 5432:5432 \
pgvector/pgvector:pg16
启动后验证:
docker exec -it postgres-pgvector psql -U postgres -d vectordb -c "SELECT 1"
第二步:创建 Spring Boot 项目
访问 start.spring.io,选择:
- Spring Boot 3.5.14
- Java 17
- 依赖:Spring Web
pom.xml 核心依赖:
<!-- Spring AI 版本 -->
<properties>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<!-- 仓库配置 -->
<repositories>
<repository>
<id>spring-milestones</id>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<!-- 核心依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
</dependencies>
第三步:配置文件
application.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/vectordb
username: postgres
password: postgres
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-v4-flash
temperature: 0.7
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 384
initialize-schema: true
第四步:核心代码
DocumentService.java
@Service
public class DocumentService {
private final VectorStore vectorStore;
private final TokenTextSplitter textSplitter;
public DocumentService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
this.textSplitter = TokenTextSplitter.builder()
.withChunkSize(500)
.withMinChunkSizeChars(50)
.build();
}
public String uploadAndIndexPdf(MultipartFile file) throws IOException {
Path tempFile = Files.createTempFile("upload_", ".pdf");
file.transferTo(tempFile.toFile());
try {
PagePdfDocumentReader reader = new PagePdfDocumentReader(tempFile.toString());
List<Document> chunks = textSplitter.apply(reader.get());
vectorStore.add(chunks);
return String.format("成功索引 %d 个片段", chunks.size());
} finally {
Files.deleteIfExists(tempFile);
}
}
public List<Document> search(String query, int topK) {
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(topK)
.build();
return vectorStore.similaritySearch(request);
}
}
RagController.java
@RestController
@RequestMapping("/rag")
public class RagController {
private final ChatClient chatClient;
private final DocumentService documentService;
public RagController(ChatClient.Builder builder, DocumentService documentService) {
this.chatClient = builder.build();
this.documentService = documentService;
}
@PostMapping("/upload")
public Map<String, Object> upload(@RequestParam("file") MultipartFile file) {
try {
return Map.of("success", true, "message", documentService.uploadAndIndexPdf(file));
} catch (Exception e) {
return Map.of("success", false, "message", e.getMessage());
}
}
@GetMapping("/ask")
public Map<String, Object> ask(@RequestParam String question) {
List<Document> docs = documentService.search(question, 3);
if (docs.isEmpty()) {
return Map.of("answer", "未找到相关内容");
}
String context = docs.stream().map(Document::getText).collect(Collectors.joining("\n\n"));
String prompt = String.format("基于以下内容回答问题:\n\n%s\n\n问题:%s", context, question);
return Map.of("answer", chatClient.prompt().user(prompt).call().content());
}
}
第五步:测试
# 上传文档
curl -X POST -F "file=@D:/test.pdf" http://localhost:8080/rag/upload
# 提问
curl "http://localhost:8080/rag/ask?question=这个文档主要讲了什么"
常见问题
| 问题 | 解决方案 |
|---|---|
expected 1536 dimensions, not 384 |
修改 dimensions: 384,删除旧表重建 |
password authentication failed |
添加 POSTGRES_HOST_AUTH_METHOD=trust 环境变量 |
| 依赖下载失败 | 配置 Spring Milestones 仓库 |
总结
通过本文,你完成了:
- ✅ 一个完整的 RAG 智能问答系统
- ✅ 从 0 到 1 的实战经验
下一步优化方向:
- 支持更多文件类型(Word、Excel、TXT)
- 添加文件监控,自动索引新文档
- 实现流式输出,优化用户体验
项目源码:[https://gitee.com/wang-qingmin/rag-ai-assistant.git]
欢迎 Star ⭐️ 和 Fork!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)