deepseek4j-demo快速入门
deepseek4j 是面向 DeepSeek 推出的 Java 开发 SDK,支持 DeepSeek R1 和 V3 全系列模型。提供对话推理、函数调用、JSON结构化输出、以及基于 OpenAI 兼容 API 协议的嵌入向量生成能力。通过 Spring Boot Starter 模块,开发者可以快速为 Spring Boot 2.x/3.x 以及 Solon 等主流 Java Web 框架集成 AI 能力,提供开箱即用的配置体系、自动装配的客户端实例,以及便捷的流式响应支持。
文章目录
1. 目标
实现ai对话
2.上代码
2.1.Maven 依赖
在你的 pom.xml
中添加以下依赖:
<dependency>
<groupId>io.github.pig-mesh.ai</groupId>
<artifactId>deepseek-spring-boot-starter</artifactId>
<version>1.4.3</version>
</dependency>
2.2.基础配置
在 application.yml
或 application.properties
中添加必要的配置:
deepseek:
api-key: your-api-key-here # 必填项:你的 API 密钥
model: deepseek-reasoner
base-url: https://api.deepseek.com # 可选,默认为官方 API 地址
2.3. 基础使用示例
2.3.1. 流式返回示例
@Autowired
private DeepSeekClient deepSeekClient;
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatCompletionResponse> chat(String prompt) {
return deepSeekClient.chatFluxCompletion(prompt);
}
2.3.2. 进阶配置示例
- ChatCompletionRequest 配置构造
@GetMapping(value = "/chat/advanced", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatCompletionResponse> chatAdvanced(String prompt) {
ChatCompletionRequest request = ChatCompletionRequest.builder()
.addSystemMessage("你的名字叫土豆")
// 添加用户消息
.addUserMessage(prompt)
// 设置最大生成 token 数,默认 2048
.maxTokens(1000)
// 设置响应格式,支持 JSON 结构化输出
// .responseFormat(...) // 可选
// function calling
// .tools(...) // 可选
.build();
return deepSeekClient.chatFluxCompletion(request);
}
2.3.3. 多轮会话
public final static HashMap<String, String> cache = new HashMap<>();
@GetMapping(value = "/chat/advanced", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatCompletionResponse> chatAdvanced(String prompt, String cacheCode) {
log.info("cacheCode {}", cacheCode);
ChatCompletionRequest request = ChatCompletionRequest.builder()
.addUserMessage(prompt)
.addAssistantMessage(elt.apply(cache.getOrDefault(cacheCode, "")))
.addSystemMessage("你是一个专业的助手").maxCompletionTokens(5000).build();
log.info("request {}", Json.toJson(request));
// 只保留上一次回答内容
cache.remove(cacheCode);
return deepSeekClient.chatFluxCompletion(request).doOnNext(i -> {
String content = choicesProcess.apply(i.choices());
// 其他ELT流程
cache.merge(cacheCode, content, String::concat);
}).doOnError(e -> log.error("/chat/advanced error:{}", e.getMessage()));
}
Function<List<ChatCompletionChoice>, String> choicesProcess = list -> list.stream().map(e -> e.delta().content())
.collect(Collectors.joining());
Function<String, String> elt = s -> s.replaceAll("<think>[\\s\\S]*?</think>", "").replaceAll("\n", "");
2.3.4. 同步输出 (非实时响应流)
不推荐使用同步阻塞调用方式,R1模型推理耗时较长易导致客户端连接超时,且响应延迟会影响用户体验
@GetMapping(value = "/sync/chat")
public ChatCompletionResponse syncChat(String prompt) {
ChatCompletionRequest request = ChatCompletionRequest.builder()
// 根据渠道模型名称动态修改这个参数
.model(deepSeekProperties.getModel())
.addUserMessage(prompt).build();
return deepSeekClient.chatCompletion(request).execute();
}
2.4. 前端调试页面
see.html
<!DOCTYPE html>
<html lang="zh-CN" :data-theme="currentTheme">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>deepseek 调试</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/marked/15.0.6/marked.min.js"></script>
<!-- Tailwind CSS -->
<link href="https://cdn.bootcdn.net/ajax/libs/daisyui/4.12.23/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
daisyui: {
themes: [{
light: {
"primary": "#570DF8",
"primary-focus": "#4506CB",
"secondary": "#F000B8",
"accent": "#37CDBE",
"neutral": "#3D4451",
"base-100": "#FFFFFF",
"base-200": "#F2F2F2",
"base-300": "#E5E6E6",
"base-content": "#1F2937",
"info": "#3ABFF8",
"success": "#36D399",
"warning": "#FBBD23",
"error": "#F87272"
},
dark: {
"primary": "#BB86FC",
"primary-focus": "#9965E3",
"secondary": "#03DAC6",
"accent": "#BB86FC",
"neutral": "#121212",
"base-100": "#1E1E1E",
"base-200": "#2C2C2C",
"base-300": "#242424",
"base-content": "#E1E1E1",
"info": "#0175C2",
"success": "#00C853",
"warning": "#FFB74D",
"error": "#CF6679",
}
}],
},
}
</script>
<style>
/* 暗色模式下的滚动条样式 */
[data-theme='dark'] ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
[data-theme='dark'] ::-webkit-scrollbar-track {
background: #2C2C2C;
border-radius: 4px;
}
[data-theme='dark'] ::-webkit-scrollbar-thumb {
background: #424242;
border-radius: 4px;
}
[data-theme='dark'] ::-webkit-scrollbar-thumb:hover {
background: #505050;
}
</style>
</head>
<body class="h-screen transition-colors duration-200 flex flex-col overflow-hidden">
<div id="app">
<!-- 导航栏 -->
<div class="navbar bg-base-100 shadow-lg px-4 flex-none h-16">
<div class="flex-1">
<h1 class="text-xl font-bold">Deepseek 调试工具</h1>
</div>
<div class="flex-none gap-2">
<div class="dropdown dropdown-end">
<label class="swap swap-rotate btn btn-ghost btn-circle">
<input type="checkbox"
:checked="currentTheme === 'dark'"
@change="toggleTheme"
class="theme-controller"/>
<!-- sun icon -->
<svg class="swap-on fill-current w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"/></svg>
<!-- moon icon -->
<svg class="swap-off fill-current w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"/></svg>
</label>
</div>
</div>
</div>
<main class="h-[calc(100vh-8rem)] container mx-auto px-4 py-2 overflow-hidden">
<div class="flex gap-4 h-full">
<!-- 左侧原始数据区域 -->
<div class="w-1/2 flex flex-col">
<!-- 头部控制区 -->
<div class="bg-base-100 dark:bg-base-200 rounded-box shadow-lg dark:shadow-lg/20 p-4 mb-2 flex-none backdrop-blur-sm">
<div class="mb-4 flex items-center">
<label class="label w-20">
<span class="label-text dark:text-base-content/80">SSE地址</span>
</label>
<input
type="text"
v-model="sseUrl"
placeholder="请输入SSE地址"
class="input input-bordered w-full dark:bg-base-300 dark:border-base-content/10 dark:text-base-content/90 dark:placeholder-base-content/50"
>
</div>
<div class="flex space-x-2">
<button
@click="connect"
:disabled="isConnected"
class="btn btn-primary"
>
连接
</button>
<button
@click="disconnect"
:disabled="!isConnected"
class="btn btn-error"
>
断开
</button>
<button
@click="clearMessages"
class="btn btn-ghost"
>
清空
</button>
</div>
</div>
<!-- 消息展示区 -->
<div class="flex-1 flex flex-col min-h-0">
<h2 class="text-xl font-bold mb-1 dark:text-base-content/90">原始数据</h2>
<div class="flex-1 overflow-y-auto min-h-0 messages-container">
<div class="mockup-window bg-base-300 dark:bg-base-300/50 h-full flex flex-col backdrop-blur-sm">
<div class="flex-1 px-6 py-4 bg-base-200 dark:bg-base-200/50 overflow-y-auto">
<div v-if="messages.length === 0" class="text-gray-400 dark:text-gray-500 text-center py-4">
暂无消息
</div>
<div
v-for="(message, index) in messages"
:key="index"
class="border-b border-base-200 last:border-0 py-3"
>
<div class="text-xs opacity-70 mb-1">{{ message.time }}</div>
<pre class="mockup-code mt-2">{{ message.data }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧推理过程和答案区域 -->
<div class="w-1/2 flex flex-col gap-2 min-h-0">
<div class="flex-1 flex flex-col min-h-0">
<h2 class="text-xl font-bold mb-1">推理过程</h2>
<div ref="reasoningRef" class="flex-1 overflow-y-auto min-h-0">
<div class="mockup-window bg-base-300 h-full flex flex-col">
<div class="flex-1 px-6 py-4 bg-base-200 overflow-y-auto">
{{ reasoningChain }}
</div>
</div>
</div>
</div>
<div class="flex-1 flex flex-col min-h-0">
<h2 class="text-xl font-bold mb-1">最终答案</h2>
<div ref="answerRef" class="flex-1 overflow-y-auto min-h-0">
<div class="mockup-window bg-base-300 h-full flex flex-col">
<div class="flex-1 px-6 py-4 bg-base-200 overflow-y-auto prose dark:prose-invert max-w-none" v-html="finalAnswer">
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="footer footer-center p-2 bg-base-300 dark:bg-base-300/50 text-base-content dark:text-base-content/70 flex-none h-12 backdrop-blur-sm">
<aside class="flex items-center gap-2">
<p>Powered by</p>
<div class="flex items-center gap-1">
<span class="font-semibold">Pig AI</span>
</div>
</aside>
</footer>
</div>
<script>
const { createApp, ref, computed, watch, nextTick, onMounted } = Vue
createApp({
setup() {
const sseUrl = ref('http://127.0.0.1:8080/chat?prompt=你叫什么名字')
const messages = ref([])
const isConnected = ref(false)
const currentTheme = ref('light')
const isInThinkTag = ref(false)
let eventSource = null
const reasoningRef = ref(null)
const answerRef = ref(null)
// 计算推理过程(思考链)
const reasoningChain = computed(() => {
return messages.value
.filter(m => m.parsed?.reasoning_content)
.map(m => m.parsed.reasoning_content)
.join('')
})
// 计算最终答案
const finalAnswer = computed(() => {
const rawContent = messages.value
.filter(m => m.parsed?.content)
.map(m => m.parsed.content)
.join('')
return marked.parse(rawContent)
})
// 初始化主题
onMounted(() => {
const savedTheme = localStorage.getItem('theme')
currentTheme.value = savedTheme || 'light'
document.documentElement.setAttribute('data-theme', currentTheme.value)
})
// 切换主题
const toggleTheme = () => {
currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light'
localStorage.setItem('theme', currentTheme.value)
document.documentElement.setAttribute('data-theme', currentTheme.value)
}
// 滚动到底部的函数
const scrollToBottom = (element) => {
if (element) {
// 获取实际的滚动容器(mockup-window 内的 overflow-y-auto 元素)
const scrollContainer = element.querySelector('.overflow-y-auto')
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight
}
}
}
// 监听消息变化,自动滚动
watch(() => [messages.value.length, reasoningChain.value, finalAnswer.value], () => {
nextTick(() => {
if (reasoningRef.value) {
scrollToBottom(reasoningRef.value)
}
if (answerRef.value) {
scrollToBottom(answerRef.value)
}
// 滚动原始数据区域
const messagesContainer = document.querySelector('.messages-container .overflow-y-auto')
if (messagesContainer) {
messagesContainer.scrollTop = messagesContainer.scrollHeight
}
})
}, { deep: true })
const parseSSEData = (data) => {
try {
const parsed = JSON.parse(data)
// 检查是否直接返回了 reasoning_content
const directReasoning = parsed.choices?.[0]?.delta?.reasoning_content
if (directReasoning) {
return {
id: parsed.id,
created: parsed.created,
model: parsed.model,
reasoning_content: directReasoning,
content: parsed.choices?.[0]?.delta?.content || ''
}
}
const content = parsed.choices?.[0]?.delta?.content || ''
// 处理 think 标签包裹的情况
if (content.includes('<think>')) {
isInThinkTag.value = true
const startIndex = content.indexOf('<think>') + '<think>'.length
return {
id: parsed.id,
created: parsed.created,
model: parsed.model,
reasoning_content: content.substring(startIndex),
content: content.substring(0, content.indexOf('<think>'))
}
}
if (content.includes('</think>')) {
isInThinkTag.value = false
const endIndex = content.indexOf('</think>')
return {
id: parsed.id,
created: parsed.created,
model: parsed.model,
reasoning_content: content.substring(0, endIndex),
content: content.substring(endIndex + '</think>'.length)
}
}
// 根据状态决定内容归属
return {
id: parsed.id,
created: parsed.created,
model: parsed.model,
reasoning_content: isInThinkTag.value ? content : '',
content: isInThinkTag.value ? '' : content
}
} catch (e) {
console.error('解析JSON失败:', e)
return null
}
}
const connect = () => {
if (eventSource) {
eventSource.close()
}
try {
eventSource = new EventSource(sseUrl.value)
isConnected.value = true
eventSource.onmessage = (event) => {
const parsed = parseSSEData(event.data)
messages.value.push({
time: new Date().toLocaleTimeString(),
data: event.data,
parsed: parsed
})
}
eventSource.onerror = (error) => {
console.error('SSE Error:', error)
disconnect()
}
} catch (error) {
console.error('Connection Error:', error)
disconnect()
}
}
const disconnect = () => {
if (eventSource) {
eventSource.close()
eventSource = null
}
isConnected.value = false
}
const clearMessages = () => {
messages.value = []
}
return {
sseUrl,
messages,
isConnected,
currentTheme,
toggleTheme,
connect,
disconnect,
clearMessages,
reasoningChain,
finalAnswer,
reasoningRef,
answerRef
}
}
}).mount('#app')
</script>
</body>
</html>
3. 测试
3.1. chat单元测试
@Test
public void chat(){
Flux<ChatCompletionResponse> responseFlux = deepSeekController.chat("你叫什么名字");
responseFlux
.doOnNext(item ->
{
try {
System.out.print(item.choices().get(0).delta().content());
}catch (Exception e){}
}
)
.doOnError(error -> log.error("Error: {}", error))
.doOnComplete(() -> log.info("Completed"))
.subscribe();
System.out.println(responseFlux.collectList().block());
}
3.2. chatAdvanced单元测试
@Test
public void chatAdvanced(){
Flux<ChatCompletionResponse> responseFlux = deepSeekController.chatAdvanced("你叫什么名字");
responseFlux
.doOnNext(item ->
{
try {
System.out.print(item.choices().get(0).delta().content());
}catch (Exception e){}
}
)
.doOnError(error -> log.error("Error: {}", error))
.doOnComplete(() -> log.info("Completed"))
.subscribe();
System.out.println(responseFlux.collectList().block());
}
3.3. see.html测试
点击连接
下面的deepseek4j-demo