使用LangChain4j +Springboot 实现大模型与向量化数据库协同回答

本文基于私有化部署 场景,在「文本向量化入库」的基础上,完整实现 RAG 核心链路:用户提问 → 向量库语义检索 → Prompt 增强(拼接检索结果) → 本地大模型生成回答,核心技术栈为 LangChain4j + Spring Boot + Ollama(Qwen 2/DeepSeek 大模型) + Milvus/Chroma(向量数据库),兼顾企业级场景的准确性、稳定性和可扩展性。

一、前置条件(必做)

  1. 完成「文本向量化入库」基础环境搭建(参考之前教程):

    • Spring Boot 项目已集成 LangChain4j,且能正常向 Chroma/Milvus 入库向量;

    • Ollama 已部署嵌入模型 (nomic-embed-text)和大语言模型 (如 Qwen 2 7B,中文优化):

      bash 复制代码
      # 拉取 Qwen 2 7B 模型(私有化大模型核心)
      ollama pull qwen2:7b
      # 验证大模型可调用
      curl http://localhost:11434/api/generate -H "Content-Type: application/json" -d '{
        "model": "qwen2:7b",
        "prompt": "测试回答"
      }'
    • 向量数据库(Chroma/Milvus)已有入库的文本向量(如企业知识库片段)。

  2. 补充 Maven 依赖(大模型调用核心):

    xml 复制代码
    <!-- LangChain4j Ollama 聊天模型(调用本地大模型) -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-ollama</artifactId>
        <version>0.35.0</version>
    </dependency>
    <!-- LangChain4j 记忆模块(多轮对话可选) -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-memory</artifactId>
        <version>0.35.0</version>
    </dependency>
    <!-- Redis 记忆持久化(可选,多轮对话) -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-redis</artifactId>
        <version>0.35.0</version>
    </dependency>

二、核心配置(大模型 + 记忆模块)

在原有 RagConfig.java 基础上,新增大模型 Bean对话记忆 Bean(可选,支撑多轮对话)。

1. 补充 application.yml 配置

yaml 复制代码
rag:
  # 原有配置(嵌入模型、向量库)不变
  ollama:
    base-url: http://localhost:11434
    embedding-model-name: nomic-embed-text
    chat-model-name: qwen2:7b # 新增:本地大模型名称
    temperature: 0.3 # 生成温度(0-1,越低越精准)
    max-tokens: 2048 # 最大生成长度
  # 记忆模块配置(可选)
  memory:
    redis:
      host: localhost
      port: 6379
      ttl: 3600 # 对话记忆过期时间(秒)

2. 扩展配置类(RagConfig.java)

java 复制代码
package com.rag.demo.config;

import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.memory.redis.RedisChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;

@Configuration
public class RagConfig {
    // ========== 原有 Bean(嵌入模型、向量存储)不变 ==========
    
    // ========== 新增:本地大模型 Bean(核心) ==========
    @Value("${rag.ollama.base-url}")
    private String ollamaBaseUrl;
    @Value("${rag.ollama.chat-model-name}")
    private String chatModelName;
    @Value("${rag.ollama.temperature}")
    private double temperature;
    @Value("${rag.ollama.max-tokens}")
    private int maxTokens;

    @Bean
    public ChatLanguageModel ollamaChatModel() {
        return OllamaChatModel.builder()
                .baseUrl(ollamaBaseUrl)
                .modelName(chatModelName)
                .temperature(temperature) // 降低随机性,提升回答准确性
                .maxTokens(maxTokens)     // 限制生成长度,避免冗余
                .timeout(Duration.ofMinutes(3)) // 超时时间(大模型推理可能较慢)
                .build();
    }

    // ========== 新增:对话记忆 Bean(可选,多轮对话) ==========
    @Value("${rag.memory.redis.host}")
    private String redisHost;
    @Value("${rag.memory.redis.port}")
    private int redisPort;
    @Value("${rag.memory.redis.ttl}")
    private long ttlSeconds;

    // 轻量记忆(内存版,适合测试)
    @Bean
    public ChatMemory messageWindowChatMemory() {
        // 保留最近5轮对话,避免上下文过长
        return MessageWindowChatMemory.withMaxMessages(5);
    }

    // 持久化记忆(Redis版,适合生产,注释掉内存版启用)
    /*
    @Bean
    public ChatMemory redisChatMemory() {
        return RedisChatMemory.builder()
                .redisHost(redisHost)
                .redisPort(redisPort)
                .ttl(Duration.ofSeconds(ttlSeconds))
                .build();
    }
    */
}

三、核心服务开发(RAG 全流程)

创建 RagAnswerService,整合「检索 → Prompt 增强 → 大模型生成」核心逻辑,支持单轮/多轮对话。

1. 服务接口(RagAnswerService.java)

