ライフサイクルコールバック
学習中に Arkor が発火する 5 つのコールバックと、それぞれが何に役立つか。
学習が進むにつれて Arkor は 5 つのコールバックを発火します。いずれも任意で、自分のプロセス内で動く普通の TypeScript 関数です。
createTrainer({
name: "support-bot-v1",
model: "unsloth/gemma-4-E4B-it",
dataset: { type: "huggingface", name: "arkorlab/triage-demo" },
callbacks: {
onStarted, onLog, onCheckpoint, onCompleted, onFailed,
},
});それぞれのコールバックがいつ発火するか
.start() ── ジョブ投入、{ jobId } を返す。コールバックはまだ。
│
▼
.wait() ── SSE イベントストリームを開く。コールバックはここから発火。
│
▼
onStarted ─── 1 回。ストリームが `training.started` を報告したとき
│
▼
onLog ─── 多数回。学習ステップのメトリックスバッチごとに 1 回
│
▼
onCheckpoint ── 数回。アダプターチェックポイントが保存されたとき
│
▼
onCompleted ── 1 回。正常終了時
or
onFailed ── 1 回。バックエンドが失敗を報告したとき5 つのコールバックはすべて wait() 内からディスパッチされます。start() を呼んで wait() を呼ばないと、学習はバックエンドで進行していてもコールバックは 一切発火しません。arkor start は代わりに wait() を呼びます。CLI の外側で学習を自分のコードに組み込む場合は、自分でも呼ぶようにしてください。
onCompleted と onFailed は排他で、学習 1 回あたり最大 1 つしか発火しません。終端イベント到達前に wait() が throw した場合(例えば abortSignal が Abort された、再接続試行が尽きた)、どちらも発火しないことがあります。
どのコールバックも Promise を返して構いません。Arkor は次に進む前に await するので、非同期処理(DB への書き込み、Slack への投稿、infer の呼び出し)を競合なくこなせます。
onStarted({ job })
wait() が開いた SSE ストリームが training.started イベントを報告したときに 1 回発火します。これは start() の resolve のタイミングと同じではありません。start() はジョブを投入して jobId を返すだけです。
onStarted: ({ job }) => {
console.log(`Run ${job.id} accepted`);
},ログ行、メトリックスカウンター、「学習開始しました」の通知に使ってください。
onLog({ step, loss, evalLoss, learningRate, epoch, samplesPerSecond, job })
学習が進むにつれて繰り返し発火します。各数値フィールドはバックエンドがまだそのメトリックスを出していなければ null になり得ます(例えば evalLoss は eval ステップのときだけ)。
onLog: ({ step, loss, evalLoss }) => {
if (loss !== null) {
console.log(`step=${step} loss=${loss.toFixed(4)} evalLoss=${evalLoss ?? "-"}`);
}
},よくある用途:
- 自前のメトリックスパイプライン(PostHog、Datadog など)に転送。
- 早期発散の検知:
lossが増えていれば、abortSignalでwait()を Abort し、trainer.cancel()でバックエンドの GPU を止める。 - カスタム Early Stopping(指標が悪化したら学習を自動で打ち切るパターン。詳しくは Early Stopping レシピ を参照)を実装。
onCheckpoint({ step, adapter, job, infer, artifacts })
学習中にバックエンドでチェックポイントが保存されたタイミングで発火します。
onCheckpoint: async ({ step, infer }) => {
const res = await infer({
messages: [{ role: "user", content: "Can't log in" }],
});
console.log(`step=${step} sample=`, await res.text());
},adapter はチェックポイントを識別するコンパクトなオブジェクトです({ kind: "checkpoint", jobId, step })。infer は関数で、チャット形式のリクエストを受け取り、生の Response を返します。await res.text()(または res.json()、ストリーム処理)で読み取ります。
実用上もっとも価値のあるコールバックです。学習の最後まで待たずに、途中でモデルを動作確認できます。チェックポイントが既にベースモデルより悪ければ、止めるべきだとわかります。
onCompleted({ job, artifacts })
成功時に 1 回発火します。artifacts はこの学習でバックエンドが生成した成果物のリストです。用途:
- 最終アダプター ID をアプリの他の部分から見つけられる場所に保存。
- モデルを昇格させる前に最終スモークテスト。
- 「学習完了」通知の送信。
onCompleted: ({ job, artifacts }) => {
console.log(`Run ${job.id} done, ${artifacts.length} artifacts`);
},onFailed({ job, error })
バックエンドが失敗を報告した場合に 1 回発火します。error は string(バックエンドが送ってきたメッセージ)であって Error インスタンスではないことに注意:
onFailed: ({ job, error }) => {
console.error(`Run ${job.id} failed: ${error}`);
},onFailed はバックエンドからの失敗報告専用です。コールバック内で throw した例外は onFailed を経由せず、あなたのエラーで wait() を即座に reject します。throw されたコールバックは SSE のトランスポート失敗としては扱われないため、再接続ループに送られたりリトライされたりしません。再接続ループが扱うのは一過性のストリーム障害(接続断、オープン時の 404/408/429/5xx のようなリトライ可能な HTTP エラー)だけです。401/403/410/426 のような恒久的ステータスは即座に reject します(トレーナー制御の再接続 参照)。これは意図的な挙動です。失敗したイベントの Last-Event-ID は既に進んでいるため、リトライするとそのイベントをスキップし、あなたのエラーを握りつぶし、(終端の training.completed イベントの場合は)空の artifacts で wait() を解決してしまうからです。致命的でない処理が必要なら、コールバック内で catch して、外に伝播する前にどうするか(ログ、Abort、永続化)を決めてください。
完成例
import { createTrainer } from "arkor";
export const trainer = createTrainer({
name: "support-bot-v1",
model: "unsloth/gemma-4-E4B-it",
dataset: { type: "huggingface", name: "arkorlab/triage-demo" },
lora: { r: 16, alpha: 16 },
maxSteps: 100,
callbacks: {
onStarted: ({ job }) => console.log(`started ${job.id}`),
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(`done ${job.id}`),
onFailed: ({ error }) => console.error(`failed: ${error}`),
},
});コールバックを理解してしまえば、Arkor の残りの大部分はその上に積み重なる設定にすぎません。