会話が長くなりすぎたとき
チャット履歴の切り詰めとは、モデルに送るメッセージを減らして、リストを上限に収めることです。長いチャットはターンごとに大きくなり、メッセージ1件ごとに時間とクレジットがかかるからです。
旅行案内のチャットボットと1時間話したあとは、最初の質問はもう要りません。外せば、チャットは速く、安く保てます。
このレッスンのゴール
メモ
私は、会話をモデルの上限の内に収められます。
キーワード
| ことば | いみ |
|---|---|
| トークン | モデルが数える、文章の小さな単位 |
| コンテキストの上限 | モデルが1回の呼び出しで受け取れる最大のトークン数 |
| 予算 | 送ると自分で決めた、最大のトークン数 |
| 切り詰める | 収まるまで、古いメッセージを外すこと |
| 要約 | 外したメッセージの代わりになる、短い文章 |
学生が入力するセルは、このプレビューでは空白です。
送る前にトークンを数える
モデルは文章をトークンで数え、1回の呼び出しで受け取れる数に上限があります。本当の数は、モデルごとの数え方で決まります。おおまかな目安は、4文字で1トークンです。日本語では、実際の数が目安より多くなりがちです。
このレッスンでは、役割や包みの分として、メッセージごとに4を足します。全部を足して、予算と比べます。
例: 6件のメッセージを数える
| メッセージ | 文字数 | トークン |
|---|---|---|
| system: あなたは電車の旅の案内役です。短く答えてください。 | 25 | 11 |
| user: 新宿から横浜まで、電車で何分かかりますか。 | 21 | 10 |
| assistant: およそ30分です。 | 9 | 7 |
| user: 切符はいくらですか。 | 10 | 7 |
| assistant: 時間帯によって変わります。 | 13 | 8 |
| user: 静かな車両はありますか。 | 12 | 7 |
| 合計 | 90 | 50 |
学生が入力するセルは、このプレビューでは空白です。
古いものを外し、規則は残す
切り詰めるときは、合計が予算に収まるまで、古いメッセージから外します。予算がどうでも、2件は残します。先頭の system メッセージと、最後の最新のメッセージです。
system メッセージには規則が入っています。最新のメッセージは、いま聞いている質問です。
要約が外した分の代わりになる
メッセージを外すと、事実が失われます。要約なら、主な事実を少ない言葉で残せます。外したメッセージをモデルに要約させ、その要約を system メッセージに足します。
切り詰めるたびに、呼び出しが1回増えます。短いチャットなら、ただ外すだけで足りることが多いです。
よくある間違い
注意
「一番古いメッセージは、いつも一番大事でない」と考える人が多いです。実際は、規則の入った system メッセージや、チャットが頼りにする唯一の事実であることもあります。system メッセージは残し、大事なものは外す前に要約しましょう。
モデルの補助を書く(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 BUDGET = 35; // 切り詰めが起きるように、小さな予算にする' '' 'function tokens(message) { return Math.ceil(message.content.length / 4) + 4; }' 'function total(list) {' ' return list.reduce(function (sum, m) { return sum + tokens(m); }, 0);' '}' > trim2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
切り詰めのスクリプトを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function trim(list, budget) {' ' const kept = list.slice();' ' const dropped = [];' ' while (total(kept) > budget && kept.length > 2) {' ' const at = kept[0].role === \"system\" ? 1 : 0;' ' dropped.push(kept.splice(at, 1)[0]);' ' }' ' return { kept: kept, dropped: dropped };' '}' >> trim2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
切り詰めのスクリプトを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const history = [' ' { role: \"system\", content: \"あなたは電車の旅の案内役です。短く答えてください。\" },' ' { role: \"user\", content: \"新宿から横浜まで、電車で何分かかりますか。\" },' ' { role: \"assistant\", content: \"およそ30分です。\" },' ' { role: \"user\", content: \"切符はいくらですか。\" },' ' { role: \"assistant\", content: \"時間帯によって変わります。\" },' ' { role: \"user\", content: \"静かな車両はありますか。\" },' '];' >> trim2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
切り詰めのスクリプトを書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const result = trim(history, BUDGET);' ' console.log(\"トークン 前: \" + total(history) + \"、後: \" + total(result.kept) + \"、捨てた数: \" + result.dropped.length);' >> trim2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
切り詰めのスクリプトを書く(5)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' const old = result.dropped.map(function (m) { return m.role + \": \" + m.content; }).join(\" \");' ' // 記録済みのサンプル。モデルが答えないときだけ使う。' ' const first = \"利用者は新宿から横浜までの時間を聞き、およそ30分と教えられました。\";' ' const summary = await ask([{ role: \"user\", content: \"次の会話を短い一文で要約してください: \" + old }], first, 50);' ' console.log(\"要約:\", summary);' ' const prompt = result.kept.slice();' ' prompt[0] = { role: \"system\", content: prompt[0].content + \" このチャットの前の部分: \" + summary };' ' console.log(\"返事:\", await ask(prompt, \"はい、たいていの電車には静かな車両があります。\", 50));' '}' 'main();' >> trim2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
切り詰めて要約する
docker exec -w /home/node/project agent node trim2.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 表示されるかもしれないもの |
|---|---|
| トークン | 前: 50、後: 33、捨てた数: 2(いつも同じ。推測はしません) |
| 要約 | 外した、所要時間についての一文 |
| 返事 | 静かな車両についての、短い答え |
学生が入力するセルは、このプレビューでは空白です。