java 复制代码
package com.rag.demo.service;

import java.util.Map;

public interface RagAnswerService {

    /**
     * 单轮 RAG 问答(核心)
     * @param userId 用户ID(用于记忆区分,多轮对话必填)
     * @param question 用户问题
     * @return 回答结果(包含回答文本、检索来源、相似度得分)
     */
    Map<String, Object> ragAnswer(String userId, String question);

    /**
     * 清空用户对话记忆(可选)
     * @param userId 用户ID
     */
    void clearMemory(String userId);
}

2. 服务实现类(核心逻辑)

java 复制代码
package com.rag.demo.service;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class RagAnswerServiceImpl implements RagAnswerService {

    // 注入核心 Bean
    private final EmbeddingModel embeddingModel; // 向量化模型
    private final EmbeddingStore<TextSegment> embeddingStore; // 向量库
    private final ChatLanguageModel chatModel; // 本地大模型
    private final ChatMemory chatMemory; // 对话记忆(可选)

    // 企业级 Prompt 模板(核心:约束大模型仅基于检索结果回答)
    private static final String PROMPT_TEMPLATE = """
            角色:你是企业专属的智能问答助手,仅能基于提供的参考资料回答用户问题。
            参考资料:
            {reference_documents}
            
            严格遵守以下规则:
            1. 必须完全基于参考资料回答,禁止编造任何未提及的信息;
            2. 若参考资料中无相关内容,仅回复「暂无相关信息」,不添加额外内容;
            3. 回答语言简洁、专业,符合企业话术规范,使用中文;
            4. 涉及敏感信息(如手机号、姓名)自动脱敏;
            5. 回答长度控制在500字以内。
            
            用户问题:{user_question}
            """;

    // 检索参数
    private static final int TOP_K = 5; // 检索最相似的5个片段
    private static final double SIMILARITY_THRESHOLD = 0.5; // 相似度阈值(低于则视为无相关内容)

    @Override
    public Map<String, Object> ragAnswer(String userId, String question) {
        try {
            // ========== 步骤1:语义检索(从向量库获取相关片段) ==========
            // 1.1 问题向量化
            Embedding questionEmbedding = embeddingModel.embed(question).content();
            // 1.2 向量库检索相似片段
            List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(questionEmbedding, TOP_K);
            // 1.3 过滤低相似度片段,提取有效参考资料
            List<EmbeddingMatch<TextSegment>> validMatches = matches.stream()
                    .filter(match -> match.score() >= SIMILARITY_THRESHOLD)
                    .collect(Collectors.toList());
            log.info("检索到有效片段数:{},平均相似度:{}", 
                    validMatches.size(), 
                    validMatches.stream().mapToDouble(EmbeddingMatch::score).average().orElse(0));

            // 1.4 拼接参考资料文本(带相似度和元数据,便于溯源)
            String referenceDocuments = validMatches.stream()
                    .map(match -> String.format(
                            "【相似度:%.2f】%s(元数据:%s)",
                            match.score(),
                            match.embeddedItem().text(),
                            match.embeddedItem().metadata()
                    ))
                    .collect(Collectors.joining("\n\n"));
            // 无有效检索结果时的兜底
            if (referenceDocuments.isEmpty()) {
                return Map.of(
                        "code", 200,
                        "answer", "暂无相关信息",
                        "sources", "",
                        "similarityScore", 0
                );
            }

            // ========== 步骤2:Prompt 增强(拼接问题+参考资料) ==========
            PromptTemplate promptTemplate = PromptTemplate.from(PROMPT_TEMPLATE);
            Prompt prompt = promptTemplate.apply(Map.of(
                    "reference_documents", referenceDocuments,
                    "user_question", question
            ));

            // ========== 步骤3:调用大模型生成回答(支持多轮对话) ==========
            AiMessage aiMessage;
            if (chatMemory != null) {
                // 多轮对话:从记忆中加载历史,拼接新问题
                List<ChatMessage> historyMessages = chatMemory.getMessages(userId);
                aiMessage = chatModel.generate(
                        ChatMessage.combine(historyMessages, UserMessage.from(prompt.text()))
                ).content();
                // 保存本轮对话到记忆
                chatMemory.add(userId, UserMessage.from(question), aiMessage);
            } else {
                // 单轮对话:直接调用
                aiMessage = chatModel.generate(prompt.toUserMessage()).content();
            }

            // ========== 步骤4:组装返回结果(含溯源信息) ==========
            return Map.of(
                    "code", 200,
                    "answer", aiMessage.text(), // 大模型回答
                    "sources", referenceDocuments, // 检索来源(便于企业溯源)
                    "similarityScore", validMatches.stream().mapToDouble(EmbeddingMatch::score).average().orElse(0) // 平均相似度
            );

        } catch (Exception e) {
            log.error("RAG 问答失败", e);
            return Map.of(
                    "code", 500,
                    "answer", "系统异常,请稍后重试",
                    "sources", "",
                    "similarityScore", 0
            );
        }
    }

    @Override
    public void clearMemory(String userId) {
        if (chatMemory != null) {
            chatMemory.clear(userId);
            log.info("清空用户 {} 对话记忆", userId);
        }
    }
}

