Langchain4J+SpringBoot 保姆级教程来了

你好!作为一名Java开发,我也曾看着Python那边的LangChain生态眼馋。不过现在好了,LangChain4j 让我们Java开发者也能优雅地接入大模型了。下面我就把整套方案给你梳理出来,从零到一,保姆级,咱们直接开干!


一、项目初始化

1.1 技术栈版本(建议)

  • JDK 17+(LangChain4j 要求 JDK 17 起步)
  • Spring Boot 3.x
  • MySQL 8.0+
  • Redis Stack 7.x(必须开启 RediSearch 模块,用于向量检索)
  • Maven 3.6+

1.2 核心依赖(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>langchain4j-springboot-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>21</java.version>
        <langchain4j.version>1.0.0-beta3</langchain4j.version>
    </properties>

    <!-- BOM统一管理版本,防止依赖冲突 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- LangChain4j 核心 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
        </dependency>

        <!-- LangChain4j Spring Boot Starter(声明式AI服务、RAG、Tools等) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
        </dependency>

        <!-- OpenAI兼容接口的Spring Boot Starter(支持通义千问等) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
        </dependency>

        <!-- ========== Redis 相关依赖(用于向量存储) ========== -->
        <!-- Spring Boot Redis Starter(提供 Redis 客户端) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- LangChain4j Redis 向量存储 Spring Boot Starter -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-redis-spring-boot-starter</artifactId>
            <version>1.0.1-beta6</version>
        </dependency>

        <!-- MyBatis-Plus(ORM框架,操作数据库) -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.6</version>
        </dependency>

        <!-- MySQL驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok(简化代码) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Jackson(JSON序列化,LangChain4j内部已包含,此处显式引入确保版本一致) -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>
</project>

小贴士langchain4j-community-redis-spring-boot-starter 依赖了 Jedis,如果和 spring-boot-starter-data-redis(默认用 Lettuce)版本冲突,可以手动排除 Jedis 或保持两者并存,实际测试中 Lettuce 和 Jedis 可以共存。


二、配置文件(application.yml)

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/langchain4j_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

  # ============ Redis 配置(用于向量存储) ============
  data:
    redis:
      host: localhost
      port: 6379
      # password: your_redis_password  # 如果有密码则配置
      database: 0
      timeout: 5000ms
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0

# LangChain4j 配置
langchain4j:
  open-ai:
    chat-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1  # 通义千问兼容OpenAI接口
      api-key: sk-your-api-key-here  # 去阿里云百炼申请
      model-name: qwen-max
      log-requests: true
      log-responses: true

# MyBatis-Plus配置
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto

注意:RAG 的向量检索依赖 Redis Stack(带 RediSearch 模块)。如果用 Docker,推荐命令:docker run -d --name redis-vector -p 6379:6379 -p 8001:8001 redis/redis-stack:latest


三、数据库表设计

根据需求,我们需要两张表:会话表(conversation)消息表(message)

-- 创建数据库
CREATE DATABASE IF NOT EXISTS langchain4j_db DEFAULT CHARACTER SET utf8mb4;

USE langchain4j_db;

-- 会话表:存储每个对话会话的元信息
CREATE TABLE conversation (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    conversation_id VARCHAR(64) NOT NULL UNIQUE COMMENT '会话唯一标识(对外暴露)',
    user_id VARCHAR(64) NOT NULL COMMENT '用户ID(用于多用户隔离)',
    title VARCHAR(200) DEFAULT '' COMMENT '会话标题',
    status TINYINT DEFAULT 1 COMMENT '状态:1-活跃 0-已关闭',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    INDEX idx_user_id (user_id),
    INDEX idx_conversation_id (conversation_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话会话表';

-- 消息表:存储每条对话消息
CREATE TABLE message (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    conversation_id VARCHAR(64) NOT NULL COMMENT '所属会话ID',
    role VARCHAR(20) NOT NULL COMMENT '角色:user/assistant/system/tool',
    content TEXT NOT NULL COMMENT '消息内容',
    tool_name VARCHAR(100) DEFAULT '' COMMENT '工具名称(仅tool角色时有值)',
    tool_execution_id VARCHAR(100) DEFAULT '' COMMENT '工具执行ID',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    INDEX idx_conversation_id (conversation_id),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话消息表';

四、实体类与Mapper

4.1 会话实体(Conversation.java)

package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("conversation")
public class Conversation {
    
    @TableId(type = IdType.AUTO)
    private Long id;
    
    @TableField("conversation_id")
    private String conversationId;
    
    @TableField("user_id")
    private String userId;
    
    private String title;
    
    private Integer status;  // 1-活跃 0-已关闭
    
    @TableField("created_at")
    private LocalDateTime createdAt;
    
    @TableField("updated_at")
    private LocalDateTime updatedAt;
}

4.2 消息实体(Message.java)

package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("message")
public class Message {
    
    @TableId(type = IdType.AUTO)
    private Long id;
    
    @TableField("conversation_id")
    private String conversationId;
    
    private String role;  // user / assistant / system / tool
    
    private String content;
    
    @TableField("tool_name")
    private String toolName;
    
    @TableField("tool_execution_id")
    private String toolExecutionId;
    
    @TableField("created_at")
    private LocalDateTime createdAt;
}

4.3 Mapper接口

package com.example.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.Conversation;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ConversationMapper extends BaseMapper<Conversation> {
}
package com.example.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.Message;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MessageMapper extends BaseMapper<Message> {
}

五、服务层(核心业务)

5.1 会话服务(ConversationService.java)

package com.example.demo.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.Conversation;
import com.example.demo.mapper.ConversationMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.UUID;

@Slf4j
@Service
public class ConversationService extends ServiceImpl<ConversationMapper, Conversation> {

    /**
     * 创建新会话
     */
    public Conversation createConversation(String userId, String title) {
        Conversation conversation = new Conversation();
        conversation.setConversationId(UUID.randomUUID().toString().replace("-", ""));
        conversation.setUserId(userId);
        conversation.setTitle(title != null ? title : "新对话");
        conversation.setStatus(1);
        save(conversation);
        log.info("创建会话成功:conversationId={}, userId={}", conversation.getConversationId(), userId);
        return conversation;
    }

    /**
     * 获取用户的会话列表
     */
    public List<Conversation> listByUserId(String userId) {
        LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Conversation::getUserId, userId)
                .orderByDesc(Conversation::getUpdatedAt);
        return list(wrapper);
    }

    /**
     * 关闭会话
     */
    public boolean closeConversation(String conversationId, String userId) {
        LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Conversation::getConversationId, conversationId)
                .eq(Conversation::getUserId, userId);
        Conversation conversation = getOne(wrapper);
        if (conversation == null) {
            return false;
        }
        conversation.setStatus(0);
        return updateById(conversation);
    }
}

5.2 消息服务(MessageService.java)

package com.example.demo.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.Message;
import com.example.demo.mapper.MessageMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
public class MessageService extends ServiceImpl<MessageMapper, Message> {

    /**
     * 保存一条消息
     */
    public void saveMessage(String conversationId, String role, String content) {
        saveMessage(conversationId, role, content, null, null);
    }

    /**
     * 保存一条消息(含工具信息)
     */
    public void saveMessage(String conversationId, String role, String content, 
                            String toolName, String toolExecutionId) {
        Message message = new Message();
        message.setConversationId(conversationId);
        message.setRole(role);
        message.setContent(content);
        message.setToolName(toolName != null ? toolName : "");
        message.setToolExecutionId(toolExecutionId != null ? toolExecutionId : "");
        save(message);
        log.debug("保存消息成功:conversationId={}, role={}, content长度={}", 
                  conversationId, role, content != null ? content.length() : 0);
    }

    /**
     * 查询会话的所有消息(按时间升序)
     */
    public List<Message> listByConversationId(String conversationId) {
        LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Message::getConversationId, conversationId)
                .orderByAsc(Message::getCreatedAt);
        return list(wrapper);
    }
}

六、MySQL持久化的ChatMemoryStore实现

这一步是关键!我们需要实现 ChatMemoryStore 接口,让LangChain4j的对话记忆能持久化到MySQL。

package com.example.demo.store;

import com.example.demo.entity.Message;
import com.example.demo.service.MessageService;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * 基于MySQL的ChatMemoryStore实现
 * 将对话记忆持久化到MySQL,每个会话的消息以JSON数组形式存储
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class MySQLChatMemoryStore implements ChatMemoryStore {

    private final MessageService messageService;

    /**
     * 根据memoryId获取该会话的所有消息
     * memoryId 对应 conversationId
     */
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        String conversationId = memoryId.toString();
        log.debug("从MySQL加载会话消息:conversationId={}", conversationId);
        
        List<Message> messages = messageService.listByConversationId(conversationId);
        List<ChatMessage> chatMessages = new ArrayList<>();
        
        for (Message msg : messages) {
            // 将数据库中的消息反序列化为ChatMessage对象
            String json = String.format(
                "{\"role\":\"%s\",\"text\":\"%s\"}", 
                msg.getRole(), 
                msg.getContent().replace("\"", "\\\"")
            );
            // 使用LangChain4j内置的序列化工具
            ChatMessage chatMessage = ChatMessageDeserializer.messageFromJson(json);
            chatMessages.add(chatMessage);
        }
        
        return chatMessages;
    }

    /**
     * 更新会话的所有消息(全量替换)
     * LangChain4j的ChatMemory在每次对话后都会调用此方法
     */
    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        String conversationId = memoryId.toString();
        log.debug("更新MySQL会话消息:conversationId={}, 消息数={}", conversationId, messages.size());
        
        // 简单起见:先删除该会话所有旧消息,再批量插入新消息
        // 生产环境可优化为增量更新
        messageService.lambdaUpdate()
                .eq(Message::getConversationId, conversationId)
                .remove();
        
        for (ChatMessage msg : messages) {
            String role = msg.type().name().toLowerCase();
            String content = msg.text();
            messageService.saveMessage(conversationId, role, content);
        }
    }

    /**
     * 删除会话的所有消息
     */
    @Override
    public void deleteMessages(Object memoryId) {
        String conversationId = memoryId.toString();
        log.info("删除MySQL会话消息:conversationId={}", conversationId);
        messageService.lambdaUpdate()
                .eq(Message::getConversationId, conversationId)
                .remove();
    }
}

