3680 字
约 12 分钟
1
DeepSeek V4 来了,这波我真的上头(附技术拆解报告)。

大家好,我是二哥呀。

DeepSeek V4(预览版),1.6 万亿参数的 V4-Pro、284B 参数的 V4-Flash,原生 1M 上下文,Agentic Coding 评测直接对标 Opus 4.6。

所以这一期我们来把 DeepSeek V4 接入到 PaiCLI。可以通过 /model deepseek 切到 DeepSeek V4,/model glm 切换到 GLM-5.1。

像 OpenClaw 那样。

01、DeepSeek V4 有哪些优化?

V4-Pro 在 Agentic Coding 评测里已经到了开源最佳水平,官方给的判断是“优于 Sonnet 4.5,交付质量接近 Opus 4.6 非思考模式”。

之前 DeepSeek V3.2 在每个新用户消息开始时会丢弃 thinking trace,等于模型每轮都重新想。V4 改成了在工具调用场景里完整保留全部 reasoning content,包括跨用户消息边界。

也就是说,V4 能在多轮调用之间维持累积的推理线索,连贯性好了一大截。

V4 支持 Non-think(直觉回应)、Think High(逻辑分析)、Think Max(推理极限)三个档位。简单工具调用走 Non-think,复杂的代码分析任务会切到 Think Max。

这意味着同一个模型可以在速度和深度之间灵活切换。

02、模型切换很简单

因为 GLM 和 DeepSeek 的 API 协议都是兼容 OpenAI 和 Anthropic 的。

意味着我们不需要给每个模型写一套完全独立的客户端。只需要提供 base_url、api_key 和 model 就行。

第一步,定义统一接口 LlmClient。

我们把GLMClient.MessageGLMClient.ToolCallGLMClient.ChatResponse全部放到接口级别:

两个 chat 方法,一个带流式监听器,一个不带。

getModelName() 返回当前模型名(比如 glm-5.1),getProviderName() 返回供应商名(比如 deepseek)。

reasoningContent 是给支持思维链输出的模型准备的,GLM-5.1 和 DeepSeek V4 都支持。

// 改造前
public Agent(String apiKey) {
    this.llmClient = new GLMClient(apiKey);
    // ...
}

// 改造后
public Agent(LlmClient llmClient) {
    this.llmClient = llmClient;
    // ...
}

接口定义好了,下一步是实现。

所以我们用模板方法模式,把共享逻辑抽到基类 AbstractOpenAiCompatibleClient 里:

三个抽象方法——getApiUrl()getModel()getApiKey()

基类负责组装 HTTP 请求、解析 SSE 等,子类只需要告诉基类“我的 API 地址是什么、我用哪个模型、我的密钥是什么”就可以了。

流式读取也非常简单。

大模型返回 tool_calls 的时候不是一次性给完整 JSON 的,而是把参数拆成很多小的片段,一块一块返回。

比如参数 {"path": "/src/main/java"} 可能被拆成三个 delta:{"path": "/src/main/java"}

ToolCallAccumulator 负责把这些碎片拼起来:

private static final class ToolCallAccumulator {
    private String id;
    private final StringBuilder name = new StringBuilder();
    private final StringBuilder arguments = new StringBuilder();
}

每来一个 delta,就往对应的 StringBuilder 追加。全部接收完之后,用 buildToolCalls 方法把累加器转成最终的 ToolCall 列表。

HTTP 客户端是共享的:

protected static final OkHttpClient SHARED_HTTP_CLIENT = new OkHttpClient.Builder()
        .connectTimeout(60, TimeUnit.SECONDS)
        .readTimeout(120, TimeUnit.SECONDS)
        .writeTimeout(60, TimeUnit.SECONDS)
        .build();

static final 保证整个 JVM 里只有一个 OkHttpClient 实例。因为 OkHttp 内部维护了连接池和线程池,多个实例会导致资源浪费。而且切换模型也不需要重建 HTTP 客户端,切换的只是 URL 和密钥。

