クリーム色の背景、色付き表示、高コントラスト、大きな文字のほうが読みやすいですか?配色と文字を選ぶと、すべてのページに一度で反映されます。

無料レッスン

遅いモデル呼び出しとキュー

このレッスンのあとで: 私は、遅い呼び出しをリクエストから外し、その進み具合を知らせられます。

65分のレッスンコマンド7件AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

なぜ遅い呼び出しをジョブにするのか

ジョブキューは、遅い仕事を別の場所で進め、リクエストにはすぐ返事をする仕組みです。モデル呼び出しは数秒かかることがあり、待たせ続ける Web リクエストは、途中で切れたり、ほかの処理を塞いだりします。

ユーザーが四十ページの要約を頼みます。サーバーは数ミリ秒で「受け付けました、job-17」と返します。要約ができるまで、ページは状態を尋ね続けます。

このレッスンのゴール

メモ

私は、遅い呼び出しをリクエストから外し、その進み具合を知らせられます。

キーワード

ことばいみれい
ジョブ状態を持つ、時間のかかる仕事の単位
キューワーカーを待つジョブの列
ワーカージョブを取り出して実行するコード
冪等キー繰り返しても害がないようにする、クライアントの札order-1
バックオフ失敗するたびに待つ時間を延ばすこと

学生が入力するセルは、このプレビューでは空白です。

ジョブの四つの状態

ジョブは、待機、実行中、そして完了か失敗のどれかです。再試行する失敗のあとは、待機に戻ります。ジョブは、試行の回数と結果を持ち続けます。

クライアントは、すぐにジョブ ID をもらいます。その ID で状態を尋ね、状態が完了になってから結果を読みます。

例: job-2 だけが一度失敗する

段階状態試行時計(job-2 だけ)
受け付け待機00 秒
一回目の試行が始まる実行中12 秒
上流でタイムアウト待機17 秒
二回目の試行が始まる実行中29 秒
結果を保存完了29 秒

学生が入力するセルは、このプレビューでは空白です。

冪等キーで二度実行を防ぐ

ネットワークはリクエストを繰り返します。クライアントが送信を二度押すことも、プロキシが再送することもあります。守りがないと、コピーごとにジョブができ、仕事も請求も倍になります。

クライアントは冪等キーを送ります。キューは、キーからジョブ ID への表を持ちます。見たことのあるキーには、今あるジョブを返し、新しいジョブは作りません。

再試行には上限と間隔を

試行の最大回数を決めます。それを超えたジョブは失敗とし、次は人か規則が決めます。失敗のたびに待つ時間を延ばすと、困っているサービスに余裕ができます。

完了したジョブは、巡回が二度訪れても、二度と実行しません。完了したジョブは結果であり、依頼ではないからです。

よくある間違い

注意

再試行はいつも安全だ、と考える人は多いです。安全なのは、仕事が冪等か、キューが覚えているときだけです。支払い済みで終わった呼び出しを再試行すると、また払うことになります。実行の前に、毎回状態を確かめます。

モデル用の補助を書く(1)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    return (await res.json()).choices[0].message.content;' > model.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

モデル用の補助を書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  } catch (error) {' '    console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' '    const list = [].concat(sample);' '    const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' '    return list[Math.min(turn, list.length - 1)];' '  }' '}' 'module.exports = { ask };' >> model.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

キューを書く(1)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const jobs = {}; // ジョブ ID からジョブへ' 'const byKey = {}; // 冪等キーからジョブ ID へ' 'let clock = 0; // 作り物の時計(秒)' 'let next = 1;' 'function submit(key, payload) {' '  if (key in byKey) return byKey[key];' '  const id = \"job-\" + next++;' '  jobs[id] = { id: id, payload: payload, state: \"待機\", tries: 0, result: null };' '  byKey[key] = id;' '  return id;' '}' > queue.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

キューを書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const failFirst = { \"job-2\": 1 }; // job-2 は一度失敗し、次は成功する' 'function work(job) {' '  if (job.tries <= (failFirst[job.id] || 0)) throw new Error(\"上流でタイムアウト\");' '  return job.payload.toUpperCase();' '}' 'function runOne(job) {' '  if (job.state === \"完了\" || job.state === \"失敗\") return;' '  while (job.tries < 3) {' '    job.tries++;' '    job.state = \"実行中\";' '    clock += 2;' '    try { job.result = work(job); job.state = \"完了\"; return; }' '    catch (error) { job.state = \"待機\"; clock += job.tries * 5; }' '  }' '  job.state = \"失敗\";' '}' >> queue.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

キューを書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'submit(\"order-1\", \"invoice a\");' 'submit(\"order-2\", \"invoice b\");' 'submit(\"order-1\", \"invoice a\"); // クライアントの再送: 同じキーなので同じジョブ' 'Object.values(jobs).forEach(runOne);' 'Object.values(jobs).forEach(runOne); // 二周目は何も実行しない' 'for (const id in jobs) console.log(id, jobs[id].state, \"試行\", jobs[id].tries, jobs[id].result);' 'console.log(\"ジョブ数:\", Object.keys(jobs).length, \"時計:\", clock + \" 秒\");' >> queue.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

キューを書く(4)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const sample = \"二件の請求書の準備ができました。\";' '  const text = \"二件のジョブが終わりました。お客様向けの短い状況の一文を書いてください。\";' '  console.log(\"本物の呼び出し:\", await ask([{ role: \"user\", content: text }], sample, 60));' '}' 'main();' >> queue.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

キューを動かす

コマンド
docker exec -w /home/node/project agent node queue.js

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

本物ではなく代用品です

ここでのキューはメモリ上のオブジェクトで、時計はただの数です。プロセスが止まると、ジョブはすべて消えます。本物のシステムは、そのための専用サービスにジョブを置き、ワーカーは別のプロセスで動きます。

考え方は同じです。状態、試行の回数、キーの表、再試行の上限です。

出力の一例

行何を示すか
job-1 完了 試行 1 INVOICE A一回で成功したジョブ
job-2 完了 試行 2 INVOICE B一度失敗してから成功したジョブ
ジョブ数: 2 時計: 11 秒三回の受け付けで、ジョブは二つ
本物の呼び出し短い状況の一文。または、ラベル付きの記録済みサンプル

学生が入力するセルは、このプレビューでは空白です。

このラボとソースについて: AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

授業で使いますか?このモジュールを学生に割り当てると、教員用ガイド、自動採点つきの課題、学生ごとのラボを使えます。