⚠️ 特别说明:上面的 getMessages 方法中,我用了简化的JSON反序列化方式。生产环境中建议使用 ChatMessageSerializerChatMessageDeserializer 配合Jackson来做完整的序列化/反序列化。


七、配置类:组装AI服务

package com.example.demo.config;

import com.example.demo.store.MySQLChatMemoryStore;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.function.Function;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class LangChain4jConfig {

    private final ChatLanguageModel chatModel;
    private final MySQLChatMemoryStore memoryStore;

    /**
     * 基础AI服务(无记忆)
     */
    @Bean
    public SimpleAiService simpleAiService() {
        return AiServices.builder(SimpleAiService.class)
                .chatModel(chatModel)
                .build();
    }

    /**
     * 带提示词模板的AI服务
     */
    @Bean
    public PromptAiService promptAiService() {
        return AiServices.builder(PromptAiService.class)
                .chatModel(chatModel)
                .build();
    }

    /**
     * 带会话记忆的AI服务(保留对话轮次)
     * 使用MessageWindowChatMemory,保留最近N条消息
     */
    @Bean
    public MemoryAiService memoryAiService() {
        return AiServices.builder(MemoryAiService.class)
                .chatModel(chatModel)
                // 每个会话独立记忆,最多保留20条消息
                .chatMemoryProvider(memoryId -> 
                    MessageWindowChatMemory.builder()
                        .id(memoryId)
                        .maxMessages(20)
                        .chatMemoryStore(memoryStore)  // 持久化到MySQL
                        .build()
                )
                .build();
    }

    /**
     * RAG + Tool Calling + 会话管理的综合AI服务
     */
    @Bean
    public AdvancedAiService advancedAiService(
            dev.langchain4j.rag.content.retriever.ContentRetriever contentRetriever,
            List<Object> tools) {
        
        var builder = AiServices.builder(AdvancedAiService.class)
                .chatModel(chatModel)
                // 会话记忆:使用MySQL持久化
                .chatMemoryProvider(memoryId -> 
                    MessageWindowChatMemory.builder()
                        .id(memoryId)
                        .maxMessages(30)
                        .chatMemoryStore(memoryStore)
                        .build()
                )
                // RAG检索增强
                .contentRetriever(contentRetriever);
        
        // 注册工具
        if (tools != null && !tools.isEmpty()) {
            builder.tools(tools.toArray());
        }
        
        return builder.build();
    }
}