有了基类,GLMClient 和 DeepSeekClient 就变得非常简单:

DeepSeekClient 几乎一模一样,就是 URL 和默认模型不同:

子类有了,谁来决定创建子类呢?

直接在 Main.java 里写一堆 if-else?

用工厂模式:

create 方法根据 provider 名称创建对应的客户端。createFromConfig 方法先尝试用户配置的默认 provider,如果没配置或者 API Key 缺失,就按优先级依次尝试。

03、/model 命令切换模型

配置和工厂都准备好了,最后一步是在 CLI 里加上运行时切换的能力。

CliCommandParser 新增了 SWITCH_MODEL 命令类型:

if (trimmed.equalsIgnoreCase("/model")) {
    return new ParsedCommand(CommandType.SWITCH_MODEL, null);
}
if (trimmed.regionMatches(true, 0, "/model ", 0, 7)) {
    return new ParsedCommand(CommandType.SWITCH_MODEL,
            trimmed.substring(7).trim());
}

/model 不带参数显示当前模型信息,/model glm/model deepseek 切换模型。

切换的时候做三件事:

第一,通过工厂创建新的 LlmClient。如果目标 provider 没有配置 API Key,直接报错,不会把当前可用的模型搞丢。

第二,更新默认 provider 并持久化到 config.json。下次启动 PaiCLI 的时候会自动用上次选择的模型。

第三,通过 setLlmClient() 热替换 Agent 内部的模型客户端。注意这里不是重建 Agent,而是原地替换——对话历史、工具注册表、记忆管理器全部保留。这样大家在用 GLM 跑到一半觉得不够好,可以直接切到 DeepSeek 继续,新模型能看到之前的对话上下文。想要干净的对比,手动 /clear 一下就行。

Agent 里的实现很简单,llmClient 字段从 final 改为可变,setLlmClient 同时更新 MemoryManager 内部的引用:

public void setLlmClient(LlmClient llmClient) {
    this.llmClient = llmClient;
    this.memoryManager.setLlmClient(llmClient);
}

用到的设计模式有三个:

策略模式LlmClient 接口就是策略。Agent 不知道自己在用 GLM 还是 DeepSeek,它只依赖 LlmClient 接口。运行时通过 /model 命令切换策略实现。

模板方法模式AbstractOpenAiCompatibleClient 是模板。骨架逻辑(HTTP 请求、SSE 解析)在基类里写死,子类只需要填 getApiUrl()getModel()getApiKey() 三个方法。

工厂模式LlmClientFactory 封装了客户端的创建逻辑。

策略模式管“面向谁编程”,模板方法管“公共逻辑怎么复用”,工厂模式管“对象怎么创建”。

04、保证两个模型是公平的

接了两个模型之后,第一个想法肯定是——让它们跑同一个任务比一比。

但这里有个容易踩的坑:如果两个模型收到的上下文不一样,比出来的结果就没意义。

第一,system prompt 完全一致。 两个模型拿到的系统提示词是同一份 SYSTEM_PROMPT,不随模型切换而变化。Memory 检索注入的上下文也走同一条路径。

private void updateSystemPromptWithMemory(String memoryContext) {
    if (memoryContext == null || memoryContext.isEmpty()) {
        conversationHistory.set(0, LlmClient.Message.system(SYSTEM_PROMPT));
    } else {
        String enrichedPrompt = SYSTEM_PROMPT + "\n" + memoryContext;
        conversationHistory.set(0, LlmClient.Message.system(enrichedPrompt));
    }
}

第二,对话历史默认保留,手动清空。 /model deepseek 切换模型的时候,PaiCLI 不会重建 Agent,而是通过 setLlmClient() 原地替换模型客户端,对话历史保留。想做干净的对比,切完模型后手动 /clear 清空对话历史就行。

reactAgent.setLlmClient(llmClient);
// 对话上下文已保留,使用 /clear 可清空

