DeepSeek官网太卡了,自己使用Spring AI和Ollama本地部署使用DeepSeek

520 阅读6分钟

前言

DeepSeek太火了,我使用官网的DeepSeek每次只能回答一次,之后就会服务器繁忙,请稍后再试。虽然DeepSeek确实非常nice,使用起来我感觉真的非常好,但是现在这种情况对我来说是非常难受的。之后我自己看到了很多教人本地部署DeepSeek的教程,于是自己也试着去通过Spring AI和Ollama本地部署DeepSeek来使用。


DeepSeek介绍

DeepSeek是一家专注于人工智能领域的创新型科技公司,致力于开发先进的大型语言模型。其发布的多个模型,如DeepSeek LLM、DeepSeek-Coder、DeepSeek Math等,在各自领域取得了显著成果。特别是DeepSeek-R1模型,其性能对标OpenAI的o1正式版,在数学、代码、自然语言推理等任务上表现出色。

Ollama介绍

Ollama是一个基于Go语言的本地大语言模型运行框架,类似于Docker产品,保留了Docker的操作习惯,并支持上传大语言模型仓库。Ollama支持多种大型语言模型,如Llama 2、Code Llama、Mistral等,并允许用户根据特定需求定制和创建自己的模型。

一、环境准备

在开始之前,你需要确保你自己的开发环境满足一下需求:

  • Ollama
  • JDK17
  • SpringBoot 3.x
  • Maven或Gradle构建工具

Ollama的部署需要你自己去网上找教程,网上非常多,安装Ollama之后,请下载DeepSeek模型。

二、项目结构

树型结构图:

├── deepseek-ollama
│   ├── src  # 源代码文件夹
│   │   ├── main  # 主要源代码目录
│   │   │   ├── java  # Java 源代码文件夹
│   │   │   │   └── com.linyi  # 包名
│   │   │   │       ├── config  # 配置类文件夹
│   │   │   │       │   ├── OllamaConfig.java  # Ollama 配置类
│   │   │   │       │   └── WebConfig.java  # Web 配置类
│   │   │   │       ├── controller  # 控制器文件夹
│   │   │   │       │   └── ChatController.java  # 聊天控制器类
│   │   │   │       └── DeekSeepollamaApplication.java  # 主应用程序启动类
│   │   │   ├── entity  # 实体类文件夹
│   │   │   │   ├── Chat.java  # 聊天实体类
│   │   │   │   └── Result.java  # 结果实体类
│   │   │   └── utils  # 工具类文件夹
│   │   │       └── MdUtil.java  # MarkDown 工具类
│   │   ├── resources  # 配置资源文件夹
│   │   │   ├── application.yml  # 应用配置文件
│   │   │   └── banner.txt  # 启动欢迎信息文本
│   └── test  # 测试代码目录
│       └── java  # Java 测试源代码文件夹

image.png


三、项目配置

下面是项目的pom.xml文件

image.png

 <dependencies>
        <!--knife4j-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>4.4.0</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--ollama-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
        </dependency>
        <!--devtools-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
​
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0-M3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>public</id>
            <name>aliyun nexus</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

主要的介绍:

名称版本说明
Java17编程语言
SpringBoot3.3.5基于Spring框架的开源Java框架
Spring AI1.0.0-M3人工智能集成到Spring应用程序中
Ollama1.0.0-M3与Spring AI相关的项目或库
Knife4j4.4.0Swagger UI在中国的增强版

以下是项目的应用配置文件:

image.png

server:
  port: 9292
  servlet:
    context-path: /api
spring:
  application:
    name: deepseek-ollama
    description: deepseek-ollama 是一个结合了 Spring AI  Ollama 的项目,完成调用本地部署的 deepseek 模型的功能。
# springdoc-openapi项目配置
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  api-docs:
    path: /v3/api-docs
  group-configs:
    - group: 'default'
      packages-to-scan: com.linyi.controller
