iT邦幫忙

2026 iThome 鐵人賽

0
Build on Google AI

2026年,如何利用 Antigravity CLI、Gemini、各項技能及 MCP Server 建構基於 Firebase 的 Angular 應用系列 第 37 篇

Day 37: 別再到處呼叫 getGenerativeModel() 了!將 Firebase AI 擴展至多模型 Angular 應用程式的隱藏陷阱

  • 分享至 

  • xImage
  •  

直接在 VisionService 與 TextToSpeechService 內部呼叫 getGenerativeModel(),會重複撰寫 firebase/ai 匯入、安全設定(Safety Settings)與思考設定(Thinking Configurations),同時在每次請求時於用戶端重新建立 GenerativeModel 物件,進而觸發不必要的垃圾回收(Garbage Collection)。在本篇部落格文章中,我們將抽離出 AiModelFactory 來集中管理並快取模型執行個體以支援多模型路由,同時解決兩個隱藏的快取陷阱:請求層級的 generationConfig 覆寫問題,以及 Remote Config 更新後導致的過期 AI 後端問題。


架構與核心概念

VisionService 與 TextToSpeechService 將模型的建立與生命週期管理委派給 AiModelFactory:

  • 依需求請求模型:VisionService 會傳入 AiModelOptions 呼叫 getModel(options),而 TextToSpeechService 則呼叫 getTtsModel()。
  • 後端驗證與快取清除(getValidBackend()):在回傳模型之前,getValidBackend() 會將 ConfigService.getAiBackend() 與 #cachedBackend 進行比對。如果 Remote Config 重新整理了 AI 後端,它會清空 #models 並將 #ttsModel 重設為 null。
  • 具索引鍵的視覺模型快取(#models):getModel(options) 會透過 generateKey() 建立決定性的快取鍵(Deterministic Key),並將執行個體快取於 #models(Map<string, GenerativeModel>)中。
  • 單例 TTS 模型快取(#ttsModel):getTtsModel() 會快取單一 #ttsModel 執行個體,並將其設定為 responseModalities: [ResponseModality.AUDIO]。
flowchart TD
    subgraph Callers["Domain Services"]
        VS["VisionService (1 model now -> 3 models with MediaPipe routing)"]
        TTS["TextToSpeechService (per-request voice speechConfig)"]
    end

    subgraph Factory["AiModelFactory (@/core/firebase)"]
        GVB["getValidBackend(): compare with #cachedBackend"]
        EVICT["Backend changed? #models.clear() & #ttsModel = null"]
        GM["getModel(options?: AiModelOptions)"]
        KEY["generateKey(): model_thinking_instruction_schema_tools"]
        MAP[("#models: Map<string, GenerativeModel>")]
        GTTS["getTtsModel()"]
        TTS_CACHE[("#ttsModel: GenerativeModel | null")]
    end

    VS -- "getModel(options)" --> GVB
    TTS -- "getTtsModel()" --> GVB
    GVB -- "New AI instance" --> EVICT --> GM & GTTS
    GVB -- "Same AI instance" --> GM & GTTS
    GM --> KEY --> MAP
    GTTS --> TTS_CACHE

1. 定義 AiModelOptions 與決定性快取鍵

1.1. AiModelOptions 介面

AiModelOptions 介面定義了建立視覺模型以及計算其快取鍵所需的設定資訊:

  • 選用的設定屬性:systemInstruction、schema(SchemaRequest | TypedSchema)與 tools 陣列(Tool[])可讓呼叫端設定結構化 JSON 輸出、純文字回應或接地(Grounding)工具。
// src/app/core/firebase/ai-model-options.interface.ts
import type { SchemaRequest, Tool, TypedSchema } from 'firebase/ai';

export interface AiModelOptions {
  systemInstruction?: string;
  schema?: SchemaRequest | TypedSchema;
  tools?: Tool[];
}

1.2. AiModelFactory 中的私有狀態與 generateKey()

  • 設定注入(#configService):AiModelFactory 將 ConfigService 注入至 #configService,以讀取 Remote Config 數值並取得 Firebase AI Logic 後端。
  • 快取的 AI 後端(#cachedBackend):#cachedBackend 儲存用於建立生成式模型的有效 AI 後端參照。
  • 單例 TTS 快取(#ttsModel):#ttsModel 保存單一執行個體的 TTS 模型,用於將英文文字合成為語音。
  • 視覺模型映射表(#models 與 generateKey):#models 是一個 Map<string, GenerativeModel>,會以 generateKey() 回傳的索引鍵來儲存視覺模型。該索引鍵會以底線(_)字元串接模型名稱(geminiModelName)、thinkingLevel、systemInstruction、結構化輸出的 schema 以及 tools。
// src/app/core/firebase/ai-model-factory.service.ts (fields & key generation)
import type { AI, GenerationConfig, GenerativeModel, ModelParams, SafetySetting } from 'firebase/ai';

@Service()
export class AiModelFactory {
  readonly #configService = inject(ConfigService);
  readonly #models = new Map<string, GenerativeModel>();
  #ttsModel: GenerativeModel | null = null;
  #cachedBackend: AI | null = null;

  private generateKey(options?: AiModelOptions): string {
    const { geminiModelName, thinkingLevel } = this.#configService.appConfig;
    const instruction = options?.systemInstruction ?? '';
    const schemaKey = options?.schema ? JSON.stringify(options.schema) : '';
    const toolsKey = options?.tools?.length ? JSON.stringify(options.tools) : '';

    return `${geminiModelName}_${thinkingLevel}_${instruction}_${schemaKey}_${toolsKey}`;
  }

2. 在 AiModelFactory 中依需求建立模型與使快取失效

2.1. 延遲載入 SDK 與組裝參數(createModel、createModelParams、createGenerationConfig)

我們很容易想直接在檔案頂端匯入 HarmBlockThreshold 與 HarmCategory 列舉(Enum),但這麼做會導致 firebase/ai 的執行階段程式碼洩漏至 main.js 中,進而增加初始打包檔案的大小:

  • 動態匯入 SDK(createModel):createModel() 會透過動態匯入從 firebase/ai 載入 getGenerativeModel、HarmBlockThreshold 與 HarmCategory。
  • 共用安全設定(createModel):createModel() 會建立 SafetySetting[] 陣列,並呼叫 getGenerativeModel(backend, modelParams)。
  • 思考設定(createGenerationConfig):createGenerationConfig() 會設定 thinkingLevel 並將 includeThoughts 設為 true。
  • 結構化輸出綱要(createGenerationConfig):當呼叫端提供 options?.schema 時,createGenerationConfig() 會附加 responseSchema 與 responseMimeType: 'application/json'。
  • 組裝模型參數(createModelParams):createModelParams() 會結合模型名稱、generationConfig 與 safetySettings,並依條件加入 tools 與 systemInstruction。
  // src/app/core/firebase/ai-model-factory.service.ts (model creation helpers)
  private async createModel(backend: AI, options?: AiModelOptions): Promise<GenerativeModel> {
    const { getGenerativeModel, HarmBlockThreshold, HarmCategory } = await import('firebase/ai');
    const safetySettings: SafetySetting[] = [
      { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
      { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
      { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
      { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
    ];

    const modelParams = this.createModelParams(safetySettings, options);
    return getGenerativeModel(backend, modelParams);
  }

  private createModelParams(safetySettings: SafetySetting[], options: AiModelOptions | undefined): ModelParams {
    const generationConfig = this.createGenerationConfig(options);

    const modelParams: ModelParams = {
      model: this.#configService.appConfig.geminiModelName,
      generationConfig,
      safetySettings,
    };

    if (options?.tools?.length) {
      modelParams.tools = options.tools;
    }

    if (options?.systemInstruction) {
      modelParams.systemInstruction = options.systemInstruction;
    }
    return modelParams;
  }

  private createGenerationConfig(options: AiModelOptions | undefined): GenerationConfig {
    const generationConfig: GenerationConfig = {
      thinkingConfig: {
        thinkingLevel: this.#configService.appConfig.thinkingLevel,
        includeThoughts: true,
      },
    };

    if (options?.schema) {
      generationConfig.responseSchema = options.schema;
      generationConfig.responseMimeType = 'application/json';
    }
    return generationConfig;
  }

2.2. 依需求重用模型(getModel 與 getTtsModel)

在 AiModelFactory 中,取得視覺模型與取得 TTS 模型遵循兩種不同的快取流程:

  • 查找視覺模型(getModel):getModel() 會透過 generateKey(options) 從 AiModelOptions 產生決定性快取鍵,並檢查 this.#models.get(key)。
  • 視覺模型快取未命中(Cache Miss):當索引鍵不存在於 #models 時,getModel() 會呼叫 createModel() 來實例化模型,並將其存入映射表中。
  • TTS 模型流程(getTtsModel):getTtsModel() 會呼叫一次 createTtsModel(),以實例化設定為 responseModalities: [ResponseModality.AUDIO] 的單例 TTS 模型,並將其指派給 #ttsModel。
  • 消除每次呼叫的記憶體配置與垃圾回收:跨請求重用快取的執行個體,可避免重複呼叫 getGenerativeModel() 以及不必要的垃圾回收。
  // src/app/core/firebase/ai-model-factory.service.ts (on-demand getters)
  async getModel(options?: AiModelOptions): Promise<GenerativeModel> {
    const backend = await this.getValidBackend();
    const key = this.generateKey(options);
    let model = this.#models.get(key);
    if (!model) {
      model = await this.createModel(backend, options);
      this.#models.set(key, model);
    }

    return model;
  }

  private async createTtsModel(ai: AI): Promise<GenerativeModel> {
    const { getGenerativeModel, ResponseModality } = await import('firebase/ai');

    return getGenerativeModel(ai, {
      model: this.#configService.appConfig.geminiTTSModelName,
      generationConfig: {
        responseModalities: [ResponseModality.AUDIO],
      },
    });
  }

  async getTtsModel(): Promise<GenerativeModel> {
    const backend = await this.getValidBackend();
    if (!this.#ttsModel) {
      this.#ttsModel = await this.createTtsModel(backend);
    }

    return this.#ttsModel;
  }

2.3. 當 Remote Config 更新後端時清除快取模型(getValidBackend)

  • Remote Config 重新整理導致後端過期:建構 GenerativeModel 需要有效的 Firebase AI Logic AI 後端;當相依的 Firebase Remote Config 參數(例如 useLimitedUseAppCheckTokens 與 vertexAILocation)收到更新值且 ConfigService 重設其 AI 執行個體時,原先的後端就會過期。
  • 清除快取模型(getValidBackend):當 getValidBackend() 偵測到 this.#configService.getAiBackend() 與 #cachedBackend 不再相符時,它會更新 #cachedBackend、清空 #models 映射表,並將 #ttsModel 重設為 null。
  // src/app/core/firebase/ai-model-factory.service.ts (cache invalidation)
  private async getValidBackend(): Promise<AI> {
    const backend = await this.#configService.getAiBackend();
    if (backend !== this.#cachedBackend) {
      this.#cachedBackend = backend;
      this.#models.clear();
      this.#ttsModel = null;
    }

    return this.#cachedBackend;
  }
}

3. 重構 VisionService 與 TextToSpeechService(以及淺層展開陷阱)

3.1. 簡化 VisionService 以支援未來的多模型路由

我們透過將模型建立邏輯委派給 AiModelFactory,重構了 VisionService 以提升其擴充性與擴展性:

  • 移除 SDK 設定樣板程式碼:引入 AiModelFactory 後,即可從 VisionService 移除 SAFETY_SETTINGS、ConfigService 與私有的 getGenerativeAIModel() 輔助方法。
  • 抽離 altTextPrompt:將 altTextPrompt 移至 vision.prompt.ts,可讓 VisionService 專注於協調影像分析流程——產生標籤、撰寫無障礙替代文字(Alt Text),以及發掘冷知識。
  • 搭配自訂工具依需求取得模型:VisionService 將 AiModelFactory 注入至 #aiModelFactory,並呼叫 this.#aiModelFactory.getModel({ schema: ImageAnalysisSchema, tools: [{ googleSearch: {} }] })。雖然目前的模型使用 Google Search 接地工具,但 VisionService 也可以透過 AiModelOptions 傳入 Google Maps 工具或自訂函式呼叫(Function Calling)工具來設定其他模型。
  • 為 MediaPipe 多模型路由做好準備:由於 getModel() 會依設定鍵快取執行個體,我們可以透過裝置端(On-Device)的 MediaPipe 分類器擴充 VisionService,依需求請求 3 個專用模型(地點、人物與臉孔,以及一般物件)。
// src/app/core/vision/vision.service.ts (refactored excerpt)
@Service()
export class VisionService {
  readonly #window = inject(WINDOW);
  readonly #aiModelFactory = inject(AiModelFactory);

  async analyzeImage(image: File): Promise<ImageAnalysisResponse> {
    const { data, mimeType, optimizationMetrics } = await preprocessImageForVision(image, this.#window);
    const imagePart: InlineDataPart = {
      type: 'inlineData',
      inlineData: { data, mimeType },
    };

    const aiModel = await this.#aiModelFactory.getModel({
      schema: ImageAnalysisSchema,
      tools: [{ googleSearch: {} }],
    });
    const result = await aiModel.generateContent([altTextPrompt, imagePart]);
    // ... parse response, tokenUsage, and grounding citations
  }
}

3.2. 在 TextToSpeechService 中重用 #ttsModel 而不覆蓋 responseModalities

同樣地,TextToSpeechService 將 TTS 模型的建立委派給 AiModelFactory,並在每次請求時傳入所選的語音:

  • 查找共用 TTS 模型:TextToSpeechService 將 AiModelFactory 注入至 #aiModelFactory,並由 createAIModelGenerationRequest() 透過 this.#aiModelFactory.getTtsModel() 取得快取的 TTS 模型,而不再於每次合成呼叫時實例化新模型。
  • 組裝每次請求的 speechConfig:createAIModelGenerationRequest(text, voiceName) 輔助方法會建立 GenerateContentRequest(由 synthesizeStream() 與 synthesize() 共用),並以請求的 voiceName 與 languageCode: 'en-US' 設定 speechConfig。
  • 防止淺層覆寫以保留 ResponseModality.AUDIO:在 @firebase/ai 中,傳入請求層級的 generationConfig 會覆寫模型預設的 aiModel.generationConfig。將 ...aiModel.generationConfig 展開(Spread)至請求的 generationConfig 中,即可在加入 speechConfig 的同時保留 responseModalities: [ResponseModality.AUDIO]。
// src/app/core/speech/text-to-speech.service.ts (refactored excerpt)
@Service()
export class TextToSpeechService {
  readonly #aiModelFactory = inject(AiModelFactory);

  private async createAIModelGenerationRequest(text: string, voiceName: string): Promise<AIModelGenerativeRequest> {
    const aiModel = await this.#aiModelFactory.getTtsModel();
    const contents: Content[] = [
      {
        role: 'user',
        parts: [{ type: 'text', text }],
      },
    ];
    const generationConfig: GenerationConfig = {
      ...aiModel.generationConfig,
      speechConfig: {
        voiceConfig: {
          prebuiltVoiceConfig: { voiceName },
        },
        languageCode: 'en-US',
      },
    };

    return {
      aiModel,
      generationRequest: { contents, generationConfig },
    };
  }

  async *synthesizeStream({ text, voice }: SpeechPrompt): AsyncGenerator<AudioStreamChunk> {
    const { aiModel, generationRequest } = await this.createAIModelGenerationRequest(text, voice);
    const responseStream = await aiModel.generateContentStream(generationRequest);

    for await (const chunk of responseStream.stream) {
      const chunkData = this.extractValidChunkData(chunk);
      if (chunkData) {
        yield decodeAudioChunk(chunkData.data, chunkData.mimeType);
      }
    }
  }

  async synthesize({ text, voice }: SpeechPrompt): Promise<Blob> {
    const { aiModel, generationRequest } = await this.createAIModelGenerationRequest(text, voice);
    const result = await aiModel.generateContent(generationRequest);

    const chunk = this.extractValidChunkData(result.response);
    if (!chunk) {
      throw new Error('No audio data received in response.');
    }
    return toWavBlob(chunk.data, chunk.mimeType);
  }
}

重點回顧

  • 集中化延遲建立模型:AiModelFactory 會動態匯入 firebase/ai 來建構文字轉語音(TTS)與影像分析模型。它集中管理影像分析模型的安全設定與 thinkingLevel,同時將 TTS 模型的 responseModalities 鎖定為 [ResponseModality.AUDIO]。
  • 重用執行個體以取代每次請求配置:AiModelFactory 會將模型快取於 #models 映射表或 #ttsModel 欄位中並跨請求重用,消除每次請求不必要的 GenerativeModel 實例化與垃圾回收。
  • 讓 VisionService 具備可擴展的多模型路由:#models 映射表中的鍵值快取機制讓 VisionService 具備擴展性,使未來整合的 MediaPipe 分類器能將影像路由至專用模型(地點、人物與臉孔,以及一般物件)進行分析。
  • 在每次請求切換語音時保留 ResponseModality.AUDIO:由於文字轉語音合成會在每次請求時接收語音參數,將 ...aiModel.generationConfig 展開至請求的 generationConfig 中,即可在傳入所選 voiceName 的同時保留 ResponseModality.AUDIO。
  • 在 Remote Config 更新時自動使快取失效:當 ConfigService 在背景擷取 Remote Config 數值並使快取的 AI 後端失效時,AiModelFactory 會清除所有快取模型,並在下一次呼叫時重新建立新的執行個體。

參考資源


上一篇
Day 36: 使用 Email/Password 身分驗證保護 Angular Firebase AI 應用程式(第 3 部分:輕量級首頁畫面、延遲載入 CDK Dialog 與頁首導覽)
系列文
2026年,如何利用 Antigravity CLI、Gemini、各項技能及 MCP Server 建構基於 Firebase 的 Angular 應用 共 37 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言