ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AI网盘实现:基于RAG与LangChain4j的文档问答系统

AI网盘实现:基于RAG与LangChain4j的文档问答系统 如果把普通网盘比作一个仓库那 CloudVault 想做的就是在仓库里配一位“随叫随到的图书管理员”。你不需要记住文件叫什么名字只需要用自然语言提问就能从自己上传的文档里拿到答案。本文将完整拆解 CloudVault 的核心实现覆盖 LangChain4j RAG 文档问答、PostgreSQL pgvector 向量检索、Redis 缓存与实时通知三大部分包含可直接复制的代码、配置和踩坑记录。文章适合具备 Spring Boot 基础的后端开发者也适合正在调研 RAG 工程化落地的同学。如果你想知道“RAG 知识库到底怎么和业务系统集成”“pgvector 怎么装、怎么建索引、怎么写相似度查询”“Redis 在网盘系统里除了做缓存还能做什么”那这篇内容能给你一份相对完整的参考答案。1. 背景与核心概念1.1 为什么做“AI 网盘”而不是普通网盘传统网盘解决的是“文件存储与下载”问题核心能力是上传、下载、分享、权限管理。但一个真实业务里文件被存进去之后真正高频的场景其实是“找内容”。比如产品经理上传了一份 20 页的需求文档第二天想确认“v2.3 版本中登录页的按钮文案是什么”他需要下载文档、翻页、搜索关键词效率很低。更麻烦的是如果资料分散在几十个文档里靠人力整理会非常痛苦。CloudVault 的思路是在网盘文件存储之上叠加一层 AI 认知能力。文件上传后自动进行解析、切片、向量化用户直接用自然语言提问系统从向量库中召回相关片段再交给大模型生成答案。这就是 RAGRetrieval-Augmented Generation检索增强生成的典型应用。1.2 RAG 在网盘场景中的价值RAG 的核心思想是“先检索再生成”。它不会让大模型凭空回答而是先从私有文档库中检索出和问题最相关的内容片段把这些片段拼进提示词再让大模型基于这些内容生成答案。这样做有三个非常明显的好处数据私密可控。文档不需要上传给外部模型训练你可以使用本地部署的 Embedding 模型和对话模型完成私有化知识库建设。答案有据可查。通过向量检索拿到的片段可以展示给用户回答的每一步都能溯源。知识实时更新。只要往知识库里新增文档问答能力立刻覆盖新知识不需要重新训练模型。在 CloudVault 的语境里RAG 解决了“网盘文件太多、用户找不到内容”的核心痛点。1.3 核心术语Embedding、向量数据库、pgvector、Redis这几个概念在后文会反复出现先统一说明Embedding向量化把一段文本转换成一串浮点数数组。意思相近的文本向量在高维空间里的距离更近。向量数据库专门存储和检索向量的数据库支持余弦相似度、欧氏距离等最近邻算法。pgvectorPostgreSQL 的扩展插件。它让原生的 PostgreSQL 具备了向量存储和相似度检索能力不用额外引入独立的向量数据库组件。Redis开源的内存键值存储系统。在 CloudVault 里承担缓存、分布式锁、Pub/Sub 消息通知多重职责。2. 整体架构与模块设计2.1 系统模块拆分CloudVault 按业务边界分为五个模块模块职责关键技术点文件模块上传、下载、秒传、分片、元数据管理Spring Boot、Redis 缓存、分布式锁AI 问答模块文档解析、切片、向量化、检索、生成回答LangChain4j、pgvector、Ollama实时通知模块上传/索引状态向前端推送Redis Pub/Sub、WebSocket用户与权限模块登录鉴权、文件归属校验Spring Security、JWT存储模块文件对象存储本地磁盘或 MinIOMinIO / 本地文件系统在实际交付中文件存储可以直接用本地磁盘也可以替换成 MinIO、阿里云 OSS 等对象存储服务。重点是文件存储方案与 AI 索引流程解耦。2.2 技术选型对照表组件选型作用开发语言Java 17Spring Boot 3.x 的基础要求主框架Spring Boot 3.xWeb API、依赖注入、异步任务AI 编排框架LangChain4j统一封装文档加载、切片、Embedding、模型调用向量数据库PostgreSQL 16 pgvector存储文档切片向量支持相似度检索缓存与消息Redis 7.x文件元数据缓存、分布式锁、Pub/Sub 通知Embedding 模型Ollama 本地部署的 qwen 系列文本向量化对话模型Ollama 本地部署的 qwen2.5基于检索结果生成答案构建工具Maven依赖管理2.3 核心数据流从文件上传到 AI 问答整个系统的核心流程可以拆成两条链路。链路一文件上传与 AI 索引文件上传成功后判断文件类型是否支持解析TXT、PDF、Word、Markdown 等。如果支持系统从文件中抽取文本按固定长度切片生成向量写入 pgvector 表。这一步建议采用异步处理避免阻塞上传接口。链路二文档问答用户输入问题系统对问题做向量化从 pgvector 中召回最相似的 TopK 文档片段拼装提示词调用大模型生成答案。最终把答案和引用片段一起返回给前端。3. 环境准备与版本说明3.1 环境清单版本需要根据你的实际环境调整本文以常见组合为例组件版本建议说明JDK17Spring Boot 3.x 必须使用 JDK 17 及以上Spring Boot3.2.x本文示例基于 Spring Boot 3LangChain4j0.35.x版本更新较快以官网最新稳定版为准PostgreSQL16建议使用官方或 pgvector 镜像pgvector0.7.x支持 HNSW 索引Redis7.x推荐 7.0 以上版本Ollama最新版本地运行 embedding 与聊天模型执行下面的命令确认基础环境java -version mvn -version docker --version3.2 项目目录结构为了方便后文阅读这里先给出一个最小项目结构cloudvault/ ├── pom.xml ├── docker-compose.yml ├── src/main/java/com/cloudvault/ │ ├── CloudVaultApplication.java │ ├── file/ │ │ ├── controller/FileController.java │ │ ├── service/FileStorageService.java │ │ └── service/FileMetaService.java │ ├── ai/ │ │ ├── controller/ChatController.java │ │ ├── service/KnowledgeBaseService.java │ │ ├── service/RagChatService.java │ │ └── repository/VectorSearchRepository.java │ └── notify/ │ ├── config/RedisPubSubConfig.java │ ├── config/WebSocketConfig.java │ ├── handler/RedisNotifyHandler.java │ └── service/NotifyService.java └── src/main/resources/ └── application.yml后文代码均按此目录结构组织。4. PostgreSQL pgvector 环境搭建pgvector 是 RAG 链路中的关键基础设施。下面分别介绍 Docker Compose 一键部署和 Windows 本地安装两种方式。4.1 Docker Compose 一键启动 PostgreSQL 与 Redis在项目根目录创建docker-compose.ymlversion: 3.8 services: postgres: image: pgvector/pgvector:pg16 container_name: cloudvault-postgres environment: POSTGRES_USER: cloudvault POSTGRES_PASSWORD: cloudvault123 POSTGRES_DB: cloudvault ports: - 5432:5432 volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U cloudvault] interval: 10s timeout: 5s retries: 5 redis: image: redis:7.2-alpine container_name: cloudvault-redis ports: - 6379:6379 command: redis-server --appendonly yes --requirepass cloudvault123 volumes: - redisdata:/data volumes: pgdata: redisdata:这里选择了pgvector/pgvector:pg16镜像它已经预装了 pgvector 插件。Redis 镜像启动时通过--requirepass设置了访问密码避免裸奔。启动服务docker compose up -d验证 PostgreSQL 与 Redis 是否正常docker ps docker exec -it cloudvault-postgres psql -U cloudvault -d cloudvault -c SELECT version(); docker exec -it cloudvault-redis redis-cli -a cloudvault123 ping需要注意--requirepass方式仅适合开发环境。生产环境更推荐通过 Redis ACL 配置独立用户和权限。4.2 Windows 安装 pgvector 插件如果后端服务直接在 Windows 上运行需要为本机 PostgreSQL 安装 pgvector。各大平台安装方式不同Windows 下通常从 pgvector 官方 GitHub Releases 页面下载与 PostgreSQL 版本匹配的预编译安装包。安装完成后在 PostgreSQL 的share/extension目录下应该能看到vector.control文件。如果看不到说明扩展文件没有复制成功。Windows 安装常见的坑是 PostgreSQL 版本与 pgvector 版本不匹配。例如 PostgreSQL 16 必须选择pgvector-pg16对应的安装包。4.3 创建向量表和 HNSW 索引连接数据库启用扩展CREATE EXTENSION IF NOT EXISTS vector;创建文档表注意embedding VECTOR(1024)的维度必须和后续所用 Embedding 模型的输出维度一致CREATE TABLE file_documents ( id BIGSERIAL PRIMARY KEY, file_id VARCHAR(64) NOT NULL, file_name VARCHAR(255) NOT NULL, owner_id VARCHAR(64) NOT NULL, chunk_index INT NOT NULL, chunk_text TEXT NOT NULL, embedding VECTOR(1024), created_at TIMESTAMP DEFAULT NOW() );为余弦相似度检索创建 HNSW 索引CREATE INDEX idx_file_documents_embedding ON file_documents USING hnsw (embedding vector_cosine_ops); CREATE INDEX idx_file_documents_file_id ON file_documents (file_id);HNSWHierarchical Navigable Small World索引适合大数据量下的近似最近邻检索。小规模测试时也可以使用 IVFFlat 索引但 HNSW 在查询速度和构建复杂度上更均衡。pgvector 从 0.5.0 版本开始支持 HNSW低版本需要先升级。字段说明file_id关联网盘文件表中的文件记录。owner_id文件归属用户用于问答时隔离不同用户的知识库。chunk_index切片序号方便定位原文。chunk_text原始文本片段在 RAG 中会作为上下文传给大模型。embedding文本片段对应的向量。5. 基于 LangChain4j 的 RAG 文档问答实现5.1 为什么使用 LangChain4jLangChain4j 是 Java 生态中面向大模型应用的编排框架类比 Python 生态的 LangChain。它提供了一套统一 API把文档加载、文本切片、Embedding、向量存储、模型调用等组件串联起来。对于 Java 技术栈的团队选择 LangChain4j 可以直接复用现有工程能力不必引入 Python 服务。5.2 添加 Maven 依赖在pom.xml中加入以下依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId scoperuntime/scope /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-pgvector/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-ollama/artifactId version0.35.0/version /dependency版本说明LangChain4j 发布节奏较快上述 0.35.0 是示例版本请以官方最新稳定版为准。不同版本之间的配置前缀和 API 可能略有调整。5.3 配置模型与向量存储在生产项目中强烈建议先通过 Ollama 拉取模型ollama pull qwen2.5:7b然后在application.yml中配置数据源、Redis 和 LangChain4jspring: datasource: url: jdbc:postgresql://localhost:5432/cloudvault username: cloudvault password: cloudvault123 driver-class-name: org.postgresql.Driver data: redis: host: localhost port: 6379 password: cloudvault123 langchain4j: ollama: base-url: http://localhost:11434 chat-model: model-name: qwen2.5:7b temperature: 0.2 embedding-model: model-name: qwen2.5:7b配置PgVectorEmbeddingStore的 Bean// 文件路径src/main/java/com/cloudvault/ai/config/VectorStoreConfig.java Configuration public class VectorStoreConfig { Value(${spring.datasource.url}) private String dbUrl; Value(${spring.datasource.username}) private String dbUser; Value(${spring.datasource.password}) private String dbPassword; Value(${ai.embedding.dimension:1024}) private Integer dimension; Bean public PgVectorEmbeddingStore pgVectorEmbeddingStore() { return PgVectorEmbeddingStore.builder() .host(localhost) .port(5432) .database(cloudvault) .user(dbUser) .password(dbPassword) .table(file_documents) .dimension(dimension) .build(); } }需要注意向量维度必须与 Embedding 模型输出维度一致。如果 Ollama 中 embedding 模型输出维度是 768则VECTOR(768)和dimension(768)需要同步修改否则会报维度不匹配错误。5.4 文档解析、切分与向量化文档进入知识库要经过四个步骤读取文件、抽取文本、切片、生成向量并入库。// 文件路径src/main/java/com/cloudvault/ai/service/KnowledgeBaseService.java Service RequiredArgsConstructor Slf4j public class KnowledgeBaseService { private final EmbeddingModel embeddingModel; private final PgVectorEmbeddingStore embeddingStore; public void ingestFile(MultipartFile file, String fileId, String ownerId) { // 1. 读取文件内容这里以纯文本为例 // 实际项目中 PDF、Word 需要引入对应的解析库 String content; try { content new String(file.getBytes(), StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(读取文件失败, e); } // 2. 将内容封装为 LangChain4j 的 Document Document document Document.from(content); // 3. 切片每段 500 字符重叠 100 字符 DocumentSplitter splitter DocumentSplitters.recursive(500, 100); ListTextSegment segments splitter.split(document); // 4. 批量向量化 ListEmbedding embeddings embeddingModel.embedAll(segments).content(); // 5. 写入向量库 for (int i 0; i segments.size(); i) { TextSegment segment segments.get(i); // 通过 metadata 保存文件归属信息便于后续权限隔离 segment.metadata().put(fileId, fileId); segment.metadata().put(ownerId, ownerId); segment.metadata().put(fileName, file.getOriginalFilename()); segment.metadata().put(chunkIndex, String.valueOf(i)); embeddingStore.add(segment, embeddings.get(i)); } log.info(文件入库完成, fileId{}, chunks{}, fileId, segments.size()); } }几个关键点解释一下切片大小500 字符是一个相对均衡的默认值。切太大会导致检索精度下降切太细容易丢失上下文语义。重叠区间相邻切片保留 100 字符重叠避免一个完整语义被拦腰切断。Metadata 信息向量库中除了向量本身还保存了文件与用户信息。这为后续“只检索自己的文档”提供了基础。上面示例使用的是纯文本情况。如果支持 PDF 或 Word需要额外引入 Apache PDFBox、Apache POI 等库先把二进制内容抽取成文本再做后续处理。5.5 自定义 SQL 向量检索LangChain4j 自带的embeddingStore.findRelevant()可以完成基础检索但在真实业务中往往需要组合条件过滤比如按owner_id隔离用户、按file_id限定文档范围。此时更推荐直接使用 Spring JDBC 执行原生 SQL。// 文件路径src/main/java/com/cloudvault/ai/repository/VectorSearchRepository.java Repository RequiredArgsConstructor public class VectorSearchRepository { private final JdbcTemplate jdbcTemplate; public ListDocumentChunk searchSimilar( String ownerId, float[] queryVector, int topK) { // 将 float 数组转为 PostgreSQL 向量字面量 String vectorLiteral Arrays.stream(queryVector) .mapToObj(v - String.format(Locale.ROOT, %.6f, v)) .collect(Collectors.joining(,)); String sql SELECT file_id, file_name, chunk_index, chunk_text, 1 - (embedding ?::vector) AS similarity FROM file_documents WHERE owner_id ? ORDER BY embedding ?::vector LIMIT ? ; return jdbcTemplate.query( sql, new Object[]{vectorLiteral, ownerId, vectorLiteral, topK}, (rs, rowNum) - new DocumentChunk( rs.getString(file_id), rs.getString(file_name), rs.getInt(chunk_index), rs.getString(chunk_text), rs.getDouble(similarity) ) ); } }这里使用的是 pgvector 的余弦距离操作符。返回结果中1 - distance就是余弦相似度值越大表示向量越接近。通过WHERE owner_id ?实现了知识库的按用户隔离避免用户 A 问到用户 B 的文档内容。如果希望在整个系统里做更复杂的过滤例如同时限定目录、文件类型可以继续在 WHERE 子句中追加条件这是原生 SQL 方案的最大优势。5.6 问答接口完整实现问答服务的核心逻辑分四步向量化用户问题、从知识库检索候选片段、组装带上下文的提示词、调用大模型生成回答。// 文件路径src/main/java/com/cloudvault/ai/service/RagChatService.java Service RequiredArgsConstructor Slf4j public class RagChatService { private final EmbeddingModel embeddingModel; private final ChatLanguageModel chatModel; private final VectorSearchRepository vectorSearchRepository; public RagAnswer answer(String ownerId, String question) { // 1. 用户问题向量化 Embedding questionEmbedding embeddingModel.embed(question).content(); // 2. 从向量库中检索 Top5 相关片段 ListDocumentChunk chunks vectorSearchRepository.searchSimilar( ownerId, questionEmbedding.vector(), 5 ); if (chunks.isEmpty()) { return RagAnswer.fail(知识库中没有找到相关内容); } // 3. 组装上下文 StringBuilder context new StringBuilder(); for (DocumentChunk chunk : chunks) { context.append(【文档) .append(chunk.getFileName()) .append(】\n) .append(chunk.getChunkText()) .append(\n---\n); } // 4. 构造提示词并调用大模型 String prompt 你是一个网盘智能助手。请严格根据下面提供的参考资料回答用户问题。 如果参考资料不足以回答请直接说明“知识库中未找到相关内容”不要编造答案。 参考资料 %s 用户问题%s 请用简洁、准确的中文回答。 .formatted(context.toString(), question); String answer chatModel.generate(prompt); // 5. 返回答案和引用片段方便前端展示出处 return RagAnswer.success(answer, chunks); } }在提示词中明确要求“不要编造答案”是 RAG 项目里最重要的工程经验之一。大模型在缺乏信息时倾向于“合理化”猜测必须通过提示词约束同时在后端增加引用片段展示能力让用户能核查答案来源。ChatController提供 HTTP 接口// 文件路径src/main/java/com/cloudvault/ai/controller/ChatController.java RestController RequestMapping(/api/chat) RequiredArgsConstructor public class ChatController { private final RagChatService ragChatService; PostMapping(/ask) public ApiResultRagAnswer ask( RequestBody AskRequest request, RequestHeader(X-User-Id) String userId) { return ApiResult.success(ragChatService.answer(userId, request.getQuestion())); } }请求体结构通过一个简单的 DTO 封装Data public class AskRequest { private String question; }至此一条完整的 RAG 问答链路已经打通上传文件时解析入库用户提问时检索召回大模型生成答案。6. Redis 在系统中的应用与实时通知Redis 在 CloudVault 中不是配角。除了常规缓存它还承担分布式锁和实时通知两条关键链路。6.1 Redis 缓存文件元数据文件元数据是高频访问数据。用户打开网盘首页、搜索文件、下载校验时都会查询。如果每次都查 PostgreSQL接口压力会很大。// 文件路径src/main/java/com/cloudvault/file/service/FileMetaService.java Service RequiredArgsConstructor Slf4j public class FileMetaService { private static final long CACHE_TTL_MINUTES 30; private final StringRedisTemplate redisTemplate; private final FileMetaMapper fileMetaMapper; public FileMeta getFileMeta(String fileId) { // 1. 先查询缓存 String json redisTemplate.opsForValue().get(file:meta: fileId); if (json ! null) { return JSON.parseObject(json, FileMeta.class); } // 2. 缓存未命中则访问数据库 FileMeta meta fileMetaMapper.selectById(fileId); if (meta ! null) { redisTemplate.opsForValue().set( file:meta: fileId, JSON.toJSONString(meta), CACHE_TTL_MINUTES, TimeUnit.MINUTES ); } return meta; } }当文件发生变化时需要主动删除缓存public void evictFileMeta(String fileId) { redisTemplate.delete(file:meta: fileId); }对于文件元数据这类读多写少的数据Cache-Aside 模式足够。注意即使 Redis 缓存了数据也必须有 TTL 兜底防止缓存与数据库长期不一致。6.2 Redis 分布式锁实现秒传秒传是网盘系统的高频需求。用户上传文件时系统先计算文件 MD5。如果该文件已经存在直接复用已有文件记录避免重复上传。这里有一个并发问题同一个 MD5 可能被多个用户同时上传如果都发现“文件不存在”就会重复写入。解决办法是给 MD5 加一个分布式锁。// 文件路径src/main/java/com/cloudvault/file/service/FileStorageService.java Service RequiredArgsConstructor Slf4j public class FileStorageService { private final StringRedisTemplate redisTemplate; private final FileMetaMapper fileMetaMapper; public UploadResult uploadByMd5(String md5, MultipartFile file, String ownerId) { String lockKey lock:upload: md5; // 1. 尝试加锁2 分钟自动过期避免异常情况下死锁 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 2, TimeUnit.MINUTES); if (!Boolean.TRUE.equals(locked)) { // 其他线程正在处理同一文件可以稍后重试或直接等待 throw new BizException(文件正在处理中请稍后重试); } try { // 2. 加锁后再次检查文件是否存在双重检查 FileMeta existing fileMetaMapper.selectByMd5(md5); if (existing ! null) { return UploadResult.fastSuccess(existing.getFileId()); } // 3. 真正保存文件并写入元数据 FileMeta meta saveFileToStorage(file, md5, ownerId); return UploadResult.success(meta.getFileId()); } finally { // 4. 释放锁 redisTemplate.delete(lockKey); } } }关于分布式锁有两点需要重点说明setIfAbsentSETNX加上过期时间必须在一个命令里完成否则加锁和设置过期时间之间发生宕机锁永远不会释放。业务执行时间要小于锁过期时间。上传大文件耗时长如果锁提前过期其他请求就会并发进入。更稳妥的方案是使用 Redisson 的看门狗机制自动续期。6.3 Redis Pub/Sub WebSocket 实时通知实时通知的典型场景是用户上传一份大文档AI 索引可能需要几十秒甚至几分钟。如果网页一直停留在一个等待页面体验很差。更好的做法是让用户先做其他事AI 索引完成后系统主动推送“索引完成”通知。CloudVault 采用 Redis Pub/Sub WebSocket 组合。第一步配置 WebSocket// 文件路径src/main/java/com/cloudvault/notify/config/WebSocketConfig.java Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker(/topic); registry.setApplicationDestinationPrefixes(/app); } Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws) .setAllowedOriginPatterns(*) .withSockJS(); } }前端订阅指定用户的频道const socket new SockJS(/ws); const stompClient Stomp.over(socket); stompClient.connect({}, function () { stompClient.subscribe(/topic/user/ userId, function (response) { const notify JSON.parse(response.body); console.log(收到通知:, notify); // 根据 notify.type 刷新文件列表或上传状态 }); });第二步发布 Redis 消息// 文件路径src/main/java/com/cloudvault/notify/service/NotifyService.java Service RequiredArgsConstructor Slf4j public class NotifyService { private final StringRedisTemplate redisTemplate; public void notifyUser(String userId, String eventType, String message) { String channel cloudvault:notify: userId; String payload String.format( {\type\:\%s\,\message\:\%s\,\timestamp\:\%s\}, eventType, message, System.currentTimeMillis() ); redisTemplate.convertAndSend(channel, payload); log.info(发布通知: userId{}, event{}, userId, eventType); } }第三步监听 Redis 消息并转发给 WebSocket// 文件路径src/main/java/com/cloudvault/notify/config/RedisPubSubConfig.java Configuration public class RedisPubSubConfig { Bean public RedisMessageListenerContainer container( RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) { RedisMessageListenerContainer container new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.addMessageListener(listenerAdapter, new PatternTopic(cloudvault:notify:*)); return container; } Bean public MessageListenerAdapter listenerAdapter(RedisNotifyHandler handler) { return new MessageListenerAdapter(handler); } }// 文件路径src/main/java/com/cloudvault/notify/handler/RedisNotifyHandler.java Component Slf4j public class RedisNotifyHandler implements MessageListener { private final SimpMessagingTemplate messagingTemplate; public RedisNotifyHandler(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate messagingTemplate; } Override public void onMessage(Message message, byte[] pattern) { String channel new String(message.getChannel()); String payload new String(message.getBody()); log.info(收到Redis消息: channel{}, payload{}, channel, payload); String userId channel.substring(channel.lastIndexOf(:) 1); messagingTemplate.convertAndSend(/topic/user/ userId, payload); } }这套机制的核心价值在于Spring Boot 多个实例都能收到 Redis Pub/Sub 消息但只有持有用户 WebSocket 连接的实例负责下发。如果以后把系统扩展成多实例部署Redis Pub/Sub 天然支持广播不需要额外引入消息队列。7. 常见问题与排查思路下面是 CloudVault 开发过程中最可能遇到的五类问题按频率排序。问题现象常见原因解决思路CREATE EXTENSION vector报错pgvector 插件未安装或版本与 PostgreSQL 不匹配检查share/extension是否有vector.controlWindows 环境重新下载匹配安装包LangChain4j 调用 Ollama 超时Ollama 服务未启动、端口错误、模型名错误执行ollama list确认模型访问http://localhost:11434验证服务向量检索结果不相关切片大小不合适、Embedding 模型与文本领域不匹配、相似度阈值太低尝试 300-800 字符切片换用中文效果更好的 Embedding 模型调高相似度阈值向量维度不一致报错建表VECTOR(n)的 n 与模型输出维度不同打印模型输出维度修改表结构时重建列或重新建表Redis 连接被拒绝Redis 地址、端口、密码配置错误检查application.yml本地执行redis-cli -a password pingWebSocket 连接成功但收不到通知Redis 频道名称不一致或消息监听器未绑定检查发布频道和订阅 Pattern 是否一致查看 RedisNotifyHandler 日志排查问题时建议给RedisNotifyHandler和KnowledgeBaseService加详细日志。分布式场景下没有日志问题几乎无法定位。我在实际开发中遇到最多的是向量维度问题。Ollama 上不同模型输出的 embedding 维度差异很大有的模型是 768 维有的是 1024 维。建表的时候如果维度写错查询时 PostgreSQL 会直接报错而且是在数据已经入库之后才发现。建议在项目启动时增加一个自检任务用一条样本数据跑一次向量化把维度打印出来与表结构比对。8. 最佳实践与工程建议8.1 安全边界文件权限是网盘的生命线。所有文件下载、删除、预览接口都必须校验文件归属。向量检索 SQL 中必须带owner_id条件否则可能存在越权问答。对于 AI 相关配置Embedding 模型和对话模型的地址、API Key、模型名称不要明文写在代码仓库中建议使用环境变量或配置中心管理。如果使用云厂商模型 APIAPI Key 泄露会造成真实经济损失。8.2 异步与重试文档解析和向量化是耗时操作绝不能同步阻塞上传接口。建议方案是文件上传接口只做文件存储和元数据入库然后发送一条“待索引”消息到消息队列由独立消费者完成 AI 入库。如果当前技术栈里没有消息队列可以先使用 Spring 的Async 线程池。但要注意线程池方案遇到进程重启会丢失任务生产环境应尽快引入 MQ。索引失败必须支持重试。重试时需要设置最大失败次数超过次数后标记文件为“AI 索引失败”允许用户重新触发索引。8.3 缓存与数据库一致性Redis 缓存文件元数据时删除操作必须同时清缓存。建议在事务提交后清理避免事务回滚但缓存已删的脏读问题。分布式锁方面推荐封装成注解或工具类避免业务代码里到处写setIfAbsent和delete。更复杂的手段比如锁续期建议直接引入 Redisson。8.4 向量索引与性能当文档切片数量达到百万级时必须使用 HNSW 或 IVFFlat 索引。没有索引的暴力搜索在数据量增大后性能会急剧下降。HNSW 参数可以按数据规模调整CREATE INDEX idx_file_documents_embedding ON file_documents USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);查询时可以通过SET hnsw.ef_search 100;调节召回质量与速度的平衡这个参数越大检索越精确但耗时越长。8.5 日志与可观测性RAG 链路涉及文件解析、Embedding、向量检索、模型调用四个环节。建议为
返回列表