八、AI服务接口定义(声明式)

8.1 基础AI服务(普通对话)

package com.example.demo.service.ai;

import dev.langchain4j.service.AiService;
import dev.langchain4j.service.UserMessage;

@AiService
public interface SimpleAiService {
    
    /**
     * 普通对话接口:用户说什么,AI回什么
     */
    String chat(@UserMessage String userMessage);
}

8.2 带提示词模板的AI服务

package com.example.demo.service.ai;

import dev.langchain4j.service.AiService;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;

/**
 * 带提示词模板的AI服务
 * 通过@SystemMessage设定角色,@V绑定变量
 */
@AiService
public interface PromptAiService {

    /**
     * 带系统提示词的对话
     * @param userMessage 用户输入
     * @return AI响应
     */
    @SystemMessage("你是一位资深的Java技术专家,擅长Spring Boot和微服务架构。请用专业且易懂的方式回答问题。")
    String chat(@UserMessage String userMessage);

    /**
     * 带变量的提示词模板
     * @param name 用户名称
     * @param question 用户问题
     * @return AI响应
     */
    @SystemMessage("你是一位{{role}}专家")
    @UserMessage("你好{{name}},请回答:{{question}}")
    String chatWithTemplate(@V("role") String role, 
                            @V("name") String name, 
                            @V("question") String question);
}

