Ollama 本地跑 DeepSeek-Coder V3 保姆级教程(Java 调用示例)

0 阅读11分钟

本文基于 Ollama 0.6.x + DeepSeek-Coder-V2:16b,全程离线运行,数据不出内网。适合希望在本地搭建 AI 代码辅助能力的 Java 开发者或架构师。

performance.png ▲ DeepSeek-Coder-V2 在代码/数学 Benchmark 上全面超越 GPT-4 Turbo 和 Claude 3 Opus(来源:DeepSeek 官方 GitHub)


一、为什么选择本地部署?

在 SaaS API 盛行的今天,选择本地跑大模型看起来"多此一举",但以下几个场景会让你觉得值:

  • 代码隐私:企业内部代码、未公开算法不适合上传到云端 API;
  • 零延迟抖动:内网请求无需排队,响应更稳定;
  • 无 Token 计费:跑多少都不花钱,适合集成到 CI/CD 流程;
  • 离线可用:断网环境(保密机房、飞行途中)照样跑。

二、环境准备

2.1 硬件要求

模型规格显存/内存需求推荐场景
deepseek-coder-v2:16b12 GB(GPU)/ 16 GB(CPU only)个人开发机、MacBook M 系列
deepseek-coder-v2:236b128 GB+服务器、高端工作站

本文以 16b 为例,M2 MacBook Pro(16 GB 统一内存)或 RTX 3080(12 GB 显存)均可流畅运行。

注意:CPU-only 模式也能跑,只是推理速度约为 GPU 的 1/5~1/10,日常调试够用。

2.2 软件依赖

  • macOS 12+ / Linux(Ubuntu 22.04+)/ Windows 11(WSL2)
  • Java 17+(本文示例基于 Java 21 虚拟线程)
  • Maven 3.9+ 或 Gradle 8+
  • Ollama(见下文安装)

三、安装 Ollama

macOS / Linux 一键安装

curl -fsSL https://ollama.com/install.sh | sh

安装完成后验证:

ollama --version
# ollama version 0.6.2

Windows(WSL2)

在 PowerShell 中启用 WSL2 后,进入 Ubuntu 子系统执行上述 curl 命令即可。Windows 原生版可直接从 ollama.com/download 下载安装包。

💡 扩展阅读:如果你希望用图形界面与本地模型对话(而不是纯 API 调用),可以配合 Open WebUI 使用,效果如下图所示:

Open WebUI — 本地 Ollama 的 ChatGPT 风格图形界面 ▲ Open WebUI 提供类 ChatGPT 的本地对话界面,支持模型切换、文档上传、RAG 等功能(来源:Open WebUI 官方 GitHub)


四、拉取 DeepSeek-Coder V2 模型

DeepSeek-Coder-V2 Logo转存失败,建议直接上传图片文件

ollama pull deepseek-coder-v2:16b

模型文件约 9 GB,国内网络建议挂代理或等待耐心拉取。拉取完成后本地缓存于 ~/.ollama/models/,后续无需重新下载。

查看已安装模型:

ollama list
# NAME                       ID              SIZE    MODIFIED
# deepseek-coder-v2:16b      8e9e29a4edce    9.1 GB  2 hours ago

五、启动 Ollama 服务

Ollama 安装后会自动注册系统服务(macOS 为 launchd,Linux 为 systemd)。手动启动:

# 前台运行(方便看日志)
ollama serve

# 或后台运行
nohup ollama serve > /tmp/ollama.log 2>&1 &

服务默认监听 http://127.0.0.1:11434,验证:

curl http://localhost:11434/api/tags

返回 JSON 中能看到已拉取的模型列表即表示服务正常。

远程访问配置(可选)

如果希望局域网内其他机器调用,修改监听地址:

OLLAMA_HOST=0.0.0.0:11434 ollama serve

安全提示:生产环境请在前面加 Nginx 反向代理并配置认证,不要将 11434 端口直接暴露到公网。


六、命令行快速测试

在集成 Java 之前,先用命令行验证模型是否正常:

