モデルが失敗しても動き続ける
代替手段とは、モデルの呼び出しが失敗したときに機能が行う別の対応で、ユーザーに何かを返すためのものです。呼び出しは何通りもの形で失敗するので、すべてに同じ対応をするのは、たいていの失敗にとって誤りです。
タイムアウトには再試行が向いています。断りには向きません。一日の予算が尽きたときは、キャッシュした答えか、正直なメッセージが要ります。失敗の種類ごとに選びましょう。
このレッスンのゴール
メモ
私は、失敗の種類ごとに代替手段を選び、モデル呼び出しが失敗しても機能を動かし続けられます。
キーワード
| ことば | いみ |
|---|---|
| 代替手段 | 呼び出しが失敗したときに、代わりに行うこと |
| 再試行 | 待ってから、同じモデルにもう一度尋ねること |
| 二つ目のモデル | 最初のモデルが失敗したときに尋ねる別のモデル |
| 定型の返答 | モデルを使わない、固定で正直なメッセージ |
| 縮退 | 何も返さない代わりに、小さな答えを返すこと |
| 失敗の種類 | 呼び出しが失敗した理由 |
学生が入力するセルは、このプレビューでは空白です。
代替手段は順番に並べた鎖
選択肢を、費用の小さいものから、縮退の大きいものへ並べます。最初は再試行で、少し待ってから行います。次に二つ目のモデル。次に、古いかもしれないキャッシュの答え。最後に定型の返答で、固定の正しいメッセージを返し、ユーザーがすべきことを伝えます。
どの段階にも時間とお金がかかります。失敗のたびにすべてを試す鎖は、障害のときに請求額を倍にしてしまいます。
例: どの代替手段が合うか
| 失敗 | 最初に | 理由 |
|---|---|---|
| タイムアウト、レート制限 | 一回だけ再試行 | ひとりでに通ることが多い |
| サーバーのエラー | 二つ目のモデル | 最初のモデルが止まっている |
| 形式の誤り | 一回だけ再試行 | 二回目で通ることが多い |
| 断り | 定型の返答 | もう一度尋ねても変わらない |
| 予算切れ | キャッシュの答え | 呼び出し自体が許されない |
学生が入力するセルは、このプレビューでは空白です。
鎖に上限を付け、それを伝える
どの段階にも上限を付けます。再試行は一回、二つ目のモデルも一回です。試行の間は待ち、その待ち時間はユーザーのタイムアウトに含めます。代替手段が失敗を隠してはいけません。どの段階が答えたかを記録します。
キャッシュの答えは古いかもしれないので、重要なら、そう伝えます。定型の返答は、モデルの答えのふりをしてはいけません。
よくある間違い
注意
「再試行がいつも一番安全な代替手段だ」と考える人が多くいます。しかし、断りを再試行するとお金が無駄になり、障害中の再試行は負荷を増やします。ひとりでに通りうる失敗だけを、一回だけ再試行しましょう。
モデル用の助っ人を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// 応答に含まれるトークン数。なければ null。' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用の助っ人を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' '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 || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
代替手段の鎖を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'let now = 0; // ミリ秒。自分で進める時計' 'const store = new Map([[\"営業時間\", \"営業は午前九時から午後五時までです。\"]]);' 'const PLAIN = \"ただいま、アシスタントは答えられません。しばらくしてからもう一度お試しください。\";' '// 記録された失敗が、本物の失敗の代わりになる。ここでは本物のエラーは起こさない。' 'function fail(kind) { throw new Error(kind); }' > fallback5.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
代替手段の鎖を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function secondModel() {' ' const messages = [{ role: \"user\", content: \"営業時間は午前九時から午後五時です。一文で伝えてください。\" }];' ' return (await ask(messages, \"営業は午前九時から午後五時までです。\", 40)).text;' '}' >> fallback5.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
代替手段の鎖を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function answer(question, second) {' ' const steps = [' ' [\"最初のモデル\", function () { fail(\"rate_limit\"); }],' ' [\"再試行\", function () { now = now + 200; fail(\"rate_limit\"); }],' ' [\"二つ目のモデル\", second],' ' [\"キャッシュ\", function () { return store.has(question) ? store.get(question) : fail(\"miss\"); }],' ' ];' >> fallback5.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
代替手段の鎖を書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' for (const [name, run] of steps) {' ' try {' ' const text = await run();' ' console.log(question + \": \" + name + \" が \" + now + \" ms の時点で答えました\");' ' return text;' ' } catch (error) {' ' console.log(question + \": \" + name + \" は失敗しました (\" + error.message + \")\");' ' }' ' }' ' console.log(question + \": 定型の返答\");' ' return PLAIN;' '}' >> fallback5.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
代替手段の鎖を書く(5)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const down = function () { fail(\"server_error\"); };' ' await answer(\"営業時間\", secondModel);' ' await answer(\"営業時間\", down);' ' await answer(\"返金\", down);' '}' 'main();' >> fallback5.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
鎖を実行する
docker exec -w /home/node/project agent node fallback5.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
失敗は記録されたものです
この Run は、本物の失敗を起こしません。レート制限とサーバーのエラーは、記録されたデータとしてスクリプトに書かれ、時計はスクリプトが進める数です。本物の呼び出しは二つ目のモデルだけです。
本物のシステムでは、これらのエラーは本物のネットワークから届き、試行の間は本当に待ちます。
出力の例
| 行 | 示していること |
|---|---|
| 営業時間: 最初のモデル は失敗しました (rate_limit) | 記録された失敗 |
| 営業時間: 再試行 は失敗しました (rate_limit) | 自分の時計で 200 ms 待ったあとの再試行 |
| 営業時間: 二つ目のモデル が 200 ms の時点で答えました | 唯一の本物の呼び出し |
| 営業時間: キャッシュ が 400 ms の時点で答えました | 二つ目のモデルが止まっているので、キャッシュが答える |
| 返金: 定型の返答 | キャッシュに何もなかったので、定型のメッセージを使う |
学生が入力するセルは、このプレビューでは空白です。