学習中の評価
チェックポイントごとに固定プロンプトで半学習済みモデルをサニティーチェックし、学習完了前に確認する。
onCheckpoint を使うと、学習がまだ進行中の状態で、自分のコードから半学習済みモデルを呼べます。バックエンドがチェックポイントをアップロードするたびに、SDK が登録された関数を呼び出し、そのチェックポイントアダプターに紐づいた infer を渡してくれます。
このレシピは固定プロンプトに対してそれを組み込み、Loss(モデルの誤差を表す指標)の曲線が異常を示すよりずっと前にリグレッションを検知できるようにします。
パターン
// src/arkor/trainer.ts
import { createTrainer } from "arkor";
const GOLDEN_PROMPT = [
{ role: "user" as const, content: "I can't log in to my account." },
];
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: {
onCheckpoint: async ({ step, infer }) => {
try {
const res = await infer({
messages: GOLDEN_PROMPT,
stream: false, // スニペットを短く保つため単一の JSON ボディで受け取る
maxTokens: 80,
});
const data = (await res.json()) as {
choices: Array<{ message: { content?: string | null } }>;
};
const sample = data.choices[0]?.message.content ?? "";
console.log(`step=${step} sample=${JSON.stringify(sample.slice(0, 80))}`);
} catch (err) {
console.error(`step=${step} infer failed:`, err);
}
},
},
});これがすぐ得られるもの:
- チェックポイントごとに短い生成サンプルを stdout に書き出し、Loss の数字と並べて見られる。
- 新しいアダプターに対して推論自体が動くことの確認(つまり推論側で気付かないうちに発生したリグレッションを学習時に捕まえられる)。
- 後で比較やアサーションを足す自然な場所。
なぜ他の場所では難しいのか
infer は 直前に保存されたチェックポイントに紐づいています({ kind: "checkpoint", jobId, step })。Studio の Playground から中間チェックポイントには到達できず、専用の CLI コマンドもありません。現状の唯一の方法は onCheckpoint の中から呼ぶことです。そのためこのレシピは事後ではなく、この場で実行する必要があります。
関数はクラウド API からの生の Response を返すので、ストリーミングやデコードの方法は自由です。上のスニペットは stream: false を渡してボディを単一の JSON ドキュメントとして受け取っています。本格的なストリーミングは SDK § infer を参照してください。
バリエーション
チェックポイントごとのサンプルをレビューキューへ送る。 コールバックの infer は保存直後のチェックポイントに固定されており、InferArgs にベースモデルへ切り替える指定もないため、このコールバック単体ではベース vs アダプター比較はできません(ベースモデル側は CloudApiClient.chat({ baseModel }) で別リクエストを発行するか、手動なら Studio Playground の Base / Adapter 切替を使ってください)。コールバック単体で自動化できるのは、チェックポイントごとに 1 サンプルを取得して後からレビューできるようにすることです:
async function generate(prompt: typeof GOLDEN_PROMPT, infer: (args: any) => Promise<Response>) {
const res = await infer({ messages: prompt, stream: false, maxTokens: 80 });
const data = (await res.json()) as {
choices: Array<{ message: { content?: string | null } }>;
};
return data.choices[0]?.message.content ?? "";
}
onCheckpoint: async ({ step, infer }) => {
const sample = await generate(GOLDEN_PROMPT, infer);
await postSampleToReviewQueue({ step, sample });
},JSON Schema で型付きフィールドを抽出。 free-form なパースを responseFormat: { type: "json_schema", json_schema: { name, schema, strict: true } } に置き換えると、data.choices[0].message.content がスキーマを満たす JSON 文字列で返ってくるので、JSON.parse 一発で型付きオブジェクトとしてそのまま分岐できます(parsed.urgency === "high"、parsed.category in VALID_LABELS など)。型付きフィールドを Early Stopping に繋ぐ完全パターンは 構造化出力と Function Calling レシピ を参照。
サンプルから Early Stopping をトリガー。 Early Stopping レシピ と組み合わせる: チェックポイント出力が参照テキストから許容を超えてドリフトしたら controller を Abort。次のチェックポイントは発火しません。
チェックポイントを Slack チャネルにレビュー用で送る。 通知レシピ と組み合わせる。各ステップのサンプルを Slack メッセージとして投稿し、学習が続いている間にレビュアーがリアクションで投票できます。
心に留めておくこと
try / catchで囲む。onCheckpointから throw すると即座にwait()が reject します。SSE 再接続ループに catch されることもリトライされることもありません(このループはトランスポート障害のみを扱います)(SDK § ライフサイクルコールバック 参照)。致命的に扱いたくないならコールバック内でエラーを catch してどうするか決める。- 推論は実際の呼び出しコストがかかる。 バックエンドはライブの学習クラスタからリクエストを返します。毎チェックポイントを叩くなら
maxTokensは控えめに。 inferは呼び出しごと、メモ化されない。 同じonCheckpoint内で 2 回呼ぶとバックエンドリクエストも 2 つ。可能なら 1 回の呼び出しでプロンプトをまとめて。