基于 Spring AI 和 Redis 向量库的智能对话系统实践

基于 Spring AI 和 Redis 向量库的智能对话系统实践

在当前大模型技术飞速发展的背景下,构建一个功能强大、可扩展的 AI 助手系统成为许多开发者的目标。本文将分享我们基于 Spring AI 框架,结合 Redis 作为向量数据库,实现的一个多模态、支持文档检索与文件上传的智能对话系统。系统具备图片识别、文件解析、中英文翻译、深度思考等核心能力,并实现了灵活的多模型管理和切换。

界面预览

技术架构概览

整个系统采用前后端分离架构,后端基于 Spring Boot + Spring AI 构建,前端为简洁的 Web 界面。核心技术栈如下:

  • 核心框架 : Spring AI - 提供了与大语言模型(LLM)交互的标准化接口。
  • 向量数据库 : Redis (通过 spring-ai-redis 实现) - 用于存储和检索文档的嵌入向量(Embeddings),实现基于内容的语义搜索。
  • 模型服务: 自行部署的 Qwen 大模型(通过 vLLM 推理服务),以及阿里百炼提供的视觉模型和向量模型。
  • 文件处理: 使用 Apache Tika 解析多种格式的文档(PDF, Word, PPT 等),并利用 OCR 技术处理图片中的文字。
  • 通信协议 : 使用 SSE (Server-Sent Events) 实现流式响应,提供流畅的聊天体验。
  • 状态管理: 利用 Redis 缓存会话记忆和图片识别结果,提升性能。

核心功能实现

1. 多模型管理与切换

系统支持管理多个不同类型的 AI 模型,并允许用户在聊天过程中自由切换。

复制代码
// 在配置界面展示所有模型
@GetMapping("/models")
public List<ModelVO> listModels() {
    return modelService.listByCondition(new ModelQuery());
}

用户可以通过下拉菜单选择不同的模型进行对话,如 /data/model/Qwen3-30B-A3B(本地大模型)、qwen3-max(阿里百炼Max大模型)等。

2. 图片与文件上传预览及提问

系统支持用户上传图片和文件,并能对其进行分析和问答。

图片识别

当用户上传图片时,系统会自动调用多模态模型(Multi-Modal Model)来识别图片内容,并将其描述文本作为上下文传递给主语言模型。

复制代码
/**
 * 处理图片:调用多模态模型识别图片内容
 */
private String processImagesWithMultiModal(List<String> fileUrls, String conversationId) {
    if (CollectionUtils.isEmpty(fileUrls)) {
        return "没有找到图片文件";
    }

    // 分类文件,只处理图片
    FileClassificationResult classification = classifyFiles(fileUrls);
    List<String> imageUrls = classification.getImageUrls();

    if (imageUrls.isEmpty()) {
        return "";
    }

    // 获取可用的多模态模型
    AiModel multiModalModel = getMultiModalModel();
    if (multiModalModel == null) {
        log.warn("未找到可用的多模态模型,跳过图片识别");
        return "未找到可用的多模态模型";
    }

    StringBuilder imageDescriptions = new StringBuilder();
    AIParams multiModalParams = mergeParams(multiModalModel, null);

    for (int i = 0; i < imageUrls.size(); i++) {
        String imageUrl = imageUrls.get(i);
        try {
            // 使用 Redis 缓存图片识别结果,避免重复请求
            String cacheKey = IMAGE_CACHE_PREFIX + conversationId + ":" + extractFileNameFromUrl(imageUrl);
            String cachedDescription = stringRedisTemplate.opsForValue().get(cacheKey);

            if (StrUtil.isNotBlank(cachedDescription)) {
                imageDescriptions.append("图片").append(i + 1).append(": ");
                imageDescriptions.append(cachedDescription);
            } else {
                // 调用多模态模型进行识别
                byte[] imageBytes = ossService.readBytes(imageUrl);
                UserMessage visionMessage = UserMessage.builder()
                        .text("请描述这张图片的内容。")
                        .media(new Media(MimeTypeUtils.IMAGE_PNG, new ByteArrayResource(imageBytes)))
                        .build();

                String imageDescription = llmHandler.blockChatWithVision(visionMessage, multiModalParams);

                // 将结果缓存到 Redis(5分钟有效)
                stringRedisTemplate.opsForValue().set(cacheKey, imageDescription, 5, TimeUnit.HOURS);
                imageDescriptions.append("图片").append(i + 1).append(": ");
                imageDescriptions.append(imageDescription.trim()).append("\n");
            }
        } catch (Exception e) {
            log.error("图片识别失败: {}", imageUrl, e);
            String fileName = extractFileNameFromUrl(imageUrl);
            imageDescriptions.append("图片").append(i + 1).append(": ").append(fileName).append("\n");
        }
    }

    return imageDescriptions.toString();
}

