JitRL:无梯度持续强化学习,低成本提升LLM Agent能力 LLM Agent 的持续强化学习一直是让中小团队头疼的事——想提效果就得训模型想训模型就得堆卡堆完卡还得调参一圈下来预算和时间全烧在试错上。最近有一篇被某机器学习顶会收录为 Oral 的论文提出了一个叫JitRL的思路标题里那句“98 美元替代 3500 美元 GPU 训练”很抓眼球。它的核心主张是Agent 的能力提升不必走梯度更新这条路把强化学习挪到推理阶段、让策略以文本形式沉淀下来同样能跑出持续学习的闭环。这个思路对预算有限、又想让 Agent 在实际任务里越用越好的团队来说参考价值相当大。下面我按自己的理解把这套方法拆开讲顺带把能直接抄的最小实现和踩坑点一起写出来不管你是刚接触 Agent 的新手还是已经在做 RLHF 的老手都能拿到点东西。1. 先把问题摆正JitRL 到底在解决什么1.1 传统 Agent 强化学习的钱一大半花在“不该花”的地方想让一个 Agent 在真实任务上稳定提升常规做法是收集轨迹、算优势、做策略梯度更新。这条路技术上成熟但成本结构非常难看。我拿一个 7B 量级的模型举例走 PPO 或者 GRPO 这类路线硬件上至少需要一组 80GB 显存档位的训练卡常驻因为你要同时装下策略模型、参考模型、优化器状态还要留出 rollout 的显存余量。租用成本按小时算一轮完整训练加验证几十小时是常态账单四位数美元起步。更麻烦的是隐性成本。第一是调参返工学习率、KL 系数、batch 组织方式、奖励归一化任何一个设歪了都要重跑而每次重跑都是真金白银。第二是环境搭建Agent 任务往往涉及多轮工具调用、外部状态变更要把这些包装成可并行、可复现的训练环境工程量不比模型训练本身小。第三是奖励工程你很难给“这个回答是否解决了用户问题”写一个可微的奖励函数最后还是要靠规则或者另一个模型打分而这一步在传统 RL 里是前置的、一次性的打得不准就直接把策略带偏。所以真实情况是一个团队花 3000 多美元最后拿到的可能只是一个在特定分布上稍微好一点的 checkpoint换个任务场景又得重来。这个投入产出比对绝大多数非大厂团队来说是不成立的。1.2 换一个问法Agent 的能力一定要存在权重里吗JitRL 最有价值的地方不是某个具体算法而是它把问题重新问了一遍。一个 Agent 表现好不好能力其实存在三个地方模型权重、上下文里的指令与示例、外部可检索的记忆与工具。传统强化学习只动第一个而工业界早就发现后两个的杠杆率极高——同一个基座模型提示词写得好不好任务成功率能差出二三十个点。这就引出一个很自然的想法既然改上下文就能显著改变行为那为什么不把“强化学习”这件事直接作用在上下文上把策略的搜索空间从“几亿个浮点参数”换成“几百条自然语言规则”把梯度下降换成“采样、打分、筛选、归纳”整个训练流程就不需要反向传播了也就不需要训练卡常驻。打个比方传统 RL 像是把员工送去重新读一遍大学改的是他的大脑结构JitRL 像是给员工配一本不断增厚的岗位手册遇到新情况就翻手册、试新招、把有效的招记下来。手册写得越厚越准员工的表现就越好但员工本人没变。这个类比不完美但它解释了为什么“无梯度更新”不等于“不学习”。1.3 98 美元和 3500 美元这笔账到底怎么算标题里的两个数字很容易被误读我得把口径说清楚。3500 美元那一侧通常对应的是“租用多卡训练集群、跑完一轮包含多次迭代的完整 RL 训练”里面包含了无效迭代和调参浪费。98 美元那一侧对应的是“只调推理接口、跑若干轮经验采集与打分”成本几乎全部是 token 费用。具体拆开看假设一次任务尝试平均消耗 4000 个输入 token 加 800 个输出 token采样 8 条候选、跑 200 个任务、迭代 5 轮总调用量大概在千万 token 级别。按当前主流商用接口的价格几十到一百多美元的区间是合理的。再加上打分环节用的小模型调用总盘子控制在 100 美元上下并不夸张。注意这两个数字不是“同等效果下的成本对比”而是“达到某个可接受效果水位所需的成本对比”。JitRL 在某些任务上能追平甚至超过梯度训练的结果在另一些任务上会明显落后。把它理解成“低成本快速试错的一条新路径”比理解成“训练卡的替代品”要准确得多。我个人的判断是这套方法真正的价值在于把试错成本压到了一顿外卖的量级。你可以用极低的代价先验证“这个任务到底值不值得投入更多”验证通过了再去考虑要不要上真正的权重训练。这个决策顺序的调整比省下来的那几千美元更重要。2. 核心机制无梯度更新的强化学习怎么跑起来2.1 把“参数搜索”换成“策略文本搜索”JitRL 的整个循环可以概括成四步采样、打分、蒸馏、复用。给定一个任务Agent 在注入当前经验库的情况下生成 N 条候选轨迹打分器给每条轨迹一个分数把高分轨迹和低分轨迹做对比提炼出“下次应该怎么做”的规则文本规则经过准入判断后进入经验库下一轮任务开始时被检索注入。这里的关键设计在于搜索空间的定义。传统 RL 搜索的是参数空间梯度告诉你怎么走JitRL 搜索的是自然语言规则空间没有梯度可用所以必须靠“候选生成加相对比较”来近似梯度方向。具体做法是让模型同时看到成功样本和失败样本问它“这两条轨迹的差异在哪里哪条做法是可以复用的”让模型自己完成差分归纳。这一步本质上是用模型的归纳能力替代了反向传播的信用分配。为什么这个替代能成立因为 LLM 本身就是在海量文本上训练出来的模式归纳器给它几组对照样本让它总结规律是它最擅长的事情之一。相比之下让它从稀疏的标量奖励里学到一个策略反而是它的弱项。JitRL 相当于把学习任务交给了模型更擅长的能力通道。2.2 经验库Agent 的“外挂长期记忆”怎么设计经验库是整个方法的地基。设计得粗糙几轮之后就会变成一堆互相矛盾、检索不准、越用越差的垃圾文本。我在复现时把每条经验设计成下面这些字段字段作用设计要点exp_id唯一标识用规则文本的哈希天然去重task_sig任务签名用于检索匹配建议用任务类型加关键实体不要用完整原文rule可执行规则必须是祈使句、可直接塞进提示词禁止写成分析报告evidence支撑证据一两条原始轨迹的摘要用于人工审计和后续再蒸馏score入库得分入库时的平均分作为冷启动的先验uses/wins使用次数与胜出次数支撑置信区间估计与淘汰决策created_at/last_used时间戳用于老化淘汰task_sig的设计特别值得多说两句。很多人第一反应是拿任务文本做向量化然后按余弦相似度检索。这在任务高度同质的时候好使但一旦任务类型混杂向量相似度会失灵——两个任务字面很像但一个是“改代码”、一个是“改文案”需要的规则完全不同。我的做法是两级签名第一级是任务类型标签分类、抽取、多步工具调用等第二级是关键实体或约束条件的集合。检索时先按第一级硬过滤再在候选集里算相似度。这样召回精度会稳很多。2.3 信用分配没有梯度怎么知道哪一步做对了这是整套方法里最难的部分也是最容易被做歪的地方。轨迹级打分只能告诉你“这次成了还是没成”但一条轨迹可能有十几步到底是第三步的检索用对了还是第七步的格式没出错光看总分是看不出来的。我试过三种归因方式效果差别很大。第一种是整条轨迹一起喂给模型让它自己找差异成本最低但模型容易被表面差异带偏经常总结出“要多思考”这种没用的废话。第二种是步骤级打分让打分器对每个中间步骤单独给分再取分数波动最大的步骤作为关键点精度高但调用成本翻好几倍。第三种是反事实对比把成功轨迹的某一步替换成失败轨迹对应的步骤看整体分数掉多少掉得多说明这一步关键。这种方式最准但需要重跑轨迹成本最高。实测下来我推荐一个折中方案先用整条对比做粗筛只对分数差距大的样本对做步骤级定位。也就是说只有成功轨迹和失败轨迹的分差超过阈值时才值得花额外成本去做精细归因分差本来就不大的样本对说明差异是噪声直接跳过。这个策略能把归因成本压到原来的三分之一左右精度损失很小。2.4 防止经验库退化三道必须设的闸门经验库有个很讨厌的特性它会自我强化。一条规则一旦入库后续任务就会被它影响如果这条规则其实是过拟合的它会持续制造同类失败而这些失败又会被误读成“这条规则不够强”于是继续加码。跑上十几轮整个库就被几条错误规则绑架了。第一道闸门是准入。规则必须在至少两个不同的任务实例上验证有效才能入库单次成功的样本一律当噪声处理。第二道闸门是淘汰。给每条规则维护使用次数和胜出次数用置信下界做排序连续多轮使用但胜率低于基线的规则直接下线不要舍不得。第三道闸门是冲突消解。新规则入库前先和库里的高相似规则做比对如果语义高度重叠但结论相反不要两条都留而是生成一个“在什么条件下用哪条”的元规则把冲突显式化。提示这三道闸门里第二道最容易被忽略。很多人建库很积极清理很消极结果库越滚越大检索命中率越来越低最后模型被一堆低质量上下文拖垮。我的习惯是每轮结束后固定跑一次清理脚本把胜率排在末位的 10% 强制下线宁可少几条也不要留垃圾。3. 动手实现一套最小可跑的 JitRL 骨架3.1 环境准备与目录结构这套东西对硬件几乎没有要求一台普通开发机就够因为所有计算都在远端推理接口上完成。依赖也很简单一个 HTTP 客户端、一个向量或关键词检索引擎、一个结构化存储。我不建议一上来就上向量数据库先用 JSONL 加关键词匹配跑通闭环等库超过几千条再考虑换。目录我习惯这么组织jitrl/ ├── exp_store.py # 经验条目定义与读写 ├── retriever.py # 两级签名检索 ├── scorer.py # 打分器组合 ├── distiller.py # 轨迹对比与规则蒸馏 ├── agent.py # 单次任务执行 └── loop.py # 主训练循环把打分、蒸馏、执行拆成独立模块的好处是每一块都能单独换实现。比如打分器一开始用规则后来想换成模型评判只要接口不变主循环一行都不用改。3.2 经验条目的数据结构import json import hashlib import time from dataclasses import dataclass, asdict, field dataclass class Experience: exp_id: str task_type: str # 一级签名任务类型 task_keys: list # 二级签名关键实体与约束 rule: str # 祈使句形式可直接注入提示词 evidence: str # 原始轨迹摘要便于审计 score: float # 入库时的平均分 uses: int 0 wins: int 0 created_at: float field(default_factorytime.time) last_used: float 0.0 staticmethod def make_id(rule: str) - str: return hashlib.md5(rule.strip().encode(utf-8)).hexdigest()[:12] def load_pool(path: str) - list: pool [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if line: pool.append(Experience(**json.loads(line))) return pool def append_pool(path: str, exp: Experience) - None: with open(path, a, encodingutf-8) as f: f.write(json.dumps(asdict(exp), ensure_asciiFalse) \n)用哈希做 ID 是个小技巧它天然实现了“同一条规则不会重复入库”。前提是入库前对规则文本做统一清洗去首尾空白、统一标点、去掉“我们”“应该”这类无意义前缀。清洗做得好去重效果能提升一大截。3.3 打分器规则分和模型分的加权组合打分器是决定整个系统上限的部件。我的经验是优先把能写成规则的硬指标做扎实模型评判只做兜底。原因很实在规则分稳定、可复现、零成本模型分有方差、有偏见、要花钱。一个任务如果连硬指标都定义不出来说明它本身还不适合用这套方法优化。def score_trajectory(traj, task, judge_client, weights(0.7, 0.3)): # 硬指标可程序化验证的部分比如格式合法、字段齐全、数值在区间内 hard hard_check(traj, task) # 返回 0~1 # 软指标交给模型评判只问一个具体问题不要让它自由发挥 soft judge_client.rate( instruction判断该回答是否完整解决了任务描述中的全部要求只输出 0 到 10 的整数, contexttask.text, answertraj.final_output, ) / 10.0 w_hard, w_soft weights return w_hard * hard w_soft * soft这里有个细节值得强调给模型评判的指令必须极度具体绝对不能写成“评价这个回答好不好”。我见过太多人在这上面翻车评判指令一模糊分数就变成模型心情的映射同一份输出跑两次给两个分整个训练循环的信号全是噪声。正确的做法是把评判维度拆到最细一次只问一件事比如“是否遗漏了任务里的第几个要求”“数字是否正确”“格式是否符合规定”。3.4 检索与注入用 UCB 平衡“稳妥”和“探索”经验库检索不是简单的相似度排序。相似度高但从来没被验证过的规则和相似度略低但胜率稳定的规则哪个更值得注入这是典型的探索利用权衡问题用多臂老虎机的 UCB 公式处理很合适。import math def select_experiences(task, pool, top_k3, c0.5): total_uses sum(e.uses for e in pool) 1 cands [ e for e in pool if e.task_type task.type and overlap(e.task_keys, task.keys) 0.5 ] def ucb(e): avg e.wins / e.uses if e.uses else e.score bonus c * math.sqrt(math.log(total_uses) / (e.uses 1)) return avg bonus cands.sort(keyucb, reverseTrue) return cands[:top_k]top_k不建议开大。我实测下来 3 到 5 条是最优区间再往上加规则之间开始互相干扰模型会顾此失彼。而且注入的经验越多提示词越长成本越高边际收益递减得很明显。有个粗暴但有效的判断方法如果加到第 6 条规则后分数没有提升就砍回 4 条。3.5 主循环采样、打分、蒸馏、入库MAX_EPISODE 200 N_SAMPLE 8 ADMIT_THRESHOLD 0.75 PAIR_GAP 0.25 # 只有分差够大的样本对才做精细归因 for ep in range(MAX_EPISODE): task task_queue.next() exps select_experiences(task, pool, top_k4) prompt build_prompt(task, exps) trajs [agent.run(prompt, temperature0.8) for _ in range(N_SAMPLE)] scored [(t, score_trajectory(t, task, judge)) for t in trajs] scored.sort(keylambda x: x[1], reverseTrue) worst (scored[0][0], scored[0][1]) for best_traj, best_score in scored[:3]: if best_score ADMIT_THRESHOLD: continue if best_score - worst[1] PAIR_GAP: continue # 分差不明显差异可能是噪声跳过 rule distiller.extract(best_traj, worst[0], task) if not rule or Experience.make_id(rule) in existing_ids: continue if validator.verify(rule, task, min_cases2): append_pool(POOL_PATH, Experience( exp_idExperience.make_id(rule), task_typetask.type, task_keystask.keys, rulerule, evidencebest_traj.summary, scorebest_score, )) cleanup_pool(POOL_PATH, drop_ratio0.1)这段代码里有几个我认为不能省的设计。PAIR_GAP那个判断很关键它挡掉了大量“分差本来就是噪声”的无效蒸馏能省下可观的调用成本。validator.verify里的min_cases2是我反复调出来的值只验证一次的话漏网率太高验证三次以上成本又开始不划算。cleanup_pool放在循环末尾而不是定期跑是为了让库的状态始终可控不会出现某几轮突然膨胀的情况。4. 实测数据与调优哪些参数真的重要4.1 一组小规模实测记录我在一个包含多步信息抽取的任务上跑了两百轮粗略记录了几个关键指标的变化。起始状态下不做任何注入任务成功率大概 58%跑完第一轮经验注入后涨到 66%第五轮之后进入平台期稳定在 71% 到 74% 之间波动经验库规模从 0 涨到 40 条左右之后基本不再增长因为清理机制在同步起作用。成本的构成也很有意思。总花费里任务执行本身只占四成打分和蒸馏加起来占了六成。这有点反直觉因为直觉上执行轨迹是最耗 token 的。原因在于我一开始把打分做得太细每条轨迹都要跑三次模型评判。后来把硬指标补齐、软指标压到只问一个问题打分成本直接降了一半多。阶段成功率库规模累计成本相对值基线无注入58%01.0第 1 轮后66%111.3第 5 轮后72%342.1第 20 轮后74%414.6第 50 轮后73%389.8这张表最值得看的是最后两行。第 20 轮之后成本翻了倍效果反而略有回落。这不是个例几乎每次跑长循环都会遇到。原因通常是库里的规则开始互相覆盖检索时注入了次优组合。所以跑多少轮要提前定死不要抱着“再跑跑可能更好”的侥幸心理我一般把轮数卡在效果曲线的拐点之后一点点就停。4.2 关键参数的影响排序按我踩坑的顺序排影响从大到小大概是这几个参数建议取值调大的后果调小的后果每条任务的采样数6~10成本线性涨收益边际递减候选质量差蒸馏出噪声注入规则条数3~5规则互相干扰提示词膨胀复用不足学不到东西入库分数阈值0.7~0.8库增长慢可能漏掉好规则垃圾规则混入污染库精细归因的分差门槛0.2~0.3蒸馏次数少学习慢大量无效蒸馏纯烧钱每轮淘汰比例5%~10%好规则被误杀库震荡库缓慢膨胀检索变差采样数这一项我想多说一句。很多人第一反应是“采样越多越好”实际上采样数超过 10 之后候选轨迹的同质化会非常严重模型在相同提示下反复生成换汤不换药的东西。这时候继续加采样纯属浪费真正有效的手段是提高采样温度把分布的尾部打开。但温度调太高又会引入大量废轨迹所以两者要配合着调我一般是先定温度在 0.7 到 0.9再看候选多样性决定采样数。4.3 什么任务适合什么任务别碰这套方法有个很清晰的适用边界它优化的对象是“怎么做”这类程序性知识不是“知不知道”这类事实性知识。适合的场景长这样任务有明确的可验证结果有多种解法且效果差异大同一个任务类型会反复出现。比如格式规范化、多步检索的组织方式、工具调用的顺序编排、报告结构的搭建这些都属于“方法定了效果就上去了”的类型。不适合的场景也很明确。第一类是知识密集型任务模型不知道某个事实你写一百条规则它还是不知道这时候该做的是检索增强或者微调不是强化学习。第二类是主观评价任务好坏没有稳定标准打分器自己都摇摆整个循环就没有可靠信号。第三类是低频任务同一类型一年遇到三五次经验库还没来得及积累就过时了。注意判断一个任务适不适合我有个简单的自测方法——找两个熟悉业务的人各自独立写一份“做好这个任务的要点清单”如果两份清单有大面积重合说明这个方法有明确的优化空间如果两份清单各说各的说明任务本身的标准就是模糊的套这套框架大概率白花钱。5. 踩坑实录与排查速查表5.1 五个最典型的故障故障一跑了几十轮成功率纹丝不动。最可能的原因是打分器没有区分度。排查方法很直接把每轮的轨迹分数打出来看分布如果所有分数都挤在 0.7 到 0.8 之间说明打分器根本没在区分好坏。这时候要做的是给硬指标加区分度比如把“格式合法”这种二值判断拆成“必填字段缺失几个”“格式错误有几处”让分数真正拉开差距。故障二库里规则越来越多效果却越来越差。这是典型的检索污染。诊断方法是做一次消融分别只注入排名第 1 的规则、只注入第 1 到 3 的规则、全部注入看三组效果。如果只有第 1 条好使说明后面的全是干扰项问题出在检索排序上而不是规则质量上。修法是收紧top_k并且提高任务签名的匹配阈值。故障三规则写得很漂亮模型就是不照做。问题多半出在规则的表述形式上。分析性表述比如“注意到在某些情况下应该考虑……”模型基本会忽略只有祈使句加具体条件的表述才会被执行比如“当输入包含日期时先统一成 YYYY-MM-DD 再处理”。蒸馏环节的提示词里必须强制要求输出祈使句必要时加一步格式校验不符合的直接打回。故障四成本远超预期一天烧掉几十美元。按我的经验八成是蒸馏次数失控。加上分差门槛之后再看看如果还高就检查是不是每轮都在对同一批任务重复蒸馏。另一个常见浪费是给模型评判的提示词写得太长把整个上下文原样贴进去评判任务的输入量能占到总 token 的一半以上。评判时只给必要的片段别偷懒。故障五跨任务迁移时全线崩盘。一换任务类型之前积累的规则全部失效。这不一定是方法的问题很可能是任务签名设计得太粗。检查方法是在库里按task_type分组统计如果某一组里塞了几十条明显不属于同一类的规则说明分类粒度不够需要把类型标签拆细。5.2 排查速查表现象最可能原因优先检查项处理动作分数无区分度硬指