8.3 保留对话轮次的AI服务(带记忆)

package com.example.demo.service.ai;

import dev.langchain4j.service.AiService;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;

/**
 * 带会话记忆的AI服务
 * 通过@MemoryId实现多用户/多会话隔离
 */
@AiService
public interface MemoryAiService {

    /**
     * 带记忆的对话
     * @param conversationId 会话ID(用于隔离不同会话的记忆)
     * @param userMessage 用户输入
     * @return AI响应
     */
    String chat(@MemoryId String conversationId, @UserMessage String userMessage);
}

8.4 综合AI服务:RAG + Tool Calling + 会话管理

package com.example.demo.service.ai;

import dev.langchain4j.service.AiService;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;

@AiService
public interface AdvancedAiService {

    /**
     * 综合对话接口:支持RAG检索 + 工具调用 + 会话记忆
     * 
     * @param conversationId 会话ID(用于记忆隔离和持久化)
     * @param userMessage 用户输入
     * @return AI响应(包含检索增强和工具调用的结果)
     */
    @SystemMessage("""
        你是一个智能助手,可以访问知识库和调用工具来帮助用户。
        如果用户的问题涉及专业知识,请优先从知识库中检索相关信息。
        如果需要实时数据或执行特定操作,请调用相应的工具。
        回答要准确、简洁、友好。
        """)
    String chat(@MemoryId String conversationId, @UserMessage String userMessage);
}

九、RAG配置(检索增强生成)- 基于Redis向量存储

RAG需要向量数据库的支持。这里我们选择 Redis Stack(带 RediSearch 模块),相比 PGVector,Redis 能提供亚毫秒级的向量检索速度,非常适合实时对话场景。

9.1 Redis连接配置(可选,用于自定义)

如果默认的 RedisConnectionFactory 自动配置不满足需求,可以手动配置:

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName("localhost");
        config.setPort(6379);
        // config.setPassword(RedisPassword.of("your_password"));
        config.setDatabase(0);
        return new LettuceConnectionFactory(config);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
}

9.2 RAG核心配置类

package com.example.demo.config;

