Java快速接入DeepSeek实现流式、联网、知识库以及多轮问答

Java快速接入DeepSeek实现流式、联网、知识库以及多轮问答

本文将详细的说明,如何使用Java、JDK8快速接入deepseek的聊天服务,包含官方的API服务,以及本地Ollama的服务。并搭建一个简单的前端界面,用于流式输出、多轮问答、联网、知识库问答的效果展示。

1. 创建spring boot应用

图片[1]-Java快速接入DeepSeek实现流式、联网、知识库以及多轮问答 - 拾光赋-拾光赋

2. 引入pom

引入ai4j库的依赖。

AI4J是一款JavaSDK用于快速接入AI大模型应用,整合多平台大模型,如OpenAi、Ollama、智谱Zhipu(ChatGLM)、深度求索DeepSeek、月之暗面Moonshot(Kimi)、腾讯混元Hunyuan、零一万物(01)等等,提供统一的输入输出(对齐OpenAi)消除差异化,优化函数调用(Tool Call),优化RAG调用、支持向量数据库(Pinecone),并且支持JDK1.8,为用户提供快速整合AI的能力。
AI4J-GitHub

        <dependency>
            <groupId>io.github.lnyo-cly</groupId>
            <artifactId>ai4j-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>

3. 配置application.yml

在deepseek官网,创建API-KEY,然后在application.yml中配置

DeepSeek-APIKEY

ai:
  deepseek:
    api-key: "sk-123456789"

4. 创建聊天服务Controller

接下来实现流式输出:

@RestController
public class OpenAiController { 

    // 注入Ai服务
    @Autowired
    private AiService aiService;
    
