Reasoning Core:程序化数据与Completion-Supervised提升大模型推理能力 📅 发布时间:2026/8/28 16:59:44 👁 浏览次数: 1. 背景与核心概念1.1 什么是 Reasoning Core大语言模型LLM的推理能力一直是评测和落地中最难稳定复现的部分。早期的模型依赖大量人工标注的思维链数据成本高、覆盖窄、风格不一致后续出现了蒸馏、强化学习等方法虽然效果有提升但对算力和奖励模型的要求也更高。Reasoning Core 这个名字描述的是一类以“程序化生成 补全监督”为核心的数据与训练方案它的目标是用可扩展的程序化数据生成器替代手工标注让模型在 completion-supervised 的范式下稳定提升推理能力。通俗地讲Reasoning Core 是一套“为了让模型学会按步骤推理而设计的数据核心”。它不是某一个模型名称也不是某个固定框架而是覆盖数据生成、数据清洗、训练目标设计、评估闭环的一套工程思路。它强调两件事第一数据要足够“过程化”也就是每一步推理都有清晰中间结果第二训练方式要足够简单直接对标准答案的补全部分做监督而不是依赖复杂的强化学习流程。1.2 为什么需要程序化推理数据当前推理模型的训练有一个典型矛盾手动标注的思维链数据质量高但规模上不去自动生成的推理路径规模大但错误率和噪声也高。程序化数据Procedural Data就是为了解决这个矛盾。所谓程序化数据是指由代码脚本按照一定规则自动生成的训练样本。这类数据有四个显著特点可扩展只要生成器写得足够好数据规模可以从几千条扩展到几百万条。可验证每一步中间结果都有明确的规则约束错误可以被自动发现。可控难度可以通过参数调节问题复杂度形成从简单到困难的递进序列。可审计每一条样本的生成过程都可以追溯不像纯大模型蒸馏数据那样是黑盒。把程序化数据用于推理训练最大的收益是模型可以“看见”完整且正确的中间推导过程从而学到可迁移的推理模式而不是只记住表层的问题-答案映射。1.3 Completion-Supervised 训练范式Completion-Supervised 是相对于 RLAIF基于 AI 反馈的强化学习和全量 SFT监督微调的一种中间范式。在标准 SFT 中模型需要对整个 prompt response 计算 loss。在 Completion-Supervised 中训练时只对“补全部分”计算损失prompt 部分或者固定的中间思考模板部分不参与梯度更新。这样做的原因在于推理过程往往包含固定模板例如“Let’s think step by step”模板本身不是学习重点。只监督中间推导步可以避免模型把注意力浪费在复述问题上。减少无关 token 对梯度方向的干扰提高训练信号密度。Reasoning Core 的核心主张就是把程序化生成的数据配合 completion-supervised 的目标函数能同时获得数据规模、数据质量和训练效率三个优势。2. 环境准备与依赖2.1 实验环境说明Reasoning Core 本质上是一个数据与训练策略不依赖特定硬件型号。但为了跑通完整流程建议准备以下环境类别推荐配置说明操作系统LinuxUbuntu 20.04容器环境也可以关键在于 NVIDIA 驱动GPU单卡 24GB如 RTX 3090/4090训练小模型7B 以下够用内存64GB 以上数据生成阶段如果并行内存消耗较大Python3.10现代深度学习框架均支持PyTorch2.1本文示例基于 PyTorchTransformers4.36需要支持 completion-supervised 的 loss 屏蔽写法数据工具NumPy、Datasets用于数据生成与格式转换版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。2.2 核心工具链在工程实践中Reasoning Core 的数据管线通常由三个工具层组成数据生成层利用 Python 脚本生成程序化推理样本输出为 JSONL。数据转换层将原始 JSONL 转为 Hugging Face Dataset 格式并构建 completion mask。训练层基于 Transformers Trainer 实现 completion-supervised 的损失函数。安装基础依赖pip install torch transformers datasets numpy pandas如果你的环境支持多卡训练可以再安装pip install accelerate deepspeed3. 核心概念拆解3.1 Procedural Data 设计原则程序化数据不是简单写几个模板而是需要设计一套“数据生成引擎”。在设计时我比较推荐遵循以下五条原则。第一可分解性。每个问题必须能拆成多个独立、可验证的中间步骤。例如数学问题可以拆成设未知数、列方程、解方程、验证四个步骤。中间步骤越清晰模型越容易学到结构化推理。第二确定性验证。每一步都必须有自动校验方法。对于数学题验证方法可能是带入方程对于逻辑题验证方法可能是真值表对于代码题验证方法可能是单测。如果中间步骤无法验证这条数据就应该被过滤掉。第三难度可控。生成器需要暴露难度参数例如多位数运算时数字的位数范围几何题中图形元素的数量逻辑题中条件链条的长度。通过调节这些参数可以生成从入门到竞赛级别的数据分布。第四低噪声。程序化生成虽然比大模型蒸馏更可控但仍然可能出现边界情况。比如除法分母为零、索引越界、逻辑矛盾等。因此需要对生成结果做一轮规则校验。第五多样性。数据生成器不能只靠替换数字生成数据否则模型学到的只是“格式模仿”。需要在问题表述、推理路径、数据分布三个层面都做随机化。3.2 Completion-Supervised 训练范式拆解Completion-Supervised 的实现思路并不复杂核心是构建一个 mask。假设我们有一个训练样本结构如下[Instruction] 请计算 23 * 17 的结果。 [Reasoning Step 1] 将 23 拆分为 20 和 3。 [Reasoning Step 2] 分别计算 20 * 17 340 和 3 * 17 51。 [Reasoning Step 3] 将两个结果相加340 51 391。 [Answer] 391在训练时我们希望模型重点学习推理步骤和答案部分而指令部分更多是上下文。于是 loss mask 记为[Instruction] 请计算 23 * 17 的结果。 - 不参与 loss [Reasoning Step 1] 将 23 拆分为 20 和 3。 - 参与 loss [Reasoning Step 2] ... - 参与 loss [Answer] 391 - 参与 loss这种设计本质上是在告诉模型真正学会推导过程比记住问题更关键。实验中也经常观察到reasoning mask 做得越干净推理效果越好。3.3 数据模板与训练目标的关系这里有一个容易被忽略的细节程序化数据的模板结构会直接影响模型最终输出的格式。如果你在数据中使用[Reasoning Step]分隔每一步模型在实际推理时就倾向于按同样的格式输出。这本身是好事但如果模板过于僵硬模型在其他任务上可能无法适应自由推理。因此我建议在数据生成阶段就采用“结构化 自然语言混合”的方式。例如指令计算 23 * 17 推理先把 23 拆成 20 和 3然后分别乘 17得到 340 和 51最后相加得到 391。 答案391这样的样本既保留了中间步骤又不强制模型使用特定标签泛化性更好。在训练目标层面使用交叉熵损失即可关键是 loss 的 mask。下面我们来看完整实战。4. 完整实战构建 Reasoning Core 数据管线4.1 创建项目结构我们先建立一个清晰的项目目录方便后续扩展。reasoning-core-demo/ ├── config/ │ └── train_config.yaml ├── data/ │ ├── raw/ # 原始 JSONL │ ├── processed/ # 转换后的 Dataset │ └── eval/ # 评测集 ├── scripts/ │ ├── generate_data.py # 数据生成器 │ ├── convert_dataset.py # 数据转换 │ └── train.py # 训练脚本 ├── src/ │ ├── __init__.py │ ├── data_schema.py # 数据格式定义 │ ├── validator.py # 数据校验器 │ └── loss_mask.py # loss mask 构建 └── outputs/ └── checkpoints/4.2 编写数据生成器下面这段代码示例是一个简化版的多步算术推理数据生成器重点展示“程序化生成 步骤级验证”的思路。文件路径scripts/generate_data.pyimport json import random from typing import Dict, List def generate_multi_step_arithmetic(max_digits: int 3) - Dict: 生成一个多步算术推理样本。 设计思路 1. 随机生成三个整数保证中间结果可验证 2. 构建自然语言的推理步骤 3. 返回包含 answer 的样本。 max_val 10 ** max_digits a random.randint(10, max_val) b random.randint(10, max_val) c random.randint(10, max_val) step1_result a * b step2_result step1_result c sample { instruction: f请计算 {a} * {b} {c} 的结果。, reasoning_steps: [ f第一步先计算 {a} * {b}得到 {step1_result}。, f第二步将上一步结果与 {c} 相加即 {step1_result} {c}得到 {step2_result}。, ], answer: str(step2_result), } # 自动校验答案避免生成错误数据 assert eval(sample[instruction].replace(请计算, ).replace(的结果。, )) step2_result return sample def generate_dataset(num_samples: int, seed: int 42) - List[Dict]: random.seed(seed) samples [] for _ in range(num_samples): try: sample generate_multi_step_arithmetic() samples.append(sample) except AssertionError: # 如果校验失败跳过当前样本避免脏数据 continue return samples if __name__ __main__: dataset generate_dataset(1000) output_path data/raw/multi_step_arithmetic.jsonl with open(output_path, w, encodingutf-8) as f: for item in dataset: f.write(json.dumps(item, ensure_asciiFalse) \n) print(f生成 {len(dataset)} 条数据 - {output_path})运行方式cd reasoning-core-demo python scripts/generate_data.py预期输出生成 1000 条数据 - data/raw/multi_step_arithmetic.jsonl这里要注意eval只适合在受控脚本中做简单校验生产环境建议使用专用的数值解析器或符号计算库避免安全隐患。4.3 将数据转换为 Dataset 格式接下来我们要把 JSONL 转换成模型训练可用的格式并构建 completion-supervised 所需的 loss mask。这个环节的输入输出是输入data/raw/multi_step_arithmetic.jsonl输出data/processed/train_datasetHugging Face Dataset 目录文件路径scripts/convert_dataset.pyimport json from datasets import Dataset from transformers import AutoTokenizer def build_conversation(instruction: str, reasoning_steps, answer: str) - str: 将单条样本拼接成模型输入的文本序列。 reasoning_text .join(reasoning_steps) return f指令{instruction}\n推理{reasoning_text}\n答案{answer} def create_completion_mask(tokenizer, instruction: str, full_text: str, answer: str): 构建 completion-supervised 需要的 mask。 只在推理过程和答案部分计算 loss指令部分 mask 为 -100。 # 指令部分的 token 数 instruction_text f指令{instruction}\n推理 instruction_tokens len(tokenizer.encode(instruction_text, add_special_tokensFalse)) full_tokens tokenizer(full_text, return_tensorspt) labels full_tokens[input_ids].clone() # 将指令部分设为 -100忽略 loss labels[:, :instruction_tokens] -100 return { input_ids: full_tokens[input_ids], attention_mask: full_tokens[attention_mask], labels: labels, } def main(): tokenizer AutoTokenizer.from_pretrained(your-base-model, use_fastTrue) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token samples [] with open(data/raw/multi_step_arithmetic.jsonl, r, encodingutf-8) as f: for line in f: samples.append(json.loads(line)) dataset_dict { input_ids: [], attention_mask: [], labels: [], } for item in samples: full_text build_conversation(item[instruction], item[reasoning_steps], item[answer]) encoded create_completion_mask(tokenizer, item[instruction], full_text, item[answer]) dataset_dict[input_ids].append(encoded[input_ids][0].tolist()) dataset_dict[attention_mask].append(encoded[attention_mask][0].tolist()) dataset_dict[labels].append(encoded[labels][0].tolist()) ds Dataset.from_dict(dataset_dict) ds.save_to_disk(data/processed/train_dataset) print(Dataset 转换完成 str(len(ds)) 条) if __name__ __main__: main()这段代码的核心是create_completion_mask函数。它把指令...这个 segment 的 label 全部置为-100这样模型在计算交叉熵时就会自动忽略这些 token。4.4 编写训练脚本训练脚本可以基于 Hugging Face Trainer 实现注意需要把DataCollator中 label 的-100保留。文件路径scripts/train.pyfrom datasets import load_from_disk from transformers import ( AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments, DataCollatorForLanguageModeling, ) model_name your-base-model dataset load_from_disk(data/processed/train_dataset) tokenizer AutoTokenizer.from_pretrained(model_name, use_fastTrue) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token model AutoModelForCausalLM.from_pretrained(model_name) training_args TrainingArguments( output_diroutputs/checkpoints, per_device_train_batch_size4, gradient_accumulation_steps8, num_train_epochs3, learning_rate2e-5, logging_steps10, save_steps500, fp16True, report_tonone, ) data_collator DataCollatorForLanguageModeling( tokenizertokenizer, mlmFalse, ) trainer Trainer( modelmodel, argstraining_args, train_datasetdataset, tokenizertokenizer, data_collatordata_collator, ) if __name__ __main__: trainer.train() trainer.save_model(outputs/reasoning_core_model)这里有一个细节DataCollatorForLanguageModeling在mlmFalse时默认不会修改 label 的-100所以我们可以安全地使用它做动态 padding。如果你的代码中手动实现了 collator要注意保证-100不被替换成pad_token_id。4.5 运行验证完成上述文件后按顺序执行python scripts/generate_data.py python scripts/convert_dataset.py python scripts/train.py如果一切正常你会在outputs/reasoning_core_model目录下看到训练好的模型权重。训练完成后可以写一个简单的推理脚本验证效果。from transformers import AutoModelForCausalLM, AutoTokenizer model_path outputs/reasoning_core_model tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForCausalLM.from_pretrained(model_path) prompt 指令请计算 45 * 12 37 的结果。\n推理 inputs tokenizer(prompt, return_tensorspt) outputs model.generate(**inputs, max_new_tokens128, do_sampleFalse) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))预期输出应该包含一步步的推理过程和最终答案例如指令请计算 45 * 12 37 的结果。 推理第一步先计算 45 * 12得到 540。第二步将上一步结果与 37 相加即 540 37得到 577。 答案5775. 常见问题与排查思路在实践 Reasoning Core 时大家遇到最多的问题集中在数据质量、mask 构建和训练收敛三个方面。我整理了一张排查表供你按图索骥。问题现象常见原因解决思路训练 loss 一直不下降completion mask 没有生效指令部分也在参与 loss检查 labels 中指令部分是否为 -100模型输出格式混乱没有中间推理步数据模板不统一模型学不到固定推理格式统一模板至少保证同一批数据使用一致的 step 分隔符生成数据通过校验但逻辑错误校验逻辑只查了最终答案没有检查中间步骤对每一步都添加断言或反向推导验证训练时显存不足max_length 设置过大或 batch size 过大降低 batch size增加 gradient_accumulation_steps训练后模型只会复述指令数据中推理部分过短或缺失检查数据生成器确保 reasoning_steps 至少包含 2 个步骤模型在简单问题上变强复杂问题变差数据分布单一难度没有递进在生成器中加入难度采样混合简单、中等、困难样本推理时出现 NaN lossfp16 精度问题尝试 bf16或者调整学习率5.1 mask 构建最常见的坑mask 构建最容易出错的地方是 token 对齐。很多人会直接计算instruction字符串的字符长度然后用于 token 长度切分。但中文字符在 tokenizer 中可能被拆成多个 token字符长度和 token 长度并不一致。所以建议统一用tokenizer.encode(instruction_text, add_special_tokensFalse)得到 token 数而不是用len(instruction_text)。5.2 数据校验的边界程序化数据生成器看似简单实际写的时候要特别小心边界条件。例如多位数相乘时中间结果可能溢出你预设的答案长度随机生成的数字可能导致重复问题某些语言模型 tokenizer 处理超长数字时会产生异常 token。我的建议是在数据生成后单独跑一遍统计脚本检查样本长度分布、重复率、答案分布避免喂给模型一批高度雷同的数据。5.3 训练过程中的监控指标为了及时发现训练异常建议至少记录以下几项train/loss train/learning_rate train/global_step train/epoch eval/loss如果有验证集如果发现 loss 下降但模型输出质量没有明显提升优先检查数据质量而不是盲目调整学习率。6. 最佳实践与工程建议6.1 程序化数据生成器的分层设计真正生产级的程序化数据生成器不是单个脚本而是多个模块的组合。我比较推荐如下分层结构- 问题采样器Question Sampler负责决定生成什么类型、什么难度的问题 - 求解引擎Solver Engine负责用规则或外部工具解出正确答案和中间步骤 - 自然化器Naturalizer把求解步骤转成自然语言推理过程 - 校验器Validator对最终数据和中间步骤做多轮验证 - 分布控制器Distribution Controller控制不同难度、不同主题的数据比例。这个分层设计的好处是每个模块可以独立测试和替换。比如你想从算术题扩展到逻辑题只需要新增一个 Solver Engine其他模块不需要改动。6.2 建议保留 5%-10% 的全量 SFT 数据虽然本文重点是 completion-supervised但纯 reasoning 数据训练出来的模型在通用对话能力上可能出现回退。一个常见的做法是80%-90% 数据来自 Reasoning Core 程序化推理数据5%-10% 数据保留通用指令跟随数据5% 数据是纯文本语料或代码语料帮助稳定输出格式。这样做既能提升推理能力又不会让模型丢失基本的对话和格式能力。6.3 Completion Mask 的工程化封装在大型项目中completion mask 必须在数据转换时就生成好而不是在训练时临时拼接。原因很简单如果每个 epoch 都重新拼接和切分文本容易引入不一致也浪费训练前的时间。建议把 mask 作为 Dataset 的一列保存训练时直接使用。另外我建议训练脚本中增加一个显式断言防止 mask 错误被悄悄带入训练def assert_mask_correct(labels, input_ids): # labels 中 -100 的数量不应超过总 token 的一半否则可能是 mask 范围出错 masked_ratio (labels -100).sum() / len(labels) assert masked_ratio 0.5, fmask 比例异常: {masked_ratio:.3f}这种显式检查虽然简单但能避免很多“模型训完才发现数据不对”的悲剧。6.4 评测闭环构建 Reasoning Core 数据管线时一个常见的误区是只关注训练集忽略评测集。程序化数据的最大优势就是可扩展你要充分利用这一点用不同的随机种子生成一份独立的评测集评测集与训练集来自同一生成器但保证问题不重叠同时准备一份人工标注的通用推理评测集防止模型过拟合到生成器分布。推荐至少跑四个维度维度评测方式目标正确率答案是否与标准结果一致越高越好步骤完整度推理中间步骤是否齐全越高越好格式稳定性输出是否符合约定格式越高越好泛化性在未见过的难度/题型上的表现关注是否有掉点6.5 关于安全与合规在工程落地中如果 Reasoning Core 用于生产环境的模型训练需要注意以下几点训练数据不能包含个人隐私、未授权内容或违规信息若数据生成器涉及外部代码执行必须放在沙箱环境中运行使用开源模型和数据集时注意检查许可协议任何涉及数据清洗、删除、修改的操作都要在测试环境验证后再执行。这些看起来是“流程问题”但在实际项目里往往比模型效果更容易被忽视也更容易带来风险。7. 实战扩展方向到这里你其实已经拥有了一套从数据生成、数据转换到 completion-supervised 训练的闭环框架。下一步可以往以下方向扩展。第一支持多种推理任务。在当前的算术生成器基础上可以增加代数、几何、逻辑推理、代码推理等任务。关键是每个任务都需要一个可验证的 Solver Engine。没有验证机制的任务不建议放入 Reasoning Core 管线。第二引入课程学习。将程序化数据的难度从低到高排列训练时可以按难度阶段逐步递增。例如前 1/3 个 epoch 只训练简单问题后面再逐渐加入复杂问题。这种方法在多个推理任务上都有稳定提升。第三与 RLHF 或 DPO 结合。Reasoning Core 训练出的模型可以作为基础策略模型再用少量偏好标注做对齐效果通常比直接从通用模型开始对齐要好。核心原因是推理数据的中间步骤为对齐阶段提供了更稳定的信号。第四从生成数据到主动学习。让生成器根据模型当前的错误分布动态生成更多难度相近的样本实现“越练越难、越难越练”的迭代闭环。这些小方向每一个单独拿出来都值得单独写一篇博客。但核心思想是一致的程序化数据解决“数据从哪来”completion-supervised 解决“数据怎么学”两条线结合才是 Reasoning Core 的精髓。希望这套思路对你后续的模型训练有实际帮助。