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

無料レッスン

エージェント間の共有状態

このレッスンのあとで: エージェントが互いを上書きせずに記録を共有できるようにし、衝突を報告できる。

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

2つのエージェント、1つの記録、消えた更新

共有状態とは、複数のエージェントが読み書きする記録です。2つのエージェントが同時に読み、そのあと同時に書くと、お互いの仕事を気づかないうちに消してしまうことがあります。

何も壊れません。記録には最後の書き込みが残るだけで、良い発見が消えます。

このレッスンのゴール

メモ

エージェントが互いを上書きせずに記録を共有できるようにし、衝突を報告できる。

キーワード

ことばいみ
共有の記録全エージェントが読み書きする、事実の1つのコピー
メッセージあるエージェントから別のエージェントへ送る文
版受理された書き込みのたびに増える数
基準の版書く人が、書く前に読んだ版
衝突読んだあとに変わった項目を基にした書き込み
消えた更新気づかないうちに別の書き込みを消す書き込み

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

メッセージか、共有の記録か

エージェントはメッセージを渡し合えます。各自が聞いたことのコピーを持ち、衝突は起きませんが、コピーが少しずつずれていきます。

あるいは、1つの記録を共有します。真実は1つですが、2人の書き手がぶつかることがあります。多くのチームには両方が必要です。何をするかはメッセージで、分かっていることは記録で扱います。

例: メッセージと記録

メッセージ共有の記録
事実を持つのは各エージェントが別々に1か所
2つのエージェントがずれるかずれる。だれも気づかないずれない。値は1つ
書き込みが消えるか消えない確認しなければ消える
向いているのはワーカーに何をするか伝えるチームがここまでに見つけたこと

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

書き込みは読んだ版を名乗る

各項目に版を持たせます。エージェントは読むときに版を覚え、書くときにその基準の版を一緒に送ります。

記録は、そのあと項目が変わっていないときだけ書き込みを受け付けます。変わっていれば衝突を返し、エージェントは読み直して決めます。まだ誰も書いていない項目は、衝突しません。

よくある間違い

注意

衝突を、あとの書き込みを勝たせて片づける人が多くいます。それは消えた更新で、ログが1行増えるだけです。衝突は書く人への問いです。新しい値を読んでから、決めてください。

モデル用の補助を書く(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 record = { version: 0, fields: {} };' 'function read() {' '  return { version: record.version, fields: JSON.parse(JSON.stringify(record.fields)) };' '}' > state.js"

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

共有の記録を書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// 書き込みは、読んだ版を名乗ります。そのあと誰も項目を変えていないときだけ通ります。' 'function write(agent, field, value, base) {' '  const f = record.fields[field];' '  const word = \" が \" + field + \" を版 \" + base + \" から書く: \";' '  if (f && f.version > base) {' '    console.log(agent + word + \"衝突。版 \" + f.version + \" を持つのは \" + f.by);' '    return false;' '  }' '  record.version += 1;' '  record.fields[field] = { value: value, version: record.version, by: agent };' '  console.log(agent + word + \"反映。記録は版 \" + record.version);' '  return true;' '}' >> state.js"

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

共有の記録を書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const a = read();' '  const b = read();' '  write(\"価格担当\", \"cost\", \"10800円\", a.version);' '  if (write(\"読み手\", \"cost\", \"12000円\", b.version) === false) {' '    const again = read();' '    console.log(\"読み手が読み直す: cost は \" + again.fields.cost.value + \"、版 \" + again.version);' '    write(\"読み手\", \"cost_note\", \"2つ目の数字は12000円でした\", again.version);' '  }' '  write(\"読み手\", \"wants\", \"400人中212人\", b.version);' '  await last();' '}' >> state.js"

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

共有の記録を書く(4)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function last() {' '  const q = [{ role: \"user\", content: \"同じ費用について2つの数字が食い違っている、と伝える12語以内のメモを書いてください。\" }];' '  const note = (await ask(q, \"同じ費用の数字が食い違っています。出典を確認してください。\", 30)).text.trim();' '  write(\"レビュアー\", \"note\", note, record.version);' '  const names = Object.keys(record.fields);' '  console.log(\"項目: \" + names.map(function (k) { return k + \"@\" + record.fields[k].version; }).join(\"、\"));' '}' 'main();' >> state.js"

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

共有の記録を実行する

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

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

これは代用品です

ラボにはデータベースがないので、記録はスクリプト内のオブジェクトです。本物のチームは、この確認を1つの手順でこなすデータベースを使います。確認してから書く、という2つの手順では、別の書き手が入り込む隙ができます。

ライブ呼び出しは、メモの文面を作らせます。言葉は実行のたびに変わります。

出力の一例

行何を示すか
価格担当 が cost を版 0 から書く: 反映。記録は版 1最初の書き手が勝つ
読み手 が cost を版 0 から書く: 衝突。版 1 を持つのは 価格担当2人目は古い版を読んでいた
読み手が読み直す: cost は 10800円、版 1衝突のあと、読み手は新しい値を読む
読み手 が cost_note を版 1 から書く: 反映。記録は版 2読み直してから、メモを書く
読み手 が wants を版 0 から書く: 反映。記録は版 3新しい項目は衝突しない
レビュアー が note を版 3 から書く: 反映。記録は版 4メモの文面はモデルが作るので、実行のたびに変わる
項目: cost@1、cost_note@2、wants@3、note@4各項目が最後に書かれた版

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

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

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