【DeepSeek 聊天】五分钟部署本地 DeepSeek

前言

这里需要做到的可以根据上下文内容进行聊天,下面是做到的简单效果图

一、Ollama与DeepSeek-R1简介

1. Ollama是什么?

Ollama是一款开源的大模型本地化部署工具,支持一键安装、管理和运行多种大型语言模型

。其核心优势在于:

  • 提供类似Docker的模型管理体验(创建、加载、运行)
  • 支持Windows/macOS/Linux多平台
  • 仅需命令行即可完成所有操作
  • 默认API接口(11434端口)便于集成其他应用

2. DeepSeek-R1:7B模型特点

作为国产明星开源模型,DeepSeek-R1:7B在代码生成与中文理解方面表现优异:

  • 70亿参数规模平衡性能与资源消耗
  • 支持128K超长上下文处理
  • 特别优化中文语义理解能力
  • 适用于技术问答、文档总结等场景

二、Ollama安装指南(Mac & Windows)

🍎 Mac用户安装步骤

方法1:Homebrew安装(推荐)

bash 复制代码
# 安装Homebrew(已安装可跳过)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 通过Homebrew安装Ollama
brew install ollama

# 启动
ollama serve &

# 验证安装
ollama --version

方法2:手动安装

  1. 访问官网下载页面:ollama.com/download/ma...

  2. 下载Ollama-darwin.zip并解压

  3. 将Ollama拖入应用程序文件夹

  4. 首次运行时在终端授权:

    bash 复制代码
    xattr -d com.apple.quarantine /Applications/Ollama.app

⊞ Windows用户安装步骤

  1. 访问官网下载安装包:ollama.com/download

  2. 双击运行OllamaSetup.exe

  3. 按默认设置完成安装(无需手动配置环境变量

  4. 验证安装:

    powershell 复制代码
    ollama --version

三、DeepSeek-R1:7B模型部署

1. 一键安装模型

bash 复制代码
# 终端执行(Mac/Win命令相同)
ollama run deepseek-r1:7b
  • 首次运行自动下载(约4.7GB)

  • 终端显示下载进度:

    markdown 复制代码
    pulling manifest ███████████████ 100%
    pulling 96c415656d37... ███████████████ 4.7GB
    ...
    writing manifest success

2. 验证模型安装

bash 复制代码
ollama list
text 复制代码
NAME              SIZE  
deepseek-r1:7b    4.7GB

四、模型测试与交互方式

1. API接口测试

bash 复制代码
curl http://localhost:11434/api/generate -d '{
  "model": "deepseek-r1:7b",
  "prompt": "你好,你是谁",
  "stream": false
}'

# 接口返回结构
{
    "model": "deepseek-r1:7b",
    "created_at": "2025-07-10T02:47:51.018357Z",
    "response": "<think>\n我是DeepSeek-R1,一个由深度求索公司开发的智能助手,我会尽我所能为您提供帮助。\n</think>\n\n我是DeepSeek-R1,一个由深度求索公司开发的智能助手,我会尽我所能为您提供帮助。",
    "done": true,
    "done_reason": "stop",
    "context": [
        151644,
        108386,
        ......
    ],
    "total_duration": 3981124167,
    "load_duration": 2727482917,
    "prompt_eval_count": 7,
    "prompt_eval_duration": 185178833,
    "eval_count": 53,
    "eval_duration": 1064137792
}

五、spring boot 中使用模型聊天

1、添加流式支持的依赖

目前的 AI 聊天都是逐字显示的,就需要用到这个流式依赖

xml 复制代码
<!-- WebFlux (SSE流式支持) -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

2、核心服务方法

java 复制代码
package com.spring.ldj.ollama;

