目录

核心功能

界面设计

项目创建

接口定义

进行对话

获取会话列表

获取会话记录

删除会话记录

后端实现

进行对话

对话记忆

ChatMemory

历史记录

删除记录


Spring AI-CSDN博客 文章中,我们学习了 Spring AI 的基础知识,在本篇文章中,我们就来实现一个简单的 聊天机器人 示例,来更好的学习和使用 Spring AI

项目代码:project: 存放项目代码

核心功能

我们首先来看需要实现的核心功能:

对话:
(1)支持用户与机器人进行文本对话
 (2) 实时响应用户输入,输出自然语言回复
多轮对话:
(1)能够理解和处理多轮对话,保持上下文连续性
(2)支持基于上下文的智能应答
历史记录:
(1)自动保存用户和机器人的对话历史
(2)支持用户查看历史对话记录

界面设计

前端代码获取: 项目完整代码/聊天机器人/chat-bot/src/main/resources/static/chat-board.css · Echo/project - 码云 - 开源中国

项目创建

创建 Spring Boot 项目,并在 pom 文件中添加依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
            <version>1.0.0-M6</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

添加配置文件:

server:
  port: 8080
spring:
  application:
    name: chat-bot
  ai:
    openai:
      api-key: 申请的API-Key
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat
          temperature: 0.7
logging:
  level:
    org.springframework.ai.chat.client.advisor: debug
  pattern:
    console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

接口定义

项目中要实现的接口主要有:

进行对话

[请求]

GET/POST /chat/stream

[参数]

prompt(String):用户输入的消息内容

chatId(String):会话标识id,由前端生成,不重复,新建会话时,创建新的 chatId

[响应]

Flux<String>:流式返回的机器人回复内容

获取会话列表

[请求]

GET/POST /chat/getChatIds

[参数]

[响应]

List<ChatInfo>:会话id列表

获取会话记录

[请求]

GET/POST /chat/getChatHistory

[参数]

chatId(String):会话标识id

[响应]

List<MessageVO>:会话历史记录

删除会话记录

[请求]

GET/POST /chat/deleteChat

[参数]

chatId(String):删除的会话id

[响应]

true/false:删除是否成功

接下来,就是编写后端代码,实现对应功能了,我们先来实现对话功能

后端实现

进行对话

在这里,我们借助 ChatClient 与 AI模型 进行交互,因此,我们先配置 Client

@Configuration
public class CommonConfiguration {
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("你叫小小鱼, 擅长Java 和 Python")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}

然后进行流式返回:

@Slf4j
@RestController
@RequestMapping("/chat")
public class ChatController {

    @Autowired
    private ChatClient chatClient;

    /**
     * 进行对话
     * @param prompt
     * @param chatId
     * @return
     */
    @GetMapping(value = "/stream", produces = "text/html;charset=utf-8")
    public Flux<String> stream(String prompt, String chatId) {
        log.info("进行对话,chatId:{}, prompt:{}", chatId, prompt);
        return chatClient.prompt()
                .user(prompt)
                .stream()
                .content();
    }
}

前端输入内容进行测试:

但是,当前实现的会话功能是没有记忆力的,也就是说,多轮对话之间是没有关系的,如:

就如上图,输入 "再+1",是希望在原来答案 2 的基础上 +1,返回 3,但由于此时会话没有记忆,因此返回的结果不符合我们的预期,这种情况比较影响用户的体验

因此,接下来,我们就来看如何实现模型的对话记忆

对话记忆

大模型本身是无状态,即不具备记忆能力的,它们不会保留有关以前的交互信息

那么,想要让大模型记住之前的聊天内容,需要把之前的聊天内容与新的提示词一起发送给模型

Spring AI 中,提供了 ChatMemory 接口,负责存储和管理对话历史,它的主要作用包括:

1. 保证上下文连贯性:通过存储用户与 AI 的历史消息(如用户提问、AI 回复、系统提示),让模型在后续对话中理解上下文

2. 支持多用户隔离:通过 forUser(userId) 方法,为每个用户维护独立的对话记忆,避免不同用户的对话历史混串

