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 | 各項目が最後に書かれた版 |
学生が入力するセルは、このプレビューでは空白です。