電話番号の入った問い合わせ
プロンプトに個人情報を入れないとは、モデルに、仕事に必要なものだけを見せることです。プロンプトは別のサービスへ送られ、しばしばログに残るので、大切になります。
サポートツールが、問い合わせをモデルに送って返信案を作らせます。問い合わせには、名前、メールアドレス、電話番号があります。返信にはどれも要りません。ツールは、それを置き換え文字に変え、あとで元に戻せます。
このレッスンのゴール
メモ
モデルを呼ぶ前に個人情報を置き換え文字に変え、あとで元に戻すことができる。
重要語句
| ことば | いみ | れい |
|---|---|---|
| 個人情報 | 生きている個人を特定できる、あらゆる情報 | |
| データの最小化 | 仕事に必要なものだけを送ること | |
| マスクする | 値を、代わりのものに置き換えること | |
| 置き換え文字 | 取り除いた値の代わりに置くラベル | [メール_1] |
| 対応表 | 各置き換え文字と元の値を結ぶ表 |
学生が入力するセルは、このプレビューでは空白です。
仕事に必要なものだけを送る
まず、その値がモデルに必要かどうかを問います。苦情への返信に要るのは苦情の中身で、電話番号ではありません。要らないものは取り除きます。
返信に人の名前が必要なら、置き換え文字を使います。モデルが「[氏名_1]様」と書き、あとで自分のコードが本当の名前を戻します。
例: 前と後
| 値 | モデルに送るもの | 対応表に残すもの |
|---|---|---|
| さくら | [氏名_1] | [氏名_1] はさくら |
| [email protected] | [メール_1] | [メール_1] は [email protected] |
| 09012345678 | [電話_1] | [電話_1] は 09012345678 |
| さくら、もう一度 | [氏名_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 { ask } = require(\"./model\");' '' 'const names = [\"さくら\", \"健太\"];' '// 単純にするため、空白で区切った語を一つずつ調べる。' 'const ticket = \"こんにちは、 さくら です。 [email protected] に メール、 09012345678 に 電話 してください。 健太 が 昨日 手伝ってくれました。\";' 'function allDigits(text) {' ' return text.length >= 8 && text.split(\"\").every(function (c) { return c >= \"0\" && c <= \"9\"; });' '}' > redact.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
マスクの処理を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function redact(text, names) {' ' const map = {};' ' const seen = {};' ' const words = text.split(\" \");' ' for (let i = 0; i < words.length; i++) {' ' let bare = words[i];' ' while (bare.length > 0 && \".,。、\".includes(bare.slice(-1))) bare = bare.slice(0, -1);' ' const tail = words[i].slice(bare.length);' ' let kind = \"\";' ' if (bare.includes(\"@\")) kind = \"メール\";' ' if (allDigits(bare)) kind = \"電話\";' ' if (names.includes(bare)) kind = \"氏名\";' >> redact.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
マスクの処理を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' if (kind === \"\") continue;' ' if (seen[bare] === undefined) {' ' const n = Object.keys(map).filter(function (k) { return k.startsWith(\"[\" + kind); }).length + 1;' ' seen[bare] = \"[\" + kind + \"_\" + n + \"]\";' ' map[seen[bare]] = bare;' ' }' ' words[i] = seen[bare] + tail;' ' }' ' return { text: words.join(\" \"), map: map };' '}' >> redact.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
マスクの処理を書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function restore(text, map) {' ' let out = text;' ' for (const key of Object.keys(map)) out = out.split(key).join(map[key]);' ' return out;' '}' 'async function main() {' ' const safe = redact(ticket, names);' ' console.log(\"モデルに送る文章: \" + safe.text);' ' console.log(\"@が残っている: \" + safe.text.includes(\"@\"));' ' const rule = \"お客様への二文の返信を書いてください。角かっこの置き換え文字は、書かれているとおりに残してください。\";' ' const messages = [{ role: \"system\", content: rule }, { role: \"user\", content: safe.text }];' ' const sample = \"[氏名_1]様、お問い合わせありがとうございます。本日中に[メール_1]へご連絡します。\";' >> redact.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
マスクの処理を書く(5)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' const reply = await ask(messages, sample, 100);' ' console.log(\"置き換え文字つきの返信: \" + reply);' ' console.log(\"お客様向けに元へ戻した返信: \" + restore(reply, safe.map));' ' console.log(\"ログの行: \" + Object.keys(safe.map).length + \" 件を置き換え、返信は \" + reply.length + \" 文字\");' '}' 'main();' >> redact.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
マスクの処理を実行
docker exec -w /home/node/project agent node redact.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 意味 |
|---|---|
| モデルに送る文章 | 値の代わりに [氏名_1]、[メール_1]、[電話_1]、[氏名_2] が入った問い合わせ |
| @が残っている | false |
| 記録済みサンプル | モデルが答えなかったときだけ、三行目に出る |
| 置き換え文字つきの返信 | 置き換え文字を残した二文。言葉は実行のたびに変わる |
| お客様向けに元へ戻した返信 | 本当の値を戻した返信 |
| ログの行 | 置き換えた件数と返信の長さだけで、個人の文章はない。長さは返信によって変わる |
学生が入力するセルは、このプレビューでは空白です。
ログに入れてよいもの
ログは、アプリより多くの人に読まれ、より長く残ります。不具合を見つけるのに必要なものを記録します。リクエストID、利用者ID、件数、時刻、判断です。
プロンプトと返信を、全文は記録しません。デバッグのために残すなら、マスクしたものを、短い期間、アクセス制限をつけて残します。
よくある間違い
注意
プロンプトをマスクすれば、ログは安全だと考える人が多くいます。返信、エラーメッセージ、元のリクエストには、まだ本当の値が残っていることがあります。文章が通るすべての場所を確かめます。プロンプト、返信、ログ、そしてキャッシュです。