チームを組み立てる
エージェントのリサーチチームは、オーケストレーター、数人のワーカー、各結果の確認、止まる規則でできています。ここまでの部品にはどれも費用がかかり、上限のないチームには終わりがありません。
このレッスンは、モジュール全体を、トークンの予算つきの1つのループにまとめます。
このレッスンのゴール
メモ
予算、確認、止まる規則を持たせて、オーケストレーターとワーカーを動かせる。
キーワード
| ことば | いみ |
|---|---|
| 予算 | 実行全体が使ってよいトークンの最大量 |
| 止まる規則 | 依頼が尽きる前に、実行を終わらせる判定 |
| 受理 | すべての確認を通った結果 |
| 却下 | 理由つきで外された結果 |
| 使用済み | ここまでに使ったトークン。良い結果も悪い結果も含む |
| 必要数 | 実行を終わらせる、受理した結果の件数 |
学生が入力するセルは、このプレビューでは空白です。
1つのループ、3つの上限
オーケストレーターは、依頼を順に取ります。各依頼の前に、2つ尋ねます。もう十分にそろっているか。この依頼で予算を超えるおそれはないか。
そのあとワーカーに尋ね、結果を確かめます。返答にはすべて費用がかかるので、使用済みには、良い結果も悪い結果も数えます。
例: ループの1周
| 段階 | 問い | はいなら |
|---|---|---|
| 1 | 受理した結果は十分か | 止まる。理由は enough |
| 2 | 使用済み + この依頼の上限が予算を超えるか | 止まる。理由は budget |
| 3 | ワーカーに尋ね、そのトークンを使用済みに足す | いつでも。良くても悪くても |
| 4 | 結果は確認を通るか | 受理する。通らなければ理由つきで却下 |
学生が入力するセルは、このプレビューでは空白です。
呼び出しの前に上限を置く理由
呼び出しのあとに予算を調べても、遅すぎます。トークンはもう使われています。そこでループは、最悪の場合を先に取っておきます。依頼自身の上限を使い、収まらないなら止まります。
同じ考え方が、時間とラウンド数も守ります。チームには、いつも終わらせる規則があります。
よくある間違い
注意
良い結果だけを予算に数える人が多くいます。失敗したワーカーも、トークンを読み書きしています。実行が頼んだものはすべて数えてください。そうしないと、請求が予算と合いません。
モデル用の補助を書く(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 BRIEFS = [' ' { id: \"b1\", question: \"日曜開館を希望する来館者は何人ですか。\", sources: [\"s1\"], max: 900 },' ' { id: \"b2\", question: \"日曜1日の人件費はいくらですか。\", sources: [\"s2\"], max: 900 },' ' { id: \"b3\", question: \"週末の1日は、どれくらい混みますか。\", sources: [\"s3\"], max: 900 },' '];' '// 台本どおりの3人のワーカーの返答。記録済みです。' 'const RECORDED = {' ' b1: { answer: \"400人中212人が希望しています\", sources: [\"s1\"], tokens: 700 },' ' b2: { answer: \"日曜1日で10800円です\", sources: [\"s2\"], tokens: 850 },' ' b3: { answer: \"土曜は310人が来館します\", sources: [\"s3\"], tokens: 1200 },' '};' > team.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リサーチチームを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function problems(b, r) {' ' const out = [];' ' if (r.answer.trim().length === 0) out.push(\"答えがありません\");' ' r.sources.forEach(function (s) { if (b.sources.includes(s) === false) out.push(\"知らない出典 \" + s); });' ' if (r.tokens > b.max) out.push(\"上限を超えました\");' ' return out;' '}' >> team.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リサーチチームを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function team(budget, enough) {' ' const out = { accepted: [], rejected: [], spent: 0, stopped: \"完了\" };' ' for (const b of BRIEFS) {' ' if (out.accepted.length >= enough) { out.stopped = \"十分\"; break; }' ' if (out.spent + b.max > budget) { out.stopped = \"予算\"; break; }' ' const r = RECORDED[b.id];' ' out.spent += r.tokens;' ' const p = problems(b, r);' ' if (p.length > 0) out.rejected.push(b.id + \"(\" + p.join(\"、\") + \")\");' ' else out.accepted.push(r.answer);' ' }' ' return out;' '}' >> team.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リサーチチームを書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' let first = null;' ' for (const run of [[2500, 3], [1500, 3], [4000, 2]]) {' ' const o = team(run[0], run[1]);' ' first = first || o;' ' console.log(\"予算 \" + run[0] + \"、必要数 \" + run[1] + \": 受理 \" + o.accepted.length + \"、却下 \" + o.rejected.length + \"、使用 \" + o.spent + \"、停止理由 \" + o.stopped);' ' if (o.rejected.length > 0) console.log(\" 却下: \" + o.rejected.join(\"、\"));' ' }' ' const q = [{ role: \"user\", content: \"次の発見から1文を書いてください: \" + first.accepted.join(\"、\") }];' ' const r = await ask(q, \"来館者は日曜開館を希望していて、日曜1日の人件費は10800円です。\", 40);' ' console.log(\"要約: \" + r.text.trim());' '}' 'main();' >> team.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リサーチチームを実行する
docker exec -w /home/node/project agent node team.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ワーカーは記録済み
3人のワーカーは、トークン数つきの記録済みの返答です。だから、どの学生にも同じ合計が見えます。ライブ呼び出しは、受理した内容の要約文を作らせます。
本物のチームは、各依頼をモデルに送ります。その周りのループは同じです。
1回目の Run は、予算 2500 に対して 2750 を使います。3つ目のワーカーが、自分の上限 900 を守らず、1200 を使ったからです。予算の確認は呼び出しの前なので、この使いすぎは止められません。だから、その返答は却下されても、使用には数えられます。
出力の一例
| 行 | 何を示すか |
|---|---|
| 予算 2500、必要数 3: 受理 2、却下 1、使用 2750、停止理由 完了 | 使用が 2500 を超えて 2750 になったのは、3つ目のワーカーが自分の上限 900 を破り、1200 を使ったから。予算は呼び出しの前に調べるので、呼び出しの中の使いすぎは止められない |
| 却下: b3(上限を超えました) | 理由が残る |
| 予算 1500、必要数 3: 受理 1、却下 0、使用 700、停止理由 予算 | 2つ目の依頼は収まらないかもしれなかった |
| 予算 4000、必要数 2: 受理 2、却下 0、使用 1550、停止理由 十分 | 良い結果が2つで足りた |
| 要約: … | 実行のたびに変わる1文 |
学生が入力するセルは、このプレビューでは空白です。