遅い呼び出しで固まらない
モデルへの呼び出しは、数秒かかることがあります。待っている間にプログラムが固まっては困ります。非同期のコードなら、止まらずに待てます。
ストリームは、さらに一歩進んだ方法です。答えが少しずつ届くので、利用者はすぐに読み始められます。
このレッスンのゴール
メモ
遅い呼び出しをなぜ await するのか、ストリームの答えがどう届くのかを説明できる。
キーワード
| ことば | いみ |
|---|---|
| プロミス | まだ届いていない結果の代わりになるもの |
| await | プロミスの結果が出るまで、関数を一時停止すること |
| async関数 | await を使ってよい関数 |
| ストリーム | 作られるそばから少しずつ送られる答え |
| チャンク | ストリームの答えの1つの断片 |
学生が入力するセルは、このプレビューでは空白です。
遅い呼び出しはプロミスを返す
遅い仕事を始めると、呼び出しはすぐにプロミスを返して戻ります。結果はまだありません。プログラムは次の行へ進みます。
結果が届くと、プロミスがそれを渡します。順序に注意しましょう。呼び出しの後ろの行は、結果より先に実行されることがあります。
起きる順番を見る
docker exec -w /home/node/project agent node -e "const wait=(ms,t)=>new Promise(r=>setTimeout(()=>r(t),ms));console.log('1 開始');wait(300,'3 遅い呼び出しが終わった').then(console.log);console.log('2 プログラムは先へ進む')"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
awaitは関数を止める
async関数の中では、await が、プロミスの結果が出るまでその関数を一時停止します。プログラムのほかの部分は止まりません。
互いに関係のない遅い呼び出し2つは、Promise.all で同時に始められます。かかる時間は、遅いほうと同じくらいで、足し算にはなりません。
順に待つ場合と同時に待つ場合
docker exec -w /home/node/project agent node -e "const wait=(ms,t)=>new Promise(r=>setTimeout(()=>r(t),ms));const tick=(t0)=>Math.round((Date.now()-t0)/100)*100;(async()=>{let t0=Date.now();await wait(200,'a');await wait(200,'b');console.log('順番に待つ:約'+tick(t0)+' ms');t0=Date.now();await Promise.all([wait(200,'a'),wait(200,'b')]);console.log('同時に待つ:約'+tick(t0)+' ms')})()"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ストリームの答えはチャンクで届く
stream: true を指定すると、サービスは答えを作るそばから送ります。多くのモデルAPIはサーバー送信イベントを使います。data: で始まる行が並び、それぞれに小さなテキストが入っています。
最後の行は data: [DONE] です。断片を順につなげると、答え全体になります。
例:ストリームを1行ずつ見る
| 受け取った行 | 得られるもの |
|---|---|
| data: {"choices":[{"delta":{"content":"こん"}}]} | こん |
| data: {"choices":[{"delta":{"content":"にちは"}}]} | にちは |
| data: [DONE] | 終わり。テキストなし |
| 結合 | こんにちは |
学生が入力するセルは、このプレビューでは空白です。
ストリームの答えを読む
docker exec -w /home/node/project agent node -e "const u=(process.env.AI_URL||'http://ai:4000')+'/v1/chat/completions';(async()=>{try{const r=await fetch(u,{method:'POST',headers:{'content-type':'application/json',authorization:'Bearer lab'},body:JSON.stringify({model:'lab',messages:[{role:'user',content:'一から五まで、漢数字で数えてください。'}],max_tokens:40,stream:true})});if(r.ok===false)throw new Error(r.status);const d=new TextDecoder();let b='',n=0,t='';for await(const p of r.body){b+=d.decode(p,{stream:true});const L=b.split(String.fromCharCode(10));b=L.pop();for(const l of L){const x=l.slice(6);if(l.startsWith('data: ')&&(x==='[DONE]')===false){const c=JSON.parse(x).choices?.[0]?.delta?.content;if(c){n++;t+=c}}}}console.log('テキストを持つチャンク:'+n);console.log('結合したテキスト:'+t)}catch(e){console.log('ストリームなし('+e.message+')。記録済みサンプル:');console.log('テキストを持つチャンク:5');console.log('結合したテキスト:一、二、三、四、五')}})()"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある間違い
注意
ストリームにするとモデルが速くなる、と考える人が多くいます。答え全体にかかる時間は、ほぼ同じです。最初の言葉が早く見えるので、速く感じるのです。ストリームは途中で切れることもあるので、終わりの印が届いたかを確かめましょう。