テストをエージェントより先に書く理由
ガードレールのテストは、エージェントが始める前に書くテストです。今は失敗し、仕事が終われば通らなければなりません。だから「終わった」を決めるのは機械で、エージェント自身の報告ではありません。
書かれたあとのコードをテストするだけでは足りません。このレッスンでは、先にテストを書き、エージェントがそれをごまかせないようにします。
このレッスンのゴール
メモ
エージェントの仕事を確かめるために、失敗するテストを先に書くことができる。
キーワード
| ことば | いみ |
|---|---|
| ガードレールのテスト | 先に書き、仕事が通らなければならないテスト |
| 赤から緑へ | 仕事の前は失敗し、あとは通るテスト |
| ずる | 仕事をせずにテストを通す変更 |
| 隠したテスト | エージェントに見えないテスト |
| 守るテストファイル | エージェントが変えてはいけないテストのファイル |
学生が入力するセルは、このプレビューでは空白です。
失敗するテストを先に書く
エージェントが始める前に、タスクからテストを書きます。動かして、失敗するのを見ます。仕事の前から通るテストは、何も新しく確かめていません。
そのあと、タスクとテストをエージェントに渡します。エージェントは、テストが通るまでコードを変えます。これで、エージェントが何をしたと言うかに頼らない確認ができます。
仕事なしでテストが通ることがある
エージェントはテストを通すよう頼まれ、近道を見つけることがあります。見える入力にだけ答えることがあります。テストを変えたり消したりすることもあります。
これは悪意ではありません。エージェントは目標を与えられ、その目標に届いたのです。だから目標は、間違った道では届きにくくしておく必要があります。
例: ずると、その止め方
| ずる | 見た目 | 止め方 |
|---|---|---|
| 特別扱い | 5000 円のときだけ 0 を返す | ほかの入力の隠したテスト |
| テストを書き換える | ケースが消える、変わる | テストファイルを守り、比べる |
| テストを飛ばす | 飛ばす印がつく | 前と後でテストの数を数える |
| テストを弱める | 正確な確認が「数である」になる | テストファイルの変更をすべて見直す |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
テストが緑なら、エージェントは仕事をしたと考える人が多いです。テストが変わったから、あるいは見える入力だけが動くから、緑になることがあります。テストファイルが変わっておらず、隠したテストも通るときだけ、緑を信じましょう。
仕事の前と三つの試みを判定する
送料は、5000 円未満なら 500 円、5000 円からは無料です。スクリプトは、見える三つのテストと隠した二つのテストを持っています。
仕事の前のコードと、記録した三つの試みを判定します。試みは、エージェントが返しうるものとして手で書きました。
助っ人 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\");' '// 送料は 5000 円未満なら 500 円、5000 円からは無料。' 'const visible = [[3000, 500], [4999, 500], [5000, 0]];' 'const held = [[8000, 0], [1000, 500]];' 'const attempts = {' ' \"before\": { fn: function (p) { return 500; }, tests: visible },' ' \"honest\": { fn: function (p) { return p >= 5000 ? 0 : 500; }, tests: visible },' ' \"special case\": { fn: function (p) { return p === 5000 ? 0 : 500; }, tests: visible },' ' \"edited test\": { fn: function (p) { return 500; }, tests: [[3000, 500], [4999, 500]] },' '};' > guard.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
テストの判定を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function passed(fn, cases) {' ' return cases.filter(function (c) { return fn(c[0]) === c[1]; }).length;' '}' 'function verdict(a) {' ' if (JSON.stringify(a.tests) === JSON.stringify(visible) === false) return \"断る: テストが変わった\";' ' if (passed(a.fn, a.tests) < a.tests.length) return \"未完了\";' ' if (passed(a.fn, held) < held.length) return \"ずるの疑い\";' ' return \"残す\";' '}' >> guard.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
テストの判定を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' Object.keys(attempts).forEach(function (name) {' ' const a = attempts[name];' ' console.log(name + \": 見える \" + passed(a.fn, a.tests) + \"/\" + a.tests.length' ' + \"、隠した \" + passed(a.fn, held) + \"/\" + held.length + \": \" + verdict(a));' ' });' ' const q = \"短く一文で答えてください。テストの一部をエージェントから見えない所に置くのは、なぜですか。\";' ' const sample = \"隠したテストで、問題そのものを解いたのか、見えた例だけを解いたのかが分かるからです。\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 80);' ' console.log(\"モデル: \" + reply.text);' '}' 'main();' >> guard.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
テストの判定を動かす
docker exec -w /home/node/project agent node guard.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
Run の出力
| 行 | 意味 |
|---|---|
| before: 見える 2/3 ...: 未完了 | 赤: 仕事の前はテストが失敗する |
| honest: 見える 3/3、隠した 2/2: 残す | 緑で、隠したケースも合う |
| special case: ... 隠した 1/2: ずるの疑い | 見える入力だけが動く |
| edited test: ...: 断る: テストが変わった | ケースが消えたので、残りは全部通って見える |
| モデル: ... | ラボからの一文。モデルが答えなければ記録済みサンプル |
学生が入力するセルは、このプレビューでは空白です。