import dev.langchain4j.community.store.embedding.redis.RedisEmbeddingStore;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class RagConfig {

    private final EmbeddingModel embeddingModel;
    private final RedisConnectionFactory redisConnectionFactory;

    /**
     * 配置 Redis 作为向量存储
     * 
     * Redis Stack 必须安装 RediSearch 模块才能支持向量搜索
     * Docker 部署命令:
     *   docker run -d --name redis-vector -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
     */
    @Bean
    public EmbeddingStore<TextSegment> embeddingStore() {
        // 从 RedisConnectionFactory 中获取连接信息
        LettuceConnectionFactory factory = (LettuceConnectionFactory) redisConnectionFactory;
        String host = factory.getHostName();
        int port = factory.getPort();
        String password = factory.getPassword();
        int database = factory.getDatabase();

        log.info("初始化 RedisEmbeddingStore:host={}, port={}, database={}", host, port, database);

        return RedisEmbeddingStore.builder()
                .host(host)
                .port(port)
                .password(password != null ? password : "")
                .database(database)
                // 索引名称,用于在 Redis 中标识向量索引
                .indexName("knowledge_vectors")
                // 向量维度,必须与 EmbeddingModel 的输出维度一致
                // 通义千问 text-embedding-v4 的维度是 1536
                .dimension(1536)
                // 距离度量类型:COSINE(余弦相似度)、EUCLIDEAN(欧氏距离)、IP(内积)
                .distanceMetric(redis.embedding.DistanceMetric.COSINE)
                .build();
    }

    /**
     * 配置内容检索器
     */
    @Bean
    public ContentRetriever contentRetriever(EmbeddingStore<TextSegment> embeddingStore) {
        return EmbeddingStoreContentRetriever.builder()
                .embeddingStore(embeddingStore)
                .embeddingModel(embeddingModel)
                .maxResults(3)   // 最多检索 3 条相关片段
                .minScore(0.7)   // 最低相似度阈值
                .build();
    }

    /**
     * 启动时加载知识文档到 Redis 向量库
     */
    @PostConstruct
    public void loadKnowledgeDocuments(EmbeddingStore<TextSegment> embeddingStore) {
        try {
            // 从 resources/knowledge 目录加载文档
            Path path = Paths.get("src/main/resources/knowledge");
            List<Document> documents = FileSystemDocumentLoader.loadDocuments(
                    path,
                    new TextDocumentParser()
            );

            if (documents.isEmpty()) {
                log.warn("未找到知识文档,跳过加载");
                return;
            }

            // 分割文档并存入 Redis 向量库
            EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                    .documentSplitter(DocumentSplitters.recursive(500, 0))
                    .embeddingModel(embeddingModel)
                    .embeddingStore(embeddingStore)
                    .build();

            ingestor.ingest(documents);
            log.info("知识文档加载完成,共 {} 个文档已存入 Redis", documents.size());

        } catch (Exception e) {
            log.error("加载知识文档到 Redis 失败", e);
        }
    }
}

十、工具类定义(Tool Calling)

package com.example.demo.tool;

import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 天气查询工具
 * 演示Tool Calling的基本用法
 */
@Slf4j
@Component
public class WeatherTool {

    @Tool("查询指定城市的当前天气信息")
    public String getWeather(String city) {
        log.info("调用天气工具:city={}", city);
        
        // 模拟天气数据(实际可调用第三方API)
        String[] weathers = {"晴", "多云", "小雨", "阴天"};
        String weather = weathers[(int) (Math.random() * weathers.length)];
        int temperature = 15 + (int) (Math.random() * 20);
        
        return String.format("【%s】当前天气:%s,温度:%d℃", 
                city, weather, temperature);
    }
}

/**
 * 计算器工具
 */
@Slf4j
@Component
public class CalculatorTool {

    @Tool("计算两个数字的和")
    public double sum(double a, double b) {
        log.info("调用计算工具:{} + {}", a, b);
        return a + b;
    }

    @Tool("计算两个数字的差")
    public double subtract(double a, double b) {
        log.info("调用计算工具:{} - {}", a, b);
        return a - b;
    }

    @Tool("计算两个数字的乘积")
    public double multiply(double a, double b) {
        log.info("调用计算工具:{} × {}", a, b);
        return a * b;
    }
}

/**
 * 时间工具
 */
@Slf4j
@Component
public class DateTimeTool {

    @Tool("获取当前日期和时间")
    public String getCurrentDateTime() {
        log.info("调用时间工具");
        return LocalDateTime.now().format(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
        );
    }
}

十一、Controller接口层

package com.example.demo.controller;

import com.example.demo.entity.Conversation;
import com.example.demo.service.ConversationService;
import com.example.demo.service.ai.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
@Tag(name = "AI对话接口", description = "LangChain4j集成Spring Boot演示")
public class ChatController {

    private final SimpleAiService simpleAiService;
    private final PromptAiService promptAiService;
    private final MemoryAiService memoryAiService;
    private final AdvancedAiService advancedAiService;
    private final ConversationService conversationService;

    // ==================== 1. 普通AI对话接口 ====================
    