3. 文档上传与向量化

用户可以上传 PDF、Word 等文档,系统会使用 Tika 工具解析其内容,并将其分块后转换为向量,存储到 Redis 向量库中。

复制代码
@PostMapping(value = "/tikaParseEmbeddingFileSse", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public SseEmitter tikaParseEmbeddingFileSse(@RequestPart("file") MultipartFile file) {
    AssertUtil.notNull(file, "请上传文件");
    String filename = file.getOriginalFilename();
    AssertUtil.notNull(filename, "请上传文件");

    // 区分图片和普通文件
    String fileSuffix = filename.substring(filename.lastIndexOf(".") + 1);
    if (fileSuffix.equals("jpg") || fileSuffix.equals("png") || fileSuffix.equals("jpeg")) {
        return embeddingFileService.ocrImage(file); // 图片使用OCR
    } else {
        return embeddingFileService.tikaParseEmbeddingFileSse(file); // 其他文件使用Tika解析
    }
}

embeddingFileService 内部会调用 documentParseService 解析文件,并最终调用 llmHandler.vectorizeAndStore() 方法将文本内容向量化并存入 Redis。

4. 基于文档的智能问答

当用户提出问题时,系统会先从 Redis 向量库中检索与问题最相关的文档片段,然后将这些片段作为上下文,连同用户的问题一起发送给 LLM 进行回答。

复制代码
/**
 * 检索最相关的文档
 */
public List<Document> retrieveRelevantDocuments(String query, List<String> documentIds, AIParams params) {
    RedisVectorStore vectorStore = getInitializedVectorStore(params);

    // 构建过滤器,只检索指定文档ID的内容
    Filter.Expression filter = null;
    if (!documentIds.isEmpty()) {
        filter = VectorFilterBuilder.buildDocumentIdFilter(documentIds);
    }

    SearchRequest request = SearchRequest.builder()
            .query(query)
            .topK(20) // 返回前20个最相关的文档
            .similarityThreshold(0.5) // 相似度阈值
            .filterExpression(filter)
            .build();

    List<Document> documents = vectorStore.similaritySearch(request);
    // 进一步过滤确保返回的文档属于目标列表
    return documents.stream()
            .filter(doc -> documentIds.contains(doc.getMetadata().get("document_id")))
            .collect(Collectors.toList());
}

5. 流式对话与深度思考

系统使用 SSE 实现流式输出,用户可以实时看到 AI 的生成过程。同时,支持"深度思考"模式,让 AI 在回答前进行更深入的推理。

复制代码
public void streamChatSse(String systemMsg, UserMessage message, String conversationId, Boolean enableThinking, AIParams params, SseEmitter emitter) {
    // ... 参数校验 ...

    AiModelOptions modelOptions = params.toModelOptions();
    ChatModel chatModel = AiModelFactory.createStreamingChatModel(modelOptions);

    ChatClient chatClient = ChatClient.builder(chatModel)
            .defaultAdvisors(
                    new SimpleLoggerAdvisor(),
                    new MessageChatMemoryAdvisor(chatMemory) // 记住聊天历史
            ).build();

    // 设置会话 ID,用于在 Redis 中存储聊天记忆
    Integer userId = Objects.requireNonNull(UserUtil.getUser()).getId();
    String memoryIdWithUserId = userId + ":u:" + conversationId;

    Prompt prompt = new Prompt(message);
    prompt.setEnableThinking(enableThinking); // 开启深度思考

    Disposable disposable = chatClient.prompt(prompt)
            .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, memoryIdWithUserId))
            .system(systemMsg)
            .stream()
            .content()
            .takeWhile(chunk -> !chatSessionHandler.isCancelled(conversationId))
            .doOnNext(chunk -> {
                try {
                    MessageData messageData = new MessageData();
                    messageData.setMessage(chunk);
                    emitter.send(SseEmitter.event().data(SseResponse.chunk(conversationId, messageData)));
                } catch (IOException e) {
                    emitter.completeWithError(e);
                }
            })
            .doOnComplete(() -> {
                try {
                    emitter.send(SseEmitter.event().data(SseResponse.end()));
                    emitter.complete();
                } catch (IOException e) {
                    emitter.completeWithError(e);
                }
            })
            .doOnError(error -> {
                try {
                    emitter.send(SseEmitter.event().data(SseResponse.error("服务端错误: " + error.getMessage())));
                } catch (Exception ex) {
                    log.error("发送错误事件失败", ex);
                }
                emitter.completeWithError(error);
            })
            .subscribe();

    chatSessionHandler.registerSession(conversationId, disposable, emitter);
}