# knife4j的增强配置,不需要增强可以不配
knife4j:
  enable: true
  setting:
    language: zh_cn
# 配置ai
ai:
  ollama:
    url: http://localhost:11434 #这里是我本地部署的ollama默认地址(需要根据你自己的更换)
    chat:
      options:
        model: deepseek-r1:14b # 模型名称 本地ollama 下载的模型名称(需要根据你自己的更换)

请注意项目的配置ai部分,url和model需要根据你自己的配置。


四、功能实现

创建Ollama配置:

image.png

@Configuration
public class OllamaConfig {
​
    @Value("${ai.ollama.url}")
    private String url;
​
    /**
     * 创建并返回一个 OllamaChatModel 实例
     * 该实例使用配置文件中定义的 ollama 服务 URL 进行初始化
     *
     * @return 初始化后的 OllamaChatModel 实例
     */
    @Bean
    public OllamaChatModel getOllamaChatModel() {
        return new OllamaChatModel(new OllamaApi(url));
    }
}

如果发现调用出现跨域问题,需要配置:

image.png

@Configuration
public class WebConfig implements WebMvcConfigurer {
​
    /**
     * 开启跨域
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路由
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                //.allowedOrigins("/*")
                .allowedOriginPatterns("*")//生产环境下慎用,否则可能会被攻击
                // 设置允许的方法
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*") // 允许的头
                .allowCredentials(true); // 是否允许携带凭证
    }
​
}
​

接下来创建项目需要的实体类:

image.png

@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "对话")
public class Chat implements Serializable {
​
    @Serial
    private static final long serialVersionUID = 1L;
​
    //输入信息
    private String senderMessage;
​
    //输出信息
    private String receiverMessage;
​
    //状态 true:成功 false:失败
    private boolean status;
​
    //耗时
    private String cost;
​
    //模型
    private String model;
​
}
​
@Data
public class Result<T> {
    // 返回码
    private Integer code;
​
    // 返回消息
    private String message;
​
    // 返回数据
    private T data;
​
    // 私有构造函数,防止外部直接实例化
    private Result(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
​
    // 成功时的静态方法
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "Success", data);
    }
​
    // 成功时的静态方法,无数据
    public static <T> Result<T> success() {
        return new Result<>(200, "Success", null);
    }
​
    // 失败时的静态方法
    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null);
    }
}

创建管理类:

image.png

@Tag(name = "ChatController", description = "调用 Ollama 模型")
@Slf4j
@RestController
@SuppressWarnings({"unchecked", "rawtypes"})
public class ChatController {
    @Resource
    private OllamaConfig ollamaConfig;
​
    @Value("${ai.ollama.chat.options.model}")
    private String defaultChatOptionsModel;
​
    /**
     * 处理聊天请求的控制器方法
     *
     * @param msg 用户输入的聊天消息,不能为空
     * @return 返回聊天机器人的回复结果
     */
    @Operation(summary = "对话")
    @GetMapping("/chat")
    public Result<Chat> chat(String msg) {
        // 记录接收到的问题消息
        log.info("问题是:{}", msg);
        // 初始化结果字符串
        String result = "";
​
        // 检查消息是否为空,如果为空则返回错误提示
        if (StringUtils.isBlank(msg)) {
            return Result.error(400, "请输入chat内容");
        }
​
        // 创建Prompt对象,包含聊天消息和配置选项
        Prompt prompt = new Prompt(
                msg,
                OllamaOptions.builder()
                        .withModel(defaultChatOptionsModel)
                        .withTemperature(0.4)
                        .build()
        );
​
        // 记录开始时间
        long startTime = System.nanoTime();
​
        // 调用聊天模型并获取响应
        ChatResponse response = ollamaConfig.getOllamaChatModel().call(prompt);
​
        // 记录结束时间
        long endTime = System.nanoTime();
​
        // 计算并打印耗时
        String formattedTimeTaken = String.format("%.2f", (endTime - startTime) / 1e9);
        log.info("耗时: " + formattedTimeTaken + " 秒");
​
        // 提取并记录聊天机器人的回复内容
        result = response.getResult().getOutput().getContent();
        log.info("回答:{}", result);
​
        // 返回成功结果,包含聊天机器人的回复
        Chat chat = new Chat(msg, result, true, formattedTimeTaken, defaultChatOptionsModel);
        return Result.success(chat);
    }
​
    /**
     * 回答写入.md文件
     * 此方法接收用户消息,调用聊天模型生成回复,并将回复内容写入markdown文件中
     * 主要用于演示如何将聊天内容转换为文档保存
     *
     * @param msg 用户输入的消息,用于生成聊天回复
     * @return 聊天模型生成的回复内容
     */
    @Operation(summary = "回答写入.md文件")
    @GetMapping("/chatToMd")
    public Result<Chat> chatToMd(String msg) {
        // 记录用户提问内容
        log.info("问题是:{}", msg);
        // 初始化结果变量
        String result = "";
        // 验证输入内容是否为空
        if (StringUtils.isBlank(msg)) {
            return Result.error(400, "请输入chat内容");
        }
​
        // 构建聊天模型的提示信息,包括模型参数配置
        Prompt prompt = new Prompt(
                msg,
                OllamaOptions.builder()
                        .withModel(defaultChatOptionsModel)
                        .withTemperature(0.4)
                        .build()
        );
        // 记录开始时间
        long startTime = System.nanoTime();
​
        // 调用聊天模型生成回复
        ChatResponse response = ollamaConfig.getOllamaChatModel().call(prompt);
​
        // 记录结束时间
        long endTime = System.nanoTime();
​
        // 计算并打印耗时
        String formattedTimeTaken = String.format("%.2f", (endTime - startTime) / 1e9);
        log.info("耗时: " + formattedTimeTaken + " 秒");
​
        // 提取回复内容
        result = response.getResult().getOutput().getContent();
​
        // 记录回答内容
        log.info("回答:{}", result);
​
        // 回答写入.md文件
        long l = System.currentTimeMillis();
        MdUtil.createAndWriteMarkdownFile(result, l + ".md");
​
        // 返回成功结果
        Chat chat = new Chat(msg, result, true, formattedTimeTaken, defaultChatOptionsModel);
        return Result.success(chat);
    }
​
​
}

