第 23 章:从 Trial 生成 SFT 数据集
一个评测 Job 跑完后,团队通常很快就会看到第二种用途:把成功轨迹整理成监督微调(Supervised Fine-Tuning,SFT)数据。此时最危险的误解是把“能够导出”当成“可以训练”。Trial 目录可能同时保存任务指令、模型原始响应、工具参数、终端输出、Verifier 日志、内部主机名乃至临时凭据;同一任务的多次尝试和同一轨迹的多个前缀样本,还可能跨越训练集与验证集。若直接上传,这些问题会从评测产物变成长期数据资产。
本章把贯穿全书的“系统配置与故障诊断 Agent Benchmark”作为数据源,建立一条可审计的路径:先从 Harbor v0.18.0 的 Trial 提取对话,再执行 Reward 门槛、失败排除、脱敏、精确去重、按任务分组切分和质量抽检,最后只把通过门禁的 JSONL 交给训练团队。所有示例数据均为教学性构造;本章没有调用真实模型、没有上传数据,也没有训练或声称提升任何模型。
读完本章,读者应当能够:
- 解释 ATIF Trial 如何变成一行或多行对话样本;
- 正确处理原始内容、上下文压缩、continuation 和 subagent 轨迹;
- 区分 Harbor 内置导出能力与需要自行实现的数据治理步骤;
- 用确定性脚本构建并验收一个不包含已知测试凭据的 SFT 数据集。
23.1 先划清边界:导出器不是训练流水线
本书继续锁定 Harbor v0.18.0,提交 527d50deb63a5d279e8c20593c18a2cbc7f61f9e;该版本把 Hugging Face datasets>=4.4.1 放在 huggingface 可选依赖中。1 Harbor 随附的 SFT 文档把 traces_utils、独立 CLI、Job 结束后导出和 sweeps 分流列为入口;这些入口最终复用同一组 trace helper。2 本章所说的“Harbor 导出”只包括以下能力:发现 Trial、读取 ATIF 轨迹、构造 OpenAI 风格的 conversations 列、可选增加 ShareGPT 列、按正负 Reward 过滤、选择全部或最后一个 episode、处理特定的 continuation/subagent 文件,并返回 datasets.Dataset;还可显式推送到 Hugging Face Hub。3
下面这些事情 不是 v0.18.0 导出器替你完成的:
- 任意 Reward 阈值,例如
reward >= 0.8; - PII、凭据、内部地址、客户数据或 Verifier 答案的脱敏;
- 精确或语义去重;
- 防泄露的 train/validation/test 分组切分;
- 许可证、授权、保留期限与删除请求管理;
- SFT 模板适配、tokenization、训练、模型评测或收益归因。
因此,工程边界应当画成下面这条单向流水线:
只读 Trial 区
-> ATIF/元数据校验
-> Harbor 对话导出(隔离暂存区)
-> Reward 与异常门禁
-> 脱敏和推理内容策略
-> 去重
-> 按任务组切分
-> 静态校验 + 受控人工抽检
-> 可发布数据区
-> 训练系统(不在本章范围内)
每一步都产生计数和拒绝原因。原始 Trial 不应因转换成功而删除;转换结果也不应覆盖原始证据。这样,后续发现脱敏规则缺陷时,可以修订策略并重新构建,而不是在一个来历不明的 JSONL 上继续打补丁。
23.2 Trial 到样本:v0.18.0 实际做了什么
23.2.1 输入不是任意日志,而是带元数据的已注册 ATIF Agent
ATIF(Agent Trajectory Interchange Format)是 Harbor 的 JSON 轨迹规范。根对象保存 agent、顺序 steps、可选指标、continuation 引用和 subagent 轨迹;step 的来源只能是 system、user 或 agent,并可携带工具调用与观察。4 但“文件看起来像 ATIF”仍不足以让 v0.18.0 的 SFT 导出器接受它。
源码中的发现逻辑把“存在 agent/ 子目录”作为 Trial 候选条件;随后 load_run_metadata() 必须向上找到 result.json,否则该 Trial 会被跳过。主轨迹还必须位于 agent/trajectory.json。5 高层导出函数会把元数据里的 Agent 名称转成 AgentName,再通过工厂取得类并检查 SUPPORTS_ATIF。换言之,本版本的实现要求名称能被 Harbor 注册表识别,未知的外部名称即使写出了 ATIF,也不能仅凭文件格式保证被该入口导出。6
注意:v0.18.0 随附 SFT 文档把能力概括为“任何产生 ATIF 的 Agent”。锁定源码还多了一层
AgentName/factory 门禁。本书按源码描述;接入 Custom Agent 时,应先做一个最小导出回归测试,不要只验证 JSON schema。
23.2.2 一条 ATIF 轨迹会产生累积前缀样本
导出器把每个未标记 is_copied_context 的 source="agent" step 当作一个 episode 的结束点。第一个样本包含到第一个 Agent step 为止的历史,第二个包含到第二个 Agent step 为止的历史,以此类推。设置 episodes="last" 时,只保留该 Trial 最后一个完成的前缀。被复制的 Agent step 不产生新的边界,但仍可能作为后续对话的上下文出现。7
这一区别直接影响数据集:all 适合研究每个中间决策,却会生成大量相互重叠的前缀;last 更接近“每个 Trial 一条完整主对话”。本章的治理示例选择 last,避免同一条长轨迹按长度被重复加权。若训练目标是单步工具决策,可以改用 all,但必须在采样权重和切分策略中显式处理前缀相关性。
ATIF system step 在导出对话时被映射成 user role;Agent step 变成 assistant。若存在 reasoning_content,导出器会把它放进 <think>...</think>,再附加消息和序列化后的 <tool_call>;上一轮 Agent 的 observation 会作为下一轮之前的 user 消息。工具定义默认既可作为列保存,也会嵌入第一个 system-derived 消息的 <tools> 块。8 这是面向训练的转换,不是 ATIF 的无损副本。调试、审计和重建仍应以原始 trajectory.json 为准。
23.2.3 success 只是“主 Reward 大于零”
导出结果的 result 来自 Job 聚合统计或 Trial 的 result.json。对于 Trial 级文件,源码优先返回 exception_info.exception_type;否则只读取 verifier_result.rewards["reward"]。多维 Reward 中其他键不会参与这一过滤。9 _trial_is_success() 尝试把结果转成浮点数,并以 > 0.0 判定成功;异常字符串判为失败,使用过滤器时缺失结果的 Trial 会被跳过。10
于是:
--filter success不等于reward == 1.0;0.01在内置过滤器看来也是成功;reward = 0属于失败,而不是“可保留的零分样本”;correctness=1, safety=0若没有名为reward的主维度,不能据此完成所需筛选;- 任意阈值和多维门禁必须在导出后自行实现。
这也是本章脚本不传 success_filter="success" 的原因:它先保留可读取的行,再用明确的 MIN_REWARD 做二次门禁,并拒绝非数值结果。
23.2.4 把导出行当作有版本的数据契约
主数据行至少包含 conversations、agent、model、model_provider、date、task、episode、run_id、trial_name 和 result;工具定义、instruction、Verifier 输出、ShareGPT 列与 trace_source 取决于参数和输入轨迹。3 不要让训练代码直接依赖“当前碰巧出现的所有列”。更稳妥的做法是先定义允许列,再显式重命名和丢弃:conversations 是训练负载,task_group 与 hash 是治理元数据,Reward 只用于筛选或离线分析;时间、明文 Trial 名和运行 ID 通常不进入公开数据。
还要验证“一个样本”的含义。导出器生成的是对话前缀,不是原始 ATIF step,也不是自动套好目标模型 chat template 的 token 序列。不同模型可能要求不同的 system role、工具调用编码、结束标记和 loss mask。应在数据治理之后、训练之前再做模型专用适配,并保留适配器版本;否则同一份 JSONL 换一个 tokenizer 就可能得到不同监督目标。这个阶段属于训练系统,不是 Harbor v0.18.0 导出器的保证。
23.3 在运行时保存可训练、可解释的轨迹
数据质量从 Trial 运行时开始,而不是从导出命令开始。Terminus-2 的 TrajectoryConfig 有两个与 SFT 直接相关的布尔项:raw_content=True 保存 LLM 的原始响应,而不是只保存解析后的 analysis/plan 与合成工具调用;linear_history=True 在上下文总结发生时把轨迹切成 continuation 文件,使每个文件代表一段实际送给模型的连续历史。两者默认均为 False。11
在 Job 配置的 Agent kwargs 中可以这样表达:
agents:
- name: terminus-2
model_name: provider/model-placeholder
kwargs:
trajectory_config:
raw_content: true
linear_history: true
这里的模型名是教学占位符,不可直接运行;配置形状已按 v0.18.0 的 AgentConfig.kwargs 与 Terminus-2 构造参数核验。12 raw_content=True 并不意味着“更安全”或“更准确”,只意味着更忠实地保留模型输出,因此更可能保存意外回显的凭据、受保护文本或显式推理内容。采集目的若不需要某类字段,就不要仅为“以后也许有用”而保留。NIST SP 800-122 把最小化 PII 的使用、收集和保留视为基本隐私原则;对轨迹数据也应采用同样的最小必要思路。13
当 linear_history=True 且发生总结时,Terminus-2 在前一段写入 continued_trajectory_ref,随后生成 trajectory.cont-1.json 等文件;复制到新段的上下文会被标记为 is_copied_context。14 v0.18.0 的导出器从 trajectory.json 沿这条引用链读取 continuation,并为 episode 重新编号。它不会把名字碰巧类似 continuation 的文件盲目拼接。
Subagent 需要单独决策。Python 入口默认 export_subagents=True 且 merge_subagents=True,会把文件型 subagent 对话与主数据合并,并增加 trace_source。文件扫描针对 trajectory.*.json,排除 .cont-;每个 subagent 文件按一条完整对话导出。15 ATIF v1.7 本身还允许根对象内的 subagent_trajectories 嵌入形式,但 v0.18.0 这一 SFT 收集路径扫描的是外部文件,并没有遍历嵌入数组。由此可以推断:不要把 ATIF schema 的所有表达能力等同于当前 SFT 导出器的消费能力。
本章先使用 --no-subagents 建立主 Agent 数据集。总结 subagent 的“摘要”“提问”“回答”是不同训练目标,若确实要用,应单独导出、单独抽检并单独记录来源,不能因为默认合并就与主任务回答混为一谈。
23.4 做一次确定性原始导出
23.4.1 安装与命令
从锁定源码目录或已固定到 v0.18.0 的项目环境运行:
uv sync --extra huggingface
uv run harbor traces export \
--path jobs/system-diagnosis-sft \
--recursive \
--episodes last \
--filter success \
--no-subagents \
--no-sharegpt \
--no-push \
--verbose
harbor traces 在 v0.18.0 的主 CLI 中被注册为 hidden command,但子命令和上述选项都存在;--push 还必须配合 --repo。16 这里刻意使用 --no-push。push_dataset() 会读取 HUGGINGFACE_TOKEN 或 HF_TOKEN 并把数据发送到指定 Hub 仓库;这应当发生在脱敏和审批之后,而不是原始导出阶段。17
命令行入口只打印行数。若要保存本地文件或继续治理,应使用 Python API:
from pathlib import Path
from harbor.utils.traces_utils import export_traces
raw = export_traces(
root=Path("jobs/system-diagnosis-sft"),
recursive=True,
episodes="last",
success_filter=None,
export_subagents=False,
include_instruction=False,
include_verifier_output=False,
embed_tools_in_conversation=False,
use_rich_progress=False,
)
print(len(raw), raw.column_names)
# 仅在隔离暂存区需要快照时:raw.to_parquet("quarantine/raw.parquet")
Dataset.to_parquet() 是 Hugging Face Datasets 提供的本地导出方法,不是 Harbor 自定义文件格式。18
include_instruction 与 include_verifier_output 默认关闭。后者会拼接 verifier/test-stdout.txt 和 test-stderr.txt;测试输出可能包含标准答案、测试路径或内部诊断信息,不应为了“元数据更完整”而默认加入训练样本。19 同样,文本导出器检测到 ATIF 消息或 observation 中的图片时会抛出 MultimodalExportError,避免静默丢图;本章脚本不处理多模态轨迹。20
本章验证时构造了两个无模型参与的 Trial:一个 reward=1.0,一个 reward=0.0,二者各有三个 ATIF step。对锁定源码调用导出器后,得到如下确定性测试结果:
success_rows=1
failure_rows=1
success_episode=episode-1
之所以最后一个 episode 是 episode-1,是因为轨迹中有两个 Agent step。这个输出不是模型评测成绩,只验证了 discovery、Reward 过滤、last 选择和 ShareGPT 转换的代码路径。
23.5 构建带泄露防护的治理脚本
下面的 build_sft.py 是本书提供的教学实现,不是 Harbor v0.18.0 内置命令。它实现八条保守策略:只取主 Agent 最后一个 episode;使用任意数值阈值;拒绝异常或无 Reward 行;移除显式 <think> 内容;替换若干已知敏感模式;只保留允许的元数据;在脱敏后做精确去重;最后按 task 分组做稳定切分。
from __future__ import annotations
import argparse
import hashlib
import json
import math
import re
from pathlib import Path
from typing import Any
from harbor.utils.traces_utils import export_traces
MIN_REWARD = 0.8 # 教学策略,不是 Harbor 默认值
POLICY_VERSION = "book-sft-policy-v1"
THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
SENSITIVE_PATTERNS = [
re.compile(r"\bsk-[A-Za-z0-9_-]{8,}\b"),
re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+"),
re.compile(r"(?i)\b(password|passwd|token|secret)\s*[:=]\s*[^\s,;]+"),
re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"),
re.compile(
r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?"
r"-----END [A-Z ]*PRIVATE KEY-----",
re.DOTALL,
),
]
def sha256_text(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def redact_text(value: str) -> str:
value = THINK_BLOCK.sub("[REDACTED_REASONING]", value)
for pattern in SENSITIVE_PATTERNS:
value = pattern.sub("[REDACTED]", value)
return value
def redact(value: Any) -> Any:
if isinstance(value, str):
return redact_text(value)
if isinstance(value, list):
return [redact(item) for item in value]
if isinstance(value, dict):
return {key: redact(item) for key, item in value.items()}
return value
def numeric_reward(row: dict[str, Any]) -> float | None:
try:
reward = float(row.get("result"))
except (TypeError, ValueError):
return None
return reward if math.isfinite(reward) else None
def validate_conversation(messages: Any) -> list[dict[str, str]]:
if not isinstance(messages, list) or not messages:
raise ValueError("empty conversation")
clean: list[dict[str, str]] = []
for message in messages:
if not isinstance(message, dict):
raise ValueError("message is not an object")
role, content = message.get("role"), message.get("content")
if role not in {"user", "assistant"}:
raise ValueError(f"unsupported role: {role!r}")
if not isinstance(content, str) or not content.strip():
raise ValueError("empty message content")
clean.append({"role": role, "content": content})
if clean[-1]["role"] != "assistant":
raise ValueError("conversation does not end with assistant")
return clean
def split_for_task(task_group: str) -> str:
# 教学用 80/10/10 稳定切分;生产比例应由实验方案决定。
bucket = int(sha256_text(task_group)[:8], 16) % 100
return "train" if bucket < 80 else "validation" if bucket < 90 else "test"
def build(root: Path, output: Path) -> dict[str, int]:
dataset = export_traces(
root=root,
recursive=True,
episodes="last",
success_filter=None, # 在下方应用明确阈值
export_subagents=False,
include_instruction=False,
include_verifier_output=False,
embed_tools_in_conversation=False,
use_rich_progress=False,
)
counts = {
"exported": len(dataset), "below_threshold": 0,
"invalid": 0, "duplicate": 0, "accepted": 0,
}
accepted: list[dict[str, Any]] = []
seen: set[str] = set()
for source in dataset:
row = dict(source)
reward = numeric_reward(row)
if reward is None or reward < MIN_REWARD:
counts["below_threshold"] += 1
continue
task = row.get("task")
if not isinstance(task, str) or not task or task == "unknown-task":
counts["invalid"] += 1
continue
try:
messages = validate_conversation(redact(row.get("conversations")))
except ValueError:
counts["invalid"] += 1
continue
canonical = json.dumps(messages, ensure_ascii=False, sort_keys=True)
fingerprint = sha256_text(canonical)
if fingerprint in seen:
counts["duplicate"] += 1
continue
seen.add(fingerprint)
provenance = "|".join(
str(row.get(key, ""))
for key in ("run_id", "task", "trial_name", "episode")
)
task_group = sha256_text(task)
accepted.append({
"conversations": messages,
"agent": str(row.get("agent", "unknown-agent")),
"model": str(row.get("model", "unknown-model")),
"reward": reward,
"task_group": task_group[:16],
"source_hash": sha256_text(provenance),
"conversation_hash": fingerprint,
"data_label": "teaching-sft-example",
})
counts["accepted"] += 1
output.mkdir(parents=True, exist_ok=True)
splits = {name: [] for name in ("train", "validation", "test")}
for row in accepted:
splits[split_for_task(row["task_group"])].append(row)
for name, rows in splits.items():
with (output / f"{name}.jsonl").open("w", encoding="utf-8") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
counts[f"split_{name}"] = len(rows)
manifest = {
"policy_version": POLICY_VERSION,
"min_reward": MIN_REWARD,
"counts": counts,
"note": "Teaching data only; no model was trained.",
}
(output / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8"
)
return counts
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("trial_root", type=Path)
parser.add_argument("output", type=Path)
args = parser.parse_args()
print(json.dumps(build(args.trial_root, args.output), sort_keys=True))
运行方式如下:
uv run python build_sft.py \
jobs/system-diagnosis-sft \
build/sft-v1
wc -l build/sft-v1/*.jsonl
python -m json.tool build/sft-v1/manifest.json
这个脚本故意不在发布行中保留 run_id、明文 trial_name、时间、provider、instruction、Verifier 输出或工具定义。source_hash 纳入 run_id、task、Trial 名与 episode;只有保留只读原始 Trial,或另存受控的 hash→来源映射表时,它才支持回溯,哈希本身不可逆。先脱敏再去重也很重要:两个只在密钥值上不同的样本,脱敏后可能完全相同,应当只保留一次。
警告:示例正则只能识别已列出的模式,不构成匿名化保证,也检测不了所有准标识符、业务机密、编码后的凭据或可从上下文重识别的信息。NIST SP 800-188 明确区分简单遮罩与完整的去标识治理,并要求在发布模型、风险目标和重识别风险之间作系统评估。21 生产流程应叠加组织专用检测器、数据所有者审批、受控人工复核和删除/重建机制。
示例也直接写入 output,因此它不是原子发布器。生产构建应拒绝复用已有版本目录,先在同一文件系统的临时同级目录生成三份 JSONL、私有来源映射和 manifest;校验“导出数 = 阈值拒绝 + 无效 + 重复 + 接收”以及“接收数 = 三个 split 之和”后,再原子切换目录名。manifest 还应记录输入 Trial 清单摘要、构建代码提交、Harbor/Agent 版本和审批状态。远端 Hub 提交是另一条发布事务,不能因为本地 rename 成功就推断上传、权限或历史删除也已完成。
23.6 去重、切分与抽检:最容易出现的三类泄露
23.6.1 行级随机切分会泄露任务
同一 task 的多次 attempt 通常共享 instruction、文件结构和 Verifier 目标;episodes="all" 还会产生同一对话的累积前缀。由这些源码行为可以推断:按行随机切分,很容易把近乎相同的输入分到训练集和验证集。本章以 task 明文的 SHA-256 作为 task_group,同一任务的所有 Trial 必然进入同一 split。若多个 task 来自同一模板或同一底层仓库,还应使用更高层的 template_id、仓库 commit 或案例族作为分组键。
哈希切分提供稳定性,不提供随机性的统计魔法。数据量很小时,某个 split 为空是正常的门禁信号,而不是把几行复制过去的理由。应当先增加独立任务,再冻结分组清单;不能为了得到“好看”的验证分数而移动已看过结果的任务。
23.6.2 精确去重抓不到语义重复
示例的 conversation_hash 只识别脱敏后字节级一致的对话。它抓不到变量名变化、日志时间戳变化、同一模板换一组端口、或同一解法的自然语言改写。更完整的流程至少分三层:规范化后的精确哈希;按 task/template 的结构去重;最后在隔离环境中做近邻或聚类抽检。近邻模型本身也可能把敏感文本发送给外部服务,因此应优先使用本地、经过审批的实现。
去重时不要只保留“第一行”而丢失来源关系。清单至少应记录策略版本、输入 Job/Trial 集合摘要、保留行的 source_hash、剔除原因和构建时间。若同一对话来自多个独立 Trial,可以把来源集合保存在受控 side table,而不是全部写入公开训练文件。
23.6.3 Verifier 与工具输出可能泄露答案
成功轨迹天然倾向于包含正确结果,这正是 SFT 的价值;但它不应包含 Agent 本来不可见的测试实现、Solution 文件或 Verifier 私有日志。如果 Agent 通过越权读取测试得分,reward=1 也不能把这条轨迹洗成高质量数据。Reward 门槛之前,应复用第 13 章的 gaming 审计:检查是否读取 /tests、写 Reward 文件、访问 Solution,或利用了任务包中的答案泄露。
人工抽检不要只抽“看起来正常”的训练行。建议从每个数据来源、Reward 区间、Agent/模型组合、长度区间、含工具调用与不含工具调用、被脱敏与未触发脱敏的样本中分别抽取;失败队列也要抽检,以判断规则是在拦截真实风险,还是误删有效数据。抽检记录只保存样本 hash、结论、问题类别和审阅者,不把敏感正文复制进工单或聊天系统。
23.7 失败模式与排查顺序
导出为零行
先用 --verbose 看 Trial 是否被发现,再依次确认 result.json、agent/trajectory.json、Agent 名称注册和 SUPPORTS_ATIF。不要先放宽 Reward 门槛;输入结构缺失与样本质量不足是两类问题。episodes="last" 也要求轨迹至少有一个未复制的 Agent step。
success 数量与预期不符
检查 result.json 的 verifier_result.rewards.reward,而不是只看 Viewer 中其他维度。记住内置 success 是 >0。若需要 >=0.8、多个维度同时达标或排除特定异常,请在治理层显式编码并把规则写入 manifest。
continuation 丢失或重复
检查每段的 continued_trajectory_ref 是否指向存在的文件,以及新段复制的 step 是否标记 is_copied_context。不要用文件名排序替代引用链。对含总结的真实 Agent,在升级 Harbor 或 Agent 版本时,应把固定 golden trajectory 导出结果纳入回归测试。
多模态轨迹抛错
这是文本导出器的保护行为。把多模态 Trial 分流到保留图片引用和媒体文件的专用训练格式;不要捕获异常后直接删除图片 part,因为那会改变模型当时实际看到的输入。
脱敏后仍发现凭据
立即阻断发布并轮换仍然有效的凭据;定位该值从 instruction、工具参数、observation、原始响应还是 Verifier 输出进入。添加规则后必须从原始 Trial 重建全部 split,因为只修补一个发布文件会留下缓存、Parquet、Hub commit 或下游副本。若数据已推送远端,还要按平台和组织流程处理历史版本,而不是只删除最新文件。
23.8 验收清单
一个 SFT 数据版本至少通过以下门禁:
- Harbor 版本、commit、Agent 版本、模型标识和构建策略版本可追溯;
- 输入 Trial 均有可读
result.json与agent/trajectory.json,ATIF 生产者已做导出回归; - Reward 规则写明主维度、阈值、异常处理和缺失值处理;
- instruction、Verifier 输出、工具定义、显式推理是否保留都有字段级决策;
- 已知凭据扫描、组织专用敏感信息扫描和人工抽检均通过;
- 去重发生在脱敏之后,并记录精确重复与结构/语义重复策略;
- split 按 task 或更高层案例族分组,跨 split 精确 hash 交集为空;
- 每行对话非空、role 合法、最后一条为
assistant,来源 hash 可回溯; - manifest 的输入数、拒绝数、重复数、接收数和各 split 数量守恒;
- 推送远端前完成权限、许可证、保留期限和删除流程审批。
只有这些条件满足后,数据才进入“可交付给训练系统”的状态。它仍不等于模型已经训练,更不等于训练会提高独立测试集表现。
23.9 本章小结
- Harbor v0.18.0 从已注册且声明 ATIF 支持的 Agent Trial 构造 Hugging Face Dataset,但不负责完整数据治理。
episodes="all"生成累积前缀;主 Agent SFT 通常先从last开始,并单独处理 subagent 数据。- 内置 success 语义是主
reward > 0,任意阈值和多维门禁必须自行实现。 raw_content与linear_history分别保留原始生成和线性调用历史,却也扩大敏感信息暴露面。- 脱敏、去重、分组切分、gaming 审计和人工抽检共同组成发布门禁。
- 导出成功只说明转换完成;本章没有训练模型,也没有产生训练收益结论。
23.10 练习
- 用两个教学 Trial 复现
episodes="all"与"last",画出每行包含的 step 前缀,并解释为什么行数不同。 - 把治理脚本的门禁改为
reward >= 0.8且safety >= 1.0。由于当前导出行只保留主result,说明你需要从何处补充并校验多维 Reward,而不是假装列已存在。 - 构造同一对话仅凭测试 API Key 不同的两行,验证“先脱敏、后去重”只保留一行;再构造一次自然语言改写,说明精确 hash 为何抓不到它。
- 为 continuation 断链、未知 Agent 名称、多模态内容和缺失 Reward 各写一个最小失败夹具,记录 v0.18.0 的实际错误或跳过行为。
- 设计一份人工抽检表,覆盖来源、Reward、长度、工具调用、脱敏命中和 gaming 风险;表中只允许保存 hash 与结论,不复制正文。