6. 翻译与深度思考功能

系统内置了简单的翻译功能,可以快速将中文翻译成英文,反之亦然。

复制代码
// 在前端页面上,用户可以选择"翻译"按钮
// 系统会调用类似以下逻辑
if (requestType.equals("translate")) {
    String result = translate(text, sourceLang, targetLang);
    return result;
}

"深度思考"功能则通过设置 enableThinking=true 来触发,这会改变 LLM 的行为,使其在生成答案前进行更长时间的内部推理。

关键代码解析

AIChatHandler 类

AIChatHandler 是核心业务逻辑处理类,负责协调各个服务。

  • mergeParams: 合并全局模型参数和具体对话的参数,确保配置正确。
  • streamChatSse: 主要的流式聊天入口,支持传入文档ID和文件URL。
  • processImagesWithMultiModal: 调用多模态模型识别图片内容。
  • buildDocumentContextSafely: 从向量库检索相关文档内容。
  • buildUserMessageWithFiles: 构建包含文件信息的用户消息。

LLMHandler 类

LLMHandler 是与 Spring AI 框架交互的桥梁。

  • blockChat: 阻塞式调用 LLM。
  • blockChatWithVision: 支持图片输入的阻塞式调用。
  • streamChatSse: 实现流式对话的核心方法。
  • vectorizeAndStore: 将文本内容向量化并存储到 Redis。
  • retrieveRelevantDocuments: 从 Redis 向量库检索相关文档。

总结与展望

本项目成功构建了一个功能全面的智能对话系统,充分利用了 Spring AI 的强大能力和 Redis 作为向量数据库的高效性。系统不仅支持基础的文本聊天,还集成了图片识别、文档解析、语义检索等高级功能,极大地提升了用户体验。

未来可以进一步优化的方向包括:

  1. 引入 RAG(检索增强生成):更精细地控制检索和生成的流程。
  2. 增加工具调用(Tool Calling):让 AI 能够调用外部 API 完成复杂任务。
  3. 优化向量存储:探索更高效的向量索引算法。
  4. 增强安全与权限控制:对敏感数据和操作进行更严格的保护。

通过不断迭代和优化,这样的 AI 助手系统将成为企业知识管理、客户服务和个人学习的强大工具。

相关推荐
NAGNIP3 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab4 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab4 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP8 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年8 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼8 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS9 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区10 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈10 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang10 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx