たまったメッセージの山
文章からレコードを作る流れとは、雑なメッセージを、プログラムが保存できる、確かめ済みのレコードにする仕組みです。ここまでの各段階は小さく、実際の仕事では、全部がいっしょに必要になるからです。
高校の事務室に、自由な文章の備品の依頼がたまります。誰かが、品物、数、急ぎかどうかを、行にしてほしいと考えます。300件を手で打ちたい人はいません。
このレッスンのゴール
メモ
雑な文章から、確かめ済みのレコードを作る、小さな流れを組み立てられる。
キーワード
| ことば | いみ |
|---|---|
| パイプライン | 結果を次へ渡していく、段階の列 |
| バッチ | まとめて処理する、文章の集まり |
| 却下 | 理由を付けて、その文章を除くこと |
| 却下率 | 却下された文章の割合 |
| 保存 | 確かめ済みのレコードを、どこかへ書くこと |
| 個人データ | 人を特定できる情報 |
学生が入力するセルは、このプレビューでは空白です。
4つの段階を、順に行う
どのメッセージも、同じ段階を通ります。取り出し: モデルに項目を聞きます。検証: スキーマでレコードを確かめます。修復: 失敗したら、違反を付けて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 texts = [' ' \"美術室に鉛筆を12本送ってください。急ぎではありません。\",' ' \"今日中に、チョークを3箱お願いします。できるだけ急ぎです。\",' ' \"職員室はどこにありますか。\",' '];' '// 記録済みサンプル。モデルが答えないときだけ使う。きれいな返事、修復が1回要る返事、' '// 依頼のないメッセージ (却下される)。' 'const samples = [' ' JSON.stringify({ item: \"鉛筆\", quantity: 12, urgent: false }),' ' [\"はい、こちらです: \" + JSON.stringify({ item: \"チョーク\", quantity: \"3\", urgent: true }), JSON.stringify({ item: \"チョーク\", quantity: 3, urgent: true })],' ' [JSON.stringify({ item: null, quantity: null, urgent: false }), \"このメッセージには依頼が見つかりません。\"],' '];' > pipeline6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
流れ全体を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function pullJson(text) {' ' const start = text.indexOf(\"{\");' ' const end = text.lastIndexOf(\"}\");' ' if (start === -1 || end < start) return null;' ' try { return JSON.parse(text.slice(start, end + 1)); } catch (error) { return null; }' '}' '' '// 最初の問題。レコードが良ければ空の文字列。' 'function problemOf(record) {' ' if (record === null) return \"JSON ではない\";' ' if ((typeof record.item === \"string\") === false || record.item === \"\") return \"item が不正\";' ' if (Number.isInteger(record.quantity) === false || record.quantity < 1 || record.quantity > 20) return \"quantity が不正\";' ' if ((typeof record.urgent === \"boolean\") === false) return \"urgent が不正\";' ' return \"\";' '}' >> pipeline6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
流れ全体を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const PROMPT = \"このメッセージから item、quantity (1から20)、urgent (true か false) を取り出してください。JSON だけで答えてください。\n\nメッセージ: \";' '' 'async function handle(text, sample) {' ' const messages = [{ role: \"user\", content: PROMPT + text }];' ' let reply = await ask(messages, sample, 80);' ' let problem = problemOf(pullJson(reply));' ' if (problem.length > 0) {' ' // 修復を1回: 悪い返事と問題を送り返し、もう1度だけ聞く。' ' messages.push({ role: \"assistant\", content: reply });' ' messages.push({ role: \"user\", content: \"次の問題を直し、JSON だけで答えてください。\" + problem });' ' reply = await ask(messages, sample, 80);' ' problem = problemOf(pullJson(reply));' ' }' ' return { record: pullJson(reply), problem: problem };' '}' >> pipeline6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
流れ全体を書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const saved = [];' ' const rejected = [];' ' for (let at = 0; at < texts.length; at = at + 1) {' ' const result = await handle(texts[at], samples[at]);' ' if (result.problem.length > 0) rejected.push({ text: texts[at], why: result.problem });' ' else saved.push({ item: result.record.item, quantity: result.record.quantity, urgent: result.record.urgent });' ' }' ' fs.writeFileSync(\"records.json\", JSON.stringify(saved, null, 2));' ' console.log(\"保存: \" + saved.length + \" 件を records.json に書きました\");' ' for (const one of rejected) console.log(\"却下: \" + one.text + \" (\" + one.why + \")\");' ' console.log(\"却下率: \" + Math.floor(rejected.length * 100 / texts.length) + \" パーセント\");' '}' 'main();' >> pipeline6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
3つのメッセージで実行する
docker exec -w /home/node/project agent node pipeline6.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 表示されるかもしれないもの |
|---|---|
| 記録済みサンプル: ... | モデルが答えなかったときだけ、呼び出しごとに1行出る (サンプルでは5行) |
| 保存 | たとえば、2 件を records.json に書きました |
| 却下 | 職員室のメッセージと、item が不正のような理由 |
| 却下率 | 3通のうち1通が却下されたときは、33 パーセント |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
「却下されたメッセージがあるなら、流れは失敗だ」と考える人が多くいます。依頼のないメッセージを却下するのは正しい動きです。失敗なのは、数えず、理由も残さずに却下することです。