    @GetMapping("/simple")
    @Operation(summary = "普通对话", description = "无记忆、无提示词,最简单的AI对话")
    public Map<String, String> simpleChat(
            @RequestParam @Parameter(description = "用户输入") String prompt) {
        log.info("普通对话请求:prompt={}", prompt);
        String result = simpleAiService.chat(prompt);
        return Map.of("response", result);
    }

    // ==================== 2. 带提示词的对话接口 ====================
    
    @GetMapping("/prompt")
    @Operation(summary = "带提示词对话", description = "使用@SystemMessage设定AI角色")
    public Map<String, String> promptChat(
            @RequestParam @Parameter(description = "用户输入") String prompt) {
        log.info("带提示词对话请求:prompt={}", prompt);
        String result = promptAiService.chat(prompt);
        return Map.of("response", result);
    }

    @GetMapping("/prompt/template")
    @Operation(summary = "带模板变量的提示词对话")
    public Map<String, String> promptTemplateChat(
            @RequestParam @Parameter(description = "角色") String role,
            @RequestParam @Parameter(description = "姓名") String name,
            @RequestParam @Parameter(description = "问题") String question) {
        log.info("模板对话请求:role={}, name={}, question={}", role, name, question);
        String result = promptAiService.chatWithTemplate(role, name, question);
        return Map.of("response", result);
    }

    // ==================== 3. 保留对话轮次的接口(带记忆) ====================
    
    @PostMapping("/memory")
    @Operation(summary = "带记忆对话", description = "同一conversationId会记住对话历史")
    public Map<String, String> memoryChat(
            @RequestParam @Parameter(description = "会话ID") String conversationId,
            @RequestParam @Parameter(description = "用户输入") String prompt) {
        log.info("带记忆对话请求:conversationId={}, prompt={}", conversationId, prompt);
        String result = memoryAiService.chat(conversationId, prompt);
        return Map.of("response", result);
    }

    // ==================== 4. 保留会话的接口(会话管理) ====================
    
    @PostMapping("/conversation/create")
    @Operation(summary = "创建新会话")
    public Map<String, Object> createConversation(
            @RequestParam @Parameter(description = "用户ID") String userId,
            @RequestParam(required = false) @Parameter(description = "会话标题") String title) {
        log.info("创建会话请求:userId={}, title={}", userId, title);
        Conversation conversation = conversationService.createConversation(userId, title);
        Map<String, Object> result = new HashMap<>();
        result.put("conversationId", conversation.getConversationId());
        result.put("title", conversation.getTitle());
        result.put("createdAt", conversation.getCreatedAt());
        return result;
    }

    @GetMapping("/conversation/list")
    @Operation(summary = "获取用户会话列表")
    public List<Conversation> listConversations(
            @RequestParam @Parameter(description = "用户ID") String userId) {
        log.info("获取会话列表:userId={}", userId);
        return conversationService.listByUserId(userId);
    }

    @PostMapping("/conversation/close")
    @Operation(summary = "关闭会话")
    public Map<String, Boolean> closeConversation(
            @RequestParam @Parameter(description = "会话ID") String conversationId,
            @RequestParam @Parameter(description = "用户ID") String userId) {
        log.info("关闭会话请求:conversationId={}, userId={}", conversationId, userId);
        boolean result = conversationService.closeConversation(conversationId, userId);
        return Map.of("success", result);
    }

    @PostMapping("/conversation/chat")
    @Operation(summary = "在会话上下文中对话", description = "自动关联会话,保留完整对话历史")
    public Map<String, String> conversationChat(
            @RequestParam @Parameter(description = "会话ID") String conversationId,
            @RequestParam @Parameter(description = "用户输入") String prompt) {
        log.info("会话对话请求:conversationId={}, prompt={}", conversationId, prompt);
        // 使用memoryAiService,conversationId作为@MemoryId
        String result = memoryAiService.chat(conversationId, prompt);
        return Map.of("response", result);
    }

    // ==================== 5. 综合接口:RAG + Tool Calling + 会话管理 ====================
    