3. 灵活的存储策略:支持多种存储实现(如内存、Redis、数据库),适应不同环境需求(开发/测试/生产)

4. 可扩展的记忆管理:允许自定义记忆的保留时长轮次限制(记忆窗口)、过滤规则(如忽略敏感消息),满足复杂业务需求

接下来,我们就来看 ChatMemory 的使用

ChatMemory

public interface ChatMemory {

	// TODO: consider a non-blocking interface for streaming usages

	default void add(String conversationId, Message message) {
		this.add(conversationId, List.of(message));
	}

	void add(String conversationId, List<Message> messages);

	List<Message> get(String conversationId, int lastN);

	void clear(String conversationId);

}

核心功能方法:

方法签名 功能说明 参数说明 返回值
add(String, Message) 添加单条消息到指定会话

conversationId: 会话ID

message: 单条消息

void
add(String, List<Message>) 批量添加消息到指定会话

conversationId: 会话ID

messages: 消息列表

void
get(String, int) 获取会话的最近消息

conversationId: 会话ID

lastN: 获取最近N条

List<Message>
clear(String) 清空指定会话的记忆 conversationId: 会话ID void

可以看到,所有的方法都依赖 conversationId 参数,确保不同会话的记忆完全隔离,且接口未限定存储实现(内存/Redis/DB),支持灵活扩展

Spring AI 提供了默认实现 InMemoryChatMemory,用于在内存中存储对话历史:

public class InMemoryChatMemory implements ChatMemory {

	Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();

	@Override
	public void add(String conversationId, List<Message> messages) {
		this.conversationHistory.putIfAbsent(conversationId, new ArrayList<>());
		this.conversationHistory.get(conversationId).addAll(messages);
	}

	@Override
	public List<Message> get(String conversationId, int lastN) {
		List<Message> all = this.conversationHistory.get(conversationId);
		return all != null ? all.stream().skip(Math.max(0, all.size() - lastN)).toList() : List.of();
	}

	@Override
	public void clear(String conversationId) {
		this.conversationHistory.remove(conversationId);
	}

}

使用 ConcurrentHashMap来区分不同的对话(key:会话Id,value:消息列表) ,并保证线程安全的并发访问

接下来,我们就通过 InMemoryChatMemory 来实现历史对话的存储

定义 ChatMemory 并将其注入到  ChatClient

@Configuration
public class CommonConfiguration {
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("你叫小小鱼, 擅长Java 和 Python")
                .defaultAdvisors(new SimpleLoggerAdvisor(), new MessageChatMemoryAdvisor(chatMemory()))
                .build();
    }
    
    @Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }
}

通过 MessageChatMemoryAdvisor 将 ChatMemory 添加到 ChatClient 中,从而实现在对话过程中自动将用户输入和AI响应添加到记忆,并在请求AI前自动注入相关历史上下文,以及为不同会话(conversationId)维护独立记忆,防止不同用户的对话历史相互干扰

工作原理:

当用户发送请求时,Advisor 激活,从 ChatMemory 中获取历史列表,此时可对消息进行处理,如过滤无关消息、进行优先级排序等,然后再将历史消息和当前消息发送给 AI 模型

