プロンプトをコードのように出す
プロンプトは機能の動きを変えるので、コードと同じ丁寧さが要ります。ファイル、バージョン、レビュー、段階的な展開、そして戻す方法です。言葉を一つ変えると、一つのケースがよくなり、別のケースが壊れることがあり、テストがなければ誰も気づかないからです。
あるチームが、金曜の午後にダッシュボードでプロンプトを書き換えます。月曜に苦情が届きます。何が変わったのか、誰も言えず、元にも戻せません。
このレッスンのゴール
メモ
私は、プロンプトをコードのように版管理し、レビューし、展開できます。
キーワード
| ことば | いみ | れい |
|---|---|---|
| プロンプトの版 | 名前をつけて保存した、プロンプトのある時点の姿 | v2 |
| 評価ケース | 合否の規則を持つ一つの入力 | |
| 後退 | 前は合格し、今は失敗するケース | |
| 段階的な展開 | 変更を、ユーザーの一部にだけ届けること | |
| バケット | ユーザーごとの 0 から 99 の数 |
学生が入力するセルは、このプレビューでは空白です。
プロンプトは版つきのファイルに
プロンプトは、名前とバージョンをつけて、リポジトリのファイルに置きます。変更は、コミットとレビューと差分になります。コードは、貼りつけた文字列ではなく、名前とバージョンでプロンプトを指します。
バージョンは、ログの一行ごとに書きます。悪い答えが出たとき、どのプロンプトが作ったのか言えます。
記録済みの結果で変更を判断する
同じ評価ケースを、旧と新のプロンプトで動かします。合格率を比べ、後退を一覧にします。前は合格し、今は失敗するケースです。合格率が同じでも、合格するケースが違うことがあります。
ユーザーを守る拒否のようなケースは、必須に印をつけます。必須が一つでも失敗する変更は、合格率にかかわらず出しません。
例: v1 と v2 の比較
| ケース | v1 | v2 | 備考 |
|---|---|---|---|
| c1(必須) | 合格 | 合格 | 維持 |
| c2(必須) | 合格 | 合格 | 維持 |
| c3 | 失敗 | 合格 | 直った |
| c4 | 合格 | 合格 | 維持 |
| c5 | 合格 | 失敗 | 後退: 人が見る必要がある |
学生が入力するセルは、このプレビューでは空白です。
決まったバケットで展開する
全ユーザーを一度に切り替えません。ユーザー ID を、決まった関数で 0 から 99 のバケットにします。バケットが展開の割合より小さいユーザーに、新しいプロンプトを使います。
同じユーザーには、いつも同じバージョンが届き、割合を上げると、ユーザーが増えるだけです。リクエストごとの乱数は使いません。一人の人が両方の版を見てしまうからです。
戻る道を残しておく
旧バージョンはリポジトリに残し、割合は一つの設定に置きます。戻すには、割合を 0 にします。デプロイも、編集も、言い争いも要りません。
割合は段階的に上げ、各段階で同じ指標を見ます。実際のサンプルでの合格、費用、苦情です。
よくある間違い
注意
合格率が上がれば安全な変更だ、と考える人は多いです。入れ替わりが隠れることがあります。一つ直り、別の一つが壊れるのです。合格率だけでなく、後退の一覧と必須のケースを確かめます。
モデル用の補助を書く(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"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
プロンプトの公開を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const fs = require(\"fs\");' 'const { ask } = require(\"./model\");' '// プロンプトは、バージョンつきのファイルに置きます。変更が差分になります。' 'const file = { name: \"summary\", version: \"v2\", text: \"チケットを一文で要約してください。製品名を入れてください。\" };' 'fs.writeFileSync(\"prompt-summary.json\", JSON.stringify(file, null, 1));' '// 記録済みの評価結果: 各ケースは合格したか。レッスン用の作り物です。' 'const oldRun = { c1: true, c2: true, c3: false, c4: true, c5: true };' 'const newRun = { c1: true, c2: true, c3: true, c4: true, c5: false };' 'const MUST = [\"c1\", \"c2\"]; // 決して失敗してはいけないケース' > prompts.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
プロンプトの公開を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function rate(run) {' ' const names = Object.keys(run);' ' const passed = names.filter(function (n) { return run[n]; });' ' return Math.floor(passed.length * 100 / names.length);' '}' 'function reasons(a, b) {' ' const out = [];' ' if (rate(b) < rate(a)) out.push(\"合格率が下がる\");' ' MUST.forEach(function (n) { if (b[n] === false) out.push(\"必須が失敗: \" + n); });' ' return out;' '}' 'const slipped = Object.keys(oldRun).filter(function (n) { return oldRun[n] && newRun[n] === false; });' 'console.log(\"旧\", rate(oldRun), \"新\", rate(newRun));' 'console.log(\"出せる:\", reasons(oldRun, newRun).length === 0, \"後退:\", slipped.join(\",\"));' >> prompts.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
プロンプトの公開を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function bucket(user) {' ' let h = 0;' ' for (const c of user) h = (h * 31 + c.charCodeAt(0)) % 1000003;' ' return h % 100;' '}' 'const rollout = { prompt: \"v2\", fallback: \"v1\", percent: 20 };' 'function version(user) { return bucket(user) < rollout.percent ? rollout.prompt : rollout.fallback; }' '[\"riku\", \"taro\", \"yui\", \"haru\", \"kenta\", \"sakura\"].forEach(function (u) { console.log(u, bucket(u), version(u)); });' 'rollout.percent = 0; // 戻す方法は一つの数' 'console.log(\"戻したあと:\", version(\"riku\"), version(\"taro\"));' >> prompts.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
プロンプトの公開を書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const text = file.text + \" チケット: Orbit のスマホが月曜日から充電できません。\";' ' const sample = \"Orbit のスマホが月曜日から充電できません。\";' ' console.log(\"v2 での本物の呼び出し:\", await ask([{ role: \"user\", content: text }], sample, 60));' '}' 'main();' >> prompts.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
プロンプトを判断して展開
docker exec -w /home/node/project agent node prompts.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
記録済みのものと本物のもの
評価結果は記録済みで、作り物です。本物のチームは、自分のケースを動かし、結果をプロンプトの版の隣に置きます。バケットの関数と展開の設定は、本物の技術です。
Run は prompt-summary.json を書きます。開いて、バージョンを読んでください。
出力の一例
| 行 | 何を示すか |
|---|---|
| 旧 80 新 80 | 合格率は同じ |
| 出せる: true 後退: c5 | 規則は許すが、人は c5 を読む |
| riku 4 v2 と、ほかのユーザー | ユーザーごとに、決まったバケットと版がある |
| 戻したあと: v1 v1 | 割合 0 なら、全員が v1 になる |
学生が入力するセルは、このプレビューでは空白です。