なぜ名前を忘れてしまうのか
チャットボットは、会話のメッセージを保管し、新しい質問のたびにもう一度送ります。言語モデルの呼び出しには、それ自身の記憶がないからです。
お客さんがバス会社のチャットボットに、切符をなくしたと伝えます。そのあと「弟の分はどうですか」と聞きます。前のメッセージが一緒に届いて、はじめて答えられます。
このレッスンのゴール
メモ
私は、リクエストをまたいで会話を保てます。
キーワード
| ことば | いみ | れい |
|---|---|---|
| セッション | 一人の人とチャットボットの会話 | |
| セッション ID | 一つのセッションを指す札 | sakura |
| 状態 | リクエストの間も、プログラムが覚えていること | |
| 保管場所 | すべてのセッションをしまっておく所 | |
| メッセージのリスト | 呼び出しのたびに送るメッセージ |
学生が入力するセルは、このプレビューでは空白です。
呼び出しは毎回ゼロから始まる
モデルの呼び出しは、毎回まっさらな状態で始まります。モデルが知っているのは、その呼び出しで送ったメッセージだけです。
会話にするには、プログラムがすべてのメッセージを保管します。そして毎回、リスト全体を送り、新しい質問を最後に置きます。このリストが、チャットの状態です。
例: 3回のリクエスト
| リクエスト | 送るメッセージ | モデルに見えるもの |
|---|---|---|
| 1回目 | system, user | 規則と「私の名前はさくらです。」だけ |
| 2回目 | system, user, assistant, user | 最初のやり取りと「私の名前は何ですか。」 |
| 3回目 | system, user, assistant, user, assistant, user | ここまでのすべてと、最新の質問 |
学生が入力するセルは、このプレビューでは空白です。
人ごとに別のセッションを持つ
チャットボットは、たくさんの人と同時に話します。そこで保管場所は、セッション ID ごとに分けます。会話一つにつき、リスト一つです。新しい ID なら新しいリストを作り、知っている ID ならそのリストに足します。
保管場所は、プログラムが動いている間は変数で足ります。止めたあとも残すには、プロジェクトのフォルダーに小さな JSON ファイルを置きます。
状態は誰のものか
状態は、モデルではなくプログラムのものです。だから、何をどれだけの間保管するかは、自分で決めます。
パスワードやカード番号など、チャットボットに要らないものは保管しません。セッションを消す方法も用意します。セッションを消すと、リストが丸ごとなくなります。
よくある間違い
注意
「モデルは、誰が話しているかを覚えている」と考える人が多いです。そうではありません。二人が一つのリストを共有すると、お互いの言葉が見えてしまいます。セッション 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 fs = require(\"fs\");' 'const { ask } = require(\"./model\");' 'const FILE = \"sessions.json\"; // 保管場所は、プロジェクトのフォルダーに置く' 'const RULE = \"あなたは親切なアシスタントです。短い一文で答えてください。\";' '' 'function load() {' ' try { return JSON.parse(fs.readFileSync(FILE, \"utf8\")); } catch (error) { return {}; }' '}' 'function save(store) { fs.writeFileSync(FILE, JSON.stringify(store, null, 1)); }' > chat1.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
セッションの保管場所を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function say(store, id, text, samples) {' ' if (store[id] === undefined) store[id] = [{ role: \"system\", content: RULE }];' ' store[id].push({ role: \"user\", content: text });' ' const answer = await ask(store[id], samples, 40);' ' store[id].push({ role: \"assistant\", content: answer });' ' save(store);' ' return answer;' '}' >> chat1.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
セッションの保管場所を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const store = {};' ' // 記録済みのサンプル。モデルが答えないときだけ使う。' ' const sakuraSamples = [\"はじめまして、さくらさん。\", \"あなたの名前は、さくらさんです。\"];' ' const kentaSamples = [\"あなたの名前は、まだ知りません。\"];' ' console.log(\"さくら:\", await say(store, \"sakura\", \"私の名前はさくらです。覚えておいてください。\", sakuraSamples));' ' console.log(\"さくら:\", await say(store, \"sakura\", \"私の名前は何ですか。\", sakuraSamples));' ' console.log(\"健太:\", await say(store, \"kenta\", \"私の名前は何ですか。\", kentaSamples));' ' const saved = load();' ' console.log(\"保存済み: sakura は \" + saved.sakura.length + \" 件、kenta は \" + saved.kenta.length + \" 件\");' '}' 'main();' >> chat1.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
二つの会話を実行する
docker exec -w /home/node/project agent node chat1.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 表示されるかもしれないもの |
|---|---|
| さくら(1回目) | 名前を使うかもしれない、短いあいさつ |
| さくら(2回目) | 前のメッセージを送ったので、名前を言えるかもしれない答え |
| 健太 | 健太は別のリストなので、さくらさんの名前を知らない答え |
| 保存済み | sakura は 5 件、kenta は 3 件 |
学生が入力するセルは、このプレビューでは空白です。