四、测试接口开发(Controller)

创建 RagAnswerController,提供 HTTP 接口测试完整 RAG 流程。

java 复制代码
package com.rag.demo.controller;

import com.rag.demo.service.RagAnswerService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagAnswerController {

    private final RagAnswerService ragAnswerService;

    /**
     * RAG 核心问答接口
     * 请求示例:
     * POST /api/rag/answer
     * {
     *   "userId": "user001",
     *   "question": "LangChain4j 适合开发什么类型的应用?"
     * }
     */
    @PostMapping("/answer")
    public Map<String, Object> ragAnswer(@RequestBody Map<String, String> request) {
        String userId = request.get("userId");
        String question = request.get("question");
        if (userId == null || question == null) {
            return Map.of("code", 400, "msg", "userId和question不能为空");
        }
        return ragAnswerService.ragAnswer(userId, question);
    }

    /**
     * 清空用户对话记忆
     * 请求示例:DELETE /api/rag/memory?userId=user001
     */
    @DeleteMapping("/memory")
    public Map<String, Object> clearMemory(@RequestParam String userId) {
        ragAnswerService.clearMemory(userId);
        return Map.of("code", 200, "msg", "清空记忆成功");
    }
}

五、启动与测试验证

1. 启动应用

确保以下服务均已启动:

  • Ollama(嵌入模型 + Qwen 2 大模型);
  • Chroma/Milvus(向量数据库,已有入库数据);
  • Redis(可选,多轮对话记忆);
  • Spring Boot 应用(指定 Profile:spring.profiles.active=chromamilvus)。

2. 测试步骤

(1)单轮问答测试

使用 Postman/Curl 发送 POST 请求:

bash 复制代码
curl -X POST http://localhost:8080/api/rag/answer \
-H "Content-Type: application/json" \
-d '{
  "userId": "user001",
  "question": "LangChain4j 适合开发什么类型的应用?"
}'

响应示例(核心包含回答、检索来源、相似度):

json 复制代码
{
  "code": 200,
  "answer": "LangChain4j 是JVM生态的LLM应用开发框架,适合开发私有化RAG系统、企业级AI应用,支持Java和Kotlin语言,可集成Ollama、Milvus等工具。",
  "sources": "【相似度:0.98】LangChain4j 是JVM生态的LLM应用开发框架,支持Java和Kotlin语言,可快速构建私有化RAG系统。它集成了Ollama、Milvus等工具,适合企业级AI应用开发。(元数据:doc-001|langchain4j-knowledge)",
  "similarityScore": 0.98
}
(2)多轮对话测试(可选)

继续发送追问请求(相同 userId):

bash 复制代码
curl -X POST http://localhost:8080/api/rag/answer \
-H "Content-Type: application/json" \
-d '{
  "userId": "user001",
  "question": "它如何集成Ollama?"
}'

响应示例(大模型结合历史对话和检索结果回答):

json 复制代码
{
  "code": 200,
  "answer": "LangChain4j 提供了专门的Ollama集成模块,通过配置Ollama的Base URL和模型名称,可快速初始化OllamaChatModel和OllamaEmbeddingModel,实现本地大模型调用和文本向量化,无需依赖云端服务。",
  "sources": "【相似度:0.95】LangChain4j 集成了Ollama、Milvus等工具,通过OllamaEmbeddingModel调用本地嵌入模型,OllamaChatModel调用本地大模型,适配私有化部署。(元数据:doc-001|langchain4j-knowledge)",
  "similarityScore": 0.95
}

六、企业级优化(核心)

1. 检索精度优化

  • 混合检索 :结合向量检索(语义)+ BM25 检索(关键词),提升召回率:

    java 复制代码
    // 引入 BM25 检索器
    import dev.langchain4j.retriever.bm25.Bm25Retriever;
    // 初始化混合检索器
    Bm25Retriever bm25Retriever = Bm25Retriever.from(textSegments);
    HybridRetriever hybridRetriever = HybridRetriever.builder()
            .vectorRetriever(embeddingStore.asRetriever())
            .bm25Retriever(bm25Retriever)
            .build();
  • 重排序 :用 Cross-BERT 对检索结果二次排序,过滤低相关片段:

    xml 复制代码
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-reranker-cross-encoder</artifactId>
        <version>0.35.0</version>
    </dependency>
    java 复制代码
    CrossEncoderReranker reranker = CrossEncoderReranker.builder().build();
    List<TextSegment> rerankedSegments = reranker.rerank(question, retrievedSegments);