第三,工具定义一模一样。 toolRegistry.getToolDefinitions() 返回的工具列表不依赖模型,GLM 和 DeepSeek 拿到的工具描述、参数定义完全相同。两个模型面对的是同一套“工具箱”。

第四,reasoningContent 的处理差异。 这是唯一一个两个模型表现不同的地方。DeepSeek V4 在工具调用场景中会保留完整的 reasoning_content,而 GLM-5.1 的思维链行为不太一样。但 LlmClient.MessagereasoningContent 字段是统一的——两个模型的思考内容都走同一个字段存储和传递,不会因为模型不同就丢失或格式不同。

// 两个模型的响应都走同一个 record 结构
record ChatResponse(String role, String content, String reasoningContent,
                    List<ToolCall> toolCalls, int inputTokens, int outputTokens)

05、同一个任务,两个模型跑出来什么样

说了这么多,直接跑一个真实场景看看。

任务是让 PaiCLI 分析项目的 pom.xml,找出核心依赖并给出简要说明。这个任务刚好能覆盖工具调用(read_file)和文本生成两个环节,可以同时对比工具使用策略和回答质量。

先用 GLM-5.1:

✅ 已加载模型: glm-5.1 (glm)
🔄 使用 ReAct 模式

👤 你: 读取 pom.xml 告诉我项目用了哪些核心依赖

切到 DeepSeek V4:

👤 你: /model deepseek
✅ 已切换到: deepseek-v4-flash (deepseek)

👤 你: 读取 pom.xml 告诉我项目用了哪些核心依赖

Token 消耗对比:

  • GLM-5.1: 2719 输入 / 511 输出 / 3230 合计 | ⏱ 22.4s
  • DeepSeek V4: 3028 输入 / 648 输出 / 3676 合计 | ⏱ 19.2s

06、拆一拆 DeepSeek V4 的技术报告

