Deepseek V4 Flash 模型微调的心得详细教会一个模型的后训练。
前几天,我们团队针对 DeepSeek V4 Flash 做了一轮比较完整的微调和后训练实验。这次真正把模型拉到训练环境里跑起来以后,我们最大的感受是:微调一个模型本身并不是最困难的事情,真正困难的是如何确定“到底要把模型训练成什么样”,以及如何证明训练之后的模型在真实工程环境中确实比原始模型更强,而不是仅仅在某几个 Benchmark、某一批训练数据或者某几种固定 Prompt 上表现得更好。
这次过程中我们遇到的第一个非常现实的问题就是内存、显存和训练资源。模型后训练和普通推理完全是两个概念。推理阶段我们主要考虑模型权重、KV Cache、上下文长度和并发,而进入训练以后,还需要考虑梯度、Optimizer State、Activation、中间张量、Checkpoint、DataLoader、通信 Buffer 等大量额外开销。如果直接按照“模型能推理,所以应该也能训练”的思路估算资源,很容易在真正开始训练之后遇到 OOM、Host RAM 爆掉、CPU Offload 把系统内存吃满、Checkpoint 保存瞬间产生峰值、长序列 Batch 导致显存突然上涨等问题。
所以这次之后,我们会把资源规划本身当成后训练工程的一部分,而不是训练开始之前随便算一下显存。对于每一轮实验,我们都需要提前记录模型权重精度、训练精度、Optimizer 类型、是否使用 LoRA/QLoRA、LoRA Rank、Target Modules、Sequence Length、Micro Batch Size、Gradient Accumulation、Gradient Checkpointing、Flash Attention、CPU Offload、ZeRO/FSDP 策略、Checkpoint 策略以及训练节点之间的通信方式,然后实际测量 GPU Peak Memory、Host RAM Peak、吞吐量、Tokens/s、MFU、Step Time 和数据加载时间。尤其是在长上下文训练里面,Sequence Length 的影响非常大,不能只按照参数量估算训练资源。同样数量的 Token,如果是很多短样本和少量超长样本,对显存峰值、Padding 浪费和训练吞吐的影响完全不同。因此后续我们会按照 Token Length 做 Bucketing 和 Packing,减少无意义 Padding,并且把超长样本单独作为一个训练阶段或者单独控制 Batch,而不是和普通样本混在一起硬跑。
但相比显存问题,我们认为真正决定后训练质量的还是数据。
如果我们的目标只是让 DeepSeek V4 Flash 学会一种固定输出格式,那么准备大量 Instruction/Response 数据做 SFT 就已经能够获得比较明显的效果。但是我们的目标不是做一个“更听话的聊天模型”,而是希望它在真实软件工程环境里面成为一个更可靠的 Coding Agent,能够理解大型 Repository、阅读规划文档、定位代码、修改多个文件、调用工具、执行测试、读取错误、分析失败原因、继续修复、验证结果,并且在几十分钟甚至几个小时的 Agent Loop 里面持续保持对任务目标和当前状态的理解。
在这种目标下面,最有价值的数据就不再只是“问题—答案”,而是“任务—环境—决策—动作—环境反馈—修正—验证—最终结果”的完整工程轨迹。
因此我们下一阶段收集数据时,第一优先级会放在真实工程任务轨迹,而不是单纯从互联网收集更多代码。
例如,一个高质量训练样本不应该只是:
“实现这个功能。”
然后直接给模型最终 Patch。
我们更希望得到这样的完整数据:
用户提出需求 → Agent 阅读项目规划文档 → 阅读 Repository Structure → 搜索相关模块 → 找到现有实现 → 阅读接口定义 → 判断影响范围 → 制定修改计划 → 修改代码 → 编译 → 出现错误 → 阅读 Compiler Error → 定位问题 → 修改 → 执行 Unit Test → 某个测试失败 → 分析失败原因 → 再次修改 → 执行 Integration Test → 检查 Lint/Type Check → 检查 Git Diff → 发现修改范围过大 → 回退无关修改 → 再次验证 → 最终通过验收。
这种数据里面真正有价值的不是最终那几百行代码,而是模型在环境反馈出现以后如何继续行动。
因为真实的软件工程不是“一次生成正确答案”,而是一个闭环控制过程。
Observe → Understand → Plan → Act → Verify → Diagnose → Repair → Re-verify。
如果我们希望模型最终成为 Coding Agent,就应该按照 Agent 的工作方式训练,而不是按照 Chatbot 的工作方式训练。
这也是我们认为后训练 DeepSeek V4 Flash 最需要注意的第一件事情:训练数据分布必须尽量接近最终 Deployment Distribution。
如果最终模型 80% 的时间是在 Repository 里面工作,那么训练数据就不应该 80% 都是 LeetCode;如果最终模型需要频繁使用 Shell、Git、测试框架、文件搜索、编译器,那么训练里面就必须存在大量 Tool Use;如果最终模型需要维护大型项目,那么就必须有 Cross-file、Cross-module、Long-context 的任务;如果最终模型需要修 Bug,就必须存在大量“第一次修改失败以后继续修复”的轨迹,而不能只留下最终成功答案。
第二个需要重点解决的问题,是训练数据的质量远比数量重要。
我们不会简单追求几百万条或者几千万条 Instruction。对于工程模型来说,一条真正来自复杂 Repository、经过验证、包含完整执行过程的高质量轨迹,可能比几十条人工拼出来的 Coding QA 更有价值。
因此所有进入核心训练集的数据,我们希望尽可能附带机器可验证信号,例如:
代码是否能够编译;
Unit Test 是否通过;
Integration Test 是否通过;
Regression Test 是否通过;
Lint 是否通过;
Type Check 是否通过;
API Contract 是否满足;
是否修改了不应该修改的文件;
是否引入新的 Dependency;
性能是否发生 Regression;
Memory 是否出现 Regression;
最终 Git Diff 是否符合任务范围;
原有测试是否仍然通过。
这样做的一个巨大优势是,我们不需要完全依赖人工判断“这个回答是不是好回答”,因为软件工程本身天然存在大量 Verifier。
Compiler 是 Verifier。
Test Suite 是 Verifier。
Type Checker 是 Verifier。
Linter 是 Verifier。
Benchmark 是 Verifier。
API Contract 是 Verifier。
甚至 Git Diff 本身也可以成为 Verifier。
因此我们希望尽量把 Coding 后训练做成 Verifiable Training,而不是纯粹依赖主观 Preference。
第三个问题是失败数据不能全部扔掉。
以前整理数据的时候,很容易只留下成功轨迹。但真正训练 Agent 时,失败轨迹其实非常有价值。关键不是把“错误答案”直接作为正确答案训练,而是保留:
错误动作 → 环境错误 → 错误原因 → 正确诊断 → 修复动作 → 验证成功。
比如模型第一次修改代码以后 Compiler 报错,这个 Compiler Error 本身就是非常高价值的信息。如果模型随后能够根据错误信息找到真正的问题并完成修复,那么整段轨迹实际上是在训练一种非常重要的能力:Recovery。
我们甚至认为可以单独建立 Failure/Recovery Dataset。
里面专门包含编译失败、测试失败、Dependency Conflict、API Misuse、Wrong File Modification、Context Misunderstanding、Hallucinated API、Incorrect Assumption、Incomplete Implementation、Regression、Race Condition、Memory Problem 等各种工程失败,然后训练模型看到真实环境反馈以后如何恢复。
第四个重点是训练模型“先理解项目,再写代码”。
现在很多 Coding Model 最大的问题并不是不会写代码,而是太快写代码。
拿到任务以后立刻生成 Patch,看起来效率很高,但在大型项目里面经常意味着它没有真正理解 Architecture、Existing Convention、Dependency Graph 和 Requirement。
因此我们需要专门收集一种数据:Repository Understanding。
让模型学习在开始修改之前先回答几个隐含问题:
这个需求真正属于哪个模块?
入口在哪里?
现有 Architecture 是什么?
哪些代码可以复用?
哪些 API 已经存在?
修改会影响哪些模块?
项目自己的 Coding Convention 是什么?
测试在哪里?
完成标准在哪里?
规划文档里面有没有对这个功能做过约束?
这种能力对于真实 Coding Agent 的提升可能比继续增加纯代码生成数据更加重要。
第五个重点是 Long Context 不能简单理解成“把上下文窗口拉长”。
真正的大型工程任务需要的是 Long-horizon Context Management。
模型需要知道哪些信息应该一直保留,哪些信息只是临时观察结果,哪些信息应该压缩成摘要,哪些决策已经确定不能反复推翻,哪些 TODO 尚未完成,当前修改过哪些文件,哪些测试已经跑过,哪些失败还没有解决。
因此我们需要训练的不只是 Long Context Retrieval,还包括 Context Compression、State Tracking、Task Memory 和 Plan Maintenance。
例如一个 Agent 工作两个小时以后,不应该还把两小时前读取过的每一行日志完整塞在 Context 里面。更合理的方法应该是形成一个持续更新的 Working State:
当前目标;
已经完成的模块;
正在处理的问题;
关键 Architecture Decision;
修改过的文件;
未解决错误;
测试状态;
下一步动作;
不能违反的约束。
模型必须学会维护这个状态,而不是无限制累积上下文。
第六个问题是 Tool Calling。
如果未来 DeepSeek V4 Flash 需要作为 Agent 使用,那么 Tool Use 应该成为后训练的核心能力之一。
我们希望模型能够稳定地学习:
什么时候应该 Search;
什么时候应该 Read File;
什么时候应该执行 Shell;
什么时候应该运行 Test;
什么时候应该查看 Git Diff;
什么时候应该查询 Documentation;
什么时候应该调用外部 Agent;
什么时候应该停止继续修改。
尤其重要的是“什么时候不要调用工具”。
优秀 Agent 并不是 Tool Call 越多越好。大量无意义 grep、重复读取同一个文件、反复运行昂贵测试、没有分析错误就重新执行命令,本质上都是低质量 Agent 行为。
因此 Tool Efficiency 本身也应该进入 Reward。
第七个问题是 Planning。
我们并不希望模型每一个简单任务都输出非常长的计划,但是对于复杂任务,它必须具备 Hierarchical Planning 能力。
也就是:
Goal → Milestone → Module → Task → Action。
一个跨十几个文件的需求,不应该直接变成几十个随机 Tool Calls。模型应该先建立任务结构,再逐层执行,并且在环境发生变化以后更新计划。
所以我们的训练数据里面需要同时包含短任务和长任务。短任务训练快速执行能力,长任务训练规划、状态维护和跨模块协调能力。
第八个问题是后训练不能只做 SFT。
我们的理解是,SFT 更适合建立模型的基础行为模式,比如让模型学会 Repository Exploration、Tool Calling Format、代码修改格式、测试流程、输出规范以及基本 Agent Loop。
但是当模型已经“知道应该怎么做”以后,进一步提高工程能力,更重要的是让模型学会区分“可以工作的方案”和“更好的方案”。
因此后续可以加入 Preference Optimization。
同一个任务生成多个 Candidate Trajectory:
A 能运行但是改了很多无关代码;
B 能运行而且修改范围更小;
C 能运行但没有补测试;
D 第一次失败以后正确恢复;
E 看起来写得很好但实际上 Test Failed。
然后通过自动 Verifier 和人工 Review 构建 Preference Pair。
模型最终学习的不应该只是“能完成任务”,而是:
正确;
最小修改;
符合 Architecture;
可维护;
有测试;
没有 Regression;
Tool 使用合理;
Token 成本合理;
执行时间合理。
进一步,如果条件允许,我们会把可验证工程任务引入 RL 或基于 Verifier 的强化学习。
Coding 是非常适合做这一类训练的领域,因为 Reward 相对容易定义。
例如:
Build Pass:奖励;
Test Pass:奖励;
Regression Test Pass:奖励;
Task-specific Test Pass:高奖励;
引入 Compilation Error:惩罚;
修改无关模块:惩罚;
破坏已有 API:惩罚;
产生 Security Regression:高惩罚;
最终没有完成需求:高惩罚。
这样模型优化的目标就逐渐从“预测一个看起来像正确答案的 Token Sequence”转向“在真实 Environment 中完成任务”。
第九个问题是数据污染和数据泄漏。
如果 Evaluation Repository、Benchmark、测试答案或者标准 Patch 被模型训练过,那么最终得到的 Benchmark Improvement 没有意义。
所以 Train / Validation / Test 必须严格隔离,而且对于 Coding Evaluation,最好按照 Repository、Project、时间甚至 Dependency Family 做隔离,而不是简单随机切分样本。
否则同一个 Repository 的类似函数可能同时存在于 Train 和 Test 里面,最终测出来的并不是真正 Generalization。
我们尤其希望建立一个 Private Holdout Benchmark。
这个 Benchmark 不进入训练、不进入数据生成、不进入 Prompt 优化,只用于最终验收。
第十个问题是灾难性遗忘。
如果我们为了强化 Coding Agent 能力,把训练数据全部换成工程数据,很可能导致模型原来的 General Reasoning、Instruction Following、语言能力或者其他能力下降。
所以每次后训练都必须有能力回归测试。
不能只看 Coding Benchmark 从 70 提升到 75,还必须检查其他核心能力有没有从 80 掉到 60。
因此训练数据需要一定比例的 General Capability Replay,或者使用其他方式维持基础能力,同时控制 Learning Rate、训练 Epoch 和数据比例,避免过拟合某种 Agent Pattern。
第十一个问题是 Data Mixture。
我们下一阶段更倾向于建立多个明确的数据池,而不是把所有数据混在一个 JSONL 里面:
General Reasoning;
Code Generation;
Code Understanding;
Repository Understanding;
Bug Fixing;
Refactoring;
Testing;
Tool Use;
Agent Trajectory;
Failure Recovery;
Long-horizon Task;
Planning;
Documentation;
API Integration;
Performance Optimization;
Security Fix;
Cross-platform Engineering。
然后针对不同训练阶段调整 Sampling Ratio。
这样我们才能知道某一次能力变化到底来自哪一部分数据,而不是训练完以后只知道“好像变强了”。
第十二个问题是数据难度。
如果所有数据都是简单任务,模型最终会非常擅长简单任务,但是复杂工程能力不会自然出现。
所以我们需要建立 Difficulty Curriculum。
可以把任务大致分成:
单函数;
单文件;
多文件;
单模块;
跨模块;
跨服务;
Repository Level;
Long-horizon Agent Task。
同时记录每个任务需要多少 Tool Calls、多少文件、多少 Token、多少次失败恢复、多少个 Dependency、执行时间以及最终是否成功。
然后训练过程中逐渐增加复杂任务比例。
第十三个问题,是我们需要大量“高手是怎么解决真实工程问题”的数据,而不是只有最终代码。
如果能够收集真实开发过程中经过许可和脱敏的 Agent Session、Issue → Investigation → Commit → PR → Review → Fix → Merge 轨迹,这类数据价值非常高。
尤其是 Code Review。
因为 Review 数据天然包含:
什么实现虽然能运行但不够好;
为什么不够好;
Architecture 哪里有问题;
Edge Case 在哪里;
Test 哪里不完整;
为什么这个 API Design 不合适;
为什么这个 Patch Scope 太大。
这些信息非常适合做 Preference Data 和 Critic Training。
第十四个问题,是让模型学会自我验证,而不是自我相信。
一个非常危险的 Agent Pattern 是:
“我已经实现完成。”
但它根本没有运行测试。
我们希望训练模型形成一种默认行为:任何可以验证的结论都尽量通过环境验证。
不要说“应该可以编译”。
运行 Compiler。
不要说“测试应该能通过”。
运行 Test。
不要说“API 应该兼容”。
执行 Contract Test。
不要说“没有修改其他东西”。
检查 Git Diff。
所以最终我们希望模型形成的工程习惯是:
Evidence before assertion。
第十五个问题,是建立真正的 Evaluation Harness。
没有 Evaluation Harness,后训练基本就是盲飞。
我们需要给 DeepSeek V4 Flash 建立固定版本的能力矩阵,每一次新的 Checkpoint 都自动运行同一套 Evaluation:
Coding Correctness;
Repository Understanding;
Bug Fix Success Rate;
Multi-file Task Success Rate;
Tool Call Accuracy;
Tool Call Efficiency;
Build Pass Rate;
Test Pass Rate;
Regression Rate;
Long-horizon Completion Rate;
Recovery Rate;
Hallucinated API Rate;
Unnecessary Change Rate;
Token Cost;
Wall-clock Time;
Peak Memory;
Context Usage。
最终我们不应该问:
“这个模型是不是感觉更聪明了?”
而应该问:
“Checkpoint B 相比 Checkpoint A,在完全相同的环境和任务下,Repository-level Task Success Rate 提高了多少?平均 Tool Calls 增加还是减少?Regression Rate 有没有下降?平均完成时间有没有改善?失败以后恢复成功率提高了多少?”
只有到了这个阶段,模型训练才真正进入工程化。
因此,如果重新总结这次 DeepSeek V4 Flash 微调,我们现在认为最重要的事情已经不是单纯研究 LoRA Rank 应该设置多少、Learning Rate 应该是多少、训练多少 Epoch。
这些当然重要,但它们属于优化层。
更上层的问题是:
我们到底想让这个模型形成什么行为?
如果目标是把它变成真正能够参与软件开发的工程 Agent,那么我们最终训练的不是“代码生成能力”,而是一整套 Software Engineering Policy:
理解需求;
理解 Repository;
读取规划;
识别约束;
建立计划;
调用工具;
修改代码;
执行验证;
发现失败;
诊断失败;
恢复;
控制修改范围;
维护长期任务状态;
完成验收;
提供证据。
从这个角度来看,我们后面真正需要建设的甚至不只是一个 Fine-tuning Pipeline,而是一套完整的 Post-training Infrastructure。
它应该包括:
Data Collection Pipeline;
Trajectory Recorder;
Data Cleaning;
PII/Secret Filtering;
Deduplication;
Quality Scoring;
Difficulty Scoring;
Dataset Versioning;
SFT Pipeline;
Preference Pipeline;
Verifier;
RL Environment;
Evaluation Harness;
Regression Dashboard;
Checkpoint Management;
Deployment Canary。
同时所有 Dataset、Training Config、Base Model、Tokenizer、Code Version、Evaluation Version 和最终 Checkpoint 都必须能够追踪。
否则当三个月以后某个模型突然表现很好,我们甚至无法准确回答:
它为什么变