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

無料レッスン

動くチャットボットを作る方法

このレッスンのあとで: 私は、状態、切り詰め、プロンプト、ストリーム、出典を一つのチャットボットにまとめられます。

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

5つの部品を一つにまとめる

動くチャットボットは、5つの部品を一つのループにまとめます。状態を保ち、切り詰め、システムプロンプトに従い、答えを流れで送り、出典を示します。部品は一つずつなら動くので、問題は、つなぎ目で起こるからです。

図書館のチャットボットが、3つの質問に続けて答えます。最初の質問を覚え、予算に収め、毎回出典を示さなければなりません。

このレッスンのゴール

メモ

私は、状態、切り詰め、プロンプト、ストリーム、出典を一つのチャットボットにまとめられます。

キーワード

ことばいみ
チャットのループ利用者のメッセージごとに繰り返す手順
ターン利用者のメッセージ一つと、その返事
履歴ここまでのチャットのすべてのメッセージ
予算送ると自分で決めた最大のトークン数
代わりの対応どのモデルも答えないときの、ボットの動き

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

1ターンで5つの部品を使う

どのターンも、同じ手順です。履歴を読み込み、利用者のメッセージを足し、送る分を切り詰め、モデルを呼び、返事を確かめ、履歴を保存します。

この手順を、turn という一つの関数に書きます。チャットは、その関数を何度も呼ぶことになります。

例: 1ターンの手順

手順出てきたレッスン起こること
読み込んで足す会話を覚える仕組み利用者のメッセージが、セッションのリストに入る
切り詰める履歴の切り詰め古いメッセージは、この呼び出しから外れる
プロンプトと出典システムプロンプト、出典system メッセージに、規則と番号付きの出典が入る
呼んで確かめる出典出典のない印がないか、返事を確かめる
流して保存するストリーム表示断片がページへ行き、履歴が保存される

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

保管するものと送るものは別

履歴は、すべてのメッセージを保管します。呼び出しで送るのは、予算に収まる分だけです。だから、切り詰めても記録は変わりません。変わるのは、一つの呼び出しだけです。

あとから予算を変えることもできますし、人はチャット全体を読み返せます。

モデルが答えない場合に備える

どのモデルも答えないことがあります。AI が切られている、その日のクレジットを使い切った、ネットワークが切れた、などです。代わりの対応は、そう伝える、わかりやすいメッセージです。チャットが止まったり、壊れたりしません。

このラボでは、そのとき Run が、記録済みサンプルとはっきり示して表示します。本物の製品なら、独自のお知らせを出します。

よくある間違い

注意

「デモが動けば、チャットボットは完成だ」と考える人が多いです。デモでは、答えやすい質問をします。答えにくい質問、とても長いチャット、モデルがない状態も試しましょう。ほかの人が使う前に、1回のチャットのトークン数を数えましょう。

モデルの補助を書く(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 BUDGET = 85; // system メッセージと履歴を合わせた、トークンの予算。3回目で切り詰めが起きる小ささ' 'const SOURCES = [' '  \"会員は、本を6冊まで、3週間借りられます。\",' '  \"返却が遅れた本は、1日につき10円かかります。\",' '  \"図書館は、平日の午前9時に開き、午後5時に閉まります。\",' '];' > chatbot6.js"

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

チャットボットを書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function tokens(m) { return Math.ceil(m.content.length / 4) + 4; }' 'function total(list) {' '  return list.reduce(function (sum, m) { return sum + tokens(m); }, 0);' '}' 'function systemMessage() {' '  const numbered = SOURCES.map(function (t, at) { return \"[\" + (at + 1) + \"] \" + t; }).join(\" \");' '  const rule = \"あなたは図書館の案内係です。次の資料だけを使い、2文までで答えてください。事実のあとに、資料の番号を角かっこで書いてください。資料に書かれていなければ、「わかりません」と答えてください。\";' '  return { role: \"system\", content: rule + numbered };' '}' >> chatbot6.js"

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

チャットボットを書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function trim(history, system) {' '  const kept = history.slice();' '  while (total([system].concat(kept)) > BUDGET && kept.length > 1) kept.shift();' '  return kept;' '}' 'function badMarkers(reply) {' '  const bad = [];' '  const re = /\[(\d+)\]/g;' '  let m = re.exec(reply);' '  while (m) {' '    if (Number(m[1]) < 1 || Number(m[1]) > SOURCES.length) bad.push(m[1]);' '    m = re.exec(reply);' '  }' '  return bad;' '}' >> chatbot6.js"

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

チャットボットを書く(4)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function turn(state, text, sample) {' '  state.history.push({ role: \"user\", content: text });' '  const system = systemMessage();' '  const sent = trim(state.history, system);' '  const held = state.history.length;' '  const reply = await ask([system].concat(sent), sample, 70);' '  state.history.push({ role: \"assistant\", content: reply });' '  fs.writeFileSync(\"chat.json\", JSON.stringify(state, null, 1));' '  console.log(\"あなた: \" + text);' '  console.log(\"ボット: \" + reply);' '  console.log(\"  送信 \" + sent.length + \" 件 / 保持 \" + held + \" 件、誤った印: \" + badMarkers(reply).length);' '}' >> chatbot6.js"

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

チャットボットを書く(5)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const state = { history: [] };' '  // 記録済みのサンプル。モデルが答えないときだけ使う。' '  await turn(state, \"本は何冊借りられますか。\", \"本は6冊まで、3週間借りられます[1]。\");' '  await turn(state, \"遅れた場合の料金はいくらですか。\", \"遅れると、1日10円かかります[2]。\");' '  await turn(state, \"金曜日は何時に閉まりますか。\", \"図書館は、平日の午後5時に閉まります[3]。\");' '  console.log(\"保存: chat.json に \" + state.history.length + \" 件\");' '}' 'main();' >> chatbot6.js"

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

3ターンのチャットを行う

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

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

出力の一例

行表示されるかもしれないもの
あなたとボット3つの質問と、出典を付けるかもしれない短い答え
送信 n 件 / 保持 m 件1 件 / 1 件、3 件 / 3 件、4 件 / 5 件。3回目は予算を超えるので、古いメッセージが 1 件外れる
誤った印たいてい 0。0 より大きければ、存在しない出典を挙げている
保存保存: chat.json に 6 件

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

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

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