ollama run deepseek-coder-v2:16b "用 Java 写一个线程安全的单例模式,要求双重检查锁定"

几秒内应该会流式输出代码。如能看到标准的 DCL 单例实现,说明模型已就绪。


七、Java 集成

Ollama 提供标准 HTTP REST API,兼容 OpenAI API 格式,因此 Java 接入方式非常灵活。下面从三个层次由浅入深演示。

7.1 项目结构

ollama-demo/
├── pom.xml
└── src/main/java/com/example/
    ├── OllamaClient.java          # 原生 HttpClient 封装
    ├── OllamaStreamClient.java    # 流式输出版本
    ├── OllamaOpenAIClient.java    # OpenAI 兼容接口版本
    └── Main.java                  # 示例入口

7.2 Maven 依赖

<dependencies>
    <!-- JSON 处理,使用 Jackson -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.17.2</version>
    </dependency>

    <!-- OpenAI Java SDK(兼容 Ollama) -->
    <!-- 如果使用 OpenAI 兼容接口方式,引入此依赖 -->
    <dependency>
        <groupId>com.theokanning.openai-gpt3-java</groupId>
        <artifactId>service</artifactId>
        <version>0.18.2</version>
    </dependency>
</dependencies>

7.3 方式一:原生 Java HttpClient(无第三方依赖)

这是最轻量的方式,适合不想引入额外依赖的场景。

package com.example;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

/**
 * Ollama 原生 REST API 封装
 * 对应接口:POST /api/generate
 */
public class OllamaClient {

    private static final String BASE_URL = "http://localhost:11434";
    private static final String MODEL    = "deepseek-coder-v2:16b";

    private final HttpClient    httpClient;
    private final ObjectMapper  mapper;

    public OllamaClient() {
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();
        this.mapper = new ObjectMapper();
    }

    /**
     * 同步调用,返回完整响应文本
     *
     * @param prompt 用户输入的提示词
     * @return 模型生成的文本
     */
    public String generate(String prompt) throws Exception {
        // 构建请求体
        ObjectNode body = mapper.createObjectNode();
        body.put("model", MODEL);
        body.put("prompt", prompt);
        body.put("stream", false);          // 非流式,一次性返回全部结果

        // 可选:设置推理参数
        ObjectNode options = mapper.createObjectNode();
        options.put("temperature", 0.2);    // 代码场景建议低温,减少随机性
        options.put("top_p", 0.9);
        options.put("num_ctx", 8192);       // 上下文窗口,根据显存调整
        body.set("options", options);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE_URL + "/api/generate"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body)))
                .timeout(Duration.ofMinutes(5))
                .build();

        HttpResponse<String> response = httpClient.send(
                request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException("Ollama 返回异常状态码:" + response.statusCode()
                    + "\n" + response.body());
        }

        JsonNode json = mapper.readTree(response.body());
        return json.get("response").asText();
    }

    public static void main(String[] args) throws Exception {
        OllamaClient client = new OllamaClient();
        String result = client.generate(
                "请用 Java 实现一个泛型栈(Stack),要求线程安全,并附上单元测试示例。"
        );
        System.out.println(result);
    }
}

7.4 方式二:流式输出(Streaming)

代码生成场景下,流式输出能让用户感知到"正在思考"的过程,体验更接近 Copilot。

package com.example;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.function.Consumer;

/**
 * 流式调用封装,逐 token 回调
 */
public class OllamaStreamClient {

    private static final String BASE_URL = "http://localhost:11434";
    private static final String MODEL    = "deepseek-coder-v2:16b";

    private final HttpClient   httpClient;
    private final ObjectMapper mapper;

    public OllamaStreamClient() {
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();
        this.mapper = new ObjectMapper();
    }

