使用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 场景的准确性、安全性和性能要求,可直接作为智能客服、内部知识库问答等场景的核心架构。

相关推荐
冬奇Lab23 分钟前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab24 分钟前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
点光3 小时前
使用Sentinel作为Spring Boot应用限流组件
后端
不要秃头啊4 小时前
别再谈提效了:AI 时代的开发范式本质变了
前端·后端·程序员
AngelPP4 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年4 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
有志4 小时前
Java 项目添加慢 SQL 查询工具实践
后端
九狼4 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS5 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
山佳的山5 小时前
KingbaseES 共享锁(SHARE)与排他锁(EXCLUSIVE)详解及测试复现
后端