Zenn
🔥

AIアプリ開発者が知っておくべきFirebase Cloud Functionsのstream機能

2025/03/21に公開
2

課題

Firebaseを使用してAIアプリを開発する際、Cloud Functionsを利用して、OpenAI、Claude、Gemini、DeepseekなどのAPIを呼び出すことがよくあるかと思います。

しかし、Cloud Functionsにはデータをストリーミング形式で返す機能がないという大きな問題がありました。つまり、言語モデルがデータ生成を完了するまで何も返すことができなかったということです。OpenAIをはじめとした他のサービスのAPIはデータストリーミングをサポートしているのに・・・

レスポンスが届くまで数秒間待つ必要があり、ユーザーの体験が悪くなります。
アプリが遅く感じてしまいます。

Firestore を使って解決する方法

解決策として、Cloud Function内でストリーミングAPIを呼び出し、新たなデータ断片が得られる度にFirestoreのドキュメントを更新しました。クライアント側では、onSnapshotを使用してドキュメントを監視し、自動的に更新を受け取ることができました。

ただ、この方法は以下のデメリットがあります:

  • Firestoreのドキュメントへの読み書きが増えるため、コストが上がります。
  • データは一回Firestoreに書き込まれてからフロントに通知されるのでまだ遅く感じてしまいます。

そこで Firebase Cloud Function の新機能が登場しました

Firebaseはこの問題を認識し、Callable Functionsがデータをストリーミング形式で返せる新機能を導入しました。

やり方

Cloud Function 側の実装

まず、firebase-functionsの最新バージョンを使用しているか確認してください。
次に、Callable Functionを作成する際に新たに追加された response.sendChunk(foo) メソッドを使ってみましょう。

Callable FunctionからOpenAIのAPIを使って、よくみる「AIにポエムを書いてもらう」例を紹介します。

import { onCall } from 'firebase-functions/https'
import { defineSecret } from 'firebase-functions/params'
import OpenAI from 'openai'

// あらかじめGCPのシークレットマネージャーで設定しておいてくださいね
const openAIapiKey = defineSecret('OPEN_AI_API_KEY')

type PoemRequest = { subject: string }
type PoemResponse = { poem: string }

// onCall<RequestType, ResponseType, StreamChunkType> というシグネチャになります
export const onCallWritePoem = onCall<PoemRequest, Promise<PoemResponse>, string>({
  secrets: [openAIapiKey],
}, async (request, response) => {
  const openai = new OpenAI({
    apiKey: openAIapiKey.value(),
  })

  const prompt = `${request.data.subject}についてポエムを書いてください。`

  const responseStream = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: 'あなたは優秀な詩人です。' },
      { role: 'user', content: prompt },
    ],
    stream: true, // ストリームを有効にする
  })

  let poem = ''

  for await (const chunk of responseStream) {
    const content = chunk.choices[0].delta.content ?? ''

    // 少しずつ文字列を連結してポエムを作成
    poem += content

    if (request.acceptsStreaming) {
      // クライアント側で stream が要求されている場合、少しずつデータを送信する
      response?.sendChunk(content)
    }
  }

  return { poem }
})

フロントエンド側の実装

クライアント側でストリームを受け取るための実装は下記になります。
(ここではVue.jsとVuefireを使用していますが、他のフレームワークでも同様に適用可能です)

async function generatePoem() {
  poem.value = ''

  const { stream, data } = await httpsCallable<PoemRequest, PoemResponse>(
    functions,
    'onCallWritePoem',
  ).stream({ subject: subject.value }) // ここの .stream が大事

  for await (const chunk of stream) {
    poem.value += chunk
  }

  const response = await data
  poem.value = response.poem
}

ちなみに、この関数は.stream()を使わない形式でも動きます。その場合は、sendChunk が呼ばれないだけです。

Cloud Functionをデプロイして、試してみましょう!

これで生成中でもユーザーにデータを確認することができますのでスムーズなUXになります。

以上!Happy coding

2

Discussion

ログインするとコメントできます