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

無料レッスン

エージェントのリサーチチームを作る

このレッスンのあとで: 予算、確認、止まる規則を持たせて、オーケストレーターとワーカーを動かせる。

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

チームを組み立てる

エージェントのリサーチチームは、オーケストレーター、数人のワーカー、各結果の確認、止まる規則でできています。ここまでの部品にはどれも費用がかかり、上限のないチームには終わりがありません。

このレッスンは、モジュール全体を、トークンの予算つきの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文

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

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

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