自分の関数を使う小さなアシスタント
ツールを使うアシスタントの部品は、これでそろいました。モデルが名前で呼べるツールと、それを実行するループが必要です。さらに、ステップ上限、結果として返すエラー、危険な呼び出しを承認する人も必要です。
このレッスンでは、それらの部品を1つの小さなプログラムにまとめます。
このレッスンのゴール
メモ
自分の関数を使う小さなアシスタントを作れる。
キーワード
| ことば | いみ |
|---|---|
| アシスタント | モデルとツールを使って仕事をするプログラム |
| ループ | 答えか上限まで繰り返す手順 |
| 振り分け役 | 各依頼をその関数へ送るコード |
| 承認 | 書き込みツールの前に出る、人の「はい」 |
| 安全装置 | 呼び出しの実行を止める確認 |
学生が入力するセルは、このプレビューでは空白です。
アシスタントは5つの部品でできている
どの部品も、すでに作ったものです。ツールは、あなたの関数とその説明文です。ループは、聞いて、実行して、また聞きます。振り分け役は、各依頼をその関数へ送ります。
安全装置は、ステップ上限、エラー結果、書き込みの承認です。1つでも外すと、失敗が問題になります。
例で確認:各部品が防ぐもの
| 部品 | すること | ないと起きること |
|---|---|---|
| ステップ上限 | 3ステップで止める | 終わらないループ |
| 振り分けの表 | 不明なツールを拒否する | 依頼が別のものを実行する |
| エラー結果 | 失敗を返す | 立て直せずクラッシュする |
| 承認 | 書き込みの前に尋ねる | ミスでデータが変わる |
| ログ | 呼び出しを記録する | 何が起きたか誰も分からない |
学生が入力するセルは、このプレビューでは空白です。
モデルと承認のヘルパーを保存する
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)};const tool=(n,d,k)=>({type:'function',function:{name:n,description:d,parameters:{type:'object',properties:{[k]:{type:'string'}}}}});const mk=()=>{const rl=require('readline').createInterface({input:process.stdin});const it=rl[Symbol.asyncIterator]();return{yes:async q=>{process.stdout.write(q+'(y/n) ');const l=await it.next();const ok=l.value?.trim()=='y';console.log(ok?'y':'n');return ok},close:()=>rl.close()}};require('fs').writeFileSync('agent.cjs','exports.ask='+ask+';exports.tool='+tool+';exports.mk='+mk+';');console.log('agent.cjsを保存しました')"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
お店のツールを保存する
docker exec -w /home/node/project agent node -e "const make=()=>{const{tool}=require('./agent.cjs');const N=[];const k=(i,n,o)=>({role:'assistant',tool_calls:[{id:i,function:{name:n,arguments:JSON.stringify(o)}}]});return{N,T:[tool('get_price','商品の値段を返す。','item'),tool('add_note','メモを保存する。','text')],F:{get_price:a=>({りんご:120,なし:150})[a.item]||'エラー:見つからない',add_note:a=>N.push(a.text)},FK:[k('c1','get_price',{item:'なし'}),k('c2','add_note',{text:'なし150円'}),{role:'assistant',content:'保存しました。'}]}};require('fs').writeFileSync('tools.cjs','exports.make='+make+';');console.log('tools.cjsを保存しました')"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
呼び出しを決まった順で確かめる
依頼ごとに、同じ質問を同じ順で行います。名前は表にあるか。承認が必要か。人はyと言ったか。実行して、エラーはすべて結果にします。
質問の1つで止まった呼び出しは、次へ進みません。実行されたかどうかにかかわらず、すべての結果が、ツールメッセージとログの1行になります。
アシスタント全体を実行する
printf 'y\n' | docker exec -i -w /home/node/project agent node -e "const{ask,mk}=require('./agent.cjs');const{T,F,N,FK}=require('./tools.cjs').make();const{yes,close}=mk();let rec=0;(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=FK[s-1]}m.push(a);if(a.tool_calls==null){console.log('答え:'+a.content);break}for(const c of a.tool_calls){const n=c.function.name;let r='エラー:不明なツール';if(Object.hasOwn(F,n))r=n=='add_note'&&(await yes(n+' '+c.function.arguments+'を許可しますか?'))===false?'人が断りました':String(F[n](JSON.parse(c.function.arguments)));console.log('ステップ'+s+':'+n+' -> '+r);m.push({role:'tool',tool_call_id:c.id,content:r})}}console.log('メモ:'+N.join('、'));close()})()"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある間違い
注意
モデルがよければ、アシスタントは安全だと思っている人が多くいます。よいモデルでも間違えます。安全は、あなたのコードにあります。ツールの表、ステップ上限、承認、ログです。先にこれらを作り、モデルを信頼するのはそのあとにしましょう。
さらに:仕事を変えてみる
アシスタントの実行の質問をメモを保存してください:パンを買うに変えます。先に入れる答えを、1回目はy、2回目はnにして実行しましょう(コマンドのprintfの中の文字です)。
2つのメモの行を比べます。プログラムのどの部品が違いを生んだか、書き留めます。