|【/思考】)", response, re.S) think = think_match.group(1).strip() if think_match else None answer = re.sub(r"(?: |【思考】).*?(?: |【/思考】)", "", response, flags=re.S).strip() return think, answer
这里需要注意:R1 蒸馏 Qwen 的实际 think 标记可能是 `<|User|>...<|Assistant|>` 或 `### 思考`。我不能编造太具体的标记。为了安全,解析函数可以写成“根据不同模板做配置”,展示思路而非写死。可以让函数接受 think_start/think_end 参数,默认空。 GRPO 思路代码要小心: ```python # 这段代码是教学示意图,不是可训练实现 import torch import torch.nn.functional as F def grpo_single_step(ref_logps, policy_logps, rewards, beta=0.04): # ref_logps: 参考模型在旧策略采样上的 logprob # policy_logps: 当前策略重新计算出的 logprob # rewards: 每条样本的奖励 group_mean = rewards.mean() group_std = rewards.std() + 1e-8 advantages = (rewards - group_mean) / group_std ratio = torch.exp(policy_logps - ref_logps) kl = (torch.exp(ref_logps - policy_logps) - (ref_logps - policy_logps) - 1).mean() loss = - (advantages * ratio).mean() + beta * kl return loss这个思路代码能够展示 GRPO 的基本思想:组内相对优势 + ratio + KL。它不会真的训练,但解释性好。需要在文中明确注明“这是我为了理解原理写的玩具版本,不能在真实训练中直接使用”。
reward 示例:
def simple_rule_reward(output: str, expected: str = "2") -> float: score = 0.0 if expected in output: score += 1.0 if "<answer>" in output: score += 0.5 return scoreOK。
小训练示例其实很难“两天跑通”,所以我说“能观察 loss 曲线就算胜利”。
还有关于“热词”搜索内容为空,无所谓。
然后需要避免过度项目化的“教程”语气,但我还是要遵守 CSDN 代码规范。这不冲突。
最后全文控制在 7000 字左右。
现在开始正式生成。注意:不要有额外前后说明。直接从“DeepSeek Harness……”开始。
注意不要出现“大家好”这种模板开头?可以用“打开 CSDN 的发文界面之前……”这种?还是直接切入。我选择直接以解释标题开始,更自然。
正文开始:
“DeepSeek Harness 还是太权威了。” 这句话我在微信上看到别人调侃时,第一反应是:Harness 不是那个给马穿的挽具吗…… 然后联想:在 LLM 工程里,Harness 指的是把模型、数据、评测、训练管线打包好的那一层“缰绳和挽具”。对普通人来说,DeepSeek 开源出来的不只是一个模型权重,而是一整套驯服模型的装备。这套装备很好用,也很有压迫感。
接着写主题。
不写“本文”太多。可以用“这篇记录”等。
我尽量让“两天”的时间线清晰:第一天跑推理,晚上研究格式,第二天上午评测,下午浅尝训练。
关于数字/结果:不编造具体跑分。可以说“我特意没有强迫自己和官方基准对齐,因为评测条件完全不一样”。这也很真实。
关于“太权威”的哲学:权威感来自三层:参数的默认合理、约定格式的强制、训练框架的隐蔽工程。文章反复提这个。
还有安全事项:不涉及。数据库删除等没有。
现在逐字生成。
为了满足“每段不得少于 150 字”,我写作时每段会拉长。但不要为了凑字数重复。段落之间注意连贯。
生成时结构: 标题不输出一级标题。直接开始第一段,然后## 1. ...。
我估计各章字数:
- 开头 300
- 第 1 章 800
- 第 2 章 600
- 第 3 章 1200
- 第 4 章 700
- 第 5 章 1000
- 第 6 章 1200
- 第 7 章 600
- 第 8 章 500(表格)
- 第 9 章 400 合计 7300。合理。
注意 CSDN 要求“少量 emoji 可选”,我默认不用。
还有,标题中的“DeepSeek Harness还是太权威了”我在正文中最好再次出现。比如 “玩了两天之后,我最想保留的评价就是标题这句:太权威了。”
关于“普通人”视角:避免声称过多的个人具体经历,用“对于一个写业务代码、不太碰训练的开发者来说……”等。
现在动手输出。
需要小心代码块语言标签。用到 bash/python。OK。
还有一个点:“lm_eval”命令在新版本中可能是lm_eval --model hf,而老版本lm-eval --model ...。我写lm_eval并加说明。另外模型加载参数trust_remote_code=True在 lm_eval 的 model_args 里用逗号分隔。
还有--tasks arc_challenge需要下载数据集,联网。我不说“联网”,就说“会自动从 HuggingFace 下载数据集”。
最后 FAQ 表格 8 行左右。
OK 开始输出。 “DeepSeek Harness 还是太权威了。”看到这句话的时候,我第一反应是发了个愣:Harness 不是马具吗?怎么和 DeepSeek 扯到了一起。后来才反应过来,在大模型工程里,Harness 这个词常被用来形容“模型之外的那一整圈管线”——数据处理、提示词模板、生成协议、评测脚本、训练框架,全都捆在一起,像一套勒得很紧的挽具。你按它的规则来,它能带你跑很远;你不按它的规则来,第一步就会卡死。
所以这篇不是教程,也没有“保姆级”的承诺。它只是记录一个不搞算法研究、平时写写业务后台的普通开发者,用两天时间在 DeepSeek 开源生态里到底能做出多少东西。我的目标是:跑通推理、搭起服务、跑一次基准评测,再浅浅地碰一下 R1 背后的强化学习思路。两天之后,我的结论是——这套东西确实太权威了,但“权威”不全是坏事,关键在于你知道它为什么权威。
1. 先把“Harness”这个概念拆开
1.1 普通人接触到的 DeepSeek,不只是模型权重
很多人对 DeepSeek 的第一印象是“它开放了模型权重,下载下来就能用”。这个理解不算错,但只对了一小部分。真正值得注意的是,DeepSeek 连带把一套“怎么使用模型的方式”也推到开源社区里了。
比如说,原版 R1 是一个 671B 参数的 MoE 模型,普通人的显卡根本跑不动,所以大多数人其实是在用它的蒸馏版本,比如 DeepSeek-R1-Distill-Qwen-7B、DeepSeek-R1-Distill-Llama-8B。这些模型的参数规模小,单卡就能跑,但它们的对话风格、输出格式、分隔符习惯,都是顺着 R1 的训练经验迁移过来的。
你把模型下载下来,只是拿到了一匹好马;要让这匹马按你的指令走,还需要一套挽具。所谓 Harness,就是模型之外的提示词模板、采样参数、响应解析规则、评测脚本、训练管线。DeepSeek 给人的“权威感”,很大程度就来自它把这一整套东西的默契程度拉满了——它希望你在它建议的框架里使用,而不是随手丢一句 prompt 进去。
1.2 R1、R1-Zero、蒸馏模型之间的关系
如果只有两天时间,没必要把论文细节啃完,但三个名字得先分清。
R1-Zero 是最早的尝试:直接在基座模型上做强化学习,不依赖 SFT 阶段,模型自己长出了长链条推理能力。但它的输出格式不够规整,可读性差。
R1 是在 R1-Zero 基础上加入多阶段训练后的正式版本。它有更规范的长思考格式,回答质量也更稳定。论文里提到,R1 的训练中使用了 GRPO(Group Relative Policy Optimization),这是一种省显存的强化学习策略,不需要像 PPO 那样单独维护庞大的价值网络。
蒸馏版本则是把大模型的能力转移到小模型上:用 R1 生成的高质量数据去训练 Qwen、Llama 这些小架构,最终得到一个普通显卡也能运行的推理模型。我们普通人玩两天,接触最多的其实是这些蒸馏模型。
了解这些背景有助于理解后面每一步操作:为什么提示词要按特定格式写,为什么输出里会出现“思考段落”,为什么评测结果和官方报告不一样。这些不是玄学,都是训练过程的印记。
1.3 “太权威”到底体现在哪里
两天玩下来,我总结了三个“权威感”来源。
第一,默认参数太合理。DeepSeek 系列模型对 temperature、top_p、max_tokens 的预期和传统对话模型不同。把 temperature 调到 0.7 以上的宽松值,R1 式模型反而容易开始胡编乱造,或者陷入无意义的重复。
第二,响应格式是强约束。R1 的输出通常被设计成“思考在前,答案在后”,这个结构在训练阶段就被固定下来了。你想让它像普通闲聊模型那样直接给结论,它也会先来一大段内部推理过程。这既是特色,也是限制。
第三,底层工程细节藏在深处。比如 tokenizer 的 chat template、特殊分隔符、远端代码加载,这些对普通开发者来说是黑盒。看起来非常权威的封装,本质上是为了降低模型乱说的概率,而不是为了考验你的耐心。
理解这三点之后,后面的操作才有意义。
2. 环境准备:一张显卡就够,但别贪大
2.1 运行方案怎么选
普通开发者的两天时间,不应该浪费在编译源码和排环境冲突上。我建议按下面这个组合来准备:Linux 系统(Ubuntu 22.04 或相近发行版)、Python 3.10/3.11、PyTorch、transformers、vLLM、lm-evaluation-harness。
如果你有一张 24GB 显存的显卡,跑 7B/8B 量级的蒸馏模型完全够用,开 bfloat16,batch size 小一点即可。如果没有显卡,也不想租云服务器,那体验会大打折扣,因为大模型推理在 CPU 上虽然能跑,但速度慢到会让人怀疑人生。我是建议你保留一点耐心预算,不至于在连续输出时等到失去兴趣。
2.2 创建干净的 Python 环境
强烈建议用虚拟环境,不要直接往系统 Python 里装包。深度学习库的依赖冲突非常频繁,一个隔离环境能帮你挡掉大部分麻烦。
conda create -n deepseek python=3.11 -y conda activate deepseek然后安装基础依赖:
pip install torch transformers accelerate这里不写死具体的 PyTorch 版本号,因为版本需要根据你的 CUDA 驱动情况调整。如果显卡驱动较新,直接默认安装最新稳定版即可;如果驱动较老,则要回到 PyTorch 官网选择合适版本。核心思路是:先安装 CUDA 匹配的 PyTorch,再装其他库。
2.3 显存和模型大小的对应关系
7B/8B 模型用 bfloat16 加载,权重大约占 14GB 到 16GB 显存,生成时的 KV Cache 还要额外占几 GB。所以 24GB 显存是舒服线,16GB 也能凑合但需要压缩序列长度,8GB 就只能考虑 4bit 量化的方案了。
不过这里要提醒一句:不要一开始就追求跑原版 R1。671B 参数的模型不是“大一点点”,而是大到需要多机多卡集群。普通人的两天时间,跑蒸馏 7B 模型更现实。
3. 第一天:先把推理链路打通
3.1 用 transformers 直接加载模型
第一天上午只做一件事:让模型开口说话。我先用 transformers 直接加载 DeepSeek-R1-Distill-Qwen-7B,验证环境和模型权重没问题。
# 文件路径:quick_start.py from transformers import AutoModelForCausalLM, AutoTokenizer model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B" model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype="auto", device_map="auto", trust_remote_code=True ) tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) def build_messages(question: str): return [{"role": "user", "content": question}] def generate_answer(question: str, max_new_tokens: int = 2048): messages = build_messages(question) text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) inputs = tokenizer(text, return_tensors="pt").to(model.device) outputs = model.generate( **inputs, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.6, top_p=0.95, pad_token_id=tokenizer.eos_token_id ) result = tokenizer.decode( outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True ) return result if __name__ == "__main__": question = "小明有3个苹果,他又买了5个,然后又吃掉了2个,请问小明现在有几个苹果?" answer = generate_answer(question) print(answer)这段代码里有两个细节值得注意。
第一,我用了apply_chat_template而不是自己手工拼接提示词。这是最稳妥的做法,因为不同系列的模型,模板格式差异很大。DeepSeek 原版 R1 有自己特殊的分隔符,蒸馏到 Qwen 上的版本又继承了一部分 Qwen 的聊天模板风格,自己拼接很容易踩坑。
第二,生成参数里我限制了temperature=0.6、top_p=0.95。这不是随便填的,R1 系列模型在训练时更适应较低的随机性,temperature 太高会让推理过程变得松散。
3.2 看一次真实的 R1 式输出
运行上面的代码,你会看到输出不是直接给答案,而是先来一大段思考过程,再给出最终结论。这种“思考在前、答案在后”的结构,是 R1 最显著的特征。
我第一次看到的时候还挺震撼:模型会把题目条件重新组织,列出计算步骤,甚至自我纠错。比如上面的苹果题,它可能会写:“用户有3个苹果,又买5个,那么现在应该是有8个,吃掉了2个,所以最终剩下6个……” 然后才写一句简洁的最终答案。
这种长思考是不是必要的?不一定。有些问题直接答反而更快。但这是训练出来的行为模式,想改掉它,不是靠 prompt 加一句“直接回答”就能彻底解决的。理解这一点非常重要,因为你后续写任何应用,都要考虑“如何处理模型的思考段”。
3.3 用 vLLM 搭一个 OpenAI 兼容服务
transformers 跑通之后,第二件事是搭一个服务。因为在实际开发中,没人会直接调 transformers 接口,都是通过 HTTP 服务来用模型。vLLM 是当前最常用的推理服务框架,它支持 OpenAI 兼容接口,意味着你现有调用 GPT 接口的代码,改一下 base_url 就能切到 DeepSeek 模型上。
vllm serve deepseek-ai/DeepSeek-R1-Distill-Qwen-7B \ --served-model-name deepseek-r1-distill \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --trust-remote-code \ --gpu-memory-utilization 0.9如果你用的是较老版本的 vLLM,入口可能不是vllm serve,可以用下面这个等价命令:
python -m vllm.entrypoints.openai.api_server \ --model deepseek-ai/DeepSeek-R1-Distill-Qwen-7B \ --served-model-name deepseek-r1-distill \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --trust-remote-code \ --gpu-memory-utilization 0.9启动成功后,用一个请求验证服务是否正常:
curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-r1-distill", "messages": [ {"role": "user", "content": "写一段50字左右的自我介绍"} ], "max_tokens": 1024, "temperature": 0.6 }'返回结构里会包含choices[0].message.content,你把这段内容打印出来,会发现它同样带着思考段。这让“直接展示给用户”这件事变得有点尴尬,因为思考过程经常夹杂着模型内部的一些分析和旁白,不适合原样展示。
3.4 第一天踩到的主要坑
第一天我的踩坑记录可以浓缩成三句话:显存不够就换量化;模型输出太乱就先看模板;服务起不来多半是版本不匹配。
特别是版本问题。transformers、vLLM、tokenizer 这三者之间偶尔会出现兼容性摩擦。遇到报错时,第一步不是上网搜索“如何解决”,而是先看是哪个库抛出的异常,再去该库的 release notes 里查对应变更。大多数情况下,升级或降级一个依赖版本就能解决。
4. 第一晚:摸索提示词协议
4.1 R1 式回答的结构拆解
跑通推理之后,我花了一个晚上琢磨输出格式。因为 R1 系列模型的输出不是普通对话,它自带一个“推理包”,而且这个推理包在不同版本里长得不一样。
常见结构大概是这样的:
- 思考段:模型推理过程,内部独白,有时会重新表述问题,有时会自我纠错。
- 答案段:正式回答,面向用户的最终结论。
思考段和答案段之间往往有特殊分隔符。DeepSeek 原版 R1 和蒸馏版可能使用了不同的标记,但整体规律不变:你需要在拿到输出后,把思考段和答案段分开。
4.2 为什么要做格式分离
格式分离不是洁癖,而是工程需要。
如果你在做一个客服问答系统,直接把思考段发到前端,用户会看到模型各种犹豫和内部推理,体验非常差。而且思考段的内容没有经过最终整理,有可能包含错误的前提假设。
如果你在做自动化评测,评测逻辑通常只关心最终答案,思考段会严重干扰评分。比如你判断答案是“6”,但思考段里出现了一串中间数字,简单的字符串匹配就可能误判。
如果你后续想做强化学习训练,思考段和答案段更是必须分开处理,因为训练时对不同部分的策略更新方式不同。思考段被鼓励探索,答案段被要求准确。
4.3 一个简单的格式解析函数
不同模型的分隔符不一样,所以我做了一个可配置的解析函数。核心思路是通过正则把“思维内容”和“回答内容”切分出来。
# 文件路径:response_parser.py import re def parse_r1_response(response: str, think_start: str, think_end: str): think_start = re.escape(think_start) think_end = re.escape(think_end) pattern = re.compile( rf"{think_start}(.*?){think_end}", re.S ) think_match = pattern.search(response) think = think_match.group(1).strip() if think_match else None answer = pattern.sub("", response).strip() return think, answer # 示例:当你确认某个版本的思考段标记为 <think> 和 </think> think, answer = parse_r1_response( response, think_start="<think>", think_end="</think>" ) print("思考段:", think) print("答案段:", answer)这段代码不是最优写法,但它解决了最核心的问题:你不需要再用手写字符串切割的方式去处理 R1 输出了。等模型的注意力告一段落,答案段就是你真正要推给下游逻辑的内容。
5. 第二天上午:用评测 harness 跑基准
5.1 为什么要跑评测
模型能说话之后,下一个问题是:它到底行不行?这个问题不能靠感觉回答,得跑基准评测。
社区里有一套常用的开源评测工具叫 lm-evaluation-harness,它提供了大量现成的评测任务。你可以把模型跑在一组标准化题目上,拿到准确率、F1 等指标,然后横向对比不同模型、不同量化方式、不同提示词策略的影响。
5.2 一个最小评测命令
先安装:
pip install lm-evaluation-harness然后跑一个简单的任务。以arc_challenge为例,这是科学问答类任务,比较适合快速观察模型的基础推理能力:
lm_eval --model hf \ --model_args pretrained=deepseek-ai/DeepSeek-R1-Distill-Qwen-7B,trust_remote_code=True,dtype=bfloat16 \ --tasks arc_challenge \ --device cuda:0 \ --batch_size auto \ --output_path results_arc_challenge这个命令会下载模型权重和测试数据集,然后在指定设备上执行评测,最后把结果写入results_arc_challenge目录。如果你用的是老版本 harness,命令入口可能是lm-eval,参数风格也会略有差异,以你实际安装版本的帮助输出来确定即可。
5.3 评测结果怎么看
评测完成后,终端会输出一个 metric 汇总表。你需要关注的是acc(准确率)和acc_norm(规范化准确率)。不同任务对这两个指标的解释略有不同,但大体上都是越高越好。
这里要特别强调:不要拿你的结果和官方报告里的数值硬比。原因有三个。
第一,模型权重版本可能不同。官方报告通常基于完整版模型,而你跑的是蒸馏版、量化版或带trust_remote_code的变体。
第二,评测超参数可能不同。batch_size、max_new_tokens、是否使用 few-shot 都会影响最终分数。工具默认值变化也会带来偏差。
第三,采样随机性。生成式模型的输出有随机性,除非你把temperature设为 0,否则同一次评测跑两次也可能得到细微不同的结果。
所以,评测的真正价值不在于“超越官方”,而在于给你一个参照系:改动提示词、调整参数、换不同模型后,分数怎么变化,这是可对照的。
5.4 评测时反复出现的报错
短时间跑评测,最常遇到三类问题。
一类是模型加载失败,报错信息里常出现trust_remote_code相关提示。这类问题往往只需要在model_args里显式加上参数。
另一类是显存溢出。batch_size设置过大时,评测任务会瞬间占用大量显存。解决办法是把batch_size调小,或者使用--batch_size auto让工具自动选择。
还有一类是任务名拼写错误。harness 中的任务名是固定的,你如果写了一个不存在的任务名,它会报task not found。解决方法是先执行lm_eval --task_list查看可用任务列表,再选择任务名。
6. 第二天下午:浅尝一次 GRPO 训练
6.1 R1 的训练思路简述
跑通评测之后,我对一个问题产生了好奇:R1 的强推理能力到底是怎么训出来的?它提到最多的关键词就是 GRPO。GRPO 全称 Group Relative Policy Optimization,翻译过来是“组相对策略优化”。
传统的 PPO 方法需要训练一个价值网络来估计状态价值,非常吃显存。GRPO 的巧妙之处在于,它不单独维护价值网络,而是对一个问题的多个采样输出进行对比:在一组样本里,哪个回答得到的奖励更高,就让策略模型更倾向于生成这种回答;奖励低的则抑制。
这种“组内对比”的思路非常符合直觉:不需要绝对的标准,只需要知道哪个更好。放到强化学习里,就变成了一个省资源的优化策略。
6.2 一个小规模 GRPO 的“玩具实现”
为了搞懂 GRPO 在代码层面到底发生了什么,我写了一个玩具版本。先声明:这不是可运行的分布式训练脚本,只是一个教学示意图,用来展示核心逻辑。
# 文件路径:grpo_toy_illustration.py # 注意:这段代码用于理解原理,不能直接用于真实训练 import torch def grpo_single_step(ref_logps, policy_logps, rewards, beta=0.04): # ref_logps: 参考模型在旧策略采样的 logprob # policy_logps: 当前策略重新计算出的 logprob # rewards: 每组样本的奖励 group_mean = rewards.mean() group_std = rewards.std() + 1e-8 advantages = (rewards - group_mean) / group_std ratio = torch.exp(policy_logps - ref_logps) kl = ( torch.exp(ref_logps - policy_logps) - (ref_logps - policy_logps) - 1 ).mean() loss = - (advantages * ratio).mean() + beta * kl return loss这个函数演示了 GRPO 的三个关键组件。
第一,组内相对优势:用(rewards - group_mean) / group_std标准化奖励。它回答的是“这个回答在同样的问题下,算不算好”,而不是“绝对得了几分”。
第二,概率比:policy_logps - ref_logps。如果当前策略对某个高奖励样本的重视程度高于旧策略,这个值就是正的。
第三,KL 惩罚:用来控制当前策略不要偏离参考模型太远,防止训练崩溃。
奖励函数也是可以自定义的。为了让过程直观,我写了一个简单的规则奖励:如果输出包含正确答案,就给高分;如果保持了答案格式,再额外加分。
def simple_rule_reward(output: str, expected: str = "2") -> float: score = 0.0 if expected in output: score += 1.0 if "<answer>" in output: score += 0.5 return score这个玩具例子当然训不出 R1 级别的模型,它缺少真实训练中的数据集管理、多机多卡调度、长序列优化、奖励模型调整等大量工程环节。但通过这个简化实现,我至少理解了 GRPO 为什么比 PPO 更省显存,也明白了 R1 的训练为何是建立在高强度工程封装之上。
6.3 跑起来之后看什么
如果你的目的大概和我一样,只是想验证 GRPO 的代码逻辑能跑通,那有那么一个小规模示例就够你观察了。
你会看到,同样一组答案,得分高的让 loss 降低,得分低的被抑制。这个过程反复迭代,模型的输出风格逐渐向右奖励的方向靠拢。这比空读论文印象深刻得多,因为你能直观感受到“奖励塑造行为”这件事在数字上是如何发生的。
真正要做完整训练,建议去研究社区里成熟的 RL 训练框架,而不是自己从零造轮子。DeepSeek 本身使用的是大规模的分布式训练环境,普通人根本没有对应的资源。
6.4 普通人两天能做的边界
到了这里,两天时间差不多用完了。回头看看,你完成了从加载模型、搭 HTTP 服务、解析输出格式、跑基准评测,到初步理解 GRPO 训练原理的完整链路。这条链路覆盖了“使用模型”和“理解训练机制”两个层面,恰好是一个普通开发者能承受的合理范围。
但你也应该承认边界:你没有真正复现 R1,没有训练出一个像样的 reward model,也没有跑出一个和官方对齐的 SFT/RL 流程。这些不是一天能做出来的,甚至不是一周能做完的。认清这个边界,反而能帮你在后续深入学习时保持耐心。
7. 这两天做不出来、也建议别急着碰的事情
7.1 复现完整版 R1
完整版 R1 有 671B 参数,训练需要的算力、数据清洗、多阶段调度超出绝大多数个人开发者的承受范围。就算你有并行卡集群,复现训练过程还需要对齐模型初始化、数据配比、奖励设计、强化学习超参等多个环节。任何一个环节不对,最终结果都会偏离官方版本。普通人玩两天,不建议碰这条线。
7.2 自己从头设计奖励模型
GRPO 训练中奖励函数直接决定了模型朝什么方向优化。写一个“答案对就给高分”的玩具很容易,但要设计出能稳定提升推理质量的奖励函数就难了。奖励过于稀疏,学习信号不足;奖励过于稠密,模型可能学会钻空子。这需要大量实验积累,不是靠两天的热情能搞定的。
7.3 追求评测分数与官方完全一致
一开始我就说过,评测结果受模型版本、量化方式、采样参数、评测工具版本等因素影响。硬要对齐官方报告,会让你陷入调参泥潭。更好的做法是把自己的评测配置固定下来,只做横向对比。这个原则在后续工程实践中同样适用。
8. 高频问题排查清单
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
模型加载时报trust_remote_code错误 | 模型仓库包含自定义代码,未开启加载许可 | 在from_pretrained或model_args中加入trust_remote_code=True |
| 加载过程中显存溢出 | 模型权重和 KV Cache 占用超出显存 | 换更小模型或使用量化,或减小max_model_len/batch_size |
| 生成结果全是重复内容 | temperature 过大或 max_new_tokens 过长 | 降低 temperature,比如设为 0.6;检查生成长度是否超出模型习惯 |
| vLLM 服务启动失败 | vLLM 与 PyTorch/transformers 版本不兼容 | 检查版本匹配关系,必要时升级 vLLM 或回退 PyTorch |
| 输出里没有思考段,直接给答案 | 模型版本是纯对齐模型,或 prompt 模板改变 | 确认使用的是 R1 系列权重;按模型官方模板构造输入 |
评测任务报task not found | 任务名拼写错误或 harness 版本不支持 | 先执行lm_eval --task_list查看可用任务名 |
| 评测分数远低于官方报告 | 模型版本、量化、采样参数、任务配置不一致 | 固定自己的评测配置,只做横向对比,不追求复现官方数字 |
| 训练脚本 loss 直接变 NaN | 学习率过大或 logits 溢出 | 降低学习率,检查 reward 归一化是否有除零风险 |
9. 给同样想“玩两天”的普通开发者一些建议
如果你也准备抽出两天时间在 DeepSeek 开源生态里折腾,以下几条建议是我觉得最有价值的。
先跑通再调参。不要一上来就抠细节,先把 transformers 加载、vLLM 服务、HTTP 请求这一整条链路跑通。链路通了,之后所有实验都有基础。
不要只复制教程里的答案,要复现过程。拿到代码后,故意改一些参数,比如把 temperature 从 0.6 改成 1.0,看看输出变化。这种“破坏性体验”比照着教程抄一遍更能建立手感。
学会读报错。大模型工程里的报错绝大多数不是玄学,而是版本、路径、显存、模板四类问题。遇到报错,先看是哪个库抛出的,再顺着调用栈往前查,绝大多数问题都能定位。
把评测当成习惯,而不是终点。基准测试的意义不在于证明模型多厉害,而在于给你一个客观尺度。以后你改提示词、换推理参数,都要习惯用评测结果说话。
如果你在这个两天之旅里也能感受到那种“被权威工具架着走”的压迫感,别慌。那说明你已经意识到,真正难的不是生成一段文字,而是让模型稳定、可控、可评测地生成文字。这两天的价值不在于做出什么产品,而在于把黑盒拆出几条缝。下一步可以继续研究完整的提示词工程,或者深入 GRPO 的工程实现,路线已经清楚了。