import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class OllamaChatService {

    private final WebClient webClient = WebClient.builder().build();
    private final ObjectMapper objectMapper = new ObjectMapper();
    private Map<String, Boolean> isThinking = new HashMap<>();
    // 是否回显思考流程
    private static final boolean SHOW_THINKING = false;
    // 适合单次回答
//    private static final String OLLAMA_URL = "http://localhost:11434/api/generate";
    // 适合聊天模式,可根据上下文生成内容
    private static final String OLLAMA_URL = "http://localhost:11434/api/chat";

    public Flux<String> chat(String sessionId, String userInput) {
        // 1. 添加上下文
        ChatContextManager.addMessage(sessionId, "user", userInput);
        List<Map<String, String>> fullContext = ChatContextManager.getContext(sessionId);
        for (Map<String, String> context : fullContext) {
            System.out.println(JSONUtil.parse(context));
        }

        // 2. 构造Ollama请求体
        Map<String, Object> request = new HashMap<>();
        request.put("model", "deepseek-r1:7b");
        request.put("messages", fullContext);
        request.put("stream", true); // 关闭流式
        request.put("options", Map.of(
                "temperature", 0.7,  // 控制随机性
                "num_ctx", 4096      // 上下文窗口大小
        ));
        StringBuilder sb = new StringBuilder(); // 临时存储流式上下文
        // 是否显示思考内容
        isThinking.put(sessionId,true);
        return webClient.post()
                .uri(OLLAMA_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(request)
                .retrieve()
                .bodyToFlux(String.class) // 接收文本流
                .map(context -> parseResponseChunk(context,sessionId))
                .doOnNext(content -> {
                    if (StringUtils.isNotBlank(content)){
                        sb.append(content);
                    }
                    if (content.equals("[DONE]")) {
                        // 结束时保存完整上下文
                        ChatContextManager.addMessage(sessionId, "assistant", sb.toString());
                    }
                }); // 解析每个数据块
    }

    private String parseResponseChunk(String json, String sessionId) {
        try {
            System.out.println("parseResponseChunk: " + json);
            JsonNode node = objectMapper.readTree(json);
            JsonNode messageNode = node.has("message") ? objectMapper.readTree(node.get("message").toString()) : objectMapper.nullNode();
            String content = messageNode.has("content") ? messageNode.get("content").asText() : "";
            if (!SHOW_THINKING){
                if (content.equals("</think>")){
                    isThinking.remove(sessionId);
                    return "";
                }
                if (isThinking.containsKey(sessionId)){
                    return "";
                }
            }
            boolean isDone = node.has("done") && node.get("done").asBoolean();
            if (isDone) {
                content = "[DONE]";
            }
            return content;
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON parsing error", e);
        }
    }


}

3、接口

java 复制代码
@RestController
@RequestMapping("/api/ai")
public class AiController {

    @Autowired
    private OllamaChatService ollamaChatService;

    @GetMapping("/getSessionId")
    public CommonResult<String> getSessionId() {
        String response = DateUtil.format(new Date(), "yyyyMMddHHmmss");
        return CommonResult.ok(response);
    }

    // SSE流式接口
    @GetMapping(value = "/ask")
    public SseEmitter ask1(@RequestParam String content, @RequestParam String sessionId) {
        SseEmitter emitter = new SseEmitter(30_000L);
        ollamaChatService.chat(sessionId,content)
                .subscribe(
                        chunk -> {
                            try {
                                emitter.send(SseEmitter.event().data(chunk));
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        }, // 发送数据块
                        emitter::completeWithError, // 错误处理
                        emitter::complete // 流结束
                );
        return emitter;// 超时30秒
    }

}

4、前端

vue 复制代码
<template>
  <div class="chat-container">
    <div class="chat-history" ref="history">
      <!-- 消息容器添加对齐类 -->
      <div 
        v-for="msg in messages" 
        :key="msg.id" 
        :class="['message', msg.type]"
      >
        <div class="message-bubble">
          {{ msg.content }}
        </div>
      </div>
      <div v-if="isLoading" class="loading">回答中...</div>
    </div>
    
    <div class="input-area">
      <textarea 
        v-model="inputContent"
        @keyup.enter="fetchStreamResponse"
        placeholder="输入您的问题..."
      ></textarea>
      <button 
        @click="fetchStreamResponse"
        :disabled="isLoading"
      >
        {{ isLoading ? '发送中...' : '发送' }}
      </button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      inputContent: '',
      aiResponse: "",
      messages: [],
      isLoading: false,
      chatHistory: [],
      sessionId: ''
    }
  },
  methods: {
    async fetchStreamResponse() {
      // 添加用户消息
      this.messages.push({
        id: Date.now(),
        type: "user",
        content: this.inputContent
      });
      // 初始化AI消息
      const aiMsgId = Date.now() + 1;
      this.messages.push({
        id: aiMsgId,
        type: "ai",
        content: "" // 初始为空
      });
      // 创建SSE连接
      console.log('创建SSE连接',this.inputContent);
      const eventSource = new EventSource(`/ldj001/api/ai/ask1?sessionId=${this.sessionId}&content=${encodeURIComponent(this.inputContent)}`, 
        { withCredentials: true } // 处理跨域
      );
      this.inputContent = '';
      this.isLoading = true;
      this.aiResponse = ""; // 重置响应内容

      eventSource.onmessage = (event) => {
        console.log(event);
        const chunk = event.data;

        // 结束
        if (chunk === "[DONE]") {
          eventSource.close(); // 主动关闭连接
          this.isLoading = false;
          return; // 终止后续处理
        }
        this.aiResponse += chunk;
        
        // 更新AI消息内容
        const aiMsgIndex = this.messages.findIndex(msg => msg.id === aiMsgId);
        if (aiMsgIndex !== -1) {
          this.messages[aiMsgIndex].content = this.aiResponse;
        }

        this.scrollToBottom(); // 自动滚动到底部
      };

      eventSource.onerror = (error) => {
        console.error("Stream error:", error);
        eventSource.close();
      };
    },
    scrollToBottom() {
      this.$nextTick(() => { // 确保DOM更新后执行
        const container = this.$refs.history;
        container.scrollTop = container.scrollHeight;
      });
    }
  },
  async created() {
    try {
      this.isLoading = true
      const response = await fetch('/ldj001/api/ai/getSessionId', {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json'
        }
      })
      if (!response.ok) {
        throw new Error(`请求失败: ${response.status}`)
      }
      const rawText = await response.text() // 先获取原始文本
      console.log('Raw response:', rawText) // 检查实际内容
      const data = rawText ? JSON.parse(rawText) : null
      this.sessionId = data.content
    } catch (err) {
      console.error('获取sessionId失败:', err)
      this.error = err.message || '获取sessionId失败'
    } finally {
      this.isLoading = false
    }
  }
}
</script>

