LangChain4j + Spring项目保姆级教程
大家好!今天咱们来聊聊如何在普通Spring项目 中集成LangChain4j,一步步构建一个功能完整的AI对话应用。
为啥要强调“普通Spring项目”?因为网上绝大多数教程都是Spring Boot的,但现实中很多老项目还是基于XML配置的Spring MVC,或者出于各种原因没法用Spring Boot。咱们今天就专门给这部分同学写一份能直接用的教程。
废话不多说,直接开整!
一、项目概览
咱们要实现的5个功能,从简单到复杂层层递进:
| 序号 | 功能 | 说明 |
|---|---|---|
| 1 | 普通AI对话接口 | 最简单的“一问一答” |
| 2 | 带提示词的对话接口 | 通过SystemMessage设定AI角色 |
| 3 | 保留对话轮次的接口 | 单会话内记住上下文 |
| 4 | 保留会话的接口 | 多会话隔离,每个会话独立记忆 |
| 5 | RAG + Tool Calling + 保留会话及消息 | 综合实战,企业级能力 |
会话和会话消息会持久化到MySQL的两张表中,重启服务也不丢数据。
技术栈:
- Spring 5.x(普通Spring,非Boot)
- LangChain4j 0.35.0
- MySQL 8.0
- MyBatis 3.5.x
- JDK 17+(LangChain4j 0.36+要求JDK 17,咱们用0.35.0兼容性更好)
二、项目结构
先看一眼整体结构,心里有个数:
langchain4j-spring-demo/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ ├── config/
│ │ │ │ ├── SpringConfig.java # Spring配置(Java Config)
│ │ │ │ └── LangChain4jConfig.java # LangChain4j相关Bean配置
│ │ │ ├── controller/
│ │ │ │ └── ChatController.java # 对话接口控制器
│ │ │ ├── service/
│ │ │ │ ├── ChatService.java # 对话服务接口
│ │ │ │ └── ChatServiceImpl.java # 对话服务实现
│ │ │ ├── memory/
│ │ │ │ └── MysqlChatMemoryStore.java # MySQL持久化记忆存储
│ │ │ ├── rag/
│ │ │ │ ├── DocumentLoader.java # 文档加载器
│ │ │ │ └── RAGService.java # RAG服务
│ │ │ ├── tool/
│ │ │ │ └── MathTool.java # 工具类(数学计算)
│ │ │ ├── entity/
│ │ │ │ ├── ChatSession.java # 会话实体
│ │ │ │ └── ChatMessage.java # 消息实体
│ │ │ └── mapper/
│ │ │ ├── ChatSessionMapper.java
│ │ │ └── ChatMessageMapper.java
│ │ ├── resources/
│ │ │ ├── spring/
│ │ │ │ └── applicationContext.xml # Spring XML配置(可选)
│ │ │ ├── mapper/
│ │ │ │ ├── ChatSessionMapper.xml
│ │ │ │ └── ChatMessageMapper.xml
│ │ │ └── config.properties # 配置文件
│ │ └── webapp/
│ │ └── WEB-INF/
│ │ └── web.xml
│ └── test/
│ └── java/
│ └── com/
│ └── example/
│ └── test/
│ └── ChatServiceTest.java
三、依赖配置(pom.xml)
普通Spring项目没有Spring Boot的starter自动配置,所以咱们需要手动引入所有依赖:
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>langchain4j-spring-demo</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<spring.version>5.3.27</spring.version>
<langchain4j.version>0.35.0</langchain4j.version>
</properties>
<dependencies>
<!-- ============ Spring 核心依赖 ============ -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- ============ LangChain4j 核心 ============ -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- ============ OpenAI 集成(用官方SDK方式) ============ -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- ============ 嵌入模型(RAG需要) ============ -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- ============ MyBatis + MySQL ============ -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.18</version>
</dependency>
<!-- ============ JSON 序列化 ============ -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- ============ 日志 ============ -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.36</version>
</dependency>
<!-- ============ Servlet API ============ -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- ============ 工具类 ============ -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!-- ============ 测试 ============ -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>langchain4j-demo</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>
💡 小贴士:LangChain4j 0.35.0 支持 JDK 8+,如果你们项目还在用 JDK 8,用这个版本没问题。但新项目建议直接上 JDK 17+,后面升级框架也方便。
四、配置文件
4.1 config.properties
在 src/main/resources/config.properties 中配置模型参数和数据库参数:
# ============ OpenAI 配置 ============
openai.api.key=sk-your-api-key-here
openai.base.url=https://api.openai.com/v1
openai.model.name=gpt-4o-mini
openai.temperature=0.7
openai.max.tokens=2048
# ============ 数据库配置 ============
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/langchain4j_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
db.username=root
db.password=123456
db.max.active=20
db.initial.size=5
# ============ 对话记忆配置 ============
chat.memory.max.messages=20
4.2 数据库表结构
执行以下SQL创建两张表:
-- 创建数据库
CREATE DATABASE IF NOT EXISTS langchain4j_demo
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
USE langchain4j_demo;
-- ============ 会话表 ============
CREATE TABLE IF NOT EXISTS chat_session (
id VARCHAR(64) PRIMARY KEY COMMENT '会话ID',
session_name VARCHAR(255) DEFAULT NULL COMMENT '会话名称(可选)',
user_id VARCHAR(64) DEFAULT NULL COMMENT '用户ID(用于多用户隔离)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
is_active TINYINT(1) DEFAULT 1 COMMENT '是否活跃',
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI对话会话表';
-- ============ 消息表 ============
CREATE TABLE IF NOT EXISTS chat_message (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '消息ID',
session_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 NULL COMMENT '工具名称(如果是工具调用)',
tool_execution_id VARCHAR(100) DEFAULT NULL COMMENT '工具执行ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_session_id (session_id),
INDEX idx_created_at (created_at),
FOREIGN KEY (session_id) REFERENCES chat_session(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='对话消息表';
📌 设计说明:LangChain4j 的 ChatMemory 存储的是“记忆”(可能经过裁剪、摘要),而咱们这张
chat_message表存的是完整的“历史记录”。两件事分开做:chat_message存全量历史供用户查询,ChatMemory 存裁剪后的上下文供模型使用。这就是“历史”和“记忆”的区别。
五、Spring配置
普通Spring项目我们采用 Java Config + XML混合 的方式,既保留XML的灵活性,又享受Java Config的类型安全。
5.1 SpringConfig.java - 核心Spring配置
package com.example.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import javax.sql.DataSource;
import java.util.Objects;
import java.util.Properties;
/**
* Spring核心配置类
* 相当于applicationContext.xml的Java版本
*/
@Configuration
@EnableWebMvc
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.example"})
@PropertySource(value = {"classpath:config.properties"}, ignoreResourceNotFound = false)
public class SpringConfig {
/**
* 配置Druid数据源
*/
@Bean
public DataSource dataSource(Environment env) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(env.getProperty("db.driver"));
dataSource.setUrl(env.getProperty("db.url"));
dataSource.setUsername(env.getProperty("db.username"));
dataSource.setPassword(env.getProperty("db.password"));
dataSource.setMaxActive(Integer.parseInt(Objects.requireNonNull(env.getProperty("db.max.active"))));
dataSource.setInitialSize(Integer.parseInt(Objects.requireNonNull(env.getProperty("db.initial.size"))));
// Druid监控配置(生产环境建议开启)
Properties props = new Properties();
props.setProperty("druid.stat.mergeSql", "true");
props.setProperty("druid.stat.slowSqlMillis", "5000");
dataSource.setConnectProperties(props);
return dataSource;
}
/**
* 配置MyBatis SqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeAliasesPackage("com.example.entity");
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
);
// 配置MyBatis设置
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true); // 下划线转驼峰
configuration.setLogImpl(org.apache.ibatis.logging.log4j.Log4jImpl.class);
factoryBean.setConfiguration(configuration);
return factoryBean.getObject();
}
/**
* Mapper扫描
*/
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer scanner = new MapperScannerConfigurer();
scanner.setBasePackage("com.example.mapper");
scanner.setSqlSessionFactoryBeanName("sqlSessionFactory");
return scanner;
}
/**
* 事务管理器
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
/**
* 视图解析器(如果你需要返回JSP页面)
*/
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}
5.2 LangChain4jConfig.java - LangChain4j专属配置
package com.example.config;
import com.example.memory.MysqlChatMemoryStore;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import java.time.Duration;
/**
* LangChain4j 配置类
* 手动创建所有LangChain4j相关的Bean
*/
@Configuration
public class LangChain4jConfig {
/**
* 创建ChatLanguageModel - 与大模型对话的底层API
*
* ChatLanguageModel是LangChain4j中与LLM交互的低级API,
* 提供最大的能力和灵活性
*/
@Bean
public ChatLanguageModel chatLanguageModel(Environment env) {
return OpenAiChatModel.builder()
.apiKey(env.getProperty("openai.api.key"))
.baseUrl(env.getProperty("openai.base.url"))
.modelName(env.getProperty("openai.model.name"))
.temperature(Double.parseDouble(env.getProperty("openai.temperature", "0.7")))
.maxTokens(Integer.parseInt(env.getProperty("openai.max.tokens", "2048")))
.timeout(Duration.ofSeconds(60))
.logRequests(true) // 开发阶段开启,方便调试
.logResponses(true)
.build();
}
/**
* 创建ChatMemoryStore - MySQL持久化存储
*
* 默认情况下ChatMemory将消息存储在内存中。
* 如果需要持久化,可以实现自定义ChatMemoryStore
*/
@Bean
public ChatMemoryStore chatMemoryStore() {
return new MysqlChatMemoryStore();
}
/**
* 创建默认的ChatMemory(用于单会话场景)
* 使用滑动窗口,保留最近N条消息
*/
@Bean
public MessageWindowChatMemory defaultChatMemory(ChatMemoryStore chatMemoryStore) {
return MessageWindowChatMemory.builder()
.id("default-session") // 固定ID,所有请求共享同一个记忆
.maxMessages(20) // 保留最近20条消息
.chatMemoryStore(chatMemoryStore)
.build();
}
}
5.3 web.xml(如果是Servlet容器部署)
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>LangChain4j Spring Demo</display-name>
<!-- 配置Spring上下文 -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.example.config.SpringConfig</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.example.config.SpringConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
<!-- 字符编码过滤器 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
六、实体类与Mapper
6.1 ChatSession.java
package com.example.entity;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 会话实体
*/
@Data
public class ChatSession {
private String id; // 会话ID(UUID)
private String sessionName; // 会话名称
private String userId; // 用户ID
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Integer isActive; // 1-活跃 0-已删除
}
6.2 ChatMessage.java
package com.example.entity;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 消息实体
*/
@Data
public class ChatMessage {
private Long id;
private String sessionId;
private String role; // user / assistant / system / tool
private String content;
private String toolName;
private String toolExecutionId;
private LocalDateTime createdAt;
}
6.3 ChatSessionMapper.java
package com.example.mapper;
import com.example.entity.ChatSession;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface ChatSessionMapper {
/** 插入会话 */
int insert(ChatSession session);
/** 根据ID查询 */
ChatSession selectById(@Param("id") String id);
/** 根据用户ID查询所有活跃会话 */
List<ChatSession> selectByUserId(@Param("userId") String userId);
/** 更新会话 */
int update(ChatSession session);
/** 逻辑删除会话 */
int deleteById(@Param("id") String id);
}
6.4 ChatMessageMapper.java
package com.example.mapper;
import com.example.entity.ChatMessage;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface ChatMessageMapper {
/** 插入消息 */
int insert(ChatMessage message);
/** 批量插入消息 */
int batchInsert(@Param("list") List<ChatMessage> messages);
/** 根据会话ID查询所有消息(按时间升序) */
List<ChatMessage> selectBySessionId(@Param("sessionId") String sessionId);
/** 根据会话ID查询最近N条消息 */
List<ChatMessage> selectRecentBySessionId(@Param("sessionId") String sessionId,
@Param("limit") int limit);
/** 删除会话的所有消息 */
int deleteBySessionId(@Param("sessionId") String sessionId);
}
6.5 Mapper XML
ChatSessionMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.ChatSessionMapper">
<resultMap id="BaseResultMap" type="com.example.entity.ChatSession">
<id column="id" property="id"/>
<result column="session_name" property="sessionName"/>
<result column="user_id" property="userId"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
<result column="is_active" property="isActive"/>
</resultMap>
<insert id="insert" parameterType="com.example.entity.ChatSession">
INSERT INTO chat_session (id, session_name, user_id, is_active)
VALUES (#{id}, #{sessionName}, #{userId}, #{isActive})
</insert>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM chat_session
WHERE id = #{id} AND is_active = 1
</select>
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT * FROM chat_session
WHERE user_id = #{userId} AND is_active = 1
ORDER BY updated_at DESC
</select>
<update id="update" parameterType="com.example.entity.ChatSession">
UPDATE chat_session
<set>
<if test="sessionName != null">session_name = #{sessionName},</if>
<if test="isActive != null">is_active = #{isActive},</if>
</set>
WHERE id = #{id}
</update>
<update id="deleteById">
UPDATE chat_session SET is_active = 0
WHERE id = #{id}
</update>
</mapper>
ChatMessageMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.ChatMessageMapper">
<resultMap id="BaseResultMap" type="com.example.entity.ChatMessage">
<id column="id" property="id"/>
<result column="session_id" property="sessionId"/>
<result column="role" property="role"/>
<result column="content" property="content"/>
<result column="tool_name" property="toolName"/>
<result column="tool_execution_id" property="toolExecutionId"/>
<result column="created_at" property="createdAt"/>
</resultMap>
<insert id="insert" parameterType="com.example.entity.ChatMessage" useGeneratedKeys="true" keyProperty="id">
INSERT INTO chat_message (session_id, role, content, tool_name, tool_execution_id)
VALUES (#{sessionId}, #{role}, #{content}, #{toolName}, #{toolExecutionId})
</insert>
<insert id="batchInsert" parameterType="list">
INSERT INTO chat_message (session_id, role, content, tool_name, tool_execution_id)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.sessionId}, #{item.role}, #{item.content}, #{item.toolName}, #{item.toolExecutionId})
</foreach>
</insert>
<select id="selectBySessionId" resultMap="BaseResultMap">
SELECT * FROM chat_message
WHERE session_id = #{sessionId}
ORDER BY created_at ASC
</select>
<select id="selectRecentBySessionId" resultMap="BaseResultMap">
SELECT * FROM chat_message
WHERE session_id = #{sessionId}
ORDER BY created_at DESC
LIMIT #{limit}
</select>
<delete id="deleteBySessionId">
DELETE FROM chat_message WHERE session_id = #{sessionId}
</delete>
</mapper>
七、MySQL持久化记忆存储(核心)
这是整个教程最关键的部分——实现 ChatMemoryStore 接口,把对话记忆持久化到MySQL。
LangChain4j 的 ChatMemoryStore 接口有三个方法:
getMessages(Object memoryId)- 根据记忆ID获取消息列表updateMessages(Object memoryId, List<ChatMessage> messages)- 更新消息列表deleteMessages(Object memoryId)- 删除消息
package com.example.memory;
import com.example.entity.ChatMessage;
import com.example.mapper.ChatMessageMapper;
import com.example.mapper.ChatSessionMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* MySQL持久化的ChatMemoryStore实现
*
* 将LangChain4j的ChatMessage序列化为JSON存储在MySQL中
*
* 注意:这里存储的是"记忆"(可能经过裁剪),而chat_message表存的是完整"历史"
*/
@Slf4j
@Component
public class MysqlChatMemoryStore implements ChatMemoryStore {
@Autowired
private ChatMessageMapper chatMessageMapper;
@Autowired
private ChatSessionMapper chatSessionMapper;
private final ObjectMapper objectMapper;
public MysqlChatMemoryStore() {
this.objectMapper = new ObjectMapper();
this.objectMapper.registerModule(new JavaTimeModule());
}
/**
* 根据memoryId获取消息列表
* memoryId就是sessionId
*/
@Override
public List<dev.langchain4j.data.message.ChatMessage> getMessages(Object memoryId) {
String sessionId = memoryId.toString();
log.debug("从MySQL加载记忆: sessionId={}", sessionId);
// 从数据库查询该会话的所有消息
List<ChatMessage> dbMessages = chatMessageMapper.selectBySessionId(sessionId);
if (dbMessages == null || dbMessages.isEmpty()) {
return new ArrayList<>();
}
// 将数据库记录转换为LangChain4j的ChatMessage
List<dev.langchain4j.data.message.ChatMessage> messages = new ArrayList<>();
for (ChatMessage dbMsg : dbMessages) {
try {
// 用LangChain4j提供的序列化工具反序列化
// 注意:我们存储的是完整的消息JSON,可以直接反序列化
String json = dbMsg.getContent();
// 如果是简单文本,需要包装成对应的消息类型
dev.langchain4j.data.message.ChatMessage msg = deserializeMessage(dbMsg);
if (msg != null) {
messages.add(msg);
}
} catch (Exception e) {
log.error("反序列化消息失败: id={}", dbMsg.getId(), e);
}
}
log.debug("加载了 {} 条记忆", messages.size());
return messages;
}
/**
* 更新消息列表(全量替换)
*/
@Override
public void updateMessages(Object memoryId, List<dev.langchain4j.data.message.ChatMessage> messages) {
String sessionId = memoryId.toString();
log.debug("更新记忆: sessionId={}, 消息数={}", sessionId, messages.size());
// 先删除旧消息
chatMessageMapper.deleteBySessionId(sessionId);
if (messages == null || messages.isEmpty()) {
return;
}
// 批量插入新消息
List<ChatMessage> dbMessages = new ArrayList<>();
for (dev.langchain4j.data.message.ChatMessage msg : messages) {
ChatMessage dbMsg = new ChatMessage();
dbMsg.setSessionId(sessionId);
dbMsg.setRole(getRoleName(msg));
dbMsg.setContent(serializeMessage(msg));
dbMsg.setCreatedAt(LocalDateTime.now());
dbMessages.add(dbMsg);
}
if (!dbMessages.isEmpty()) {
chatMessageMapper.batchInsert(dbMessages);
}
}
/**
* 删除消息
*/
@Override
public void deleteMessages(Object memoryId) {
String sessionId = memoryId.toString();
log.debug("删除记忆: sessionId={}", sessionId);
chatMessageMapper.deleteBySessionId(sessionId);
}
// ============ 辅助方法 ============
/**
* 序列化ChatMessage为JSON
* LangChain4j提供了ChatMessageSerializer工具类
*/
private String serializeMessage(dev.langchain4j.data.message.ChatMessage message) {
try {
return ChatMessageSerializer.messageToJson(message);
} catch (Exception e) {
log.error("序列化消息失败", e);
// 降级方案:只存文本
return "{\"text\":\"" + message.text() + "\"}";
}
}
/**
* 从数据库记录反序列化为ChatMessage
*/
private dev.langchain4j.data.message.ChatMessage deserializeMessage(ChatMessage dbMsg) {
try {
// 尝试用LangChain4j的序列化工具反序列化
return ChatMessageDeserializer.messageFromJson(dbMsg.getContent());
} catch (Exception e) {
log.warn("反序列化失败,尝试简单文本解析: {}", e.getMessage());
// 降级方案:根据角色构造简单消息
String role = dbMsg.getRole();
String content = dbMsg.getContent();
if ("user".equals(role)) {
return UserMessage.from(content);
} else if ("system".equals(role)) {
return SystemMessage.from(content);
} else if ("assistant".equals(role)) {
return AiMessage.from(content);
}
return null;
}
}
/**
* 获取消息角色名称
*/
private String getRoleName(dev.langchain4j.data.message.ChatMessage message) {
if (message instanceof UserMessage) {
return "user";
} else if (message instanceof SystemMessage) {
return "system";
} else if (message instanceof AiMessage) {
return "assistant";
} else {
return "unknown";
}
}
/**
* 创建新会话
*/
public String createSession(String userId, String sessionName) {
String sessionId = UUID.randomUUID().toString();
com.example.entity.ChatSession session = new com.example.entity.ChatSession();
session.setId(sessionId);
session.setUserId(userId);
session.setSessionName(sessionName != null ? sessionName : "新对话");
session.setIsActive(1);
chatSessionMapper.insert(session);
return sessionId;
}
}
八、功能1:普通AI对话接口
最简单的“一问一答”,不保留任何上下文。
ChatService.java
package com.example.service;
/**
* 对话服务接口
*/
public interface ChatService {
/**
* 功能1:普通对话 - 单次问答,无上下文
*/
String simpleChat(String userMessage);
/**
* 功能2:带提示词的对话
*/
String chatWithPrompt(String systemPrompt, String userMessage);
/**
* 功能3:保留对话轮次的对话(单会话)
*/
String chatWithMemory(String userMessage);
/**
* 功能4:多会话对话(每个会话独立记忆)
*/
String chatWithSession(String sessionId, String userMessage);
/**
* 功能5:综合 - RAG + Tool Calling + 会话持久化
*/
String chatWithRagAndTools(String sessionId, String userMessage);
}
ChatServiceImpl.java - 功能1实现
package com.example.service;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 对话服务实现
*/
@Slf4j
@Service
public class ChatServiceImpl implements ChatService {
@Autowired
private ChatLanguageModel chatModel;
// ================================================================
// 功能1:普通AI对话接口 - 最简单的"一问一答"
// ================================================================
@Override
public String simpleChat(String userMessage) {
log.info("【功能1】普通对话: {}", userMessage);
try {
// 直接用ChatLanguageModel的便捷方法
String response = chatModel.chat(userMessage);
log.info("【功能1】响应: {}", response);
return response;
} catch (Exception e) {
log.error("对话失败", e);
return "抱歉,AI服务暂时不可用:" + e.getMessage();
}
}
}
ChatController.java - 功能1接口
package com.example.controller;
import com.example.service.ChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/chat")
public class ChatController {
@Autowired
private ChatService chatService;
// ================================================================
// 功能1:普通AI对话接口
// ================================================================
@PostMapping("/simple")
public Map<String, Object> simpleChat(@RequestBody Map<String, String> request) {
String message = request.get("message");
Map<String, Object> result = new HashMap<>();
if (message == null || message.trim().isEmpty()) {
result.put("code", 400);
result.put("message", "消息不能为空");
return result;
}
try {
String response = chatService.simpleChat(message);
result.put("code", 200);
result.put("data", response);
} catch (Exception e) {
result.put("code", 500);
result.put("message", e.getMessage());
}
return result;
}
}
测试:
curl -X POST http://localhost:8080/api/chat/simple \
-H "Content-Type: application/json" \
-d '{"message": "你好,请介绍一下自己"}'
九、功能2:带提示词的对话接口
通过 SystemMessage 设定AI的角色和行为。
ChatServiceImpl.java - 功能2实现
// ================================================================
// 功能2:带提示词的对话接口
// ================================================================
@Override
public String chatWithPrompt(String systemPrompt, String userMessage) {
log.info("【功能2】带提示词对话 - 系统提示: {}, 用户: {}", systemPrompt, userMessage);
try {
// 创建SystemMessage和UserMessage
SystemMessage systemMsg = SystemMessage.from(systemPrompt);
UserMessage userMsg = UserMessage.from(userMessage);
// 发送消息列表
ChatResponse response = chatModel.chat(systemMsg, userMsg);
String answer = response.aiMessage().text();
log.info("【功能2】响应: {}", answer);
return answer;
} catch (Exception e) {
log.error("带提示词对话失败", e);
return "抱歉,AI服务暂时不可用:" + e.getMessage();
}
}
ChatController.java - 功能2接口
// ================================================================
// 功能2:带提示词的对话接口
// ================================================================
@PostMapping("/with-prompt")
public Map<String, Object> chatWithPrompt(@RequestBody Map<String, String> request) {
String systemPrompt = request.get("systemPrompt");
String userMessage = request.get("message");
Map<String, Object> result = new HashMap<>();
if (userMessage == null || userMessage.trim().isEmpty()) {
result.put("code", 400);
result.put("message", "消息不能为空");
return result;
}
// 如果没传系统提示词,给个默认的
if (systemPrompt == null || systemPrompt.trim().isEmpty()) {
systemPrompt = "你是一个友好的AI助手,请用简洁、清晰的语言回答问题。";
}
try {
String response = chatService.chatWithPrompt(systemPrompt, userMessage);
result.put("code", 200);
result.put("data", response);
} catch (Exception e) {
result.put("code", 500);
result.put("message", e.getMessage());
}
return result;
}
测试:
curl -X POST http://localhost:8080/api/chat/with-prompt \
-H "Content-Type: application/json" \
-d '{"systemPrompt": "你是一位资深Java架构师,请用专业但不失通俗的方式回答问题", "message": "什么是微服务"}'
十、功能3:保留对话轮次的接口
用 ChatMemory 在单会话内记住上下文。
ChatServiceImpl.java - 功能3实现
@Autowired
private MessageWindowChatMemory defaultChatMemory; // 从配置中注入
// ================================================================
// 功能3:保留对话轮次的对话(单会话)
// ================================================================
@Override
public String chatWithMemory(String userMessage) {
log.info("【功能3】带记忆对话: {}", userMessage);
try {
// 将用户消息加入记忆
defaultChatMemory.add(UserMessage.from(userMessage));
// 获取当前所有记忆中的消息
List<dev.langchain4j.data.message.ChatMessage> history = defaultChatMemory.messages();
log.debug("当前记忆中有 {} 条消息", history.size());
// 发送完整的历史消息给模型
ChatResponse response = chatModel.chat(history);
String answer = response.aiMessage().text();
// 将AI的回复也加入记忆
defaultChatMemory.add(AiMessage.from(answer));
log.info("【功能3】响应: {}", answer);
return answer;
} catch (Exception e) {
log.error("带记忆对话失败", e);
return "抱歉,AI服务暂时不可用:" + e.getMessage();
}
}
ChatController.java - 功能3接口
// ================================================================
// 功能3:保留对话轮次的接口
// ================================================================
@PostMapping("/with-memory")
public Map<String, Object> chatWithMemory(@RequestBody Map<String, String> request) {
String message = request.get("message");
Map<String, Object> result = new HashMap<>();
if (message == null || message.trim().isEmpty()) {
result.put("code", 400);
result.put("message", "消息不能为空");
return result;
}
try {
String response = chatService.chatWithMemory(message);
result.put("code", 200);
result.put("data", response);
} catch (Exception e) {
result.put("code", 500);
result.put("message", e.getMessage());
}
return result;
}
测试(连续对话):
# 第一轮
curl -X POST http://localhost:8080/api/chat/with-memory \
-H "Content-Type: application/json" \
-d '{"message": "我叫张三"}'
# 第二轮 - AI应该还记得你叫张三
curl -X POST http://localhost:8080/api/chat/with-memory \
-H "Content-Type: application/json" \
-d '{"message": "我叫什么名字?"}'
十一、功能4:保留会话的接口
多会话隔离,每个会话有独立的记忆。通过 @MemoryId 注解实现用户/会话隔离。
ChatServiceImpl.java - 功能4实现
@Autowired
private ChatMemoryStore chatMemoryStore;
// 会话记忆缓存(生产环境建议用更健壮的缓存)
private final Map<String, MessageWindowChatMemory> sessionMemoryCache = new ConcurrentHashMap<>();
// ================================================================
// 功能4:多会话对话(每个会话独立记忆)
// ================================================================
@Override
public String chatWithSession(String sessionId, String userMessage) {
log.info("【功能4】会话对话: sessionId={}, message={}", sessionId, userMessage);
try {
// 获取或创建该会话的记忆
MessageWindowChatMemory memory = sessionMemoryCache.computeIfAbsent(sessionId, id -> {
log.info("创建新会话记忆: {}", id);
return MessageWindowChatMemory.builder()
.id(id)
.maxMessages(20) // 保留最近20条
.chatMemoryStore(chatMemoryStore) // MySQL持久化
.build();
});
// 添加用户消息
memory.add(UserMessage.from(userMessage));
// 获取历史并发送
List<dev.langchain4j.data.message.ChatMessage> history = memory.messages();
ChatResponse response = chatModel.chat(history);
String answer = response.aiMessage().text();
// 添加AI回复
memory.add(AiMessage.from(answer));
log.info("【功能4】响应: {}", answer);
return answer;
} catch (Exception e) {
log.error("会话对话失败", e);
return "抱歉,AI服务暂时不可用:" + e.getMessage();
}
}
/**
* 创建新会话
*/
public String createNewSession(String userId, String sessionName) {
if (chatMemoryStore instanceof MysqlChatMemoryStore) {
return ((MysqlChatMemoryStore) chatMemoryStore).createSession(userId, sessionName);
}
return UUID.randomUUID().toString();
}
ChatController.java - 功能4接口
// ================================================================
// 功能4:保留会话的接口
// ================================================================
@PostMapping("/session")
public Map<String, Object> chatWithSession(@RequestBody Map<String, String> request) {
String sessionId = request.get("sessionId");
String message = request.get("message");
Map<String, Object> result = new HashMap<>();
if (message == null || message.trim().isEmpty()) {
result.put("code", 400);
result.put("message", "消息不能为空");
return result;
}
// 如果没有传sessionId,自动创建
if (sessionId == null || sessionId.trim().isEmpty()) {
sessionId = chatService.createNewSession("default-user", null);
result.put("sessionId", sessionId);
}
try {
String response = chatService.chatWithSession(sessionId, message);
result.put("code", 200);
result.put("data", response);
result.put("sessionId", sessionId);
} catch (Exception e) {
result.put("code", 500);
result.put("message", e.getMessage());
}
return result;
}
/**
* 获取会话历史
*/
@GetMapping("/session/{sessionId}/history")
public Map<String, Object> getSessionHistory(@PathVariable String sessionId) {
Map<String, Object> result = new HashMap<>();
try {
List<com.example.entity.ChatMessage> messages =
chatMessageMapper.selectBySessionId(sessionId);
result.put("code", 200);
result.put("data", messages);
} catch (Exception e) {
result.put("code", 500);
result.put("message", e.getMessage());
}
return result;
}
测试(多会话隔离):
# 会话A
curl -X POST http://localhost:8080/api/chat/session \
-H "Content-Type: application/json" \
-d '{"sessionId": "session-a", "message": "我叫张三"}'
curl -X POST http://localhost:8080/api/chat/session \
-H "Content-Type: application/json" \
-d '{"sessionId": "session-a", "message": "我叫什么?"}'
# 会话B - 独立记忆,不会混淆
curl -X POST http://localhost:8080/api/chat/session \
-H "Content-Type: application/json" \
-d '{"sessionId": "session-b", "message": "我叫什么?"}'
十二、功能5:综合 - RAG + Tool Calling + 保留会话及消息
这是终极功能,结合了RAG(检索增强生成)和Tool Calling(工具调用)。
12.1 工具类 - MathTool.java
package com.example.tool;
import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 数学工具类
* 使用@Tool注解标记可被AI调用的方法
*/
@Slf4j
@Component
public class MathTool {
/**
* 加法运算
*/
@Tool("计算两个数字的和")
public double add(double a, double b) {
log.info("工具调用: add({}, {})", a, b);
return a + b;
}
/**
* 减法运算
*/
@Tool("计算两个数字的差")
public double subtract(double a, double b) {
log.info("工具调用: subtract({}, {})", a, b);
return a - b;
}
/**
* 乘法运算
*/
@Tool("计算两个数字的乘积")
public double multiply(double a, double b) {
log.info("工具调用: multiply({}, {})", a, b);
return a * b;
}
/**
* 除法运算
*/
@Tool("计算两个数字的商")
public double divide(double a, double b) {
log.info("工具调用: divide({}, {})", a, b);
if (b == 0) {
throw new IllegalArgumentException("除数不能为0");
}
return a / b;
}
/**
* 平方根
*/
@Tool("计算一个数字的平方根")
public double sqrt(double x) {
log.info("工具调用: sqrt({})", x);
if (x < 0) {
throw new IllegalArgumentException("不能计算负数的平方根");
}
return Math.sqrt(x);
}
}
12.2 RAG服务 - 文档加载与检索
package com.example.rag;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* RAG服务 - 检索增强生成
*/
@Slf4j
@Service
public class RAGService {
@Value("${openai.api.key}")
private String apiKey;
@Value("${openai.base.url}")
private String baseUrl;
private EmbeddingStore<TextSegment> embeddingStore;
private EmbeddingModel embeddingModel;
@PostConstruct
public void init() {
// 初始化嵌入模型
this.embeddingModel = OpenAiEmbeddingModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName("text-embedding-ada-002")
.build();
// 初始化内存向量存储(生产环境建议用pgvector或Milvus)
this.embeddingStore = new InMemoryEmbeddingStore<>();
// 加载知识文档
loadKnowledgeDocuments();
}
/**
* 加载知识文档
* 实际项目中可以从文件、数据库、网络等加载
*/
private void loadKnowledgeDocuments() {
log.info("加载知识文档...");
// 示例知识库 - 实际项目中从文件读取
String knowledge = """
LangChain4j是一个Java框架,用于构建大语言模型应用。
它提供了统一的API来集成各种LLM,包括OpenAI、通义千问、Claude等。
RAG(检索增强生成)是一种技术,通过检索外部知识来增强大模型的回答能力。
工具调用(Tool Calling)允许大模型调用外部函数来执行特定任务。
ChatMemory是LangChain4j中管理对话历史的组件。
""";
// 创建文档并分块
Document document = Document.from(knowledge);
List<TextSegment> segments = DocumentSplitters.recursive(500, 50)
.split(document);
// 嵌入并存储
List<dev.langchain4j.data.embedding.Embedding> embeddings =
embeddingModel.embedAll(segments).content();
for (int i = 0; i < segments.size(); i++) {
embeddingStore.add(embeddings.get(i), segments.get(i));
}
log.info("知识文档加载完成,共 {} 个分块", segments.size());
}
/**
* 检索相关文档片段
*/
public List<TextSegment> searchRelevant(String query, int maxResults) {
log.info("RAG检索: {}", query);
// 将查询转换为向量
dev.langchain4j.data.embedding.Embedding queryEmbedding =
embeddingModel.embed(query).content();
// 相似度检索
List<dev.langchain4j.store.embedding.EmbeddingMatch<TextSegment>> matches =
embeddingStore.findRelevant(queryEmbedding, maxResults);
return matches.stream()
.map(dev.langchain4j.store.embedding.EmbeddingMatch::embedded)
.toList();
}
/**
* 构建RAG增强的提示词
*/
public String buildRagPrompt(String userQuery, List<TextSegment> segments) {
StringBuilder context = new StringBuilder();
for (int i = 0; i < segments.size(); i++) {
context.append("【文档").append(i + 1).append("】")
.append(segments.get(i).text())
.append("\n");
}
return """
请基于以下参考信息回答用户的问题。
如果参考信息中没有相关内容,请如实告知用户。
=== 参考信息 ===
%s
=== 用户问题 ===
%s
请给出清晰、准确的回答。
""".formatted(context.toString(), userQuery);
}
}
12.3 综合服务实现
// ================================================================
// 功能5:综合 - RAG + Tool Calling + 会话持久化
// ================================================================
@Autowired
private MathTool mathTool;
@Autowired
private RAGService ragService;
@Autowired
private ChatMessageMapper chatMessageMapper;
@Override
public String chatWithRagAndTools(String sessionId, String userMessage) {
log.info("【功能5】综合对话: sessionId={}, message={}", sessionId, userMessage);
try {
// ===== 第一步:RAG检索 =====
List<TextSegment> relevantDocs = ragService.searchRelevant(userMessage, 3);
String ragPrompt = ragService.buildRagPrompt(userMessage, relevantDocs);
log.debug("RAG增强后的提示词: {}", ragPrompt);
// ===== 第二步:获取会话记忆 =====
MessageWindowChatMemory memory = sessionMemoryCache.computeIfAbsent(sessionId, id -> {
log.info("创建新会话记忆: {}", id);
return MessageWindowChatMemory.builder()
.id(id)
.maxMessages(20)
.chatMemoryStore(chatMemoryStore)
.build();
});
// ===== 第三步:构建带工具调用的请求 =====
// 使用AiServices高级API,自动处理工具调用循环
// 这里演示手动处理工具调用的方式
// 先添加用户消息
memory.add(UserMessage.from(ragPrompt));
// 获取工具规范
List<ToolSpecification> toolSpecs = ToolSpecifications.toolSpecificationsFrom(
mathTool.getClass()
);
// 发送请求
ChatRequest request = ChatRequest.builder()
.messages(memory.messages())
.parameters(ChatRequestParameters.builder()
.toolSpecifications(toolSpecs)
.build())
.build();
ChatResponse response = chatModel.chat(request);
AiMessage aiMessage = response.aiMessage();
// ===== 第四步:处理工具调用 =====
// 检查AI是否请求调用工具
if (aiMessage.hasToolExecutionRequests()) {
log.info("AI请求调用工具: {}", aiMessage.toolExecutionRequests());
// 执行工具
List<ToolExecutionResultMessage> toolResults = new ArrayList<>();
for (ToolExecutionRequest toolRequest : aiMessage.toolExecutionRequests()) {
Object result = executeTool(toolRequest);
toolResults.add(ToolExecutionResultMessage.from(toolRequest, String.valueOf(result)));
}
// 将AI的工具调用请求和工具执行结果加入记忆
memory.add(aiMessage);
for (ToolExecutionResultMessage resultMsg : toolResults) {
memory.add(resultMsg);
}
// 再次调用模型,获取最终回答
ChatResponse finalResponse = chatModel.chat(memory.messages());
String finalAnswer = finalResponse.aiMessage().text();
// 保存最终回答到记忆
memory.add(AiMessage.from(finalAnswer));
log.info("【功能5】最终响应: {}", finalAnswer);
return finalAnswer;
}
// ===== 第五步:没有工具调用,直接返回 =====
String answer = aiMessage.text();
memory.add(AiMessage.from(answer));
log.info("【功能5】响应: {}", answer);
return answer;
} catch (Exception e) {
log.error("综合对话失败", e);
return "抱歉,AI服务暂时不可用:" + e.getMessage();
}
}
/**
* 执行工具调用
*/
private Object executeTool(ToolExecutionRequest toolRequest) {
String toolName = toolRequest.name();
Map<String, Object> arguments = toolRequest.arguments();
log.info("执行工具: {}, 参数: {}", toolName, arguments);
switch (toolName) {
case "add":
return mathTool.add(
((Number) arguments.get("a")).doubleValue(),
((Number) arguments.get("b")).doubleValue()
);
case "subtract":
return mathTool.subtract(
((Number) arguments.get("a")).doubleValue(),
((Number) arguments.get("b")).doubleValue()
);
case "multiply":
return mathTool.multiply(
((Number) arguments.get("a")).doubleValue(),
((Number) arguments.get("b")).doubleValue()
);
case "divide":
return mathTool.divide(
((Number) arguments.get("a")).doubleValue(),
((Number) arguments.get("b")).doubleValue()
);
case "sqrt":
return mathTool.sqrt(
((Number) arguments.get("x")).doubleValue()
);
default:
throw new IllegalArgumentException("未知工具: " + toolName);
}
}
12.4 ChatController.java - 功能5接口
// ================================================================
// 功能5:综合 - RAG + Tool Calling + 会话持久化
// ================================================================
@PostMapping("/advanced")
public Map<String, Object> advancedChat(@RequestBody Map<String, String> request) {
String sessionId = request.get("sessionId");
String message = request.get("message");
Map<String, Object> result = new HashMap<>();
if (message == null || message.trim().isEmpty()) {
result.put("code", 400);
result.put("message", "消息不能为空");
return result;
}
if (sessionId == null || sessionId.trim().isEmpty()) {
sessionId = chatService.createNewSession("default-user", "高级对话");
result.put("sessionId", sessionId);
}
try {
String response = chatService.chatWithRagAndTools(sessionId, message);
result.put("code", 200);
result.put("data", response);
result.put("sessionId", sessionId);
} catch (Exception e) {
result.put("code", 500);
result.put("message", e.getMessage());
}
return result;
}
测试:
# RAG知识问答
curl -X POST http://localhost:8080/api/chat/advanced \
-H "Content-Type: application/json" \
-d '{"sessionId": "adv-session-1", "message": "什么是RAG?"}'
# 工具调用 - 数学计算
curl -X POST http://localhost:8080/api/chat/advanced \
-H "Content-Type: application/json" \
-d '{"sessionId": "adv-session-1", "message": "请计算 12345 乘以 6789 等于多少"}'
# 多轮对话 - 看AI是否记住上下文
curl -X POST http://localhost:8080/api/chat/advanced \
-H "Content-Type: application/json" \
-d '{"sessionId": "adv-session-1", "message": "刚才我们计算了什么?"}'
十三、常见问题与避坑指南
13.1 依赖版本兼容性
LangChain4j 0.36.0 开始要求 JDK 17,如果项目还在用 JDK 8,记得用 0.35.0 版本。
13.2 ChatMemory vs 历史记录
这是一个很容易混淆的概念:
- 历史记录(History) :完整的对话记录,用户能看到的所有消息
- 记忆(Memory) :经过裁剪后喂给模型的消息,可能只保留最近N条
咱们的 chat_message 表存的是完整历史,ChatMemoryStore 存的是记忆。两者可以共用一张表,也可以分开。
13.3 API Key 安全
千万别把 API Key 硬编码在代码里!用环境变量或者配置文件管理。
13.4 工具调用的循环
当 AI 请求调用工具时,需要执行工具并将结果返回给 AI,AI 可能再次请求调用工具。生产环境需要处理多轮工具调用循环。
13.5 内存缓存 vs 数据库
sessionMemoryCache 用的是 ConcurrentHashMap,生产环境建议替换为 Redis 或 Caffeine 等更健壮的缓存方案。
十四、总结
到这里,咱们就从零到一完成了 LangChain4j + 普通Spring项目的完整集成,实现了:
- ✅ 普通AI对话 - 最简单的问答
- ✅ 带提示词的对话 - 通过 SystemMessage 控制AI角色
- ✅ 保留对话轮次 - 单会话上下文记忆
- ✅ 保留会话 - 多会话隔离,每个会话独立记忆
- ✅ 综合 RAG + Tool Calling + 会话持久化 - 企业级能力
关键点回顾:
- 普通Spring项目需要手动配置所有 Bean,不像 Spring Boot 那样自动配置
ChatMemoryStore是实现持久化的核心接口,实现它就能把记忆存到任何地方- RAG 通过检索增强大模型的知识
- Tool Calling 让 AI 能调用外部函数
希望这份教程能帮到正在普通Spring项目中集成AI能力的同学们!有问题欢迎交流讨论~
❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!
本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/21739027

浙公网安备 33010602011771号