    @PostMapping("/advanced")
    @Operation(summary = "综合AI对话", description = "RAG检索增强 + 工具调用 + 会话记忆持久化")
    public Map<String, String> advancedChat(
            @RequestParam @Parameter(description = "会话ID") String conversationId,
            @RequestParam @Parameter(description = "用户输入") String prompt) {
        log.info("综合对话请求:conversationId={}, prompt={}", conversationId, prompt);
        long startTime = System.currentTimeMillis();
        
        String result = advancedAiService.chat(conversationId, prompt);
        
        long costTime = System.currentTimeMillis() - startTime;
        log.info("综合对话完成,耗时:{}ms", costTime);
        
        Map<String, String> response = new HashMap<>();
        response.put("response", result);
        response.put("costTime", costTime + "ms");
        return response;
    }
}

十二、Spring Boot启动类

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LangChain4jDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(LangChain4jDemoApplication.class, args);
        System.out.println("╔══════════════════════════════════════════════════════════╗");
        System.out.println("║   🚀 LangChain4j + Spring Boot 集成成功!              ║");
        System.out.println("║   📌 访问 http://localhost:8080/api/chat/simple 试试    ║");
        System.out.println("║   🧠 向量存储:Redis Stack (RediSearch)                ║");
        System.out.println("╚══════════════════════════════════════════════════════════╝");
    }
}

十三、接口测试示例

接口 方法 说明 示例
/api/chat/simple?prompt=你好 GET 普通对话 返回AI基础回答
/api/chat/prompt?prompt=什么是微服务 GET 带提示词 AI以"Java技术专家"身份回答
/api/chat/memory?conversationId=xxx&prompt=我叫张三 POST 带记忆 AI记住你是谁
/api/chat/conversation/create?userId=user001&title=技术咨询 POST 创建会话 返回conversationId
/api/chat/conversation/chat?conversationId=xxx&prompt=继续刚才的话题 POST 会话对话 基于历史上下文回答
/api/chat/advanced?conversationId=xxx&prompt=帮我查下今天的天气 POST 综合接口 RAG+工具调用+记忆

十四、生产避坑指南(Redis 特别版)

1. 会话记忆 vs 历史记录

LangChain4j提供的ChatMemory是服务于大模型的"短期记忆",用于拼接上下文。而"历史记录"是面向用户展示的完整对话流水,需要你手动维护message表中。本文的方案中,MySQLChatMemoryStoreupdateMessages是全量替换,生产环境建议改成增量追加

2. Redis 向量维度必须匹配

RedisEmbeddingStore.builder().dimension(1536) 中的维度必须与你使用的 EmbeddingModel 输出维度一致。通义千问 text-embedding-v4 是 1536 维,如果用其他模型(如 OpenAI text-embedding-ada-002 是 1536 维,text-embedding-3-small 是 1536 维),请相应调整。

3. Redis Stack 部署检查

Redis 原生不支持向量搜索,必须安装 RediSearch 模块。检查方法:连接 Redis 后执行 MODULE LIST,看是否包含 search。推荐直接使用 redis/redis-stack 镜像,一步到位。

4. 工具描述要清晰

@Tool注解的value描述一定要写清楚,AI能否正确调用工具全看这个描述。

5. 会话隔离

使用@MemoryId注解标识会话ID,不同会话的记忆互不干扰。多用户场景下,建议用userId + conversationId组合作为记忆ID。

6. 向量检索的实时性

Redis 向量检索基于内存,速度极快,适合对延迟敏感的实时对话场景。但如果知识库非常大(超过百万级向量),需要考虑内存容量规划。

7. 版本兼容性(重要)

LangChain4j从0.36.0起要求JDK 17。langchain4j-community-redis-spring-boot-starter 是社区模块,建议使用与 BOM 版本匹配的版本(本文使用 1.0.1-beta6)。如果遇到 Jedis/Lettuce 版本冲突,可以在 pom.xml 中显式排除。


好了,整套方案就这些了。从最简单的 Hello World 到综合的 RAG+Tool Calling+会话管理,向量存储也换成了更轻量快速的 Redis,该有的都有了。代码可以直接复制到项目里跑,有问题随时交流!

posted @ 2026-08-03 10:02  佛祖让我来巡山  阅读(98)  评论(0)    收藏  举报

佛祖让我来巡山博客站 - 创建于 2018-08-15

开发工程师个人站,内容主要是网站开发方面的技术文章,大部分来自学习或工作,部分来源于网络,希望对大家有所帮助。

Bootstrap中文网