    /**
     * 流式生成,每收到一个 token 就触发 onToken 回调
     *
     * @param prompt   提示词
     * @param onToken  token 消费者,可用于实时打印或写入 SSE 响应
     * @param onDone   完成回调
     */
    public void generateStream(String prompt,
                               Consumer<String> onToken,
                               Runnable onDone) throws Exception {

        ObjectNode body = mapper.createObjectNode();
        body.put("model", MODEL);
        body.put("prompt", prompt);
        body.put("stream", true);    // 开启流式

        ObjectNode options = mapper.createObjectNode();
        options.put("temperature", 0.2);
        body.set("options", options);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE_URL + "/api/generate"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body)))
                .timeout(Duration.ofMinutes(10))
                .build();

        // 使用 InputStream 逐行读取 NDJSON(Newline-Delimited JSON)
        HttpResponse<java.io.InputStream> response = httpClient.send(
                request, HttpResponse.BodyHandlers.ofInputStream());

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(response.body()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.isBlank()) continue;

                JsonNode node = mapper.readTree(line);
                String token = node.path("response").asText("");

                if (!token.isEmpty()) {
                    onToken.accept(token);
                }

                // done=true 表示本次生成结束
                if (node.path("done").asBoolean(false)) {
                    onDone.run();
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        OllamaStreamClient client = new OllamaStreamClient();

        System.out.print("AI: ");
        client.generateStream(
                "用 Java 21 虚拟线程实现一个简单的 HTTP 服务器",
                token -> {
                    System.out.print(token);   // 实时打印每个 token
                    System.out.flush();
                },
                () -> System.out.println("\n\n[生成完毕]")
        );
    }
}

7.5 方式三:多轮对话(Chat API)

Ollama 同样支持 OpenAI 格式的 /api/chat 接口,便于维护多轮上下文。

package com.example;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

/**
 * 多轮对话封装,维护完整的消息历史
 */
public class OllamaChatClient {

    private static final String BASE_URL = "http://localhost:11434";
    private static final String MODEL    = "deepseek-coder-v2:16b";

    // 系统提示词,设定 AI 角色
    private static final String SYSTEM_PROMPT = """
            你是一位资深 Java 架构师,代码风格遵循阿里巴巴 Java 开发手册。
            回答时请:
            1. 先给出核心思路
            2. 提供可直接运行的完整代码
            3. 指出潜在的性能或安全风险
            """;

    private final HttpClient       httpClient;
    private final ObjectMapper     mapper;
    private final List<ObjectNode> messages;  // 消息历史

    public OllamaChatClient() {
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();
        this.mapper   = new ObjectMapper();
        this.messages = new ArrayList<>();

        // 注入系统角色
        ObjectNode sysMsg = mapper.createObjectNode();
        sysMsg.put("role", "system");
        sysMsg.put("content", SYSTEM_PROMPT);
        messages.add(sysMsg);
    }

    /**
     * 发送用户消息,返回 AI 回复
     */
    public String chat(String userMessage) throws Exception {
        // 追加用户消息
        ObjectNode userMsg = mapper.createObjectNode();
        userMsg.put("role", "user");
        userMsg.put("content", userMessage);
        messages.add(userMsg);

        // 构建请求
        ObjectNode body = mapper.createObjectNode();
        body.put("model", MODEL);
        body.put("stream", false);

        ArrayNode msgArray = mapper.createArrayNode();
        messages.forEach(msgArray::add);
        body.set("messages", msgArray);

        ObjectNode options = mapper.createObjectNode();
        options.put("temperature", 0.1);
        body.set("options", options);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE_URL + "/api/chat"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body)))
                .timeout(Duration.ofMinutes(5))
                .build();

        HttpResponse<String> response = httpClient.send(
                request, HttpResponse.BodyHandlers.ofString());

        JsonNode json = mapper.readTree(response.body());
        String assistantReply = json
                .path("message")
                .path("content")
                .asText();

        // 将 AI 回复追加到历史,维护上下文
        ObjectNode assistantMsg = mapper.createObjectNode();
        assistantMsg.put("role", "assistant");
        assistantMsg.put("content", assistantReply);
        messages.add(assistantMsg);

        return assistantReply;
    }

    /**
     * 清除对话历史(保留系统提示)
     */
    public void clearHistory() {
        messages.subList(1, messages.size()).clear();
    }

    public static void main(String[] args) throws Exception {
        OllamaChatClient client = new OllamaChatClient();

        // 第一轮
        System.out.println("=== 第一轮 ===");
        System.out.println(client.chat("帮我写一个 Redis 分布式锁的 Java 实现"));

        // 第二轮(AI 记得上轮对话)
        System.out.println("\n=== 第二轮(追问)===");
        System.out.println(client.chat("刚才的实现中,如果业务执行时间超过锁过期时间怎么办?给出看门狗机制的改进版本"));
    }
}

