你好!作为一名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
<?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)
yaml
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)。
sql
-- 创建数据库
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)
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)
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接口
java
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> {
}
java
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)
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)
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。
java
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反序列化方式。生产环境中建议使用ChatMessageSerializer和ChatMessageDeserializer配合Jackson来做完整的序列化/反序列化。
七、配置类:组装AI服务
java
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服务(普通对话)
java
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服务
java
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服务(带记忆)
java
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 + 会话管理
java
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 自动配置不满足需求,可以手动配置:
java
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核心配置类
java
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)
java
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接口层
java
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启动类
java
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表中。本文的方案中,MySQLChatMemoryStore的updateMessages是全量替换,生产环境建议改成增量追加。
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,该有的都有了。代码可以直接复制到项目里跑,有问题随时交流!