    @GetMapping("/chatStream")
    public SseEmitter getChatMessageStream(@RequestParam String question) { 
        SseEmitter emitter = new SseEmitter();

        // 获取DEEPSEEK的聊天服务
        IChatService chatService = aiService.getChatService(PlatformType.DEEPSEEK);

        // 创建请求参数
        ChatCompletion chatCompletion = ChatCompletion.builder()
                .model("deepseek-chat")
                .message(ChatMessage.withUser(question))
                .build();


        Executors.newSingleThreadExecutor().submit(() -> { 
            try { 
                SseListener sseListener = new SseListener() { 
                    @Override
                    protected void send() { 
                        try { 
                            emitter.send(this.getCurrData());
                            System.out.println(this.getCurrData());  // 打印当前发送的内容
                        } catch (IOException e) { 
                            emitter.completeWithError(e);
                        }
                    }
                };

                emitter.onCompletion(() -> { 
                    System.out.println("完成");
                    sseListener.getEventSource().cancel();

                });

                // 发送流式数据
                chatService.chatCompletionStream(chatCompletion, sseListener);

                // 完成后关闭连接
                emitter.complete();
            } catch (Exception e) { 
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }


}

或者

    @GetMapping("/chatStream")
    public SseEmitter getChatMessageStream(@RequestParam String question) throws Exception { 
        SseEmitter emitter = new SseEmitter();

        // 获取OLLAMA的聊天服务
        IChatService chatService = aiService.getChatService(PlatformType.DEEPSEEK);

        // 创建请求参数
        ChatCompletion chatCompletion = ChatCompletion.builder()
                .model("deepseek-chat")
                .message(ChatMessage.withUser(question))
                .build();


        SseListener sseListener = new SseListener() { 
            @Override
            protected void send() { 
                try { 
                    emitter.send(this.getCurrData());
                    System.out.println(this.getCurrData());  // 打印当前发送的内容
                    if ("[DONE]".equals(this.getCurrData())) { 
                        emitter.complete();
                    }
                } catch (IOException e) { 
                    emitter.completeWithError(e);
                }
            }
        };

        emitter.onCompletion(() -> { 
            System.out.println("完成");
            sseListener.getEventSource().cancel();

        });

        // 发送流式数据
        sseListener.getCountDownLatch().countDown(); // 取消同步阻塞
        chatService.chatCompletionStream(chatCompletion, sseListener);


        return emitter;
    }

测试流式接口如下:

5. 修改为ollama调用

    @GetMapping("/chatStream")
    public SseEmitter getChatMessageStream(@RequestParam String question) { 
        SseEmitter emitter = new SseEmitter(-1L);


        // 获取OLLAMA的聊天服务
        IChatService chatService = aiService.getChatService(PlatformType.OLLAMA);

        // 创建请求参数
        ChatCompletion chatCompletion = ChatCompletion.builder()
                .model("deepseek-r1:1.5b")
                .message(ChatMessage.withUser(question))
                .build();


        Executors.newSingleThreadExecutor().submit(() -> { 
            try { 
                SseListener sseListener = new SseListener() { 
                    @Override
                    protected void send() { 
                        try { 
                            emitter.send(this.getCurrData());
                            System.out.println(this.getCurrData());  // 打印当前发送的内容
                        } catch (IOException e) { 
                            emitter.completeWithError(e);
                        }
                    }
                };

                emitter.onCompletion(() -> { 
                    System.out.println("完成");
                    sseListener.getEventSource().cancel();

                });

                // 发送流式数据
                chatService.chatCompletionStream(chatCompletion, sseListener);

                // 完成后关闭连接
                emitter.complete();
            } catch (Exception e) { 
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }

修改两处即可:

  • 修改PlatformTypeOLLAMA
  • 修改modeldeepseek-r1:1.5b

6. 搭建前端界面

注意:此前端界面由AI生成,并未经过严格测试,仅供参考。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI聊天助手</title>
    <!-- Font Awesome CDN -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style> :root {  --primary-color: #6366f1; --primary-light: #818cf8; --primary-dark: #4f46e5; --text-light: #ffffff; --text-dark: #1e293b; --bg-light: #f8fafc; --bg-dark: #0f172a; --message-user: #6366f1; --message-bot: #f1f5f9; --border-color: #e2e8f0; } * {  margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', 'Arial', sans-serif; } body {  background-color: var(--bg-light); color: var(--text-dark); display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; } .chat-container {  width: 100%; max-width: 900px; background: white; border-radius: 16px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05); overflow: hidden; display: flex; flex-direction: column; height: 85vh; position: relative; border: 1px solid var(--border-color); } .chat-header {  background: var(--primary-color); color: var(--text-light); padding: 18px 24px; display: flex; align-items: center; gap: 12px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); z-index: 10; } .chat-header i {  font-size: 1.5rem; } .chat-header h1 {  font-size: 1.3rem; font-weight: 600; } .chat-messages {  flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 20px; scroll-behavior: smooth; } .message-container {  display: flex; gap: 12px; max-width: 85%; } .user-container {  align-self: flex-end; flex-direction: row-reverse; } .bot-container {  align-self: flex-start; } .avatar {  width: 38px; height: 38px; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 1.2rem; flex-shrink: 0; } .user-avatar {  background: var(--primary-light); color: var(--text-light); } .bot-avatar {  background: var(--primary-dark); color: var(--text-light); } .message {  padding: 14px 20px; border-radius: 18px; font-size: 1rem; line-height: 1.6; position: relative; max-width: 100%; } .user-message {  background: var(--message-user); color: var(--text-light); border-top-right-radius: 4px; } .bot-message {  background: var(--message-bot); color: var(--text-dark); border-top-left-radius: 4px; } .message-time {  font-size: 0.7rem; opacity: 0.7; margin-top: 6px; text-align: right; } .user-message .message-time {  color: rgba(255, 255, 255, 0.9); } .bot-message .message-time {  color: rgba(0, 0, 0, 0.6); } .chat-input-container {  padding: 16px 24px; background: white; border-top: 1px solid var(--border-color); display: flex; align-items: center; gap: 14px; z-index: 10; } .chat-input {  flex: 1; padding: 14px 20px; border: 1px solid var(--border-color); border-radius: 30px; font-size: 1rem; outline: none; transition: all 0.3s; background: var(--bg-light); } .chat-input:focus {  border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); } .send-button {  width: 50px; height: 50px; border: none; background: var(--primary-color); color: var(--text-light); border-radius: 50%; cursor: pointer; transition: all 0.3s; display: flex; justify-content: center; align-items: center; box-shadow: 0 2px 10px rgba(99, 102, 241, 0.3); } .send-button:hover {  background: var(--primary-dark); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4); } .send-button:active {  transform: translateY(0); } .send-button i {  font-size: 1.2rem; } /* 滚动条样式 */ .chat-messages::-webkit-scrollbar {  width: 6px; } .chat-messages::-webkit-scrollbar-track {  background: transparent; } .chat-messages::-webkit-scrollbar-thumb {  background: #d1d5db; border-radius: 10px; } .chat-messages::-webkit-scrollbar-thumb:hover {  background: #9ca3af; } /* 打字机效果 */ .typing {  display: flex; align-items: center; gap: 4px; padding: 8px 12px; border-radius: 18px; background: var(--message-bot); width: fit-content; } .typing-dot {  width: 8px; height: 8px; background: var(--primary-color); border-radius: 50%; animation: typing-animation 1.4s infinite both; } .typing-dot:nth-child(2) {  animation-delay: 0.2s; } .typing-dot:nth-child(3) {  animation-delay: 0.4s; } @keyframes typing-animation {  0%, 100% {  opacity: 0.3; transform: scale(0.8); } 50% {  opacity: 1; transform: scale(1); } } /* 消息进入动画 */ @keyframes message-in {  from {  opacity: 0; transform: translateY(20px); } to {  opacity: 1; transform: translateY(0); } } .message-container {  animation: message-in 0.3s ease-out forwards; } /* 响应式调整 */ @media (max-width: 768px) {  .chat-container {  height: 90vh; border-radius: 12px; } .message-container {  max-width: 90%; } .chat-header h1 {  font-size: 1.1rem; } } @media (max-width: 480px) {  .chat-container {  height: 92vh; border-radius: 8px; } .message {  padding: 12px 16px; } .avatar {  width: 32px; height: 32px; font-size: 1rem; } .chat-input {  padding: 12px 16px; } .send-button {  width: 45px; height: 45px; } .chat-header {  padding: 14px 20px; } .chat-messages {  padding: 20px; } } </style>
</head>
<body>
<div class="chat-container">
    <div class="chat-header">
        <i class="fas fa-robot"></i>
        <h1>AI聊天助手</h1>
    </div>

    <div class="chat-messages" id="chat-messages">
        <div class="message-container bot-container">
            <div class="avatar bot-avatar">
                <i class="fas fa-robot"></i>
            </div>
            <div class="message-content">
                <div class="message bot-message">
                    您好!我是AI助手,很高兴为您服务。请问有什么我可以帮助您的吗?
                </div>
                <div class="message-time">
                    刚刚
                </div>
            </div>
        </div>
    </div>

    <div class="chat-input-container">
        <input type="text" class="chat-input" id="user-input" placeholder="输入您的问题..." autofocus>
        <button class="send-button" id="send-button">
            <i class="fas fa-paper-plane"></i>
        </button>
    </div>
</div>

<script> document.addEventListener('DOMContentLoaded', function() {  const chatMessages = document.getElementById('chat-messages'); const userInput = document.getElementById('user-input'); const sendButton = document.getElementById('send-button'); let eventSource = null; // 获取当前时间 function getCurrentTime() {  const now = new Date(); let hours = now.getHours(); let minutes = now.getMinutes(); // 确保分钟为两位数 minutes = minutes < 10 ? '0' + minutes : minutes; return `${ hours}:${ minutes}`; } // 添加用户消息 function addUserMessage(message, time) {  const messageContainer = document.createElement('div'); messageContainer.className = 'message-container user-container'; const avatarDiv = document.createElement('div'); avatarDiv.className = 'avatar user-avatar'; avatarDiv.innerHTML = '<i class="fas fa-user"></i>'; const messageContentDiv = document.createElement('div'); messageContentDiv.className = 'message-content'; const messageDiv = document.createElement('div'); messageDiv.className = 'message user-message'; messageDiv.textContent = message; const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; timeDiv.textContent = time; messageContentDiv.appendChild(messageDiv); messageContentDiv.appendChild(timeDiv); messageContainer.appendChild(avatarDiv); messageContainer.appendChild(messageContentDiv); chatMessages.appendChild(messageContainer); chatMessages.scrollTop = chatMessages.scrollHeight; } // 发送消息函数 function sendMessage() {  const message = userInput.value.trim(); if (!message) return; // 添加用户消息到聊天区域 const time = getCurrentTime(); addUserMessage(message, time); // 清空输入框 userInput.value = ''; // 添加机器人正在输入的指示 const typingContainer = document.createElement('div'); typingContainer.className = 'message-container bot-container'; typingContainer.id = 'bot-typing'; const avatarDiv = document.createElement('div'); avatarDiv.className = 'avatar bot-avatar'; avatarDiv.innerHTML = '<i class="fas fa-robot"></i>'; const typingDiv = document.createElement('div'); typingDiv.className = 'typing'; typingDiv.innerHTML = '<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>'; typingContainer.appendChild(avatarDiv); typingContainer.appendChild(typingDiv); chatMessages.appendChild(typingContainer); chatMessages.scrollTop = chatMessages.scrollHeight; // 更改按钮为暂停 sendButton.innerHTML = '<i class="fas fa-pause"></i>'; sendButton.onclick = stopStream; // 创建EventSource连接 const url = `http://127.0.0.1:8080/chatStream?question=${ encodeURIComponent(message)}`; eventSource = new EventSource(url); let botResponse = ''; let responseContainer = null; eventSource.onmessage = function(event) {  // 如果这是第一条消息,创建回复容器 if (!responseContainer) {  // 移除打字指示器 const typingIndicator = document.getElementById('bot-typing'); if (typingIndicator) {  typingIndicator.remove(); } // 创建回复容器 responseContainer = document.createElement('div'); responseContainer.className = 'message-container bot-container'; responseContainer.id = 'current-bot-response'; const avatarDiv = document.createElement('div'); avatarDiv.className = 'avatar bot-avatar'; avatarDiv.innerHTML = '<i class="fas fa-robot"></i>'; const messageContentDiv = document.createElement('div'); messageContentDiv.className = 'message-content'; const messageDiv = document.createElement('div'); messageDiv.className = 'message bot-message'; messageDiv.id = 'current-bot-message'; const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; timeDiv.textContent = getCurrentTime(); messageContentDiv.appendChild(messageDiv); messageContentDiv.appendChild(timeDiv); responseContainer.appendChild(avatarDiv); responseContainer.appendChild(messageContentDiv); chatMessages.appendChild(responseContainer); } // 更新回复内容 botResponse += event.data; const messageDiv = document.getElementById('current-bot-message'); if (messageDiv) {  messageDiv.textContent = botResponse; } // 自动滚动到底部 chatMessages.scrollTop = chatMessages.scrollHeight; }; eventSource.onerror = function() {  // 处理完成或错误时 completeResponse(); }; } // 停止流式响应 function stopStream() {  if (eventSource) {  eventSource.close(); completeResponse(); } } // 完成响应处理 function completeResponse() {  // 关闭连接 if (eventSource) {  eventSource.close(); eventSource = null; } // 移除打字指示器 const typingIndicator = document.getElementById('bot-typing'); if (typingIndicator) {  typingIndicator.remove(); } // 恢复发送按钮 sendButton.innerHTML = '<i class="fas fa-paper-plane"></i>'; sendButton.onclick = sendMessage; // 移除id,以便下次使用 const currentBotResponse = document.getElementById('current-bot-response'); if (currentBotResponse) {  currentBotResponse.removeAttribute('id'); } const currentBotMessage = document.getElementById('current-bot-message'); if (currentBotMessage) {  currentBotMessage.removeAttribute('id'); } } // 设置事件监听器 sendButton.addEventListener('click', sendMessage); userInput.addEventListener('keydown', function(e) {  if (e.key === 'Enter') {  sendMessage(); } }); // 自动聚焦到输入框 userInput.focus(); }); </script>
</body>
</html>

7. 多轮对话

只需要简单修改,即可携带历史上下文进行对话:


    private List<ChatMessage> history = new ArrayList<>(); // 1. 创建历史消息列表

    @GetMapping("/chatStream")
    public SseEmitter getChatMessageStream(@RequestParam String question) { 

        // ......

        history.add(ChatMessage.withUser(question)); // 2. 向历史中添加用户输入
        // 创建请求参数
        ChatCompletion chatCompletion = ChatCompletion.builder()
                .model("deepseek-chat")
                .messages(history) // 3. 添加完整历史消息
                .build();

        // ...... 
        Executors.newSingleThreadExecutor().submit(() -> { 
            try { 

                // ......
                emitter.onCompletion(() -> { 
                    System.out.println("完成");
                    history.add(ChatMessage.withAssistant(sseListener.getOutput().toString())); // 4. 向历史中添加AI回复
                    sseListener.getEventSource().cancel();
                });

                // ......

            } catch (Exception e) { 
                emitter.completeWithError(e);
            }
        });



        // ......
    }

8. 联网对话

  1. 配置application.yml中的searxng,将其中的url替换为你已经部署的searxng服务的地址。
ai:
  websearch:
    searxng:
      url: "http://127.0.0.1:29080/search"
      nums: 10
  1. 修改代码,添加联网对话的功能:
    @GetMapping("/chatStream")
    public SseEmitter getChatMessageStream(@RequestParam String question) { 
        // ......

        // 获取DEEPSEEK的聊天服务
        IChatService chatService = aiService.webSearchEnhance(aiService.getChatService(PlatformType.DEEPSEEK)); // 1. 使用webSearchEnhance对原本chat服务增加联网功能,该联网服务使用的为searxng

        // ......
        // ......

    }


9. 搭建知识库

本文使用的向量数据库为Pinecone

9.1 创建Pinecone

大家可以进入Pinecone官网进行注册和登录,至于注册账号,这里不在演示,相信大家都会。

选择Database->Indexes->Create Index来创建索引

在这里可以输入你的维度,或者点击Setup by model,根据模型来选择向量维度。这里我以text-embedding-3-large模型为例子
图片[2]-Java快速接入DeepSeek实现流式、联网、知识库以及多轮问答 - 拾光赋-拾光赋

创建完成后,记录自己的Host,我们后面要用到

创建自己的API Key

9.2 配置application.yml

请将上文得到的HostAPI Key填入application.yml

ai:
  vector:
    pinecone:
      host: "https://XXXXXXX-XXXXXXXXX.io"
      key: "XXXXXX"

9.3 构建RAG知识库文档

既然要建立RAG应用,那肯定少不了知识库。

本文搭建的是一个简单的法律AI助手,所以我们需要一个法律知识库。

接下来我以刑法知识库为例为大家讲解

可以将所需要的知识库,存入一个文本文档当中:

注意:如果有现成的文档,你也可以忽略这一步,例如你已经有了txt、word、pdf等文件的知识库文档。

9.4 存储至Pinecone向量数据库中

@SpringBootTest
public class RagTest { 

    // 1. 注入Pinecone服务
    @Autowired
    private PineconeService pineconeService;

    // 2. 注入AI服务
    @Autowired
    private AiService aiService;

    @Test
    public void test_rag_store() throws Exception { 
        // 3. 获取Embedding服务
        IEmbeddingService embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);

        // 4. Tika读取file文件内容
        String fileContent = TikaUtil.parseFile(new File("D:\\data\\test.txt"));
        System.out.println(fileContent);

        // 5. 分割文本内容
        RecursiveCharacterTextSplitter recursiveCharacterTextSplitter = new RecursiveCharacterTextSplitter(1000, 200);
        List<String> contentList = recursiveCharacterTextSplitter.splitText(fileContent);
        System.out.println(contentList.size());

        // 6. 转为向量
        Embedding build = Embedding.builder()
                .input(contentList)
                .model("text-embedding-3-large")
                .build();
        EmbeddingResponse embedding = embeddingService.embedding(build);
        List<List<Float>> vectors = embedding.getData().stream().map(EmbeddingObject::getEmbedding).collect(Collectors.toList());

        VertorDataEntity vertorDataEntity = new VertorDataEntity();
        vertorDataEntity.setVector(vectors);
        vertorDataEntity.setContent(contentList);
        System.out.println(vertorDataEntity);

        // 7. 向量存储至pinecone
        Integer count = pineconeService.insert(vertorDataEntity, "abc-123-abc");
        System.out.println(count > 0 ? "存储成功" : "存储失败");
    }

}

下图是插入成功的数据

9.5 知识库查询

下面代码只多了对embedding的处理,chat部分基本不变。


    @Autowired
    private AiService aiService;
    @Autowired
    private PineconeService pineconeService;
    private List<ChatMessage> history = new ArrayList<>();


    @GetMapping("/chatStream")
    public SseEmitter getChatMessageStream(@RequestParam String question) throws Exception { 
        SseEmitter emitter = new SseEmitter(-1L);

        // 获取Embedding服务
        IEmbeddingService embeddingService = aiService.getEmbeddingService(PlatformType.OPENAI);

        // 构建要查询的问题,转为向量
        Embedding build = Embedding.builder()
                .input(question)
                .model("text-embedding-3-large")
                .build();
        EmbeddingResponse embedding = embeddingService.embedding(build);
        List<Float> questionEmbedding = embedding.getData().get(0).getEmbedding();

        // 构建向量数据库的查询对象
        PineconeQuery pineconeQueryReq = PineconeQuery.builder()
                .namespace("abc-123-abc")
                .topK(5)
                .vector(questionEmbedding)
                .build();

        // 查询
        // PineconeQueryResponse queryResponse = pineconeService.query(pineconeQueryReq);
        // delimiter为想用什么字符拼接查询出来的内容
        String retrievalContent = pineconeService.query(pineconeQueryReq, " ");

        String contentFormat = "你是一个善于回答中华人民共和国刑法相关问题的助手。请使用以下提供的检索内容和自身知识来回答问题。如果你不知道答案,请直接说不知道,不要杜撰答案。请用三句话以内回答,保持简洁。\n" +
                "\n" +
                "问题:%s\n" +
                "\n" +
                "检索内容:%s";

        String content = String.format(contentFormat, question, retrievalContent);


        // 获取DEEPSEEK的聊天服务
        IChatService chatService = aiService.getChatService(PlatformType.DEEPSEEK);


        history.add(ChatMessage.withUser(content)); // 向历史中添加用户输入
        // 创建请求参数
        ChatCompletion chatCompletion = ChatCompletion.builder()
                .model("deepseek-chat")
                .messages(history) // 添加完整历史消息
                .build();


        Executors.newSingleThreadExecutor().submit(() -> { 
            try { 
                SseListener sseListener = new SseListener() { 
                    @Override
                    protected void send() { 
                        try { 
                            emitter.send(this.getCurrStr());
                            System.out.println(this.getCurrData());  // 打印当前发送的内容
                        } catch (IOException e) { 
                            emitter.completeWithError(e);
                        }
                    }
                };

                emitter.onCompletion(() -> { 
                    System.out.println("完成");
                    history.add(ChatMessage.withAssistant(sseListener.getOutput().toString())); // 向历史中添加AI回复
                    sseListener.getEventSource().cancel();
                });

                // 发送流式数据
                chatService.chatCompletionStream(chatCompletion, sseListener);

                // 完成后关闭连接
                emitter.complete();
            } catch (Exception e) { 
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }

原文链接:Java快速接入DeepSeek实现流式、联网、知识库以及多轮问答

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容