役に立ち、漏らさない記録
ログとは、アプリが何をしたかを書き残した記録です。返事が遅いときや間違うとき、まずログを見ます。
不注意なログは、すべてのプロンプトとメールを、多くの人が読めるファイルに写してしまいます。目指すのは、直すのに足りるだけ残し、それ以上は残さないことです。
このレッスンのゴール
メモ
リクエストを直すのに足りるだけ記録し、残してはいけないものは残さないことができる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| ログの記録 | 一つの出来事について、プログラムが書く一行 | |
| リクエスト ID | 一つのリクエストの記録を、すべて結ぶ札 | r-001 |
| ハッシュ | 値から作る短い符号。元には戻せない | |
| ソルト | ハッシュにする前に混ぜる、秘密の言葉 | |
| 切り詰める | 値を、数文字だけに短くすること |
学生が入力するセルは、このプレビューでは空白です。
直すのに要るものを残す
まず、ログを何に使うかを考えます。遅い返事を直すなら、リクエスト ID、時間、モデル、文字数が要ります。言葉そのものは要りません。
言葉のない記録も、元のリクエストに結び付けられます。結ぶ鍵が、リクエスト ID です。
例: 一つの記録の前と後
| 項目 | 生の記録 | 安全な記録 | 理由 |
|---|---|---|---|
| request_id | r-001 | r-001 | ログとリクエストを結ぶ |
| user_email | [email protected] | ハッシュ 8 文字 | ソルト付きのハッシュで、人数は数えられる |
| message | 注文の返金をお願いします | 12(文字数) | 言葉はログに入れない |
| ms | 420 | 420 | 遅い返事を探すのに要る |
学生が入力するセルは、このプレビューでは空白です。
ハッシュは値を隠すが、人は隠さない
ハッシュは、メールを短い符号にします。同じメールはいつも同じ符号になるので、人数は数えられます。符号を見ても、元のメールには戻せません。
ソルトを混ぜると、符号を当てにくくなります。そのうえで、誰がログを読めるかも決めます。誰も読む必要のないログは、作らないのが一番です。
メールの最初の数文字だけに切り詰める方法もあります。ただ、残った文字から人を当てられることがあるので、ハッシュと同じく、まだ個人情報として扱います。
よくある間違い
注意
ハッシュは匿名だ、と考える人が多いです。そうではありません。メールの一覧を持つ人は、全部をハッシュにして符号を照らし合わせられます。ハッシュは仮の名前で、ログはまだ個人情報です。ソルトを使い、ソルトをログから離して置きます。
助っ人 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;' '}' > 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 };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
安全な記録の道具を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const crypto = require(\"crypto\");' 'const raw = [' ' { request_id: \"r-001\", user_email: \"[email protected]\", message: \"注文の返金をお願いします\", model: \"lab\", ms: 420 },' ' { request_id: \"r-002\", user_email: \"[email protected]\", message: \"090-0000-1111に電話してください\", model: \"lab\", ms: 380 },' ' { request_id: \"r-003\", user_email: \"[email protected]\", message: \"荷物はどこにありますか\", model: \"lab\", ms: 910 },' '];' > logsafe.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
安全な記録の道具を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function hash8(v) {' ' return crypto.createHash(\"sha256\").update(\"lab-salt:\" + v).digest(\"hex\").slice(0, 8);' '}' 'function safe(r) {' ' return { request_id: r.request_id, user: hash8(r.user_email), length: r.message.length, model: r.model, ms: r.ms };' '}' 'function holdsEmail(r) {' ' return Object.values(r).some(function (v) {' ' return typeof v === \"string\" && (v.includes(\"@\") || v.includes(\"@\"));' ' });' '}' >> logsafe.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
安全な記録の道具を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const out = raw.map(safe);' ' raw.forEach(function (r, i) {' ' console.log(r.request_id + \" 生の記録にメール: \" + holdsEmail(r) + \" | 安全な記録にメール: \" + holdsEmail(out[i]));' ' console.log(\" \" + JSON.stringify(out[i]));' ' });' ' console.log(\"r-001 と r-003 は同じ利用者: \" + (out[0].user === out[2].user));' ' console.log(\"r-001 と r-002 は同じ利用者: \" + (out[0].user === out[1].user));' ' const q = \"短く一文で答えてください。ログには、メッセージそのものではなく、なぜ文字数を残すのですか。\";' ' const sample = \"文字数は遅い返事を調べる役に立ち、本人については何も含まないからです。\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 80);' ' console.log(\"モデル: \" + reply.text);' '}' 'main();' >> logsafe.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
安全な記録の道具を動かす
docker exec -w /home/node/project agent node logsafe.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
Run の出力
| 行 | 意味 |
|---|---|
| r-001 生の記録にメール: true | 安全な記録にメール: false | 安全な記録にメールはない |
| {"request_id":"r-001","user":"...",...} | ハッシュと文字数だけ |
| r-001 と r-003 は同じ利用者: true | 同じ人は今も結び付く |
| r-001 と r-002 は同じ利用者: false | 違う二人は別のまま |
| モデル: ... | 一文。言葉は毎回変わる |
学生が入力するセルは、このプレビューでは空白です。