AWS 官方详解如何在 Nova Forge 中设计复合多轮奖励函数、安全执行模型生成的代码,并给出各组件的常见陷阱及排查方法。
在多轮强化学习(RL)中,自定义奖励函数决定了模型实际学到什么。一个细微错误的奖励可能在所有训练曲线看起来都健康的情况下,悄悄地教会错误的东西。为多轮、代理型任务设计一个能经受考验的奖励,是定制 Amazon Nova 模型最难的部分之一。对于多轮训练,Amazon Nova Forge 通过其自建编排(Bring Your Own Orchestration,BYOO)能力,在你自己的环境中运行你的奖励逻辑。你可以专注于定义什么是好的结果,而由 Nova Forge 协调跨轮次的 rollout、消息传递和对话状态。Nova Forge 还提供无服务器多轮 RL 选项(现已正式发布),供不想管理该环境的团队使用。本文使用 BYOO 路径。
Amazon Nova 提供多种定制方法,强化微调(Reinforcement Fine-Tuning,RFT)因其可以通过迭代反馈教会模型你想要的行为而脱颖而出。RFT 与监督微调(Supervised Fine-Tuning,SFT)采用不同方法。它不需要带有标注推理路径的精选示例,而是从模型自身输出的评估信号中学习。多轮 RFT 将其扩展到跨一系列步骤行动的代理,例如调用工具、执行代码或从错误中恢复。它优化整个轨迹的累积奖励,而不是对单个响应打分。RFT 的核心是奖励函数:指导模型的打分机制,也是你需要设计的部分。

图 1 — 从共享检查点等计算量的后训练后的分布外(OOD)性能。RL 在所有任务变体中改善了 OOD 泛化能力,而 SFT 则有所下降。改编自 Chu et al.,2025
本文专注于奖励函数本身:如何设计一个 GRPO 可以学习的复合多轮奖励。本文还展示了如何在奖励内安全地执行模型生成的代码,以及为什么需要对每个组件进行检测,以便你可以信任训练正在学习什么。本系列第 1 部分涵盖了 Amazon SageMaker HyperPod 和 Nova Forge 基础设施。还涵盖了运行这些奖励的训练配置。最后,我们从一次真实运行中提取了可能悄然导致奖励崩溃的陷阱,其中权重最高的组件实际上静默地没有贡献任何学习信号。我们会展示如何发现它们。全文代码仅作说明使用。将其作为你自己奖励实现的起点。
要跟随本文内容,你需要:
自定义奖励环境是可选加入的:在 cdk.json 中,将 use_custom_env 设置为"true",并将 custom_env_id 设置为你的环境 ID(例如"my-custom-env"),然后再部署。默认情况下栈使用内置的 wordle 环境。
熟悉强化微调和 GRPO。
使用 Amazon Nova Forge 构建自定义奖励
RFT 的工作原理是从当前模型中采样补全,并用奖励函数对其进行打分。在 Nova Forge 中,奖励函数是你用代码编写的评分器,而不是单独训练的奖励模型。它可以是一个基于规则的检查,验证输出(使用可验证奖励的强化学习),也可以调用另一个大型语言模型(LLM)来评判响应,这种方法称为 LLM-as-Judge。
然后 RFT 调整模型权重,使更高奖励的补全更有可能出现。Nova Forge 使用 GRPO。对于每个对话,GRPO 使用奖励函数对 K 个模型 rollout 进行排名。GRPO 使用排名最高的模型补全,根据批次的归一化奖励(优势)来更新模型。使用 GRPO 的 RFT 是一项基本技术,在初始 SFT 基础上实现了显著的性能提升。
奖励信号只有通过其在组内产生的变异才能影响学习。如果一个项在组内每个补全中取值相同,它对优势没有任何贡献。因此它对梯度也没有任何贡献。
你的奖励函数如何在 Nova Forge 上运行取决于任务。对于单轮 RFT,你将奖励注册为 AWS Lambda 函数,并通过 reward_lambda_arn 指向它。多轮任务(如本文中的任务)超出了单个 Lambda 调用支持的范围。多轮对话和长时间运行的评分会超过 15 分钟的 Lambda 调用限制。对于这些情况,Nova Forge 使用 BYOO。你设置 rollout.delegate: true,并在环境容器中运行你的环境和奖励逻辑,例如在 Amazon ECS 上。Nova Forge 将每个 rollout 委托给你的环境。然后它收集完成的片段用于训练。你的容器管理多轮交互和对话状态:它运行用户模拟器、执行代码并调用验证器。然后它返回每个样本的聚合奖励(aggregate_reward_score),以及一个可选的按组件得分列表(metrics_list)。本系列第 1 部分涵盖了此基础设施及其 AWS Cloud Development Kit(AWS CDK)部署。本文专注于奖励本身。
奖励评估如何工作
训练任务从 Nova 模型为每个提示生成候选 rollout。在多轮任务中,一个 rollout 是一个完整的片段,包含一系列轮次(一个轨迹),而不是单个响应。你的奖励函数接收每个 rollout 并执行三个步骤:

图 2 — 单次多轮 rollout:Nova Forge 委托给你的环境容器,该容器询问模拟器或运行已提交代码,然后为 GRPO 返回奖励分数
这个循环在许多训练步骤中重复,逐步塑造模型以最大化整个序列的累积奖励。模型优化你所奖励的任何东西,正如我们展示的,这并不总是你认为自己写的东西。
选择多轮奖励的结构
单一标量奖励很容易被操纵,而单一终端奖励在多轮任务中往往太稀疏而无法学习。因此,大多数生产级多轮奖励结合三种信号:结果奖励、行为奖励和惩罚。
片段级(结果)奖励捕获最终产物是否满足了目标。例如,单元测试是否通过,或者工作流是否完成。它们针对你最终关心的东西,但往往稀疏且在训练早期接近于零。
轮次级(行为)奖励捕获模型是否表现出你想要的中间行为,例如在行动前先询问、调用正确的工具或避免循环。它们最适合塑造结果奖励太稀疏而无法教授的行为,但如果设计不当,即使没有真正的进展也可以获得。惩罚明确阻止失败模式,例如猜测、重复或停滞。它们区分好的和坏的策略,以便优化器看到梯度。
将这些结合在一起,使模型既能学习行为也能学习结果,而不会让一个组件掩盖或饿死另一个组件。本文的其余部分将其具体化。我们为真实任务设计了一个四组件奖励,并在其中安全地执行模型生成的代码。然后我们走过可能崩溃这种奖励的陷阱以及如何修复它们。
实战示例:教 Amazon Nova Lite 2.0 在编码前先询问
我们基于 500 道独特的编程任务构建了一个多轮协作编程任务。我们在其上使用多轮 RFT 配合 GRPO 与 Low-Rank Adaptation(LoRA)对 Amazon Nova Lite 2.0 进行了训练,运行在 Amazon SageMaker HyperPod 上,并在客户托管环境容器(Nova Forge BYOO 路径)中实现奖励函数。
其机制如下:
模型收到一个简短且规格不完整的编程请求。
用户模拟器私下持有完整规格,只有当模型提问时才透露细节。
每一轮,模型要么提出澄清问题,要么提交代码。如果提问,模拟器回答,对话继续;如果提交代码,则 rollout 结束,奖励处理器执行该代码并对照隐藏的单元测试进行正确性评分。(安全地运行模型生成的代码是一个我们稍后会讨论的问题。)
设计意图是:猜测会产生错误的代码,而提问能暴露隐藏细节并导向正确的代码。"先问后写"应该被任务设计所强制。
让目标行为可以直接、独立地被奖励,并对失败模式进行显式惩罚。对于这个任务,奖励是四个分量的加权和:
两个原则驱动了这个设计。第一,解除对你想要的行为的门槛:asked_before_coding 独立计分,不与正确性挂钩,但它确实要求模型最终提交代码,从而关闭了"一直问下去、永远不回答"的漏洞。第二,显式惩罚失败模式:guessed_immediately 使猜测严格劣于提问,从而在 GRPO 组内保持策略之间的多样性——这是算法产生梯度所依赖的多样性。
在环境容器的奖励处理器中调用这些分量评分器,并通过 metrics_list 报告各个值:
def asked_before_coding(completion, answer, **kw) -> float:
msgs = _messages(completion, kw)
first_q = _first_question_turn(msgs, parser)
final = _final_code(completion, parser)
committed = bool(final) and not _is_question(final)
if first_q == 1 and committed:
return 1.0 # asked first, then committed (ideal)
if first_q is not None and committed:
return 0.6 # asked later, then committed
return 0.0 # never asked, or asked but never committed
def guessed_immediately(completion, answer, **kw) -> float:
for m in _assistant_turns(completion, kw):
code = _code_of(parser.parse(m["content"]))
return -1.0 if (code and not _is_question(code)) else 0.0
return 0.0
安全地执行模型生成的代码
正确性分量运行模型生成的代码对照单元测试。RL 下的模型输出经过探索优化,因此应将其视为未经验证的。容器运行在独立的隔离执行环境中,但你仍应采取预防措施。不要向生成的代码暴露凭据或网络。应用资源限制并在临时目录中运行。使用每次运行独立的随机哨兵值,使模型无法通过向 stderr 写入预期的标记来伪造结果。对于需要额外隔离的执行,调用专用沙箱。这个测试框架展示了这一模式:
import resource, secrets, subprocess, sys, tempfile
from pathlib import Path
def run_tests(code: str, test: str, timeout_s: int = 30) -> float:
nonce = secrets.token_hex(8) # unforgeable per-run marker
harness = (
"import sys, unittest, json\n"
f"{code}\n\n{test}\n\n"
'if __name__ == "__main__":\n'
" r = unittest.TextTestRunner(stream=sys.stderr, verbosity=0).run(\n"
" unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]))\n"
f" sys.stderr.write('__{nonce}__' + json.dumps("
"{'total': r.testsRun, 'passed': r.testsRun - len(r.failures) - len(r.errors)}) + '__"
f"{nonce}__')\n"
)
def _limit():
resource.setrlimit(resource.RLIMIT_CPU, (timeout_s, timeout_s))
resource.setrlimit(resource.RLIMIT_AS, (2 * 1024**3, 2 * 1024**3)) # 2 GB
resource.setrlimit(resource.RLIMIT_NPROC, (64, 64))
with tempfile.TemporaryDirectory() as cwd:
path = Path(cwd) / "h.py"
path.write_text(harness)
try:
proc = subprocess.run(
[sys.executable, str(path)], capture_output=True, text=True,
timeout=timeout_s, cwd=cwd, env={"PATH": "/usr/bin"}, # no creds, no network env
preexec_fn=_limit,
)
except Exception:
return 0.0
# parse the nonce-delimited summary and validate the test count before scoring
...
还要验证实际运行的测试数量是否与预期数量一致,这样模型就无法通过自行编写的轻松通过的测试来稀释分数。对于在生产环境中部署的奖励函数,应实现这些安全措施,而非将其视为可选项。
陷阱:什么会导致奖励崩溃,以及如何修复
多轮奖励设计有一系列公认的失败模式。奖励黑客攻击是指模型在代理指标上作弊而非达成目标。训练不稳定性是指更新发散、熵崩溃或 Kullback-Leibler(KL)项爆炸。奖励崩溃是指信号退化到组内差异消失、学习悄然停止。前两者通常会在转录文本或 loss 和 KL 曲线中显现。崩溃是最危险的:聚合奖励、loss 和完成长度曲线都可能看起来正常,而你依赖的分量却毫无贡献。本节涵盖在这个任务中造成最多时间损耗的两种崩溃失败,以及如何发现它们。
当奖励崩溃为单一策略时
这个奖励的一个早期版本将提问奖励的门槛设置在正确性之后。只有当最终代码也通过时才能获得提问奖励。它还添加了一个效率项,奖励更短的对话。训练崩溃了。模型在第一轮就收敛到猜测。平均奖励冻结了,GRPO 优势变为零。
两个设计错误导致了这一结果。第一,门槛设置在一个不可达的条件之后。在这些困难任务上正确性接近于零,因此提问奖励几乎从不触发。我们想要奖励的行为对优化器来说是不可见的。第二,效率项有一个退化的最优解。更少的轮次使其最大化,因此策略崩溃到一个单一的、不表态的轮次。每个完成结果看起来都一样,组内差异消失了,学习停止了。
修复方法就是上一节中的设计:解除对你想要的行为的门槛,并显式惩罚失败模式。有了这两者,不同的策略在组内持续产生不同的奖励,这保持了 GRPO 学习所需的方差。
静默死亡的分量
当一个奖励分量在 GRPO 组内的每个完成结果上返回相同的值时,其组内方差为零。因此,无论权重有多高,它对优势或梯度都没有贡献。仍在变化的分量使得聚合奖励、策略 loss、优势和完成长度看起来都保持健康,因此曲线永远不会暴露它。代码奖励中的一个常见原因是正确性评分器因测试工具从未执行模型的输出而在每个 rollout 上返回 0。这可能是因为入口点名称不匹配、导入失败,或者一个设置错误导致每个测试在其断言运行前就失败了。在我们的运行中,实际情况正是如此:模型的澄清问题率从大约 34% 上升到 96%。代码正确性几乎没有动,因为正确性评分器在每个 rollout 上返回的都是相同的值。
要发现死亡分量,请跟踪每个分量的组内标准差,而不是聚合奖励曲线。聚合曲线会将死亡通道隐藏在活跃通道后面。如果该离散度处于或接近零,无论权重如何,该分量都没有在训练。你可能会将 0.000 的平坦奖励均值解释为"这些任务就是很难",但平坦的组内方差是明确的。将其自动化为一个分量级别的优势方差面板,这样死亡通道就会被自动标记,而无需人工检查。
尽早用仪表化捕获这些失败
有几个习惯可以捕获这些失败,而且本可以在第一天就捕获我们的失败:
对优势的分量贡献进行仪表化,而不仅仅是奖励的分量贡献。通过 metrics_list 报告每个分量,并跟踪其均值和组内标准差。任何组内方差接近零的分量对学习都没有贡献,无论其权重如何。你可能会将 0.000 的平坦奖励均值解释为"这些任务就是很难",但平坦的组内方差是明确的。将其自动化为一个分量级别的优势方差面板,这样死亡通道就会被自动标记,而无需人工检查。
按待测组件排序阅读对话记录,而非按总奖励排序。按总奖励排序会将一个已失效的组件隐藏在正常运作的组件背后。按可疑组件排序则能立即暴露问题。
消融或恢复你声称在起作用的每个组件。如果移除某个组件后没有任何变化,说明它根本没有在起作用。如果恢复某个组件后恢复了你以为已被优化的指标,说明它原本就不在目标函数中。
针对组内方差进行设计。GRPO 从同一提示下不同回复之间的差异中学习。不可达的关卡、退化的塑造最优解以及饱和项都会压缩这种差异,导致学习停滞,即使奖励看起来正常。解除对目标行为的门控,同时惩罚失败模式,使不同策略得以区分。
警惕一种稠密奖励饿死另一种稠密奖励。一旦我们的稠密提问奖励饱和,稀疏的正确性奖励就无法再移动策略。如果某个行为塑形项占据了主导地位,你关心的结果项可能永远得不到梯度。等塑形项饱和后考虑对其降权,或者对结果项进行升权。
将模型输出视为未经验证的状态。对任何生成的代码执行进行沙箱隔离(不授予凭证、不连接网络、限制资源),并使验证器不可伪造(随机哨兵、测试数量校验)。
本文中的训练运行和环境使用了 SageMaker HyperPod 和 Amazon ECS 资源,这些资源在运行期间会产生费用。实验完成后,请按照系列文章第一部分中的拆除步骤删除 SageMaker HyperPod 集群和 Amazon ECS 环境,以停止产生主要费用。如果不再需要,请从 Amazon S3 存储桶中删除 rollout 数据和检查点。
奖励函数是 RFT 中由你设计的部分,也是细微失败藏身之处。在你的运行中,模型可能学会你训练的行为,而你关心的某个项对学习没有任何贡献,却没有任何聚合指标揭示这一点。更好的 instrumentation,而非更好的算法,解决了这个问题。测量每个组件对优势(advantage)的贡献,从待测组件的角度阅读对话记录,并消融你声称正在起作用的组件。通过 Amazon Nova Forge 的自定义奖励函数,你对奖励拥有完全的控制权,这意味着把奖励做对的责任也完全在你这边。关于使这些运行可复现的基础设施和 AWS CDK 部署,请参阅本系列文章的第一部分。
特别感谢 Mahima Chaudhary 对本文的审核和贡献。