<style scoped>
/* 基础布局 */
.chat-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.chat-history {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
}

/* 消息通用样式 */
.message {
  margin-bottom: 15px;
  max-width: 80%; /* 控制最大宽度 */
  align-self: flex-start; /* 默认左对齐 */
}

.message-bubble {
  padding: 12px 16px;
  border-radius: 18px;
  display: inline-block;
  word-wrap: break-word;
  white-space: pre-line; /* 保留换行符 */
}

/* 用户消息右对齐 */
.message.user {
  align-self: flex-end; /* 右对齐 */
}

.message.user .message-bubble {
  background-color: #dcf8c6; /* 用户气泡颜色 */
  border-bottom-right-radius: 4px; /* 气泡尖角效果 */
}

/* AI消息左对齐 */
.message.ai .message-bubble {
  background-color: #f0f0f0; /* AI气泡颜色 */
  border-bottom-left-radius: 4px; /* 气泡尖角效果 */
}

/* 输入区域样式 */
.input-area {
  padding: 15px;
  border-top: 1px solid #eee;
  display: flex;
  gap: 10px;
}

textarea {
  flex: 1;
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 20px;
  resize: none;
  height: 50px;
}

button {
  padding: 0 20px;
  border: none;
  border-radius: 20px;
  background: #0084ff;
  color: white;
  cursor: pointer;
}
</style>
相关推荐
求知摆渡40 分钟前
共享代码不是共享风险——公共库解耦的三种进化路径
java·后端·架构
李元豪43 分钟前
【知足常乐ai笔记】机器人强化学习
人工智能·笔记·机器人
沫儿笙44 分钟前
焊接机器人智能节气装置
人工智能·机器人
MidJourney中文版1 小时前
老年人与机器人玩具的情感连接
人工智能·机器人·语音识别
Codebee1 小时前
AI驱动的低代码革命:解构与重塑开发范式
人工智能·低代码·代码规范
数据库安全1 小时前
首批|美创智能数据安全分类分级平台获CCIA“网络安全新产品”
大数据·人工智能·web安全
Dymc1 小时前
【目标检测之Ultralytics预测框颜色修改】
人工智能·yolo·目标检测·计算机视觉
brzhang1 小时前
前端死在了 Python 朋友的嘴里?他用 Python 写了个交互式数据看板,着实秀了我一把,没碰一行 JavaScript
前端·后端·架构
该用户已不存在1 小时前
不知道这些工具,难怪的你的Python开发那么慢丨Python 开发必备的6大工具
前端·后端·python
HuashuiMu花水木1 小时前
PyTorch笔记3----------统计学相关函数
人工智能·pytorch·笔记