正しい JSON、間違ったデータ
スキーマで JSON を検証するとは、レコードの各項目を、規則のリストと照らし合わせることです。規則は、型、必須かどうか、許される値を決めます。読み取れた JSON でも、ページ数が「たくさん」という文字列のことがあるからです。
図書館のアプリが、本のレコードをモデルに頼みます。返事は問題なく読めます。ところが、ページ数が文字列の「180」で届き、合計のページが意味のない数字を出します。
このレッスンのゴール
メモ
返事を、スキーマで確かめられる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| スキーマ | レコードの項目を確かめる、チェックリスト | |
| 型 | 項目が持つ値の種類 | integer |
| 必須 | 必ずなければならない項目 | |
| 許される値 | 項目が取れる、唯一の値 | 小説、歴史、科学 |
| 違反 | 規則を破る、1つの形 | |
| レコード | 項目の集まりを持つ、1つのオブジェクト |
学生が入力するセルは、このプレビューでは空白です。
スキーマはチェックリスト
読み取りで分かるのは、文字列が JSON だということだけです。データが正しいかどうかは分かりません。そのためにスキーマを使います。項目ごとに1つの規則を持つ決まりです。
規則は、項目の型、必須かどうか、場合によっては許される値を決めます。JSON Schema という標準は、この規則を決まった書式で書きます。ここでは、自分のチェックリストをコードで書きます。
例: 本のスキーマ
| 項目 | 型 | 必須 | 許される値 |
|---|---|---|---|
| title | string | はい | 任意の文字列 |
| pages | integer | はい | 任意の整数 |
| genre | string | はい | 小説、歴史、科学 |
| rating | number | いいえ | 任意の数値 |
学生が入力するセルは、このプレビューでは空白です。
最初の1つだけでなく、すべての違反を集める
最初の違反で止まる確認は、教えてくれることが少ないです。すべての違反を、短い文字列として、リストに集めます。たとえば pages: 型が違う です。
このリストには、2つの使い道があります。レコードを受け入れるかを決めます。次のレッスンでは、これをモデルへの知らせとして送り返します。
モデル用の補助を書く(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\");' '' '// スキーマは、項目ごとに1つの規則を持つチェックリストです。' 'const schema = [' ' { name: \"title\", type: \"string\", required: true },' ' { name: \"pages\", type: \"integer\", required: true },' ' { name: \"genre\", type: \"string\", required: true, allowed: [\"小説\", \"歴史\", \"科学\"] },' ' { name: \"rating\", type: \"number\", required: false },' '];' > check2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
スキーマの確認を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function isRecord(x) {' ' if (x === null) return false;' ' if (Array.isArray(x)) return false;' ' return typeof x === \"object\";' '}' '' 'function typeOk(value, type) {' ' if (type === \"string\") return typeof value === \"string\";' ' if (type === \"integer\") return Number.isInteger(value);' ' if (type === \"number\") return typeof value === \"number\";' ' if (type === \"boolean\") return typeof value === \"boolean\";' ' return false;' '}' >> check2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
スキーマの確認を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// 最初の1つだけでなく、すべての違反を集めます。' 'function faults(record, schema) {' ' if (isRecord(record) === false) return [\"オブジェクトではない\"];' ' const found = [];' ' for (const rule of schema) {' ' if (Object.prototype.hasOwnProperty.call(record, rule.name) === false) {' ' if (rule.required) found.push(rule.name + \": 欠けている\");' ' continue;' ' }' ' const value = record[rule.name];' ' if (typeOk(value, rule.type) === false) found.push(rule.name + \": 型が違う\");' ' else if (rule.allowed && rule.allowed.includes(value) === false) found.push(rule.name + \": 許可されていない値\");' ' }' ' return found;' '}' >> check2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
スキーマの確認を書く(4)
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; }' '}' '' 'const records = [' ' { title: \"川の地図帳\", pages: 240, genre: \"科学\" },' ' { title: \"潮見表\", pages: \"180\", genre: \"歴史\" },' ' { title: \"月の手紙\", pages: 96, genre: \"詩\", rating: 4.5 },' ' { pages: 310, genre: \"小説\", rating: \"良い\" },' '];' 'records.forEach(function (record, at) {' ' const found = faults(record, schema);' ' console.log(\"レコード \" + (at + 1) + \":\", found.length === 0 ? \"ok\" : found.join(\"、\"));' '});' >> check2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
スキーマの確認を書く(5)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const messages = [{ role: \"user\", content: \"大学図書館の本を1冊考えてください。キー title (文字列)、pages (整数)、genre (小説、歴史、科学のどれか) を持つ JSON オブジェクトだけで答えてください。\" }];' ' // 記録済みサンプル。モデルが答えないときだけ使う。わざと2つの規則を破ってある。' ' const sample = JSON.stringify({ title: \"静かな港\", pages: \"210\", genre: \"推理\" });' ' const reply = await ask(messages, sample, 100);' ' const record = pullJson(reply);' ' const found = faults(record, schema);' ' console.log(\"モデルのレコード:\", JSON.stringify(record));' ' console.log(\"違反:\", found.length === 0 ? \"なし\" : found.join(\"、\"));' '}' 'main();' >> check2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
確認を実行する
docker exec -w /home/node/project agent node check2.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 表示されるもの |
|---|---|
| レコード 1 | ok |
| レコード 2 | pages: 型が違う |
| レコード 3 | genre: 許可されていない値 |
| レコード 4 | title: 欠けている、rating: 型が違う |
| 記録済みサンプル: ... | モデルが答えなかったときだけ、5行目に出る |
| モデルのレコード、違反 | 実行ごとに変わる。モデルのレコードと、その違反またはなし |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
「180」と 180 は同じものだと考える人が多くいます。見た目は似ていますが、プログラムは、文字列を数値のように足したり比べたりできません。値の見た目ではなく、型を確かめてください。