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项目的完整集成,实现了:

  1. 普通AI对话 - 最简单的问答
  2. 带提示词的对话 - 通过 SystemMessage 控制AI角色
  3. 保留对话轮次 - 单会话上下文记忆
  4. 保留会话 - 多会话隔离,每个会话独立记忆
  5. 综合 RAG + Tool Calling + 会话持久化 - 企业级能力

关键点回顾:

  • 普通Spring项目需要手动配置所有 Bean,不像 Spring Boot 那样自动配置
  • ChatMemoryStore 是实现持久化的核心接口,实现它就能把记忆存到任何地方
  • RAG 通过检索增强大模型的知识
  • Tool Calling 让 AI 能调用外部函数

希望这份教程能帮到正在普通Spring项目中集成AI能力的同学们!有问题欢迎交流讨论~

posted @ 2026-07-31 12:58  佛祖让我来巡山  阅读(44)  评论(0)    收藏  举报

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

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

Bootstrap中文网