如果你一直追我这个系列,应该知道前面的文章基本都在聊“怎么把Qwen3用起来”——本地部署、量化、Ollama集成、智能体调用。但前两天有个读者留言,说他用Ollama挂着Qwen3 4B,想让WorkBuddy那种工具代理帮他改代码,模型根本操作不了电脑,甚至把上下文全搞乱了。这个问题其实很典型:基础模型即使推理能力再强,它也不知道“你的任务目标是什么”“什么样的输出才叫好”。要让模型真正适配某个场景、按你想要的方式干活,单靠提示词是不够的,必须上强化微调。
这篇就是系列的第九篇,聊Qwen3的强化微调,核心玩法是GRPO。我会把GRPO的来龙去脉、数学直觉、实际训练脚本、踩坑经验一次讲透,最后再回头解释为什么Ollama上的Qwen3干不了“操作电脑改代码”这种智能体任务,以及强化微调能怎么救。
1. GRPO到底改了什么:从PPO到GRPO的思路转变
先聊个基础问题:为什么强化微调现在这么火,而且大多数人一提就是GRPO,而不是更早的PPO?
LLM的强化微调和传统强化学习不完全一样。模型输出一句完整的话,环境(或者奖励模型)给一个总分,这个过程中模型每一步生成token其实都在做决策,但奖励要等整句话出来才知道。这属于典型的延迟奖励问题,PPO就是应对这种场景的经典算法。PPO的做法是:训练一个Critic网络(价值模型)去预测每个状态下的期望收益,然后用“实际收益减去预测收益”作为优势函数来更新策略。思路没问题,但Critic训起来很麻烦:它要和策略模型一起迭代,规模巨大,训练不稳定,而且Critic的预测误差会直接污染策略更新。
GRPO是DeepSeekMath那篇论文里带出来的,核心思路特别直接:不要Critic了,用一组采样输出的相对表现来替代绝对价值估计。
为了帮你建立直觉,我打个比方。想象一个班级考试,PPO的做法是请一位助教(Critic)提前预测每个学生能考多少分,考完看实际分数和预测的差距,差距大的学生重点辅导。而GRPO的做法是:把试卷发给同一道题的8个学生,考完直接看8个人的相对排名,你排名靠后就是差,排名靠前就是好,不需要任何人提前预测分数。
这个“组内相对比较”就是GRPO最核心的“Group Relative”含义。对应到训练上:对同一个prompt,让当前模型采样出G个输出,分别算出奖励,然后做归一化得到一个优势值。奖励高于组内均值的输出,梯度方向是增强;低于的,则抑制。公式上就是:
[ A_i = \frac{r_i - \mathrm{mean}(r_1, ..., r_G)}{\mathrm{std}(r_1, ..., r_G)} ]
这个A_i就是每个样本的优势值。整套训练目标函数在GRPO原论文里还带一个KL散度约束项,防止模型一下子飘太远把语言能力全丢掉,这一点下面细说。
所以GRPO相比PPO的优势很明确:不需要训练Critic,显存占用低得多,框架简单不容易崩,收敛也相对稳定。在Qwen3这种参数量动辄几十B的模型上做全参微调,省掉一个和主模型同体量的Critic,意味着你硬件的压力几乎砍半。这也是为什么现在开源社区一提“强化微调”,几乎默认就是GRPO。
1.1 GRPO的奖励机制与KL散度控制
GRPO的训练循环里,每个prompt会采样G个补全结果,G通常取4到16之间。采样完成后,奖励函数对这G个结果分别打分,分数可以是程序生成的规则奖励(比如格式对不对、代码能不能运行),也可以是人类偏好奖励模型打分。得到G个分数后,用它们的均值和标准差做归一化,得到每个样本的优势值。这个归一化操作特别巧妙:它不关心奖励函数的绝对尺度是0到1还是0到100,只看相对高低,天然对奖励函数的scale不敏感。
但有一个隐患:如果只优化奖励,模型很快就会学会“刷分”——找到奖励函数里某个漏洞,输出一堆在奖励上得分高但实际没用的内容。为了约束这种行为,GRPO在目标函数里加了一个KL散度惩罚项,限制更新后的模型不要离参考模型(通常是SFT出来的那个版本)太远。数学上KL散度衡量的是两个概率分布的距离,这里就是说:“你可以为了奖励改变策略,但不能改得面目全非。”
实际操作中这个KL项有一个很直观的系数beta。beta调得太大,模型根本不怎么动,奖励上不去;调得太小,模型放飞自我,生成内容开始退化。我在Qwen3 14B上试跑的时候,beta从0.01起步是一个比较稳妥的选择,训练后期可以略微调小让模型更贴合任务。具体怎么监控,后面的实操章节讲。
1.2 为什么Qwen3特别适合GRPO微调
Qwen3这一代模型有个和GRPO天然契合的设计——思考模式(thinking mode)和非思考模式。你可以在配置里强制开启思考模式,让模型先输出一段推理过程再给最终答案,也可以关闭思考模式直接回答。GRPO奖励信号没法直接监督模型“该怎么思考”,但可以通过最终结果的质量间接塑造它的思考风格。比如代码生成任务里,模型如果学会“先拆解需求再写代码”,最终代码的运行通过率往往更高,这个信号会被GRPO捕捉并放大。
Qwen3本身在预训练阶段已经做了大规模的强化学习对齐,基础能力已经相当扎实。拿来做GRPO的起点,比从纯基座模型开始要顺滑得多。你再用GRPO去做某个垂直场景(比如SQL生成、工具调用、格式化输出)的适配,有点“在好地基上装修”的意思,不需要从零学语言,只需要学会你的任务偏好。
另外一个实际考量是:Qwen3的官方技术报告明确提到他们在训练中混合了标准和思考模式的数据,这意味着模型内部对两种模式都有一定的驾驭能力。但默认情况下你直接调用它,它未必知道你的场景里要不要思考、思考多久。GRPO可以在奖励函数里明确告诉它:输出正确给多少分、输出冗长扣多少分、想太久扣多少分。训练完,模型会自己学会“在这个任务里,简要思考并快速给答案”是最优策略。
2. 强化微调的目标与数据准备:别急着改权重,先搞清楚你要什么
开始写代码之前,先说一句可能得罪人的大实话:大部分人做强化微调失败,不是因为代码写错,而是因为没想清楚“奖励函数到底奖励什么”。
SFT(监督微调)是拿“标准答案”去教模型模仿,强化微调是拿“评价标准”去引导模型探索。评价标准就是奖励函数,它决定了模型演进的方向。奖励函数定义错了,你训出来的模型可能分数很好看,实际一用就废。
2.1 明确任务目标和奖励信号
Qwen3的GRPO微调能做的任务大致分三类,不同任务的奖励函数长完全不一样。
第一类是规则可验证的确定性任务,典型代表是数学题、SQL生成、代码生成、JSON格式输出。这类任务的好处是结果可以自动判对错,程序就能当裁判。比如数学题,答案对了给1分,错了给0分,外加格式规范分。代码题可以真的把代码扔进沙盒跑一遍,通过测试用例给高分。这一类是GRPO最容易见效的,因为奖励信号干净、无歧义。
第二类是偏好类任务,比如生成营销文案、调整语气风格。这类没有标准答案,需要训练一个奖励模型来模仿人类偏好打分。一般流程是:先让Qwen3生成一批候选回答,人工排序,用排序数据训练一个Reward Model,再用这个RM的分数作为GRPO的奖励信号。这一套链路复杂不少,对新手不太友好。
第三类是智能体任务,包括工具调用、浏览器操作、操作电脑改代码——就是开头那个读者遇到的问题。这类任务最麻烦,因为最终奖励往往是“任务是否完成”,但中间过程可能有几十步,每一步的对错很难自动判断,奖励信号稀疏且延迟。一种做法是过程奖励模型逐点评分,另一种是设计密集的规则奖励,比如每正确调用一个工具就给小分。WorkBuddy那种“让模型操作电脑”的场景,想要做强化微调,最务实的奖励函数是:任务完成后截屏对比,或者代码修改后跑测试套件看通过率。
我做GRPO微调的经验是,第一类任务拿来入门最合适,数学题和JSON格式提取都是很好的练手项目。奖励函数能自动算、训练循环能跑通、损失曲线有明显变化,你才能逐步理解强化微调的行为模式。一上来就做智能体任务,排查问题时变量太多,很容易崩溃。
2.2 数据集的构建与格式要求
GRPO微调的数据集结构比SFT多一个维度。SFT只需要prompt和response对,GRPO的每个训练样本只需要prompt,因为输出是训练过程中实时采样生成的。这意味着你的数据集配置里,最重要的字段是“系统提示词+用户请求”,不需要为每条数据准备标准答案。
以数学题为例,一条训练数据可以长这样:
{ "prompt": "一个水池有两个进水口和一个排水口,单独开第一个进水口需要6小时注满,单独开第二个进水口需要4小时注满,打开排水口需要8小时排空。如果三个口同时打开,多久能注满?请给出最终答案。" }是的,就这简单。真正的标准答案不在数据集里,而是藏在奖励函数里。你需要另写一个check_answer函数,把模型的输出和真实答案比较。这就要求你额外准备一份“答案列表”,每条prompt配一个标准答案用于判分,这个答案不进训练集,只进验证函数。
我是建议把答案直接嵌在prompt的后端字段里,比如搞成一个JSONL文件,每行包含prompt和expected_answer,训练时一个字段喂给模型、一个字段喂给奖励函数。这种配置对排查问题也方便,你能一眼看出某条样本的预期结果是什么。
对于工具调用或智能体场景,数据格式更复杂。每一条数据需要包含完整的工具定义、可用操作列表、任务描述。奖励函数里要对“调用了正确的工具”“传参格式合法”“最终结果达成了目标”分别给分。这类数据集我建议先手工构建50到100条高质量样本就够了。强化微调阶段不需要海量数据,它更像一个“偏好塑造器”,几百条精挑细选的任务,就足以让模型行为产生明显的定向偏移。我第一次跑GRPO实验时用了2000条数学题,训练到后面其实过拟合了——模型把那些题目的解题套路背下来了,但对新题泛化一般。后来降到300条,效果反而更好。
3. 训练环境配置与代码实现:基于TRL跑通Qwen3 GRPO
工具链上我推荐直接用Hugging Face的TRL库。TRL从0.12版本开始把GRPOTrainer集成得比较完善,RewardFunc API也写得很舒服。版本选型上,建议trl>=0.14.0,transformers>=4.46.0,accelerate>=1.1.0。前阵子我还遇到过trl 0.12和transformers 4.48之间兼容性出问题的情况,旧版本在处理Qwen3的chat template时偶发字段错位。
硬件方面先说清楚:如果你用Qwen3 4B做LoRA微调,一张24GB显存的显卡(4090或3090)就够跑了。但如果是Qwen3 14B,做LoRA微调最少要48GB以上,全参微调直接上80GB的A100/H100才靠谱。GRPO因为一次要采样组内G个输出,显存开销比SFT高不少,批量大小要适当调小。
3.1 环境安装与基础配置
先建一个干净的conda环境,Python用3.11。安装依赖的时候注意,trl、transformers、peft这几个库的版本要互相兼容,我建议直接装最新版,然后跑一个小模型冒烟测试,确认训练循环能起来再切Qwen3。
conda create -n grpo python=3.11 -y conda activate grpo pip install --upgrade transformers accelerate peft trl datasets pip install bitsandbytes然后写训练脚本,推荐用TRL官方的GRPOTrainer,配置起来思路清晰。核心配置项主要关注这几个:
from trl import GRPOConfig, GRPOTrainer from transformers import AutoModelForCausalLM, AutoTokenizer import torch model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen3-4B", torch_dtype=torch.bfloat16, device_map=None ) tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-4B") training_args = GRPOConfig( output_dir="./qwen3_grpo_math", learning_rate=2e-6, per_device_train_batch_size=2, gradient_accumulation_steps=4, num_train_epochs=1, max_completion_length=2048, beta=0.01, temperature=0.9, seed=42, logging_steps=1, save_steps=50, bf16=True, )几个参数我特别标注一下:learning_rate用2e-6起步,强化微调的学习率一定要比SFT小。SFT用5e-5都没事,GRPO用2e-6我都嫌跳,试过5e-6都能看到输出质量明显波动。max_completion_length要覆盖模型可能生成的最长输出。Qwen3的思考模式有个特点——一旦开启思考,它会输出一大段推理过程,如果max_completion_length设短了,模型在思考到一半被截断,训练就废了。temperature设到0.9左右,采样多样性才能保证同一个prompt产出的G个输出差异够大,GRPO的归一化才有意义。如果temperature太低,G个输出几乎一模一样,优势值全是噪声。
3.2 奖励函数编写:GRPO微调的灵魂
TRL的GRPOTrainer里,奖励函数就是一个接收completion和**kwargs的Python函数,返回一个分数。可以同时传多个奖励函数,训练器会把分数线性加权求和。我一般写两个奖励函数:一个管“正确性”,一个管“格式”。
数学题的正确性奖励函数,最稳的写法是用字符串匹配:让Qwen3输出答案时带一个特殊标记,比如“最终答案:xxx”,然后提取并和标准答案比较。
import re def correctness_reward(completions, **kwargs): """检查数学答案是否正确。completions是模型生成的一组文本列表。""" rewards = [] for completion in completions: # 从输出里提取最终答案标记 match = re.search(r"最终答案[::]\s*([^\n]+)", completion) if match: answer_text = match.group(1).strip() else: answer_text = "" # 和标准答案做归一化比较(去掉空格和单位) normalized_answer = answer_text.replace(" ", "").replace("小时", "") if normalized_answer == kwargs["expected_answer"].replace(" ", ""): rewards.append(1.0) else: rewards.append(0.0) return rewards这个函数里有个细节值得注意:completions是一个列表,对应同一个prompt采样的G个输出,bonus奖励可以考虑“格式分”,如果模型规规矩矩带“最终答案”标记,额外给0.1分。经验是纯正确性奖励下模型偶尔会出现“答案对了但输出格式乱糟糟”的情况,带上格式分训出来的东西更像样。
def format_reward(completions, **kwargs): """要求输出必须包含思考过程和最终答案部分。""" rewards = [] for completion in completions: if "最终答案" in completion and "思考" in completion: rewards.append(0.1) else: rewards.append(0.0) return rewards需要注意,TRL的奖励函数传入的completions不包含prompt部分,只包含模型新生成的内容。如果你的奖励判断需要看完整输出,得通过**kwargs把原始prompt传进去自己拼。
3.3 启动训练与实时监控
把所有奖励函数组成列表传给GRPOTrainer:
trainer = GRPOTrainer( model=model, processing_class=tokenizer, args=training_args, train_dataset=dataset, reward_funcs=[correctness_reward, format_reward], ) trainer.train()训练起来以后,日志里除了常规loss,还有一组关键指标:rewards、completion_length、kl。我强烈建议你盯紧这三个数。
rewards mean稳步上升是好事,但如果上升太快,比如几百步就到了0.9以上,要警惕过拟合或reward hacking。completion_length如果出现剧烈下降,比如从2000掉到300,说明模型正在偷懒,学会用最短输出撞奖励了。kl如果持续飙升,说明beta设太小,模型在语言能力上退化,该调大beta或者降低学习率。至于loss本身,GRPO里的loss包含策略梯度项和KL项,看它绝对值意义不大,关注相对变化即可。
我在14B模型上跑过一次数学微调,550步左右模型已经在训练集上能稳定达到95%正确率,但评测集只有78%,这就是典型的reward hacking迹象——模型找到了奖励函数的漏洞。比如有的样本标准答案是“12”,模型直接输出“最终答案:12”蒙对了,但它根本没学会解题。这个问题的根源是GRPO本身不惩罚“猜”的行为,只惩罚“错”。要缓解,要么在奖励函数里加过程分(比如包含正确的中间步骤),要么在数据集里掺入大量“无法蒙对”的题目,要么降低beta让模型不敢走极端。没有一招制敌的办法,只能多管齐下。
4. 实战中的坑与排查手册:我的GRPO翻车实录
这一节把我在Qwen3 GRPO微调过程中实际踩过的坑盘一盘。这些坑单个看都不致命,但叠在一起足够让一个训练任务从“看起来正常”变成“彻底报废”。
4.1 生成空内容的崩溃
第一个坑:模型训练过程中突然开始输出空字符串或几个无意义token。loss看起来在降,但奖励全是0,模型越训越差。排查过程最后定位到是采样参数问题——GRPO内部采样时如果temperature设置过低加上Top-P截断,模型容易塌缩到高概率token的重复循环,极端情况下会生成空内容。
解决办法有两个:一是把temperature调到0.9以上,二是给completion长度设一个下限,比如在奖励函数里发现completion长度小于50个字符直接给一个大的负分。我后来两种都做了,空内容基本绝迹。
4.2 显存溢出与批量大小调整
GRPO的显存压力主要来自组内采样。一个batch里如果有2个prompt,每个prompt采8个输出,等于一次前向要同时推理16个序列,再加上反向传播计算图,显存占用比同batch的SFT翻了不止一倍。4B模型的LoRA微调在24GB卡上,per_device_train_batch_size开到2勉强能跑,14B模型就得降到1,甚至要靠gradient_accumulation_steps补batch size。
另外一个容易被忽略的显存杀手是max_completion_length。Qwen3开启思考模式后,模型经常会生成2000到3000个token的长输出,如果max_completion_length设到4096,显存占用会指数级上升。建议先用原始模型跑一小批数据,统计一下输出长度分布,再定这个参数。
4.3 奖励函数的长度偏向
GRPO对奖励函数的scale不敏感,但对偏差非常敏感。如果你的奖励函数里隐含着“输出越长分越高”或“输出越短分越高”的规律,模型会疯狂利用这一点。
我之前写代码生成任务奖励函数时,给“正确通过测试用例”打1分,但在实现时忘了对“编译失败”打负分。结果模型学到一个新策略:输出一段很长的代码,但根本不运行,因为奖励函数只检查代码片段里是否包含关键函数名。这种reward hacking让人哭笑不得。从那之后我养成了一个习惯:每写一个奖励函数,都要想清楚“模型可能怎么钻空子”。
表格总结一下常见问题和排查方向:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| loss不降,reward长期为0 | 奖励函数有bug,模型输出永远无法命中 | 先用固定输出测试奖励函数 |
| reward飙升高但评测集差 | reward hacking | 加过程奖励、降低beta、丰富数据集 |
| completion_length断崖式下跌 | 模型学会用短输出撞奖励 | 奖励函数里惩罚过短输出 |
| kl指标持续暴涨 | beta太小,模型偏离参考模型太远 | 增大beta或降低学习率 |
| 训练正常但模型语言能力退化 | KL约束失效,模型过于专注任务格式 | 调大beta、加入通用语料数据 |
| 显存OOM | 组采样数G过大或max_completion_length过长 | 减小G、缩短输出长度上限 |
4.4 回到开头的WorkBuddy问题:为什么Ollama上的Qwen3操作不了电脑
现在可以解释开头那个场景了。WorkBuddy这类智能体要“操作电脑改代码”,核心链路是:模型接收桌面截图或文件内容,生成操作动作,调用工具修改代码文件,然后观察新截图判断下一步。这条链路上模型要同时具备视觉理解、工具调用格式化、长程规划三个能力,任何一个拉胯都会让整个过程崩掉。
而默认的Qwen3-4B量化版,也就是读者在Ollama里跑的那个,是一个通用对话模型,而不是一个训练过的智能体操作模型。它不知道“操作电脑”这个动作空间的语法,也没学过“观察截图的编码规则”。直接拿提示词硬钢,就像让一个从没摸过方向盘的人直接上赛道——理论上他有驾驶知识,但完全不具备操作能力。
要救这个场景,强化微调还真是一条正路:收集一批“屏幕截图+操作动作”的轨迹数据,把“成功完成一次文件修改”设为最终奖励,用GRPO把模型从“能聊天”微调成“会操作”。这个工程量不小,但从技术上完全可行。Qwen2.5和Qwen3系列都有对应的VLM版本,为这类任务提供了足够好的底座。
5. 本地部署与推理验证:GRPO微调后的模型怎么用起来
训练完的模型权重如果只存在checkpoint里,价值就打了折扣。我用LoRA方式微调时,习惯把Adapter合并回主模型,导出成完整的模型文件,再部署到Ollama或者vLLM上供日常调用。
5.1 合并LoRA权重与导出
TRL训练完的LoRA Adapter会存在output_dir下。合并权重的操作很简单:
from peft import PeftModel base_model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen3-4B", torch_dtype=torch.bfloat16, device_map="auto" ) lora_model = PeftModel.from_pretrained(base_model, "./qwen3_grpo_math/checkpoint-500") merged_model = lora_model.merge_and_unload() merged_model.save_pretrained("./qwen3_grpo_math_merged") tokenizer.save_pretrained("./qwen3_grpo_math_merged")合并之后一定要先做一个冒烟测试:设定和训练时一致的system prompt,喂一条训练集里的题目,看输出是否包含思考过程和最终答案标记;再喂一条没见过的题目,看回答质量。如果新题回答明显退化,大概率是过拟合了,回训练阶段调数据多样性。
5.2 部署到本地推理服务
合并后的模型可以直接用Ollama部署。Qwen3官方提供了适合Ollama的GGUF量化版本,但那是原版模型。微调合并版要用llama.cpp先转成GGUF格式,再导入Ollama。
# 使用llama.cpp的convert脚本将HF模型转为GGUF python convert_hf_to_gguf.py ./qwen3_grpo_math_merged \ --outfile qwen3_grpo_math.gguf \ --outtype q8_0 # 创建Ollama模型文件 cat > Modelfile << 'EOF' FROM ./qwen3_grpo_math.gguf TEMPLATE "{{ .Prompt }}" EOF ollama create qwen3-grpo-math -f Modelfile ollama run qwen3-grpo-math如果只是自己API调用,vLLM是更省事的选择,直接加载HF格式:
vllm serve ./qwen3_grpo_math_merged \ --port 8000 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9vLLM加载HF格式不需要转格式,部署速度更快。如果你希望本地部署+走OpenAI API风格调用,vLLM的/v1/chat/completions接口几乎零成本接入现有代码。需要注意的是,vLLM部署时要记得带上训练时用的system prompt模板,如果模板不一致,模型行为会有非常明显的偏差。
5.3 部署后效果的前后对比
最后分享一个14B模型做代码生成任务GRPO微调的实际效果数字。微调前模型直接生成Python代码,测试用例通过率大约47%,输出里经常夹杂解释性文字。用GRPO微调500步后(数据集约350条代码题),测试用例通过率提升到69%,并且输出的代码格式更加规范——模型学会只在代码块里给代码,不加废话。
更让我意外的是,GRPO后的模型在错误处理上明显更稳健。遇到编译错误时,微调前的模型会重复输出同一条错误代码,而微调后的模型学会了自己尝试修复——从“重新导入模块”到“调整函数签名”,开始呈现出策略性的试错行为。这种“策略涌现”是纯SFT很难训出来的,因为SFT本质上是在模仿,而GRPO是在奖励的引导下自主探索。你给它一个目标,它自己摸索出一套路径,这个过程永远看不够。
6. 写在最后的实操体会
说了这么多,最后分享几条我实际跑GRPO微调攒下来的体会。
第一,GRPO的训练效果对“奖励函数质量”的敏感度远超对“模型基座选择”的敏感度。哪怕是用Qwen3-4B这样的小模型,只要奖励函数设计合理,微调后的行为偏移会非常明显;反过来,奖励函数写稀烂,14B照样训成一坨。所以动手写代码前,先在纯逻辑层面把奖励函数推演一遍。这看起来不酷,但比训练翻车后去调代码有效得多。
第二,强化微调是周期性项目,不是一次性工程。我之前以为训完一次就完事,直到上线部署发现真实场景分布和训练分布差别大了,效果下跌非常快。后来我形成了一个习惯:每隔一两周,把线上收集到的新失败样本加进数据集,重新跑一轮GRPO微调。这个迭代周期并不长,几百条数据跑500步就够,但模型始终保持对真实场景的适应能力。
第三,GRPO的训练曲线不会像SFT那样平滑优美。SFT的loss曲线一路向下,而GRPO的reward曲线在训练早期往往震荡剧烈,甚至出现先涨后跌的情况。看到这种曲线别慌,先检查有没有出现4.3节说的“短输出撞奖励”迹象,如果没有,就让它继续跑。强化学习本身就带有探索性质,震荡是正常现象,关键看总体趋势。
第四,关于Ollama部署的Qwen3做智能体操作这件事,我的判断是:现阶段直接用通用模型走提示词路线确实不靠谱,但也不要因此否定“本地小模型”的潜力。先用GRPO微调把模型的行为空间锚定到具体任务,再配合合理的工具接口设计,4B-7B级别的模型在单机自动化场景上完全能做成一个可用的原型。这条路我还在继续走,后续有新进展会在这个系列里更新。今天这篇写到这,GRPO的原理、实操和坑基本都覆盖到了,希望对正要上路的朋友有帮助。