Text to SQL の機能を試す方法
Text to SQL の評価用の集まりとは、正しい答えが分かっている質問の一覧で、プロンプトやモデルが変わるたびに動かすものです。変更でよくなったか悪くなったかが分かります。
モジュール 68 と 85 は、言葉の答えを採点しました。このレッスンは、SQL を、それが返す行で採点します。
このレッスンのゴール
メモ
答えの分かっている質問の集まりで、候補のクエリを採点することができる。
キーワード
| ことば | いみ |
|---|---|
| 評価用の集まり | 変更のたびに動かす、答えの分かっている質問 |
| 正解のクエリ | 手で確かめた、正しい行を返すクエリ |
| 実行の一致 | 正解のクエリと同じ行 |
| 回帰 | 前は合格し、今は通らない質問 |
| 関所 | 変更を止める決まり |
学生が入力するセルは、このプレビューでは空白です。
どの事例にも質問と正解のクエリ
事例は、質問、正解のクエリ、順が大事かどうかを持ちます。正解のクエリは自分で書き、その行をフィクスチャで手で確かめます。
正解のクエリを動かして、期待する行を得ます。こうすれば、正解のクエリが正しい限り、データが増えても集まりは使い続けられます。
例: 図書館の五つの事例
| 事例 | 質問 | 正解の行 |
|---|---|---|
| 1 | 3 月の貸し出しの件数 | 4 |
| 2 | 3 月に借りた会員の人数 | 3 |
| 3 | まだ貸し出し中の本 | 5 |
| 4 | 会員 2 が借りた題名 | 雨の地図、静かなエンジン、七つのやかん |
| 5 | 一度も借りられていない本 | 正午の北 |
学生が入力するセルは、このプレビューでは空白です。
文字ではなく結果で採点する
ここでは COUNT(id) と COUNT(*) は同じ数を返します。結合は、副問い合わせでも書けます。違う文字の二つが、どちらも正しいことがあります。
だから、候補をそれぞれ動かし、その行を正解の行と比べます。文字の完全一致で採点すると、正しい答えを間違いにしてしまいます。Run でそれが分かります。
例: 文字と結果が食い違う
| 候補 | 正解と同じ文字 | 正解と同じ行 |
|---|---|---|
| SELECT COUNT(id) FROM loans WHERE month = 3 | いいえ | はい |
| IN (SELECT book_id ...) で書いた結合 | いいえ | はい |
| SELECT COUNT(*) FROM loans | いいえ | いいえ |
学生が入力するセルは、このプレビューでは空白です。
回帰は同じ点数の中に隠れる
新しいプロンプトが、質問 2 を直し、質問 3 を壊したとします。どちらの版も五つのうち四つで合格するので、点数は動きません。
事例ごとに比べます。前は合格し、今は通らない質問が回帰です。関所は、直るまでその変更を止めます。
よくある間違い
注意
合格率が同じなら何も悪くなっていない、と考える人が多いです。一つの質問が直り、別の質問が壊れることがあります。事例ごとに前の実行と比べ、合格を一つでも失う変更は止めましょう。
助っ人 model.js を書く、その 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;' '}' '// 応答がクエリを囲みで包んでいたら、内側だけを残す。' 'function sqlOf(text) {' ' const parts = text.split(String.fromCharCode(96).repeat(3));' ' const inner = (parts.length > 2 ? parts[1] : text).trim();' ' return inner.toLowerCase().startsWith(\"sql\") ? inner.slice(3).trim() : inner;' '}' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
助っ人 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 data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask, sqlOf };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
小さな図書館のデータを書く、その 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { DatabaseSync } = require(\"node:sqlite\");' '// 作り話の図書館: 本が六冊、会員が四人、貸し出しが九件。' 'const BOOKS = [[1, \"塩の道\", 2019], [2, \"ガラスの港\", 2021], [3, \"雨の地図\", 2015],' ' [4, \"静かなエンジン\", 2023], [5, \"七つのやかん\", 2018], [6, \"正午の北\", 2022]];' 'const MEMBERS = [[1, \"上田\"], [2, \"佐藤\"], [3, \"高橋\"], [4, \"中村\"]];' '// id, book_id, member_id, month, returned(1 は返却済み、0 は貸し出し中)' 'const LOANS = [[1, 1, 1, 3, 1], [2, 2, 1, 3, 1], [3, 3, 2, 3, 0], [4, 1, 3, 3, 1], [5, 1, 1, 4, 0],' ' [6, 4, 2, 4, 0], [7, 2, 3, 4, 1], [8, 5, 2, 5, 0], [9, 2, 4, 5, 0]];' > library.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
小さな図書館のデータを書く、その 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function put(db, table, rows) {' ' const marks = rows[0].map(function () { return \"?\"; }).join(\", \");' ' const insert = db.prepare(\"INSERT INTO \" + table + \" VALUES (\" + marks + \")\");' ' rows.forEach(function (row) { insert.run(...row); });' '}' 'function open(file) {' ' const db = new DatabaseSync(file || \":memory:\");' ' db.exec(\"CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, year INTEGER)\");' ' db.exec(\"CREATE TABLE members (id INTEGER PRIMARY KEY, name TEXT)\");' ' db.exec(\"CREATE TABLE loans (id INTEGER PRIMARY KEY, book_id INTEGER REFERENCES books(id), \"' ' + \"member_id INTEGER REFERENCES members(id), month INTEGER, returned INTEGER)\");' ' put(db, \"books\", BOOKS);' ' put(db, \"members\", MEMBERS);' ' put(db, \"loans\", LOANS);' ' return db;' '}' 'module.exports = { open };' >> library.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
評価のスクリプトを書く、その 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask, sqlOf } = require(\"./model\");' 'const { open } = require(\"./library\");' 'const db = open();' 'const cases = [' ' { gold: \"SELECT COUNT(*) FROM loans WHERE month = 3\" },' ' { gold: \"SELECT COUNT(DISTINCT member_id) FROM loans WHERE month = 3\" },' ' { gold: \"SELECT COUNT(*) FROM loans WHERE returned = 0\" },' ' { gold: \"SELECT b.title FROM loans l JOIN books b ON l.book_id = b.id WHERE l.member_id = 2\" },' ' { gold: \"SELECT title FROM books WHERE id NOT IN (SELECT book_id FROM loans)\" },' '];' > eval.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
評価のスクリプトを書く、その 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const A = [\"SELECT COUNT(id) FROM loans WHERE month = 3\",' ' \"SELECT COUNT(member_id) FROM loans WHERE month = 3\", cases[2].gold,' ' \"SELECT b.title FROM books b JOIN loans l ON l.book_id = b.id WHERE l.member_id = 2\",' ' \"SELECT b.title FROM books b LEFT JOIN loans l ON l.book_id = b.id WHERE l.id IS NULL\"];' 'const B = [cases[0].gold, cases[1].gold, \"SELECT COUNT(*) FROM loans\",' ' \"SELECT title FROM books WHERE id IN (SELECT book_id FROM loans WHERE member_id = 2)\", cases[4].gold];' 'function rows(sql) {' ' try { return db.prepare(sql).all().map(function (r) { return JSON.stringify(Object.values(r)); }).sort().join(\"|\"); }' ' catch (error) { return null; }' '}' >> eval.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
評価のスクリプトを書く、その 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function score(sql, i) {' ' const got = rows(sql);' ' return got === null ? \"エラー\" : got === rows(cases[i].gold) ? \"合格\" : \"不合格\";' '}' 'function rate(list, test) {' ' return Math.floor(100 * list.filter(test).length / list.length) + \" パーセント\";' '}' 'const a = A.map(score);' 'const b = B.map(score);' 'const same = function (sql, i) { return sql === cases[i].gold; };' '// 回帰: A で合格し、B で合格しない質問。' 'const lost = [];' 'a.forEach(function (v, i) { if (v === \"合格\" && (b[i] === \"不合格\" || b[i] === \"エラー\")) lost.push(i + 1); });' >> eval.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
評価のスクリプトを書く、その 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' a.forEach(function (v, i) { console.log(\"質問 \" + (i + 1) + \": A \" + v + \"、B \" + b[i]); });' ' const pass = function (v) { return v === \"合格\"; };' ' console.log(\"結果で採点: A \" + rate(a, pass) + \"、B \" + rate(b, pass));' ' console.log(\"文字の一致で採点: A \" + rate(A, same) + \"、B \" + rate(B, same));' ' console.log(\"B の回帰: \" + (lost.length ? lost.join(\", \") : \"なし\"));' ' console.log(\"関所: \" + (lost.length ? \"止める。前に合格した質問が、今は通らない\" : \"出す\"));' ' await live();' '}' >> eval.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
評価のスクリプトを書く、その 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function live() {' ' const q = \"loans(id, book_id, member_id, month, returned)\n質問: 3 月に本を借りた会員は、何人ですか(同じ人は一人と数える)。\";' ' const sample = \"SELECT COUNT(DISTINCT member_id) FROM loans WHERE month = 3;\";' ' const reply = await ask([{ role: \"system\", content: \"SQLite の SELECT 文を一つだけ答えてください。ほかには何も書かないでください。\" },' ' { role: \"user\", content: q }], sample, 120);' ' const sql = sqlOf(reply.text);' ' console.log(\"モデルが書いたクエリ: \" + sql);' ' console.log(\" 合計とは別に採点: \" + score(sql, 1));' '}' 'main();' >> eval.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
二つの版を五つの質問で採点
docker exec -w /home/node/project agent node --experimental-sqlite --no-warnings eval.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
Run の出力
| 行 | 意味 |
|---|---|
| 質問 2: A 不合格、B 合格 | 新しいプロンプトが質問 2 を直した |
| 質問 3: A 合格、B 不合格 | 新しいプロンプトが質問 3 を壊した |
| 結果で採点: A 80 パーセント、B 80 パーセント | 点数は動かなかった |
| 文字の一致で採点: A 20 パーセント、B 60 パーセント | 文字の一致は正しい答えを間違いにする |
| B の回帰: 3 | 失われた質問 |
| 関所: 止める。前に合格した質問が、今は通らない | 決定 |
学生が入力するセルは、このプレビューでは空白です。