
1. 背景与核心概念从“会写代码”到“竞赛夺金”先问一个问题我们平时说某个大语言模型“会写代码”和它在编程竞赛中拿到金牌之间到底差了多少答案是差距非常大。日常开发场景中我们让模型写一个接口、生成一段 SQL、补全一个函数这类任务的特点是逻辑相对固定、上下文信息充足、允许试错和反复修改。但编程竞赛是另一套游戏规则。以 Codeforces、AtCoder 为代表的竞赛平台题目通常需要在限定时间内理解题意、设计算法、分析复杂度、处理边界条件然后提交一份能在严格时间和内存限制内通过全部测试用例的代码。这里没有“差不多能用”的说法要么通过要么超时或答案错误结果非常二元。我在实际项目里观察到一个很明显的现象一个在常规代码生成评测集上表现不错的模型放到竞赛题目上往往分数惨淡。这背后其实是训练目标和评估目标不一致的问题。常规代码生成任务考的是“模型能不能写出一段像样的代码”而竞赛任务考的是“模型能不能像一个训练有素的选手一样在有限信息下做出正确的算法决策”。Post-Training后训练正是为了解决这个 gap 而存在的。后训练不是一个新概念它泛指在预训练完成之后对模型进行的一切额外训练阶段包括监督微调SFT、指令微调Instruction Tuning、人类反馈对齐RLHF、以及更偏推理增强的强化学习训练。这篇文章要拆解的就是如何通过 Post-Training 手段把一个大语言模型从“会写代码”逐步训练到“能在编程竞赛中拿到金牌水平”。为了便于理解我们可以把整个过程拆成四个层次预训练模型学习了海量代码文本理解了语法和基础语义。监督微调模型学会跟随指令具备代码生成的基本能力。推理增强训练模型学会在复杂问题上进行多步推理而不是“一次性猜答案”。竞赛专项训练模型针对竞赛题型、判题规则、时间限制进行强化。不少公开研究已经验证了这条路线。比如一些团队用竞赛题目的测试结果作为奖励信号对模型进行强化学习训练让模型在生成代码之后获得“通过/不通过”的反馈从而逐步优化自己的解题策略。这跟我们人类选手刷题提升水平的过程非常相似——先做大量题目再根据提交结果调整思路。本文接下来的内容会围绕 Post-Training 这条主线展开依次讲解为什么竞赛场景对模型提出更高要求、后训练的整体架构、核心训练方法拆解、评估与调优思路、以及工程落地时的常见问题和最佳实践。不管你是做大模型应用开发、算法工程还是对推理模型感兴趣的研究者这篇文章都能给你一条从原理到实践的比较完整的参考路径。2. 环境准备与版本说明在进入具体方法之前先来明确一下技术栈。Post-Training 一套完整的实验链路通常涉及以下几个部分基础模型可以选择开源模型作为底座例如 Llama 系列、Qwen 系列、DeepSeek 系列或者其他支持商用和研究的模型。训练框架PyTorch 是目前最主流的深度学习框架分布式训练可以配合 DeepSpeed 或 Megatron-LM。微调工具LLaMA-Factory、Axolotl、TRL 都是社区常用的微调工具它们封装了 SFT、LoRA、DPO、PPO 等训练流程。强化学习组件如果要做基于代码执行结果的强化学习需要自己搭建一个“生成代码 - 运行测试 - 计算奖励”的闭环环境。评测工具Codeforces、AtCoder 的公开数据集以及 HumanEval、MBPP 等经典基准。需要说明的是以下示例代码展示的是训练流程的核心思路具体版本号和 API 需要根据你实际使用的框架和模型进行调整不要盲目照搬。尤其是大模型领域更新非常快几个月前的参数写法可能已经变了。# 建议的 Python 环境 python3.10 torch2.1.0 transformers4.40.0 datasets2.18.0 deepspeed0.14.0如果你本机资源有限可以考虑以下替代方案使用 Google Colab 或 Kaggle Notebook 跑小规模实验。使用 LoRA、QLoRA 等参数高效微调方法大幅降低显存需求。先在小规模数据集上验证流程再扩展到完整训练。一个典型的后训练项目目录结构如下post-training-code/ ├── config/ │ ├── sft_config.yaml │ └── rl_config.yaml ├── data/ │ ├── train_sft.jsonl │ ├── train_rl.jsonl │ └── eval_set.jsonl ├── scripts/ │ ├── run_sft.py │ ├── run_rl.py │ └── evaluate.py ├── src/ │ ├── dataset.py │ ├── reward.py │ └── models.py └── models/ ├── base_model/ └── sft_model/在实际操作中我建议先把整个流程跑通一个小规模版本再逐步放大数据量和模型规模。这样能尽快发现管道中的问题避免资源浪费。3. 核心训练方法拆解Post-Training 竞赛模型的核心环节可以分成三个部分指令微调让模型学会“解题格式”代码执行反馈训练让模型学会“如何拿到更高分”推理能力增强让模型学会“复杂问题的拆解”。下面逐个拆开讲。3.1 监督微调先让模型学会竞赛题目的“套路”竞赛解题和我们平时让模型写代码有一个很大的不同竞赛题目需要模型先理解问题再设计算法最后写出代码。如果直接让模型输出最终代码它往往会跳过算法设计步骤直接凭记忆拼凑这在面对新题目时表现很差。解决办法是在 SFT 阶段为模型提供结构化的思维链数据。每道训练样本不仅包含最终代码还包含中间步骤问题理解、算法选择、复杂度分析、代码实现。一个典型的训练样本格式如下{ instruction: 给定一个整数数组找出其中第 k 大的元素。要求时间复杂度 O(n)。, reasoning: 这是一个 Top-K 问题。可以使用快速选择算法平均时间复杂度 O(n)最坏 O(n^2)。也可以使用堆但时间复杂度 O(n log k)。题目要求 O(n)因此选择快速选择。, response: python\ndef find_kth_largest(nums, k):\n def partition(left, right):\n pivot nums[right]\n i left\n for j in range(left, right):\n if nums[j] pivot:\n nums[i], nums[j] nums[j], nums[i]\n i 1\n nums[i], nums[right] nums[right], nums[i]\n return i\n \n left, right 0, len(nums) - 1\n while True:\n pos partition(left, right)\n if pos k - 1:\n return nums[pos]\n elif pos k - 1:\n left pos 1\n else:\n right pos - 1\n }这一阶段的目标不是让模型记住题目和答案而是让它学会“看到题目 - 进行推理 - 生成代码”的模式。数据质量比数据数量更重要。如果思维链写得混乱或错误模型会学到错误的推理方式。3.2 基于代码执行结果的强化学习让模型从“对错”中学习SFT 只能让模型模仿训练数据中的行为但无法让它超越数据。真正让模型在竞赛场景中持续提升的是基于执行结果的强化学习。核心思想是这样的让模型针对一道题目生成多份答案然后在真实的测试用例上运行这些答案根据通过测试用例的比例计算奖励再用强化学习算法更新模型参数。流程可以简化为采样给定一道竞赛题目让当前模型生成 N 份候选代码。执行把每份代码放进沙箱运行题目对应的测试用例。打分通过的测试用例数越多奖励越高。更新使用策略梯度算法如 PPO、REINFORCE根据奖励更新模型参数。这种方法和人类选手的成长路径非常一致。选手刷题时提交代码后会收到“通过”或“错误”的反馈然后根据反馈调整自己的解题策略。模型也一样只不过它调整的不是“思路”而是“参数”。一个简化的奖励函数思路如下def compute_reward(code: str, test_cases: list) - float: 根据代码在测试用例上的表现计算奖励。 test_cases: [(input, expected_output), ...] passed 0 for case_input, case_output in test_cases: try: result run_code_in_sandbox(code, case_input) if result case_output: passed 1 except Exception: # 编译错误、超时、运行错误都算未通过 continue return passed / len(test_cases)这里有几个关键点需要注意沙箱隔离是必须的。模型生成的代码是任意程序必须限制系统调用、网络访问、文件读写和资源消耗否则可能带来安全问题。奖励信号要足够区分度。如果一道题大部分候选代码都得 0 分模型很难学到有效信息。这时可以引入部分奖励比如“通过前几个样例得一半分”。测试用例的覆盖度很重要。训练时用的测试用例应该能反映题目的各种边界情况否则模型可能在训练集上过拟合。3.3 推理增强从单次生成到多轮验证强化学习解决了“模型如何根据反馈改进”的问题但还有一个隐藏瓶颈模型在生成答案时是一次性输出遇到复杂问题时容易在中途出错。一个有效的改进方向是让模型在生成过程中进行“自我验证”。具体来说模型生成候选代码后不直接提交而是先让它自己模拟运行几个简单样例检查输出是否符合预期如果发现错误则重新生成。这个过程在工程上可以做成多轮采样 过滤第一轮模型生成初始答案。验证用题目自带的样例测试初始答案。修正如果样例未通过模型读取错误信息生成修正版本。重复最多尝试 K 次取表现最好的版本作为最终提交。这种“生成-验证-修正”的循环被很多竞赛模型采用效果提升非常明显。因为大语言模型生成代码时错误往往不是整体性的而是局部的逻辑疏漏多一轮修正可以显著提升通过率。4. 完整实战案例从零搭建竞赛模型后训练管线下面我们从头搭建一个最小可用的竞赛模型后训练管线。为了控制篇幅和可运行性这里以一个小规模示例演示流程生产环境需要按实际数据量和算力调整。4.1 数据准备与格式设计首先准备两类数据SFT 训练数据和强化学习训练数据。为了快速验证这里只构造几条示例数据。mkdir -p post-training-code/{data,scripts,src,config,models}创建 SFT 训练数据data/train_sft.jsonl{instruction: 给定一个正整数 n输出斐波那契数列的第 n 项。, reasoning: 斐波那契数列可以用递推法时间复杂度 O(n)。当 n 较大时可以使用矩阵快速幂但这里 n 的范围较小直接递推即可。, response: python\ndef fib(n):\n a, b 0, 1\n for _ in range(n):\n a, b b, a b\n return a\n} {instruction: 给定一个只包含小写字母的字符串判断它是否是回文串。, reasoning: 回文串的判定可以双指针从两端向中间扫描时间复杂度 O(n)空间复杂度 O(1)。, response: python\ndef is_palindrome(s):\n left, right 0, len(s) - 1\n while left right:\n if s[left] ! s[right]:\n return False\n left 1\n right - 1\n return True\n}创建强化学习训练数据data/train_rl.jsonl。每条数据包含题目描述和测试用例{problem: 给定一个整数数组 nums 和一个目标值 target返回两个数的下标使它们相加等于 target。, test_cases: [{input: [2,7,11,15], 9, expected: [0,1]}, {input: [3,2,4], 6, expected: [1,2]}, {input: [3,3], 6, expected: [0,1]}]}4.2 SFT 阶段编写训练脚本这里使用 Hugging Face Transformers 和 TRL 库进行 SFT 训练。为了适配不同环境代码采用尽量通用的写法。# 文件路径scripts/run_sft.py import json from datasets import Dataset from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments from trl import SFTTrainer # 1. 加载数据 def load_data(path): samples [] with open(path, r, encodingutf-8) as f: for line in f: sample json.loads(line) text f### 题目{sample[instruction]}\n\n### 思路{sample[reasoning]}\n\n### 代码\n{sample[response]} samples.append({text: text}) return Dataset.from_list(samples) dataset load_data(../data/train_sft.jsonl) # 2. 加载模型和分词器 # 生产环境请按实际模型填写这里以 Qwen2.5-1.5B-Instruct 为例 model_name Qwen/Qwen2.5-1.5B-Instruct tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name) # 3. 配置训练参数 training_args TrainingArguments( output_dir../models/sft_model, per_device_train_batch_size2, gradient_accumulation_steps8, learning_rate2e-5, num_train_epochs3, logging_steps10, save_steps500, fp16True, max_seq_length2048, ) # 4. 使用 SFTTrainer trainer SFTTrainer( modelmodel, argstraining_args, train_datasetdataset, tokenizertokenizer, dataset_text_fieldtext, max_seq_length2048, ) trainer.train() trainer.save_model(../models/sft_model)需要注意SFTTrainer的dataset_text_field参数在不同版本的 TRL 中可能存在差异请以你实际安装版本的文档为准。如果遇到字段名不一致的报错可以查看对应版本的 API 说明。4.3 强化学习阶段环境与奖励设计接下来是实现强化学习训练。这里以 REINFORCE 算法为例因为它实现简单不需要维护额外的 critic 模型适合作为演示。实际生产环境中 PPO 通常更稳定。# 文件路径scripts/run_rl.py import json import subprocess import tempfile import os import torch from transformers import AutoModelForCausalLM, AutoTokenizer # 1. 加载 SFT 后的模型 model_name ../models/sft_model tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name) model.train() # 2. 沙箱执行代码并返回测试结果 def run_code_in_sandbox(code: str, input_str: str, timeout: int 5): 将代码写入临时文件并执行传入 input_str 作为标准输入。 with tempfile.TemporaryDirectory() as tmpdir: code_path os.path.join(tmpdir, solution.py) with open(code_path, w, encodingutf-8) as f: f.write(code) try: result subprocess.run( [python, code_path], inputinput_str, capture_outputTrue, textTrue, timeouttimeout, ) return result.stdout.strip() except subprocess.TimeoutExpired: return TIME_LIMIT_EXCEEDED except Exception as e: return fERROR: {str(e)} # 3. 计算奖励 def compute_reward(code: str, test_cases: list) - float: passed 0 for case in test_cases: input_str case[input] expected case[expected] result run_code_in_sandbox(code, input_str) if result expected: passed 1 return passed / len(test_cases) # 4. 组织强化学习训练数据 def load_rl_data(path): samples [] with open(path, r, encodingutf-8) as f: for line in f: samples.append(json.loads(line)) return samples # 5. REINFORCE 训练循环 # 核心思路生成多个样本 - 根据奖励计算损失 - 反向传播更新 # 注意这里只展示正向传播和损失计算的核心思路完整训练需要配合优化器和分布式框架 rl_data load_rl_data(../data/train_rl.jsonl) optimizer torch.optim.AdamW(model.parameters(), lr1e-6) for step, sample in enumerate(rl_data): problem sample[problem] test_cases sample[test_cases] # 生成候选代码 prompt f### 题目{problem}\n\n### 代码 inputs tokenizer(prompt, return_tensorspt).to(model.device) # 在训练模式下生成注意设置 pad_token with torch.no_grad(): outputs tokenizer.batch_decode( model.generate( **inputs, max_new_tokens512, do_sampleTrue, top_p0.95, num_return_sequences4, ), skip_special_tokensTrue, ) # 提取代码部分并计算奖励 rewards [] for output in outputs: code output.split(### 代码)[-1].replace(python, ).replace(, ).strip() reward compute_reward(code, test_cases) rewards.append(reward) # 选择奖励最高的样本作为“正样本”计算语言建模损失 best_idx rewards.index(max(rewards)) best_output outputs[best_idx] best_reward rewards[best_idx] # 这里使用简单的加权损失奖励越高梯度更新幅度越大 full_sequence prompt best_output encodings tokenizer(full_sequence, return_tensorspt).to(model.device) logits model(**encodings).logits labels encodings[input_ids][:, 1:].contiguous() logits logits[:, :-1, :].contiguous() loss torch.nn.functional.cross_entropy( logits.view(-1, logits.size(-1)), labels.view(-1), ignore_indextokenizer.pad_token_id, ) # 用奖励作为权重奖励越高这个样本的 loss 权重越大 weighted_loss loss * (0.1 best_reward) weighted_loss.backward() optimizer.step() optimizer.zero_grad() if (step 1) % 10 0: print(fStep {step 1}, Avg Reward: {sum(rewards) / len(rewards):.3f}, Loss: {weighted_loss.item():.3f}) model.save_pretrained(../models/rl_model)这里要特别说明上面代码是一个简化版的教学实现重点展示强化学习训练循环的骨架。实际生产环境中你不会直接这样写而是会使用更成熟的分布式训练框架并引入 PPO、GRPO 等更稳定的算法。但核心思想是一致的让模型在大量题目的执行反馈中不断调整自己的生成策略。4.4 推理阶段生成-验证-修正循环训练完成后的推理阶段不要直接让模型一次性生成答案。推荐使用“生成-验证-修正”的循环策略。# 文件路径scripts/inference.py from transformers import AutoModelForCausalLM, AutoTokenizer model_name ../models/rl_model tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name) model.eval() def solve_problem_with_retry(problem: str, sample_inputs: list, max_retries: int 3): 生成代码并用自带样例验证。失败则让模型修正。 sample_inputs: 题目给出的样例格式为 [(输入, 期望输出), ...] for attempt in range(max_retries): prompt f### 题目{problem}\n\n### 代码 inputs tokenizer(prompt, return_tensorspt).to(model.device) outputs tokenizer.batch_decode( model.generate( **inputs, max_new_tokens512, do_sampleTrue, top_p0.95, num_return_sequences1, ), skip_special_tokensTrue, ) code outputs[0].split(### 代码)[-1].strip() # 用样例验证 all_passed True for case_input, case_expected in sample_inputs: result run_code_in_sandbox(code, case_input) if result ! case_expected: all_passed False # 把错误信息返回给模型让它修正 problem f{problem}\n\n你之前的代码运行结果如下\n输入{case_input}\n期望输出{case_expected}\n实际输出{result}\n请检查代码并重新生成。 break if all_passed: return code return code # 重试次数用完后返回最后一版代码这种推理策略在竞赛场景中非常实用因为它充分利用了“测试用例”这个宝贵信号。竞赛题目自带的样例往往覆盖了最基本的正确性判断只要样例能通过正确率就会大幅提高。4.5 运行与预期结果依次执行以下命令即可跑通整个流程cd post-training-code/scripts python run_sft.py python run_rl.py python inference.py在一台具备 24GB 显存的消费级 GPU 上用 1.5B 参数模型和少量示例数据整个流程可以在几小时内跑完。你会看到强化学习阶段的平均奖励随训练步数逐步上升这说明模型确实在从“执行结果反馈”中学习。5. 常见问题与排查思路在实际操作中我遇到过不少问题这里整理成排查清单供参考。问题现象常见原因解决思路SFT 训练时 loss 不下降学习率过高数据格式不一致分词器未设置 pad token调低学习率统一数据格式设置tokenizer.pad_token tokenizer.eos_token生成的代码全是注释没有逻辑SFT 数据质量差模型没有学到代码模式检查训练数据中的代码是否完整增加高质量竞赛题数据强化学习训练不收敛奖励设计过于稀疏采样数量太少学习率过高增加采样数量设计更细粒度的部分奖励调低学习率模型在训练集表现好但新题表现差过拟合训练数据分布增加题目多样性加入更多未见过的题目使用正则化手段代码执行超时导致训练中断沙箱超时设置过短模型生成了死循环增加超时时间在奖励函数中对超时代码给予低分考虑在提示中强调时间限制训练显存不足模型较大批量大小过大使用 LoRA/QLoRA减小批量大小使用梯度累积开启梯度检查点沙箱中出现模型代码执行危险操作未做安全隔离使用 Docker 或 gVisor 之类的强隔离容器限制网络、文件系统、系统调用这里重点展开两个高频问题。第一个训练数据质量问题。很多时候模型表现上不去不是算法问题而是训练数据本身就是脏的。比如你从网上爬取的竞赛题解可能包含大量错误代码或者缺少题目描述和测试用例。这类数据如果直接进训练集会严重干扰模型学习。建议每一条训练数据都至少经过一轮自动化验证代码能在题目自带的测试用例上通过思维链和代码逻辑保持一致。宁可少一些数据也不要让脏数据污染模型。第二个奖励信号设计问题。如果奖励只有“通过/不通过”两种取值模型在很多难题上会得到全零奖励梯度信号非常稀疏。这时可以引入层次化奖励编译通过0.2通过题目自带的样例0.3通过隐藏测试用例0.5这样模型即使不能满分也能从“部分正确”的行为中学到东西。6. 最佳实践与工程建议6.1 数据层面构建高质量竞赛训练集数据是 Post-Training 的地基。从竞赛平台收集数据时建议注意以下几点按难度分层。入门题、中等题、困难题按比例混合初期以中等题为主难度太高会导致奖励信号稀疏。覆盖常用算法类别。贪心、动态规划、图论、数论、字符串、数据结构每个类别都要有题目。验证代码正确性。所有训练样本中的参考代码必须通过测试用例否则会教坏模型。控制重复度。同一道题多次出现会导致过拟合需要做去重。6.2 训练层面分阶段推进建议按照以下路径逐步推进先用小模型1B-3B在小数据集上跑通全流程。验证训练管道的稳定性后再切换到更大模型和更完整的数据集。SFT 阶段重点提升推理能力RL 阶段重点提升“从反馈中学习”的能力。每个阶段结束都保存 checkpoint方便回滚对比。6.3 安全层面沙箱隔离是红线竞赛代码训练涉及大量模型生成的任意代码如果这些代码在训练服务器上直接运行安全风险极高。必须把代码执行放在隔离环境中。优先使用 Docker 容器限制 CPU 和内存资源禁止网络访问。设置严格的超时时间防止死循环拖垮训练进程。对生成的代码进行静态扫描过滤明显的危险操作如文件删除、系统调用。记录所有执行日志便于问题回溯。6.4 评估层面不要只盯着一个指标竞赛模型的评估不能只看单道题的通过率。建议建立多维度的评估体系整体通过率在所有题目中能通过全部测试用例的比例。分难度通过率入门、中等、困难题分别统计观察模型的能力边界。样本效率为了通过一道题模型平均需要生成多少次。修正能力在第一次错误后模型能否根据反馈修正代码。这些指标可以帮助你定位模型的具体短板。比如如果分难度通过率显示困难题远低于中等题说明模型的推理能力还不足应该加强思维链数据的比例。6.5 生产环境部署注意事项如果你想把训练好的模型部署到线上服务还需要注意以下工程问题推理延迟每次生成多份候选代码会增加响应时间需要权衡质量与延迟。并发控制代码执行服务需要接口限流防止恶意调用浪费资源。模型版本管理使用模型注册表管理不同训练阶段的模型方便线上快速回滚。监控告警监控生成代码的通过率、执行耗时、沙箱异常率及时发现模型退化或环境问题。7. 总结与下一步建议这篇文章围绕“Post-Training Language Models for Gold-Medal Performance in Coding Competitions”这条主线拆解了大语言模型从“基础代码生成”到“竞赛夺金”的核心训练思路。我们回顾一下关键要点第一常规代码生成和竞赛解题之间存在显著差距需要通过后训练来弥补。第二Post-Training 的核心链路是SFT 让模型学会结构化推理强化学习让模型从代码执行反馈中学习生成-验证-修正循环让模型在推理阶段具备自我纠错能力。第三数据质量、奖励设计、沙箱安全是工程落地的三大关键点。任何一个环节出问题整个训练管线都会受影响。对于想深入这个方向的读者我建议从以下几个方面继续学习算法层面深入理解 PPO、GRPO 等强化学习算法的原理和实现它们比本文演示的 REINFORCE 更稳定。数据层面研究如何自动化地从竞赛平台构建高质量训练集包括题解验证、难度标注和去重。工程层面学习大规模分布式训练框架掌握 LoRA/QLoRA 等参数高效微调技术。前沿方向关注推理时计算inference-time compute的进展比如让模型在生成答案时进行更长时间的多步推理和验证这正在成为提升模型推理能力的重要方向。最后给准备动手实践的读者一个建议不要一开始就追求“金牌模型”先把一条最小可行的训练管线跑通用一个小模型验证每个环节的正确性再逐步增加数据量和模型规模。竞赛模型的提升是一个渐进的过程更像选手日复一日地刷题和复盘而不是一次训练就能一蹴而就。如果本文对你有帮助可以收藏备用。后续我也会在模型推理增强和竞赛专项训练方向继续更新欢迎在评论区交流你的训练经验和遇到的问题。