Article
从工程实战到训练数据:AI工程化自动产出SFT数据的系统化方法
承接第7篇的数据闭环,本文聚焦如何将已筛选的工程资产加工为高质量SFT样本,并接入可治理、可评估、可迭代的训练流水线。
版权声明与免责声明 本文基于InstructGPT、Flan等SFT训练研究,结合工程实践经验进行综合解读。
数据边界说明 本文后半部分的流程均为实施建议与示例流程,不声称来自作者团队的真实训练实验。涉及训练收益时,仅引用公开来源披露的数据;团队内部若要报告效果,必须提供数据窗口、样本口径、切分策略、评估集和复核方式。
原创性质 本文提出的反向数据生成框架、数据质量评估标准和构建流程为作者原创。
开头:从“有很多记录”到“有可训练样本”
在第 7 篇里,我们解决的是闭环入口问题:哪些 AI 协作轨迹值得留下,哪些必须丢弃,哪些应该进入 eval。接下来真正会卡住团队的,是另一个更具体的问题:
同样是“AI 辅助交付留下的记录”,为什么有些只能用于复盘,有些可以进入知识库,只有少数能成为训练样本?
这篇文章只做一件事:把这个筛选与加工过程讲清楚。重点不是“怎么把日志导出成 JSONL”,而是如何在工程语境里定义可训练样本的质量边界、数据契约和路由规则。
你可以把本文当作一条中段流水线:
- 输入端是第 7 篇已经治理过的工程资产(任务契约、错误类型、评审反馈、验证证据)。
- 处理中段是样本构建、质量门控、脱敏、分桶、导出与版本化。
- 输出端是可进入 SFT 的候选样本,以及可回归验证的评估资产。
所以本文不讨论“是否应该训练模型”的战略问题,而是回答“当团队决定要训练时,怎样避免把低质量工程噪音训练成模型默认行为”。
整体架构:从开发到训练数据的自动化流水线
在深入细节之前,先来看整体架构。理想的SFT数据生成不是手动整理,而是在开发过程中自动产出:
这个流水线将BMAD-Speckit-SDD-Flow的开发流程与SFT数据提取无缝集成,实现**“开发即训练”**。
核心概念:什么是SFT训练数据
SFT的基本概念
SFT(监督微调)是让预训练模型学习特定任务的方法。
基本形式:输入(指令/问题)→ 输出(期望回答)
编程场景的SFT数据:
{
"instruction": "实现一个支持LRU淘汰策略的缓存类",
"input": "要求:\n- 支持get和put操作\n- 时间复杂度O(1)\n- 容量可配置",
"output": "class LRUCache:\n def __init__(self, capacity: int):\n ...",
"metadata": {
"difficulty": "medium",
"source": "engineering_project",
"verified": true,
"quality_score": 0.92
}
}
为什么工程产出适合转化为SFT数据
特点1:真实性强
- 来自实际项目需求
- 经过实践检验
- 不是理论假设
特点2:质量可控
- 经过人工审查
- 有明确的对错标准
- 有改进轨迹记录
特点3:领域相关
- 符合团队技术栈
- 符合业务场景
- 符合编码规范
特点4:持续产生
- 每个项目都在产生新数据
- 数据量随时间增长
- 可以持续更新模型
可转化的工程资产类型
资产类型1:评估题目集
来源:给AI出题时积累的题目
转化方式:
原始题目(问题描述 + 测试用例)
↓
优质答案(人工编写或精选AI答案)
↓
SFT数据(指令-输入-输出三元组)
示例:
{
"instruction": "编写一个函数,找出数组中第K大的元素",
"input": "要求:\n- 函数签名:find_kth_largest(nums: List[int], k: int) -> int\n- 时间复杂度:O(n)或O(n log n)\n- 不使用内置排序",
"output": "def find_kth_largest(nums: List[int], k: int) -> int:\n def partition(left, right, pivot_idx):\n ...",
"metadata": {
"algorithm": "quickselect",
"difficulty": "medium",
"source": "evaluation_suite",
"pass_rate": 0.75
}
}
资产类型2:代码审查反馈
来源:对AI生成代码的审查意见
转化方式:
问题代码(AI生成的不良代码)
↓
审查反馈(指出问题+改进建议)
↓
修正代码(人工修正后的优质代码)
↓
SFT数据(用于训练代码修复能力)
示例:
{
"instruction": "修复以下代码中的安全问题",
"input": "```python\ndef query_user(username):\n sql = f\"SELECT * FROM users WHERE name = '{username}'\"\n return db.execute(sql)\n```",
"output": "```python\ndef query_user(username):\n sql = \"SELECT * FROM users WHERE name = %s\"\n return db.execute(sql, (username,))\n```\n\n修复说明:\n1. 使用参数化查询防止SQL注入\n2. 永远不要拼接用户输入到SQL中",
"metadata": {
"issue_type": "security",
"severity": "high",
"source": "code_review",
" CWE_id": "CWE-89"
}
}
资产类型3:多轮对话记录
来源:与AI协作解决问题的完整对话
转化方式:
完整对话(问题→AI回答→反馈→改进→...)
↓
抽取高质量轮次
↓
SFT数据(用于训练对话能力)
示例:
{
"messages": [
{
"role": "user",
"content": "请实现一个线程安全的单例模式"
},
{
"role": "assistant",
"content": "class Singleton:\n _instance = None\n \n def __new__(cls):\n if cls._instance is None:\n cls._instance = super().__new__(cls)\n return cls._instance"
},
{
"role": "user",
"content": "这个实现在多线程环境下有问题。如果两个线程同时判断_instance为None,会创建两个实例。"
},
{
"role": "assistant",
"content": "class Singleton:\n _instance = None\n _lock = threading.Lock()\n \n def __new__(cls):\n if cls._instance is None:\n with cls._lock:\n if cls._instance is None:\n cls._instance = super().__new__(cls)\n return cls._instance"
}
],
"metadata": {
"conversation_type": "iterative_refinement",
"topic": "concurrency",
"source": "chat_logs"
}
}
资产类型4:成功项目案例
来源:AI辅助完成的完整项目代码
转化方式:
项目代码(模块化、带注释、高质量)
↓
拆解为教学案例
↓
SFT数据(用于训练复杂任务分解)
示例:
{
"instruction": "设计并实现一个JWT认证系统",
"input": "需求:\n1. 支持token生成和验证\n2. 支持token刷新\n3. 支持token吊销\n4. 使用Redis存储会话\n\n请按以下步骤实现:\n1. 设计数据模型\n2. 实现核心功能\n3. 添加错误处理\n4. 编写测试用例",
"output": "## 步骤1:数据模型设计\n...\n\n## 步骤2:核心功能实现\n...\n\n## 步骤3:错误处理\n...\n\n## 步骤4:测试用例\n...",
"metadata": {
"project_type": "authentication_system",
"complexity": "high",
"source": "successful_project",
"verified_in_production": true
}
}
BMAD-Speckit-SDD-Flow架构:自动化SFT数据生成系统
在前面的章节中,我们讨论了手动从工程资产中提取SFT数据的方法。但手动方式效率低下,难以规模化。本节将深入介绍BMAD-Speckit-SDD-Flow架构——一个能够在AI工程化开发过程中自动产出SFT训练数据的系统化方案。
架构设计理念
BMAD-Speckit-SDD-Flow结合了BMAD方法(多智能体敏捷开发)与Spec-Driven Development(规范驱动开发),在以下环节自动捕获和转化训练数据:
核心数据模型:CanonicalSftSample
系统的核心是一个标准化的数据模型CanonicalSftSample,它将所有来源的训练数据统一为规范格式:
interface CanonicalSftSample {
// 样本唯一标识
sample_id: string;
sample_version: 'v1';
// 数据来源追踪
source: {
run_id: string; // 执行运行ID
stage: string; // 开发阶段
flow: string; // 工作流类型
epic_id?: string; // 所属Epic
story_id?: string; // 所属Story
artifact_refs: Array<{ // 原始工件引用
path: string;
content_hash: string;
kind: string;
}>;
};
// 对话消息(兼容OpenAI格式)
messages: Array<{
role: 'system' | 'user' | 'assistant' | 'tool';
content: string;
tool_calls?: ToolCall[];
tool_call_id?: string;
weight?: 0 | 1;
}>;
// 工具定义(用于tool calling训练)
tools?: Tool[];
// 元数据
metadata: {
schema_targets: string[]; // 目标格式
language: string; // 语言
tags?: string[];
notes?: string[];
};
// 质量评估
quality: {
acceptance_decision: 'accepted' | 'rejected' | 'downgraded';
phase_score: number | null;
dimension_scores?: Record<string, number>;
veto_triggered: boolean;
iteration_count: number;
has_code_pair: boolean;
token_estimate: number;
rejection_reasons: string[];
warnings: string[];
};
// 数据来源追溯
provenance: {
base_commit_hash: string | null;
content_hash: string | null;
source_path: string | null;
patch_ref: string | null;
lineage: string[];
generated_at: string;
};
// 数据集划分
split: {
assignment: 'train' | 'validation' | 'test' | 'holdout';
seed: number;
strategy: string;
group_key: string | null;
};
// 数据脱敏信息
redaction: {
status: 'clean' | 'redacted' | 'blocked';
applied_rules: string[];
findings: Array<{
kind: string;
severity: 'low' | 'medium' | 'high' | 'critical';
field_path: string;
action?: string;
}>;
redacted_fields: string[];
};
// 导出兼容性
export_compatibility: {
openai_chat: ExportDecision;
hf_conversational: ExportDecision;
hf_tool_calling: ExportDecision;
};
}
SFT数据提取管道详解
数据提取管道包含四个核心阶段:
阶段1:Candidate Builder(候选构建)
// 从运行记录构建候选样本
function buildCanonicalSample(
record: RunScoreRecord, // 评估运行记录
sourceContent: string, // 原始工件内容
codePair: { input: string; output: string }, // 代码对比
options: BuildOptions
): CanonicalSftSample {
// 1. 从审计报告中提取指令(§1问题描述 + §4修复方案)
const instruction = extractInstruction(sourceContent);
// 2. 构建对话消息
const messages = buildCanonicalMessages(
instruction,
codePair.input, // 修改前代码
codePair.output // 修改后代码
);
// 3. 计算确定性数据集划分(基于story hash)
const split = assignDeterministicSplit({
seed: options.splitSeed ?? 42,
groupKey: parsedStory
? `epic-${parsedStory.epicId}/story-${parsedStory.storyId}`
: record.run_id,
});
// 4. 构建完整样本
return {
sample_id: buildCanonicalSampleId({...}),
source: { run_id, stage, epic_id, story_id, artifact_refs },
messages,
quality: { phase_score, iteration_count, has_code_pair, ... },
provenance: { base_commit_hash, content_hash, patch_ref, ... },
split,
// ... 其他字段
};
}
关键特性:
- Git Diff提取:自动从base_commit到当前HEAD提取代码变更
- 指令提取:从审计报告的标准章节(§1问题、§4方案)提取训练指令
- 缓存机制:避免重复构建,提高性能
阶段2:Quality Gates(质量门控)
质量门控系统对候选样本进行多维度评估,决定是否接受:
interface QualityGateOptions {
minScore?: number; // 最低分数门槛(默认90)
maxIterations?: number; // 最大迭代次数
maxTokens?: number; // 最大token数
requireCodePair?: boolean; // 是否要求代码对比
}
function applyQualityGates(
sample: CanonicalSftSample,
options: QualityGateOptions
): CanonicalSftSample {
const hardReasons: string[] = []; // 硬性拒绝原因
const softReasons: string[] = []; // 软性警告原因
// 硬性检查
if (!sample.provenance.base_commit_hash) {
hardReasons.push('prov_missing_hash');
}
if ((sample.quality.phase_score ?? 0) < minScore) {
hardReasons.push('score_below_floor');
}
if (sample.quality.veto_triggered) {
hardReasons.push('veto_triggered'); // 关键审计项否决
}
if (sample.redaction.status === 'blocked') {
hardReasons.push('redaction_blocked'); // 脱敏阻断
}
// 软性检查
if (sample.quality.iteration_count > maxIterations) {
softReasons.push('too_many_iterations');
}
if (!sample.quality.has_code_pair) {
softReasons.push('missing_code_pair');
}
// 决定:accepted / rejected / downgraded
const acceptanceDecision =
hardReasons.length > 0 ? 'rejected' :
softReasons.length > 0 ? 'downgraded' : 'accepted';
return { ...sample, quality: { ...sample.quality,
acceptanceDecision,
rejection_reasons: [...hardReasons, ...softReasons]
}};
}
质量维度:
| 检查项 | 类型 | 说明 |
|---|---|---|
| 来源完整性 | 硬性 | 必须有commit hash、source path |
| 分数门槛 | 硬性 | phase_score >= 90(可配置) |
| 否决触发 | 硬性 | 关键审计项未通过 |
| 脱敏阻断 | 硬性 | 检测到私钥等敏感信息 |
| 消息完整性 | 硬性 | 必须包含user和assistant消息 |
| 迭代次数 | 软性 | 超过maxIterations降级 |
| 代码对比 | 软性 | 无input/output代码对降级 |
阶段3:Redaction(数据脱敏)
自动检测和脱敏敏感信息,确保训练数据安全:
// 脱敏规则
const REDACTION_RULES = {
// 邮箱地址 → 中等风险,脱敏处理
email: {
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
severity: 'medium',
action: 'redact', // 替换为[REDACTED_EMAIL]
},
// Secret Token → 高风险,脱敏处理
secretToken: {
pattern: /\bsk-[A-Za-z0-9]{16,}\b/g, // OpenAI API Key等
severity: 'high',
action: 'redact', // 替换为[REDACTED_SECRET]
},
// 私钥 → 致命风险,阻断样本
privateKey: {
pattern: /BEGIN [A-Z ]+ PRIVATE KEY/,
severity: 'critical',
action: 'block', // 样本被拒绝
},
};
function applyCanonicalRedaction(sample: CanonicalSftSample): CanonicalSftSample {
let status: 'clean' | 'redacted' | 'blocked' = 'clean';
const findings: RedactionFinding[] = [];
const messages = sample.messages.map((message, index) => {
let content = message.content;
// 应用每条规则
for (const [ruleName, rule] of Object.entries(REDACTION_RULES)) {
if (rule.pattern.test(content)) {
if (rule.action === 'block') {
status = 'blocked';
findings.push({ kind: ruleName, severity: rule.severity, ... });
} else if (rule.action === 'redact') {
status = status === 'blocked' ? 'blocked' : 'redacted';
content = content.replace(rule.pattern, `[REDACTED_${ruleName.toUpperCase()}]`);
findings.push({ kind: ruleName, severity: rule.severity, action: 'redact' });
}
}
}
return { ...message, content };
});
return { ...sample, messages, redaction: { status, findings } };
}
阶段4:Split Assignment(数据集划分)
使用确定性算法分配训练/验证/测试集,确保可复现:
function assignDeterministicSplit(options: {
seed: number;
groupKey: string | null;
}): CanonicalSplit {
const stableKey = `${options.seed}:${options.groupKey ?? 'ungrouped'}`;
const hash = sha256(stableKey).digest('hex');
const bucket = parseInt(hash.slice(0, 8), 16) % 100;
// 80%训练 / 10%验证 / 10%测试
let assignment: 'train' | 'validation' | 'test' = 'train';
if (bucket >= 80 && bucket < 90) assignment = 'validation';
if (bucket >= 90) assignment = 'test';
return { assignment, seed: options.seed, strategy: 'story_hash_v1' };
}
多格式导出系统
系统支持导出为多种流行的SFT训练格式:
OpenAI Chat格式
// 导出为OpenAI微调格式
{
"messages": [
{ "role": "system", "content": "You are a senior coding agent." },
{ "role": "user", "content": "修复以下SQL注入问题...\n\nCurrent implementation:\ndef query(user):\n sql = f\"SELECT * FROM users WHERE name = '{user}'\"" },
{ "role": "assistant", "content": "def query(user):\n sql = \"SELECT * FROM users WHERE name = %s\"\n return db.execute(sql, (user,))" }
],
"tools": [...], // 可选工具定义
"parallel_tool_calls": false
}
HuggingFace Conversational格式
// 导出为HF对话格式
{
"system": "You are a senior coding agent.",
"conversations": [
{ "from": "human", "value": "修复以下SQL注入问题..." },
{ "from": "gpt", "value": "def query(user):\n sql = \"SELECT * FROM users WHERE name = %s\"..." }
]
}
HuggingFace Tool Calling格式
// 导出为HF工具调用格式
{
"system": "You are a coding assistant with tool access.",
"conversations": [
{ "from": "human", "value": "分析这段代码的复杂度" },
{ "from": "gpt", "value": "", "tool_calls": [...] },
{ "from": "tool", "value": "{\"complexity\": \"O(n^2)\", ...}" },
{ "from": "gpt", "value": "这段代码的时间复杂度是O(n^2)..." }
],
"tools": [...]
}
CLI使用示例
# 基础提取(默认phase_score >= 90)
npx ts-node scripts/sft-extract.ts
# 指定最低分数
npx ts-node scripts/sft-extract.ts --min-score 85
# 指定输出路径
npx ts-node scripts/sft-extract.ts --output ./custom-sft-data.jsonl
# 完整示例
npx ts-node scripts/sft-extract.ts \
--min-score 90 \
--output ./training-data/sft-v1.0.jsonl
输出摘要示例:
共提取 156 条,覆盖 12 个 Story;跳过 23 条(原因:无source_path: 10, git diff失败: 8, phase_score低于阈值: 5)
数据构建流程:从原始资产到训练数据
这一段要解决的不是“把哪些文件丢进训练脚本”,而是“怎样把工程过程压缩成模型可以学习的决策样本”。原始资产里有事实、有噪音、有上下文,也有很多只对当时团队成员才成立的隐含信息。数据构建流程的任务,就是把这些材料逐层变成可登记、可构建、可门控、可治理、可切分导出的训练资产。
可以把这条链路理解成四道关口:先确认资产来自哪里,再把资产构建成候选样本并通过质量门控;再决定是否需要补充证据和治理标记;最后才进入切分和多格式导出。它和前面的实现管道是一条线的两种视角:资产登记喂给Candidate Builder,样本构建与清洗门控对应Quality Gates和Redaction,证据增强发生在硬门通过之后,切分与导出对应Split Assignment和多格式导出系统。
阶段1:资产登记与分类
资产登记不是把文件越堆越多,而是为每一份材料建立“为什么可以进入候选池”的证据入口。对SFT来说,最有价值的不是孤立代码片段,而是带有任务目标、约束、反馈和验收结果的工程闭环。没有登记,后面的Candidate Builder只能根据文本猜测样本边界;有了登记,才能按任务类型、来源链路和最低证据要求构建候选样本。
登记范围可以从五类资产开始:
- 评估题目及优质答案
- 代码审查记录(问题代码+反馈+修正)
- 多轮对话日志
- 项目文档和代码
- 错误案例分析
分类的目的不是给数据贴标签,而是决定后续该用什么样的样本结构承载它。代码生成任务适合instruction-input-output;代码审查更适合problem-feedback-solution;多轮协作要保留messages;架构设计则需要把约束、取舍和边界写入上下文。
DATA_CATEGORIES = {
"code_generation": {
"description": "代码生成任务",
"sources": ["evaluation_questions", "project_code"],
"format": "instruction-input-output",
"must_have": ["requirements", "reference_solution", "tests"]
},
"code_review": {
"description": "代码审查和修复",
"sources": ["review_feedback", "bug_fixes"],
"format": "problem-feedback-solution",
"must_have": ["problem_code", "review_reason", "fixed_code"]
},
"conversation": {
"description": "多轮对话",
"sources": ["chat_logs"],
"format": "messages",
"must_have": ["task_goal", "human_feedback", "final_acceptance"]
},
"architecture": {
"description": "架构设计",
"sources": ["design_docs", "system_docs"],
"format": "instruction-context-output",
"must_have": ["constraints", "tradeoffs", "decision_record"]
}
}
这段配置真正表达的是一条治理原则:每一种资产进入候选池之前,都必须先定义“最低证据要求”。如果没有测试,评估题就只是题面;如果没有修复理由,代码审查就只是diff;如果没有最终验收,多轮对话就只是聊天记录。训练数据不是记录的搬运,而是证据的整理。Candidate Builder应该只负责把登记充分的资产切成候选样本,不应该替团队补造不存在的上下文。
阶段2:样本构建与清洗门控
这一阶段对应实现管道里的Quality Gates和Redaction。它不是简单地删空行、统一字段名,而是把不适合训练的样本挡在训练池外。候选样本进入这里以后,系统要同时处理重复、正确性、安全、可追溯性和脱敏状态。
-
样本构建与去重
同一缺陷可能在多个接口里重复出现,同一段AI回答也可能被不同人复制到多个任务里。去重时不能只比较文本相似度,还要比较任务类型、模块路径、错误类型和修复模式。保留版本也不能只看“更短”或“更像标准答案”,而要保留证据更完整、验收更清楚的版本。
def deduplicate(data): # 基于代码相似度去重 # 基于问题描述去重 # 基于 story_id / module_path / fix_pattern 聚合同类问题 # 保留质量更高的版本 -
质量门控
质量门控要回答“这条样本是否值得模型模仿”。能通过测试只是最低条件,还要看问题和答案是否一一对应、解释是否覆盖关键边界、安全风险是否已经处理、是否存在脱敏失败。没有通过硬门的样本不应该被“人工感觉还不错”放行,它只能进入拒绝报告或待补全队列。
def quality_filter(item): # 代码必须通过测试 # 代码质量评分 > 阈值 # 有明确的问题-答案对应关系 # 无敏感信息(密码、密钥等) # 能追溯到任务、commit、review或验收记录 -
脱敏与格式标准化
标准化不是把所有数据压成同一种样子,而是在不破坏语义的前提下让训练框架能够读取。字段名可以统一,任务边界不能丢;代码风格可以整理,修复理由不能删;元数据可以脱敏,来源链路不能断。脱敏失败的样本即使答案正确,也不能进入训练池。
def normalize_format(item, target_format): # 统一字段命名 # 统一代码风格 # 添加元数据 # 保留 lineage / evidence / split_key
筛选标准可以先用一张可讨论的权重表表达。这里的数字不是固定答案,而是帮助团队明确:正确性、安全性和可解释性都要进入同一套判断。
| 维度 | 判断问题 | 示例权重 |
|---|---|---|
| 功能正确性 | 是否通过测试、是否满足任务契约 | 40% |
| 代码质量 | 是否符合项目风格、复杂度是否可控 | 25% |
| 安全性 | 是否移除高危漏洞、是否完成脱敏 | 20% |
| 可理解性 | 是否解释了关键取舍和边界条件 | 15% |
阶段3:证据增强与治理标记
证据增强不是把样本数量“乘以几倍”,而是在不改变工程事实的前提下,让样本更容易被训练、审计和复用。它发生在质量门控之后,不是硬门的一部分。对代码数据来说,增强一旦改变约束,就可能把正确样本变成错误样本。因此,增强必须围绕证据做,而不是围绕表面文本做。
增强方法1:表达变体
变体生成适合改写任务表达,但不能改变任务目标、权限边界、性能约束或错误类型。比如“修复空指针异常”和“修复登录失败”不是同一个问题;“支持重试”和“支持幂等重试”也不是同一个约束。
# 生成同一问题的多种表述
def generate_variants(original_instruction, n=5):
variants = []
# 使用同义词替换
# 改变句式结构
# 增减细节信息
# 保持任务目标、边界条件和验收标准不变
return variants
增强方法2:难度调整
难度调整适合教学型数据和评估型数据。简单版本可以增加提示、缩小上下文;困难版本可以减少提示、增加边界条件。但困难版本不能凭空添加原始任务没有覆盖的业务规则,否则训练出来的模型会学习到不存在的约束。
# 生成同一问题的不同难度版本
def adjust_difficulty(item, target_difficulty):
if target_difficulty == "easy":
# 提供更多提示
# 减少约束条件
elif target_difficulty == "hard":
# 增加边界条件
# 增加性能要求
增强方法3:跨语言迁移
跨语言迁移更像重新建模,而不是简单翻译。把Python题目迁移到JavaScript或Go时,需要重新检查标准库、错误处理、并发模型、测试框架和惯用写法。只有当目标语言版本重新通过测试和人工复核后,才适合进入训练池。
# 将Python题目迁移到JavaScript/Go等
def migrate_language(item, target_language):
# 语法转换
# 惯用法调整
# 生态适配
# 重新生成测试和验收证据
增强样本应明确标记来源,例如curation_origin: "variant"、curation_origin: "difficulty_adjusted"或curation_origin: "language_migration"。这些样本可以进入训练实验,但不能混入冻结评估集。否则评估结果会被合成数据污染,看起来提升很大,实际上只是模型见过相似表达。
阶段4:切分与多格式导出
切分和导出是最后一步,不是第一步。OpenAI、Hugging Face、Alpaca、ShareGPT或内部格式都只是外层容器;真正决定训练质量的是样本是否保留了任务、上下文、答案、证据和治理状态。建议先维护一个团队内部的通用样本结构,再按Split Assignment的结果导出到不同框架,避免同一个Story的近似样本同时出现在训练集和评估集里。
通用SFT格式可以这样表达:
{
"dataset_info": {
"name": "team_coding_sft",
"version": "generated_by_pipeline",
"created_at": "generated_by_pipeline",
"source_window": "generated_by_pipeline",
"split_strategy": "story_id"
},
"data": [
{
"id": "generated_sample_id",
"type": "code_generation",
"instruction": "...",
"input": "...",
"output": "...",
"metadata": {
"source": "evaluation_suite",
"difficulty": "medium",
"language": "python",
"quality_score": "generated_by_pipeline",
"verified": true,
"lineage": {
"story_id": "...",
"commit": "...",
"review_id": "..."
},
"export_decision": {
"train": true,
"eval": false,
"reason": "accepted_for_training_only"
}
}
}
]
}
框架特定格式:
- Alpaca格式
- ShareGPT格式
- OpenAI fine-tuning格式
- HuggingFace datasets格式
导出时要同时生成dataset-card.md、split-manifest.json、lineage-map.json和rejection-report.json。训练框架只关心JSONL能不能读,团队长期运营还要关心样本从哪里来、为什么被接受、哪些被拒绝、评估集是否被冻结。做到这一步,数据构建流程才真正从“整理材料”升级为“管理训练资产”,也和前面的提取管道保持了同一套证据链。
数据质量评估标准
评估维度
维度1:准确性
- 代码能否正确运行
- 是否符合需求描述
- 测试用例通过率
维度2:完整性
- 是否包含必要信息
- 是否包含边界情况处理
- 是否包含错误处理
维度3:一致性
- 输入输出对应关系清晰
- 风格统一
- 术语一致
维度4:多样性
- 问题类型覆盖
- 难度分布合理
- 语言/框架覆盖
维度5:安全性
- 无恶意代码
- 无敏感信息泄露
- 符合安全规范
质量评分模型
def calculate_quality_score(item):
scores = {
'correctness': evaluate_correctness(item), # 40%
'completeness': evaluate_completeness(item), # 25%
'consistency': evaluate_consistency(item), # 20%
'safety': evaluate_safety(item), # 15%
}
weights = {
'correctness': 0.40,
'completeness': 0.25,
'consistency': 0.20,
'safety': 0.15
}
total_score = sum(scores[k] * weights[k] for k in scores)
return total_score
人工审核要点
必审项:
- 代码功能正确性(运行验证)
- 无安全漏洞(静态扫描)
- 无敏感信息(正则匹配)
- 格式规范(自动化检查)
抽检项(20%抽样):
- 代码质量(人工评估)
- 教学价值(专家评估)
- 场景真实性(业务方确认)
SFT训练基础:如何使用这些数据
训练流程概览
原始数据
↓
数据预处理(清洗、格式化)
↓
数据集构建(训练/验证/测试划分)
↓
模型微调(SFT训练)
↓
模型评估
↓
模型部署
简易训练示例(使用HuggingFace)
2026-03 来源口径:训练 API 和 LoRA 配置以 TRL SFTTrainer 与 PEFT LoRA 文档为准;基础模型使用占位 ID,因为实际项目必须先复核模型卡、许可证、训练数据边界和团队内部评估快照。
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
# 1. 加载基础模型(示例占位:按 2026-03 模型卡、许可证和内部评测选择)
base_model_id = "your-org/code-model-base"
model = AutoModelForCausalLM.from_pretrained(base_model_id)
tokenizer = AutoTokenizer.from_pretrained(base_model_id)
# 2. 配置LoRA(降低训练成本)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
# 3. 准备数据集
dataset = load_dataset("json", data_files="company_coding_sft_v1.json")
# 4. 配置训练参数
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
save_steps=100,
logging_steps=10,
)
# 5. 开始训练
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset["train"],
eval_dataset=dataset["validation"],
args=training_args,
)
trainer.train()
# 6. 保存模型
model.save_pretrained("./company_coding_model")
训练建议
数据量建议:
- 最小:1,000条高质量样本
- 推荐:10,000+条样本
- 理想:100,000+条样本
质量 > 数量:
- 1,000条高质量数据 > 10,000条低质量数据
- 宁可少而精,不要多而杂
持续迭代:
- 先用小数据集验证可行性
- 根据效果逐步增加数据
- 建立数据-训练-评估的闭环
实施建议:从工程资产盘点到SFT样本池
SFT数据建设最容易误判的地方,是把“团队有很多工程记录”直接等同于“团队已经有训练数据”。这两者之间差着一整条证据链。工程记录记录的是工作发生过,训练样本要求的是模型应该学什么、为什么能学、学完以后怎么验证。换句话说,工程资产面向交付复盘,训练数据面向模型学习;前者可以残缺、上下文依赖很强、只服务当时的协作,后者必须边界清楚、结论可复核、输出值得模仿。
一条PR评论、一段AI对话、一次失败的CI、一个修复commit,都可能有训练价值。但它们默认都不是训练数据。训练样本至少要回答四个问题:任务是什么,错误在哪里,正确输出是什么,证据如何证明这个输出确实更好。
如果这四个问题答不清,数据越多,风险越大。模型可能学到团队内部的历史噪音、临时绕过、过时API、错误命名和不完整解释。更糟糕的是,如果评估集也来自同一批近似任务,训练收益会看起来很漂亮,但本质上只是同源数据泄漏。很多团队失败不是因为样本少,而是因为把“发生过的文本”当成“应该学习的行为”。这会让模型更像一个复读历史记录的助手,而不是一个能理解边界、复核证据、遵守工程约束的编码伙伴。
因此,样本池建设的第一步不是写训练脚本,而是建立资产登记。资产登记不是行政台账,而是把工程资产转换为可判断对象。每条候选样本都应能回到原始任务、原始代码、审查意见、验证结果和脱敏记录。
OpenAI的Supervised fine-tuning文档把fine-tuning放在模型优化链路中,而不是孤立的数据上传动作。对工程团队来说,这意味着训练前必须先有eval、数据格式、质量判断和生产输入口径。
第一步:把资产盘点改成来源登记
不要只统计“有多少条记录”,而要登记这些记录是否能追溯到任务、代码、评审和验证证据。数量应由团队自己的仓库、CI、评审系统和对话日志导出,不能手工估算。
登记时不要只看“内容像不像训练样本”,而要看“证据能不能闭环”。一段对话如果没有最终验收,就只能作为知识库素材;一次修复如果没有测试或review结论,就只能作为候选线索;一个成功项目如果无法拆出任务边界,也不能直接变成instruction。登记字段也不应只写“来源:GitHub”或“来源:对话日志”,而要写清楚这个样本属于哪个Story、哪个模块、哪个commit窗口、哪一次评审、哪一组验证命令、哪一个人工结论。
| 资产类型 | 真实来源 | 候选口径 | 进入样本池前必须具备的证据 |
|---|---|---|---|
| 评估题目 | Eval仓库、题库、回归测试 | 有明确输入、期望输出和评分规则的题目 | 参考答案、测试用例、难度标签、失败样例 |
| 代码审查记录 | PR Review、Code Review工具 | 有问题描述、修复diff和review结论的记录 | 原始代码、修复代码、审查理由、合并状态 |
| 多轮对话日志 | AI协作平台、IDE插件、工单系统 | 有任务目标、约束、反馈和最终交付的对话 | 任务契约、关键决策、最终验收、人工反馈 |
| 项目代码模块 | Git历史、发布分支、组件库 | 有稳定接口、测试覆盖和维护责任人的模块 | commit、测试结果、许可证边界、维护状态 |
| 缺陷复盘 | 事故记录、bug工单、回滚记录 | 有症状、根因、修复和防复发动作的记录 | 影响范围、根因结论、修复diff、回归测试 |
这里的重点是“来源可信度”。同样是一段代码,来自生产修复、来自评估题、来自AI生成草稿,训练价值完全不同。来源越接近真实交付,越需要严格脱敏;来源越接近合成生成,越不能混入评估集。
资产登记还要保留“不训练”的理由。很多记录对人类复盘有价值,却不适合进入SFT:比如争论过程很长但结论不明确的设计讨论、修复有效但理由写错的commit、测试通过但实际上绕开了业务约束的补丁、只在某个临时环境成立的脚本。把这些记录标记为knowledge_only、needs_manual_review或discarded,不是浪费资产,而是在保护训练池不被模糊信号污染。
一个实用做法是给每条候选样本维护lineage字段。它不需要暴露敏感信息,但要能在内部复核时找到原始证据。
{
"sample_id": "sft_bugfix_000123",
"source_type": "pull_request_review",
"lineage": {
"repo": "<internal-repo-id>",
"story_id": "<story-id>",
"commit": "<commit-sha>",
"review_id": "<review-id>",
"ci_run": "<ci-run-id>"
},
"evidence": {
"tests_passed": true,
"review_approved": true,
"redaction_status": "clean"
}
}
有了这层登记,团队才能区分“可训练样本”“只适合知识库”“只能用于复盘”“必须丢弃”。这比追求样本数量更重要。真正可训练的样本应当像一个压缩后的工程事实包:它不暴露敏感细节,但保留足够的判断链路;它不要求读者知道内部背景,但能让模型看到任务、约束、错误、修正和验收之间的因果关系。
第二步:用漏斗记录筛选过程
高质量数据集不是从“原始数量”直接跳到“最终样本数”,而是经过一层层拒绝。建议每次构建数据集时都保留漏斗报告。没有真实流水线统计时,不要用占位数字装成结果;先把每一层的输入口径、输出口径和拒绝原因定义清楚。
| 阶段 | 输入口径 | 输出口径 | 拒绝原因 |
|---|---|---|---|
| 候选资产导入 | 指定窗口内的评估题目、review记录、对话日志、Git提交和缺陷复盘 | 能关联任务、代码和责任边界的候选资产 | 无任务上下文、无法关联代码、缺少验收证据 |
| 格式化为样本 | 已完成来源登记的候选资产 | 具备 instruction/input/output 结构的候选样本 | instruction/input/output 不完整 |
| 自动质量评分 | 结构完整的候选样本 | 通过自动评分或进入待补全队列的样本 | 正确性不足、解释缺失、边界条件缺失 |
| 去重与近重复过滤 | 通过基础质量评分的样本 | 按任务簇、模块和修复模式去重后的样本 | 同一问题多次出现、diff过度相似 |
| 脱敏与安全扫描 | 去重后的样本 | 不含敏感信息且许可证边界清楚的样本 | 凭据、客户标识、内部域名无法安全处理 |
| 人工抽检 | 通过自动门控的样本 | 可进入训练池、知识库或复盘队列的样本 | 教学价值低、修复理由不可解释 |
这张表比“最终有多少样本”更重要。它让团队能回答:哪些数据被留下,哪些数据被拒绝,拒绝是否可复核。没有漏斗,数据集增长只是数字变化;有了漏斗,数据集才有治理入口。
漏斗还会暴露工程流程的短板。如果大量样本因为缺少source_path被拒绝,问题不是SFT管道太严格,而是开发过程没有把任务、代码和证据绑定起来。如果大量样本因为脱敏失败被拒绝,问题也不是样本不够,而是团队把敏感信息暴露在了协作记录里。
因此,漏斗报告不只是训练数据报告,也是工程管理报告。它能反向推动团队改进PR模板、commit规范、CI元数据、review结构和AI协作日志格式。好的漏斗还应区分硬拒绝和软降级:硬拒绝包括密钥泄露、许可证不明、无法追溯、没有正确答案;软降级包括解释不足、边界条件缺少、样本过长、任务类型重复。硬拒绝不能靠人工“觉得还行”放行,软降级可以进入补全队列,但必须记录补全人、补全依据和补全后是否重新验收。
第三步:谨慎处理数据增强
数据增强不能被当作真实样本的倍增器。对代码训练数据来说,随意改写题面、生成变体、扩充解释,可能会引入不可验证的语义偏差。
建议把样本分成三类:
| 样本类型 | 是否进入训练 | 是否进入评估 | 说明 |
|---|---|---|---|
| 真实工程样本 | 可以 | 可以,但必须隔离 | 来自真实任务、真实diff和真实验收 |
| 人工整理样本 | 可以 | 谨慎使用 | 人工补全解释或格式,但保留原始证据 |
| 合成增强样本 | 只适合实验 | 不建议 | 必须单独标记,不能混入真实评估集 |
代码任务的数据增强尤其危险,因为很小的改写也可能改变约束。把“支持幂等重试”改成“支持自动重试”,语义就已经变了;把“只允许管理员删除”改成“允许用户删除”,安全边界就变了。自然语言看起来只是同义替换,工程含义可能完全不同。
更安全的增强方式,是围绕证据做结构化补全,而不是围绕输出做自由改写。例如给样本补充错误类型、边界条件、验证命令、失败原因和适用范围。这些字段提高的是可学习性和可检索性,不会轻易改变任务本身。增强的边界可以简单记成三句话:不能改变任务目标,不能替换真实验收,不能进入冻结评估集。任何无法被原始证据支持的改写,都只能作为实验样本,不能伪装成生产经验。
增强样本还必须携带synthetic_origin或curation_origin。模型训练时可以选择使用这些样本,但评估时必须能排除它们。否则评估集会被“看起来真实”的合成数据污染。
第四步:先做数据集报告,不急着写训练收益
如果只是完成样本池构建,结论就应停在“数据集是否可训练、可追溯、可迭代”。不要在没有训练实验的情况下写“测试集通过率提升”。
推荐输出如下报告:
## 数据集构建报告模板
- 数据窗口:<起止日期>
- 覆盖仓库/模块:<仓库或模块范围>
- 原始资产来源:<评估题目 / review记录 / 对话日志 / Git提交>
- 最终样本数:<由流水线生成>
- 真实工程样本占比:<百分比>
- 合成增强样本占比:<百分比>
- 脱敏拒绝数:<数量与原因>
- 人工抽检比例:<比例>
- 评估隔离策略:<按Story / Repo / Module切分>
- 已知限制:<覆盖不足、语言偏置、任务类型偏置>
训练收益应放到独立评估报告里,并绑定真实评估集、分母、判定标准和复核方式。数据集构建阶段的合格结论不是“模型提升了”,而是“这批样本可以被安全地拿去训练和评估”。报告里尤其要写清样本的不可用边界:哪些语言没有覆盖,哪些框架没有覆盖,哪些安全场景被排除,哪些任务只是知识库素材而不是训练样本。
这一步看似保守,但对长期迭代非常关键。没有数据集报告,半年后团队很难解释为什么某个模型版本更好或更差;没有样本来源,模型输出异常时也无法定位是训练数据问题、评估集问题,还是基础模型变化。
数据集报告还应记录“不覆盖什么”。例如只覆盖TypeScript后端,不覆盖前端交互;只覆盖bugfix,不覆盖架构设计;只覆盖内部框架,不覆盖开源生态。明确边界不是削弱数据集价值,而是防止团队把局部有效性误当成通用能力。
最终,样本池建设的目标不是一次性做出“完美数据集”,而是形成持续运营的资产。每次新增样本都能追溯来源,每次拒绝都有原因,每次训练都有版本,每次评估都能回到冻结测试集。这样的数据才值得进入SFT流程。
数据资产化:长期运营建议
建立数据收集机制
自动收集点:
- 每次代码审查后,询问是否可用于训练
- 定期导出对话日志
- 项目代码归档时筛选高质量模块
数据管道:
工程实践
↓
自动收集
↓
初步筛选
↓
质量评分
↓
人工审核(抽样)
↓
进入训练池
↓
定期训练更新
版本管理
datasets/
├── v1.0.0_2024q1/
│ ├── train.jsonl
│ ├── validation.jsonl
│ └── metadata.json
├── v1.1.0_2024q2/
│ └── ...
└── v2.0.0_2024q3/
└── ...
持续迭代
月度:收集新数据 季度:更新数据集版本 半年:重新训练模型 年度:评估整体效果,调整策略
筛选标准与素材选择:构建高质量训练集
在使用BMAD-Speckit-SDD-Flow自动提取SFT数据时,建立科学的筛选标准至关重要。以下是经过实践验证的筛选框架:
三层筛选体系
三层筛选确保数据质量的同时保持多样性,最终保留约50%的高质量样本。
素材选择策略
1. 基于Scenario的选择
BMAD-Speckit-SDD-Flow区分不同scenario,优先选择高质量场景:
| Scenario | 描述 | 优先级 | 原因 |
|---|---|---|---|
real_dev | 真实开发场景 | 高 | 来自实际编码过程,质量最高 |
evaluation | 评估场景 | 中 | 有明确的对错标准 |
synthetic | 合成数据 | 低 | 可能缺乏真实性 |
2. 基于Score Pattern的选择
// 优先选择以下score pattern的记录
const HIGH_VALUE_PATTERNS = [
// 高初始分+低迭代 = 一次做对,优秀样本
{ initialScore: '>80', iterations: '<=2' },
// 低初始分+高最终分+适度迭代 = 有效学习样本
{ initialScore: '<60', finalScore: '>90', iterations: '3-5' },
// 有veto但最终通过 = 关键改进样本
{ vetoTriggered: true, finalScore: '>90' },
];
// 避免以下pattern
const LOW_VALUE_PATTERNS = [
// 多次迭代仍低分 = 质量存疑
{ iterations: '>5', finalScore: '<80' },
// 无代码对比 = 教学价值有限
{ hasCodePair: false },
];
3. 基于Content Category的平衡
// 理想的训练集构成
const TARGET_COMPOSITION = {
codeGeneration: 0.50, // 50% 代码生成
codeReview: 0.25, // 25% 代码审查
bugFix: 0.15, // 15% Bug修复
architecture: 0.10, // 10% 架构设计
};
// 每个类别内部再细分
const CODE_GEN_SUBCATEGORIES = {
algorithm: 0.30, // 算法实现
dataStructure: 0.25, // 数据结构
apiDesign: 0.25, // API设计
utility: 0.20, // 工具函数
};
质量门控配置建议
根据不同使用场景,调整Quality Gates参数:
场景A:构建基础模型(高严格)
const STRICT_GATES = {
minScore: 95, // 只选最高分
maxIterations: 2, // 最多2次迭代
requireCodePair: true, // 必须有代码对比
maxTokens: 4096, // 限制token数
};
// 预期保留率:20-30%
场景B:构建增强数据集(中等严格)
const BALANCED_GATES = {
minScore: 85, // 较好分数即可
maxIterations: 4, // 允许更多迭代
requireCodePair: false, // 可以没有代码对比
maxTokens: 8192, // 放宽token限制
};
// 预期保留率:50-60%
场景C:构建实验数据集(宽松)
const EXPERIMENTAL_GATES = {
minScore: 70, // 及格即可
maxIterations: 10, // 不限制迭代
requireCodePair: false, // 不强制代码对比
maxTokens: 16384, // 较大token限制
};
// 预期保留率:80-90%
示例流程:从样本生成到离线评估闭环
自动化流水线的价值,不是把日志批量倒成JSONL,而是把每个训练样本放进同一套证据链:它来自哪个任务,解决了什么问题,经过了什么验证,为什么可以进入训练池。
下面的流程使用占位数量。真实项目必须由流水线报告填充这些数字,并保留原始审计记录。任何无法追溯到日志、commit、评估结果或人工复核的数字,都不应该进入模型训练报告。这里的重点不是演示一个“训练后提升多少”的故事,而是说明团队如何把工程协作记录转成可训练、可审计、可回滚的数据版本。
一个可落地的SFT数据流水线,应当把BMAD-Speckit-SDD-Flow这类规范驱动、多角色协作的工程记录视为“候选事实源”,而不是“天然答案库”。Spec、Story、任务拆分、实现记录、评审意见、CI输出、审计发现和最终合并记录共同构成上下文。自动化提取的职责,是把这些上下文重新组织成模型可以学习的输入输出对,同时保留足够的证据,使人类能够判断这个样本是否值得进入训练。
Step 1:提取候选样本
npx ts-node scripts/sft-extract.ts \
--scenario real_dev \
--min-score 85 \
--split-key story_id \
--output ./sft-training/bugfix-v1.0.jsonl
提取阶段不要只生成一个train.jsonl。它至少要输出三类报告:
| 报告 | 作用 | 必须包含 |
|---|---|---|
extraction-summary.json | 说明样本从哪里来 | 数据窗口、Story数量、commit数量、候选样本数量 |
rejection-report.json | 说明样本为什么被拒绝 | 分数不足、缺少diff、缺少验证、脱敏失败 |
lineage-map.json | 说明样本如何追溯 | sample_id、story_id、commit、audit_id、source_path |
自动化提取可以分成四个动作。第一步是读取事件源:Story文档、任务契约、对话记录、评审评论、diff、测试报告和审计结果。第二步是建立关联:同一个Story下的需求、实现、失败、修复和验收要被绑定到同一个上下文对象,不能让模型只看到一段孤立代码。第三步是生成候选样本:把“用户要求”“上下文输入”“理想输出”“解释理由”“验证证据”整理成统一结构。第四步是写入审计记录:任何样本被接受、降级或拒绝,都必须有机器可读的原因。
示例日志应保持克制,只展示流程结构,不写未经验证的样本数量和提升结论:
[INFO] Loading scoring records from <scoring-data-dir>
[INFO] Filtering by scenario: real_dev
[INFO] Filtering by phase_score >= 85
[INFO] Building candidate samples
[INFO] Applying quality gates
[INFO] Applying redaction rules
[INFO] Assigning split by story_id
[INFO] Exporting JSONL and audit reports
自动化提取阶段最容易犯的错误,是只提取“最后的好答案”。这会让样本看起来干净,却丢掉模型最需要学习的判断过程。更好的做法是保留最小必要路径:最初的任务约束、关键失败点、最终修复、为什么这样修、用什么验证。对代码模型来说,单独学习“修复后的代码”价值有限;学习“在什么约束下为什么这么修”,才更接近团队希望模型稳定复现的能力。
Step 2:用漏斗报告替代口头统计
流水线结束后,不建议只写“共提取多少条”。更好的方式是输出一张漏斗表。表里的数量应由报告自动填充;如果没有真实项目日志,就不要写具体数字,只写清每一层的输入口径、输出口径和拒绝原因。
| 阶段 | 输入口径 | 输出口径 | 主要拒绝原因 |
|---|---|---|---|
| 原始记录 | 指定时间窗口内的Story、commit、评审、CI和审计记录 | 可关联到任务ID的原始工程事件 | 非真实开发场景、缺少任务ID |
| 候选样本 | 已关联任务边界的工程事件 | 初步生成的instruction/input/output样本 | instruction/input/output 不完整 |
| 质量门控 | 初步样本 | 通过自动规则或进入待补全队列的样本 | 分数不足、缺少测试证据 |
| 脱敏扫描 | 通过质量门控的样本 | 不含敏感信息、许可证边界清楚的样本 | 凭据、密钥、客户标识无法安全处理 |
| 最终样本池 | 通过脱敏和复核的样本 | 可导出到训练集、验证集或知识库的样本 | 只保留 accepted 和可解释 downgraded 样本 |
这里最重要的不是最终保留率,而是拒绝原因是否稳定。如果每次构建都因为“缺少source_path”拒绝大量样本,问题不在训练,而在工程记录没有建立可追溯契约。
质量门控建议分为硬门控、软门控和人工门控。硬门控负责安全与可追溯,失败就直接拒绝;软门控负责质量与可学习性,失败可以降级到待补全;人工门控负责少量高价值、边界复杂、自动规则难以判断的样本。这样做的好处是让数据治理变成可调系统,而不是凭经验拍脑袋。
| 门控类型 | 判断问题 | 典型规则 | 输出状态 |
|---|---|---|---|
| 硬门控 | 这条样本能不能进入任何训练流程 | 含密钥、许可证不明、无来源、无验收证据 | rejected |
| 软门控 | 这条样本是否足够清楚 | 缺解释、缺边界条件、样本过长、任务重复 | downgraded / needs_curation |
| 人工门控 | 自动判断是否可靠 | 架构取舍、安全边界、跨模块副作用 | accepted / rejected / knowledge_only |
门控输出应写入样本元数据,而不是只写在日志里。例如quality.acceptance_decision表示最终处置,quality.reject_reasons记录拒绝原因,quality.review_required表示是否需要人工复核。训练导出时只读取明确通过的样本,知识库构建可以读取knowledge_only,复盘仪表盘可以读取rejected。同一批工程资产因此可以被多种下游系统使用,但不会互相污染。
Step 3:按任务边界切分数据
代码任务最容易发生数据泄漏。同一Story下的prompt、diff、review comment、audit finding和修复commit,如果同时出现在训练集和测试集中,评估结果会被严重高估。
建议优先使用以下切分策略:
| 切分键 | 适用场景 | 风险 |
|---|---|---|
story_id | 多数功能开发、缺陷修复任务 | 同一模块相似任务仍可能泄漏 |
repo_id + module_id | 多仓库、多模块平台 | 测试集更难,但更接近真实泛化 |
time_window | 连续迭代数据 | 容易受版本变化影响,需要记录模型训练时间 |
评估集必须冻结。训练过程中的调参、prompt改写和样本补充,都不能反向污染已经冻结的测试集。
Story级切分通常是第一优先级,因为一个Story天然包含需求、实现、审查和验收的闭环。如果把同一个Story中的“失败版本”放进训练集,又把“修复版本”放进测试集,模型并不是真的泛化了,只是见过同一问题的兄弟样本。更严格的团队可以采用模块级或仓库级切分:训练集不包含某个模块,测试集专门考察该模块。这种评估更难看,但更接近真实上线后的问题分布。
切分策略也要写入数据版本,而不是临时参数。推荐为每个数据版本维护split-manifest.json:记录每个story_id属于train、validation还是test,记录生成时间、生成脚本版本、随机种子和排除规则。后续补充样本时,已经进入测试集的Story不能被拆回训练集;如果确实要重切分,应创建新的数据大版本,而不是覆盖旧版本。
还要警惕语义近重复。两个Story不同,不代表任务不相似;两个commit不同,不代表修复模式不同。比如同一个鉴权漏洞在多个接口重复修复,如果一部分进入训练集、一部分进入测试集,评估仍然会偏乐观。比较稳妥的做法是给样本生成task_fingerprint,综合错误类型、模块路径、API形态、测试断言和修复模式,发现高度相似任务时按簇整体切分。
Step 4:导出为目标训练格式
格式导出只解决“能不能被训练框架读取”,不解决“样本是否值得训练”。OpenAI fine-tuning通常要求按对话消息组织JSONL;Hugging Face TRL的SFTTrainer也需要明确数据集字段和chat template。
await exportDataset({
input: './sft-training/bugfix-v1.0.jsonl',
output: './sft-training/openai-format/',
format: 'openai_chat',
filter: sample => sample.quality.acceptance_decision === 'accepted'
});
await exportDataset({
input: './sft-training/bugfix-v1.0.jsonl',
output: './sft-training/hf-format/',
format: 'hf_conversational',
splits: ['train', 'validation', 'test']
});
OpenAI风格的Chat JSONL通常适合保留system、user、assistant三段语义:system放团队编码规范和角色边界,user放任务、约束、上下文和失败代码,assistant放修复后的回答、代码或解释。Hugging Face TRL的训练入口更强调数据字段和chat template的一致性,团队需要明确最终是用messages字段、prompt/completion字段,还是工具调用格式。不同格式之间可以从同一个Canonical样本导出,但不能让导出格式反过来决定样本语义。
导出文件应和审计文件一起发布为一个数据版本,而不是只保存训练框架需要的JSONL。一个较完整的数据版本目录可以这样组织:
sft-training/bugfix-v1.0/
dataset-card.md
train.jsonl
validation.jsonl
test.jsonl
openai-chat/train.jsonl
hf-conversational/train.jsonl
manifests/split-manifest.json
manifests/lineage-map.json
reports/extraction-summary.json
reports/rejection-report.json
reports/redaction-report.json
dataset-card.md的作用类似数据集说明书。它要写清数据窗口、来源范围、许可证边界、脱敏策略、切分策略、样本字段、已知偏差和不适用场景。团队内部训练时,这份说明比单个JSONL更重要,因为它决定了模型输出异常时能不能追溯问题。没有数据集说明书,训练文件只是一次性脚本产物;有了说明书,数据版本才是可复用的工程资产。
Step 5:训练前先定义评估报告
训练脚本可以很短,评估报告不能很短。一个合格的训练前评估报告至少要写清楚基础模型、数据版本、随机种子、训练配置、评估集规模、成功判定和人工复核方式。注意这里说的是训练前报告,不是训练后宣传稿。它的价值在于提前冻结口径,防止训练完成后再挑选有利指标。
## 训练与评估报告模板
- 基础模型:<模型ID、模型卡、许可证>
- 数据版本:<dataset version / git hash>
- 训练方式:<SFT / LoRA / QLoRA>
- 训练配置:<epoch、batch size、learning rate、seed>
- 评估集:<冻结时间、样本量、切分键>
- 成功定义:<测试通过 / 静态扫描 / 人工评分>
- 复核方式:<单人审核 / 双人审核 / 分歧仲裁>
- 失败样本分析:<按错误类型聚合>
训练前评估报告至少应回答四个判断问题。第一,基线是什么:同一个提示词、同一个评估集、同一个判定标准下,未微调模型表现如何。第二,目标是什么:是提高代码风格一致性、减少低级bug、增强特定框架熟悉度,还是缩短人工复核时间。第三,失败如何记录:不只看总分,还要按错误类型拆分,例如API误用、边界条件缺失、安全约束遗漏、解释不充分、测试不可运行。第四,什么情况下停止:如果验证集改善但冻结测试集退化,或者自动指标改善但人工审查发现安全问题,就不能把模型推进到生产试用。
公开资料可以作为报告格式参考,而不是直接替代团队自己的评估。OpenAI在GPT-4o fine-tuning发布中披露了Cosine和Distyl等案例的公开指标,这类材料适合用来学习“公开案例如何交代任务、模型和评估结果”,不能被改写成自己团队的训练收益。OpenAI Cookbook的model distillation示例展示了如何围绕一个可复现实验组织数据、训练和评估;vision fine-tuning示例也展示了样本规模、训练集和验证集在示例报告中的位置。Hugging Face的TRL SFTTrainer和PEFT LoRA文档则适合用来校准训练入口和参数含义。
引用这些公开案例时,要把它们放在“外部参考”位置,而不是“本项目结果”位置。可以写:“公开资料显示,OpenAI在某些fine-tuning案例中披露了任务指标,因此团队内部报告也应提供可复核的评估口径。”不能写:“我们的模型也取得了类似提升。”除非团队真的完成了同样口径的训练、评估、复核和记录,否则任何数字都只能是公开来源的原文信息,不能迁移成内部结论。
最终,一个健康的闭环应当是:自动化提取负责规模,质量门控负责边界,Story级切分负责评估隔离,格式导出负责训练兼容,训练前报告负责冻结口径,公开案例负责校准透明度。这样写出来的流程不会制造虚假的成功叙事,但能让团队知道下一步该做什么、做到什么程度才有资格讨论训练收益。
结语:先把训练样本做对,再谈训练规模
把工程协作数据转成 SFT 样本,真正困难的部分从来不是“导出什么格式”,而是“哪些样本值得训练、哪些样本必须拒绝”。这背后需要任务契约、错误分类、人工反馈、验证证据和治理门控共同生效。
本文给出的核心框架可以归纳为三点:
- 先建数据契约,再建数据量。样本必须可追溯、可验证、可解释,才能进入训练池。
- 先做质量门控,再做自动化。没有硬门控和软门控,自动化只会放大噪音。
- 先守住评估隔离,再追求指标提升。train/eval 混淆会让“模型进步”变成统计幻觉。
如果团队刚起步,最实用的顺序仍然是:先从小样本做通完整流程,再逐步扩展规模;先提升样本硬度,再提升训练频率。
这一篇完成的是“从交付轨迹到可训练样本”的中段工程。系列最后一篇会回到更长期的问题:当这套闭环稳定运行后,组织如何判断未来 AI 编程评估和协作范式的演进方向,避免把今天的流程当成明天的上限。
参考与致谢
- Training language models to follow instructions with human feedback — Ouyang et al., OpenAI。本文关于SFT和人类反馈的基本概念,参考了这篇InstructGPT论文。
- The Flan Collection: Designing Data and Methods for Effective Instruction Tuning — Longpre et al., Google。本文关于任务多样性、指令数据构造和数据方法设计的讨论,参考了Flan Collection。
- Supervised fine-tuning — OpenAI。本文关于fine-tuning数据格式、训练前评估和数据准备的实践建议,参考了OpenAI官方文档。
- Fine-tuning now available for GPT-4o — OpenAI。文中的公开案例说明,参考了OpenAI披露的Cosine、Distyl等fine-tuning案例。
- Leveraging model distillation to fine-tune a model — OpenAI Cookbook。本文关于训练报告如何呈现样本量、验证集和评估结果,参考了该示例。
- Vision Fine-tuning on GPT-4o for Visual Question Answering — OpenAI Cookbook。本文关于公开示例如何记录训练/验证数据规模,参考了该多模态fine-tuning示例。
- TRL SFTTrainer — Hugging Face。本文关于Hugging Face SFT训练入口、数据集字段和chat template的说明,参考了TRL文档。
- PEFT LoRA — Hugging Face。本文关于LoRA配置项和参数高效微调的示例,参考了PEFT文档。
- BMAD-Speckit-SDD-Flow、BMAD-METHOD 与 Spec Kit — 本文中的BMAD-Speckit-SDD-Flow有明确的GitHub仓库,不是纯方法论拼接。工程读者应以该仓库、BMAD Method、GitHub Spec Kit和自己团队的真实流程为准,不应把示例输出理解为固定模板。
Series context
你正在阅读:AI Coding Mentor 系列
当前为第 8 / 9 篇。阅读进度只写入此浏览器的 localStorage,用于回到系列页时定位继续阅读入口。
Series Path
当前系列章节
点击章节会在此浏览器记录本地阅读进度;刷新后可继续阅读。
- 为什么你需要给AI当Coding Mentor? 当AI编程助手成为标配,真正的竞争力不再是会不会使用AI,而是能不能判断、校准和约束AI的工程输出。本文从信任缺口、反馈协议、评估标准和能力闭环出发,建立“人类作为Coding Mentor”的核心框架。
- AI编程能力评估全景:从HumanEval到SWE-bench,基准测试的演进与选择 公开基准不是模型排行榜的装饰,而是理解AI编程能力边界的测量工具。本文从HumanEval、APPS、CodeContests、SWE-bench、LiveCodeBench和Aider等基准出发,说明如何读榜、如何选择基准,以及如何把公开评估转化为团队自己的Coding Mentor评估体系。
- 如何设计高质量的编程题目:从题面到评估契约 高质量编程题不是更长的 prompt,而是能稳定暴露能力边界的评估契约。本文从 Bloom 层级、难度校准、任务契约、测试设计和题库治理出发,说明如何为 AI Coding Mentor 构建可复现的题目体系。
- AI能力评估四步法:从一次测试到持续评估系统 给AI当Coding Mentor不是做一次模型测评,而是建立一套能持续暴露能力边界、记录失败证据、驱动专项改进和支撑协作决策的评估运营系统。
- 与AI协作的最佳实践:任务协议、对话控制与反馈闭环 给AI当Coding Mentor的核心技能不是写更长的提示词,而是设计任务协议、控制对话节奏、识别错误模式,并把协作过程沉淀为可验证、可复用的反馈信号。
- 实战案例:反馈协议、评估闭环、代码审查与编程教育数据 案例研究不应该停留在“如何更会用AI工具”。本文用模型选型评估、反馈协议设计、代码审查信号沉淀和编程教育数据闭环四个工程场景,说明人类如何把AI协作过程转化为可评估、可训练、可复用的导师信号。
- 从交付到训练:如何把AI编程协作变成Coding Mentor数据闭环 AI编程助手真正的组织价值,不只是提高交付速度,而是在每一次需求拆解、代码生成、评审修正、测试验证和上线复盘中沉淀可训练、可评估、可复用的导师信号。本文重构AI训练、AI辅助产品工程化交付、高质量SFT数据沉淀与模型评估的闭环框架。
- 从工程实战到训练数据:AI工程化自动产出SFT数据的系统化方法 承接第7篇的数据闭环,本文聚焦如何将已筛选的工程资产加工为高质量SFT样本,并接入可治理、可评估、可迭代的训练流水线。
- 未来展望:AI编程评估的演进趋势与长期思考 作为系列收官篇,本文以工程决策视角重构 AI Coding Mentor 的未来路线:评估对象如何演进、组织能力如何分层、治理边界如何前置。
Reading path
继续沿这条专题路径阅读
按推荐顺序继续阅读 AI 编程评估 相关内容,而不是只看同专题的随机文章。
Next step
继续深入这个专题
如果这篇内容对你有帮助,下一步可以回到专题页继续系统阅读,或者订阅后续更新。
正在加载评论...
评论与讨论
使用 GitHub 账号登录参与讨论,评论将同步至 GitHub Discussions