1. 引言
在本文中,我们将介绍如何使用 Spring Boot、Vue.js 和 Langchain4j,实现与 阿里云百炼平台 的 AI 流式对话对接。通过结合这些技术,我们将创建一个能够实时互动的 AI 聊天应用。
这是一个基于 Spring Boot + Vue.js + Langchain4j 的智能对话系统,实现了类似 ChatGPT 的流式交互体验。系统通过 阿里云百炼 qwen-max 大模型 提供 AI 能力,支持多轮对话记忆(Redis 存储)、本地知识库检索(RAG)和实时流式响应(SSE 协议)。前端 Vue.js 动态渲染对话内容,后端 Spring Boot 处理业务逻辑,Langchain4j 简化大模型集成,具有低延迟、易扩展的特点,适用于智能客服、知识问答等场景。
在线预览地址
https://www.coderman.club
对话记忆功能
代码编写效果图
2. 技术栈概述
- Spring Boot:后端框架,用于处理与阿里云百炼平台的 API 对接与业务逻辑。
- Vue.js:前端框架,用于实现与用户的实时交互和流式数据展示。
- Langchain4j:Java 库,用于简化与文本生成模型的交互。
- 阿里云百炼平台:为智能对话提供 NLP 模型和 API,支持流式对话功能。
3. 阿里云百炼平台接入准备
3.1 注册阿里云百炼账号
首先,注册一个阿里云账号并访问百炼平台,获取相关的 API 密钥
3.2 创建对话模型
在阿里云百炼平台上创建并配置一个 AI 对话模型,为后续的对接做好准备。
3.3 获取 API 文档
参考阿里云的 API 文档,获取具体的接口信息和请求参数。
4. 后端实现 - Spring Boot 与阿里云百炼平台对接
4.1 创建 Spring Boot 项目
使用 Spring Initializr 创建一个基础的 Spring Boot 项目,配置相关依赖。
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j</artifactId><version>${langchain4j.version}</version></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-dashscope</artifactId><version>${langchain4j.version}</version></dependency>
4.2 编写Controller代码
@Api(value = "AI接口", tags = "AI接口")
@RestController
@Slf4j
@RequestMapping(value = "/common/chat")
public class ChatController {@Resourceprivate ChatService chatService;@ApiModelProperty(value = "AI对话")@PostMapping(value = "/completion",produces = {MediaType.TEXT_EVENT_STREAM_VALUE})public SseEmitter completion(@RequestBody ChatGptDTO chatGptDTO) {SseEmitter sseEmitter = new SseEmitter(5 * 60 * 1000L);this.chatService.completion(chatGptDTO, sseEmitter);sseEmitter.onTimeout(() -> {log.warn("SSE连接超时");sseEmitter.complete();});sseEmitter.onError(e -> {log.error("SSE连接错误", e);sseEmitter.complete();});return sseEmitter;}
}
4.2 编写Service代码
package com.coderman.admin.service.common.impl;import com.alibaba.fastjson.JSONObject;
import com.coderman.admin.dto.common.ChatGptDTO;
import com.coderman.admin.service.common.Assistant;
import com.coderman.admin.service.common.ChatService;
import dev.langchain4j.service.TokenStream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;import javax.annotation.Resource;
import java.io.IOException;@Service
@Slf4j
@Configuration
public class ChatServiceImpl implements ChatService {@Resourceprivate Assistant assistant;/*** 调用大模型接口* @param chatGptDTO 参数* @param sseEmitter sse*/public void completion(ChatGptDTO chatGptDTO, SseEmitter sseEmitter) {try {TokenStream stream = assistant.completion(chatGptDTO.getSessionId(), chatGptDTO.getPrompt());// 监听回调stream.onNext(result -> sendSseData(sseEmitter, result));stream.onError(throwable -> handleError(sseEmitter, throwable));stream.onComplete(response -> completeSse(sseEmitter));stream.start();} catch (Exception e) {throw new RuntimeException("completion error:"+e.getMessage());}}private void sendSseData(SseEmitter sseEmitter, String result) {try {JSONObject jsonObject = new JSONObject();jsonObject.put("text",result);sseEmitter.send(jsonObject, MediaType.APPLICATION_JSON);} catch (IOException e) {log.warn("SSE 发送数据失败:{}", e.getMessage());}}private void handleError(SseEmitter sseEmitter, Throwable throwable) {log.error("SSE 发生错误:{}", throwable.getMessage());sseEmitter.completeWithError(throwable);}private void completeSse(SseEmitter sseEmitter) {try {sseEmitter.send("[DONE]");sseEmitter.complete();} catch (Exception e) {log.warn("SSE 关闭失败:{}", e.getMessage());}}
}
4.3 langchain4j配置类
package com.coderman.admin.config;import com.coderman.admin.service.common.Assistant;
import com.coderman.api.constant.CommonConstant;
import com.coderman.service.util.DesUtil;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.annotation.Resource;
import java.util.List;/*** @author :zhangyukang* @date :2025/04/02 11:12*/
@Configuration
@Slf4j
public class ChatAiConfigure {@Resourceprivate RedisProperties redisProperties;@Beanpublic Assistant assistant() {EmbeddingModel embeddingModel = QwenEmbeddingModel.builder().apiKey(this.getApiKey()).build();InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder().embeddingStore(embeddingStore).embeddingModel(embeddingModel)// 最相似的3个结果.maxResults(3)// 只找相似度在0.8以上的内容.minScore(0.8).build();// 初始化知识库this.loadEmbedding(embeddingModel, embeddingStore);// 自定义存储方式RedisChatMemoryStore redisChatMemoryStore = RedisChatMemoryStore.builder().port(redisProperties.getPort()).password(redisProperties.getPassword()).host(redisProperties.getHost()).build();// 构建模型StreamingChatLanguageModel model = QwenStreamingChatModel.builder().apiKey(this.getApiKey()).modelName("qwen-max").maxTokens(1024).enableSearch(true).build();// 构建服务return AiServices.builder(Assistant.class).streamingChatLanguageModel(model).chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder().id(memoryId).maxMessages(10).chatMemoryStore(redisChatMemoryStore).build()).contentRetriever(contentRetriever).build();}/*** 初始化RAG向量* @param embeddingModel 模型* @param embeddingStore 向量存储*/private void loadEmbedding(EmbeddingModel embeddingModel, InMemoryEmbeddingStore<TextSegment> embeddingStore) {try {Document document = FileSystemDocumentLoader.loadDocument("/opt/rag.txt", new TextDocumentParser());List<TextSegment> textSegments = new RagDocumentSplitter().split(document);Response<List<Embedding>> response = embeddingModel.embedAll(textSegments);List<Embedding> embeddings = response.content();embeddingStore.addAll(embeddings, textSegments);} catch (Exception e) {log.warn("初始化知识库失败:{}", e.getMessage());}}private String getApiKey() {try {final String API_KEY_ENCRYPTED = "你的app秘钥";return DesUtil.decrypt(API_KEY_ENCRYPTED, CommonConstant.SECRET_KEY);} catch (Exception e) {log.error("API Key 解密失败", e);throw new RuntimeException("无法解密 API Key");}}
}
4.4 自定义AI对话接口类
package com.coderman.admin.service.common;import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.UserMessage;/*** @author :zhangyukang* @date :2025/04/02 11:14*/
public interface Assistant {@SystemMessage(value = {"你是由小章鱼开发的AI智能助手,专注于高效、友好地帮助用户解决问题。","你的核心能力包括:问题解答、信息查询、任务协助、建议提供等。","当用户询问你的身份时,请明确回复:'我是由小章鱼开发的AI助手,很高兴为你服务!'","回答时需保持专业且亲切,语言简洁易懂,避免冗长或模糊表述。","如果遇到无法回答的问题,应礼貌说明并建议替代方案。","始终以用户需求为核心,主动提供有价值的帮助。"})TokenStream completion(@MemoryId String sessionId, @UserMessage String userMessage);
}
4.5 使用Redis存储实现对话记忆
package com.coderman.admin.config;import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.internal.ValidationUtils;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import redis.clients.jedis.*;import java.util.ArrayList;
import java.util.List;/*** 使用 Redis 存储聊天记录的 MemoryStore*/
public class RedisChatMemoryStore implements ChatMemoryStore {private final JedisPool jedisPool;private final static String KEY_PREFIX = "chat_memory:";public RedisChatMemoryStore(String host, Integer port, String password) {// 配置 Redis 连接池JedisPoolConfig poolConfig = new JedisPoolConfig();poolConfig.setMaxTotal(10);poolConfig.setMaxIdle(5);poolConfig.setMinIdle(2);// 这里 JedisPool 支持传入密码this.jedisPool = new JedisPool(poolConfig, host, port, 2000, password);}@Overridepublic List<ChatMessage> getMessages(Object memoryId) {try (Jedis jedis = jedisPool.getResource()) {String json = jedis.get(KEY_PREFIX+ toMemoryIdString(memoryId));return json == null ? new ArrayList<>() : ChatMessageDeserializer.messagesFromJson(json);}}@Overridepublic void updateMessages(Object memoryId, List<ChatMessage> messages) {try (Jedis jedis = jedisPool.getResource()) {String json = ChatMessageSerializer.messagesToJson(ValidationUtils.ensureNotEmpty(messages, "messages"));String res = jedis.set(KEY_PREFIX + toMemoryIdString(memoryId), json);if (!"OK".equals(res)) {throw new RuntimeException("Redis set memory error: " + res);}}}@Overridepublic void deleteMessages(Object memoryId) {try (Jedis jedis = jedisPool.getResource()) {jedis.del(KEY_PREFIX + toMemoryIdString(memoryId));}}private static String toMemoryIdString(Object memoryId) {if (memoryId == null || memoryId.toString().trim().isEmpty()) {throw new IllegalArgumentException("memoryId cannot be null or empty");}return memoryId.toString();}public static Builder builder() {return new Builder();}/*** Builder 模式,方便配置 Redis 连接*/public static class Builder {private String host = "127.0.0.1";private Integer port = 6379;private String password = null;public Builder host(String host) {this.host = host;return this;}public Builder port(Integer port) {this.port = port;return this;}public Builder password(String password) {this.password = password;return this;}public RedisChatMemoryStore build() {return new RedisChatMemoryStore(host, port, password);}}
}
源码地址: https://github.com/zykzhangyukang/admin