モデルがファイルを求める
モデルのツールを制限するとは、各ツールに何を許すかを、モデルではなく自分のコードが決めることです。モデルの要求はテキストで、テキストは間違っていたり、騙されていたり、作り話だったりします。
メモのアシスタントには、名前でメモを読むツールがあります。返信が「../private」というメモを求めました。ツールは素直に試すでしょう。ポリシーが、動く前にそれを止めます。
このレッスンのゴール
メモ
何かを実行する前に、各ツールの要求を許可リストのポリシーと照らして検査することができる。
重要語句
| ことば | いみ |
|---|---|
| 許可リスト | 許されるツールと値だけを挙げた一覧 |
| ポリシー | 各ツールが受け取ってよいものを示すデータ |
| 引数の検査 | ツールを動かす前に、その入力を調べること |
| 最小権限 | ツールに、必要な力だけを与えること |
| タイムアウト | 呼び出しにかけてよい時間の上限 |
学生が入力するセルは、このプレビューでは空白です。
許可リストは、許すものだけを挙げる
禁止リストは悪いものを挙げ、残りを通します。必ず何かが漏れます。許可リストは良いものを挙げ、残りを断ります。
ポリシーはデータとして書きます。各ツール、各引数、その型と制限です。一つの関数がポリシーを読み、すべての要求を判断します。モデル自身は、何も実行しません。
例: ポリシーと四つの要求
| 要求 | ポリシーの内容 | 判断 |
|---|---|---|
| read_note、name が menu | name は todo、menu、hours のどれか | 許可 |
| read_note、name が ../private | name は todo、menu、hours のどれか | 拒否: 許可されていない |
| run_shell、任意の文章 | そのツールはない | 拒否: 不明なツール |
| add_numbers、b が 99999 | b は1000以下 | 拒否: 大きすぎる |
学生が入力するセルは、このプレビューでは空白です。
モデルの文章からコマンドを作らない
コマンドの実行、式の評価、ファイルの書き込みは、つい使いたくなります。モデルが書いた文章から、どれも作ってはいけません。巧妙なフィルターより、拒否のほうが安全です。
本物のコードを動かす必要があるなら、秘密のない別のサンドボックスで、時間制限をつけて動かします。このラボにはそれがないので、レッスンでは拒否するだけです。
モデル用の補助を書く(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 notes = { todo: \"小麦粉を買う\", menu: \"パン、ケーキ\", hours: \"8時から営業\" };' 'const policy = {' ' read_note: { name: { type: \"string\", allowed: Object.keys(notes) } },' ' add_numbers: { a: { type: \"number\", max: 1000 }, b: { type: \"number\", max: 1000 } },' '};' > policy.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ポリシーを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function check(request) {' ' const rule = policy[request.tool];' ' if (rule === undefined) return \"拒否: 不明なツール\";' ' const args = request.args || {};' ' for (const key of Object.keys(args)) {' ' if (rule[key] === undefined) return \"拒否: 想定外の引数 \" + key;' ' }' ' for (const key of Object.keys(rule)) {' ' const spec = rule[key];' ' const value = args[key];' ' if (typeof value === spec.type === false) return \"拒否: \" + key + \" の型が違う\";' ' if (spec.allowed && spec.allowed.includes(value) === false) return \"拒否: \" + key + \" は許可されていない\";' ' if (typeof spec.max === \"number\" && value > spec.max) return \"拒否: \" + key + \" が大きすぎる\";' ' }' ' return \"ok\";' '}' >> policy.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ポリシーを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function run(request) {' ' if (check(request) === \"ok\") {' ' return request.tool === \"read_note\" ? notes[request.args.name] : request.args.a + request.args.b;' ' }' ' return check(request);' '}' 'const scripted = [' ' { tool: \"read_note\", args: { name: \"menu\" } },' ' { tool: \"read_note\", args: { name: \"../private\" } },' ' { tool: \"run_shell\", args: { text: \"フォルダを一覧にする\" } },' ' { tool: \"add_numbers\", args: { a: 5, b: 99999 } },' ' { tool: \"add_numbers\", args: { a: 2, b: 3, c: 4 } },' '];' 'for (const request of scripted) console.log(request.tool + \" -> \" + run(request));' >> policy.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(\"}\");' ' try { return JSON.parse(text.slice(start, end + 1)); } catch (error) { return null; }' '}' 'async function main() {' ' const question = [{ role: \"user\", content: \"hoursという名前のメモを読むための、キーがtoolとargsのJSONオブジェクトだけを返信してください。ツール: read_note(name), add_numbers(a, b)。\" }];' ' const sample = JSON.stringify({ tool: \"read_note\", args: { name: \"hours\" } });' ' const request = pullJson(await ask(question, sample, 80));' ' console.log(\"モデルの要求: \" + JSON.stringify(request));' ' console.log(\"結果: \" + (request === null ? \"拒否: JSONではない\" : run(request)));' '}' 'main();' >> policy.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ポリシーを実行
docker exec -w /home/node/project agent node policy.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 意味 |
|---|---|
| read_note -> パン、ケーキ | 最初の要求は、すべての検査を通った |
| read_note -> 拒否: name は許可されていない | パスの細工が、許可リストで失敗した |
| run_shell -> 拒否: 不明なツール | そのツールはポリシーにない |
| add_numbers -> 拒否: b が大きすぎる | 数が上限の1000を超えた |
| add_numbers -> 拒否: 想定外の引数 c | ポリシーにない引数は断られる |
| 記録済みサンプル | モデルが答えなかったときだけ、台本の五行の後に出る |
| モデルの要求、そして結果 | 実行のたびに変わる。モデルのツール要求も同じ検査を通る |
学生が入力するセルは、このプレビューでは空白です。
時間と権限を制限する
タイムアウトは、遅いツールが要求をいつまでも抱えるのを防ぎます。書いた補助は、すでに30秒でモデルをあきらめます。
最小権限も適用します。メモを読むツールがメモを書けてはいけません。書くツールが届くのは、自分のフォルダだけにします。
よくある間違い
注意
モデルにルールを伝えてあれば、ツールは検査しなくてよいと考える人が多くいます。モデルは間違えたり、騙されたり、ツールを作り出したりします。必ず、ツールを動かすコードの中で検査してください。毎回同じことをするのは、そのコードだけです。