当我们向模型发送请求时,传入chatId

    @GetMapping(value = "/stream", produces = "text/html;charset=utf-8")
    public Flux<String> stream(String prompt, String chatId) {
        log.info("进行对话,chatId:{}, prompt:{}", chatId, prompt);
        return chatClient.prompt()
                .user(prompt)
                // 获取 chatId 的对话记忆
                .advisors(advisorSpec -> advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }

此时,我们再次进行测试:

此时 AI 模型就能够结合之前的历史记录进行分析,并给出预期结果

此时我们观察后端日志:

可以看到请求中包含 SystemMessage(你叫小小鱼, 擅长Java 和 Python)、之前发送的UserMessage(1+1=)以及回复的 AssistantMessage(1+1=2)

历史记录

ChatMemory 提供了 get 接口,可以根据会话id获取会话记录,我们可以通过一个 List 来存储会话id列表,再根据会话id来获取会话记录

定义 ChatInfo 来存储会话记录:

@Data
public class ChatInfo {
    private String chatId;
    private String title;
    public ChatInfo(){}

    public ChatInfo(String chatId, String title) {
        this.chatId = chatId;
        this.title = title;
    }
}

定义 ChatHistoryRepository 接口来管理 ChatInfo:

public interface ChatHistoryRepository {
    /**
     * 保存会话信息
     * @param chatId
     * @param chatInfo
     */
    void save(String chatId, ChatInfo chatInfo);

    /**
     * 获取所有会话
     * @return
     */
    List<ChatInfo> getChatList();

    /**
     * 删除会话id
     * @param chatId
     */
    void deletedChatId(String chatId);
}

MemoryChatHistoryRepository 实现 ChatInfo 管理

@Repository
public class MemoryChatHistoryRepository implements ChatHistoryRepository {
    private final Map<String, ChatInfo> chatHistory = new LinkedHashMap<>();

    @Override
    public void save(String chatId, ChatInfo chatInfo) {
        // 覆盖相同的 chatId 信息, 保存最后一次消息信息
        chatHistory.put(chatId, chatInfo);
    }

    @Override
    public List<ChatInfo> getChatList() {
        return chatHistory.values().stream().toList();
    }

    @Override
    public void deletedChatId(String chatId) {
        chatHistory.remove(chatId);
    }
}

修改 stream 方法,实现会话存储:

@Slf4j
@RestController
@RequestMapping("/chat")
public class ChatController {
    @Autowired
    private MemoryChatHistoryRepository chatHistoryRepository;
    
    @Autowired
    private ChatClient chatClient;


    /**
     * 进行对话
     * @param prompt
     * @param chatId
     * @return
     */
    @GetMapping(value = "/stream", produces = "text/html;charset=utf-8")
    public Flux<String> stream(String prompt, String chatId) {
        log.info("进行对话,chatId:{}, prompt:{}", chatId, prompt);
        // 保存会话信息
        chatHistoryRepository.save(chatId, new ChatInfo(chatId, prompt));
        return chatClient.prompt()
                .user(prompt)
                // 获取 chatId 的对话记忆
                .advisors(advisorSpec -> advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }
}

实现获取会话列表接口

    @Autowired
    private MemoryChatHistoryRepository chatHistoryRepository; 

    @GetMapping("/getChatIds")
    public List<ChatInfo> getChatIds() {
        return chatHistoryRepository.getChatList();
    }

定义消息 MessageVO 类型:

@Data
public class MessageVO {
    private String role;
    private String content;
    public MessageVO(Message message) {
        switch (message.getMessageType()) {
            case USER ->  this.role = "user";
            case ASSISTANT -> this.role = "assistant";
            case SYSTEM -> this.role = "system";
            case TOOL -> this.role = "tool";
        }
        this.content = message.getText();
    }
}

实现获取会话记录接口

    @Autowired
    private ChatMemory chatMemory;

    /**
     * 获取所有会话信息
     * @param chatId
     * @return
     */
    @GetMapping("/getChatHistory")
    public List<MessageVO> getChatHistory(String chatId) {
        log.info("获取 chatId:{} 会话信息", chatId);
        List<Message> messages = chatMemory.get(chatId, 20);
        if (messages.isEmpty()) {
            return List.of();
        }
        return messages.stream()
                .map(MessageVO::new)
                .collect(Collectors.toList());
    }

测试:

能够正确显示会话信息和会话消息

删除记录

实现删除接口:

    @GetMapping("/deleteChat")
    public Boolean deleteChat(String chatId) {
        log.info("删除 chatId: {} 会话记录", chatId);
        try {
            // 删除会话记录
            chatMemory.clear(chatId);
            // 删除会话id
            chatHistoryRepository.deletedChatId(chatId);
            return true;
        } catch (Exception e) {
            log.error("删除 chatId: {} 会话信息异常", chatId);
            e.printStackTrace();
            return false;
        }
    }

点击删除:

删除记录成功:

至此,聊天机器人 的核心功能就已实现完毕

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