Java快速接入DeepSeek实现流式、联网、知识库以及多轮问答
本文将详细的说明,如何使用Java、JDK8快速接入deepseek的聊天服务,包含官方的API服务,以及本地Ollama的服务。并搭建一个简单的前端界面,用于流式输出、多轮问答、联网、知识库问答的效果展示。
1. 创建spring boot应用
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
中配置
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;
}
修改两处即可:
- 修改
PlatformType
为OLLAMA
- 修改
model
为deepseek-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. 联网对话
- 配置
application.yml
中的searxng
,将其中的url
替换为你已经部署的searxng
服务的地址。
ai:
websearch:
searxng:
url: "http://127.0.0.1:29080/search"
nums: 10
- 修改代码,添加联网对话的功能:
@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
模型为例子
创建完成后,记录自己的Host
,我们后面要用到
创建自己的API Key
9.2 配置application.yml
请将上文得到的Host
和API 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;
}