八、Spring Boot 集成

真实项目中通常会把 Ollama 封装为 Spring Bean,统一管理配置和生命周期。

8.1 配置文件 application.yml

ollama:
  base-url: http://localhost:11434
  model: deepseek-coder-v2:16b
  options:
    temperature: 0.2
    num-ctx: 8192
    top-p: 0.9

8.2 配置类

@ConfigurationProperties(prefix = "ollama")
@Configuration
public class OllamaProperties {
    private String baseUrl;
    private String model;
    private Options options;

    // getters/setters 略

    public static class Options {
        private double temperature = 0.2;
        private int numCtx = 4096;
        private double topP = 0.9;
        // getters/setters 略
    }
}

8.3 Service 层封装

@Service
@Slf4j
public class CodeAssistantService {

    private final OllamaProperties props;
    private final WebClient         webClient;
    private final ObjectMapper      mapper;

    public CodeAssistantService(OllamaProperties props) {
        this.props = props;
        // Spring WebFlux WebClient,支持响应式流
        this.webClient = WebClient.builder()
                .baseUrl(props.getBaseUrl())
                .codecs(c -> c.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
                .build();
        this.mapper = new ObjectMapper();
    }

    /**
     * 响应式流式代码生成,适合 SSE 接口
     * 返回 Flux<String>,每个元素是一个 token
     */
    public Flux<String> generateCodeStream(String prompt) {
        Map<String, Object> requestBody = Map.of(
                "model", props.getModel(),
                "prompt", prompt,
                "stream", true,
                "options", Map.of(
                        "temperature", props.getOptions().getTemperature(),
                        "num_ctx", props.getOptions().getNumCtx()
                )
        );

        return webClient.post()
                .uri("/api/generate")
                .bodyValue(requestBody)
                .retrieve()
                .bodyToFlux(String.class)
                .flatMap(line -> {
                    try {
                        JsonNode node = mapper.readTree(line);
                        String token = node.path("response").asText("");
                        return token.isEmpty()
                                ? Flux.empty()
                                : Flux.just(token);
                    } catch (Exception e) {
                        log.warn("解析 token 失败: {}", line);
                        return Flux.empty();
                    }
                });
    }
}

8.4 Controller(SSE 接口)

@RestController
@RequestMapping("/api/code")
public class CodeAssistantController {

    private final CodeAssistantService service;

    @GetMapping(value = "/generate", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> generate(@RequestParam String prompt) {
        return service.generateCodeStream(prompt);
    }
}

前端通过 EventSource 即可实时接收生成内容,轻松实现类 ChatGPT 的打字机效果。

Open WebUI — 与 Spring Boot 后端整合后的完整 AI 对话平台效果 ▲ Spring Boot SSE 接口 + 前端 EventSource = 流畅的打字机输出体验(Open WebUI 同款交互逻辑)


九、生产调优建议

9.1 并发控制

Ollama 默认单队列处理请求(一次跑一个推理任务),多并发时请求会排队。如果需要处理多并发,可以:

  • 启动多个 Ollama 实例(绑定不同端口),前面用 Nginx upstream 负载均衡;
  • 设置环境变量 OLLAMA_NUM_PARALLEL=4(需 GPU 显存足够)。

9.2 超时设置

推理大模型的响应时间随 prompt 长度变化较大,建议 Java 客户端设置合理超时:

// 连接超时 10s,读取超时 5min(复杂代码生成可能需要更长时间)
HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(10))
    .build();

request = HttpRequest.newBuilder()
    .timeout(Duration.ofMinutes(5))
    ...

9.3 模型预热

服务启动后第一次推理会加载模型到显存,耗时较长(约 10~30 秒)。可以在应用启动时发送一个 warmup 请求:

@EventListener(ApplicationReadyEvent.class)
public void warmup() {
    log.info("Ollama 模型预热中...");
    try {
        client.generate("hello");
        log.info("Ollama 预热完成");
    } catch (Exception e) {
        log.error("Ollama 预热失败,请检查服务是否正常", e);
    }
}

9.4 Prompt 工程技巧

代码场景下,Prompt 质量直接影响输出质量,几个经验:

// ❌ 模糊 Prompt
"写一个排序算法"

// ✅ 精确 Prompt,明确语言、约束、预期输出格式
"""
请用 Java 17 实现归并排序,要求:
1. 泛型支持,元素实现 Comparable 接口
2. 时间复杂度 O(n log n),空间复杂度 O(n)
3. 包含完整 Javadoc 注释
4. 附带 JUnit 5 单元测试(覆盖空数组、单元素、已排序等边界情况)
"""

十、常见问题排查

Q: ollama pull 卡在 0% 不动?

A: 国内网络访问 ollama.com 受限,建议:

  • 配置 HTTP 代理:export https_proxy=http://127.0.0.1:7890
  • 或使用国内镜像(社区维护,以最新为准)

Q: 调用 API 返回 model not found

A: 检查模型名称是否完全匹配,运行 ollama list 确认本地已有该模型,名称区分大小写。

Q: Java 程序抛出 SocketTimeoutException

A: 推理时间超过客户端超时设置,适当延长 .timeout(Duration.ofMinutes(10)) 即可。

Q: 输出中文乱码?

A: Java HttpResponse 默认 UTF-8 解码,如有问题显式指定:

HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)

Q: GPU 显存不足(CUDA out of memory)?

A: 降低模型规格(如换用 7b),或降低 num_ctx(上下文长度),减少显存占用。


十一、完整可运行 Demo

package com.example;

public class Main {

    public static void main(String[] args) throws Exception {

        System.out.println("========== 场景1:代码生成 ==========");
        OllamaClient basic = new OllamaClient();
        String code = basic.generate("""
                用 Java 实现一个支持过期时间的本地缓存(LRU,容量 100),
                不使用任何外部依赖,线程安全,附使用示例。
                """);
        System.out.println(code);

        System.out.println("\n========== 场景2:流式代码审查 ==========");
        OllamaStreamClient stream = new OllamaStreamClient();
        stream.generateStream(
                """
                请审查以下代码,指出潜在的 Bug 和性能问题:
                
                public String getUserName(Long userId) {
                    User user = userDao.findById(userId);
                    return user.getName();
                }
                """,
                token -> {
                    System.out.print(token);
                    System.out.flush();
                },
                () -> System.out.println("\n[审查完毕]")
        );

        System.out.println("\n========== 场景3:多轮对话重构 ==========");
        OllamaChatClient chat = new OllamaChatClient();
        System.out.println(chat.chat("帮我把下面的 for 循环改成 Stream API:\n" +
                "List<String> result = new ArrayList<>();\n" +
                "for (User u : users) {\n" +
                "    if (u.getAge() > 18) result.add(u.getName());\n" +
                "}"));
        System.out.println(chat.chat("很好,现在加上并行流优化,并解释适用场景和注意事项"));
    }
}

小结

维度说明
部署难度⭐⭐(Ollama 封装了所有底层细节)
隐私安全✅ 数据完全本地,不经过任何云服务
Java 集成标准 REST API,无需专属 SDK
推理速度GPU 4090 约 50 token/s,CPU 约 5 token/s
适用场景代码生成、审查、注释补全、测试用例生成

DeepSeek-Coder 系列在代码任务上的表现已经非常接近 GPT-4,结合 Ollama 的本地部署能力,是目前企业内网 AI 编程辅助的最优性价比方案之一。


作者:原创技术文章 | 如有问题欢迎在评论区讨论