创建将DeepSeek回答写入.md文件的工具类:

image.png

public class MdUtil {
​
    /**
     * 创建并写入Markdown文件
     *
     * @param content 文件内容,即要写入的Markdown格式文本
     * @param fileName 要创建的文件名,包括路径和扩展名
     *
     * 此方法尝试创建一个指定名称的Markdown文件,并将给定的内容写入其中
     * 使用try-with-resources语句确保文件写入操作后资源能自动关闭
     */
    public static void createAndWriteMarkdownFile(String content, String fileName) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
            writer.write(content);
            System.out.println("创建成功: " + fileName);
        } catch (IOException e) {
            throw new RuntimeException("创建文件失败: " + fileName, e);
        }
    }
}

以上就是我自己实现的一个简单的本地调用DeepSeek的SpringBoot项目。

下面是我项目的实现:

启动项目之后,我进入knife4j进行接口的调用:

image.png

我调用对话接口:

image.png

接着我调用回答写入.md文件这个接口:

image.png

image.png

image.png

项目的接口文档URL:http://{host}:{port}/api/doc.html

我就自己的项目源码已经开源: Gitee:gitee.com/hsdchb/deep…

GitHub:github.com/linyshdhhcb…

总结:

总的来说通过Ollama调用大模型非常方便,不用我们调用什么,就对接好就行,如果你感觉这样调用太反人类了,没有实用性,那确实如此,这个项目我就是拿来练练手,如果你想要nice的UI,可以去看看Open WebUIChatboxollama-gui。这些Ollama的图形化项目都是开源的,按照它要求进行部署,也可以直接调用Ollama的大模型。