足りる中で最も安いモデルへ送る
モデルの振り分けとは、リクエストごとに、足りる中で最も安いモデルを選ぶことです。最も高いモデルは最も小さいモデルの何倍も費用がかかり、たいていのリクエストにはそこまでの力は要らないので、効果があります。
サポート用のツールが、チケットの分類、短い質問への回答、長い返信の下書きをするとします。分類に、下書き用のモデルは要りません。すべてを大きなモデルへ送ると、ほとんど使われない力に払うことになります。
このレッスンのゴール
メモ
私は、各リクエストを足りる中で最も安いモデルへ振り分け、その選択を記録したケースで確かめられます。
キーワード
| ことば | いみ |
|---|---|
| ルーター | リクエストごとにモデルを選ぶ決まり |
| 段階 | 大きさと値段で分けたモデルのグループ |
| 足りる | その仕事に十分なほど、よく合っていること |
| 記録したケース | 人が選んだ段階が付いたリクエスト |
| 不一致 | ルーターと人の判断が違うケース |
| ラッパー | 呼び出しの外側にきまりを足すコード |
学生が入力するセルは、このプレビューでは空白です。
ルーターは単純な決まり
読める決まりから始めます。短い文章や分類の仕事は small の段階へ。長い文章や推論の仕事は large の段階へ。残りは medium の段階へ送ります。
大きさと仕事による決まりは、安く、即座に動き、説明も簡単です。小さなモデルに選ばせることもできますが、リクエストのたびに呼び出しが一回増えます。まず決まりを試しましょう。
例: 四つのリクエストに決まりを当てる
| リクエスト | 仕事 | 大きさ | 段階 |
|---|---|---|---|
| チケットの分類 | classify | 150 文字 | small |
| ページの要約 | summarise | 1500 文字 | medium |
| 移行の計画 | reason | 900 文字 | large |
| 長い報告書の要約 | summarise | 5000 文字 | large |
学生が入力するセルは、このプレビューでは空白です。
決まりをケースで確かめる
ルーターはお金を節約しますが、品質を失うかもしれないので、試します。記録したケースを用意します。リクエスト、その大きさと仕事、そして人が必要だと判断した段階です。ルーターをそれらに通し、不一致を並べ、費用を合計します。
不一致は、いつも決まりの誤りとは限りません。一つずつ読み、決まりを直すか、ケースを直します。
よくある間違い
注意
「最も安いモデルが、いつも一番の節約だ」と考える人が多くいます。安いモデルの出来が悪いと、再試行、苦情、離れたユーザーという費用がかかります。記録したケースが「まだ足りる」と示す場所だけで、段階を下げましょう。
モデル用の助っ人を書く(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\");' '' '// 架空の価格。100万トークンあたりの円。トークン数に価格を掛けるとマイクロ円になる。' 'const PRICE = { small: { in: 1, out: 4 }, medium: { in: 3, out: 12 }, large: { in: 10, out: 40 } };' 'function route(request) {' ' if (request.task === \"classify\" || request.chars <= 200) return \"small\";' ' if (request.task === \"reason\" || request.chars > 2000) return \"large\";' ' return \"medium\";' '}' 'function cost(tier, c) { return c.in * PRICE[tier].in + c.out * PRICE[tier].out; }' > route6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ルーターの確認を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// 記録されたケース。各リクエストの内容と、レビュアーが必要だと判断した段階。' 'const CASES = [' ' { id: 1, task: \"classify\", chars: 150, in: 60, out: 5, expected: \"small\" },' ' { id: 2, task: \"qa\", chars: 180, in: 70, out: 40, expected: \"small\" },' ' { id: 3, task: \"summarise\", chars: 1500, in: 500, out: 150, expected: \"medium\" },' ' { id: 4, task: \"reason\", chars: 900, in: 400, out: 600, expected: \"large\" },' '];' >> route6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ルーターの確認を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const MORE = [' ' { id: 5, task: \"summarise\", chars: 5000, in: 1400, out: 300, expected: \"large\" },' ' { id: 6, task: \"classify\", chars: 3000, in: 800, out: 5, expected: \"small\" },' ' { id: 7, task: \"qa\", chars: 700, in: 250, out: 120, expected: \"medium\" },' ' { id: 8, task: \"summarise\", chars: 190, in: 80, out: 30, expected: \"medium\" },' '];' >> route6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ルーターの確認を書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const all = CASES.concat(MORE);' ' let total = 0, dear = 0, wrong = [];' ' for (const c of all) {' ' const tier = route(c);' ' total = total + cost(tier, c);' ' dear = dear + cost(\"large\", c);' ' if (tier === c.expected) console.log(\"#\" + c.id + \" \" + c.task + \": \" + tier + \" ok\");' ' else { wrong.push(c.id); console.log(\"#\" + c.id + \" \" + c.task + \": \" + tier + \"、期待は \" + c.expected); }' ' }' ' console.log(\"不一致: \" + all.length + \" 件中 \" + wrong.length + \" 件、ケース \" + wrong.join(\"、\"));' ' console.log(\"振り分けたコスト: \" + total + \"。すべて large なら \" + dear + \"。節約: \" + (dear - total));' >> route6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ルーターの確認を書く(5)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' const reply = await ask([{ role: \"user\", content: \"請求、不具合、その他のどれか答えてください。二重に請求されました。\" }], \"請求\", 10);' ' console.log(route(all[0]) + \" に振り分け、応答: \" + reply.text.slice(0, 40));' '}' 'main();' >> route6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ルーターの確認を実行する
docker exec -w /home/node/project agent node route6.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ラボのモデルは一つだけです
ラボでは、決まりがどの段階を選んでも、答えるモデルは一つで、それはプラットフォームが選びます。そのため Run が示すのは、選択と費用です。本物の呼び出しが示すのは、振り分けたリクエストに答えが返るということだけです。
価格と八件のケースは架空の記録なので、全員が同じ行を表示します。
出力の例
| 行 | 示していること |
|---|---|
| #1 classify: small ok | ルーターと人の判断が一致 |
| #8 summarise: small、期待は medium | 読んで決めるべき不一致 |
| 不一致: 8 件中 1 件、ケース 8 | 件数とケース番号 |
| 振り分けたコスト: 60820。すべて large なら 85600。節約: 24780 | マイクロ円の合計 |
| small に振り分け、応答: ... | 本物の呼び出し。言葉は実行のたびに変わる |
学生が入力するセルは、このプレビューでは空白です。