ライフサイクルコールバック
バックエンドからのストリーム中に Arkor が発火する 5 つのコールバック。
コールバックは createTrainer({ callbacks: { ... } }) の下に渡します。バックエンドの SSE イベントストリームから trainer.wait() 内でディスパッチされて動きます。
シグネチャー
interface TrainerCallbacks {
onStarted: (ctx: { job: TrainingJob }) => unknown | Promise<unknown>;
onLog: (ctx: TrainingLogContext) => unknown | Promise<unknown>;
onCheckpoint: (ctx: CheckpointContext) => unknown | Promise<unknown>;
onCompleted: (ctx: { job: TrainingJob; artifacts: unknown[] }) => unknown | Promise<unknown>;
onFailed: (ctx: { job: TrainingJob; error: string }) => unknown | Promise<unknown>;
}interface 自体は 5 プロパティすべて必須ですが、createTrainer の callbacks フィールドは Partial<TrainerCallbacks> 型なので、必要なイベントだけ指定すれば OK です。コールバックの戻り値は破棄されます(unknown | Promise<unknown> という戻り値型は、コールバックから値を return しても TypeScript が文句を言わない、というだけの意味です)。
それぞれのコールバックがいつ発火するか
trainer.start() ジョブを投入し { jobId } を返す。コールバックはまだ。
│
▼
trainer.wait() SSE ストリームを開く。コールバックはここから発火。
│
▼
onStarted 1 回。`training.started` イベント時
onLog 多数回。メトリックスフレームごとに 1 回
onCheckpoint 数回。チェックポイントアップロードごとに 1 回
onCompleted 1 回。`training.completed` 時
── または ──
onFailed 1 回。`training.failed`(バックエンド報告の失敗)時start() を呼んで wait() を呼ばないと、コールバックは決して動きません。arkor start は両方呼んでくれます。プログラムから呼び出す側も同じことをしてください。
パラメーター
onStarted({ job })
SSE ストリームが training.started を報告したときに発火します。ログ行や「学習開始しました」通知に使ってください。
onStarted: ({ job }) => {
// job: TrainingJob (id, name, status, config, ...)
}onLog({ step, loss, evalLoss, learningRate, epoch, samplesPerSecond, job })
学習が進むにつれて繰り返し発火します。各数値フィールドは number | null: バックエンドはあるステップで持っているフィールドだけ埋めます(なので evalLoss は eval 以外のステップで null、learningRate は LR スケジューラ更新の合間に null、など)。
onLog: ({ step, loss, evalLoss }) => {
if (loss !== null) {
forwardToMetrics({ step, loss, evalLoss });
}
}よくある用途は次のとおりです。自前のパイプライン(PostHog、Datadog)へのメトリックス転送、早期発散の検知、カスタム Early Stopping(指標が悪化したら学習を自動で打ち切るパターン。詳しくは Early Stopping レシピ を参照)の実装。Early Stopping を実装する際は、abortSignal の Abort がローカルの wait() を止めるだけである点に注意してください。バックエンドの GPU を実際に止めるには、その後 trainer.cancel() を呼ぶ必要があります。
onCheckpoint({ step, adapter, job, infer, artifacts })
学習中にバックエンドでアダプターチェックポイントが保存されたときに発火します。adapter は { kind: "checkpoint", jobId, step }。infer の詳細は infer ページにあります。要するにチャット形式のリクエストを取り、生の Response を返す関数です。
onCheckpoint: async ({ step, infer }) => {
const res = await infer({
messages: [{ role: "user", content: "Can't log in" }],
});
const sample = await res.text();
// モデルが順調かどうか判定
}TypeScript でファインチューニングする意義の多くはここにあります。学習途中のモデルに対して、学習完了を待たずに用意したプロンプトを試せます。
onCompleted({ job, artifacts })
成功時に 1 回発火。artifacts は unknown[]: バックエンドが送った生の artifact リスト。スキーマは進化するため SDK で絞り込みません。
onCompleted: ({ job, artifacts }) => {
saveAdapterId({ jobId: job.id, count: artifacts.length });
}onFailed({ job, error })
バックエンドからの失敗報告で 1 回発火。error は string(バックエンドが送ったメッセージ)であり、Error インスタンスではありません。
onFailed: ({ job, error }) => {
// error: string
}onFailed は バックエンド側の 失敗専用です。他のコールバック内で投げられた例外は onFailed には届きません。何が起きるかは下記の 例外ハンドリング を参照してください。
振る舞い
順序
各コールバックは次のイベントがディスパッチされる前に await されます。Promise を返してよく(DB への書き込み、Slack への投稿、infer の呼び出しなど)、SDK は次のフレーム処理前にそれを待ちます。同一トレーナーに対する並行ディスパッチはありません。
例外ハンドリング
コールバック内で throw すると、その場でエラーとともに wait() が reject します。SDK はコールバックの失敗とトランスポートの失敗を区別します。throw されたコールバックは SSE 再接続ハンドラーには ルートされない ので、その失敗を黙って飛ばして続行することはありません。
これが重要なのは、コールバックが実行される時点で、その失敗イベントの Last-Event-ID は既に進んでいるためです。もし throw をリトライしてしまうと、再接続はそのイベントの 後 から再開してイベントをスキップし、エラーを握り潰し、(終端の training.completed イベントの場合は)まるで何も生成しなかったかのように空の artifacts で wait() を解決してしまいます。代わりに reject することで、対処できる場所に失敗が表面化します。
再接続ハンドラーは純粋なトランスポート失敗(接続断や一時的な 5xx)専用です。それらは指数バックオフでリトライされますが、throw されたコールバックはリトライされません。
決定的かつ非致命的なエラーハンドリングが必要なら、コールバック内で catch してください(下の 2 つ目の例を参照)。そうすれば、回復可能な副作用の失敗(不安定な Slack 投稿、メトリクス書き込みなど)で実行全体が中断されることはありません。
例
最小例: すべてのイベントをログに出す。
const trainer = createTrainer({
name: "support-bot-v1",
model: "unsloth/gemma-4-E4B-it",
dataset: { type: "huggingface", name: "arkorlab/triage-demo" },
callbacks: {
onStarted: ({ job }) => console.log(`run ${job.id} accepted`),
onLog: ({ step, loss }) => {
if (loss !== null) console.log(`step=${step} loss=${loss.toFixed(4)}`);
},
onCheckpoint: async ({ step, infer }) => {
const res = await infer({
messages: [{ role: "user", content: "Hello" }],
});
console.log(`ckpt @ ${step}:`, await res.text());
},
onCompleted: ({ job }) => console.log(`run ${job.id} done`),
onFailed: ({ error }) => console.error(`failed: ${error}`),
},
});
await trainer.start();
await trainer.wait();コールバック内で catch して回復可能な失敗をローカルに閉じ込め、wait() が reject しないようにする:
onCheckpoint: async ({ step, infer }) => {
try {
await sendToReview({ step, sample: await (await infer({ ... })).text() });
} catch (err) {
// ログ/メトリックス/コールバックの外側で trainer.cancel() を呼んで自分で学習を失敗させるか判断
}
}型定義
interface TrainingLogContext {
step: number;
loss: number | null;
evalLoss: number | null;
learningRate: number | null;
epoch: number | null;
samplesPerSecond: number | null;
job: TrainingJob;
}
interface CheckpointContext {
step: number;
adapter: { kind: "checkpoint"; jobId: string; step: number };
job: TrainingJob;
infer: (args: InferArgs) => Promise<Response>;
artifacts?: unknown[];
}TrainingLogContext と CheckpointContext は arkor から名前付きでエクスポートされていません。自分のコードで型付きコールバックパラメーターが必要なら、インラインで同じ形を定義してください。
関連項目
createTrainer: これらのコールバックを取り付ける関数- 実行ライフサイクル: 概念的な流れ
infer:onCheckpointに渡される関数- トレーナー制御:
abortSignalとcancel() - Early Stopping レシピ