这块我是让 Claude Opus 4.6 帮我啃的 DeepSeek 官方技术报告,加上我自己的学习能力,有错误大家可以指出,共同学习下(装逼。

Hybrid Attention

V4 把上下文拉到原生 1M,核心靠的是一套混合注意力设计。

我们都知道标准 Transformer 的注意力计算量是 O(n²),100 万 token 硬算根本扛不住。V4 的解法很巧妙——既然全量关注做不到,那就换个思路。

第一个是CSA(Compressed Sparse Attention)。它先把相邻的几个 token 压缩在一起(Flash 版本里每 4 个 token 压一块),然后每个 query 只挑最相关的几百个压缩块来看。打个比方,100 万 token 先压成 25 万块,再从中只挑 512 块做注意力计算,相当于只看了原文的千分之二,计算量断崖式下降。

第二个是 HCA(Heavily Compressed Attention)。压缩粒度比 CSA 大得多(Flash 版本每 128 个 token 压一块),换来的好处是能用全量注意力——不挑不拣,每个 query 都看所有压缩块。好比用一个超低分辨率的摄像头做全景监控,细节模糊但不会漏掉大的趋势变化。

CSA 负责精确检索远处的信息,HCA 负责维持对全局的大致把握。另外每层还带了一个滑动窗口分支专门处理附近的 token,防止压缩把局部的前后文关系搞丢。

省了多少呢?

根据技术报告的数据,在 1M 长度下,V4-Pro 的推理算力开销降到了 V3.2 的不到三成,显存里的 KV 缓存更是不到十分之一。Flash 版本更狠,算力降到一成,缓存降到不足一成。

意味着什么?

多轮 ReAct 循环里,模型能记住的对话历史和工具返回结果多了好几倍。

三挡思考模式是怎么训出来的

前面提到 V4 支持 Non-think、Think High、Think Max 三档。

训练方法上 V4 做了一个大胆的范式切换:后训练阶段不再用之前 V3.2 那套混合强化学习,改成了一种叫 On-Policy Distillation(OPD) 的方法。

第一步,按领域(数学、代码、Agent、指令跟随等)各练一个专精模型,每个领域还分三种推理预算:快速直觉版给短窗口,中等思考版给 128K 窗口,深度推理版直接给 384K 窗口让模型想个够。

第二步才是关键。用 OPD 把所有领域的专精模型合体到一个统一的学生模型里。和传统蒸馏不同的是,学生不是在老师的数据上学,而是一边自己生成内容、一边向多个老师的输出分布对齐。本质上更接近一种在线策略优化。

简单的 read_file 调用走 Non-think 就够了,速度快、省 token。遇到复杂的代码分析任务,模型会自动加深度思考。API 层面通过 reasoning_effort 参数控制,对 Claude Code 这类复杂 Agent 请求还会自动升到 Max。

基础设施这次份量很足

V4 的技术报告给基础设施单独写了一整章,这在大模型报告里挺少见的,说明 DeepSeek 这次在工程侧下了很大的功夫。

挑几个我觉得有意思的聊聊:

推理加速内核 MegaMoE。 MoE 架构最头疼的就是专家间的通信开销,DeepSeek 在自家 DeepGEMM 基础上做了一个大粒度融合内核,把 dispatch、计算、combine 合成一步走。实测下来日常推理提速五到七成,强化学习阶段那种小批量长尾请求甚至能快一倍。而且这个内核同时适配了 N 卡和华为昇腾。

可复现的计算。 报告里花了不少篇幅讲确定性:同一批数据换个排列顺序、或者同样的输入跑两遍,结果都必须比特级一致。从预训练到后训练到推理,三个阶段的数值完全对齐。这事听起来简单,实际在分布式 GPU 上做到很费工夫,但对排查训练 bug 和保证模型一致性非常关键。

KV 缓存落盘。 既然前面两种注意力已经把 KV 压得很小了,那干脆把压缩后的缓存直接存磁盘。下次请求如果命中了相同的前缀,直接从磁盘读,省掉重复计算。滑动窗口那部分因为体量大得多,给了全缓存、定期打快照、不存直接重算三种可选方案。这套设计让 1M 上下文在有限显存下也能跑得动。

Agent 训练沙箱 DSec。 这个最让我意外。DeepSeek 为了训练 Agent 搭了一套专用的沙箱系统,一个集群能同时跑几十万个隔离环境。最妙的是训练中断恢复的设计——任务被抢占时沙箱不主动销毁,恢复后直接从断点继续,不会重复执行那些不能幂等的操作(比如已经写入数据库的命令)。

PaiCLI 如何写到简历上?

项目名称:PaiCLI — Java Agent CLI

项目简介:基于 ReAct 范式从零实现的 Java Agent 命令行工具,集成 Plan-and-Execute、Memory、RAG、Multi-Agent、HITL 人工审批、异步并行和多模型运行时切换,完整覆盖 AI Agent 核心技术栈。

核心职责

  1. 基于策略模式设计 LlmClient 统一接口,将 GLMClient 的内部类型(Message、ToolCall、ChatResponse)提升为接口级公共类型,实现 Agent 层与模型实现层的完全解耦
  2. 使用模板方法模式实现 AbstractOpenAiCompatibleClient 基类,共享 SSE 流式解析、请求构建和工具调用增量合并逻辑,新增模型适配只需 20 行子类代码
  3. 通过工厂模式 LlmClientFactory 封装客户端创建,结合三层配置回退(config.json → 环境变量 → .env 文件),支持运行时 /model 命令一键切换 GLM-5.1 和 DeepSeek V4
  4. 在 Agent 执行循环中集成 Token 消耗统计,累计每轮 LLM 调用的输入/输出 token 数和耗时,方便不同模型间的成本和性能对比
DeepSeek V4 来了,这波我真的上头(附技术拆解报告)。
http://www.clxhxhhr.top/posts/2183/
作者
clxstart
发布于
2026-09-19
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。