部品を一つのループにまとめる
エージェントハーネスは、モデルを包むプログラムです。ウィンドウを作り、ツールを実行し、モードを適用し、変更を記録します。モデルは、一つの段階を提案するだけです。何が起きるかを決めるのは、ハーネスです。
このレッスンでは、前の五つのレッスンを、実行して巻き戻せる一つのループにまとめます。
このレッスンのゴール
メモ
コンテキスト、ツール、モード、分離、元に戻す仕組みを一つのハーネスにまとめ、実行して巻き戻せる。
キーワード
| ことば | いみ |
|---|---|
| ハーネス | モデルを包んで、エージェントを動かすプログラム |
| ループ | 段階を頼み、判断し、実行し、記録して、繰り返すこと |
| 台本のモデル | モデルの代わりをする、決まった段階のリスト |
| 経過の記録 | 各段階とその結果の記録 |
| ウィンドウ | 次にモデルへ送る、最近の結果 |
| 巻き戻し | 選んだ段階より後の変更を、すべて元に戻すこと |
学生が入力するセルは、このプレビューでは空白です。
ループを、段階ごとに見る
毎回、ハーネスはモデルに段階を一つ頼みます。モードで判断します。許可、確認、拒否のどれかです。確認が必要なら、利用者が答えます。段階が許可されたら、ハーネスは置き換えるものを記録してから、実行します。
結果はウィンドウに入り、ウィンドウは最近のものだけを残します。そのあと、モデルが終わりと言うまで、ループが繰り返されます。
前のレッスンが加えるもの
| レッスン | ループの中の位置 |
|---|---|
| コンテキスト | ウィンドウは最近の結果だけを残す |
| ツール | 各段階が、狭いツールの名前を持つ |
| モード | 一つの関数が、許可、確認、拒否を決める |
| 分離 | 副次的な作業が、短い結果をウィンドウに返す |
| 元に戻す | 変更の前に、記録を一つ書く |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
多くの人は、拒否された段階を消してしまいます。するとモデルは、拒否されたことを学べず、また頼みます。拒否を結果として記録し、次のウィンドウに見せます。
モデル用の補助を書く(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"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用の補助を書く(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 d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"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 RISK = { read_file: \"read\", write_file: \"write\", delete_file: \"destructive\", run: \"run\" };' 'const script = [' ' { tool: \"read_file\", path: \"app.py\" },' ' { tool: \"write_file\", path: \"app.py\", text: \"x = 2\" },' ' { tool: \"run\", path: \"pytest\" },' ' { tool: \"delete_file\", path: \"notes.txt\" },' ' { tool: \"write_file\", path: \".env\", text: \"KEY=1\" },' ' { tool: \"done\" },' '];' 'const files = { \"app.py\": \"x = 1\", \"notes.txt\": \"あとで\" };' 'const log = [];' > harness6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ハーネスを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function decide(mode, risk, path) {' ' if (mode === \"plan\") return risk === \"read\" ? \"allow\" : \"deny\";' ' if (path.startsWith(\".env\") || risk === \"destructive\") return \"ask\";' ' if (mode === \"auto\" && (risk === \"read\" || risk === \"write\")) return \"allow\";' ' if (mode === \"ask\" && risk === \"read\") return \"allow\";' ' return \"ask\";' '}' >> harness6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ハーネスを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function act(s, n) {' ' if (s.tool === \"read_file\") return s.path in files ? files[s.path] : \"なし\";' ' if (s.tool === \"run\") return \"実行 \" + s.path;' ' log.push({ step: n, path: s.path, before: s.path in files ? files[s.path] : null });' ' if (s.tool === \"write_file\") { files[s.path] = s.text; return \"書き込み \" + s.path; }' ' delete files[s.path];' ' return \"削除 \" + s.path;' '}' >> harness6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ハーネスを書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function runAll(mode, approvals) {' ' const out = [];' ' for (let i = 0; i < script.length; i++) {' ' const s = script[i];' ' if (s.tool === \"done\") break;' ' const v = decide(mode, RISK[s.tool], s.path);' ' let outcome = v === \"allow\" ? \"ok\" : \"拒否\";' ' if (v === \"ask\") outcome = approvals.shift() === true ? \"承認\" : \"却下\";' ' const good = outcome === \"ok\" || outcome === \"承認\";' ' out.push({ step: i + 1, tool: s.tool, path: s.path, outcome: outcome, result: good ? act(s, i + 1) : outcome });' ' }' ' return out;' '}' >> harness6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ハーネスを書く(5)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function undoTo(step) {' ' for (const e of log.slice().reverse()) {' ' if (e.step <= step) continue;' ' if (e.before === null) delete files[e.path];' ' else files[e.path] = e.before;' ' }' '}' >> harness6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ハーネスを書く(6)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const out = runAll(\"auto\", [true, false]);' ' for (const o of out) console.log(o.step + \" \" + o.tool + \" \" + o.path + \" \" + o.outcome + \" -> \" + o.result);' ' console.log(\"ウィンドウ \" + JSON.stringify(out.slice(-3).map(function (o) { return o.result; })));' ' console.log(\"ファイル \" + JSON.stringify(files));' ' undoTo(1);' ' console.log(\"1 まで戻した後 \" + JSON.stringify(files));' ' const text = out.map(function (o) { return o.tool + \" \" + o.outcome; }).join(\"、\");' ' const reply = await ask([{ role: \"user\", content: \"次の操作を行った実行について、コミットメッセージを一行で書いてください: \" + text }], \"app.py のかごの合計を修正する\", 60);' ' console.log(\"メッセージ: \" + reply.text);' '}' 'main();' >> harness6.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ハーネスを実行して巻き戻す
docker exec -w /home/node/project agent node harness6.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデルは台本
段階は決まったリストから来るので、だれが実行しても同じ実行になります。本物のハーネスは、各段階をモデルに頼みます。短い呼び出しは、実行内容からコミットメッセージを書くだけです。
モデルがなければ、その行は記録済みサンプルを表示します。
出力の一例
| 行 | 示すこと |
|---|---|
| 1 read_file app.py ok -> x = 1 | auto モードで、読み取りは通る |
| 3 run pytest 承認 -> 実行 pytest | 一つ目の承認は「はい」だった |
| 4 delete_file notes.txt 却下 -> 却下 | 二つ目の承認は「いいえ」だった |
| 5 write_file .env 却下 -> 却下 | 承認が残っておらず、「いいえ」になった |
| ファイル {"app.py":"x = 2","notes.txt":"あとで"} | 書き込みだけが何かを変えた |
| 1 まで戻した後 {"app.py":"x = 1",...} | 巻き戻しで、書き込みが元に戻った |
学生が入力するセルは、このプレビューでは空白です。