ツールを使うモデルの裏にあるループ
ツール呼び出しは、会話の半分にすぎません。モデルが呼び出しを頼み、プログラムがそれを実行し、モデルは結果を見てからでないと答えられません。つまり、やりとりはループになります。
ツールを使うアシスタントは、どれもこのループを回しています。そして、必ず止まる仕組みが必要です。
このレッスンのゴール
メモ
聞く、呼ぶ、答える、また聞く、というループを実行できる。
キーワード
| ことば | いみ |
|---|---|
| ループ | 停止条件まで、プログラムが繰り返す手順 |
| メッセージ履歴 | 毎回の質問と一緒に送るメッセージのリスト |
| ツール結果 | 呼び出しの答えをモデルに届けるメッセージ |
| ステップ | 1回の質問と、その応答 |
| ステップ上限 | ループが進めるステップの最大数 |
学生が入力するセルは、このプレビューでは空白です。
呼び出しのたびにもう一度聞く
メッセージとツールをモデルに送ります。応答にtool_callsがあれば、各呼び出しを実行し、結果をメッセージに足して、もう一度聞きます。応答が言葉だけなら、それが答えで、ループは終わります。
モデルは、質問と質問の間に何も覚えていません。毎回の質問が、履歴をすべて運びます。
例で確認:ステップごとの履歴
| ステップ | 起きること | 次に送るメッセージ |
|---|---|---|
| 開始 | ユーザーがなしの値段を聞く | user |
| 1 | モデルがget_priceを頼む | user, assistant |
| 1 | プログラムが結果を足す | user, assistant, tool |
| 2 | モデルが言葉で答える | user, assistant, tool, assistant |
学生が入力するセルは、このプレビューでは空白です。
モデルを呼ぶヘルパーを保存する
docker exec -w /home/node/project agent node -e "const ask=async(m,t,c)=>{const r=await fetch((process.env.AI_URL||'http://ai:4000')+'/v1/chat/completions',{method:'POST',headers:{'content-type':'application/json',authorization:'Bearer lab'},body:JSON.stringify({model:'lab',messages:m,tools:t,tool_choice:c,max_tokens:120})});const j=await r.json();if(r.ok)return j.choices[0].message;throw Error(r.status+' '+j.error.message)};require('fs').writeFileSync('ai.cjs','exports.ask='+ask+';');console.log('ai.cjsを保存しました:ask(messages, tools)はモデルのメッセージを返します。')"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ツール結果は呼び出しのidを持つ
呼び出しのあと、toolというロールのメッセージを足します。答える相手の依頼のtool_call_idと、テキストにした結果が入っています。
モデルは、このidで、各結果をそれぞれの依頼に結び付けます。idがないと、2つの呼び出しを含む応答に、2つの結果を返せません。
最大3ステップでループを実行する
docker exec -w /home/node/project agent node -e "const{ask}=require('./ai.cjs');const P={りんご:120,なし:150};const T=[{type:'function',function:{name:'get_price',description:'商品の値段を返す。',parameters:{type:'object',properties:{item:{type:'string'}}}}}];let rec=0;const fake=m=>m.at(-1).role=='tool'?{role:'assistant',content:'なしは150円です。'}:{role:'assistant',tool_calls:[{id:'c1',function:{name:'get_price',arguments:JSON.stringify({item:'なし'})}}]};(async()=>{const m=[{role:'user',content:'なしはいくらですか。'}];for(let s=1;s<4;s++){let a;try{if(rec)throw 0;a=await ask(m,T)}catch(e){rec||console.log('モデルなし('+e.message+')。記録済みサンプル:');rec=1;a=fake(m)}m.push(a);if(a.tool_calls==null){console.log('答え:'+a.content);return}for(const c of a.tool_calls){const v=P[JSON.parse(c.function.arguments).item];console.log('ステップ'+s+':'+c.function.name+' -> '+v);m.push({role:'tool',tool_call_id:c.id,content:String(v)})}}console.log('ステップ上限に達しました')})()"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ステップ上限が暴走を止める
モデルは、呼び出しを何度も頼み続けることがあります。ステップごとに時間とクレジットを使うので、ステップを数え、上限で止めます。まずは3が目安です。
上限に先に達したときは、プログラムがそう伝えます。答えがあるふりはしません。
よくある間違い
注意
モデルは前の質問を覚えていると思っている人が多くいます。覚えていません。見えるのは、あなたが送ったメッセージだけです。ツール結果を省くと、呼び出しが何を返したか分からなくなります。メッセージは全部残しましょう。
さらに:質問を変えてみる
実行の質問をりんごとなしはいくらですか。に変えます。3回実行して、毎回ステップを数えましょう。
その後、s<4をs<2に変えます。これでループは最大1ステップしか進みません。上限に先に達したとき、プログラムが何を表示するか書きましょう。