2. 性能优化

  • 缓存策略 :对高频问题的检索结果+回答缓存(Redis),避免重复推理:

    java 复制代码
    // 伪代码:缓存逻辑
    String cacheKey = "rag:" + userId + ":" + MD5(question);
    String cachedAnswer = redisTemplate.opsForValue().get(cacheKey);
    if (cachedAnswer != null) {
        return Map.of("code", 200, "answer", cachedAnswer);
    }
    // 无缓存时执行RAG流程,然后存入缓存
    redisTemplate.opsForValue().set(cacheKey, aiMessage.text(), Duration.ofMinutes(30));
  • 异步处理 :用线程池异步执行检索+生成,提升接口响应速度:

    java 复制代码
    @Async("ragExecutor")
    public CompletableFuture<Map<String, Object>> ragAnswerAsync(String userId, String question) {
        return CompletableFuture.supplyAsync(() -> ragAnswer(userId, question));
    }

3. 安全与合规

  • 权限过滤 :检索时根据用户角色过滤向量库数据(如售后人员仅能检索售后知识库):

    java 复制代码
    // 检索结果过滤:仅保留元数据中role匹配当前用户角色的片段
    List<EmbeddingMatch<TextSegment>> filteredMatches = validMatches.stream()
            .filter(match -> match.embeddedItem().metadata().get("role").equals(currentUserRole))
            .collect(Collectors.toList());
  • 敏感词过滤 :对大模型回答进行敏感词检测,避免违规内容:

    java 复制代码
    // 伪代码:敏感词过滤
    String filteredAnswer = sensitiveWordFilter.filter(aiMessage.text());

4. 监控与可观测性

  • 记录关键指标:检索耗时、大模型推理耗时、相似度得分,接入 Prometheus/Grafana;
  • 日志记录:记录用户问题、检索来源、回答内容(脱敏后),便于故障排查和效果分析。

七、常见问题排查

1. 大模型回答"暂无相关信息"但向量库有数据

  • 原因:相似度阈值设置过高、嵌入模型维度不匹配、检索参数 topK 过小;
  • 解决:降低相似度阈值(如 0.3)、核对嵌入模型维度(如 nomic-embed-text 为 768)、调大 topK 至 10。

2. 大模型回答包含幻觉(编造信息)

  • 原因:Prompt 约束不足、检索结果为空但模型仍生成内容;
  • 解决:强化 Prompt 规则(如"无参考资料时仅回复暂无相关信息")、检索结果为空时直接返回兜底回答,不调用大模型。

3. 多轮对话上下文丢失

  • 原因:ChatMemory 未绑定 userId、记忆窗口过小;
  • 解决:确保每个用户的对话记忆通过 userId 区分,调大 MessageWindowChatMemory 的最大消息数(如 10)。

总结

LangChain4j + Spring Boot 实现大模型与向量数据库协同的核心逻辑是:

  1. 检索:将用户问题向量化,从向量库获取语义相似的知识片段;
  2. 增强:用结构化 Prompt 约束大模型仅基于检索结果回答;
  3. 生成:调用本地大模型生成回答,结合对话记忆实现多轮交互。

该方案完全私有化部署,数据不出内网,适配企业级 RAG 场景的准确性、安全性和性能要求,可直接作为智能客服、内部知识库问答等场景的核心架构。

相关推荐
Coding茶水间2 小时前
基于深度学习的水面垃圾检测系统演示与介绍(YOLOv12/v11/v8/v5模型+Pyqt5界面+训练代码+数据集)
图像处理·人工智能·深度学习·yolo·目标检测·机器学习·计算机视觉
乐迪信息2 小时前
乐迪信息:煤矿皮带区域安全管控:人员违规闯入智能识别
大数据·运维·人工智能·物联网·安全
Dragon水魅2 小时前
使用 LLaMA Factory 微调一个 Qwen3-0.6B 猫娘
人工智能·语言模型
上进小菜猪2 小时前
基于 YOLOv8 的智能火灾识别系统设计与实现— 从数据集训练到 PyQt5 可视化部署的完整工程实践
后端
古城小栈2 小时前
Spring Boot 数据持久化:MyBatis-Plus 分库分表实战指南
spring boot·后端·mybatis
Deepoch2 小时前
Deepoc具身模型开发板:农业机器人的“智能升级模块”革命
人工智能·科技·机器人·采摘机器人·农业机器人·具身模型·deepoc
paopao_wu2 小时前
声音克隆与情感合成:IndexTTS2让AI语音会“演戏”
人工智能
悟能不能悟3 小时前
springboot全局异常
大数据·hive·spring boot
ConardLi3 小时前
AI:我裂开了!现在的大模型评测究竟有多变态?
前端·人工智能·后端