誰も教えていない電話番号
文章からレコードを取り出すとは、モデルに段落を読ませて、決まった項目を埋めてもらうことです。「推測するな」と伝えないと、モデルは空欄を推測で埋めてしまうからです。
大学の事務室に、名前、教室、日付はあるが、電話番号のない教室の依頼が届きます。ところが、ツールは電話番号を表示します。誰も教えていません。モデルが作り出したのです。
このレッスンのゴール
メモ
値を作り出さずに、段落を1つのレコードにできる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| 取り出す | 文章から、名前のある事実を抜き出すこと | |
| 項目 | レコードの中の、名前のある1つの事実 | date |
| null | 項目がないことを表す値 | |
| 元の文章 | 事実の出どころになる段落 | |
| 裏付けがある | 元の文章で見つかり、推測ではないこと | |
| 埋まり具合 | 値が入っている項目の割合 |
学生が入力するセルは、このプレビューでは空白です。
項目を決めて、null を許す
ほしい項目と、各項目の型を、正確に伝えます。そのあと、事実が文章にないときの扱いを伝えます。null にして、推測しない、ということです。
この決まりがないと、モデルはすべての項目を埋めることがあります。埋まったレコードは、良い答えに見えるからです。空欄を残してよい、と許可してください。
例: 1通のメッセージ、5つの項目
| 項目 | メッセージにあるか | 正しい値 |
|---|---|---|
| name | ある:「田中さくらです」 | 田中さくら |
| room | ある:「教室 B12」 | B12 |
| date | ある:「2026-11-20」 | 2026-11-20 |
| students | ある:「学生24人」 | 24 |
| phone | ない | null |
学生が入力するセルは、このプレビューでは空白です。
すべての値に裏付けがあるか確かめる
プロンプトだけを信じてはいけません。返事のあとで、各値を元の文章と照らし合わせます。値が文章になければ、作り出された値とみなし、null にします。
この確認は簡単で、弱点があります。たとえば学生の数が 2 と間違っていても、2 は 2026-11-20 の中にあるので、残ってしまいます。もっと細かい確認は、語全体を比べます。それでも、作り出された電話番号は見つかります。
モデル用の補助を書く(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\");' '' 'const source = \"こんにちは、田中さくらです。2026-11-20 に教室 B12 を借りたいです。学生24人と教員3人で使います。返事はメールでお願いします。\";' 'const fields = [\"name\", \"room\", \"date\", \"students\", \"phone\"];' '' '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; }' '}' > extract4.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
取り出すスクリプトを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// 値は、元の文章に本当にあるときだけ採用します。' 'function grounded(record, source, fields) {' ' const result = {};' ' for (const name of fields) {' ' const value = record === null ? null : record[name];' ' if (value === null || value === undefined || value === \"\") {' ' result[name] = null;' ' console.log(name + \": null (ない)\");' ' } else if (source.toLowerCase().includes(String(value).toLowerCase())) {' ' result[name] = value;' ' console.log(name + \": \" + value + \" (採用)\");' ' } else {' ' result[name] = null;' ' console.log(name + \": \" + value + \" (文章にないので外した)\");' ' }' ' }' ' return result;' '}' >> extract4.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
取り出すスクリプトを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const prompt = \"次のメッセージから、キー name (文字列)、room (文字列)、date (文字列、年-月-日)、students (整数)、phone (文字列) を持つ JSON オブジェクトを取り出してください。メッセージにない項目は null にしてください。推測してはいけません。JSON だけで答えてください。\n\nメッセージ: \" + source;' '// 記録済みサンプル。モデルが答えないときだけ使う。わざと電話番号を作り出してある。' 'const sample = JSON.stringify({ name: \"田中さくら\", room: \"B12\", date: \"2026-11-20\", students: 24, phone: \"090-1234-5678\" });' >> extract4.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
取り出すスクリプトを書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const reply = await ask([{ role: \"user\", content: prompt }], sample, 150);' ' const record = grounded(pullJson(reply), source, fields);' ' let filled = 0;' ' for (const name of fields) {' ' if (record[name] === null) continue;' ' filled = filled + 1;' ' }' ' console.log(\"埋まった項目: \" + filled + \" / \" + fields.length);' '}' 'main();' >> extract4.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
取り出しを実行する
docker exec -w /home/node/project agent node extract4.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 表示されるかもしれないもの |
|---|---|
| 記録済みサンプル: ... | モデルが答えなかったときだけ、1行目に出る |
| name、room、date、students | 値と (採用) が付く |
| phone | null (ない)、または (文章にないので外した) が付いた値 |
| 埋まった項目 | モデルが4つの事実だけを見つけたときは、4 / 5 |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
「埋まった項目は、空の項目より良い」と考える人が多くいます。作り出された値は本物に見え、誰も確かめようとしません。null は、人が見る必要があると、はっきり伝えます。