クリーム色の背景、色付き表示、高コントラスト、大きな文字のほうが読みやすいですか?配色と文字を選ぶと、すべてのページに一度で反映されます。

無料レッスン

AIエージェントの最終状態を検査する

このレッスンのあとで: 実行が何を言ったかではなく、何を変えたかを検査できる。

65分のレッスンコマンド7件AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

返金したと言ったのに

最終状態の検査は、実行が実際に変えたものを、変えるべきだったものと比べます。必要な理由は、エージェントが、仕事が済んでいてもいなくても、自信に満ちた報告を書くからです。

店のエージェントの2回の実行が、どちらも「A17 は返金済みです」と言います。実際に返金したのは、1回だけです。

このレッスンのゴール

メモ

実行が何を言ったかではなく、何を変えたかを検査できる。

キーワード

ことばいみ
最終状態実行が止まったときに、世界が持っているもの
期待状態正しい実行が残す最終状態
差分2つの状態が食い違う場所
副作用タスクが求めていない変更
レコード状態の中の1件。たとえば注文

学生が入力するセルは、このプレビューでは空白です。

文ではなく、状態を比べる

ほとんどのエージェントは、ファイル、行、チケットなど何かを変えます。そこで最終状態を検査します。世界の新しいコピーを作り、その上で実行をやり直し、結果を期待状態と比べます。

2つが食い違う場所は、すべて差分です。差分のリストが空なら合格です。

例: 記録済みの3回の実行

実行発言実際の操作状態の検査
AA17 は返金済みA17 を返金一致
BA17 は返金済みメモを書いただけA17 は支払い済みのまま
CA17 を返金しましたA17 と B02 を返金B02 が変わった。変えてはいけない

学生が入力するセルは、このプレビューでは空白です。

誰も求めていない変更を探す

頼まれた仕事をしても、害をなす実行があります。実行Cは、頼まれた A17 を返金したあと、2つ目の注文まで返金しました。検査がすべてのレコードを見るときだけ、これが見つかります。

副作用とは、タスクが触れずにおきたかったのに変わったレコードです。変更前と変更後の状態を比べて、そのレコードを並べます。

モデル用の補助を書く(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"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

店と実行を書く

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const start = {' '  A17: { status: \"支払い済み\", amount: 1200 },' '  B02: { status: \"発送済み\", amount: 800 },' '};' 'const expected = {' '  A17: { status: \"返金済み\", amount: 1200 },' '  B02: { status: \"発送済み\", amount: 800 },' '};' '// 記録済みの実行。それぞれ、言ったことと、実際にしたことが違います。' 'const runs = [' '  { name: \"実行A\", said: \"完了しました。A17 は返金済みです。\", actions: [[\"refund\", \"A17\"]] },' '  { name: \"実行B\", said: \"完了しました。A17 は返金済みです。\", actions: [[\"note\", \"A17\"]] },' '  { name: \"実行C\", said: \"A17 を返金しました。\", actions: [[\"refund\", \"A17\"], [\"refund\", \"B02\"]] },' '];' 'module.exports = { start, expected, runs };' > shop.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

状態の検査を書く(1)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { start, expected, runs } = require(\"./shop\");' 'const { ask } = require(\"./model\");' 'function copy(state) { return JSON.parse(JSON.stringify(state)); }' 'function apply(actions) {' '  const state = copy(start);' '  actions.forEach(function (action) {' '    if (action[0] === \"refund\") state[action[1]].status = \"返金済み\";' '  });' '  return state;' '}' > check.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

状態の検査を書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function differences(want, got) {' '  const lines = [];' '  Object.keys(want).sort().forEach(function (id) {' '    Object.keys(want[id]).sort().forEach(function (field) {' '      if ((want[id][field] === got[id][field]) === false) {' '        lines.push(id + \".\" + field + \": 期待 \" + want[id][field] + \"、実際 \" + got[id][field]);' '      }' '    });' '  });' '  return lines;' '}' >> check.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

状態の検査を書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'runs.forEach(function (run) {' '  const found = differences(expected, apply(run.actions));' '  console.log(run.name + \" の発言: \" + run.said);' '  console.log(run.name + \" の状態: \" + (found.length === 0 ? \"一致\" : found.join(\"; \")));' '});' 'async function main() {' '  const prompt = \"返金ツールが注文A17でエラーを返しました。上司に伝える短い1文を書いてください。\";' '  const sample = \"注文A17の返金はエラーで失敗したため、注文は支払い済みのままです。\";' '  const text = await ask([{ role: \"user\", content: prompt }], sample, 60);' '  console.log(\"ライブの報告: \" + text.trim());' '  console.log(\"状態の検査: A17 は今も \" + start.A17.status + \" です。言葉がどうであっても同じです。\");' '}' 'main();' >> check.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

3つの最終状態を検査する

コマンド
docker exec -w /home/node/project agent node check.js

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

検査が表示するもの

行示すこと
実行A一致
実行BA17.status: 期待 返金済み、実際 支払い済み
実行CB02.status: 期待 発送済み、実際 返金済み
ライブの報告出力例: 返金の失敗についての1文
状態の検査A17 は支払い済みのまま

学生が入力するセルは、このプレビューでは空白です。

試してみましょう

ヒント

shop.js で、実行Aの操作を refund A17 と note A17 の2つにして、もう一度検査します。状態は一致のままです。メモは、期待状態が持つものを何も変えないからです。

よくある間違い

注意

タスクが挙げたフィールドだけを比べる人がいます。それでは、ほかのものも消した実行が通ってしまいます。状態全体を比べるか、無視するフィールドとその理由を書き出します。実行のたびに変わるタイムスタンプなどが例です。

このラボとソースについて: AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

授業で使いますか?このモジュールを学生に割り当てると、教員用ガイド、自動採点つきの課題、学生ごとのラボを使えます。